小编典典

在 Python 中包装 C 库:C、Cython 还是 ctypes?

all

我想从 Python 应用程序调用 C 库。我不想包装整个 API,只包装与我的案例相关的函数和数据类型。在我看来,我有三个选择:

  1. 在 C 中创建一个实际的扩展模块。可能有点矫枉过正,而且我还想避免学习扩展编写的开销。
  2. 使用Cython将 C 库中的相关部分公开给 Python。
  3. 在 Python 中完成所有工作,ctypes用于与外部库进行通信。

我不确定 2) 还是 3) 是更好的选择。3) 的优势在于它ctypes是标准库的一部分,生成的代码将是纯 Python
——尽管我不确定这个优势实际上有多大。

任何一种选择都有更多的优点/缺点吗?您推荐哪种方法?


编辑:
感谢您的所有回答,它们为任何想要做类似事情的人提供了很好的资源。当然,这个决定仍然是针对单一案件做出的——这里没有人会“这是正确的事情”之类的答案。对于我自己的情况,我可能会使用
ctypes,但我也期待在其他项目中尝试 Cython。

由于没有单一的正确答案,因此接受一个有点武断。我选择了 FogleBird 的答案,因为它提供了对 ctypes
的一些很好的见解,而且它目前也是投票率最高的答案。但是,我建议阅读所有答案以获得良好的概述。

再次感谢。


阅读 88

收藏
2022-04-21

共1个答案

小编典典

ctypes是您快速完成它的最佳选择,并且很高兴与您一起工作,因为您仍在编写 Python!

我最近包装了一个FTDI驱动程序,用于使用 ctypes 与 USB
芯片进行通信,它很棒。我在不到一个工作日的时间内完成了所有工作。(我只实现了我们需要的功能,大概15个功能)。

我们之前使用第三方模块PyUSB来达到同样的目的。PyUSB 是一个实际的 C/Python
扩展模块。但是 PyUSB 在阻止读/写时没有释放 GIL,这给我们带来了问题。所以我使用 ctypes 编写了我们自己的模块,它在调用本机函数时确实释放了
GIL。

需要注意的一点是,ctypes 不知道#define您正在使用的库中的常量和内容,只知道函数,因此您必须在自己的代码中重新定义这些常量。

这是代码最终看起来如何的示例(很多被剪掉,只是想向您展示它的要点):

from ctypes import *

d2xx = WinDLL('ftd2xx')

OK = 0
INVALID_HANDLE = 1
DEVICE_NOT_FOUND = 2
DEVICE_NOT_OPENED = 3

...

def openEx(serial):
    serial = create_string_buffer(serial)
    handle = c_int()
    if d2xx.FT_OpenEx(serial, OPEN_BY_SERIAL_NUMBER, byref(handle)) == OK:
        return Handle(handle.value)
    raise D2XXException

class Handle(object):
    def __init__(self, handle):
        self.handle = handle
    ...
    def read(self, bytes):
        buffer = create_string_buffer(bytes)
        count = c_int()
        if d2xx.FT_Read(self.handle, buffer, bytes, byref(count)) == OK:
            return buffer.raw[:count.value]
        raise D2XXException
    def write(self, data):
        buffer = create_string_buffer(data)
        count = c_int()
        bytes = len(data)
        if d2xx.FT_Write(self.handle, buffer, bytes, byref(count)) == OK:
            return count.value
        raise D2XXException

有人对各种选项进行了一些基准测试。

如果我必须用大量的类/模板/等包装一个 C++ 库,我可能会更犹豫。但是 ctypes
与结构配合得很好,甚至可以回调到 Python 中。

2022-04-21