构建“基于ctypes”的模型带有 distutils 的 C 库

发布于 2024-10-09 02:32:43 字数 1111 浏览 7 评论 0原文

按照此建议,我编写了一个本机C扩展库通过 ctypes 优化 Python 模块的一部分。我选择 ctypes 而不是编写 CPython 原生库,因为它更快、更容易(只有几个函数,里面有所有紧密的循环)。

我现在遇到了障碍。如果我希望使用 distutils 使用 python setup.py install 轻松安装我的工作,那么 distutils 需要能够构建我的共享库并将其安装(大概到 /usr/lib/ 我的项目)。然而,这不是一个 Python 扩展模块,据我所知,distutils 无法做到这一点。

我发现了一些对其他遇到此问题的人的参考:

我知道我可以做一些本机的事情,而不是使用 distutils 作为共享库,或者确实使用我的发行版的打包系统。我担心这会限制可用性,因为并不是每个人都能轻松安装它。

所以我的问题是:当前使用 distutils 分发共享库的最佳方法是什么,该共享库将由 ctypes 使用,但否则是操作系统本机的,而不是 Python 扩展模块?

如果您可以扩展它并证明为什么这是最好的方法,请随意回答上面链接的黑客之一。如果没有更好的办法,至少所有的信息都会集中在一处。

Following this recommendation, I have written a native C extension library to optimise part of a Python module via ctypes. I chose ctypes over writing a CPython-native library because it was quicker and easier (just a few functions with all tight loops inside).

I've now hit a snag. If I want my work to be easily installable using distutils using python setup.py install, then distutils needs to be able to build my shared library and install it (presumably into /usr/lib/myproject). However, this not a Python extension module, and so as far as I can tell, distutils cannot do this.

I've found a few references to people other people with this problem:

I am aware that I can do something native and not use distutils for the shared library, or indeed use my distribution's packaging system. My concern is that this will limit usability as not everyone will be able to install it easily.

So my question is: what is the current best way of distributing a shared library with distutils that will be used by ctypes but otherwise is OS-native and not a Python extension module?

Feel free to answer with one of the hacks linked to above if you can expand on it and justify why that is the best way. If there is nothing better, at least all the information will be in one place.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

秋心╮凉 2024-10-16 02:32:43

distutils 文档此处指出:

CPython 的 AC 扩展是一个共享库(例如 Linux 上的 .so 文件,Windows 上的 .pyd),它导出初始化函数。

因此,关于普通共享库的唯一区别似乎是初始化函数(除了合理的文件命名约定之外,我认为您没有任何问题)。现在,如果您查看 distutils.command.build_ext,您将看到它定义了一个 get_export_symbols() 方法:

返回共享扩展必须导出的符号列表。这可以使用“ext.export_symbols”,或者如果未提供,则使用“PyInit_”+ module_name。仅与 Windows 相关,其中 .pyd 文件 (DLL) 必须导出模块“PyInit_”函数。

因此,将它用于普通共享库应该是开箱即用的,除了在
视窗。但解决这个问题也很容易。 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})

The distutils documentation here states that:

A C extension for CPython is a shared library (e.g. a .so file on Linux, .pyd on Windows), which exports an initialization function.

So the only difference regarding a plain shared library seems to be the initialization function (besides a sensible file naming convention I don't think you have any problem with). Now, if you take a look at distutils.command.build_ext you will see it defines a get_export_symbols() method that:

Return the list of symbols that a shared extension has to export. This either uses 'ext.export_symbols' or, if it's not provided, "PyInit_" + module_name. Only relevant on Windows, where the .pyd file (DLL) must export the module "PyInit_" function.

So using it for plain shared libraries should work out-of-the-box except in
Windows. But it's easy to also fix that. The return value of get_export_symbols() is passed to distutils.ccompiler.CCompiler.link(), which documentation states:

'export_symbols' is a list of symbols that the shared library will export. (This appears to be relevant only on Windows.)

So not adding the initialization function to the export symbols will do the trick. For that you just need to trivially override build_ext.get_export_symbols().

Also, you might want to simplify the module name. Here is a complete example of a build_ext subclass that can build ctypes modules as well as extension modules:

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})
像极了他 2024-10-16 02:32:43

我在这里设置了一个带有 ctypes 扩展的最小工作 python 包:
https://github.com/himbeles/ctypes-example
适用于 Windows、Mac、Linux。

  • 它采用上面的 memeplex 方法覆盖 build_ext.get_export_symbols() 并强制库扩展对于所有操作系统都相同 (.so)。
  • 此外,c / c++ 源代码中的编译器指令可确保在 Windows 与 Unix 上正确导出共享库符号。
  • 作为奖励,二进制轮子由 GitHub Action 自动编译,适用于所有操作系统:-)

