0、前言

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

官方提供了详尽的文档:https://docs.python.org/zh-cn/3.12/library/ctypes.html ,配合网友的示例可以很快上手。本文主要是记录一些基本操作。

import platform
from ctypes import *

if platform.system() == 'Windows':
    libc = cdll.LoadLibrary('msvcrt.dll')
elif platform.system() =='Linux':
    libc = cdll.LoadLibrary('libc.so.6')
    
libc.printf(b'Hello ctypes!\n')

1、数据类型

ctypes 提供了一些基本数据类型用来映射 C 语言和 Python 的类型,对照表可见文档,常用的几个:

对一个 ctypes 类型乘以一个正数可以创建一个数组类型。 

# int数组
int_arr = ctypes.c_int * 5  # 相当于C语言的 int[5]
# 可以指定数据来初始化数组
my_arr = int_arr(1, 3, 5, 7, 9)
# <__main__.c_long_Array_5 object at 0x0000023A3FF4F8C0>
print(my_arr)

ctypes 预定义的指针类型只提供了几个, 可以使用 ctypes.POINTER 来定义新的指针类型,ctypes.pointer() 函数构造一个指针对象, ctypes.byref() 函数相当于对对象取地址。无参调用指针类型可以创建一个 NULL 指针, NULL 指针的布尔值是 False。

# int类型
num = ctypes.c_int(1992)
# int指针
ctypes.POINTER(ctypes.c_int)
# 对实例取地址,可作为指针参数传递
ctypes.byref(num)
# 生成一个指针对象
ctypes.pointer(num)

# 空指针
null_ptr = ctypes.POINTER(ctypes.c_int)()
print(bool(null_ptr)) # 打印 False

结构体和联合必须继承自 ctypes 模块中的 Structure 和 Union 。子类必须定义 _fields_ 属性。 _fields_ 是一个二元组列表( tuple ),二元组中包含 field name 和 field type 。

type 字段必须是一个 ctypes 类型,比如 c_int,或者其他 ctypes 类型: 结构体、联合、数组、指针。

默认情况下,结构体和联合的字段与 C 的字节对齐是一样的。也可以在定义子类的时候指定类的 _pack_ 属性指定字节对齐大小。

# 如果指定大小端,可使用基类BigEndianStructure或LittleEndianStructure
class MyStruct(ctypes.Structure):
    _pack_ = 1  # 指定为1字节对齐
    _fields_ = [
        ("a", ctypes.c_char),
        ("b", ctypes.c_int),
        ("c", ctypes.c_double)
    ]

# 打印字节大小,测试pack设置是否有效
print("sizeof MyStruct:", ctypes.sizeof(MyStruct))

2、变量访问与函数调用 

使用 ctypes.cdll.LoadLibrary(path) 加载对应的 dll 后,可以访问 C/C++ 导出的变量/函数符号。如果是 C++ 编译器,需要使用 extern "C"。此外,编译时选择的位数应和使用的 Python 位数一致,比如都是 x86 或者 x64 的。

在 Windows MSVC 中,可以这样写:

#ifdef MYDLL_EXPORTS
#define MYDLL_API __declspec(dllexport)
#else
#define MYDLL_API __declspec(dllimport)
#endif

extern "C"
{
	// 导出变量
	extern MYDLL_API int my_number;
	// 导出函数
	MYDLL_API int my_func(int a, int b);
}

对于上面导出的变量和函数,访问方式如下:(可以看到函数可以直接访问,变量通过 type.in_dll() 函数访问)

import ctypes

dll = ctypes.cdll.LoadLibrary("D:/TestSpace/TestCpp_mydll/x64/Release/mydll.dll")
# 访问变量
print(dll.my_number)  # 打印<_FuncPtr object at 0x0000022537F1B450>,默认应该是当作函数访问的
print(ctypes.c_int.in_dll(dll, 'my_number').value)
# 访问函数
print(dll.my_func(12, 34))

ctypes 默认假定函数返回 int 类型,可以设置函数对象的 restype 属性来指定具体类型。对于参数类型也可以通过设置函数对象的 argtypes 来指定具体类型,防止不合理的参数传递。

my_func = dll.my_func # 函数对象
my_func.argtypes = [ctypes.c_double, ctypes.c_double] # 指定参数类型
my_func.restype = ctypes.c_double # 指定返回值类型
print(my_func(12, 34.5))

对于指针参数或者结构体参数的构造参见上一节,或者下面测试代码的内容。  

3、测试代码

(C/C++ 代码在 Windows VS 环境编写)

// 下列 ifdef 块是创建使从 DLL 导出更简单的宏的标准方法。
// 在预处理器中定义了 MYDLL_EXPORTS 符号,而调用方不包含此符号。
// 源文件中包含此文件的任何其他项目都会将 MYDLL_API 视为是从 DLL 导入的,
// 而此 DLL 则将用此宏定义的符号视为是被导出的。
#ifdef MYDLL_EXPORTS
#define MYDLL_API __declspec(dllexport)
#else
#define MYDLL_API __declspec(dllimport)
#endif

