楔子
我们知道python的执行效率不是很高,而且由于GIL的原因,导致python不能充分利用多核CPU。一般的解决方式是使用多进程,但是多进程开销比较大,而且进程之间的通信也会比较麻烦。因此在解决效率问题上,我们会把那些比较耗时的模块使用C或者C++编写,然后编译成动态链接库,Windows上面是dll,linux上面则是so,编译好之后,交给python去调用。而且通过动态链接库的方式还可以解决python的GIL的问题,因此如果想要利用多核,我们仍然可以通过动态链接库的方式。
python如何调用动态链接库
python调用动态链接库的一种比较简单的方式就是使用ctypes这个库,这个库是python官方提供的,任何一个版本的python都可以使用,我们通过ctypes可以很轻松地调用动态链接库。
#include <stdio.h>
void test()
{
printf("hello world
");
}
我们定义了一个很简单的函数,下面我们就可以将其编译成动态链接库了。在Windows是dll,linux上是so,编译的命令是一样的。我这里以Windows 为例,记得在Windows上要安装MinGW,或者安装VsCode,我这里使用的是MinGW,因为VsCode太大了。
gcc -o dll文件或者so文件 -shared c或者c++源文件
我这里的C源文件叫做1.c,我们编译成mmp.dll吧,所以命令就可以这么写:gcc -o mmp.dll -shared 1.c
下面就可以使用python去调用了。
import ctypes
# 使用ctypes很简单,直接import进来,然后使用ctypes.CDLL这个类来加载动态链接库
# 如果在Windows上还可以使用ctypes.WinDLL。
# 因为看ctypes源码的话,会发现WinDLL也是一个类并且继承自CDLL
# 所以在linux上使用ctypes.CDLL,
# 而在Windows上既可以使用WinDLL、也可以使用CDLL加载动态链接库
lib = ctypes.CDLL("./mmp.dll") # 加载之后就得到了动态链接库对象
# 我们可以直接通过.的方式去调用里面的函数了,会发现成功打印
lib.test() # hello world
# 但是为了确定是否存在这个函数,我们一般会使用反射去获取
# 因为如果函数不存在通过.的方式调用会抛异常的
func = getattr(lib, "test", None)
if func:
print(func) # <_FuncPtr object at 0x0000029F75F315F0>
func() # hello world
# 不存在test_xx这个函数,所以得到的结果为None
func1 = getattr(lib, "test_xx", None)
print(func1) # None
所以使用ctypes去调用动态链接库非常方便
1.通过ctypes.CDLL("dll或者so的路径"),如果是Windows还可以使用ctypes.WinDLL("dll路径")。另外这两种加载方式分别等价于:ctypes.CDLL("dll或者so的路径") == ctypes.cdll.LoadLibrary("dll或者so的路径"),ctypes.WinDLL("dll路径") == ctypes.windll.LoadLibrary("dll路径")。但是注意的是:linux上只能使用ctypes.CDLL和ctypes.cdll.LoadLibrary,而Windows上ctypes.CDLL、ctypes.cdll.LoadLibrary、ctypes.WinDLL、ctypes.windll.LoadLibrary都可以使用。但是一般我们都使用ctypes.CDLL即可,另外注意的是:dll或者so文件的路径最好是绝对路径,即便不是也要表明层级,比如我们这里的py文件和dll文件是在同一个目录下,但是我们加载的时候不可以写mmp.dll,这样会报错找不到,要写成./mmp.dll。
2.加载动态链接库之后会返回一个对象,我们上面起名为lib,这个lib就是得到的动态链接库了。
3.然后可以直接通过lib调用里面的函数,但是一般我们会使用反射的方式来获取,因为不知道函数到底存不存在,如果不存在直接调用会抛出异常,如果存在这个函数我们才会执行。
python类型与C语言类型之间的转换
我们知道可以使用ctypes调用动态链接库,主要是调用动态链接库中使用C编写好的函数,但这些函数肯定都是需要参数的,还有返回值,不然编写动态链接库有啥用呢。那么问题来了,不同的语言变量类型不同,所以python能够直接往C编写的函数中传参吗?显然不行,所以ctypes还提供了大量的类,帮我们将python中的类型转成C语言中的类型。
我们说了,python中类型不能直接往C语言的函数中传递(整型是个例外)
,那么ctypes就提供了很多的类可以帮助我们将python的类型转成C语言的类型。常见的类型分为以下几种:数值、字符、指针
数值类型转换
c语言的数值类型分为如下:
int:整型
unsigned int:无符号整型
short:短整型
unsigned short:无符号短整型
long:长整形
unsigned long:无符号长整形
long long:64位机器上等同于long
unsigned long long:等同于unsigned long
float:单精度浮点型
double:双精度浮点型
long double:看成是double即可
_Bool:布尔类型
ssize_t:等同于long或者long long
size_t:等同于unsigned long或者unsigned long long
import ctypes
# 下面都是ctypes中提供的类,将python中的对象传进去,就可以转换为C语言能够识别的类型
print(ctypes.c_int(1)) # c_long(1)
print(ctypes.c_uint(1)) # c_ulong(1)
print(ctypes.c_short(1)) # c_short(1)
print(ctypes.c_ushort(1)) # c_ushort(1)
print(ctypes.c_long(1)) # c_long(1)
print(ctypes.c_ulong(1)) # c_ulong(1)
# c_longlong等价于c_long,c_ulonglong等价于c_ulong
print(ctypes.c_longlong(1)) # c_longlong(1)
print(ctypes.c_ulonglong(1)) # c_ulonglong(1)
print(ctypes.c_float(1.1)) # c_float(1.100000023841858)
print(ctypes.c_double(1.1)) # c_double(1.1)
# 在64位机器上,c_longdouble等于c_double
print(ctypes.c_longdouble(1.1)) # c_double(1.1)
print(ctypes.c_bool(True)) # c_bool(True)
# 相当于c_longlong和c_ulonglong
print(ctypes.c_ssize_t(10)) # c_longlong(10)
print(ctypes.c_size_t(10)) # c_ulonglong(10)
字符类型转换
c语言的字符类型分为如下:
char:一个ascii字符或者-128~127的整型
wchar:一个unicode字符
unsigned char:一个ascii字符或者0~255的一个整型
import ctypes
# 必须传递一个ascii字符并且是字节,或者一个int,来代表c里面的字符
print(ctypes.c_char(b"a")) # c_char(b'a')
print(ctypes.c_char(97)) # c_char(b'a')
# 传递一个unicode字符,当然ascii字符也是可以的,并且不是字节形式
print(ctypes.c_wchar("憨")) # c_wchar('憨')
# 和c_char类似,但是c_char既可以传入字符、也可以传整型,而这里的c_byte则要求必须传递整型。
print(ctypes.c_byte(97)) # c_byte(97)
print(ctypes.c_ubyte(97)) # c_ubyte(97)
指针类型转换
c语言的指针类型分为如下:
char *:字符指针
wchar_t *:字符指针
void *:空指针
import ctypes
# c_char_p就是c里面字符数组指针了
# char *s = "hello world";
# 那么这里面也要传递一个bytes类型的字符串,返回一个地址
print(ctypes.c_char_p(b"hello world")) # c_char_p(2082736374464)
# 直接传递一个unicode,同样返回一个地址
print(ctypes.c_wchar_p("憨八嘎~")) # c_wchar_p(2884583039392)
# ctypes.c_void_p后面演示
至于其他的类型,比如整型指针啊、数组啊、结构体啊、回调函数啊,ctypes都支持,我们后面会介绍。
参数传递
下面我们来看看如何传递参数。
#include <stdio.h>
void test(int a, float f, char *s)
{
printf("a = %d, b = %.2f, s = %s
", a, f, s);
}
这是一个很简单的C文件,然后编译成dll之后,让python去调用。这里我们编译之后的文件名叫做mmp.dll
import ctypes
from ctypes import *
lib = ctypes.CDLL("./mmp.dll")
try:
lib.test(1, 1.2, "hello world")
except Exception as e:
print(e) # argument 2: <class 'TypeError'>: Don't know how to convert parameter 2
# 我们看到一个问题,那就是报错了,告诉我们不知道如何转化第二个参数
# 正如我们之前说的,整型是会自动转化的,但是浮点型是不会自动转化的
# 因此我们需要使用ctypes来包装一下,当然还有整型,即便整型会自动转,我们还是建议手动转化一下
# 这里传入c_int(1)和1都是一样的,但是建议传入c_int(1)
lib.test(c_int(1), c_float(1.2), c_char_p(b"hello world")) # a = 1, b = 1.20, s = hello world
我们看到完美的打印出来了
我们再来试试布尔类型
#include <stdio.h>
void test(_Bool flag)
{
//布尔类型本质上是一个int
printf("a = %d
", flag);
}
import ctypes
from ctypes import *
lib = ctypes.CDLL("./mmp.dll")
lib.test(c_bool(True)) # a = 1
lib.test(c_bool(False)) # a = 0
# 可以看到True被解释成了1,False被解释成了0
# 我们说整型会自动转化,而布尔类型继承自整型所以布尔类型也可以直接传递
lib.test(True) # a = 1
lib.test(False) # a = 0
ctypes类型
关于ctypes转化之后的类型:
from ctypes import *
v = c_int(1)
# 我们看到c_int(1)它的类型就是ctypes.c_long
print(type(v)) # <class 'ctypes.c_long'>
# 当然你把c_int,c_long,c_longlong这些花里胡哨的都当成是整型就完事了
# 此外我们还能够拿到它的值,调用value方法
print(v.value, type(v.value)) # 1 <class 'int'>
v = c_char(b"a")
print(type(v)) # <class 'ctypes.c_char'>
print(v.value, type(v.value)) # b'a' <class 'bytes'>
v = c_char_p(b"hello world")
print(type(v)) # <class 'ctypes.c_char_p'>
print(v.value, type(v.value)) # b'hello world' <class 'bytes'>
调用value方法能够拿到对应python类型的值。
字符与字符数组的传递
来看一个稍微复杂点的例子:
#include <stdio.h>
#include <string.h>
void test(int age, char *gender)
{
if (age >= 18)
{
if (strcmp(gender, "female") == 0)
{
printf("社会人,合情合理
");
}
else
{
printf("抱歉,打扰了
");
}
}
else
{
if (strcmp(gender, "female") == 0)
{
printf("虽然担些风险,但也值得一试
");
}
else
{
printf("可爱的话也是没有问题的
");
}
}
}
import ctypes
from ctypes import *
lib = ctypes.CDLL("./mmp.dll")
lib.test(c_int(20), c_char_p(b"female")) # 社会人,合情合理
lib.test(c_int(20), c_char_p(b"male")) # 抱歉,打扰了
lib.test(c_int(14), c_char_p(b"female")) # 虽然担些风险,但也值得一试
lib.test(c_int(14), c_char_p(b"male")) # 可爱的话也是没有问题的
# 我们看到C中的字符数组,我们直接通过c_char_p来传递即可
# 至于单个字符,使用c_char即可。
然后看看unicode字符的传递,我们说char *传递的是ascii字符数组,如果想传入unicode的话需要使用wchar_t *。
#include <stdio.h>
#include <locale.h>
//当然里面可以定义多个函数
void test1(char a, char *b)
{
printf("a = %c, b = %s", a, b);
}
void test2(wchar_t a, wchar_t *b)
{
//打印宽字符需要引入一个头文件<locale.h>
setlocale(LC_ALL, "chs");
//wchar_t叫做宽字符,打印宽字符需要使用wprintf,占位符也要改成lc或者ls
//并且要改成L""的格式
wprintf(L"a = %lc, b = %ls", a, b);
}
import ctypes
from ctypes import *
lib = ctypes.CDLL("./mmp.dll")
lib.test1(c_char(b"a"), c_char_p(b"hello")) # a = a, b = hello
lib.test2(c_wchar("憨"), c_wchar_p("憨八嘎")) # a = 憨, b = 憨八嘎
# 当然我们说C中的char,还可以使用c_byte来传递,只不过接收的是对应的ascii码,不再是字符
lib.test1(c_byte(97), c_char_p(b"hello")) # a = a, b = hello
字符串的修改
我们知道C中不存在字符串这个概念,python中的字符串在C中也是通过字符数组来实现的。我们说在C中创建一个字符数组有两种方式:
char *s1 = "hello world";
char s2[] = "hello world";
这两种方式虽然打印的结果是一样的,并且s1、s2都指向了对应字符数组的首地址,但是内部的结构确是不同的。
1.char *s1 = "hello world";此时这个字符数组是存放在静态存储区里面的,程序编译的时候这块区域就已经确定好了,静态存储区在程序的整个运行期间都是存在的,主要用来存放一些静态变量、全局变量、常量。因此s1只能够访问这个字符数组,却不能够改变它,因为它是一个常量。而char s2[] = "hello world";,这种方式创建的字符数组是存放在栈当中的,可以通过s2这个指针去修改它。
2.char *s1 = "hello world";是在编译的时候就已经确定了,因为是一个常量。而char s2[] = "hello world";则是在运行时才确定。
3.char *s1 = "hello world";创建的字符数组存于静态存储区,char s2[] = "hello world";创建的字符数组存储于栈区,所以s1访问的速度没有s2快。
所以我们说char *s
这种方式创建的字符数组在C中是不能修改的,但是我们通过ctypes却可以做到对char *s
进行修改:
#include <stdio.h>
int test(char *s1, char s2[6])
{
//两种方式都进行修改
s1[0] = 'a';
s2[0] = 'a';
printf("s1 = %s, s2 = %s
", s1, s2);
}
我们还是将C文件编译成mmp.dll
import ctypes
from ctypes import *
lib = ctypes.CDLL("./mmp.dll")
# 我们看到无论是char *s1,还是char s2[...],我们都可以使用c_char_p这种方式传递
lib.test(c_char_p(b"hello"), c_char_p(b"hello")) # s1 = aello, s2 = aello
我们看到两种方式都成功修改了,但是即便能修改,我们不建议这么做。不是说不让修改,而是应该换一种方式。如果是需要修改的话,那么不要使用c_char_p的方式来传递,而是建议通过create_string_buffer来给C语言传递可以修改字符的空间。
create_string_buffer
create_string_buffer是ctypes提供的一个函数,表示创建具有一定大小的字符缓存,就理解为字符数组即可。
from ctypes import *
# 传入一个int,表示创建一个具有固定大小的字符缓存,这里是10个
s = create_string_buffer(10)
# 直接打印就是一个对象
print(s) # <ctypes.c_char_Array_10 object at 0x000001E2E07667C0>
# 也可以调用value方法打印它的值,可以看到什么都没有
print(s.value) # b''
# 并且它还有一个raw方法,表示C语言中的字符数组,由于长度为10,并且没有内容,所以全部是x00,就是C语言中的
print(s.raw) # b'x00x00x00x00x00x00x00x00x00x00'
# 还可以查看长度
print(len(s)) # 10
当然create_string_buffer如果只传一个int,那么表示创建对应长度的字符缓存。除此之外,还可以指定字节串,此时的字符缓存大小和指定的字节串大小是一致的:
from ctypes import *
# 此时我们直接创建了一个字符缓存
s = create_string_buffer(b"hello")
print(s) # <ctypes.c_char_Array_6 object at 0x0000021944E467C0>
print(s.value) # b'hello'
# 我们知道在C中,字符数组是以 作为结束标记的,所以结尾会有一个 ,因为raw表示C中的字符数组
print(s.raw) # b'hellox00'
# 长度为6,b"hello"五个字符再加上 一共6个
print(len(s))
当然create_string_buffer还可以指定字节串的同时,指定空间大小。
from ctypes import *
# 此时我们直接创建了一个字符缓存,如果不指定容量,那么默认和对应的字符数组大小一致
# 但是我们还可以同时指定容量,记得容量要比前面的字节串的长度要大。
s = create_string_buffer(b"hello", 10)
print(s) # <ctypes.c_char_Array_10 object at 0x0000019361C067C0>
print(s.value) # b'hello'
# 长度为10,剩余的5个显然是
print(s.raw) # b'hellox00x00x00x00x00'
print(len(s)) # 10
下面我们来看看如何使用create_string_buffer来传递:
#include <stdio.h>
int test(char *s)
{
//变量的形式依旧是char *s
//下面的操作就是相当于把字符数组的索引为5到11的部分换成" satori"
s[5] = ' ';
s[6] = 's';
s[7] = 'a';
s[8] = 't';
s[9] = 'o';
s[10] = 'r';
s[11] = 'i';
printf("s = %s
", s);
}
from ctypes import *
lib = CDLL("./mmp.dll")
s = create_string_buffer(b"hello", 20)
lib.test(s) # s = hello satori
此时就成功地修改了,我们这里的b"hello"占五个字节,下一个正好是索引为5的地方,然后把索引为5到11的部分换成对应的字符。但是需要注意的是,一定要小心