I have setup a minimal working python package with ctypes extension here:
https://github.com/himbeles/ctypes-example
which works on Windows, Mac, Linux.

  • It takes the approach of memeplex above of overwriting build_ext.get_export_symbols() and forcing the library extension to be the same (.so) for all operating systems.
  • Additionally, a compiler directive in the c / c++ source code ensures proper export of the shared library symbols in case of Windows vs. Unix.
  • As a bonus, the binary wheels are automatically compiled by a GitHub Action for all operating systems :-)
半葬歌 2024-10-16 02:32:43

到 2024 年,distutils 将被弃用,但这个问题仍然与其直接后继者 setuptools 相关。 setuptools 中的 setup 命令有一个选项 libraries 用于构建非扩展模块的库。此选项旨在仅构建扩展模块依赖项所需的库(静态链接,不作为库文件包含在二进制发行版中)。但是,可以更改相应的 setuptools 命令类以生成共享库(可通过 ctypes 加载)并将其包含在二进制发行版中。这是setup.py中的相关代码:

import os

from setuptools import setup
from setuptools.command.build_clib import build_clib


class BuildSharedLibraryCommand(build_clib):
    """
    Build a shared library.

    This is a modified version of the [build_clib class in
    setuptools](https://github.com/pypa/setuptools/blob/v75.1.0/setuptools/command/build_clib.py#L16-L110).
    """

    def finalize_options(self):
        super().finalize_options()
        # `self.build_clib` is set to a temporary directory in the base class. However, we want to build the shared
        # library in the actual build directory, so reset it here.
        self.build_clib = None
        self.set_undefined_options(
            "build",
            ("build_lib", "build_clib"),
        )

    def build_libraries(self, libraries):
        for lib_name, build_info in libraries:
            sources = sorted(list(build_info.get("sources")))

            expected_objects = self.compiler.object_filenames(
                sources,
                output_dir=self.build_temp,
            )

            macros = build_info.get("macros")
            include_dirs = build_info.get("include_dirs")
            cflags = build_info.get("cflags")
            self.compiler.compile(
                sources,
                output_dir=self.build_temp,
                macros=macros,
                include_dirs=include_dirs,
                extra_postargs=cflags,
                debug=self.debug,
            )

            link_libraries = build_info.get("link_libraries")
            library_dirs = build_info.get("library_dirs")
            lflags = build_info.get("lflags")
            if platform.system() == "Windows":
                if lflags is None:
                    lflags = []
                # The `link_shared_lib` command below does not include the `/DLL` flag, so add it here.
                if "/DLL" not in lflags:
                    lflags.append("/DLL")
            # Link the object files together into a shared library and store it in the package where the first source
            # file is located.
            self.compiler.link_shared_lib(
                expected_objects,
                lib_name,
                output_dir=os.path.join(self.build_clib, os.path.dirname(sources[0])),
                libraries=link_libraries,
                library_dirs=library_dirs,
                extra_postargs=lflags,
                debug=self.debug,
            )

    def get_library_names(self):
        # `build_clib` was actually designed to build a static lib which is linked to all extension modules. However,
        # this class modifies that behavior to build a shared library which shall NOT be linked to the extension
        # modules. The extension module link code queries `build_clib` libraries by calling `get_library_names()`.
        # Therefore, clear the library list, so no unwanted libraries will be linked.
        return []


setup(
    cmdclass={"build_clib": BuildSharedLibraryCommand},
    libraries=[
        (
            "algorithm",
            {
                "sources": ["myproject/calculation/algorithm.c"],
                "include_dirs": [],
                "library_dirs": [],
                "link_libraries": [],
            },
        )
    ],
)

setup调用中,cmdclass参数用于覆盖默认的build_clib代码>命令。 libraries 参数声明需要构建的所有库。在 Linux 上,这将生成一个 libalgorithm.so ,它与第一个源文件位于同一目录中。

In 2024, distutils is deprecated, but the question is still relevant for its direct successor setuptools. The setup command in setuptools has an option libraries to build libraries which are not extension modules. This option is designed to only build libraries which are needed as a dependency for extension modules (statically linked and not included as library file in binary distributions). However, the corresponding setuptools command class can be changed to produce shared libraries (which are loadable by ctypes) and include them in binary distributions. This is the relevant code in setup.py:

import os

from setuptools import setup
from setuptools.command.build_clib import build_clib


