Bootstrap

Python 使用 ctypes 调用 C/C++ DLL 动态链接库

Python ctypes模块优点与适用场景

ctypes 有以下优点:
  • Python内建,不需要单独安装
  • Python可以直接调用C/C++ 动态链接库(.dll 或 .so)
  • 在Python一侧,不需要了解 c/c++ dll 内部的工作方式
  • 提供了 C/C++ 数据类型与Python类型的相互映射,以及转换,包括指针类型。
ctypes 在下列场景可以发挥较大作用
  • 运算量大的操作可以写成 C/C++ dll, python 通过 ctypes 来调用, 大幅提升Python代码性能。
  • python可以直接使用 C/C++一些优秀库资源,如boost库等。
  • ctypes 令python也可以使用指针类型,方便大块数据的处理,也可以应用于多进程、多线程之间的数据通信。
  • 第3方软件或硬件提供的SDK库,通过ctypes来实现调用对接。理论上C/C++的库都可以对接,这比其它语言方便得多。

Python之所以流行度高,有一个重要的原因:大量的C/C++优秀资源,通过ctypes 封装成python库,因此,掌握了这个工具以后,相信我,在大部分项目上将给你带来惊喜。

当然,除了ctypes 外,还可以通过 Cython,python C API 原生API来调用 C/C++ 函数,或者利用第3方工具包来完成类似的工作,如

  • Swig
  • pybind11

但本文只着眼于介绍 ctypes 模块, 并尝试由浅入深,完整地展现ctypes的主要功能以及使用方法:

  • ctypes模块的数据类型的使用
  • 在python中申明DLL函数参数类型
  • 如何导入实参调用DLL函数,获得并解析返回值。

在写作风格上, 以易于理解的方式来阐述。ctypes 官方文档,基本是一份产品说明书,阅读比较耗精力。 而本文旨在给提供一份优秀教程,尽可能有条理地讲清楚原理、通过实例代码演示如何使用,同时也涉及一些更复杂的使用场景,如回调函数等。

1、准备C++ DLL动态链接库

在示例讲解之前,我们先用Visual Studio2022创建并编译1个DLL项目

1) 创建新项目, 选择dll项目

在这里插入图片描述

至此创建完成,包含源文件dllmain.cpp、pch.cpp,头文件framework.h、pch.h。 在这里插入图片描述

2) 定义头文件 boFirst.h

#define IMPORT_DLL __declspec(dllimport) 定义了IMPORT_DLL 宏, __declspec(dllimport)表示函数将被编译为dll
extern "C" IMPORT_DLL  ==》 指定该函数可被外部调用。 
#pragma once

#ifdef DLL_EXPORT
#define IMPORT_DLL __declspec(dllexport)
#else
#define IMPORT_DLL __declspec(dllimport)
#endif

//自定义结构体
struct boShape
{
    char  shape[20];
    float width; 
    float height;
    float depth; 
};

// 自定义函数
extern "C" IMPORT_DLL int bo_add(int a, int b); 
//自定义函数,参数为结构体 boStruct 
extern "C" IMPORT_DLL float bo_shape_vol(boShape bs);

这里分别添加bo_add, bo_shape_vol 两个可供外部调用函数声明,
也可以更简化,只要函数前面有extern “C” __declspec(dllexport) 即可。

#ifndef _PY_LIST_2_C_ARRAY_H_
#define _PY_LIST_2_C_ARRAY_H_

#include <stdio.h>

typedef struct {
    int value;
    wchar_t* name;
} Item;

extern "C" __declspec(dllexport)
int sum_diagonal(Item** field, size_t size);

#endif

3)实现函数代码

// boFirst.cpp : Defines the exported functions for the DLL.
#include "pch.h" // use stdafx.h in Visual Studio 2017 and earlier
#include <utility>
#include <limits.h>
#include "boFirst.h"
# include <iostream>

int bo_add(int a, int b)
{
    int c;
    c = a + b; 
    return c; 
}

float bo_shape_vol(boShape bs)
{
    float volume = bs.width * bs.height * bs.depth;  
    std::cout << bs.shape << "volume is " << volume << std::endl; 
    return volume; 
}

4) 编译生成dll

选择build solution , 输出窗口可以看到,在x64\Debug\ 目录下生成了DLL_Test.dll 文件

