使用SWIG编写C/C++代码的Python接口并使用distutils进行连接静态库编译

前言

          本文主要分成两个部分,分别介绍SWIG的使用和setup.py脚本编写过程中遇到的一些坑,尽量使后人少跳这些坑:

1. SWIG的部分

1.1 SWIG是什么

     swig是一个代码包装器,为C/C++的代码提供python等脚本语言的接口代码封装,简单来讲就是你用C/C++写的函数,swig可以帮你生成链接库文件和.py文件提供给python进行使用。其实看他生成的wrapper包装文件代码来看,用的应该还是python的C API,当然自己写也可以,不过会很繁琐,而SWIG大大简化了这个过程,所以很多人用,本文重点介绍SWIG的使用。

注意:swig的多模块的编写相对而言比较复杂,本文暂只针对单模块(即只使用一个swig的.i文件,后续可能会考虑多个.i文件?)的情况。针对swig的文档实在是太多(PDF版本是653页)的情况,推荐先只看Introduction部分,看完了能知道大概是个怎么回事儿,然后再针对需要生成的语言的接口看(例如,本文的部分内容来自于SWIG and Python这章)

1.2 SWIG的使用

SWIG的使用,根据官网例子总结一下,我们只需要完成以下三个步骤:
1. 封装一个example.h和example.cc文件,在这两个文件中选择暴露给Python的C++接口
2. 编写针对上面的接口的SWIG的.i文件
3. 运行SWIG命令行生成对应的exampl_wrap.cxx文件
接下来你可以自己手动使用GCC、VS等编译器进行手动的编译、连接等等,但是官网推荐的针对Python接口做法是使用python的distuils写一个安装脚本setup.py进行编译连接。这最后一步我们留在下一部分讲解。
现在我们就以一个简单的例子看一下吧,其实最主要的是.i文件。

1) 首先是暴露接口,编写头文件和实现文件

定义类头文件

/* File: example.h */
int fact(int n);

以及相应的实现文件

/* File: example.c */
#include "example.h"
int fact(int n) {
    if (n < 0){ /* This should probably return an error, but this is simpler */
        return 0;
    }
    if (n == 0) {
        return 1;
    }
    else {
        /* testing for overflow would be a good idea here */
        return n * fact(n-1);
    }
}

这一步没什么好说的,所以简单就略过了。

2) 定义相应的SWIG接口配置文件example.i

/* File: example.i */
%module example
%{
#define SWIG_FILE_WITH_INIT
#include "example.h"
%}

int fact(int n);

这一步是我们第一部分的重点。.i文件首先以%module开头,定义module名称,这也是我们在python里面import 进来的时候import的名称(以这个例子为例就是example)。接下来是一串%{%},在这个里面的代码SWIG不会进行翻译,而是会直接照抄在生成的example_warp.cxx中,所以这一部分一般就放.cxx文件开头需要用到的头文件、函数声明等等。
接下来定义需要暴露给Python的接口(也就是在python中module可以调用的接口)。在.i文件中,使用%开头表示这是一条SWIG的指令,例如%include,%template等等。
一种更简单的写法是将需要暴露的接口和头文件定义、函数声明等等都放到一个.h当中,这样可以使得.i文件写起来更简单,如下定义了一个类以及他需要暴露给Python的接口:

/*example.h*/
#include <iostream>
using namespace std;
class Example{
    public:
        int fact(int n);
};

/*example.cpp*/
#include "example.h"
int Example::fact(int n){
   if (n < 0){ /* This should probably return an error, but this is simpler */
        return 0;
    }
    if (n == 0) {
        return 1;
    }
    else {
        /* testing for overflow would be a good idea here */
        return n * fact(n-1);
    }
}

则编写example.i可以变得更加简单:

%module example
%{
#include "example.h"
%}
%include "example.h"

这一块SWIG的文档实在是零零散散而且很长很长,网上有一个精简版文档的覆盖了绝大多数功能的版本,我个人也是看了很多这个文档里面写的东西。值得注意一看的是SWIG提供了自身的很多诸如std_vector.i和std_string.i等库文件,用以支持常用的包装,如数组,标准库等。如果使用了这些标准库,需要引用SWIG的库文件。不过vector等模板的使用需要先实例化一下。

%module example
%include "std_vector.i"
namespace std {
%template(vectori) vector<int>;
%template(vectord) vector<double>;
};

通常我们也喜欢在Python代码中插入注释内容,这在.i文件中也可以实现,具体的做法是使用Doctring features
更多的讲解篇幅有限,所以没有覆盖到的内容可以去官网上搜索,swig现在主要有1.3、2.0、3.0三个版本,我个人觉得好像没有看到什么大的变化:
http://www.swig.org/
http://www.swig.org/Doc2.0/Python.html
P.S. 顺便看看SWIG上面的主要贡献者,国人贡献还是比较少…其实实在写不来代码,搞搞翻译也是很大的贡献量啊,600多页的英文虽然都没啥有难度的词语,但是看着还是挺心烦的:)

3) 运行swig的命令生成对应的python c api包装文件:

$ swig -c++ -python example.i

这一步我没遇到什么问题,执行完这一步之后,我们会得到一个SWIG生成的xxx_wrap.cxx的低级包装代码,以及xxx.py作为高级包装代码提供给python使用,在这个例子中则是example_wrap.cxx和example.py。
打开example_wrap.cxx可以看到其使用python C-API编写,并且真正实现了的,只含有你在.i文件中配置过的暴露的接口都在static PyMethodDef SwigMethods[]里面,没定义的东西就不会在这个cpp里面实现,这也就为我们下一部分埋下了一个坑:如果我们使用了很多不想暴露给python做接口的其他cpp文件的函数,或者使用了第三方库的话,应该怎么做呢?