class BuildSharedLibraryCommand(build_clib):
    """
    Build a shared library.

    This is a modified version of the [build_clib class in
    setuptools](https://github.com/pypa/setuptools/blob/v75.1.0/setuptools/command/build_clib.py#L16-L110).
    """

    def finalize_options(self):
        super().finalize_options()
        # `self.build_clib` is set to a temporary directory in the base class. However, we want to build the shared
        # library in the actual build directory, so reset it here.
        self.build_clib = None
        self.set_undefined_options(
            "build",
            ("build_lib", "build_clib"),
        )

    def build_libraries(self, libraries):
        for lib_name, build_info in libraries:
            sources = sorted(list(build_info.get("sources")))

            expected_objects = self.compiler.object_filenames(
                sources,
                output_dir=self.build_temp,
            )

            macros = build_info.get("macros")
            include_dirs = build_info.get("include_dirs")
            cflags = build_info.get("cflags")
            self.compiler.compile(
                sources,
                output_dir=self.build_temp,
                macros=macros,
                include_dirs=include_dirs,
                extra_postargs=cflags,
                debug=self.debug,
            )

            link_libraries = build_info.get("link_libraries")
            library_dirs = build_info.get("library_dirs")
            lflags = build_info.get("lflags")
            if platform.system() == "Windows":
                if lflags is None:
                    lflags = []
                # The `link_shared_lib` command below does not include the `/DLL` flag, so add it here.
                if "/DLL" not in lflags:
                    lflags.append("/DLL")
            # Link the object files together into a shared library and store it in the package where the first source
            # file is located.
            self.compiler.link_shared_lib(
                expected_objects,
                lib_name,
                output_dir=os.path.join(self.build_clib, os.path.dirname(sources[0])),
                libraries=link_libraries,
                library_dirs=library_dirs,
                extra_postargs=lflags,
                debug=self.debug,
            )

    def get_library_names(self):
        # `build_clib` was actually designed to build a static lib which is linked to all extension modules. However,
        # this class modifies that behavior to build a shared library which shall NOT be linked to the extension
        # modules. The extension module link code queries `build_clib` libraries by calling `get_library_names()`.
        # Therefore, clear the library list, so no unwanted libraries will be linked.
        return []


setup(
    cmdclass={"build_clib": BuildSharedLibraryCommand},
    libraries=[
        (
            "algorithm",
            {
                "sources": ["myproject/calculation/algorithm.c"],
                "include_dirs": [],
                "library_dirs": [],
                "link_libraries": [],
            },
        )
    ],
)

In the setup call the cmdclass parameter is used to override the default build_clib command. The libraries parameter declares all libraries that need to be built. On Linux, this would produce a libalgorithm.so which is located in the the same directory as the first source file.

流心雨 2024-10-16 02:32:43

这里有一些澄清:

  1. 它不是一个“基于 ctypes”的库。它只是一个标准的 C 库,你想用 distutils 安装它。如果您使用 C 扩展、ctypes 或 cython 来包装该库,则与问题无关。

  2. 由于该库显然不是通用的,而只是包含针对您的应用程序的优化,因此您链接到的建议不适用于您,在您的情况下,编写 C 扩展或使用 Cython 可能更容易,在这种情况下,您的问题就可以避免。

对于实际问题,您始终可以使用自己的自定义 distutils 命令,事实上,与此类命令相关的讨论之一是 OOF2 build_shlib 命令,它可以满足您的需求。在这种情况下,虽然您想要安装一个实际上不共享的自定义库,但我认为您不需要将其安装在 /usr/lib/yourproject 中,但您可以将其安装到 /usr 中的包目录中/lib/python-xx/site-packages/yourmodule,以及您的 python 文件。但我不是 100% 确定,所以你必须尝试一下。

Some clarifications here:

  1. It's not a "ctypes based" library. It's just a standard C library, and you want to install it with distutils. If you use a C-extension, ctypes or cython to wrap that library is irrelevant for the question.

  2. Since the library apparently isn't generic, but just contains optimizations for your application, the recommendation you link to doesn't apply to you, in your case it is probably easier to write a C-extension or to use Cython, in which case your problem is avoided.

For the actual question, you can always use your own custom distutils command, and in fact one of the discussions linked to just such a command, the OOF2 build_shlib command, that does what you want. In this case though you want to install a custom library that really isn't shared, and then I think you don't need to install it in /usr/lib/yourproject, but you can install it into the package directory in /usr/lib/python-x.x/site-packages/yourmodule, together with your python files. But I'm not 100% sure of that so you'll have to try.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文