使用distutils构建基于ctypes的C库
根据这个建议,我写了一个本地的C扩展库,用来通过ctypes优化Python模块的一部分。我选择ctypes而不是写一个CPython本地库,因为这样更快更简单(只需要几个函数,所有的紧密循环都在里面)。
不过,我现在遇到了一些麻烦。如果我想让我的工作能通过distutils轻松安装,使用python setup.py install
命令,那么distutils需要能够构建我的共享库并安装它(大概是安装到/usr/lib/myproject
)。但是,这并不是一个Python扩展模块,所以据我所知,distutils无法做到这一点。
我发现了一些其他人遇到类似问题的参考资料:
我知道我可以选择不使用distutils来处理共享库,或者使用我发行版的打包系统。我的担心是,这样会限制可用性,因为并不是每个人都能轻松安装它。
所以我的问题是:目前通过distutils分发一个共享库的最佳方法是什么?这个共享库将被ctypes使用,但它本身是操作系统本地的,而不是一个Python扩展模块?
如果你能详细说明上面提到的某个解决方法,并解释为什么那是最佳方式,欢迎分享。如果没有更好的方法,至少所有信息都能集中在一个地方。
3 个回答
这里有一些澄清:
这不是一个“基于 ctypes 的”库。它只是一个标准的 C 库,你需要用 distutils 来安装它。如果你使用 C 扩展,使用 ctypes 或 Cython 来包装这个库对于这个问题来说并不重要。
因为这个库显然不是通用的,而只是为你的应用程序进行了优化,所以你链接的推荐并不适合你。在这种情况下,写一个 C 扩展或者使用 Cython 可能更简单,这样就能避免你的问题。
关于实际的问题,你可以随时使用自己定制的 distutils 命令,实际上,有一个讨论提到了这样一个命令,叫做 OOF2 的 build_shlib
命令,它可以满足你的需求。不过在这种情况下,你想安装的自定义库其实并不是共享库,我认为你不需要把它安装在 /usr/lib/yourproject 里,而是可以把它安装到 /usr/lib/python-x.x/site-packages/yourmodule 的包目录里,和你的 Python 文件一起放。不过我不能百分之百确定,所以你得试试看。
我在这里搭建了一个简单的 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 自动编译的 :-)
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})