到这里,其实SWIG的工作就已经完成了,后面你可以采用python的disutils进行扩展模块的生成,也可以选择自己写makefile或者其他的编译配置文件使用gcc或者vs进行扩展模块的编译生成。这二者的区别跟SWIG已经没啥关系,虽然SWIG上推荐的是使用python的distutils编写setup.py来进行编译,编写makefile来编译可以参考这篇文章。本文中选择使用python的distutils工具完成,进入第二部分。

2. 使用distutils的setup.py的编写部分

那么我们开始进入第二部分,如何从刚才得到的xxx_wrap.cpp和xxx.py生成我们想要的python扩展包_xxx_so(windows下面是xxxx.pyd)。首先还是以SWIG的官方例子入手,编写一个setup.py:

#!/usr/bin/env python
"""
setup.py file for SWIG example
"""
from distutils.core import setup, Extension
example_module = Extension('_example',
sources=['example_wrap.cxx', 'example.cpp'],
)
setup (name = 'example',
version = '0.1',
author = "SWIG Docs",
description = """Simple swig example from docs""",
ext_modules = [example_module],
py_modules = ["example"],
)

在这个例子中,example_module = Extension(....)创建了一个Extension,定义了extension的名称为_example,也就是说编译目标是_example.so,之所以要加下滑线是因为SWIG的python扩展规定要这么干:
When linking the module, the name of the output file has to match the name of the module prefixed by an underscore. If the name of your module is "example", then the name of the corresponding object file should be "_example.so" or "_examplemodule.so ". The name of the module is specified using the %module directive or the -module command line option.
然后接下来规定需要使用的源文件,一般来讲是你自己写的C/C++的包装接口,以及SWIG生成的wrap的C/C++接口文件。
然后执行以下指令:
python setup.py build_ext --inplace
然后在当前目录下应该就会生成一个.so或者.pyd:
图片1
然后就可以在python中进行import以及执行了:
图片2
拆解命令行:
- python 用你想要构建的python版本执行
- setup.py 安装脚本
- build_ext 编译扩展
- –inplace 表示将编译好的扩展文件就放在源代码下面(否则默认会放在build的目录下)
以上就是SWIG的官方的文档中写的如何使用distutils的例子了,但是他写的毕竟很简单,并没有涉及很多复杂的情况:比如我有多个源文件,再比如我甚至有第三方库,再比如我的第三方库里面有静态库也有动态库,这些都没有写到,也就是本文下面即将讲到的事儿。

下面开始详细说一下python的这个setup.py的文件的Extension部分的编写,主要涉及到有其他头文件和库文件的情况。首先两个放出python的官方链接:
1. https://docs.python.org/2/distutils/
2. https://docs.python.org/2/distutils/apiref.html#module-distutils.extension

不过上面举的例子都很少,而且也没有举出具体的包含其他头文件和库文件的例子,针对上面第一部分的问题,我也不卖关子了,直接给出方法:

#coding = utf-8
'''
现在有一个C++工程,包括多个.cpp和.h文件,其中,假设example.cc是预备暴露给python的接口源文件(example.i对应的函数实现源文件),但是其实现使通过调用other-cpp-wrapper.cc里面的方法,而example_wrap.cxx是SWIG根据example.i生成的包装源文件并且使用了外部的.a 静态库,也就是说example_wrap.cxx并不包含也不知道other-cpp-wrapper.cc里面的任何方法实现。
'''
from distutils.core import setup, Extension
example_module = Extension('_example', # 模块名称,和example.i中的%module对应加下划线
            include_dirs = ['../libpath1/include',#头文件搜索路径,include搜索路径,多个路径逗号分隔
                        '../libpath2/include',
                        '../libpath3/include',
                        './'],#如果用到了当前目录下的其他头文件,则包含当前路径
            library_dirs = ['./'],#动态库搜索目录,linux下动态库文件搜索路径需要,由于本次使用的都是静态库所以不用,windows平台下所有库搜索路径都应该放到libraries里面,多个路径逗号分隔
            libraries = [], # 使用到的so动态库名字。库文件都是以lib开头的,都是libxxxx,理论上只需要填写去掉lib的部分,也就是只写xxxx就行,多个库文件逗号分隔
            sources = ['example_wrap.cxx','example.cc','other-cpp-wrapper.cc'],#编译源文件,如果当前工程包含多个源文件必须把所有源文件都放进来,否则编译成功后在python中import时会报找不到other-cpp-wrapper.cc中的方法的连接错误。
            extra_objects = ['../libpath1/lib/lib1.a',#注意:Linux的用到的静态连接库应该放到这个参数中,填写完整路径才行,不能只填写库名称(和动态库有区别)
                            '../libpath1/lib/lib2.a',
                            '../libpath1/lib/lib3.a']
            )
setup(name = 'example', #定义模块基本信息
      version = '0.1',
      author = 'Arthur',
      description = """Simple test demo""",
      ext_modules = [example_module],
      py_modules = ["example"],
      ) 

到目前为止,这个setup.py就是比较完整可用了。注意,完整的Extension可用的参数包括如下参数:
这里写图片描述
其中,可能有人看到了extra_compile_args和extra_link_args,没错,是可以用的,不过最好不要想着直接加-static来进行静态库的连接……90%的情况下会报各种编译连接错误(因为-static加了之后会强制全部使用静态库吧,而这可能并不是我们想要的效果)

最后值得注意的一点是,如果使用了外部动态库,那么要把库文件的位置告诉系统,否则运行时是找不到这些库的,也就是在运行之前必须先添加:
export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:使用到的动态库的路径

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值