将 FILE * 从 Python / ctypes 传递给函数

Posted

技术标签:

【中文标题】将 FILE * 从 Python / ctypes 传递给函数【英文标题】:Pass FILE * into function from Python / ctypes 【发布时间】:2015-10-23 20:13:40 【问题描述】:

我有一个库函数(用 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 主义是否存在问题来解决这个问题。

此外,这将在一个长时间运行的过程中进行(让我们假设“永远”),因此任何泄露的文件描述符都会有问题。

【问题讨论】:

os.popen() 不正确。它至少需要一个参数,即调用和获取管道的命令行。此外,正如the docs 所说,它已被subprocess 弃用。 对不起,我的意思是os.pipe()。已更新。 除非你也打算在 Windows 上运行它,它存在 C 运行时库可能不匹配的问题,那么我认为你调用 libc.fdopen 并传递结果 FILE 指针。但不是使用c_void_p,而是创建一个不透明的class FILE(Structure): pass 并设置libc.fdopen.restype = POINTER(FILE)。这不会转换为整数结果。 OTOH,c_void_p,因为 restype 被转换为整数,所以你必须确保 mylib.write_numbers.argtypes 也设置为防止截断 64 位指针值。 你考虑过使用fmemopen吗?如果单个 write_numbers 调用将写入的数据量限制在一个相当小的固定常数范围内,那么它可以提供一个很好的替代使用管道的方法。 @BrianMcFarland 您不必(而且我不确定您是否可以)重新读取FILE *。但您可以简单地读取您传递给@的char[]数组987654344@. 【参考方案1】:

首先,请注意 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/so 链接(仅名称甚至名称+版本是不够的,您知道,由于系统上可能存在多个库实例)。虽然这绝对是可能的,因为关于它的格式的信息是广泛可用的。

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


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


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

事实上,Python 2 的类文件对象是 stdioFILE* 的包装器(Python 3 不是) PyFile_AsFile 返回包装好的 FILE* 的 API(注意 it's missing from Python 3) 对于独立的fd,您需要先构造一个类似文件的对象(这样就会有一个FILE* 来返回;))

一个对象的id() 是它的内存地址(CPython-specific)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

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

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

【讨论】:

如果您担心库使用不同的 C 运行时(主要是 Windows 问题),那么使用 PyFile_AsFile 不会解决任何问题,并且无缘无故地将代码限制为 Python 2。为什么将 Cython 带入讨论?这是一个随机的转场。 另外,永远不要将id(f) 作为指针传递。您希望 py_object(f) 传递一个 Python 对象——作为 CPython 的 PyObject *。使用id 获取基址特定于 CPython,并且将 Python 整数作为 arguments 传递也默认转换为 32 位 C int 值,这将截断 64 位指针值。 我希望看到一些支持“截断指向整数的指针”。你知道,Python 确实有长整数的概念,而且完全没有理由截断 c_void_p 您对设置api.PyFile_AsFile.argtypes=(ctypes.py_object,) 并调用api.PyFile_AsFile(f) 有何不满?它更简单,也是预期的用途。 @ivan_pozdeev - 作为一个相当有经验的 C 程序员,这是我第一次听说使用 FILE * 作为公共 API 的一部分是一个坏主意。并不是说你错了——我很少编写供公众使用的库。但是您真的是说使用文件号优越吗? FILE * 是 C 标准的一部分。来自open 的文件描述符,例如不是。所以你是说虽然stdio.h 更便携,但用于公共API 很糟糕?你有没有见过这会导致实践中的问题?阅读有关它的博客文章?还是这纯粹是推测?

以上是关于将 FILE * 从 Python / ctypes 传递给函数的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 ctypes 在 python 中正确包装 C API?

使用 ctypes 将数组从 Python 传递到 C++,无法完全处理它

如何通过 ctypes 将(非空)列表从 Python 传递到 C++?

Python ctypes:如何将 ctypes 数组传递给 DLL?

使用 ctypes 将 (uint8) NumPy 数组从 python 传递到 c++

从 Python (ctypes) 指向 C 以保存函数输出的指针