微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

通过Python C扩展包括和分发第三方库

如何解决通过Python C扩展包括和分发第三方库

我正在构建一个C Python扩展,该扩展使用了一个“第三方”库-在这种情况下,我是使用单独的构建过程和工具链构建的。将此库称为libplumbus.dylib

目录结构为:

grumbo/
  include/
    plumbus.h
  lib/
    libplumbus.so
  grumbo.c
  setup.py

我的setup.py看起来像:

from setuptools import Extension,setup

native_module = Extension(
    'grumbo',define_macros = [('MAJOR_VERSION','1'),('MInor_VERSION','0')],sources       = ['grumbo.c'],include_dirs  = ['include'],libraries     = ['plumbus'],library_dirs  = ['lib'])


setup(
    name = 'grumbo',version = '1.0',ext_modules = [native_module] )

由于libplumbus是一个外部库,所以当我运行import grumbo时,我得到:

Traceback (most recent call last):
  File "<stdin>",line 1,in <module>
ImportError: dlopen(/path/to/grumbo/grumbo.cpython-37m-darwin.so,2): Library not loaded: lib/libplumbus.dylib
  Referenced from: /path/to/grumbo/grumbo.cpython-37m-darwin.so
  Reason: image not found

最简单的设置方法是,将libplumbus包含在发行版中并在导入grumbo时正确加载? (请注意,这应该与virtualenv一起使用。)

我曾尝试将lib/libplumbus.dylib添加package_data,但这不起作用,即使我将-Wl,-rpath,@loader_path/grumbo/lib添加到扩展程序的extra_link_args中。

解决方法

这篇文章的目标是要有一个setup.py来创建源代码分发。这意味着运行后

python setup.py sdist

生成的dist/grumbo-1.0.tar.gz可用于通过安装

pip install grumbo-1.0.tar.gz

对于Linux / MacOS,我们将从setup.py开始,然后进行调整以使其也适用于Windows。


第一步是将其他数据(包含/库)放入分发中。我不确定是否真的不可能为模块添加数据,但是setuptools提供了为包添加数据的功能,因此让我们从您的模块中制作一个包(无论如何可能是个好主意)。>

package grumbo的新结构如下:

src/
  grumbo/
     __init__.py  # empty
     grumbo.c
     include/
       plumbus.h
     lib/
       libplumbus.so
setup.py

并更改了setup.py

from setuptools import setup,Extension,find_packages

native_module = Extension(
                name='grumbo.grumbo',sources = ["src/grumbo/grumbo.c"],)
kwargs = {
      'name' : 'grumbo','version' : '1.0','ext_modules' :  [native_module],'packages':find_packages(where='src'),'package_dir':{"": "src"},}

setup(**kwargs)

它还没有做很多事情,但是至少setuptools可以找到我们的软件包。构建失败,因为缺少包含项。

现在,让我们从include文件夹通过package-data向分发中添加所需的包含内容:

...
kwargs = {
      ...,'package_data' : { 'grumbo': ['include/*.h']},}
...

这样,我们的包含文件便被复制到源代码发行版中。但是,由于它将在“某个地方”构建,我们尚不知道,因此在include_dirs = ['include']定义中添加Extension并不会减少它。

必须有一种更好的方法(并且减少脆性)来找到正确的包含路径,但这就是我想出的:

...
import os
import sys
import sysconfig
def path_to_build_folder():
    """Returns the name of a distutils build directory"""
    f = "{dirname}.{platform}-{version[0]}.{version[1]}"
    dir_name = f.format(dirname='lib',platform=sysconfig.get_platform(),version=sys.version_info)
    return os.path.join('build',dir_name,'grumbo')

native_module = Extension(
                ...,include_dirs  = [os.path.join(path_to_build_folder(),'include')],)
...

现在,该扩展名已构建,但由于尚未与共享库libplumbus.so链接,因此尚无法加载,因此某些符号无法解析。

类似于头文件,我们可以将我们的库添加到发行版中:

kwargs = {
          ...,'package_data' : { 'grumbo': ['include/*.h','lib/*.so']},}
...

并为链接器添加正确的lib路径:

...
native_module = Extension(
                ...
                libraries     = ['plumbus'],library_dirs  = [os.path.join(path_to_build_folder(),'lib')],)
...

现在,我们快到了:

  • 扩展名内置于site-packages/grumbo/
  • 扩展名取决于libplumbus.so,在ldd的帮助下可以看到
  • libplumbus.so被放入site-packages/grumbo/lib

但是,我们仍然无法导入扩展名,因为import grumbo.grumbo会导致

ImportError:libplumbus.so:无法打开共享对象文件:否这样 文件或目录

因为加载程序找不到相对于我们的扩展名位于文件夹.\lib中的所需共享库。我们可以使用rpath来“帮助”加载程序:

...
native_module = Extension(
                ...
                extra_link_args = ["-Wl,-rpath=$ORIGIN/lib/."],)
...

现在我们完成了:

>>> import grumbo.grumbo
# works!

还可以构建和安装轮子:

python setup.py bdist_wheel

然后:

pip install grumbo-1.0-xxxx.whl

第一个里程碑实现了。现在我们对其进行扩展,使其也可以在其他平台上使用。


Linux和Macos的相同源分发:

要能够在Linux和MacOS上安装相同的源发行版,必须同时存在两个版本的共享库(对于Linux和MacOS)。一种选择是在共享对象的名称上添加后缀:具有libplumbus.linux.solibplumbis.macos.so。根据平台的不同,可以在setup.py中选择正确的共享对象:

...
import platform
def pick_library():
    my_system = platform.system()
    if my_system == 'Linux':
        return "plumbus.linux"
    if my_system == 'Darwin':
        return "plumbus.macos"
    if my_system == 'Windows':
        return "plumbus"
    raise ValueError("Unknown platform: " + my_system)

native_module = Extension(
                ...
                libraries     = [pick_library()],...
              )

针对Windows的调整

在Windows上,动态库是dll,而不是共享库,因此需要考虑一些差异:

  • 构建C扩展名时,它需要plumbus.lib文件,我们需要将其放入lib子文件夹中。
  • 在运行时加载C扩展名时,它需要plumbus.dll文件。
  • Windows没有rpath的概念,因此我们需要将dll放在扩展名的旁边,以便可以找到它(有关更多详细信息,请参见此SO-post)。

这意味着文件夹结构应如下:

src/
  grumbo/
     __init__.py
     grumbo.c
     plumbus.dll           # needed for Windows
     include/
       plumbus.h
     lib/
       libplumbus.linux.so # needed on Linux
       libplumbus.macos.so # needed on Macos
       plumbus.lib         # needed on Windows
setup.py

setup.py中也有一些更改。首先,扩展package_data,以便拾取dlllib

...
kwargs = {
      ...
      'package_data' : { 'grumbo': ['include/*.h','lib/*.so','lib/*.lib','*.dll',# for windows
                                   ]},}
...

第二,rpath仅可在Linux / MacOS上使用,因此:

def get_extra_link_args():
    if platform.system() == 'Windows':
        return []
    else:
        return ["-Wl,-rpath=$ORIGIN/lib/."]
    

native_module = Extension(
                ...
                extra_link_args = get_extra_link_args(),)

就可以了!


完整的安装文件(您可能要添加宏定义或类似的内容,而我已经跳过了):

from setuptools import setup,find_packages

import os
import sys
import sysconfig
def path_to_build_folder():
    """Returns the name of a distutils build directory"""
    f = "{dirname}.{platform}-{version[0]}.{version[1]}"
    dir_name = f.format(dirname='lib','grumbo')


import platform
def pick_library():
    my_system = platform.system()
    if my_system == 'Linux':
        return "plumbus.linux"
    if my_system == 'Darwin':
        return "plumbus.macos"
    if my_system == 'Windows':
        return "plumbus"
    raise ValueError("Unknown platform: " + my_system)


def get_extra_link_args():
    if platform.system() == 'Windows':
        return []
    else:
        return ["-Wl,-rpath=$ORIGIN/lib/."]
    

native_module = Extension(
                name='grumbo.grumbo',libraries     = [pick_library()],extra_link_args = get_extra_link_args(),}

setup(**kwargs)

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。