python-ctype库

ctypes是一个超级强大的Python库

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

最近要使用python调用C++编译生成的DLL动态链接库,因此学习了一下ctypes库的基本使用

一、重点讲解 调用方式导出

在Windows上动态库是dll文件,在linux上动态库是so文件。下面以windows上的操作为例
C++ 的函数现有两种调用约定:__cdecl __stdcall

ctypes.CDLL() 和 ctypes.WinDLL() 加载DLL方式

其对应的python ctypes有两种加载方法,如下

c++ 函数调用方式 python ctypes加载方式
__cdecl ctypes.CDLL(dll名字) dll = CDLL(r’MFCLibrary1.dll’)
__stdcall ctypes.WinDLL(dll名字) dll = WinDLL(r’MFCLibrary1.dll’)

c++ 函数的调用声明方式

前两种为 __cdecl, 最后一种为 __stdcall

1
2
3
extern "C"	__declspec(dllexport) int test2(std::string test_string); //c++默认 __cdecl    py:ctypes.CDLL
extern "C" __declspec(dllexport) int __cdecl test3(std::string test_string); // __cdecl py:ctypes.CDLL
extern "C" __declspec(dllexport) int __stdcall test4(std::string test_string) //__stdcall py:ctypes.WinDLL
标志 作用
extern “C” 是为了不改名,函数仅加 _ 下划线 必须使用,否则编译器会把函数改名.
__declspec(dllexport) 是为了导出函数,可以被外部找到 否则 dll 外部无法找到
__cdecl vs 默认的调用约定 上述 test2 函数没有该标志,其实默认就如此,用cytpes.CDll加载
__stdcall stdcall调用约定 需要使用 ctype.WinDll 加载dll 才可以

二、Python 调用 C/C++

visual stdio创建动态链接库

  1. 新建动态链接库项目
  2. 新建main.cpp文件
  1. main.cpp中添加include “pch.h” 另外函数的前面都加上_declspec(dllexport)前缀。Ctrl+b后就可以生成dll文件
    dll文件所在路径:E:\main\x64\Debug\main.dll

main.c

1
2
3
4
5
6
7
8
9
10
#include "pch.h"
#include <stdio.h>


#define DLLEXPORT extern "C" __declspec(dllexport)


DLLEXPORT void test() {
printf("hello world\n");
}
  1. python中调用方法如下。首先加载dll文件。然后调用其中的方法
1
2
3
4
5
6
7
from ctypes import *

dll = CDLL(r"E:\main\x64\Debug\main.dll")
print(dll) # <CDLL 'E:\main\x64\Debug\main.dll', handle 7ffa49190000 at 0x2745b985790>
func = dll.test
print(func) # <_FuncPtr object at 0x000001D6C40771E0>
print(func()) # hello world

函数参数的 ctypes数据类型

ctypes定义了一些和C兼容的基本数据类型:

ctypes 类型 C 类型 Python 类型
c_bool _Bool bool (1)
c_char char 单字符字节串对象1-character string
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) 字节串对象或 None
c_wchar_p wchar_t* (NUL terminated) 字符串或 None
c_void_p void* int 或 None

根据c语言中的函数,参数传递,参数返回等特性,总结了7大使用场景

场景一:c函数无输入参数,返回一个整数

c代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "pch.h"
#include <stdio.h>


#define DLLEXPORT extern "C" __declspec(dllexport)


DLLEXPORT void test() {
printf("hello world\n");
}

DLLEXPORT int test_int()
{
return 5;
}

Python中的调用restype指定了函数的返回类型,是int类型。然后直接调用函数,获取返回值

1
2
3
4
5
6
from ctypes import *

dll = CDLL(r"E:\main\x64\Debug\main.dll")
dll.test_int.restype = c_int
pt=dll.test_int()
print(pt) # 5

场景二:c函数无输入参数,返回一个整数类型的指针

c代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "pch.h"
#include <stdio.h>


#define DLLEXPORT extern "C" __declspec(dllexport)


DLLEXPORT int * test_int_p()
{
printf("test_int_p\n");
int num = 10;
return &num;
}

Python代码 restype指定为POINTER(c_int),获取到返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from ctypes import *

dll = CDLL(r"E:\main\x64\Debug\main.dll")

# 设置返回值类型为指针
# 通过POINTER(ctypes type)定义指针类型
dll.test_int_p.restype = POINTER(c_int) # 等价于C的 int * test_int_p
# 调用函数
ret = dll.test_int_p()

# 获取指针指向的整数值
result = ret.contents.value

print(result) # 10

场景三:c函数无输入参数,返回一个指向整数类型的指针的指针

1
2
3
4
5
6
DLLEXPORT int ** test_int_p()
{
int* num = (int*)malloc(sizeof(int)); // 分配内存
*num = 10; // 设置整数的值
return &num; // 返回指向整数指针的指针
}

Python代码 restype指定为POINTER(POINTER(c_int)),获取到返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from ctypes import *

dll = CDLL(r"E:\main\x64\Debug\main.dll")

# 设置返回值类型为指针
# 通过POINTER(ctypes type)定义指针类型
dll.test_int_p.restype = POINTER(c_int) # 等价于C的 int * test_int_p
# 调用函数
ret = dll.test_int_p()

# 获取指针指向的整数值
# 先解引用一次获取指针,再解引用一次获取整数值
result = ret.contents.contents.value


print(result) # 10

场景四:c函数 两个int类型的参数,返回一个整数

1
2
3
4
DLLEXPORT int test_para(int a, int b)
{
return a + b;
}

因为函数有两个输入参数,因此argtypes就是传入函数的输入参数类型

