Python3调用C/C++动态库(使用SWIG)

Posted Fu_Lin_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python3调用C/C++动态库(使用SWIG)相关的知识,希望对你有一定的参考价值。

SWIG文档官网

欢迎大家关注我的公众号,有更多精彩等着大家挖掘:

前言

  我之前写过一篇python3 ctypes模块使用方法与心得体会— int* ,char*等指针类型互转,如何转换对应的参数如,int*,char*等。

  今天发现如果使用C++的STL相关参数,无解啊~, 如下面提供的导出函数:

int parse(std::vector<char> & dump, std::vector<unsigned char> & dumpinfo);

  看到要用Python 传参 std::vector & 给C++接口,一阵头大,因为Ctypes已经完全不能满足这个需求了,查边全网,发现都是无解,要么就是自己定义一套基于ctypes的 std::vector * 方法来和python结合使用,这样太low,因为python传过去的只能是 void *, 然后各种中间转换,导出,麻烦至极,后面发现原来 swig 这个库,完全可以无缝调用C++的所有接口参数,在不改变原动态库的代码下,我打算自己再写一套基于swig编译的c++ 动态库,这个动态库就去加载调用我上面的C++的导出parse函数,等于就是包装了一层,因为so别人已经提供好了,你不能再让别人又下载基于swig的方式编译出so文件,所以只能自己中间编译一套swig的C++动态库来两方调用,下面看具体swig的具体使用,可以说完全比ctypes 好用太多了,因为不需要涉及到类型的转换,就和python内部使用一致!

下载安装SWIG 和 PCRE

  网上有直接 apt -get install swig 安装的,不过我这边是使用的源码安装,更加方便自己的版本控制!

Linux源码下载地址

  SWIG官网下载地址页面

  如果上面的地址打开缓慢,或者打不开,可以使用中国开源软件地址,如下:
sourceforge PCRE 全部版本

  sourceforge SWIG 全部版本

  下载最新的即可,不过看上面官网说是swig4.0.2对应pcre8.44, 我想8.45最新版也没啥问题的,不过我是通过官网地址下载的8.44

Linux安装 pcre 到用户指定目录

tar -jxvf pcre-8.44.tar.bz2 
chmod -R 777 pcre-8.44
cd pcre-8.44/
./configure --prefix=/usr/local/pcre/
make
sudo make install

安装好了以后,手动添加软链接

ln -s /usr/local/pcre/lib/libpcre.so /lib/libpcre.so
ln -s /usr/local/pcre/lib/libpcre.so.1 /lib/libpcre.so.1 

手动添加bin路径到profile文件

sudo vim /etc/profile

在最后添加一行:

PATH=/usr/local/pcre/bin:$PATH

保存后重新加载生效:

source /etc/profile

提示:
如果没有指定目录参数 –prefix 可以不用手动添加软件接以及bin路径,否则安装swig就会报错,如果报错不要慌,执行swig的

make clean

然后重新手动添加pcre的链接和路径位置,重新 configure 即可

Linux安装swig 到用户指定目录

tar -zxvf swig-4.0.2.tar.gz
cd swig-4.0.2
./configure --prefix=/usr/local/swig
make
sudo make install

如果pcre安装成功后,执行上面命令安装成功会提示如下

然后还是一样,手动添加bin路径到profile文件中去
打开profile文件

sudo vim /etc/profile

在最后添加一行:

PATH=/usr/local/swig/bin:$PATH

保存后重新加载生效:

source /etc/profile

关键步骤查看swig版本

成功与否就看这个命令了!查看swig版本

swig -version


如果发现不生效,直接将swig的bin目录加入到PATH变量中去

win系统的SWIG下载地址,免PCRE

swig官网地址

打开网址如果觉得慢,或者无响应。直接通过迅雷下载该链接(赋值连接,打开迅雷自动识别下载,秒下载好!)
下载地址:https://udomain.dl.sourceforge.net/project/swig/swigwin/swigwin-4.0.2/swigwin-4.0.2.zip

win系统版本安装解压缩swigwin-4.0.2.zip

会有个 swig.exe 文件,说明下载的zip是正确的

