从Python / ctypes将FILE *传递到函数中

10

我有一个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并创建一个libc.fdopen()包装器,返回Python c_void_t,然后将其传递到库函数中。理论上似乎应该是安全的,只是想知道是否存在这种方法的问题或解决此问题的现有Python技术。

此外,这将进入一个长时间运行的进程(假设是“永远”),因此任何泄漏的文件描述符都将成为问题。


os.popen() 是不正确的。它至少需要一个参数,即要调用和获取管道的命令行。此外,根据文档的说法,它已经被 subprocess 取代并弃用。 - ivan_pozdeev
1
除非您还计划在Windows上运行此程序,因为Windows可能存在C运行时库不匹配的问题,否则我认为调用libc.fdopen并传递生成的FILE指针不会有任何问题。但是,我建议不要使用c_void_p,而是创建一个不透明的class FILE(Structure): pass,并设置libc.fdopen.restype = POINTER(FILE)。这样就不会将其转换为整数结果了。另一方面,c_void_p作为restype会被转换为整数,所以您必须确保将mylib.write_numbers.argtypes也设置好,以防止截断64位指针值。 - Eryk Sun
你是否考虑过使用fmemopen?如果单个write_numbers调用写入的数据量受到合理小的固定常数的限制,那么它可能是使用管道的良好替代方案。 - 5gon12eder
1
@BrianMcFarland 你不必(我甚至不确定你是否能够)读取 FILE *。但是,你可以简单地读取你传递给 fmemopenchar[] 数组。 - 5gon12eder
@5gon12eder - 不知道为什么我没想到这个。现在我认为我喜欢这个想法,因为它减少了我需要担心正确清理/释放的事情的数量,并且它减少了系统调用。顺便说一下 - fmemopen允许将模式设置为“r +”或“w +”,这将允许读/写操作。 - Brian McFarland
显示剩余2条评论
1个回答

5

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

此外,在Python 3中,I/O经过了重大改进,以便将其与stdio解耦。因此,FILE*对于这种Python版本(以及可能的大多数非C版本)来说是陌生的。

现在,你需要做的是:

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

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


最干净的方法是使用与库链接的精确实例(祈祷它是动态链接的,否则将没有可调用的导出符号)。
对于第一项,我找不到任何Python模块可以分析已加载的动态模块的元数据,以找出它链接了哪些DLL / so(仅名称甚至名称+版本都不足够,您知道,由于系统上可能存在多个库的实例)。尽管它绝对是可能的,因为有关其格式的信息广泛可用。
对于第二项,这是一个微不足道的ctypes.cdll('path').fdopen(对于MSVCRT是_fdopen)。
其次,您可以编写一个小型辅助模块,该模块将与库编译成相同的(或保证兼容的)运行时,并为您执行从上述描述符/句柄的转换。这实际上是编辑库本身的解决方法的替代方法。
最后,还有一种最简单(也是最不规范)的方法,使用Python的C运行时实例(因此所有上述警告都适用),通过Python C API可以通过ctypes.pythonapi使用。 它利用了
  • the fact that Python 2's file-like objects are wrappers over stdio's FILE* (Python 3's are not)
  • PyFile_AsFile API that returns the wrapped FILE* (note that it's missing from Python 3)
    • for a standalone fd, you need to construct a file-like object first (so that there would be a FILE* to return ;) )
  • the fact that id() of an object is its memory address (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和C指针时,您需要手动确保正确的对象生命周期!
  • os.fdopen()返回的类似文件的对象在.close()上会关闭描述符
    • 因此,如果您需要在文件对象关闭/垃圾回收后继续使用它们,请使用os.dup()复制描述符
  • 在使用C结构时,使用PyFile_IncUseCount()/PyFile_DecUseCount()调整相应对象的引用计数。
  • 确保除了iter(f)/for l in f之外没有其他I/O操作在描述符/文件对象上,否则会破坏数据(例如,自从调用iter(f)/for l in f以来,已经进行了独立于stdio缓存的内部缓存)

如果你担心库使用不同的C运行时(主要是Windows问题),那么使用PyFile_AsFile解决不了任何问题,而且没有充分的理由将代码限制在Python 2上。为什么要把Cython带入讨论中呢?这是一个随意的转换话题。 - Eryk Sun
此外,永远不要将 id(f) 作为指针传递。你需要使用 py_object(f) 来传递 Python 对象 -- 在 CPython 中是 PyObject *。使用 id 获取基地址是特定于 CPython 的,而将 Python 整数作为参数传递也默认转换为 32 位 C int 值,这将截断 64 位指针值。 - Eryk Sun
我想看到一些关于“将指针截断为整数”的支持。Python确实有长整数的概念,你知道的,而且没有完全的理由去截断c_void_p - ivan_pozdeev
1
你对设置 api.PyFile_AsFile.argtypes=(ctypes.py_object,) 并调用 api.PyFile_AsFile(f) 有什么反感吗?这样做更简单,也是预期的用法。 - Eryk Sun
1
@ivan_pozdeev - 作为一个相当有经验的 C 程序员,这是我第一次听说使用FILE *作为公共 API 的一部分不是个好主意。并不是说你错了 - 我很少编写用于公共使用的库。但你真的在说文件号的使用更优吗?FILE *是 C 标准的一部分。例如来自 open 的文件描述符却不是。所以你是在说虽然 stdio.h 更具可移植性,但在公共API中使用它是不好的吗?你有没有见过这会在实践中造成问题?读过相关博客文章吗?还是这纯粹是一种猜测? - Brian McFarland
显示剩余19条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接