1
2
3
4
5
6
7
8
9
10
from ctypes import *

dll = CDLL(r"E:\main\x64\Debug\main.dll")
# 设置返回值类型为指针
dll.test_para.restype = c_int
# 设置参数类型为整数
dll.test_para.argtypes = [c_int, c_int]
# 调用函数
ret = dll.test_para(5, 3)
print(ret) # 8

场景五: c函数接受2个int类型的指针,一个char类型的指针

c代码如下

1
2
3
4
5
6
7
DLLEXPORT int  test_para_p(int* a, int* b, const char* str)
{
int c;
c = *a + *b;
printf("test_para_p:%s\n", str);
return c;
}

参数默认情况下是按值传递的,而不是按引用传递。
但是在你的 C 函数中,你希望传递指向整数的指针作为参数,因此需要通过引用传递这些指针。
因此 使用 byref 函数,它将创建指向参数的指针,并将其传递给函数,
使用 byref 的目的是确保在调用 C 函数时传递指向整数的指针,从而允许 C 函数修改这些整数的值

python代码如下

argtypesrestype的使用方式没有变化
python中将整数转换成指针的方式是通过ctypes.byref(arg1)的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from ctypes import *

dll = CDLL(r"E:\main\x64\Debug\main.dll")

# 设置返回值类型为指针
dll.test_para_p.restype = c_int
# 设置函数参数类型为整数指针
dll.test_para_p.argtypes = [POINTER(c_int), POINTER(c_int), c_char_p]

arg1 = c_int(4)
arg2 = c_int(6)
arg3 = c_char_p(b'hello world') # 也可以用 b'hello world'
# byref相当于取引用,c字符串传字节数组
ret = dll.test_para_p(byref(arg1), byref(arg2), arg3)
# 调用函数
print(ret)
# 10
# test_para_p:hello world

场景六:c函数接收一个结构体指针,并返回这个结构体指针

c代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 定义了一个名为 MyStruct 的结构体
struct MyStruct
{
int a;
double b;
};
// 定义了一个名为 s_test 的结构体变量
// 初始化了它的成员 a 和 b 分别为 10 和 20
struct MyStruct s_test = {10,20};


// 定义了一个名为 test_struct_p 的函数
// 一个指向 MyStruct 结构体的指针作为参数
DLLEXPORT struct MyStruct* test_struct_p(struct MyStruct* arg)
{
printf("test_struct\n");
arg->a = 100;
arg->b = 200.1;
return arg;
}
//当调用 test_struct_p 函数时,
// 传入的结构体指针会被修改,
// 其成员 a 的值会变为 100,成员 b 的值会变为 200

Python代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from ctypes import *

dll = CDLL(r"E:\main\x64\Debug\main.dll")

class MyStruct(Structure):
# _fields_ 属性则指定了结构体的字段及其类型。
_fields_ = [
("a", c_int),
("b", c_double)
]


# 将函数 test_struct_p 的返回类型设置为指向 MyStruct 结构体的指针
dll.test_struct_p.restype = POINTER(MyStruct)
# 创建一个 MyStruct 类的实例
arg = MyStruct()
# 并设置其成员 a 和 b 的值为 1 和 2.0
arg.a = 1
arg.b = 2.0
# 通过 byref(arg) 将该实例的引用传递给 test_struct_p 函数
ret = dll.test_struct_p(byref(arg)).contents
print(ret.a, ret.b)
# 100 200.1
# test_struct

场景七:c函数接收一个结构体指针,这个结构体中有一个字符串数组

c代码如下

1
2
3
4
5
6
7
8
9
10
11
struct MyStruct1
{
char name[20];
};

DLLEXPORT struct MyStruct1* test_struct_char_p(struct MyStruct1* arg)
{
printf("test_struct\n");
printf("name=%s\n", arg->name);
return arg;
}

python代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from ctypes import *

dll = CDLL(r"E:\main\x64\Debug\main.dll")

# 定义C结构体
class MyStruct1(Structure):
# _fields_中申明一个20个c_char类型的name
_fields_ = [
("name", c_char * 20)
]


# 设置函数 test_struct_p 的返回类型为 MyStruct1 的指针
dll.test_struct_char_p.restype = POINTER(MyStruct1)
# 创建一个 MyStruct1 类的实例
arg = MyStruct1()

# 设置 name 成员的值为 "sun"
arg.name = b"sun"
# 通过 byref(arg) 将该实例的引用传递给 test_struct_char_p 函数
ret = dll.test_struct_char_p(byref(arg)).contents

# 打印返回值
print(ret.name)
# b'sun'
# test_struct
# name=sun

场景八:c函数接收一个结构体指针,该结构体是一个链表。

c代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Yu {
int a;
struct Yu* next;
};
struct Yu node1 = { 2000,NULL };

DLLEXPORT struct Yu* test_struct_list_p(struct Yu* arg)
{
printf("test_struct_list_p\n");
arg->a = 1000;
arg->next = &node1;
return arg;
}

python代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from ctypes import *

dll = CDLL(r"E:\main\x64\Debug\main.dll")

# 定义C结构体
class Yu(Structure):
pass


Yu._fields_ = [
("a", c_int),
("next", POINTER(Yu))
]

# 设置函数的返回类型和参数类型
dll.test_struct_list_p.restype = POINTER(Yu)
dll.test_struct_list_p.argtypes = [POINTER(Yu)]

# 创建一个 Yu 类的实例
arg = Yu()
arg.a = 10 # 初始值随便设定

# 调用函数
ret = dll.test_struct_list_p(byref(arg)).contents
# 打印返回值的属性
print(ret.a)
print(ret.next.contents.a)