Rebuild started...
1>------ Rebuild All started: Project: DLL_Test, Configuration: Debug x64 ------
1>pch.cpp
1>dllmain.cpp
1>boFirst.cpp
1>D:\workplace\temp\C++\DLL_Test\DLL_Test\boFirst.cpp(8,5): warning C4273: 'bo_add': inconsistent dll linkage
1>D:\workplace\temp\C++\DLL_Test\DLL_Test\boFirst.h(20,27): message : see previous definition of 'bo_add'
1>D:\workplace\temp\C++\DLL_Test\DLL_Test\boFirst.cpp(15,7): warning C4273: 'bo_shape_vol': inconsistent dll linkage
1>D:\workplace\temp\C++\DLL_Test\DLL_Test\boFirst.h(22,29): message : see previous definition of 'bo_shape_vol'
1>Generating Code...
1>   Creating library D:\workplace\temp\C++\DLL_Test\x64\Debug\DLL_Test.lib and object D:\workplace\temp\C++\DLL_Test\x64\Debug\DLL_Test.exp
1>DLL_Test.vcxproj -> D:\workplace\temp\C++\DLL_Test\x64\Debug\DLL_Test.dll
1>Done building project "DLL_Test.vcxproj".
========== Rebuild All: 1 succeeded, 0 failed, 0 skipped ==========

2、通过 ctypes 调用DLL的简单示例

在上一节中的dll有两个方法,其中1个输入参数使用了C++ Struct 结构体, python没有对应的数据结构。

第1种调用方式 (标准方式):

Step-1:

将DLL_Test.dll 文件拷贝至python文件所在目录,
该dll中包含如下可供调用的函数

int bo_add(int a, int b)
float bo_shape_vol(boShape bs); 其中boShape为自定义struct 
Step-2:

编写代码,调用 bo_add()方法

from ctypes import *
dll = cdll.LoadLibrary("./DLL_Test.dll")
print("bo_add ", dll.bo_add(20, 80))

运行,即可以得到输出 100

cdll.LoadLibrary() 方法返回 cdll 对象,cdll 调用C/C++函数的方法遵从 cdecl 方式(C/C++函数调用的标准方式)。

上例是直接调用 dll中的函数.

第2种调用方式( 函数签名方式)

函数签名方式,相当于在python内重新申明1个C++函数的别名,申明内容包括用ctype指定函数形参与返回值的数据类型。

import ctypes
clibrary = ctypes.CDLL('clibrary.so')

# 定义1个python函数名,指向 C++ 函数
addTwoNumbers = clibrary.add

# 用ctypes 数据类型,定义函数的参数类与返回值类型, 
addTwoNumbers.argtypes = [ctypes.c_int, ctypes.c_int]
addTwoNumbers.restype = ctypes.c_int

# 使用签名函数
print("Sum of two numbers is :", addTwoNumbers(20, 10))

Output:
Sum of two numbers is : 30

ctypes 数据类型 --> Python 数据类型的转换是程序在后台自动完成的。

3、ctypes 数据类型

C/C++的数据类型与 Python数据类型及使用方式都有较多差异,因此python调用C++函数,必须需要对DLL函数形参以及返回值类型进行转换,才能把数据传入DLL,以及解析DLL的返回值。ctypes 模块提供了中间数据类型来帮助Python完成数据转换。

我们先看一下Python 调用DLL函数时,都做了哪些工作:

  1. 在Python中重新申明 DLL 函数: 除了函数名之外,还要用ctypes的数据类型将 DLL 函数的形参与返回值重新申明一遍。
    形参:函数定义时函数括号内的参数叫形参
    实参:函数调用时,实际传入的参数叫实参
  2. 实参准备:当实际调用该函数时,要将python类型数据转换为ctypes实参变量后,再调用DLL函数。
  3. 解析返回值:如果函数有返回值,还需要将返回值从ctypes 类型中解析为python类型。

其中,第1步,用ctypes 类型申明 DLL函数形参最为关键,可以认为这一步目的是将 C++的数据类型转换为Python数据类型,其中 ctypes 起到了桥梁作用。

下面介绍 ctypes 提供的数据类型,以及如何使用。

1) ctypes 与c、python数据类型的对应关系

在这里插入图片描述

2) 整数型参数定义

i = c_int()

赋值 :

i = c_int(99)  # 初始始化时赋值
j = c_int()    # 等创建后赋值
j.value=99     
print(j) 
c_long(99)

int类型的指针: ctyps中的 c_void_p 类型,目前只能指向 int 型数值

>>> ptr1 = c_void_p(99)
>>> ptr1
c_void_p(99)
>>> ptr1.value
99
>>>

3) 浮点类型参数

d1 = c_float()
d1.value=10.3
d1
c_float(10.300000190734863)
>>> d1.value
10.300000190734863

4) 字符串参数

