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

Bri*_*and 10 c 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);
}
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

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

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

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

iva*_*eev 5

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

此外,在 Python 3 中,对 I/O 进行了彻底修改,以便将其从stdio. 因此,FILE*它与 Python 风格格格不入(很可能也与大多数非 C 风格格格不入)。

现在,你需要的是

  • 以某种方式猜测您需要哪个 C 运行时,并且
  • 调用它的fdopen()(或等效的)。

(毕竟,Python 的座右铭之一是“让正确的事情变得简单,让错误的事情变得困难”)


最干净的方法是使用库链接到的精确实例(请祈祷它是动态链接的,否则将没有导出的符号可供调用)

对于第一项,我找不到任何可以分析加载的动态模块元数据以找出它已链接的 DLL/so 的 Python 模块(仅名称甚至名称+版本是不够的,你知道,由于系统上可能存在该库的多个实例)。尽管这绝对是可能的,因为有关其格式的信息是广泛可用的。

对于第二项,这是一个微不足道的事情ctypes.cdll('path').fdopen_fdopen对于 MSVCRT)。


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


最后,有一个最简单(也是最肮脏)的方法,通过通过ctypes.pythonapi. 它利用了

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

    >>> open("test.txt")
    <open file 'test.txt', mode 'r' at 0x017F8F40>
    >>> f=_
    >>> f.fileno()
    3
    >>> ctypes.pythonapi
    <PyDLL 'python dll', handle 1e000000 at 12808b0>
    >>> 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
    
    Run Code Online (Sandbox Code Playgroud)

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

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