Python 外部函数库ctypes

我比较习惯使用c++,但是最近遇到需要读取数据后画图的问题,我是以对象的形式保存数据的,可以把数据存到文本文档中,然后再用python或matlab读取画图,但是这也太麻烦了,不想这么干(大量的数据),于是就想能不能把c++程序封装成模块,然后以python为主体去调用它们。最后发现了这个库:ctypes。它是python的外部函数库,里面包含了C/C++的很多数据类型,我们可以利用这个库来调用编写好的C/C++模块(通常这些模块要编译成动态链接库供python使用)。下面对使用ctypes的方法进行简单的介绍。


1. 对于C/C++程序的要求

C:没什么要求,正常写就行。 C++:要在源文件的函数定义之前,用extern "C" {}对定义的函数进行声明,因为[1]python的ctypes可以调用C而无法调用C++代码,加了extern "C"之后会让编译器按照C语言而不是C++的方式进行编译。C++代码可以使用类等数据结构或C++头文件,只不过函数在定义前必须要提前声明在extern "C"{}里边。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// filename = test.cpp
#include<iostream>
using namespace std;

//要在extern里对所有定义的函数进行声明
extern "C"{
    void func();
}

void func()
{
    cout<<"hello !"<<endl;
}

2. 生成动态链接库

对于C++源文件,我使用g++编译器进行编译(对C源文件则使用gcc),编译动态链接库时必须给编译器指明(可能用到的)库文件、头文件目录。。

动态链接库的文件名最好以“lib”开头[1],原因是gcc/g++编译选项-l后面只能接动态链接库文件名“libXXX.so”除去“lib”和扩展名之后的部分,比如g++ test.cpp -lXXX。但也不是必须的,若不以“lib”开头,则编译选项只能用-L指定库的路径,例如g++ test.cpp -L ./libXXX.so。当然,和普通编译程序一样。

综上所述,若此动态链接库将来不会用于gcc/g++的-l选项编译过程,则此库的文件名没有限制。

例如,以下命令对test.cpp源文件进行编译,输出动态链接库.so文件。

1
g++ test.cpp -fPIC -shared -o libtest.so

3. 对于python程序的要求

若要调用动态链接库libtest.so内的函数,则首先应import库。然后使用cdll对象的加载库方法,加载动态链接库到libc对象,通过libc对象的方法便可以调用到动态链接库中的函数[2]

1
2
3
from ctypes import *
libc=cdll.LoadLibrary("./libtest.so")
libc.func()

将会输出结果

1
hello !

4. 使用ctypes时python与C/C++程序互相传递参数

上面是最简单的调用C++函数的例子,若调用过程有参数传递,则在python程序中应注意数据类型的规范定义。因为python默认函数的参数类型和返回类型为 int 型[3],所以与外部函数传参、返回值时,变量必须用ctypes数据类型来明确定义,常用的ctypes数据类型见官方文档。C/C++代码仍按照自己的方式正常传递参数。下面我来结合例子进行详细说明。

例子 C++源文件

在test.cpp中定义了两个函数,分别传递整形和整形指针参数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include<iostream>
using namespace std;
extern "C"{
    int add(int a, int b);
    void reverse(int *a);
}

int add(int a, int b)
{
    cout << "a+b=" << (a + b) << endl;
    return (a+b);
}

void reverse(int* a)//取相反数
{
    *a = -(*a);
}

用以上源文件生成动态链接库libtest.so

例子 python代码

下面是python代码及程序运行结果。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from ctypes import *
def func1(a, b):
    libc = cdll.LoadLibrary("./libtest.so")
    libc.add.restype = c_int
    num = libc.add(a, b)
    print(num)
def func2(a):
    libc = cdll.LoadLibrary("./libtest.so")
    a = (c_int)(a)
    a_p = POINTER(c_int)(a)
    libc.reverse(a_p)
    return a.value

if __name__ == "__main__":
    # print(func3())
    func1(2,3)
    print(func2(1123))

输出结果为

1
2
3
a+b=5
5
-1123

ctypes外部函数的参数及返回值类型声明

