ctypes库的使用

C+Python混合编程

Posted by Welt Xing on October 19, 2021

引言

Python简单明了,易于理解,但这样的优点带来的是速度上的缓慢。在这样的背景下,我们想在Python程序中插入C,已进行一些耗时的计算任务,以实现速度的提升。本文主要介绍ctypes库的使用方法,为后面的工作提供参考。

背景知识

ctypes 是 Python 的外部函数库。它提供了与 C 兼容的数据类型,并允许调用DLL或共享库中的函数。可使用该模块以纯 Python 形式对这些库进行封装。

动态链接库是一种不可执行的二进制程序文件,它允许程序共享执行特殊任务所必需的代码和其他资源。

准备工作

首先,我们需要有Python解释器ctypes是Python的标准库;此外,由于我们需要生成动态链接库,一个C编译器也是必须的。为了简单期间,笔者选择在Linux平台上实验后面的代码。

sudo apt install python3
sudo apt install gcc
sudo apt install g++

第一个Python/C程序

先写一个简单的C文件:

// test.c
#include <stdio.h>

int print_hello(const char* n) {
    printf("hello %s!\n", n);
    return 0;
}

将其编译成动态链接库文件:

gcc -fPIC -shared -o test.so test.c

这里-fPIC是指生成位置无关的代码,则产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加载器加载到内存的任意位置,都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的;-shared表明产生共享库;-o test.so表明生成的目标文件名为test.so

Windows系统中的动态链接库文件后缀名为dll,Linux下为so

我们用ctypes加载动态链接库,然后就可以调用其中的函数了:

from ctypes import *
lib = CDLL('./test.so')
lib.print_hello(b'world')

输出hello world!

我们这里输入参数是b'world'而不是简单的world,这是因为Python3的字符串的编码语言用的是unicode编码,由于Python的字符串类型是str,在内存中以Unicode表示,一个字符对应若干字节。而在C中,字符串是以一个字符一个字节,所以需要进行转换。

由于我们在C程序中包括了stdio.h,我们甚至可以调用printf等库函数:

lib.printf(b'hello %s, a number %d', b'world', c_int(12))

等价于

printf("hello %s, a number %d", "world", 12);

时间比较

我们来比较下纯Python和C+Python下运行速度的差别,在test.c中定义C函数add_test,用于测试两个数的加法:

int add(int x, int y) {
    return x + y;
}

void add_test(int N) {
	for(int i=0; i < N; i++) {
        add(i, i + 1);
    }
}

对应的在Python中定义add函数,计算多次循环时间:

from ctypes import *
from time import time

N = 100000
lib = CDLL("test.so")

def add(x, y):
    return x + y

def add_test(N):
    for i in range(N):
        add(i, i + 1)

# Python测试
t = time()
add_test(N)
t_python = time() - t

# C测试
t = time()
lib.add_test(c_int(N))
t_c = time() - t

print("{} times add:\nPython : {}\n     C : {}".format(N, t_python, t_c))

执行

gcc -fPIC -shared -o test.so test.c && python3 test.py

输出

100000 times add:
Python : 0.014992952346801758
     C : 0.0004227161407470703

可以发现,C和Python的混合编程可以大幅度提高程序的速度。

一些高级话题

基本数据类型

数据类型是沟通两种语言的基石,ctypes下定义了与C兼容的数据类型。

ctypes 类型 C 类型 Python 类型
c_bool _Bool bool
c_char char 单字符字节对象
c_wchar wchar_t 单字符字符串
c_byte char 整型
c_ubyte unsigned char 整型
c_short short 整型
c_ushort unsigned short 整型
c_int int 整型
c_uint unsigned int 整型
c_long long 整型
c_ulong unsigned long 整型
c_longlong __int64long long 整型
c_ulonglong unsigned __int64unsigned long long 整型
c_size_t size_t 整型
c_ssize_t ssize_tPy_ssize_t 整型
c_float float 浮点数
c_double double 浮点数
c_longdouble long double 浮点数
c_char_p char * (以 NUL 结尾) 字节串对象或 None
c_wchar_p wchar_t * (以 NUL 结尾) 字符串或 None
c_void_p void * int 或 None

我们要明确,Python的数据类型和C的数据类型有很大不同,进一步说,Python中的数据类型,比如intfloat等,是一种“类”,是面向对象式且抽象的,而C中的数据,往往具象到内存上,比如int类型就对应内存上的4字节。

设置函数参数与返回值

在前面我们可以看到,由于Python中的数据类型与C中的差别,我们无法将Python中的数据直接作为C函数的参数,而是要进行预处理:

'''
C中函数:

int add(int x, int y) {
	return x + y;
}
'''
lib.add(1, 2) # 报错 ctypes.ArgumentError
lib.add(c_int(1), c_int(2)) # 正确写法

显然,当函数使用次数增加时,这样的写法是不利于书写和阅读的。在ctypes中,我们可以指定C函数的参数类型,这样,我们就可以直接将Python数据类型作为C函数的参数,因为编译器在中间进行了隐式转换:

c_add = lib.add
c_add.argtypes = [c_int, c_int]
c_add(1, 2) # 可行,不会报错

默认情况下,函数的返回值是int类型:

double add(double a, double b) {
    double c = a + b;
    return c;
}

Python中调用函数:

x, y = 2.1, 3.2
c_add = lib.add
c_add.argtypes = [c_double, c_double]
ret = c_add(x, y)
print(ret)

输出2,这显然是错误的。在ctypes中,我们往往需要指定返回值类型:

c_add.restype = c_double

将该语句插入到函数调用前,得到正确答案5.3.

传递指针(引用)

通过ctypes.byref实现函数指针参数的传递(可以把它理解为C中的取地址符&),我们自制一个测试函数:

double ref_test(double *a, double *b) {
    // 获取两个指针变量指向的值之和,并改变指针指向的值
    double c = *a + *b;
    *a = 1, *b = 2;
    return c;
}

Python中调用:

# 省略import和dll的加载
x, y = c_double(10), c_double(20)
p_add = lib.p_add
p_add.restype = c_double # 不要忘记确定返回值
print(x.value, y.value)
ret = p_add(byref(x), byref(y))
print(x.value, y.value, ret)

调用前,xy的值分别是10和20,而调用后,我们通过指针修改了变量值,因此输出

1.0 2.0 30.0 

数组

ctypes中,我们可以像Ctypedef一样定义类型,用一个类型乘以一个正数创建数组类型,比如

ten_ele_arr = c_int * 10 # 类型:10元素整形数组类
array = ten_ele_arr()    # 实例:一个10元素数组

for ele in array:
    print(ele, end=' ')   # 默认元素全0

我们也可以用具体的元素初始化数组:

array = ten_ele_arr(1, 2, 3)
for ele in array:
    print(ele, end=' ')   # 输出1 2 3 0 0 0 0 0 0 0

总结

我们在这里介绍了ctypes的用法,如何实现PythonC的交互,以实现运行速度的提升。