win系统swig配置环境变量

为了让路径不出现中文,防止意外情况,我直接复制到D盘的根目录下,然后添加系统环境如下:

win系统swig检测安装结果

允许Cmd命令,输入swig --help,如下图


说明win系统的swig安装成功!!!

SWIG的使用

swig介绍

SWIGSimplified Wrapper and Interface Generator 的缩写,可以用来给C/C++程序生成脚本语言的接口,这样就可以使用脚本语言去调用C/C++程序。

本文主要讲述使用SWIG给C++程序生成python3接口。

C++类导出示例讲解

首先编写一个简单的 类word
word.h如下:

#ifndef __WORD_H__
#define __WORD_H__

#include <string>

class word
{
public:
	word(std::string content);
	virtual ~word();

	virtual void updateWord(std::string new_content);
	std::string getWord(void);

private:
	std::string m_content;
};

#endif

word.cpp如下:

#include "word.h"

word::word(std::string content) : m_content(content)
{
}

word::~word()
{}

void word::updateWord(std::string new_content)
{
	m_content = new_content;
}

std::string word::getWord(void)
{
	return m_content;
}

然后编写 swig 的输入文件 example.i, 关键所在~ (打通C++和Python之间的桥梁文件)

/*--- File: example.i ---*/
%module example
%{
#include "word.h"	
%}

%include "std_string.i"
%include "word.h"

下面是简单解释:

  • 第一行 是注释,表示文件名字
  • 第二行 表示模块名字叫 example,后面使用python调用模块时就是import这个模块名
  • %{…%} 里包含的是要被wrap的C/C++函数、类或者变量的声明,也可以使用头文件,因为头文件里一般都是声明,这部分内容会被直接拷贝到swig生成的wrap文件里
  • 最后2行 的%include是swig的专有指令,std_string.i 是用来处理C++的string类,而出现的word.h,是用来告诉swig对word.h里声明的内容进行分析并生成对应的wrapper

以上三个文件放在同一目录下:(仅仅演示Linux系统的,生成和执行命令在Win上也是一样的)
Linux系统文件如下:

然后在当前文件夹下打开 CMD 或者 PowerShell 输入以下命令:

swig -c++ -python example.i

会生成 example.pyexample_wrap.cxx,如下:

然后输入以下个命令:

linux下:

g++ -fpic -c example_wrap.cxx word.cpp -I/usr/local/python3/include/python3.6m

如果是在 win系统下

g++ -fpic -c example_wrap.cxx word.cpp -ID:\\Python37\\include

切记D:\\Python37\\include路径中间不能有空格,否则会报错

其中的 -I 是为了引入 Python.h ,这个文件可以通过 sudo find / -iname “Python.h” 来进行查找,注意如果想以后用python2去调用C++程序,那么就要引入python2的Python.h,这里引入的是python3的Python.h。

接着输入下面的命令,生成动态库:

g++ -shared example_wrap.o word.o -o _example.so

最终生成的文件如下

在该目录下使用python3按照如下操作进行测试:

测试OK。经过上述操作后,实际有用的文件是 _example.so和example.py

可以把这2个文件单独拷贝到别的路径下,然后再进行之前的python3测试,也是OK的。

测试C导出函数

我准备在word中新增加C导出函数看怎么样?
word.h中:

word.cpp

然后重新执行上面的命令

swig -c++ -python example.i
g++ -fpic -c example_wrap.cxx word.cpp -I/usr/local/python3/include/python3.6m
g++ -shared example_wrap.o word.o -o _example.so

在该目录下使用python3按照如下操作进行测试:

可以看到,通过引用的模块,也是可以直接调用导出的C方法,太nice了!!!而且不需要任何的变量转换,但是方法不能直接调用,看上面也知道报错了,必须通过导入的模块 example.xxx 来调用。

C++处理函数参数

C函数参数就不用多说了,全是基础类型,而C++函数参数有很多种,所以下面分3个情况分别进行分析

C/C++常规类型

常规类型如 int,double,char 等等,比较简单,传参数直接传值就可以了(常规类型是适用于所有C导出函数的,上面也测试了~)。我们给class word增加一个方法 testFunc(),其参数有int,double,char

