标签:参数 try Speak 速度 操作系统 parameter puts 大量 class
我们知道python的执行效率不是很高,而且由于GIL的原因,导致python不能充分利用多核CPU。一般的解决方式是使用多进程,但是多进程开销比较大,而且进程之间的通信也会比较麻烦。因此在解决效率问题上,我们会把那些比较耗时的模块使用C或者C++编写,然后编译成动态链接库,Windows上面是dll,linux上面则是so,编译好之后,交给python去调用。而且通过动态链接库的方式还可以解决python的GIL的问题,因此如果想要利用多核,我们仍然可以通过动态链接库的方式。
python调用动态链接库的一种比较简单的方式就是使用ctypes这个库,这个库是python官方提供的,任何一个版本的python都可以使用,我们通过ctypes可以很轻松地调用动态链接库。
#include <stdio.h>
void test()
{
printf("hello world\n");
}
我们定义了一个很简单的函数,下面我们就可以将其编译成动态链接库了。在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调用里面的函数,但是一般我们会使用反射的方式来获取,因为不知道函数到底存不存在,如果不存在直接调用会抛出异常,如果存在这个函数我们才会执行。
我们知道可以使用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\n", 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\n", 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转化之后的类型:
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("社会人,合情合理\n");
}
else
{
printf("抱歉,打扰了\n");
}
}
else
{
if (strcmp(gender, "female") == 0)
{
printf("虽然担些风险,但也值得一试\n");
}
else
{
printf("可爱的话也是没有问题的\n");
}
}
}
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\n", 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是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语言中的\0
print(s.raw) # b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
# 还可以查看长度
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中,字符数组是以\0作为结束标记的,所以结尾会有一个\0,因为raw表示C中的字符数组
print(s.raw) # b'hello\x00'
# 长度为6,b"hello"五个字符再加上\0一共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个显然是\0
print(s.raw) # b'hello\x00\x00\x00\x00\x00'
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\n", 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的部分换成对应的字符。但是需要注意的是,一定要小心\0
,我们知道C语言中一旦遇到了\0
就表示这个字符数组结束了。
from ctypes import *
lib = CDLL("./mmp.dll")
# 这里把"hello"换成"hell",看看会发生什么
s = create_string_buffer(b"hell", 20)
lib.test(s) # s = hell
# 我们看到这里只打印了"hell",这是为什么?
# 我们看一下这个s
print(s.raw) # b'hell\x00 satori\x00\x00\x00\x00\x00\x00\x00\x00'
# 我们看到这个create_string_buffer返回的对象是可变的,在将s传进去之后被修改了
# 如果没有传递的话,我们知道它是长这样的。
"""
b'hell\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
hell的后面全部是C语言中的\0
修改之后变成了这样
b'hell\x00 satori\x00\x00\x00\x00\x00\x00\x00\x00'
我们看到确实是把索引为5到11(包含11)的部分变成了"satori"
但是我们知道C语言中扫描字符数组的时候一旦遇到了\0,就表示结束了,而hell后面就是\0,
因为即便后面还有内容也不会输出了,所以直接就只打印了hell
"""
另外除了create_string_buffer之外,还有一个create_unicode_buffer,针对于wchar_t,用法和create_string_buffer一样。
C语言中如何查看字符数组的长度呢?有两种方法,一种是通过sizeof,一种是通过strlen。话说我说这个干什么?算了,不管了。
#include <stdio.h>
#include <string.h>
int main() {
char s[] = "hello world";
//C语言中查看字符串的长度可以使用strlen,这个需要导入string.h头文件。strlen计算的就是字符的个数,不包括\0
//使用sizeof计算的结果是包含\0的,所以会比strlen计算的结果多1
printf("%d %d\n", strlen(s), sizeof(s) / sizeof(s[0])); // 11 12
return 0;
}
但是我们发现字符数组的创建方式是通过char s[]
这种形式,如果是char *s
呢?
#include <stdio.h>
#include <string.h>
int main() {
char *s = "hello world";
printf("%d %d\n", strlen(s), sizeof(s) / sizeof(s[0])); // 11 8
return 0;
}
我们看到使用strlen计算的结果是一样的,但是使用sizeof得到的结果却是不一样的。因为char *s
,这个s我们虽然可以使用它来打印字符数组,但它本质上是一个指针,一个指针在64位机器上占8个字节,所以结果是8。而char s[]
中的s虽然也指向字符数组的首地址,但它本质上是一个数组名,我们使用sizeof查看得到的结果还是字符数组中所有元素的总大小。
艾玛,你扯到C上面干啥。。。。。。你又不会C。。。。。。
我们知道python解释器本质上就是使用C语言写出来的一个软件,那么操作系统呢?操作系统本质上它也是一个软件,不管是Windows、linux、macOS都自带了大量的共享库,那么我们就可以使用python去调用。
from ctypes import *
import platform
# 判断当前的操作系统平台。
# Windows平台返回"Windows",linux平台返回"Linux",macOS平台返回"Darwin"
system = platform.system()
# 不同的平台共享库不同
if system == "Windows":
libc = cdll.msvcrt
elif system == "Linux":
libc = CDLL("libc.so.6")
elif system == "Darwin":
libc = CDLL("libc.dylib")
else:
print("不支持的平台,程序结束")
import sys
sys.exit(0)
# 调用对应的函数,比如printf,注意里面需要传入字节
libc.printf(b"my name is %s, age is %d\n", b"van", 37) # my name is van, age is 37
# 如果包含汉字就不能使用b""这种形式了,因为这种形式只适用于ascii字符,我们需要手动encode成utf-8
libc.printf("姓名: %s, 年龄: %d\n".encode("utf-8"), "古明地觉".encode("utf-8"), 17) # 姓名: 古明地觉, 年龄: 17
我们上面是在Windows上调用的,这段代码即便拿到linux上也可以正常执行。
至于Mac,我这里没有Mac,就不演示了。
当然这里面还支持其他的函数,我们这里以Windows为例:
from ctypes import *
libc = cdll.msvcrt
# 创建一个大小为10的buffer
s = create_string_buffer(10)
# strcpy表示将字符串进行拷贝
libc.strcpy(s, c_char_p(b"hello satori"))
# 由于buffer只有10个字节大小,所以无法完全拷贝
print(s.value) # b'hello sato'
# 创建unicode buffer
s = create_unicode_buffer(10)
libc.strcpy(s, c_wchar_p("我也觉得很变态啊"))
print(s.value) # 我也觉得很变态啊
# 比如puts函数
libc.puts(b"hello world") # hello world
对于Windows来说,我们还可以调用一些其它的函数,但是不再是通过cdll.msvcrt这种方式了。在Windows上面有一个user32这么个东西,我们来看一下:
from ctypes import *
# 我们通过cdll.user32本质上还是加载了Windows上的一个共享库
# 这个库给我们提供了很多方便的功能
win = cdll.user32
# 比如查看屏幕的分辨率
print(win.GetSystemMetrics(0)) # 1920
print(win.GetSystemMetrics(1)) # 1080
我们还可以用它来打开MessageBoxA:
可以看到我们通过windll.user32就可以很轻松地调用Windows的api,具体有哪些api可以去网上查找,搜索win32 api
除了ctypes,还有几个专门用来操作win32服务的模块,win32gui、win32con、win32api、win32com、win32process。直接pip install pywin32即可,或者pip install pypiwin32。
import win32gui
import win32con
# 首先查找窗体,这里查找qq。需要传入 窗口类名 窗口标题名,至于这个怎么获取可以使用spy工具查看
qq = win32gui.FindWindow("TXGuifoundation", "QQ")
# 然后让窗体显示出来
win32gui.ShowWindow(qq, win32con.SW_SHOW)
# 还可以隐藏
win32gui.ShowWindow(qq, win32con.SW_HIDE)
import win32gui
import win32con
qq = win32gui.FindWindow("TXGuiFoundation", "QQ")
# 主要要接收如下参数
# 参数一:控制的窗体
# 参数二:大致方位:HWND_TOPMOST,位于上方
# 参数三:位置x
# 参数四:位置y
# 参数五:长度
# 参数六:宽度
# 参数七:比较固定,就是让窗体一直显示
win32gui.SetWindowPos(qq, win32con.HWND_TOPMOST, 100, 100, 300, 300, win32con.SWP_SHOWWINDOW)
那么我们还可以让窗体满屏幕乱跑
import win32gui
import win32con
import random
qqWin = win32gui.FindWindow("TXGuiFoundation", "QQ")
# 将位置变成随机数
while True:
x = random.randint(1, 800)
y = random.randint(1, 400)
win32gui.SetWindowPos(qqWin, win32con.HWND_TOPMOST, x, y, 300, 300, win32con.SWP_SHOWWINDOW)
import win32com.client
# 直接调用操作系统的语音接口
speaker = win32com.client.Dispatch("SAPI.SpVoice")
# 输入你想要说的话,前提是操作系统语音助手要认识。一般中文和英文是没有问题的
speaker.Speak("他能秒我,他能秒杀我?他要是能把我秒了,我当场······")
python中win32模块的api非常多,几乎可以操作整个windows提供的服务,win32模块就是相当于把Windows服务封装成了一个一个的接口。不过这些服务、或者调用这些服务具体都能干些什么,可以自己去研究,这里就到此为止了。毕竟本来是介绍python和静态语言之间的结合的,所以感觉这一篇文章似乎有点多余,但是多了解点总归是好的。
我们前面已经看到了,通过ctypes向动态链接库中的函数传参时是没有问题的,但是我们如何拿到返回值呢?我们之前都是使用printf直接打印的,但是这样显然不行,我们肯定是要拿到返回值去做一些别的事情的。那么我们看看如何使用ctypes获取函数的返回值。
int test1(int a, int b)
{
int c;
c = a + b;
return c;
}
void test2()
{
}
我们定义了两个函数,下面编译成dll文件,dll文件名叫做mmp.dll
from ctypes import *
lib = CDLL("./mmp.dll")
print(lib.test1(25, 33)) # 58
print(lib.test2()) # 125387149
我们看到对于test1的结果是正常的,但是对于test2来说即便返回的是void,在python中依旧会得到一个整型,但是这个结果肯定是不正确的。不过对于整型来说,是完全没有问题的。
当然我们后面还会遇到一个问题,这里提前说一下。动态链接库中的函数不要说返回void,即便返回一个char *,那么在python中得到的依旧是一个整型。这个是不同语言的类型不同造成的,正如我们传递参数一样,需要使用ctypes转化一下,那么在获取返回值的时候,也需要提前使用ctypes指定一下返回值到底是什么类型,只有这样才能拿到动态链接库中函数的正确的返回值。
#include <wchar.h>
char * test1()
{
char *s = "hello satori";
return s;
}
wchar_t * test2()
{
//遇到wchar_t的时候,一定要导入wchar.h头文件
wchar_t *s = L"憨八嘎";
return s;
}
我们定义了两个函数,一个返回char *,一个返回wchar_t *。
from ctypes import *
lib = CDLL("./mmp.dll")
# 我们看到神奇的事情发生了,我们在动态链接库中返回的是一个字符数组的首地址,我们希望拿到指向的字符串
# 然而python拿到的确是一个整型,而且一看感觉这像是一个地址。如果是地址的话那么从理论上讲是对的,返回地址、获取地址
print(lib.test1()) # 1801207808
# 但我们希望的是获取地址指向的字符数组,所以我们需要指定一下返回的类型
# 指定为c_char_p,告诉ctypes我们不要地址,而是要通过这个地址获取其指向的字符数组
lib.test1.restype = c_char_p
# 此时就没有问题了
print(lib.test1()) # b'hello satori'
# 同理对于unicode也是一样的,如果不指定类型,得到的依旧是一个整型
lib.test2.restype = c_wchar_p
print(lib.test2()) # 憨八嘎
因此我们就将python中的类型和C语言中的类型通过ctypes关联起来了,我们传参的时候需要转化,同理获取返回值的时候也要使用ctypes来声明一下类型。因为默认python调用动态链接库的函数返回的都是整型,至于返回的整型的值到底是什么?从哪里来的?我们不需要关心,你可以理解为地址、或者某块内存的脏数据,但是不管怎么样,结果肯定是不正确的(如果函数返回的就是整形除外)
。因此我们需要提前声明一下返回值的类型。声明方式:
lib.CFunction.restype = ctypes类型
我们说lib就是ctypes调用dll或者so得到的动态链接库,而里面的函数就相当于是一个个的CFunction,然后设置内部的restype(返回值类型)
,就可以得到正确的返回值了。另外即便返回值设置的不对,比如:test1返回一个char *,但是我们将类型设置为c_float,调用的时候也不会报错而且得到的也是一个float,但是这个结果肯定是不对的。
from ctypes import *
lib = CDLL("./mmp.dll")
lib.test1.restype = c_char_p
print(lib.test1()) # b'hello satori'
# 设置为c_float
lib.test1.restype = c_float
# 获取了不知道从哪里来的脏数据
print(lib.test1()) # 2.5420596244190436e+20
# 另外ctypes调用还有一个特点
lib.test2.restype = c_wchar_p
print(lib.test2(123, c_float(1.35), c_wchar_p("呼呼呼"))) # 憨八嘎
# 我们看到test2是不需要参数的,如果我们传了那么就会忽略掉,依旧能得到正常的返回值
# 但是不要这么做,因为没准就出问题了,所以还是该传几个参数就传几个参数
下面我们来看看浮点类型的返回值怎么获取,当然方法和上面是一样的。
#include <math.h>
float test1(int a, int b)
{
float c;
c = sqrt(a * a + b * b);
return c;
}
from ctypes import *
lib = CDLL("./mmp.dll")
# 得到的结果是一个整型,默认都是整型。
# 我们不知道这个整型是从哪里来的,就把它理解为地址吧,但是不管咋样,结果肯定是不对的
print(lib.test1(3, 4)) # 1084227584
# 我们需要指定返回值的类型,告诉ctypes返回的是一个float
lib.test1.restype = c_float
# 此时结果就是对的
print(lib.test1(3, 4)) # 5.0
# 如果指定为double呢?
lib.test1.restype = c_double
# 得到的结果也有问题,总之类型一定要匹配
print(lib.test1(3, 4)) # 5.356796015e-315
# 至于int就不用说了,因为默认就是int。所以和第一个结果是一样的
lib.test1.restype = c_int
print(lib.test1(3, 4)) # 1084227584
所以类型一定要匹配,该是什么类型就是什么类型。即便动态链接库中返回的是float,我们在python中通过ctypes也要指定为float,而不是指定为double,尽管都是浮点数并且double的精度还更高,但是结果依旧不是正确的。至于整型就不需要关心了,但即便如此,int、long也不要混用,而且也建议传参的时候进行转化。
我们使用ctypes可以创建一个字符数组并且拿到首地址,但是对于整型、浮点型我们怎么创建指针呢?下面就来揭晓。另外,一旦涉及到指针操作的时候就要小心了,因为这往往是比较危险的,所以python、java等语言把指针给隐藏掉了,当然不是说没有指针,肯定是有指针的。只不过操作指针的权限没有暴露给程序员,能够操作指针的只有对应的解释器。
from ctypes import *
v = c_int(123)
# 我们知道可以通过value属性获取相应的值
print(v.value)
# 但是我们还可以修改
v.value = 456
print(v) # c_long(456)
s = create_string_buffer(b"hello")
s[3] = b'>'
print(s.value) # b'hel>o'
# 如何创建指针呢?通过byref和pointer
v2 = c_int(123)
print(byref(v2)) # <cparam 'P' (000001D9DCF86888)>
print(pointer(v2)) # <__main__.LP_c_long object at 0x000001D9DCF868C0>
我们看到byref和pointer都可以创建指针,那么这两者有什么区别呢?byref返回的指针相当于右值,而pointer返回的指针相当于左值。举个栗子:
//以整型的指针为例:
int num = 123;
int *p = &num
对于上面的例子,如果是byref,那么结果相当于&num,拿到的就是一个具体的值。如果是pointer,那么结果相当于p。这两者在传递的时候是没有区别的,只是对于pointer来说,它返回的是一个左值,我们是可以继续拿来做文章的。
from ctypes import *
n = c_int(123)
# 拿到变量n的指针
p1 = byref(n)
p2 = pointer(n)
# pointer返回的是左值,我们可以继续做文章,比如继续获取指针,此时获取的就是p2的指针
print(byref(p2)) # <cparam 'P' (0000023953796888)>
# 但是p1不行,因为byref返回的是一个右值
try:
print(byref(p1))
except Exception as e:
print(e) # byref() argument must be a ctypes instance, not 'CArgObject'
因此两者的区别就在这里,但是还是那句话,我们在传递的时候是无所谓的,传递哪一个都可以
我们知道了可以通过ctypes.byref、ctypes.pointer的方式传递指针,但是如果函数返回的也是指针呢?我们知道除了返回int之外,都要指定返回值类型,那么指针如何指定呢?答案是通过ctypes.POINTER。
//接收两个float *,返回一个float *
float *test1(float *a, float *b)
{
static float c;
c = *a + *b;
return &c;
}
from ctypes import *
lib = CDLL("./mmp.dll")
# 声明一下,返回的类型是一个POINTER(c_float),也就是float的指针类型
lib.test1.restype = POINTER(c_float)
# 别忘了传递指针,因为函数接收的是指针,两种传递方式都可以
res = lib.test1(byref(c_float(3.14)), pointer(c_float(5.21)))
print(res) # <__main__.LP_c_float object at 0x000001FFF1F468C0>
print(type(res)) # <class '__main__.LP_c_float'>
# 我们可以调用contents方法拿到对应的值,这个值是ctypes类型,那么显然就还可以在基础上再调用value方法拿到python中的值
print(res.contents) # c_float(8.350000381469727)
print(res.contents.value) # 8.350000381469727
因此我们看到了如果返回的是指针类型可以使用POINTER(类型)来声明。也就是说POINTER是用来声明指针类型的,而byref、pointer则是用来获取指针的。另外我们调用动态链接库中的函数返回的是一个指针类型的话,那么返回的类型就是一个pointer返回值对应的类型。
from ctypes import *
lib = CDLL("./mmp.dll")
lib.test1.restype = POINTER(c_float)
print(type(lib.test1(byref(c_float(1.1)), byref(c_float(2.2))))) # <class '__main__.LP_c_float'>
# 返回的类型,和调用pointer函数返回结果是同一个类型
print(type(pointer(c_float(1.1)))) # <class '__main__.LP_c_float'>
我们知道可以事先声明返回值的类型,这样才能拿到正确的返回值。而我们传递的时候,直接传递正确的类型即可,但是其实也是可以事先声明的。
from ctypes import *
lib = CDLL("./mmp.dll")
# 通过argtypes,我们可以事先指定需要传入两个float的指针类型
lib.test1.argtypes = (POINTER(c_float), POINTER(c_float))
lib.test1.restype = POINTER(c_float)
# 但是和restype不同,argtypes实际上是可以不要的
# 因为返回的默认是一个整型,我们才需要通过restype事先声明返回值的类型,这是有必要的
# 但是对于argtypes来说,我们传参的时候已经直接指定类型了,所以argtypes即便没有也是可以的
# 所以argtypes的作用就类似于其他静态语言中的类型声明,先把类型定好,如果你传的类型不对,直接给你报错
try:
# 这里第二个参数传c_int
res = lib.test1(byref(c_float(3.21)), c_int(123))
except Exception as e:
# 所以直接就给你报错了
print(e) # argument 2: <class 'TypeError'>: expected LP_c_float instance instead of c_long
# 此时正确执行
res1 = lib.test1(byref(c_float(3.21)), byref(c_float(666)))
print(res1.contents.value) # 669.2100219726562
下面我们来看看如何使用ctypes传递数组,这里我们只讲传递,不讲返回。因为C语言返回数组给python实际上会存在很多问题,比如:返回的数组的内存由谁来管理,不用了之后空间由谁来释放,事实上ctypes内部对于返回数组支持的也不是很好。因此我们一般不会向python返回一个C语言中的数组,因为C语言中的数组传递给python涉及到效率的问题,python中的列表传递直接传递一个引用即可,但是C语言中的数组过来肯定是要拷贝一份的,所以这里我们只讲python如何通过ctypes给动态链接库传递数组,不会介绍动态链接库如何返回数组给python。
我们知道python中没有数组,或者说C中的数组在python中是一个list,我们可以通过list来得到数组,方式也很简单。
from ctypes import *
# 创建一个数组,假设叫[1, 2, 3, 4, 5]
a5 = (c_int * 5)(1, 2, 3, 4, 5)
print(a5) # <__main__.c_long_Array_5 object at 0x00000162428968C0>
# 上面这种方式就得到了一个数组
# 当然还可以使用list
a5 = (c_int * 5)(*range(1, 6))
print(a5) # <__main__.c_long_Array_5 object at 0x0000016242896940>
下面演示一下。
//字符数组默认是以\0作为结束的,我们可以通过strlen来计算长度。
//但是对于整型的数组来说我们不知道有多长
//因此有两种声明参数的方式,一种是int a[n],指定数组的长度
//另一种是通过指定int *a的同时,再指定一个参数int size,调用函数的时候告诉函数这个数组有多长
int test1(int a[5])
{
//可能有人会问了,难道不能通过sizeof计算吗?答案是不能,无论是int *a还是int a[n]
//当它作为函数的参数,我们调用的时候,传递的都是指针,指针在64位机器上默认占8个字节。
//所以int a[] = {...}这种形式,如果直接在当前函数中计算的话,那么sizeof(a)就是数组里面所有元素的总大小,因为a是一个数组名
//但是当把a传递给一个函数的时候,那么等价于将a的首地址拷贝一份传过去,此时在新的函数中再计算sizeof(a)的时候就是一个指针的大小
//至于int *a这种声明方式,不管在什么地方,sizeof(a)则都是一个指针的大小
int i;
int sum = 0;
a[3] = 10;
a[4] = 20;
for (i = 0;i < 5; i++){
sum += a[i];
}
return sum;
}
from ctypes import *
lib = CDLL("./mmp.dll")
# 创建5个元素的数组,但是只给3个元素
arr = (c_int * 5)(1, 2, 3)
# 在动态链接库中,设置剩余两个元素
# 所以如果没问题的话,结果应该是1 + 2 + 3 + 10 + 20
print(lib.test1(arr)) # 36
有了前面的数据结构还不够,我们还要看看结构体是如何传递的,有了结构体的传递,我们就能发挥更强大的功能。那么我们来看看如何使用ctypes定义一个结构体:
from ctypes import *
# 对于这样一个结构体应该如何定义呢?
"""
struct Girl {
//姓名
char *name;
//年龄
int age;
//性别
char *gender;
//班级
int class;
};
"""
# 定义一个类,必须继承自ctypes.Structure
class Girl(Structure):
# 创建一个_fields_变量,必须是这个名字,注意开始和结尾都只有一个下划线
# 然后就可以写结构体的字段了,具体怎么写估计一看就清晰了
_fields_ = [
("name", c_char_p),
("age", c_int),
("gender", c_char_p),
("class", c_int)
]
我们向C中传递一个结构体,然后再返回:
struct Girl {
char *name;
int age;
char *gender;
int class;
};
//接收一个结构体,返回一个结构体
struct Girl test1(struct Girl g){
g.name = "古明地觉";
g.age = 17;
g.gender = "female";
g.class = 2;
return g;
}
from ctypes import *
lib = CDLL("./mmp.dll")
class Girl(Structure):
_fields_ = [
("name", c_char_p),
("age", c_int),
("gender", c_char_p),
("class", c_int)
]
# 此时返回值类型就是一个Girl类型,另外我们这里的类型和C中结构体的名字不一样也是可以的
lib.test1.restype = Girl
# 传入一个实例,拿到返回值
g = Girl()
res = lib.test1(g)
print(res, type(res)) # <__main__.Girl object at 0x0000015423A06840> <class '__main__.Girl'>
print(res.name, str(res.name, encoding="utf-8")) # b'\xe5\x8f\xa4\xe6\x98\x8e\xe5\x9c\xb0\xe8\xa7\x89' 古明地觉
print(res.age) # 17
print(res.gender) # b'female'
print(getattr(res, "class")) # 2
如果是结构体指针呢?
struct Girl {
char *name;
int age;
char *gender;
int class;
};
//接收一个指针,返回一个指针
struct Girl *test1(struct Girl *g){
g -> name = "mashiro";
g -> age = 17;
g -> gender = "female";
g -> class = 2;
return g;
}
from ctypes import *
lib = CDLL("./mmp.dll")
class Girl(Structure):
_fields_ = [
("name", c_char_p),
("age", c_int),
("gender", c_char_p),
("class", c_int)
]
# 此时指定为Girl类型的指针
lib.test1.restype = POINTER(Girl)
# 传入一个实例,拿到返回值
# 如果lib.test1.restype指定的类型不是结构体指针,那么函数返回的就是该结构体(Girl)实例
# 但返回的是指针,我们还需要手动调用一个contents才可以拿到对应的值。
g = Girl()
res = lib.test1(byref(g))
print(str(res.contents.name, encoding="utf-8")) # mashiro
print(res.contents.age) # 16
print(res.contents.gender) # b'female'
print(getattr(res.contents, "class")) # 3
# 另外我们不仅可以通过返回的res去调用,因为我们传递的是g的指针
# 修改指针指向的内存就相当于修改g
# 所以我们通过g来调用也是可以的
print(str(g.name, encoding="utf-8")) # mashiro
因此对于结构体来说,我们先创建一个结构体(Girl)
实例g,如果动态链接库的函数中接收的是结构体,那么直接把g传进去等价于将g拷贝了一份,此时函数中进行任何修改都不会影响原来的g。但如果函数中接收的是结构体指针,我们传入byref(g)相当于把g的指针拷贝了一份,在函数中修改是会影响g的。而返回的res也是一个指针,所以我们除了通过res.contents来获取结构体中的值之外,还可以通过g来获取。再举个栗子对比一下:
struct Num {
int x;
int y;
};
struct Num test1(struct Num n){
n.x += 1;
n.y += 1;
return n;
}
struct Num *test2(struct Num *n){
n->x += 1;
n->y += 1;
return n;
}
from ctypes import *
lib = CDLL("./mmp.dll")
class Num(Structure):
_fields_ = [
("x", c_int),
("y", c_int),
]
# 我们在创建的时候是可以传递参数的
num = Num(x=1, y=2)
print(num.x, num.y) # 1 2
lib.test1.restype = Num
res = lib.test1(num)
# 我们看到通过res得到的结果是修改之后的值
# 但是对于num来说没有变
print(res.x, res.y) # 2 3
print(num.x, num.y) # 1 2
"""
因为我们将num传进去之后,相当于将num拷贝了一份。
函数里面的结构体和这里的num尽管长得一样,但是没有任何关系,自增1之后返回交给res。
所以res获取的结果是自增之后的结果,但是num还是之前的num
"""
# 我们来试试传递指针,将byref(num)再传进去
lib.test2.restype = POINTER(Num)
res = lib.test2(byref(num))
print(num.x, num.y) # 2 3
"""
我们看到将指针传进去之后,相当于把num的指针拷贝了一份。
然后在函数中修改,相当于修改指针指向的内存,所以是会影响外面的num的
而动态链接库的函数中返回的是参数中的结构体指针,而我们传递的byref(num)也是这里的num的指针
尽管传递指针的时候也是拷贝了一份,两个指针本身来说虽然也没有任何联系,但是它们存储的地址是一样的
那么通过res.contents获取到的内容就相当于是这里的num
因此此时我们通过res.contents获取和通过num来获取都是一样的。
"""
print(res.contents.x, res.contents.y) # 2 3
# 另外还需要注意的一点就是:如果传递的是指针,一定要先创建一个变量
# 比如这里,一定是:先要num = Num(),然后再byref(num)。不可以直接就byref(Num())
# 原因很简单,因为Num()这种形式在创建完Num实例之后就销毁了,因为没有变量保存它,那么此时再修改指针指向的内存就会有问题,因为内存的值已经被回收了
# 如果不是指针,那么可以直接传递Num(),因为拷贝了一份
我们来一个难度高的,其实也不难,我们可以传递一个结构体数组。
#include <stdio.h>
typedef struct {
char *name;
int age;
char *gender;
}Girl;
void print_info(Girl *g, int size)
{
int i;
for (i=0;i<size;i++){
printf("%s %d %s\n", g[i].name, g[i].age, g[i].gender);
}
}
from ctypes import *
lib = CDLL("./mmp.dll")
class Girl(Structure):
_fields_ = [
("name", c_char_p),
("age", c_int),
("gender", c_char_p),
]
g1, g2, g3 = Girl(c_char_p(b"mashiro"), 16, c_char_p(b"female")), Girl(c_char_p(b"satori"), 17, c_char_p(b"female")), Girl(c_char_p(b"koishi"), 16, c_char_p(b"female"))
g = (Girl * 3)(*[g1, g2, g3])
# 指定返回值类型
lib.print_info.restype = (Girl * 3)
lib.print_info(g, 3)
"""
mashiro 16 female
satori 17 female
koishi 16 female
"""
因此我们发现对于数组结构体的传递也是很简单的
什么是回调函数我就不介绍了,我们先来看看C语言中如何使用回调函数。
不过在看回调函数之前,我们先看看如何把一个函数赋值给一个变量。准确的说,是让一个指针指向一个函数,这个指针叫做函数指针。通常我们说的指针变量是指向一个整型、字符型或数组等变量,而函数指针是指向函数。函数指针可以像一般函数一样,用于调用函数、传递参数。
#include <stdio.h>
int add(int a, int b){
int c;
c = a + b;
return c;
}
int main(int argc, char const *argv[]) {
//创建一个指针变量p,让p等于指向add
//我们看到就类似声明函数一样,指定返回值类型和变量类型即可
//但是注意的是,中间一定是*p,不是p,因为这是一个函数指针,所以要有*
int (*p)(int, int) = add;
printf("1 + 3 = %d\n", p(1, 3)); //1 + 3 = 4
return 0;
}
除此之外我们还以使用typedef
#include <stdio.h>
int add(int a, int b){
int c;
c = a + b;
return c;
}
//相当于创建了一个类型,名字叫做func。这个func表示的是一个函数指针类型
typedef int (*func)(int, int);
int main(int argc, char const *argv[]) {
//声明一个func类型的函数指针p,指向add函数
func p = add;
printf("2 + 3 = %d\n", p(2, 3)); //2 + 3 = 5
return 0;
}
下面来看看如何使用回调函数,说白了就是把一个函数指针作为函数的参数
#include <stdio.h>
char *evaluate(int score){
if (score < 60 && score >= 0){
return "不及格";
}else if (score < 80){
return "及格";
}else if (score < 90){
return "不错";
}else if (score <=100){
return "真棒";
}else {
return "无效的成绩";
}
}
//接收一个整型和一个函数,函数接收一个整型返回char *
char *execute1(int score, char *(*f)(int)){
return f(score);
}
//除了上面那种方式,我们还可以跟之前一样通过typedef
typedef char *(*func)(int);
//这样声明也是可以的。
char *execute2(int score, func f){
return f(score);
}
int main(int argc, char const *argv[]) {
printf("%s\n", execute1(88, evaluate)); //不错
printf("%s\n", execute2(70, evaluate)); //及格
}
我们知道了在C中传入一个函数,那么在python中如何定义一个C语言可以识别的函数呢?毫无疑问,类似于结构体,我们肯定是要先定义一个python的函数,然后再把python的函数转化成C语言可以识别的函数。
int add(int a, int b, int (*f)(int *, int *)){
return f(&a, &b);
}
我们就以这个函数为例,add函数返回一个int,接收两个int,和一个函数指针,那么我们如何在python中定义这样的函数并传递呢?我们来看一下,不过我们记得要编译成动态链接库:gcc -o dll或者so -shared c源文件
,这里编译成mmp.dll
from ctypes import *
lib = CDLL("./mmp.dll")
# 动态链接库中的函数接收的函数的参数是两个int *,所以我们这里的a和b也是一个pointer
def add(a, b):
return a.contents.value + b.contents.value
# 此时我们把C中的函数用python表达了,但是这样肯定是不可能直接传递的,能传就见鬼了
# 那我们要如何转化呢?
# 可以通过ctypes里面的函数CFUNCTYPE转化一下,这个函数接收任意个参数
# 但是第一个参数是函数的返回值类型,然后函数的参数写在后面,有多少写多少。
# 比如这里的函数返回一个int,接收两个int *,所以就是
t = CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int))
# 如果函数不需要返回值,那么写一个None即可
# 然后得到一个类型t,此时的类型t就等同于C中的 typedef int (*t)(int*, int*);
# 将我们的函数传进去,就得到了C语言可以识别的函数func
func = t(add)
# 然后调用,别忘了定义返回值类型,当然这里是int就无所谓了
lib.add.restype = c_int
print(lib.add(88, 96, func))
print(lib.add(59, 55, func))
print(lib.add(94, 105, func))
"""
184
114
199
"""
标签:参数 try Speak 速度 操作系统 parameter puts 大量 class
原文地址:https://www.cnblogs.com/traditional/p/12243307.html