将FILE *传递给Python / ctypes中的函数

我有一个库函数(用C编写),它通过将输出写入FILE *来生成文本。 我想在Python(2.7.x)中使用创建临时文件或管道的代码将其包装,将其传递给函数,从文件中读取结果,并将其作为Python字符串返回。

这是一个简单的例子来说明我的目标:

 /* Library function */ void write_numbers(FILE * f, int arg1, int arg2) { fprintf(f, "%d %d\n", arg1, arg2); } 

Python包装器:

 from ctypes import * mylib = CDLL('mylib.so') def write_numbers( a, b ): rd, wr = os.pipe() write_fp = MAGIC_HERE(wr) mylib.write_numbers(write_fp, a, b) os.close(wr) read_file = os.fdopen(rd) res = read_file.read() read_file.close() return res #Should result in '1 2\n' being printed. print write_numbers(1,2) 

我想知道我最好的选择是MAGIC_HERE()

我很想使用ctypes并创建一个返回Python c_void_t的libc.fdopen()包装器,然后将其传递给库函数。 我觉得这在理论上应该是安全的 – 只是想知道这种方法是否存在问题,或者现有的Python主题是否能解决这个问题。

此外,这将进入一个长期运行的过程(让我们假设“永远”),所以任何泄露的文件描述符将是有问题的。

首先,请注意FILE*是特定于stdio的实体。 它在系统级别不存在。 系统级中存在的东西是UNIX中的描述符(使用file.fileno()检索)( os.pipe()已经返回普通描述符)并在Windows中处理(使用msvcrt.get_osfhandle()检索)。 因此,如果可以有多个C运行时,那么它作为库间交换格式是一个糟糕的选择。 如果您的库是针对另一个C运行库而不是您的Python副本编译的,那么您将遇到麻烦:1)结构的二进制布局可能不同(例如,由于对齐或用于调试目的的其他成员或甚至不同的类型大小); 2)在Windows中,结构链接到的文件描述符也是特定于C的实体,它们的表由内部的C运行时维护1

此外,在Python 3中,对I / O进行了大修,以便从stdio解开它。 因此, FILE*与Python风格不同(可能也是大多数非C风味)。

现在,你需要的是

  • 以某种方式猜测你需要哪个C运行时,以及
  • 调用它的fdopen() (或等价物)。

(毕竟,Python的一个座右铭 “让正确的事情变得容易而且错误的事情”


最干净的方法是使用库链接到的精确实例(请祈祷它与动态链接或不会导出符号)

对于第一项,我找不到任何可以分析加载的动态模块的元数据的Python模块,以找出它已链接到的哪些DLL(只是名称甚至名称+版本是不够的,你知道,由于系统上可能有多个库实例)。 虽然它的格式信息可以广泛使用,但它绝对是可能的。

对于第二项,它是一个简单的ctypes.cdll('path').fdopen_fdopen for MSVCRT)。


其次,您可以执行一个小帮助程序模块,该模块将针对与库相同(或保证兼容)的运行时进行编译,并将为您执行上述描述符/句柄的转换。 这实际上是正确编辑库的一种解决方法。


最后,通过ctypes.pythonapi提供的Python C API,使用Python的C运行时实例(所有上述警告全部适用)是最简单(也是最脏的)方法。 它利用了

  • 事实上,Python 2的类文件对象是stdioFILE*包装器(Python 3不是)
  • PyFile_AsFile API返回包装的FILE* (注意它在Python 3中缺失 )
    • 对于独立的fd ,你需要首先构造一个类文件对象(这样就会有一个FILE*返回;))
  • 事物的id()是它的内存地址(CPython特定的) 2

     >>> open("test.txt")  >>> f=_ >>> f.fileno() 3 >>> ctypes.pythonapi  >>> api=_ >>> api.PyFile_AsFile <_funcptr object at 0x018557B0> >>> api.PyFile_AsFile.restype=ctypes.c_void_p #as per ctypes docs, # pythonapi assumes all fns # to return int by default >>> api.PyFile_AsFile.argtypes=(ctypes.c_void_p,) # as of 2.7.10, long integers are #silently truncated to ints, see http://bugs.python.org/issue24747 >>> api.PyFile_AsFile(id(f)) 2019259400 

请记住,使用fd和C指针,您需要手动确保正确的对象生命周期!

  • os.fdopen()返回的类文件对象会关闭.close()上的描述符
    • 如果在关闭/垃圾收集文件对象后需要它们,则使用os.dup()重复描述符
  • 在使用C结构时,使用PyFile_IncUseCount() / PyFile_DecUseCount()调整相应对象的引用计数。
  • 确保描述符/文件对象上没有其他I / O,因为它会搞砸数据(例如,自从for l in f调用iter(f) / for l in f ,内部缓存完全独立于stdio的缓存)