通过 SWIG 在 python 中使用 std::ifstream、std::ofstream 的技术?

Posted

技术标签:

【中文标题】通过 SWIG 在 python 中使用 std::ifstream、std::ofstream 的技术?【英文标题】:Technique for using std::ifstream, std::ofstream in python via SWIG? 【发布时间】:2013-09-17 22:18:12 【问题描述】:

有没有办法通过 swig 在 python 中使用std::[io]fstream's?

我有一个具有以下功能的 c 类:

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

我想在 python 中构造一个std::ofstream 实例并将其作为 对writeTo 的参数(并为阅读做同样的事情)。

我试着做一个类似的函数

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

在 swig .i 文件中,这样这个函数就会成为界面的一部分。但是,这不起作用。由于流类不可复制,因此存在问题。

虽然std_iostream.i 似乎有助于使用通用的[io]stream 类, 它对制作我需要的文件流没有帮助。

【问题讨论】:

【参考方案1】:

我对这个问题的首选解决方案是让 Python 开发人员看到的界面尽可能“Pythonic”。在这种情况下,将接受 python file 对象作为您的 ostreamistream 参数。

为了实现这一点,我们必须编写一个类型映射来设置每个映射。

我编写了以下头文件来演示这一点:

#ifndef TEST_HH
#define TEST_HH
#include <iosfwd>

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

我为测试编写了一个虚拟实现:

#include <iostream>
#include <cassert>
#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());

有了它,我就可以使用以下方法成功包装它:

%module test

%
#include <stdio.h>
#include <boost/iostreams/stream.hpp>
#include <boost/iostreams/device/file_descriptor.hpp>
namespace io = boost::iostreams;
typedef io::stream_buffer<io::file_descriptor_sink> boost_ofdstream;
typedef io::stream_buffer<io::file_descriptor_source> boost_ifdstream;
%

%typemap(in) std::ostream& (boost_ofdstream *stream=NULL) 
  int fd = -1;

  #if PY_VERSION_HEX >= 0x03000000
  fd = PyObject_AsFileDescriptor($input);
  #else 
  FILE *f=PyFile_AsFile($input); // Verify the semantics of this
  if (f) fd = fileno(f);
  #endif
  if (fd < 0) 
    SWIG_Error(SWIG_TypeError, "File object expected.");
    SWIG_fail;
  
  else 
    // If threaded incrment the use count
    stream = new boost_ofdstream(fd, io::never_close_handle);
    $1 = new std::ostream(stream);
  


%typemap(in) std::istream& (boost_ifdstream *stream=NULL) 
  int fd = -1;

  #if PY_VERSION_HEX >= 0x03000000
  fd = PyObject_AsFileDescriptor($input);
  #else 
  FILE *f=PyFile_AsFile($input); // Verify the semantics of this
  if (f) fd = fileno(f);
  #endif
  if (fd < 0) 
    SWIG_Error(SWIG_TypeError, "File object expected.");  
    SWIG_fail;
  
  else 
    stream = new boost_ifdstream(fd, 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"

它的核心部分基本上是调用PyFile_AsFile() 从Python 的file 对象中获取FILE*。有了它,我们就可以构建一个 boost 对象,它使用文件描述符作为适当的源/接收器。

剩下的唯一事情就是在调用发生后清理我们创建的对象(或者如果错误阻止调用发生)。

有了它,我们就可以在 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()

请注意,缓冲语义可能不会产生您预期的结果,例如在 out.txt 我得到:

你好 蟒蛇

这是调用的相反顺序。我们也可以通过在构造 C++ 流之前强制调用我们类型映射中的 Python file 对象上的 file.flush() 来解决这个问题:

%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);
  

哪个具有所需的行为。

其他说明:

    如果您有多线程代码并且在没有 GIL 的情况下进行 C++ 调用,您需要分别在 in 和 freearg 类型映射中调用 PyFile_IncUseCountPyFile_DecUseCount 以确保在您执行时没有任何东西可以关闭文件'仍在使用它。 我假设 PyFile_AsFile 返回 NULL 如果它给出的对象不是 file - 文档似乎没有指定任何一种方式,所以你可以使用 PyFile_Check 确定. 如果您想要超级灵活,您可以接受来自 Python 的字符串,并根据需要使用 PyString_Check/PyFile_Check 构造一个 std::ifstream 来决定在类型映射中采取哪些操作。 一些 C++ 标准库提供了一个 ifstream/ofstream 构造函数,它采用 FILE* 作为扩展。如果您有其中之一,则可以使用它而不是依赖 boost。

【讨论】:

不幸的是,PyFile_AsFile 在 python 3 中不再可用 可能最简单的解决方案是使用 boost iostreams 编写一个 ifstream/ofstream 来代理 Python 的所有内容 PyFile_AsFile 不再可用,但我相信PyObject_AsFileDescriptor 具有相同的功能。它为您提供文件描述符而不是文件指针,因此您必须在其上使用fdopen @HenriMenke 实际上,对于这种情况,这会更简单,因为无论如何我只是在FILE* 上调用fileno。您可能想在此处添加答案:***.com/q/16130268/168175 我根据@HenriMenke 的建议为 Python 3 添加了(新的、未经测试的)代码。【参考方案2】:

我不知道 swig,但假设您需要创建一个可复制的对象,您可能会使用类似的函数来逃避

std::shared_ptr<std::ostream> make_ostream(std::string const& filename) 
    return std::make_shared<std::ofstream>(filename);

...然后使用转发函数调用你真正要调用的函数:

void writeTo(std::shared_ptr<std::ostream> stream) 
    if (stream) 
        writeTo(*stream);
    

(如果重载名称会导致问题,当然可以以不同的方式调用转发函数)。

【讨论】:

这个想法非常有效,但我收到内存泄漏警告,因为接口不知道如何释放(指向)shared_ptr 的(指针)。 Answer here 解决内存泄漏问题。【参考方案3】:

我最终只是编写了自己的代理类以在接口中使用。 所以我用 SWIG 来包装这个类:

 /**
 * 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;
;

在 python 调用中进行调用

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

【讨论】:

【参考方案4】:

我对@Flexo 提供的解决方案做了一些调整

主要的变化是使用了一个唯一指针拥有的 boost::iostream::stream

其他一些区别:

使用片段满足 iostream 标头要求 删除了“io”命名空间别名以避免与已使用该名称的代码冲突 利用 fileno() 为 NULL 或无效 FILE* 参数返回 -1 使用 "%#include" "%#else" "%#endif" 以便指令位于 wrap 文件中,并且 python2 和 python3 可以使用相同的 wrap 文件 包括他关于刷新输出流的建议,因为没有它就违反了最小意外原则 对 python 输出流 flush() 方法调用的结果调用 Py_DECREF - 此结果为 None,因此如果 None 的引用计数增加,世界不会着火,但它更干净,并防止引用计数溢出
%fragment("iostream_header", "header") %
#include <stdio.h>
#include <memory.h>
#include <boost/iostreams/stream.hpp>
#include <boost/iostreams/device/file_descriptor.hpp>
using boost_ofd_stream = boost::iostreams::stream<boost::iostreams::file_descriptor_sink>;
using boost_ifd_stream = boost::iostreams::stream<boost::iostreams::file_descriptor_source>;
%  

%typemap(in, fragment="iostream_header") std::ostream& (std::unique_ptr<boost_ofd_stream> stream) 
    PyObject *flush_result = PyObject_CallMethod($input, const_cast<char*>("flush"), nullptr);
    if (flush_result) Py_DECREF(flush_result);
%#if PY_VERSION_HEX < 0x03000000
    int fd = fileno(PyFile_AsFile($input));
%#else
    int fd = PyObject_AsFileDescriptor($input);
%#endif
    if (fd < 0)  SWIG_Error(SWIG_TypeError, "File object expected."); SWIG_fail; 
    stream = std::make_unique<boost_ofd_stream>(fd, boost::iostreams::never_close_handle);
    $1 = stream.get();
   

%typemap(in, fragment="iostream_header") std::istream& (std::unique_ptr<boost_ifd_stream> stream) 
%#if PY_VERSION_HEX < 0x03000000
    int fd = fileno(PyFile_AsFile($input));
%#else
    int fd = PyObject_AsFileDescriptor($input);
%#endif
    if (fd < 0)  SWIG_Error(SWIG_TypeError, "File object expected.");  SWIG_fail; 
    stream = std::make_unique<boost_ifd_stream>(fd, boost::iostreams::never_close_handle);
    $1 = stream.get();
   

【讨论】:

【参考方案5】:

根据 Dietmar 建议使用共享指针的工作 .i 文件:

%module ptrtest

%include "boost_shared_ptr.i"
%include "std_string.i"

%shared_ptr( std::ostream )

%
#include <iostream>
#include <fstream>
#include <boost/shared_ptr.hpp>

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"<<std::endl;


%

namespace std
    class ostream
    public:
        // I think we could expose things like write,put here
        // and actually make this class useful from python
    protected:
        // Need to declare this as protected otherwise swig tries 
        // to make/use a public default constructor.
        ostream();
    ;



// Tell swig to put these into the interface
typedef boost::shared_ptr< std::ostream > 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!

【讨论】:

以上是关于通过 SWIG 在 python 中使用 std::ifstream、std::ofstream 的技术?的主要内容,如果未能解决你的问题,请参考以下文章

使用 SWIG 在 Python 中公开 std::list 成员

包装 std::vector 的 std::vectors,C++ SWIG Python

Swig:在 Python 中使用 c++ STL 复杂

在 Python 中过早销毁的 SWIG 数组/类

如何在 swig & python 中为没有默认构造函数的 std::pair<> 创建接口?

SWIG:如何包装 std::string&(通过引用传递的 std::string)