字符串数组
C/C++本身没有string类型,字符串是用字符数组来表示。 ctypes 也是按这种方式表示字符串。
如: char * 32 表示定义 1个32个字符的字符串,赋值用 value属性

>>> str1 = c_char * 32
>>> str1
<class '__main__.c_char_Array_32'>
>>> str1.value = b"hello"
>>> str1
<class '__main__.c_char_Array_32'>
>>> str1.value
b'hello'

注意:由于C++ char数组每个元素都是1个字节,因此对应到python类型是 bytearray字节数组类型,因此须使用二进制形态的字符串,如b’hello, world’, 中文须按utf-8编码成二进制。

字符串指针
C/C++中字符串也可以用char* 指针来表示,ctypes 提供对应的字符串指针类型 c_char_p。
给c_char_p字符串指针赋值通常有两种方式:

  • 把 python 字符串转为 bytes 类型,使用endcode()方法。
  • 直接使用bytes 类型字节串。

将字符串转换为 bytes 对象后,用c_char_p的value属性赋值, 用法示例 str2.value = x 赋值。

>>> x = b"abcdef"
>>> str2 = c_char_p()
>>> str2.value = x
>>> str2
c_char_p(140250436808304)
>>> str2.value
b'abcdef'
>>> str2 = str1
>>> str2
<class '__main__.c_char_Array_32'>
>>> str2.value
b'hello'

c_char_p 指向的数据类型必须是二进制编码,即Bytes类型,如果是中文,可以用utf-8编码,显示时再解码.

>>> y=bytes("hello, 小王","utf-8")
>>> str2 = c_char_p(y)
>>> str2.value
b'hello, \xe5\xb0\x8f\xe7\x8e\x8b'
>>> str2.value.decode(encoding="utf-8")
'hello, 小王'

ctypes 还提供了 create_string_buffer() 方法用于生成字符串
格式: ctypes.create_string_buffer(init_or_size, size=None)

>>> str3 = create_string_buffer(b'world',32)
>>> str3
<__main__.c_char_Array_32 object at 0x7f8e9826eac0>
>>> str3.value
b'world'
>>> str3.raw
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
>>> repr(str3.raw)

create a 3 byte buffer, initialized to NUL bytes

p = create_string_buffer(3)

create a buffer containing a NUL terminated string

p = create_string_buffer(b'Hello')
print(sizeof(p), repr(p.raw))

create a 10 byte buffer

p = create_string_buffer(b'Hello', 10)
print(sizeof(p), repr(p.raw))

5) 数组类型变量

数组的创建和C/C++语言的类似,给定数据类型和长度即可
如定义 c_int 类型数组, 长度为10. 先定义1个 INT_ARRAY 类型,再创建1个数组变量,其类型为 c_long_Array_10, 示例代码:

>>> INT_ARRAY = c_int * 10
>>> array1 = INT_ARRAY(10,20,30,40,50,60,70,80,90,100)
>>> array1
<__main__.c_long_Array_10 object at 0x00000283665D7040>
>>> array1[5]
60
>>> array1[7:]
[80, 90, 100]

创建数据变量过程可以将两步并为1步,

>>> array2 = (c_int * 10)(10,20,30,40,50,60,70,80,90,100)
>>> array2
<__main__.c_long_Array_10 object at 0x0000028366558B40>
>>> array2[7:]
[80, 90, 100]

6) Structure 变量

Python的dict类型虽然与C/C++ struct 结构体类型很相似,但不能直接拿来用。ctypes模块提供了 Structure 类来进行 C/C++ struct结构体类型转换。
示例,定义1个POINT 像素点结构体

class POINT(Structure):
...     _fields_ = [("x", c_int),
...                 ("y", c_int)]
>>> point = POINT(10, 20)
>>> print(point.x, point.y)
10 20

定义1个RECT 结构体,其字段为POINT结构体

>>> class RECT(Structure):
...     _fields_ = [("upperleft", POINT),
...                 ("lowerright", POINT)]
...
>>> rc = RECT(POINT(1, 2), POINT(3, 4))
>>> print(rc.upperleft.x, rc.upperleft.y)
1 2
>>> print(rc.lowerright.x, rc.lowerright.y)
3 4

1个更复杂的结构体,
字段 b , 为浮点数, “point_arrary” 为1个包含POINT结构体类型的Array

>>> class MyStruct(Structure):
...     _fields_ = [("a", c_int),
...                 ("b", c_float),
...                 ("point_array", POINT * 4)]

说明: python3 新增了struct 模块,基于序列化的原理,可将不同类型的数据打包进 ctypes bytes buffer变量,用于向c/c++ struct 变量传值, 但直观度不如 Structure变量,有兴趣者可以深入了解一下 python struct 模块的使用

