ctypes 基本数据类型映射表
- ctypes 是 Python 的外部函数库。提供了与 C 兼容的数据类型,并允许调用 DLL 或共享库中的函数。可使用该模块以纯
Python 形式对这些库进行封装。 - 下面主要介绍如何使用 ctypes 模块对 C 语言编译的动态链接库要求的数据类型进行封装,主要包括以下几类:
- C语言中基础的数据类型 ( 如char, int等 )
- 数组类型
- 指针类型
- 结构体类型
- 嵌套结构体
- 结构体数组
- 结构体指针
- 指针数组
- 结构体指针数组
标题 ctypes 的类型对应如下:
ctypes type | C type | Python type |
---|---|---|
c_bool | _Bool | bool (1) |
c_char | char | 1-character bytes object |
c_wchar | wchar_t | 1-character string |
c_byte | char | int |
c_ubyte | unsigned char | int |
c_short | short | int |
c_ushort | unsigned short | int |
c_int | int | int |
c_uint | unsigned int | int |
c_long | long | int |
c_ulong | unsigned long | int |
c_longlong | __int64 or long long | int |
c_ulonglong | unsigned __int64 or unsigned long long | int |
c_size_t | size_t | int |
c_ssize_t | ssize_t or Py_ssize_t | int |
c_float | float | float |
c_double | double | float |
c_longdouble | long double | float |
c_char_p | char* (NUL terminated) | bytes object or None |
c_wchar_p | wchar_t* (NUL terminated) | string or None |
c_void_p | void* | int or None |
动态链接库
下面是测试用的C语言代码
#include <stdio.h>
#include <string.h>
typedef struct student {
char class;
int grade;
long array[3];
int *point;
}student_t;
typedef struct nest_stu {
char rank;
student_t nest_stu;
student_t strct_array[2];
student_t *strct_point;
student_t *strct_point_array[2];
} nest_stu_t;
typedef struct g_student {
int g_grade;
} g_stu;
g_stu g_stu_t = {11};
int test_func(char char_arg, int int_arg, float float_arg, char *stu_buf, char *nest_stu_buf, char *out_buf)
{
//data type test
printf("char arg: %c\n", char_arg);
printf("int arg: %d\n", int_arg);
printf("float arg: %f\n", float_arg);
student_t *stu_p = NULL;
nest_stu_t *nest_stu_p = NULL;
stu_p = (student_t *)stu_buf;
nest_stu_p = (nest_stu_t *)nest_stu_buf;
//struct type test
printf("struct class: %c\n", stu_p->class);
printf("struct grade: %d\n", stu_p->grade);
printf("struct array[0]: %d array[1]: %d\n", stu_p->array[0], stu_p->array[1]);
printf("struct point: %d\n", *(stu_p->point));
//nest struct test
printf("nest struct rank: %d\n", nest_stu_p->rank);
printf("nest struct stu grade: %d\n", nest_stu_p->nest_stu.grade);
//struct array
printf("nest struct array[0] grade: %d\n", nest_stu_p->strct_array[0].grade);
printf("nest struct array[1] grade: %d\n", nest_stu_p->strct_array[1].grade);
//struct point
printf("nest struct point grade: %d\n", nest_stu_p->strct_point->grade);
//struct point array
printf("nest struct point array[0] grade: %d\n", nest_stu_p->strct_point_array[0]->grade);
printf("nest struct point array[1] grade: %d\n", nest_stu_p->strct_point_array[1]->grade);
//out buf test
memcpy(out_buf, stu_p, sizeof(int)*2);
return 1;
}
编译命令gcc -Wall -g -fPIC -shared -o libstruct.so.0 ctype_code.c
set src=…\src\tab_calc.c …/src/shifting_calc.c
gcc -o calc.so --share -fPIC %src%
基础数据类型
# -*- coding: utf-8 -*-
from ctypes import *
# 字符,仅接受one character bytes, bytearray or integer
char_type = c_char(b"a")
byte_type = c_char(1) # 字节
string_type = c_wchar_p("abc") # 字符串
int_type = c_int(2) # 整型
# 直接打印输出的是对象信息,获取值需要使用 value 方法
print(char_type, byte_type, int_type)
print(char_type.value, byte_type.value, string_type.value, int_type.value)
print(c_int()) # c_long(0)
print(c_wchar_p("Hello, World")) # c_wchar_p(140018365411392)
print(c_ushort(-3)) # c_ushort(65533)
当给指针类型的对象 c_char_p、c_wchar_p 和 c_void_p 等赋值时,将改变它们所指向的 内存地址,而 不是 它们所指向的内存区域的 内容 (这是因为 Python 的 bytes 对象是不可变的):
from ctypes import *
s = "Hello, World"
c_s = c_wchar_p(s)
print(c_s) # c_wchar_p(139966785747344)
print(c_s.value) # Hello World
c_s.value = "Hi, there"
print(c_s) # 内存地址已经改变 c_wchar_p(139966783348904)
print(c_s.value) # Hi, there
print(s) # 第一个对象没有改变 Hello, World
但要注意不能将它们传递给会改变指针所指内存的函数。如果你需要可改变的内存块,ctypes 提供了 create_string_buffer() 函数,它提供多种方式创建这种内存块。当前的内存块内容可以通过 raw 属性存取,如果你希望将它作为NUL结束的字符串,请使用 value 属性
from ctypes import *
p = create_string_buffer(3)
print(sizeof(p), repr(p.raw))
p = create_string_buffer(b"Hello")
print(sizeof(p), repr(p.raw))
print(repr(p.value))
p = create_string_buffer(b"Hello", 10)
print(sizeof(p), repr(p.raw))
p.value = b"Hi"
print(sizeof(p), repr(p.raw))
数组 类型
数组的创建和C语言的类似,给定数据类型和长度 即可,如下
from ctypes import *
# 数组
# 定义类型
char_array = c_char * 3
# 初始化
char_array_obj = char_array(b"a", b"b", 2)
# 打印只能打印数组对象的信息
print(char_array_obj)
# 打印值通过value方法
print(char_array_obj.value)
from ctypes import *
int_array = (c_int * 3)(1, 2, 3)
for i in int_array:
print(i)
char_array_2 = (c_char * 3)(1, 2, 3)
print(char_array_2.value)
这里需要注意,通过value方法获取值只适用于字符数组,其他类型如print(int_array.value)的使用会报错。
5个0的整形数组:
(c_int * 5)()
前三个数为1-3,后续全为0的10长度浮点型数组:
(c_double * 10)(1, 2, 3)
对于Python而言,数字类型的数组是一个可迭代对象,其可通过for循环、next方法等方式进行迭代,
以获取其中的每一个值。例:
for i in (c_double * 10)(1, 2, 3):
print(i)
输出结果为1.0、2.0、3.0以及后续的7个0.0。
数组对象在数据类型上可看作是指针,且指针变量在ctypes中就等同于int类型,故所有涉及到指针传递的地方,均无需考虑修改argtypes属性的问题。直接以默认的int类型传入即可
多维 数组
多维数组与一维数组在语法上大体类似,但在字符串数组上略有不同。这里讨论 数字类型的高维数组。此外,为简单起见,都是对二维数组进行讨论,更高维度的数组在语法上与二维数组是相似的,不再赘述。高维数组类可简单的通过在一维数组类外部再乘上一个数字实现
(c_int * 4) * 3
这样就得到了一个所有值均为0的二维数组对象。又例:
只实例化了第一个一维数组的全部 以及 第二个一维数组的前两个值,而其他所有值均为0。
((c_int * 4) * 3)((1, 2, 3, 4), (5, 6))
二维数组在使用时与一维数组一致,其可直接作为指针参数传入C的函数接口进行访问,在C语言内部其等同于C语言中声明的二维数组。而对于Python,这样的数组对象可通过双层的for循环去迭代获取每个数值
字符串 数组
字符串数组在ctypes中的行为更接近于C语言中的字符串数组,其需要采用二维数组的形式来实现,而不是Python中的一维数组。首先,需要通过c_char类型乘上一个数,得到一个字符串类型,而后将此类型再乘上一个数,就能得到可以包含多个字符串的字符串数组。例:
实例化了一个3字符串数组,每个字符串最大长度为10。
((c_char * 10) * 3)()
对于C语言而言,上述的字符串数组实例可直接当做字符串指针传入C函数,其行为等同于在C中声明的char (*)[10] 指针。下详细讨论Python中对此对象的处理。
首先,字符串数组也是可迭代对象,可通过for循环迭代取值,对于上例的对象,其for循环得到的每一个值,都是一个10个长度的字符串对象。这样的字符串对象有两个重要属性:value和raw。value属性得到是普通字符串,即忽略了字符串终止符号(即C中的\0)以后的所有内容的字符串,而raw字符串得到的是当前对象的全部字符集合,包括终止符号。也就是说,对于10个长度的字符串对象,其raw的结果就一定是一个10个长度的字符串。例:
for i in ((c_char * 10) * 3)():
print(i.value)
print(i.raw)
上述代码中,i.value 的输出全为空字符串(b’’),而对于 i.raw,其输出则为b’\x00\x00…’,总共10个 \x00。也就是说,
value 会忽略字符串终止符号后的所有字符,是最常用的取值方式,
raw得到不忽略终止字符的字符串。
create_string_buffer
接下来讨论 ctypes 中对字符串对象的赋值方法。由于 ctypes 的字符串对象通过某个固定长度的字符串类实例化得到,故在赋值时,这样的字符串对象只可以接受等同于其声明长度的字符串对象作为替代值,这是普通 Python 字符串做不到的。要得到这样的定长字符串,需要用到 ctypes 的create_string_buffer 函数。
ctypes 提供了 create_string_buffer() 函数创建一定长度的内存区域。当前的内存块 内容可以通过raw 属性 存取,如果是创建 NULL 结束的字符串,使用 value 属性获取内存块的值
# -*- coding: utf-8 -*-
from ctypes import *
p = create_string_buffer(b"hello", 10) # create a 10 byte buffer
print(sizeof(p), repr(p.raw))
# 快速创建内存区域的方法
p = create_string_buffer(3) # create a 3 byte buffer, initialized to NUL bytes
print(sizeof(p), repr(p.raw)) # 3 b'\x00\x00\x00'
# create a buffer containing a NUL terminated string
p = create_string_buffer(b"Hello")
print(sizeof(p), repr(p.raw)) # 6 b'Hello\x00'
print(repr(p.value)) # b'Hello'
p = create_string_buffer(b"Hello", 10) # create a 10 byte buffer
print(sizeof(p), repr(p.raw)) # 10 b'Hello\x00\x00\x00\x00\x00'
p.value = b"Hi"
print(sizeof(p), repr(p.raw)) # 10 b'Hi\x00lo\x00\x00\x00\x00\x00'
create_string_buffer 函数用于创建固定长度的带缓冲字符串。其接受两个参数,
第一参数:字节字符串。必须是字节类型的字符串
第二参数:目标长度,
返回值:为被创建的定长度字符串对象,可以赋值给字符串数组中的某个对象。
在 Python2中,普通的字符串就是字节字符串,在Python3中,所有的字符串默认为Unicode字符串,故可以通过字符串的 encode()、decode() 方法进行编码方式的转化。
from ctypes import *
charList = ((c_char * 10) * 3)()
strList = ['aaa', 'bbb', 'ccc']
for i in range(3):
charList[i] = create_string_buffer(strList[i].encode(), 10)
for i in charList:
print(i.value)
或者这样写:
from ctypes import *
生成一个长度为 10 的前面部分是 abcdefg ,后面用 NULL 补齐的字符数组。
temp_string = create_string_buffer(b"abcdefg", 10)
print(temp_string)
print(temp_string.value)
print(temp_string.raw)
注意,通过create_string_buffe
注意,通过create_string_buffer函数创建的字符串对象,其长度必须严格等同于被赋值的字符串对象的声明长度,即如果声明的是10长度字符串,那么create_string_buffer的第二参数就必须也是10,否则代码将抛出TypeError异常,提示出现了类型不一致
在字符串数组的初始化过程中,这样的字符串对象也可作为初始化的参数。例:
from ctypes import *
strList = ['aaa', 'bbb', 'ccc']
charList = ((c_char * 10) * 3)(*[create_string_buffer(i.encode(), 10) for i in strList])
for i in charList:
print(i.value.decode())
上述代码将实例化与初始化合并,通过列表推导式得到了3个10长度的缓冲字符串,并使用星号展开,作为实例化的参数。则这样得到的charList效果等同于上例中通过依次赋值得到的字符串数组对象。最后通过for循环输出字符串对象的value属性(一个bytes字符串),且通过decode方法将bytes转化为str。
参数 的 引用 传递 ( byref )
有时候 C 函数接口可能由于要往某个地址写入值,或者数据太大不适合作为值传递,从而希望接收一个 指针 作为数据参数类型。这和 传递参数引用 类似。
ctypes 暴露了 byref() 函数用于通过引用传递参数,使用 pointer() 函数也能达到同样的效果,只不过 pointer() 需要更多步骤,因为它要先构造一个真实指针对象。所以在 Python 代码本身不需要使用这个指针对象的情况下,使用 byref() 效率更高。
byref 不会构造一个指针对象,因此速度比 pointer 快。( byref() 只是用来传递引用参数 )。其 _obj 是只读属性,不能更改。
指针和引用 是非常常用的(特别在C中)
byref(temp_var) temp_var 的 引用
pointer(i) temp_var 的 指针
其中如果不是必须使用指针的话,引用会更快。
>>> i = c_int()
>>> f = c_float()
>>> s = create_string_buffer(b'\000' * 32)
>>> print(i.value, f.value, repr(s.value))
0 0.0 b''
>>> libc.sscanf(b"1 3.14 Hello", b"%d %f %s",
... byref(i), byref(f), s)
3
>>> print(i.value, f.value, repr(s.value))
1 3.1400001049 b'Hello'
>>>
指针类型 ( POINTER() )
POINTER() 用于定义 某个类型 的 指针。 POINTER 返回 类型对象。
其实就是:POINTER 就是用来定义 指针类型。
使用 POINTER() 创建指针 总共2步操作:
首先 使用 POINTER 来定义 指针类型,
然后 再通过 指针类型 去 创建指针,
from ctypes import *
# POINTER(c_int) 相当于 C/C++ 中 int*
# POINTER(c_double) 相当于 C/C++ 中 double *
# POINTER(c_float) 相当于 C/C++ 中 float*
int_ptr = POINTER(c_int)
int_99 = c_int(99)
ptr = int_ptr(int_99) # 相当于 C/C++ 中 int* ptr = &int_99
print(ptr)
print(ptr[0])
print(ptr.contents)
print(ptr.contents.value)
# <__main__.LP_c_long object at 0x0000026597FB1AC0>
# 99
# c_long(99)
# 99
POINTER() 常用的作用:给 arg