word.h如下,注意引入了iostream:

#ifndef WORD_H
#define WORD_H

#include <string>
#include <iostream>

extern "C"{
    void foobar(int i);
}
class word
{
public:
	word(std::string content);
	virtual ~word();

	virtual void updateWord(std::string new_content);
	std::string getWord(void);

    int testFunc(int data1, double data2, char data3); // new added
private:
	std::string m_content;
};

#endif // WORD_H

word.cpp 如下,

#include "word.h"

word::word(std::string content) : m_content(content)
{
}

word::~word()
{}

void word::updateWord(std::string new_content)
{
	m_content = new_content;
}

std::string word::getWord(void)
{
	return m_content;
}

void foobar(int i)
{
  printf("Printing from mylib.so %d\\n", i);
}

int word::testFunc(int data1, double data2, char data3)
{
    std::cout << "data1: " << data1 << "\\n";
    std::cout << "data2: " << data2 << "\\n"; 
    std::cout << "data3: " << data3 << "\\n";

    return 100;
}

example.i 内容如下:

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

%include "std_string.i"
%include "std_iostream.i"
%include "word.h"

按照上一节的步骤进行操作生成wrapper,然后输入终端下输入python3去测试,

引用类型

对于引用类型的参数,该怎么办呢?假设要wrapp的类如下:
word.h如下: 新增testReference 方法

class word
{
public:
	word(std::string content);
	virtual ~word();

	virtual void updateWord(std::string new_content);
	std::string getWord(void);

    int testFunc(int data1, double data2, char data3); 

    void testReference(int& data);// new added
private:
	std::string m_content;
};

word.cpp如下,

void word::testReference(int& data)
{
    data = 100;
}

example.i 的内容如下,

/*File: example.i*/
%module example
%include typemaps.i
%apply int &OUTPUT {int &};
%{
#include "word.h"
%}

%include "std_string.i"
%include "std_iostream.i"
%include "word.h"

%include typemaps.i 是引入swig的类型映射,%apply int &OUTPUT { int &}; 是用来处理int的引用类型。

然后执行之前的编译语句:

swig -c++ -python example.i
g++ -fpic -c example_wrap.cxx word.cpp -I/usr/local/python3/include/python3.6m
g++ -shared example_wrap.o word.o -o _example.so"

ok后,编写python脚本test.py(注意,test.py是要跟examle.py和_example.so在同一目录下),如下

import example

obj = example.word("test")
ret = obj.testReference()
print('ret is {}'.format(ret))

然后执行 python3 test.py,得到运行结果:

可以看到引用类型的值作为函数返回值返回了,这样就可以拿到其值了。

细心的同学可能发现test.py里调用testReference()时没有传递参数,这是为什么呢?

回顾下testReference()的c++实现,对于data这个引用类型变量,我们只是对其赋值,并没有操作其原本的值,意思也就是data只作为输出使用:赋值后,testReference()的调用者就可以拿到data的新值。这样在test.py里调用testReference()时就不需要传递参数了,只需要接受其返回值就行了。

这也是example.i%apply int &OUTPUT { int &}; 语句中OUTPUT的由来。

那如果我们在c++函数里要操作引用类型的变量值怎么办(这是很普遍的用法),如下:

void word::testReference(int& data)
{
    data += 100;
}

data自加100
那么 example.i 的写法就需要改改:

/*File: example.i*/
%module example
%include typemaps.i
%apply int &INOUT { int &};
%{
#include "word.h"
%}

%include "std_string.i"
%include "std_iostream.i"
%include "word.h"

由之前的OUTPUT改为INOUT,因为我们既要使用data的原始值,又要获取函数调用后的data值,那么data就既有输出又有输入功能了。

写好 example.i 后再次运行前面的编译命令。

test.py脚本如下:

import example


obj = example.word("test")

data = 70
ret = obj.testReference(data)

print('ret is {}'.format(ret))

这个时候就需要传参数了。运行如下:

如果函数的返回类型不是void怎么办,那么该返回哪个呢?假设testReference()如下:

int testReference(int& data);

word.cpp实现如下:

int word::testReference(int& data)
{
    data += 100;
    
    return 500;
}

example.i 不变,test.py里修改打印,

import example

obj = example.word("test")

data = 70
ret = obj.testReference(data)

print(ret)

最后执行 python3 test.py,结果如下:

可以看到swig使用了python的list很好的处理了这种情况。

本小结以int举例,其它类型也是类似的

指针类型

有了上节对引用的了解,这节对于指针就很容易理解了,只是把&换成*
这里就只讲一个例子,假设要wrap的c++类如下,还是使用上面的例子,新增testPointer方法:
word.h如下

class word
{
public:
	word(std::string content);
	virtual ~word();

	virtual void updateWord(std::string new_content);
	std::string getWord(void);

    int testFunc(int data1, double data2, char data3); 

    int testPointer(int* data);// new added
private:
	std::string m_content;
};

word.cpp如下,

int word::testPointer(int* data)
{
    *data += 100;
    
    return 500;
}

example.i 的写法如下(只是把&换成了*):

/*File: example.i*/
%module example
%include typemaps.i
%apply int *INOUT { int *};
%{
#include "word.h"
%}

%include "std_string.i"
%include "std_iostream.i"
%include "word.h"

执行之前的三行编译命令,ok后编写python脚本test.py,如下

import example

obj = example.word("test")

data = 200
ret = obj.testPointer(data)

print(ret)

然后执行python3 test.py,结果如下:

本小节以int举例,其它类型也是类似的。

处理STL

这里只讲下std::string和std::vector,其它的可以去swig官网教程去查看或者谷歌查查。

std::string

其实前面的例子也讲到过 std::string 的swig处理,这里就再重新简单讲下,重新设计word类如下:
word.h代码如下:

#ifndef WORD_H
#define WORD_H

#include <string>

class word
{
public:
	word();
    std::string testFunc(std::string& data);
};

#endif // WORD_H

word.cpp代码如下:

#include "word.h"

word::word()
{
}

std::string word::testFunc(std::string& data)
{
    return data + "123";
}

example.i 内容如下:

/*--- File: example.i ---*/
%module example
%include "std_string.i"
%apply std::string &INOUT {std::string&}
%{
#include "word.h"	
%}

%include "word.h"

这里使用了**%include std_string.i去处理std::string**,将其转为python的字符串类型。如果参数不是引用,那么就不需要 %apply std::string &INOUT {std::string&} 这句话了。

执行之前的三行编译命令,ok后编写python脚本test.py,如下,

import example

obj = example.word()

ret = obj.testFunc("abc")
print(ret)

执行python3 test.py,输出如下:

std::vector

vector是个模板,实例化时需要指定类型,这里以int和unsigned int举例,

word.h如下

#include <vector>

class word
{
public:
	word();
	~word();
    int testFunc(const std::vector<int>& data1, const std::vector<unsigned int>& data2);
};

word.cpp如下


word::word()
{
    // TODO: constructor
}

word::~word() 
{
    // TODO: destructor
}

int word::testFunc(const std::vector<int>& data1, const std::vector<unsigned int>& data2)
{
    if (data1.size() == 3)
        return data1[0]+data2[0]+data1[1]+data2[1]+data1[2]+data2[2];
    
    return 0;
}

example.i 内容如下,

/*--- File: example.i ---*/
%module example
%{
#include "word.h"
%}

%include "std_vector.i"

namespace std {
    %template(VectorInt) vector<int>;
    %template(VectorUInt32) vector<unsigned int>;
}

%include "word.h"

这里使用了 %include “std_vector.i” ,用来处理std::vector,另外对需要用到的 vector 类型进行模板处理,用到哪种就要声明哪种,本例程使用了int和unsigned int

执行之前的三行编译命令后,编写测试脚本test.py如下,

import example

obj = example.word()

#data1 = [1,2,3]
#data2 = CMake、SWIG 和共享库

在 perl 中调试由 SWIG 包装的共享库

Python与C/C++互操作

用Swig将c/c++程序转为java代码(使用swig实现java调用cc++的方法)

SWIG 的应用

如何使用 SWIG 从 C++ 调用 Java?