Distutils:构建多个共享方法的 Python 扩展模块(用 Swig 编写)

Jia*_*ang 3 c++ python swig distutils

我有四个C++文件:Ah、A.cpp、Bh、B.cpp,Ah包含在B.cpp中

啊:

#pragma once 
void A();
Run Code Online (Sandbox Code Playgroud)

A.cpp:

#include <iostream>
void A() {
    std::cout << "A" << std::endl;
}
Run Code Online (Sandbox Code Playgroud)

乙:

#pragma once
void B();
Run Code Online (Sandbox Code Playgroud)

B.cpp:

#include "A.h"
#include <iostream>
void B() {
    A();
    std::cout << "B" << std::endl;
}
Run Code Online (Sandbox Code Playgroud)

现在我写了两个SWIG接口文件Ai和Bi

人工智能:

%module A
%{
#include "A.h"
%}
%include "A.h"
Run Code Online (Sandbox Code Playgroud)

双:

%module B
%{
#include "B.h"
%}
%include "B.h"
Run Code Online (Sandbox Code Playgroud)

setup.py 文件是:

from distutils.core import setup, Extension
A_ext = Extension( "_A", [ "A.i", "A.cpp", ], swig_opts = ['-c++'], extra_compile_args = ['-g'])
B_ext = Extension( "_B", [ "B.i", "B.cpp", ], swig_opts = ['-c++'], extra_compile_args = ['-g'])
setup(
    name = "test",
    version = "1.0",
    ext_modules = [ A_ext, B_ext ],
    py_modules = [ "A", "B" ]
)
Run Code Online (Sandbox Code Playgroud)

如果我输入下面的命令,它将显示“A”。

python -c 'import A; A.A()' 
Run Code Online (Sandbox Code Playgroud)

如果我输入以下命令,则会出现分段错误:

python -c 'import B; B.B()'
Run Code Online (Sandbox Code Playgroud)

我怎样才能让这个命令正确运行?由于我不想多次编译B.cpp,除了下面的方法之外还有什么方法吗?

B_ext = Extension( "_B", [ "B.i", "A.cpp", "B.cpp", ], swig_opts = ['-c++'], extra_compile_args = ['-g'])
Run Code Online (Sandbox Code Playgroud)

Cri*_*ati 6

为了清楚起见,我对你的文件做了一些更改。

#pragma once 


void funcA();
Run Code Online (Sandbox Code Playgroud)

a.cpp

#include <iostream>


void funcA() {
    std::cout << __FILE__ << " " << __LINE__ <<  " " << __FUNCTION__ <<  std::endl;
}
Run Code Online (Sandbox Code Playgroud)

人工智能

%module a
%{
    #include "a.h"
%}
%include "a.h"
Run Code Online (Sandbox Code Playgroud)

#pragma once


void funcB();
Run Code Online (Sandbox Code Playgroud)

b.cpp

#include "a.h"
#include <iostream>


void funcB() {
    std::cout << __FILE__ << " " << __LINE__ <<  " " << __FUNCTION__ <<  std::endl;
    funcA();
}
Run Code Online (Sandbox Code Playgroud)

%module b
%{
    #include "b.h"
%}
%include "b.h"
Run Code Online (Sandbox Code Playgroud)

设置.py

from distutils.core import setup
from distutils.extension import Extension


a = "a"
b = "b"

ext_a = Extension("_" + a, [a + ".i", a + ".cpp"], swig_opts=("-c++",), extra_compile_args=["-g"])
ext_b = Extension("_" + b, [b + ".i", b + ".cpp"], swig_opts=("-c++",), extra_compile_args=["-g"])

setup(
    name="test",
    version="1.0",
    ext_modules=[ext_a, ext_b],
    py_modules=[a, b]
)
Run Code Online (Sandbox Code Playgroud)

调用b.funcB时会发生什么(简化)(仅stacktrace,导入保留在一边)。每个步骤都会调用下一个步骤:

  1. 来自模块b ( b.py )的funcB
  2. 来自模块_b 的funcB_b.so_b.cpython-35m-x86_64-linux-gnu.so
    • 这里的一切都发生在C(或C++)中
    • 当前的funcBb.cpp中的不同:它是由swig生成的,名称为_wrap_funcB
    • 上一个项目符号也适用于funcAa.cpp
  3. 来自b.cpp的funcB
  4. 来自a.cpp的funcA