7) 指针变量

ctypes提供了pointer()和POINTER()两种方法创建指针

pointer( object )用于将某个类型的对象转化为指针
其参数必须是ctypes的变量对象,不能是类型. 如下例,int_p 指向1个c_int 对象的指针,当前值为99
int_obj = c_int(99)
int_p = pointer(int_obj)
print(int_p)
使用contents方法访问指针
print(int_p.contents)
# 获取指针指向的值
print(int_p[0])

output 如下

<ctypes.wintypes.LP_c_long object at 0x00000217897102C0>
c_long(99)
99
POINTER()用于定义指针变量类型

POINTER() 的参数必须是类型名称,相当于定义了新的1个指针变量类型,但指向值的类型必须是指定的类型名称。然后再用这个新类型实例化1个指针变量。
而 pointer() 方法隐式地完成了POINTER()的工作。

# 指针类型
INT_P  = POINTER(c_int)  # 定义了1个新的指针变量类型
# 实例化
int_obj = c_int(4)
int_p_obj = INT_P(int_obj)   # 实例化1个指针变量
print(int_p_obj)
print(int_p_obj.contents)
print(int_p_obj[0])

output :

<__main__.LP_c_int object at 0x7f47df7f79e0>
c_int(4)
4
指针类型的转换
ctypes提供 cast() 方法将一个ctypes对象转换为指向另一种ctypes数据类型的指针,cast()接受两个参数,一个是ctypes对象,另一个是ctypes指针类型。它返回第二个参数的一个实例,该实例引用与第一个参数相同的内存块。
# 初始化 1个 c_int的指针,当前值为4
int_p = pointer(c_int(99))
print(int_p)
# 定义1个 c_char类型的指针类型
char_p_type = POINTER(c_char)
print(char_p_type)
# 转 c_int变量转换为c_char类型指针
cast_type = cast(int_p, char_p_type)
print(cast_type)

output:

<ctypes.wintypes.LP_c_long object at 0x0000021789710140>
<class 'ctypes.LP_c_char'>
<ctypes.LP_c_char object at 0x0000021789710E40>

函数参数指针类型时,如何传入实参
如同C语言,如果函数参数为指针类型,调用时,对于实参,可以传入指针变量,或者传入引用地址。
如ctypes对函数声明如下:

sum = dll.mysum
sum.restype = c_int
sum.argtypes = ( POINTER(c_int)) 

实际调用时:

data = ( c_int * 5)(10,20,30,40,50)
# 使用指针方式传入参数
ptr = pointer(data) 
res = sum( ptr ) 
print(res) 
# 使用引用方式传入参数
res = sum( byref(data) ) 
print(res) 

两种方式传入实参,效果是一样的。

8) enum 枚举类型

在 C/C++中还有1个经常用到的类型是 enum,ctypes 没有相关类型,但python有1个enum类. 下面介绍如何 将 python enum 类用于 dll 函数参数。
如 C++程序中定义了1个枚举类型

	enum Priority {
		CRITICAL = 0x1,		
		IMPORTANT= 0x2,		
		NORMAL = 0x3,		
		MINOR = 0x4,	
		INFO = 0x5	
	};

有1个函数: long set_priority(Priority n)
在python中,也定义1个enum类

import enum 
class EnumPriority(enum.IntEnum):
	CRITICAL = 0x1
	IMPORTANT= 0x2
	NORMAL = 0x3
	MINOR = 0x4
	INFO = 0x5
	
    #注意必须将添加这个方法,ctyps 要求,将obj转为 init型
    @classmethod
    def from_param(cls, obj):
        return int(obj)

注意,必须要添加1个 from_param()方法., 将上面的枚举元素转为int型
下面将 python enum 做为参数传入dll

dll =  cdll.LoadLibrary(dll_name)
set_priority=dll.set_priority
set_priority.restype = c_init
set_priority.argtypes = [EnumPriority]
# 调用该函数
print("call back result is ",set_priority(EnumPriority.INFO))

4. 加载DLL库

Ctypes 提供有2种动态链接库的调用方式

ctypes.cdll. LoadLibrary(‘xxx.dll’)
加载 cdel调用方式的dll, 默认c++, vc++生成的dll库都是cdel方式。

ctypes.windll.LoadLibrary(‘xxx.dll’)
加载 win32调用方式(stdcall)的dll,所以很少使用此种调用方式

示例

