有没有办法使用 SWIG C++ 创建一个 python 模块,它可以在 Python2 和 Python3 中导入

Is there a way to create a python module using SWIG C++ which can be imported in both Python2 and Python3

我正在编写一个 SWIG c++ 文件来生成一个 python 模块。我想让用户在 Python2 和 Python3 脚本中导入它。由于 SWIG 具有不同的绑定标志 Python2 和 Python 3,我想知道是否有一种方法可以为两者编写一个通用模块。

让我们稍微回顾一下,让 SWIG 本身不在讨论范围之内。

从历史上看,有必要为 Python 解释器的每个(有时甚至是次要的)版本更改编译一个 Python 模块。这导致了PEP-384, which defined a stable ABI for a subset of the Python C-API from Python 3.2 onwards. So obviously that doesn't actually work for your request, because you're interested in 2 vs 3. Furthermore SWIG itself doesn't actually generate PEP-384 compatible,代码。

为了进一步试验并更多地了解发生了什么,我制作了以下 SWIG 文件:

%module test
%inline %{
  char *frobinate(const char *in) {
    static char buf[1024];
    snprintf(buf, 1024, "World of hello: %s", in);
    return buf;
  }
%}

如果我们尝试用 -DPy_LIMITED_API 编译它失败了:

swig3.0 -Wall -python test.i && gcc -Wall -Wextra -I/usr/include/python3.2 -shared -o _test.so test_wrap.c -DPy_LIMITED_API 2>&1|head
test_wrap.c: In function ‘SWIG_PyInstanceMethod_New’:
test_wrap.c:1114:3: warning: implicit declaration of function ‘PyInstanceMethod_New’ [-Wimplicit-function-declaration]
   return PyInstanceMethod_New(func);
   ^
test_wrap.c:1114:3: warning: return makes pointer from integer without a cast
test_wrap.c: In function ‘SWIG_Python_UnpackTuple’:
test_wrap.c:1315:5: warning: implicit declaration of function ‘PyTuple_GET_SIZE’ [-Wimplicit-function-declaration]
     Py_ssize_t l = PyTuple_GET_SIZE(args);
     ^
test_wrap.c:1327:2: warning: implicit declaration of function ‘PyTuple_GET_ITEM’ [-Wimplicit-function-declaration]

即没有人拿起那张票,至少在我正在测试的 SWIG 3.0.2 版本上没有。

那么,我们将何去何从?那么 SWIG 3.0 documentation on Python 3 support 说了一些有趣的事情:

SWIG is able to support Python 3.0. The wrapper code generated by SWIG can be compiled with both Python 2.x or 3.0. Further more, by passing the -py3 command line option to SWIG, wrapper code with some Python 3 specific features can be generated (see below subsections for details of these features). The -py3 option also disables some incompatible features for Python 3, such as -classic.

我对该声明的理解是,如果你想生成可以用 2.x 和 3.x Python headers 编译的源代码,你只需要do 是在 运行 SWIG 时省略 -py3 参数。这似乎适用于我的测试,SWIG 生成的相同代码编译得很好:

$ gcc -Wall -Wextra -I/usr/include/python2.7/ -shared -o _test.so test_wrap.c 
$ gcc -Wall -Wextra -I/usr/include/python3.2 -shared -o _test.so test_wrap.c 
$ gcc -Wall -Wextra -I/usr/include/python3.4 -shared -o _test.so test_wrap.c

(注意 3.4 确实产生了一些警告,但没有错误并且它确实有效)

问题是任何给定版本的编译 _test.so 之间没有互换性。尝试从一个版本导入另一个版本将失败,并出现来自动态链接器的有关丢失或未定义符号的错误。所以我们仍然停留在最初的问题上,虽然我们可以为任何 Python 解释器编译我们的模块,但我们不能为所有版本编译它。

在我看来,处理这个问题的正确方法是使用 distuitls 并将您的模块安装到您想要支持的每个 Python 版本的搜索路径中任何给定的机器。

那是说我们可以使用一种解决方法。本质上,我们希望为每个解释器构建模块的多个版本,并使用一些通用代码在各种实现之间切换。假设您没有使用从 SWIG 的 -builtin 选项生成的代码进行构建,我们可以在两个地方尝试这样做:

  1. 在共享中object本身
  2. 在一些Python代码中

后者实现起来要简单得多,让我们看一下。我通过使用以下 bash 脚本为每个我打算支持的 Python 版本构建一个共享 object 做了一些事情:

#!/bin/bash

for v in 2.7 3.2 3.4
do
    d=$(echo $v|tr . _)
    mkdir -p $d
    touch $d/__init__.py
    gcc -Wall -Wextra -I/usr/include/python$v -shared -o $d/_test.so test_wrap.c
done

这构建了 _test.so 的 3 个版本,在以它们打算支持的 Python 版本命名的目录中。如果我们现在尝试导入 SWIG 生成的模块,它会报错,因为没有指示在哪里可以找到我们编译的 _test 模块。所以我们需要解决这个问题。共有三种方法:

  1. 修改生成的 SWIG import code。我没能在 SWIG 3.0.2 上完成这项工作——也许它太旧了?
  2. 修改Python搜索路径,在第二次导入前。我们可以使用 %pythonbegin:

    %pythonbegin %{
    import os.path
    import sys
    sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), '%d_%d' % (sys.version_info.major, sys.version_info.minor)))
    %}
    
  3. 编写一个 _test.py 存根,找到正确的存根,然后将它们切换过来:

    from sys import version_info,modules
    from importlib import import_module
    real_mod = import_module('%d_%d.%s' % (version_info.major, version_info.minor, __name__))
    modules[__name__] = modules[real_mod.__name__] 
    

最后两个选项中的任何一个都起作用并导致 import test 在 Python 的所有三个版本中都取得了成功。但是从源代码安装 3 次以上确实有点作弊而且要好得多。