问题在于步骤#4 中的代码不在模块_b中,并且在运行时会失败。但事情有点奇怪:当funcB被调用时,失败(核心转储)不会出现,而是在模块(b -> _b导入时出现(我认为这是因为swig的幕后魔法),如下见下文。

输出

[cfati@cfati-ubtu16x64-0:~/Work/Dev/StackOverflow/q050938128]> ~/sopr.sh
*** Set shorter prompt to better fit when pasted in StackOverflow (or other) pages ***

[064bit prompt]> ls
a.cpp  a.h  a.i  b.cpp  b.h  b.i  setup.py
[064bit prompt]> python3 setup.py build > /dev/null 2>&1
[064bit prompt]> echo $?
0
[064bit prompt]> ls
a.cpp  a.h  a.i  a.py  a_wrap.cpp  b.cpp  b.h  b.i  b.py  build  b_wrap.cpp  setup.py
[064bit prompt]> ls ./build/lib.linux-x86_64-3.5
_a.cpython-35m-x86_64-linux-gnu.so  _b.cpython-35m-x86_64-linux-gnu.so
[064bit prompt]> PYTHONPATH=${PYTHONPATH}:./build/lib.linux-x86_64-3.5 python3 -c "import _a;_a.funcA()"
a.cpp 6 funcA
[064bit prompt]> PYTHONPATH=${PYTHONPATH}:./build/lib.linux-x86_64-3.5 python3 -c "import _b"
Segmentation fault (core dumped)
Run Code Online (Sandbox Code Playgroud)

