将 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 运行时实例(因此上述所有警告完全适用),有一个最简单(也是最脏)的方法。它利用了
stdio
的 FILE*
的包装器(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
请记住,使用 fd
s 和 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?