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

Distutils: build multiple Python extension modules (written in Swig) that share a method

我有四个C ++文件:A.h,A.cpp,B.h,B.cpp和B.cpp中包含A.h

啊:

1
2
#pragma once
void A();

A.cpp:

1
2
3
4
#include <iostream>
void A() {
    std::cout <<"A" << std::endl;
}

B.h:

1
2
#pragma once
void B();

B.cpp:

1
2
3
4
5
6
#include"A.h"
#include <iostream>
void B() {
    A();
    std::cout <<"B" << std::endl;
}

现在,我编写了两个SWIG接口文件A.i和B.i

A.i:

1
2
3
4
5
%module A
%{
#include"A.h"
%}
%include"A.h"

双:

1
2
3
4
5
%module B
%{
#include"B.h"
%}
%include"B.h"

setup.py文件是:

1
2
3
4
5
6
7
8
9
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" ]
)

如果我在下面键入命令,它将显示" A"。

1
python -c 'import A; A.A()'

如果我在下面键入命令,则会出现分段错误:

1
python -c 'import B; B.B()'

如何使此命令正确运行? 由于我不想多次编译B.cpp,除了下面的方法有什么办法吗?

1
B_ext = Extension("_B", ["B.i","A.cpp","B.cpp", ], swig_opts = ['-c++'], extra_compile_args = ['-g'])

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

啊:

1
2
3
4
#pragma once


void funcA();

a.cpp:

1
2
3
4
5
6
#include <iostream>


void funcA() {
    std::cout << __FILE__ <<"" << __LINE__ << "" << __FUNCTION__ <<  std::endl;
}

a.i:

1
2
3
4
5
%module a
%{
    #include"a.h"
%}
%include"a.h"

b.h:

1
2
3
4
#pragma once


void funcB();

b.cpp:

1
2
3
4
5
6
7
8
#include"a.h"
#include <iostream>


void funcB() {
    std::cout << __FILE__ <<"" << __LINE__ << "" << __FUNCTION__ <<  std::endl;
    funcA();
}

双:

1
2
3
4
5
%module b
%{
    #include"b.h"
%}
%include"b.h"

setup.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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]
)

调用b.funcB()时会发生什么事情(简化)(仅堆栈跟踪,不考虑导入)。每个步骤都调用下一个步骤:

  • 来自模块b的funcB(b.py)
  • 来自模块_b的funcB(_b.so或_b.cpython-35m-x86_64-linux-gnu.so)

    • 从这里开始的一切都发生在C(或C ++)中
    • 当前的funcB与b.cpp中的funcB不同:它由swig生成,名称为_wrap_funcB
    • 上一个项目符号也适用于funcA和a.cpp
  • 来自b.cpp的funcB
  • 来自a.cpp的funcA
  • 问题是步骤4中的代码。不在模块_b中,它将在运行时失败。但是事情有点奇怪:调用funcB时不会出现故障(核心转储),而是在模块(b-> _b)导入时出现(我认为这是由于Swig幕后的魔法所致),如下所示。

    输出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    [cfati@cfati-ubtu16x64-0:~/Work/Dev/StackOverflow/q050938128]> ls
    a.cpp  a.h  a.i  b.cpp  b.h  b.i  setup.py
    [cfati@cfati-ubtu16x64-0:~/Work/Dev/StackOverflow/q050938128]> python3 setup.py build > /dev/null 2>&1
    [cfati@cfati-ubtu16x64-0:~/Work/Dev/StackOverflow/q050938128]> echo $?
    0
    [cfati@cfati-ubtu16x64-0:~/Work/Dev/StackOverflow/q050938128]> 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
    [cfati@cfati-ubtu16x64-0:~/Work/Dev/StackOverflow/q050938128]> ls ./build/lib.linux-x86_64-3.5
    _a.cpython-35m-x86_64-linux-gnu.so  _b.cpython-35m-x86_64-linux-gnu.so
    [cfati@cfati-ubtu16x64-0:~/Work/Dev/StackOverflow/q050938128]> PYTHONPATH=${PYTHONPATH}:./build/lib.linux-x86_64-3.5 python3 -c"import _a;_a.funcA()"
    a.cpp 6 funcA
    [cfati@cfati-ubtu16x64-0:~/Work/Dev/StackOverflow/q050938128]> PYTHONPATH=${PYTHONPATH}:./build/lib.linux-x86_64-3.5 python3 -c"import _b"
    Segmentation fault (core dumped)

    为了解决它,可以:

  • 如您所指出,在模块_b中包含funcA(通过在ext_b的源文件列表中添加a.cpp)。这样,两个模块都是自包含的(来自funcA的PoV),每个模块都可以独立工作,但是funcA将在两个模块中重复
  • 使_b依赖于_a(毕竟,它们是共享对象)。但这不是Python扩展模块的使用方式,它在Win(以及某些Ux风格)上不起作用。因此,这更像是(lam)解决方法(gainarie)
  • 将a.cpp构建到另一个共享库(.so,但不是Python扩展模块)中,以供两个模块使用。不用说在运行时它们中的每个都将要求.so出现
  • 显然,对于选项3。是完美的候选人。但是distutils([Python 3]:API参考)没有提供所需的功能OOTB(显然,构建扩展模块和它依赖的外部共享库,不是distutils所针对的场景),或者至少,我找不到任何东西。有一个build_clib模块,它提供了构建静态库(供扩展模块使用)的功能,但与选项#1相同。

    setup.py:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    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]
    )

    笔记:

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    [cfati@cfati-ubtu16x64-0:~/Work/Dev/StackOverflow/q050938128]> ls
    a.cpp  a.h  a.i  b.cpp  b.h  b.i  setup.py
    [cfati@cfati-ubtu16x64-0:~/Work/Dev/StackOverflow/q050938128]> python3 setup.py build > /dev/null 2>&1
    [cfati@cfati-ubtu16x64-0:~/Work/Dev/StackOverflow/q050938128]> echo $?
    0
    [cfati@cfati-ubtu16x64-0:~/Work/Dev/StackOverflow/q050938128]> 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
    [cfati@cfati-ubtu16x64-0:~/Work/Dev/StackOverflow/q050938128]> 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
    [cfati@cfati-ubtu16x64-0:~/Work/Dev/StackOverflow/q050938128]> 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)
    [cfati@cfati-ubtu16x64-0:~/Work/Dev/StackOverflow/q050938128]> PYTHONPATH=${PYTHONPATH}:./build/lib.linux-x86_64-3.5 python3 -c"import _a;_a.funcA()"
    a.cpp 6 funcA
    [cfati@cfati-ubtu16x64-0:~/Work/Dev/StackOverflow/q050938128]> PYTHONPATH=${PYTHONPATH}:./build/lib.linux-x86_64-3.5 python3 -c"import _b;_b.funcB()"
    b.cpp 7 funcB
    a.cpp 6 funcA

    @ EDIT0:

    • 增加了对Win的支持(我想这对问题不是很重要)

      • 由于模块结构很简单,因此可以在setup.py的级别上进行所有操作(不必修改源文件)
      • 唯一的额外要求是swig.exe的目录应位于%PATH%中