// ctypes需要提供c接口,主要是目前cpp还没有统一的abi
extern "C"
{
	// 导出一个全局变量
	extern MYDLL_API int my_number;
	// 函数
	MYDLL_API int my_func(int a, int b);
	MYDLL_API double my_func2(double a, double b);
	MYDLL_API int * my_func3(char *a, double *b, const char *str);
	// 指针
	extern MYDLL_API char *char_ptr;
	extern MYDLL_API int *int_ptr;
	extern MYDLL_API int *null_ptr;
	// 打印dll中的变量
	MYDLL_API void print_var();
	// 结构体
	struct MYDLL_API MyStruct
	{
		int a;
		double b;
	};
	MYDLL_API MyStruct my_func4(const MyStruct &arg);
	MYDLL_API MyStruct * my_func5(MyStruct *arg);
}
// MSVC模板默认带pch,设置为不使用预编译头
// #include "pch.h"
#include "mydll.h"
#include <iostream>

MYDLL_API int my_number = 1992;

MYDLL_API int my_func(int a, int b)
{
	return a + b;
}

MYDLL_API double my_func2(double a, double b)
{
	return a + b;
}

MYDLL_API int * my_func3(char * a, double * b, const char *str)
{
	std::cout << "dll myfunc3:" << str << std::endl;
	my_number = *a + int(*b);
	return &my_number;
}

MYDLL_API char *char_ptr = new char(65);
MYDLL_API int *int_ptr = new int(250);
MYDLL_API int *null_ptr = nullptr;

MYDLL_API void print_var()
{
	std::cout << "dll print var:"
		<< "\n\tmy number:" << my_number
		<< "\n\tchar ptr:" << *char_ptr
		<< "\n\tint ptr:" << *int_ptr << std::endl;
}

MYDLL_API MyStruct my_func4(const MyStruct & arg)
{
	MyStruct ret{ 12,34.5 };
	std::cout << "dll myfunc4:"
		<< "in:" << arg.a << " " << arg.b
		<< "ret:" << ret.a << " " << ret.b << std::endl;
	return ret;
}

MYDLL_API MyStruct * my_func5(MyStruct * arg)
{
	if (arg) {
		arg->a = 67;
		arg->b = 89.5;
	}
	return arg;
}
import ctypes

# dll = ctypes.cdll.LoadLibrary("msvcrt.dll")
# dll.printf(b"hello ctypes\n")

dll = ctypes.cdll.LoadLibrary(
    "D:/TestSpace/TestCpp_20210617_mydll/x64/Release/mydll.dll")
# 访问变量
print(dll.my_number)  # 打印<_FuncPtr object at 0x0000022537F1B450>,默认应该是当作函数访问的
print(ctypes.c_int.in_dll(dll, 'my_number').value)
# 访问函数
print(dll.my_func)
print(dll.my_func(12, 34))
# ctypes 默认假定函数返回 int 类型,可以设置函数对象的 restype 属性来指定具体类型。
# 对于参数类型也可以通过设置函数对象的 argtypes 来指定具体类型,防止不合理的参数传递。
my_func2 = dll.my_func2
my_func2.argtypes = [ctypes.c_double, ctypes.c_double]
my_func2.restype = ctypes.c_double
print(my_func2(12, 34.5))
my_func3 = dll.my_func3
my_func3.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_double)]
my_func3.restype = ctypes.POINTER(ctypes.c_int)
arg1 = ctypes.c_char(12)
arg2 = ctypes.c_double(1000)
# byref相当于取引用,c字符串传字节数组
ret = my_func3(ctypes.byref(arg1), ctypes.byref(arg2), b'hello ctypes.')
print(ret.contents.value)

dll.print_var()
# 指针变量
# ptr = ctypes.POINTER(ctypes.c_int)
# char(65)->print(b'A'),可能会把后面挨着的字节内容也打印
print(ctypes.c_char_p.in_dll(dll, 'char_ptr').value)
print(ctypes.POINTER(ctypes.c_int).in_dll(dll, 'int_ptr').contents.value)
ctypes.POINTER(ctypes.c_int).in_dll(dll, 'int_ptr').contents.value = 520
# 空指针bool值为false
print(bool(ctypes.POINTER(ctypes.c_int).in_dll(dll, 'null_ptr')))
dll.print_var()

# struct或union必须派生ctypes给的基类Structure和Union
# 每个子类都必须定义一个_fields_属性,_fields_必须是一个2-tuples列表,包含一个字段名和一个字段类型。


class MyStruct(ctypes.Structure):
    _fields_ = [
        ("a", ctypes.c_int),
        ("b", ctypes.c_double)
    ]


my_func4 = dll.my_func4
my_func4.restype = MyStruct
arg = MyStruct()
arg.a = 10
arg.b = 11
# print(ctypes.sizeof(MyStruct))
ret = my_func4(arg)
print('my_func4 ret:', ret.a, '  ', ret.b)
my_func5 = dll.my_func5
my_func5.restype = ctypes.POINTER(MyStruct)
ret = my_func5(ctypes.byref(arg)).contents
print('my_func5 ret:', ret.a, '  ', ret.b)

4、参考

参考文档:ctypes --- Python 的外部函数库 — Python 3.9.17 文档

参考博客:浅谈python中使用C/C++:ctypes - 知乎

参考博客:python3 ctypes模块使用方法与心得体会--- int* ,char*等指针类型互转_ctypes int指针_Fu_Lin_的博客-CSDN博客

参考博客:https://www.cnblogs.com/gaowengang/p/7919219.html

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