¿Técnica para usar std :: ifstream, std :: ofstream en python a través de SWIG?

¿Hay alguna forma de usar std::[io]fstream en python a través de un swig?

Tengo una clase C con funciones como:

 void readFrom(std::istream& istr); void writeTo(std::ostream& ostr); 

Me gustaría construir, en python, una instancia std::ofstream y pasarla como argumento para writeTo (y hacer lo mismo para leer).

Traté de hacer una función como

 std::ostream& make_ostream(const std::string& file_name){ return std::ofstream( file_name.c_str() ); } 

dentro del archivo swig .i , de modo que esta función formaría parte de la interfaz. Sin embargo, esto no funciona. Hay un problema ya que las clases de flujo no son copiables.

Aunque std_iostream.i parece ayudar con el uso de las clases de [io]stream genéricas [io]stream , no ayuda a hacer los flujos de archivos que necesito.

No sé swig pero asumiendo que necesita crear un objeto que se pueda copiar, puede salir con una función como

 std::shared_ptr make_ostream(std::string const& filename) { return std::make_shared(filename); } 

… y luego use una función de reenvío para llamar a la función a la que realmente quiere llamar:

 void writeTo(std::shared_ptr stream) { if (stream) { writeTo(*stream); } } 

(si sobrecargar los nombres causa problemas, podría llamar a la función de reenvío de manera diferente, por supuesto).

Mi solución preferida para este problema sería hacer que la interfaz esté expuesta a los desarrolladores de Python como “Pythonic” como sea posible. En este caso, sería aceptar file objetos de file Python como tus argumentos istream y istream .

Para lograrlo, tenemos que escribir un mapa de tipo para configurar cada mapeo.

He escrito el siguiente archivo de encabezado para demostrar esto en acción:

 #ifndef TEST_HH #define TEST_HH #include  void readFrom(std::istream& istr); void writeTo(std::ostream& ostr); #endif 

Que escribí una implementación ficticia para la prueba como:

 #include  #include  #include "test.hh" void readFrom(std::istream& istr) { assert(istr.good()); std::cout << istr.rdbuf() << "\n"; } void writeTo(std::ostream& ostr) { assert(ostr.good()); ostr << "Hello" << std::endl; assert(ostr.good()); } 

Con eso en su lugar pude envolverlo exitosamente usando:

 %module test %{ #include  #include  #include  namespace io = boost::iostreams; typedef io::stream_buffer boost_ofdstream; typedef io::stream_buffer boost_ifdstream; %} %typemap(in) std::ostream& (boost_ofdstream *stream=NULL) { FILE *f=PyFile_AsFile($input); // Verify the semantics of this if (!f) { SWIG_Error(SWIG_TypeError, "File object expected."); SWIG_fail; } else { // If threaded incrment the use count stream = new boost_ofdstream(fileno(f), io::never_close_handle); $1 = new std::ostream(stream); } } %typemap(in) std::istream& (boost_ifdstream *stream=NULL) { FILE *f=PyFile_AsFile($input); // Verify that this returns NULL for non-files if (!f) { SWIG_Error(SWIG_TypeError, "File object expected."); SWIG_fail; } else { stream = new boost_ifdstream(fileno(f), io::never_close_handle); $1 = new std::istream(stream); } } %typemap(freearg) std::ostream& { delete $1; delete stream$argnum; } %typemap(freearg) std::istream& { delete $1; delete stream$argnum; } %{ #include "test.hh" %} %include "test.hh" 

El bit central de esto básicamente es llamar a PyFile_AsFile() para obtener un FILE* del objeto de file Python. Con eso podemos construir un objeto de impulso que use un descriptor de archivo como fuente / receptor, según corresponda.

Lo único que queda es limpiar los objetos que creamos después de que haya ocurrido la llamada (o si un error impidió que la llamada).

Con eso en su lugar, podemos usarlo como se espera desde Python:

 import test outf=open("out.txt", "w") inf=open("in.txt", "r") outf.write("Python\n"); test.writeTo(outf) test.readFrom(inf) outf.close() inf.close() 

Tenga en cuenta que la semántica de almacenamiento en búfer puede no producir los resultados que esperaba, por ejemplo, en out.txt obtengo:

Hola
Pitón

Que es el orden opuesto de las llamadas. Podemos arreglar eso también forzando una llamada a file.flush() en el objeto de file Python en nuestro mapa de tipo, antes de construir una secuencia de C ++:

 %typemap(in) std::ostream& (boost_ofdstream *stream=NULL) { PyObject_CallMethod($input, "flush", NULL); FILE *f=PyFile_AsFile($input); // Verify the semantics of this if (!f) { SWIG_Error(SWIG_TypeError, "File object expected."); SWIG_fail; } else { // If threaded incrment the use count stream = new boost_ofdstream(fileno(f), io::never_close_handle); $1 = new std::ostream(stream); } } 

Que tiene el comportamiento deseado.

Otras notas:

  1. Si tienes código multihilo y las llamadas a C ++ se están realizando sin la GIL, deberás llamar a PyFile_IncUseCount y PyFile_DecUseCount en los mapas tipográficos de in y freearg, respectivamente, para asegurarte de que nada pueda cerrar el archivo mientras lo estés usando.
  2. He asumido que PyFile_AsFile devuelve NULL si el objeto que se le entrega no es un file ; la documentación no parece especificar de ninguna manera, por lo que podría usar PyFile_Check para estar seguro.
  3. Si quisiera ser súper flexible, podría aceptar cadenas de Python y construir un std::ifstream según corresponda, usando PyString_Check / PyFile_Check para decidir qué acción realizar en el mapa de tipos.
  4. Algunas bibliotecas estándar de C ++ proporcionan un constructor ifstream / ofstream que toma FILE* , como una extensión. Si tienes uno de esos, puedes usarlo en lugar de confiar en boost.

Terminé simplemente escribiendo mi propia clase de proxy para usar dentro de la interfaz. Así que utilicé SWIG para envolver esta clase:

  /** * Simple class to expose std::streams in the python * interface. works around some issues with trying to directy * the file stream objects */ class ifstream_proxy: boost::noncopyable{ public: ifstream_proxy(): m_istr(){ // no op } virtual ~ifstream_proxy(){ // no op } void open(const std::string& fname ){ m_istr.close(); m_istr.open( fname.c_str(), std::ifstream::in|std::ifstream::binary) ; } std::istream& stream(){ return m_istr; } // TBD: do I want to add additional stream manipulation functions? private: std::ifstream m_istr; }; 

y en python call realiza las llamadas.

 >>> proxy=ifstream_proxy() >>> proxy.open('file_to_read_from.txt') >>> readFrom( stream_proxy.stream() ) 

Trabajando un archivo .i basado en la sugerencia de Dietmar para usar punteros compartidos:

 %module ptrtest %include "boost_shared_ptr.i" %include "std_string.i" %shared_ptr( std::ostream ) %{ #include  #include  #include  typedef boost::shared_ptr< std::ostream > ostream_ptr; ostream_ptr mk_out(const std::string& fname ){ return ostream_ptr( new std::ofstream( fname.c_str() ) ); } void writeTo(std::ostream& ostr){ ostr<<"OK"< ostream_ptr; ostream_ptr mk_out(const std::string& fname ); void writeTo(std::ostream& ostr); // Usage: //>>>ostr=mk_out('/path/to/file.txt') //>>>writeTo(ostr) # no need to cast/call-function!