1 背景
上星期申请了Prof. Anthoniou的实验室做HiWi,进入Vanpooling项目这个组,负责帮忙实现Enhancement Demand Model (Python
)和Scheduler (C++
)之间的交互。虽然说是交互,其实就是需要将Demand Model输出的结果作为函数参数通过一定的方式传入Scheduler中进行计算。期间尝试了多种方法:
- 利用SWIG来将写好的Scheduler翻译成
Python
的一个module,然后在Python
里直接调用。但是由于两种语言在各种数据类型的定义上存在区别,所以有一些参数很难用Python
定义的变量带入,比如说指针类型,甚至字符串类型。 - 利用
C++
的拓展包Boost.Python
来定义Scheduler需要的参数,这样就能够将Python
定义的变量直接使用
(同样需要用SWIG先翻译再导入到Python
中)。但是Boost
的安装摸了半天还是没能实现一个最起码的Hello, world!
程序,所以只好暂时先放放另谋出路,不过以后还是会继续学习这个包的。 - 利用
Python
里的ctypes
module来实现。因为仔细想了想,我们所需要做的像防御只是将Python
的数据类型转化为C++
所能接受的数据类型传入到Scheduler里进行计算。而ctypes
本来就是专门为此而生的,所以这是最简单直接的方法。
从而便花了一点时间去简单地学习了下如何利用ctypes
去在Python
中定义出可以传给C++
编写的程序做实际参数的变量。官方所给的说明文档虽然很好很全面,但是给的例子还是太少,好在还是发现了一篇总结得不错的,给出很多有用例子的博文A。
备注:以下操作所用工具MinGW
和Python3.6
2 利用ctypes
的大概流程
其实ctypes
是设计来方便Python
和C
之间的交互的,而不是用来实现Python
和C++
数据类型的转化的,但是C++
也是有方法将自身用C
来进行编译的,只需要简单的利用下面的语句就能够实现。
extern "C" { // 注意一下C好像必须要用大写
...这里是代码...
}
- 首先要用命令行工具
cd
到程序文件所在的文件夹。 - 然后,要将程序文件编译为
.dll
文件
- 如果用的是
C
来写的程序,假设程序名称为test.c
,则执行:
gcc -shared -fPIC test.c -o test.dll
- 如果用的是
C++
来写的程序,假设程序名称为test.cpp
,则执行:
g++ -shared -fPIC test.cpp -o test.dll
- 最后在
Python
的程序文件中将其导入即可。
from ctypes import *
test = CDLL('test.dll') #test便是程序的一个实例,可以对C或者C++程序里所定义的函数进行调用
3 一些数据类型的转化
当然,关键的还是如何在Python
里能够定义出C++
的数据类型以便于将其传入到C++
里进行计算。由于我们用到的是C++
的程序,所以下面的例子都是关于在Python
里实现C++
数据类型的实现的。
3.1 官方给出的基础转换表
一些基础数据类型可以直接对照下表利用ctypes
在Python
中定义出相应的C++
的数据类型 (C
语言和C++
的基础数据类型好像是一样的?)。
下面给出一个简单的例子。要先说明一个需要注意的问题是,在
Python
程序中调用函数时,我们要为手动为函数设定函数的参数类型,如果是有返回值的函数还需要设置返回值的类型,因为按照博文A的说法:
出现此问题的原因在于DLL文件无法在调用其中函数时自动设定数据类型,而如果不对类型进行设定,则调用函数时默认的输入、输出类型均为int。故如果函数的参数或返回值包含非int类型时,就需要对函数的参数以及返回值的数据类型进行设定。
而本人也尝试过,确实是需要的,否则会有错误提示。但是似乎如果是结构体变量,或者指针变量作为输入时则不需要。
test.cpp
#include
using namespace std;
extern "C" {
char py2c(int x, double y, char z);
}
char py2c(int x, double y, char z){
cout << x << " belong to int type!" << endl;
cout << y << " belong to double type!" << endl;
cout << z << " belong to char type!" << endl;
cout << "x + y = " <
test.py
from ctypes import *
test = CDLL('test.dll')
_x = c_int(1)
_y = c_double(1)
_z = c_char(b'z')
test.py2c.argtypes = (c_int, c_double, c_char)
test.py2c.restypes = c_char
test.py2c(_x, _y, _z)
3.2 字符串和字符串数组类型,其它的数据类型的数组以及二维数组
- 字符串,整型数组,浮点型数组
str1 = (c_char*3)() # 生成一个长度为3,且全部为None的字符串
str2 = (c_char*4)('a', 'b', 'c') #生成一个长度为4,且前三个字符为abc,最后一个为None的字符串
int1 = (c_int*5)()
float1 = (c_double*6)()
- 字符串数组、二维数组
其实字符串数组就是靠二维数组来实现的
(c_int*5)*3 # 生成5*3的二维数组
((c_int*6)*5)((1,2,3,4,5), (6,6,6), (7,7,7)) # 生成6*5的数组并进行初始化,其余未初始化的元素为0
((c_char*5)*3)() # 生成一个字符串数组!!!
字符串数组的赋值需要用到create_string_buffer
这个函数
strArr = ((c_char*5)*3)()
charList = ['aaa', 'bbb', 'ccc']
for row in range(3):
# create_string_buffer的第一个参数是赋值的内容,第二个参数是数组中每一个字符串的长度
# 而且这个数字必须和赋值的对象长度相同,比如这里根据定义strArr没一个字符串长度都是5,
# 所以create_string_buffer的第二个参数也必须是5
strArr[row] = create_string_buffer(charList[row].encode(), 5)
for row in strArr:
print(row.value.decode())
其中的encode
是用于将string转为bytes,而decode
则相反。
4 类方法的调用
这里完全是将博文A里的例子给搬过来了,因为觉得确实是个不错的方法。像下面这样定义函数就能够直接对hello()
函数进行调用了。
extern "C" {
void hello();
}
class Test{
public:
void hello();
}; // 别漏了分号
void Test::hello(){
printf("Hello!");
}
void hello(){
Test *testP = new Test;
testP->hello();
delete testP;
}
5 指针类型
ctypes
里有三个方法来构建一个变量的指针,分别是byref, pointer, POINTER
。其中byref,pointer
的对象都只能是ctypes
数据类型的,但是好像我用byref
来取结构体变量的地址的时候会报错,而用pointer
则没有报错,不知道为什么。如果使用的是pointer
,则在输出它所指地址内包含的值时必须先用contents
这个方法。'POINTER'则是用来创建一个地址类型的,而不是用来取址的。
x = pointer(c_int(3))
print(x.contents.value)
y = byref(c_int(4))
intPointer = POINTER(c_int())
6 结构体
这个就直接给例子吧,自己参悟。
test.cpp
#include
#include
struct Location {
int node;
};
struct Demand {
struct Location *pickup;
struct Location stops[5];
double real_distance;
};
typedef double SPEED;
typedef SPEED **TS;
extern "C"{
void handle(Demand *demand, TS ts);
}
void handle(Demand *demand, TS ts){
std::cout<<"Done!"<
test.py
from ctypes import *
class Location(Structure):
_fields_ = (
('id', c_int),
('node', c_int)
)
class Demand(Structure):
_fields_ = (
('pickup', POINTER(Location)),
('stops', Location*5),
('real_distance', c_double)
)
pickup_loc = Location(23)
_stops = (Location*5)()
demands = Demand(pointer(pickup_loc),_stops,10.2)
ts = c_double(45.6)
TSPointer = POINTER(c_double)
test = CDLL('test.dll')
test.handle(pointer(demands), TSPointer(TSPointer(ts)))
上面的例子是我从写的程序里删减部分后拷贝过来的,应该覆盖了大部分的转换需求,但是我也只是现学不久,可能有些地方有些问题。