为了解决这个问题,可以:

  1. 正如您所指出的,在模块_b中包含funcA(通过在ext_b的源文件列表中添加a.cpp )。这样,两个模块都将是独立的(来自funcAPoV),每个模块都将独立工作,而且funcA将在两个模块中重复
  2. 使_b依赖于_a(毕竟,它们是共享对象)。但这不是Python扩展模块的使用方式,并且它不适用于Win(以及某些Nix版本)。所以,这更像是一个(蹩脚的)解决方法(gainarie
  3. 将a.cpp构建到不同的共享库(.so 不是Python扩展模块)中以供两个模块使用。不用说,在运行时它们每个都需要.so存在

显然,选择#3。是完美的候选人。但是distutils ( [Python.Docs]: API Reference ) 没有提供所需的功能OOTB (显然构建一个扩展模块和它所依赖的外部共享库,不是distutils 的目标场景),或者至少,我找不到任何。
有一个build_clib模块,它提供构建静态库(由扩展模块使用)的功能,但这与选项 #1 相同

设置.py

import sys
import os
from distutils.core import setup
from distutils.extension import Extension
from distutils.command.build_clib import build_clib
from distutils.command.build_ext import build_ext
from distutils.ccompiler import CCompiler


__win = sys.platform[:3].lower() == "win"
export_symbols_option = "export_symbols"


class build_clib_dyn(build_clib):
    def finalize_options(self):
        self.set_undefined_options('build',
                                   ('build_lib', 'build_clib'),
                                   ('build_temp', 'build_temp'),
                                   ('compiler', 'compiler'),
                                   ('debug', 'debug'),
                                   ('force', 'force'))
        self.libraries = self.distribution.libraries
        if self.libraries:
            self.check_library_list(self.libraries)
        if self.include_dirs is None:
            self.include_dirs = self.distribution.include_dirs or []
        if isinstance(self.include_dirs, str):
            self.include_dirs = self.include_dirs.split(os.pathsep)

    def build_libraries(self, libraries):
        for (lib_name, build_info) in libraries:
            sources = build_info.get('sources')
            if sources is None or not isinstance(sources, (list, tuple)):
                raise DistutilsSetupError(
                       "in 'libraries' option (library '%s'), "
                       "'sources' must be present and must be "
                       "a list of source filenames" % lib_name)
            sources = list(sources)
            macros = build_info.get('macros')
            include_dirs = build_info.get('include_dirs')
            objects = self.compiler.compile(sources,
                                            output_dir=self.build_temp,
                                            macros=macros,
                                            include_dirs=include_dirs,
                                            debug=self.debug)
            self.compiler.link(CCompiler.SHARED_OBJECT, objects, self.compiler.library_filename(lib_name, lib_type="shared"),
                               output_dir=self.build_clib,
                               export_symbols=build_info.get(export_symbols_option),
                               debug=self.debug)


if __win:
    class build_ext_w_dyn_dep(build_ext):
        def finalize_options(self):
            super(build_ext_w_dyn_dep, self).finalize_options()
            self.library_dirs.append(os.path.dirname(self.build_temp))

else:
    class build_ext_w_dyn_dep(build_ext):
        pass


a_name = "a"
b_name = "b"
common_name = a_name + b_name + "common"
swig_opts = ["-c++"]
libraries = [common_name]
lib_common_build_info = {"sources": [a_name + ".cpp"]}
if __win:
    extra_compile_args = None
    extra_link_args = None
    lib_common_build_info[export_symbols_option] = ["funcA"]
else:
    extra_compile_args = ["-g"]
    extra_link_args = ["-Wl,-rpath,${ORIGIN}"]

lib_common_info = (common_name, lib_common_build_info)
ext_a = Extension("_" + a_name, [a_name + ".i"], libraries=libraries, extra_compile_args=extra_compile_args, extra_link_args=extra_link_args, swig_opts=swig_opts)
ext_b = Extension("_" + b_name, [b_name + ".i", b_name + ".cpp"], libraries=libraries, extra_compile_args=extra_compile_args, extra_link_args=extra_link_args, swig_opts=swig_opts)

setup(
    name="test",
    version="1.0",
    libraries=[lib_common_info],
    cmdclass={"build_clib": build_clib_dyn, "build_ext": build_ext_w_dyn_dep},
    ext_modules=[ext_a, ext_b],
    py_modules=[a_name, b_name]
)
Run Code Online (Sandbox Code Playgroud)

注意事项

  • build_clib_dyn扩展了build_clib,因为它的功能必须修改。重写了 2 个方法,但实际上只更改了其中的一小部分(注释没有从基类方法(Python 3.5.4代码库)复制,以减少代码量,但这并不算真正的更改)
  • 该任务需要相当多的distutils代码浏览,因为某些选项没有记录(而且我对此不是很熟悉)
  • 还需要一些Nix知识,因为加载不在系统路径中的共享库时事情会变得棘手,并且还要保持事情顺利(例如不更改${LD_LIBRARY_PATH}
  • build_ext_w_dyn_dep与build_clib_dyn类似(仅限Win)。由于在构建Win动态链接库(.dll)时,生成了2个文件,并且在这种情况下它们不在同一目录中,因此库搜索路径需要进行一些调整
  • Python 3 Python 2兼容

输出(再次运行上述命令):

[064bit prompt]> ls
a.cpp  a.h  a.i  b.cpp  b.h  b.i  setup.py
[064bit prompt]> python3 setup.py build > /dev/null 2>&1
[064bit prompt]> echo $?
0
[064bit prompt]> ls
a.cpp  a.h  a.i  a.py  a_wrap.cpp  b.cpp  b.h  b.i  b.py  build  b_wrap.cpp  setup.py
[064bit prompt]> ls build/lib.linux-x86_64-3.5/
_a.cpython-35m-x86_64-linux-gnu.so  _b.cpython-35m-x86_64-linux-gnu.so  libabcommon.so
[064bit prompt]> ldd build/lib.linux-x86_64-3.5/_a.cpython-35m-x86_64-linux-gnu.so
        linux-vdso.so.1 =>  (0x00007fffadb49000)
        libabcommon.so => /home/cfati/Work/Dev/StackOverflow/q050938128/build/lib.linux-x86_64-3.5/libabcommon.so (0x00007f91cd50f000)
        libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f91cd18d000)
        libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f91ccf77000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f91ccbad000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f91cc990000)
        libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f91cc687000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f91cd916000)
[064bit prompt]> PYTHONPATH=${PYTHONPATH}:./build/lib.linux-x86_64-3.5 python3 -c "import _a;_a.funcA()"
a.cpp 6 funcA
[064bit prompt]> PYTHONPATH=${PYTHONPATH}:./build/lib.linux-x86_64-3.5 python3 -c "import _b;_b.funcB()"
b.cpp 7 funcB
a.cpp 6 funcA
Run Code Online (Sandbox Code Playgroud)
  • 添加了对Win的支持(我想,这对于这个问题来说并不是很重要)
    • 由于模块结构很简单,因此可以在setup.py级别完成所有操作(不必修改源文件)
    • 唯一的额外要求是swig.exe目录应该位于%PATH%中