def find_example_ctypes(required):
    '''
    Finds and loads example shared object of the required major
    '''
    # Importing ``ctypes`` should be in scope of this function to prevent failure
    from ctypes import util, cdll

    so_name = util.find_library('example.dll')
    if so_name is None:
        raise ExampleImportError('EXAMPLE shared object not found.')
    example = cdll.LoadLibrary(so_name)
    require_version(example.example_version(), required)
    return example 

5. ctypes 向DLL函数传入参数

指定dll函数参数类型
通过argtypes属性来设置参数类型,

>>> strchr.restype = c_char_p
>>> strchr.argtypes = [c_char_p, c_char]

例2:

myadd = dll.bo_add
myadd.restype = c_int
myadd.argtypes = [c_int, c_int]
print("call bo_add() with function signature approach \n", myadd(20, 80))

传入指针参数
c++ 函数addx()的形参为两个指针, int addx( int *p1, int *p2), python 实例两个c_int型的指针传入

int_obj1 = c_int(20)
ptr1 = pointer(int_obj1)
int_obj2 = c_int(80)
ptr2 = pointer(int_obj2)
print("pass pointer to function addx(), ", dll.addx(ptr1, ptr2))

传入结构体参数

步骤:

  • 在python定义1个Ctypes.Structure 类, 字段也C++ Struct类型保持一致。
  • 用此类构建参数值
  • 传入dll函数,前面要加byref
    示例 :
from ctypes import *

class MyStruct(Structure):
    _fields_ = [('shape', c_char*20),
                ('w', c_double),
                ('h', c_double),
                ('d', c_double),
                ]
第2种调用方式
myadd = dll.bo_add
myadd.restype = c_int
myadd.argtypes = [c_int, c_int]
print("another call method to bo_add() : ", myadd(20, 80))
传入结构体参数

dw, dh, dd = 4.0, 7.0, 3.0
dstr = b"cuboid "
s1 = MyStruct(dstr, dw, dh, dd)
print(s1.w, s1.h, s1.d)
print(s1.shape)
print(dll.bo_shape_vol(byref(s1)))

Output
4.0 7.0 3.0
b’cuboid ’
cuboid volume is 84
233390192

问题: print(dll.bo_shape_vol(byref(s1))) 打印出的是地址,不是结果 84
解决办法:用类的调用方式,定义dll函数返回值类型

print("第2种调用方法") 
myVol = dll.bo_shape_vol
myVol.restype = c_double  #定义dll函数返回值类型
myVol.argtypes = [POINTER(MyStruct)]  #结构体参数是以地址方式传入,因此要转为指针
dx = myVol(byref(s1))
print(dx)

Output

第2种调用方法
cuboid volume is 84
84.0

下面是另1个例 子

from ctypes import *

class Passport(Structure):              
    _fields_ = [("name", c_char_p),    
                ("surname", c_char_p),
                ("var", c_int)]

lib_dll = cdll.LoadLibrary("DLL_example.dll")   
lib_dll.SetPassport.argtypes  = [POINTER(Passport)] 

lib_dll.GetPassport()          

lib_dll.SetName(c_char_p(b"Yury"))
lib_dll.SetSurname(c_char_p(b"Wang"))

lib_dll.GetPassport()         

name = str.encode(("Feng"))  
surname = c_char_p((b'Li')) 

passport = Passport(name, surname, 34) 

lib_dll.SetPassport(pointer(passport)) 

lib_dll.GetPassport()         

Output:

Load DLL in Python
SetName
SetSurname
GetPassport: Default | Passport | 17
SetName
SetSurname
GetPassport: Yury | Orlov | 17
SetPassport
GetPassport: Vasiliy | Pupkin | 34
DETACH DLL

6. 对回调函数的支持

C++中,用函数指针非常容易地实现回调函数,python也可以实现。
第1步: 用 CFUNCTYPE() 定义1个函数指针,windows 使用 WINFUNCTYPE() 。
CFUNCTYPE()第1个参数是回调函数返回值类型,后面是函数形参。
第2步:将回调函数名赋给CFUNCTYPE变量,相当于将函数地址赋给函数指针。
第3步:将该回调函数指针做为另1个函数的形参使用。

示例如下:

from ctypes import *
import sys

# 回调函数类型定义
if 'linux' in sys.platform:
    fun_ctype = CFUNCTYPE
else:
    fun_ctype = WINFUNCTYPE

def add(a: c_int,int b: c_int):
	return a+b

ADDFUNC = fun_ctype(c_int, c_int, c_int)
add_callback = ADDFUNC(add)
#将CFUNCTYPE变量做为函数形参
def test(x,y,ADDFUNC):
    print(f"data is {x} and {y} 
    return add_callback(x,y)

#实际测试时,把回调函数做为参数传入
test(10,20,add_callback)

< – End -->

;