使用distutils构建基于ctypes的C库

31 投票
3 回答
4447 浏览
提问于 2025-04-16 09:01

根据这个建议,我写了一个本地的C扩展库,用来通过ctypes优化Python模块的一部分。我选择ctypes而不是写一个CPython本地库,因为这样更快更简单(只需要几个函数,所有的紧密循环都在里面)。

不过,我现在遇到了一些麻烦。如果我想让我的工作能通过distutils轻松安装,使用python setup.py install命令,那么distutils需要能够构建我的共享库并安装它(大概是安装到/usr/lib/myproject)。但是,这并不是一个Python扩展模块,所以据我所知,distutils无法做到这一点。

我发现了一些其他人遇到类似问题的参考资料:

我知道我可以选择不使用distutils来处理共享库,或者使用我发行版的打包系统。我的担心是,这样会限制可用性,因为并不是每个人都能轻松安装它。

所以我的问题是:目前通过distutils分发一个共享库的最佳方法是什么?这个共享库将被ctypes使用,但它本身是操作系统本地的,而不是一个Python扩展模块?

如果你能详细说明上面提到的某个解决方法,并解释为什么那是最佳方式,欢迎分享。如果没有更好的方法,至少所有信息都能集中在一个地方。

3 个回答

-2

这里有一些澄清:

  1. 这不是一个“基于 ctypes 的”库。它只是一个标准的 C 库,你需要用 distutils 来安装它。如果你使用 C 扩展,使用 ctypes 或 Cython 来包装这个库对于这个问题来说并不重要。

  2. 因为这个库显然不是通用的,而只是为你的应用程序进行了优化,所以你链接的推荐并不适合你。在这种情况下,写一个 C 扩展或者使用 Cython 可能更简单,这样就能避免你的问题。

关于实际的问题,你可以随时使用自己定制的 distutils 命令,实际上,有一个讨论提到了这样一个命令,叫做 OOF2build_shlib 命令,它可以满足你的需求。不过在这种情况下,你想安装的自定义库其实并不是共享库,我认为你不需要把它安装在 /usr/lib/yourproject 里,而是可以把它安装到 /usr/lib/python-x.x/site-packages/yourmodule 的包目录里,和你的 Python 文件一起放。不过我不能百分之百确定,所以你得试试看。

3

我在这里搭建了一个简单的 Python 包,里面有 ctypes 扩展,链接在这里:https://github.com/himbeles/ctypes-example,这个包可以在 Windows、Mac 和 Linux 上运行。

  • 这个包采用了 memeplex 的方法,通过重写 build_ext.get_export_symbols(),强制所有操作系统的库扩展名都保持一致(都是 .so)。
  • 另外,C/C++ 源代码中的一个编译指令确保了在 Windows 和 Unix 系统下,共享库符号能够正确导出。
  • 作为额外的好处,所有操作系统的二进制文件轮子(binary wheels)都是通过 GitHub Action 自动编译的 :-)
6

distutils的文档在这里提到:

CPython的C扩展是一个共享库(比如在Linux上是一个.so文件,在Windows上是.pyd文件),它需要有一个初始化函数。

所以,跟普通的共享库相比,唯一的区别就是这个初始化函数(当然还有一些合理的文件命名规则,你应该不会有问题)。现在,如果你看看distutils.command.build_ext,你会发现它定义了一个get_export_symbols()方法,功能是:

返回共享扩展需要导出的符号列表。这个方法要么使用'ext.export_symbols',要么如果没有提供,就用"PyInit_" + 模块名。这个在Windows上是特别重要的,因为.pyd文件(DLL)必须导出模块的"PyInit_"函数。

所以,使用它来处理普通的共享库应该是可以直接用的,除了在Windows上。不过,这个问题也很容易解决。get_export_symbols()的返回值会传给distutils.ccompiler.CCompiler.link(),而它的文档说明:

'export_symbols'是共享库将要导出的符号列表。(这似乎只在Windows上相关。)

所以,不把初始化函数加入导出符号就可以解决这个问题。为此,你只需要简单地重写一下build_ext.get_export_symbols()

另外,你可能还想简化模块名。下面是一个完整的build_ext子类的例子,它可以构建ctypes模块和扩展模块:

from distutils.core import setup, Extension
from distutils.command.build_ext import build_ext


class build_ext(build_ext):

    def build_extension(self, ext):
        self._ctypes = isinstance(ext, CTypes)
        return super().build_extension(ext)

    def get_export_symbols(self, ext):
        if self._ctypes:
            return ext.export_symbols
        return super().get_export_symbols(ext)

    def get_ext_filename(self, ext_name):
        if self._ctypes:
            return ext_name + '.so'
        return super().get_ext_filename(ext_name)


class CTypes(Extension): pass


setup(name='testct', version='1.0',
      ext_modules=[CTypes('ct', sources=['testct/ct.c']),
                   Extension('ext', sources=['testct/ext.c'])],
      cmdclass={'build_ext': build_ext})

撰写回答