对于外部函数的输入参数,则要用argtypes方法和参数类型的列表或元组(元组效率比列表高)来定义,例如

1
2
# 这里动态链接库加载为了library对象
library.func2.argtypes = (c_float, c_double)

[3]对于外部函数的返回值,用外部函数的restype方法进行数据类型的定义,如

1
library.func1.restype = c_int

ctypes数据类型的规范定义

对于将要传递给外部函数的变量,用ctypes数据类型对变量进行定义,形式类似于强制类型转换,例如定义C浮点型变量1.234:a = c_float(1.234),通过ctypes变量的value方法访问、赋值变量的值:a.value = 2.345print(a.value)

数组

对于数组,直接使用元素数据类型*n即可,例如,array = c_int * 4

字符串

对于字符串,C语言函数所需的参数一般是字符指针,所以需要把python中的字符串(对象)转换成C语言的字符串(字节串)再进行参数传递。有两种方法实现,既可以使用python内置的bytes()对象[4],也可以使用python内置的字符串之encode()方法[5]

1
2
3
4
//  C/C++函数声明
extern "C"{
    void fun(char * a);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# python代码
from ctypes import *
# 这里动态链接库加载为了library对象
str = "this is a test string"

# 1. 用bytes()对象,并设定好字符编码
str = bytes(str, "utf-8")
ptr = c_char_p(str)
library.func(ptr)

# 2. 用encode()方法,encode默认编码模式为utf-8
library.func(str.encode("utf-8"))

利用第一种方法,把不同字符串生成的字符指针,再用字符指针数组进行声明,就可以传递字符串数组。

1
2
3
extern "C"{
    void test(char *b[2]);
}
1
2
3
4
5
6
str1 = "good job."
str2 = "hello, world"
str1 = c_char_p(bytes(str1, "utf-8"))
str2 = c_char_p(bytes(str2, "utf-8"))
ptr = (c_char_p * 2)(str1, str2)
library.test(ptr)
结构体

对于结构体,必须定义一个继承自ctypes中Structrue的类,类中必须含有_fields_属性,它是一个二元组构成的列表,每个二元组为(field name, field type),C的结构体中所有成员变量都以此种方式进行声明。该子类还有可选的一个_pack_属性,用于规定实例中结构体的字节对齐方式。注意,把结构体向外部函数传参时,应该总是通过指针传递。

1
2
3
4
5
6
from ctypes import *
class point(Structure):
    _pack_ = 1 #按1个字节对齐
    _fields_ = [("a", c_int),
                ("b", c_double),
                ("c", c_char)]
指针

对于指针型变量,见下面的表格。

ctypes内的函数 说明
byref(obj, offset) 返回obj的地址(指向obj的指针),obj为一个ctypes数据类型对象,offset为地址偏移量。该方法生成的指针只能作为参数来调用外部函数。
pointer(obj) 返回一个指向obj的指针实例。
POINTER(type) 返回一个指向type类型的指针类型,type必须是ctypes数据类型。

后两个的联系为,pointer(obj) == POINTER(obj_type)(obj)。如果只是想生成一个调用外部函数所需要的指针,则使用byref函数最合适,它构造起来最快,但返回的指针无法用于其他用途。

1
2
3
4
5
6
7
8
from ctypes import *
a = c_float(3.14) # 生成一个ctypes类型的对象
b = byref(a,0)
c = pointer(a)
d = POINTER(c_float)(a)
print(b)
print(c)
print(d)

运行结果为

1
2
3
<cparam 'P' (0x7fe8bda74c48)>
<__main__.LP_c_float object at 0x7fe8bda919d8>
<__main__.LP_c_float object at 0x7fe8bda919d8>

  1. https://www.it610.com/article/1295144844422881280.htm

  2. https://www.bilibili.com/video/BV1kJ411a7AD?t=768

  3. https://www.cnblogs.com/night-ride-depart/p/4907613.html

  4. https://docs.python.org/zh-cn/3/library/stdtypes.html?highlight=bytes#bytes

  5. https://docs.python.org/zh-cn/3/library/stdtypes.html?highlight=encode#str.encode

updatedupdated2021-01-062021-01-06
加载评论