Python调用C++动态库

背景

在Python中是可以使用C++动态库的,本文就围绕着这个主题阐述如何使用Python中的ctypes库调用C++动态库。
包括值传递、指针传递

前提

使用C风格的接口

在Python中使用ctypes调用C++动态库时,只能调用C接口,也就是被extern "C"修饰的接口:

extern "C"{
    void hello_world();
}

这是由于C++为了支持函数重载命名空间,会对函数名称进行修饰(name mangling)
简单而言,你定义了一个C++的"hello_world"函数,在经过编译器的编译之后,它就不叫"hello_world"了,他会换另外一个名字,比如"_Z3hello_worldi"。

换句话来说,如果你使用编译器修饰之后的函数名称,你也能正确调用到C++函数。

而加上了extern "C"之后,由于C语言不支持重名函数,所以编译器就知道没必要修饰函数名称,从而使用"hello_world"就能访问正确的函数。

最小例子

C接口

#ifndef PYTHON_C_LIBRARY_H
#define PYTHON_C_LIBRARY_H

typedef int errorCode;

extern "C" {
 errorCode hello_world(int value);
}

#endif //PYTHON_C_LIBRARY_H

#include "c++.h"

#include <iostream>

errorCode hello_world(int value) {
    std::cout << "Hello World! value:" << value << std::endl;
    return 0;
}

Python调用

import ctypes

# 调用dll
lib = ctypes.CDLL("dll路径")
lib.hello_world.argtypes = [ctypes.c_int] # 声明C接口的入参类型
lib.hello_world.restype = ctypes.c_int # 声明C接口的返回值类型
# 调用hello方法
result = lib.hello_world(8)
print(result)

控制台输出:
image.png

进阶

C中是没有C++中的stl库的,意味着string、vector、map…都无法直接在C接口中使用

值传递

接下来我们探讨双方能否传递 基本数值、结构体 等数据

基本数值 short、int、float、double、char、bool

//.h
void test_base_data(short s, int i, float f, double d, char c, bool b);
//.cpp
void test_base_data(short s, int i, float f, double d, char c, bool b) {
    std::cout << "s:" << s << ", i:" << i << ", f:" << f << ", d:" << d << ", c:" << c << ", b:" << b << std::endl;
}
import ctypes

# 调用dll
lib = ctypes.CDLL("dll路径")
# 转换编码
lib.test_base_data(5, 40000, 3.4, 3.4028235e38, 'h'.encode('utf-8'), False)

:当传递字符、字符串的时候,要转换编码 encode(‘utf-8’) ,不然会出现类型不匹配的问题。
输出:
image.png

结构体

struct BaseS {
    int value;
};
// h
void test_struct(BaseS s);
// cpp
void test_struct(BaseS s) {
    std::cout << "the struct value is:" << s.value << std::endl;
}
import ctypes
# 定义结构体
class BaseS(ctypes.Structure):
    _fields_ = [
        ("value", ctypes.c_int)
    ]

# 创建实例
base = BaseS()
base.value = 1

# 调用dll
lib = ctypes.CDLL("dll路径")
lib.test_struct(base)

输出:
image.png

复杂结构体

那么一个复杂结构体能传吗?

//h
void test_complex_struct(const Net &net);

//cpp
void test_complex_struct(const Net &net) {
    std::cout << "net value:" << net.value << std::endl;
    for (int i = 0; i < net.num_children; ++i) {
        std::cout << "net children[" << i << "] value:" << net.children[i].value << std::endl;
    }
}
import ctypes

class Net(ctypes.Structure):
    pass

Net._fields_ = [
    ("value", ctypes.c_int),
    ("children", ctypes.POINTER(Net)),
    ("num_children", ctypes.c_int)
]

# 动态库路径
lib = ctypes.CDLL(r"dll路径")

# 创建一个Net,里面包含五个Net
n1 = Net()
n1.value = 0
n1.num_children = 5
n1.children = (Net * 5)()
for i in range(5):
    n1.children[i] = Net()
    n1.children[i].value = i
    n1.children[i].num_children = 0

lib.test_complex_struct(n1)

输出:
在这里插入图片描述

指针传递

接下来我们探讨双方能否传递 **基本数值数组、结构体数组、函数指针 **等数据

基本数值数组 用int和char举例

//.h
void test_arr(int *iArr,int iArrSize,char *str);//非字符串数组需要一个代表该数组大小的变量
//.cpp
void test_arr(int *iArr, int iArrSize, char *str) {
    for (int i = 0; i < iArrSize; ++i) {
        std::cout << "iArr[" << i << "]:" << iArr[i] << std::endl;
    }
    std::cout << str << std::endl;
}

由于字符串数组会以**‘\0’**结尾,所以不需要一个代表该数组大小的变量

import ctypes

# 赋值
i_arr = [1, 2, 3, 4, 7]
s = "ni hao"
# 转换为C数组
i_arr_c = (ctypes.c_int * len(i_arr)()
for i in range(len(i_arr)):
    i_arr_c[i] = i_arr[i]

# 调用dll
lib = ctypes.CDLL("dll路径")
lib.test_arr(i_arr_c, len(i_arr), s.encode('utf-8'))

输出:
image.png

结构体数组

//.h
struct BaseS {
    int value;
};
void test_struct_arr(BaseS *sArr, int sArrSize);//非字符串数组需要一个代表该数组大小的变量

//.cpp
void test_struct_arr(BaseS *sArr, int sArrSize) {
    for (int i = 0; i < sArrSize; ++i) {
        std::cout << "sArr[" << i << "]:" << sArr[i].value << std::endl;
    }
}
import ctypes

# 定义结构体
class BaseS(ctypes.Structure):
    _fields_ = [
        ("value", ctypes.c_int)
    ]

# 创建实例
base = BaseS()
base.value = 1

base2 = BaseS()
base2.value = 2

base3 = BaseS()
base3.value = 8
b_a = [base, base2, base3]
# 创建c数组
base_array = (BaseS * 3)()
# 为数组赋值
for i in range(len(base_array)):
    base_array[i] = b_a[i]

# 调用dll
lib = ctypes.CDLL("C:/Users/Markzero/Desktop/myselfwork/python_c/cmake-build-debug/libpython_c.dll")
lib.test_struct_arr(base_array, len(base_array))

输出:
image.png

函数指针

这种方式可能也经常使用,用于绑定回调函数,使得C++也有调python函数的能力

  1. 传递字符串
//h
typedef void (*callBackFunction)(const char *); //声明函数指针类型
void bindCallBack(callBackFunction function);
//cpp
void bindCallBack(const callBackFunction function) {
    function("你好我是C");
}
import ctypes

def c_callback(s: str):
    # 注意,要对字符串解码
    print("收到了C传来的字符串:", s.decode('utf-8'))

# 加载 C 动态库
lib = ctypes.CDLL('dll动态库')
# 前者是python端函数返回值,后者是python端函数入参 最后面的括号里是你要绑定的回调函数
call_back_function = ctypes.CFUNCTYPE(None, ctypes.c_char_p)(c_callback)
# 调用C接口
lib.bindCallBack(call_back_function)

输出:
在这里插入图片描述
2. 传递结构体

//h
struct BaseS {
    int value;
};
typedef void (*callBackStructFunction)(BaseS b);
void bindCallBackStruct(callBackStructFunction function);
//cpp
void bindCallBackStruct(callBackStructFunction function) {
    BaseS b(100);
    function(b);
}
import ctypes
# 加载 C 动态库
lib = ctypes.CDLL('dll动态库')
class BaseS(ctypes.Structure):
    _fields_ = [
        ("value", ctypes.c_int)
    ]

def c_callback_struct(b: BaseS):
    print("收到了C传来的结构体:", b.value)

call_back_struct_function = ctypes.CFUNCTYPE(None, BaseS)(c_callback_struct)
# 调用dll
lib.bindCallBackStruct(call_back_struct_function)

输出:
在这里插入图片描述

注意!!!

Python中定义的指针,特别是函数指针,是会被python端的垃圾回收机制管理的,如果C++要持有这些个指针,要注意生命周期的问题,一般的解决方式是将python端的指针设为全局,延长其的生命周期。

共享内存 | 引用传递

说人话就是,python传给c的变量,c修改值后,python相应的变量也会被修改值。
c无法使用引用,只能用指针代替,标题中的引用只是为了方便理解。

基本数值

现在我希望python传递给c一个int变量,c给这个变量赋值为100后,python的变量也是100

//.h
void test_reference(int* num);

//.cpp
void test_reference(int *num) {
    *num = 100;
}
import ctypes

num = 42
print("调用前:", num)
# 转为C的数据结构
value = ctypes.c_int(num)
ptr = ctypes.pointer(value)

# 调用dll
lib = ctypes.CDLL("dll路径")
lib.test_reference(ptr)

# 转为python的数据结构
num = value.value
print("调用后:", num)

输出:
image.png
可喜可贺,这说明ptr=指向value的指针,value=原地址存的值

基本数值数组

那么在C端修改一个数组中值也如此简单吗?

//.h
void test_reference_arr(int *num, int aSize);
//.cpp
void test_reference_arr(int *num, int aSize) {
    for (int i = 0; i < aSize; ++i) {
        num[i] = 100;
    }
}
import ctypes

num_arr = [1, 2, 3, 4, 5]
print("调用前:", num_arr)

num_arr_c = (ctypes.c_int * len(num_arr))() # 可以理解为C++中的new操作
for i in range(len(num_arr)):
    num_arr_c[i] = num_arr[i]

# 调用dll
lib = ctypes.CDLL("dll路径")
lib.test_reference_arr(num_arr_c, len(num_arr))

num_arr = [num_arr_c[i] for i in range(len(num_arr))]
print("调用后:", num_arr)

输出:
image.png

动态数组

之前的例子都是在说固定数组大小的情况,那如果我希望在C端改变数组的容量该怎么做?
这里我们用一个空数组作为例子。
注意:在C中分配的内存,也得在C中释放,python的垃圾回收机制不会自动管理,所以需要额外提供一个释放内存的函数。

//.h
//增长数组函数
void test_extend_arr(int **arr, int *size);
//释放内存函数
void free_arr(int *arr);

//.cpp
void test_extend_arr(int **arr, int *size) {
    int newSize = 100;//新的数组大小
    // 重新分配内存
    int *newarr = (int *) realloc(*arr, newSize * sizeof(int));
    // 注意,内存是有可能分配失败的
    if (newarr == NULL) {
        return;
    }
    // 填充新数组
    for (int i = 0; i < newSize; i++) {
        newarr[i] = 100;
    }

    // 更新原始指针和大小
    *arr = newarr;
    *size = newSize;
}

void free_arr(int *arr) {
    free(arr);
}
import ctypes

# 加载 C 动态库
lib = ctypes.CDLL('dll路径')

# 初始化数组和大小
arr = ctypes.POINTER(ctypes.c_int)()  # 空指针
size = ctypes.c_int(0)

# 调用 C 函数来扩展数组
lib.test_extend_arr(ctypes.byref(arr), ctypes.byref(size))

# 打印数组内容
result = [arr[i] for i in range(size.value)]

# 释放内存
lib.free_arr(arr)

print(result)

输出:
image.png

结构体数组

简单结构体

我们先定义一个简单一点的结构体。

struct BaseS {
    int value;
};
//h
void get_base_arr(BaseS **base_arr, int *size);

void free_base(BaseS *base_arr);

//cpp
void get_base_arr(BaseS **base, int *size) {
    *size = 5;
    *base = new BaseS [*size];
    for (int i = 0; i < *size; i++) {
        (*base)[i].value = i;
    }
}

void free_base(BaseS *base_arr) {
    delete[] base_arr;
}
import ctypes

class BaseS(ctypes.Structure):
    _fields_ = [
        ("value", ctypes.c_int)
    ]

# 动态库路径
lib = ctypes.CDLL(r"C:\Users\Markzero\Desktop\myselfwork\python_c\cmake-build-debug\libpython_c.dll")

base = ctypes.POINTER(BaseS)()
size = ctypes.c_int()
# 通过引用方式传递给C端
lib.get_base_arr(ctypes.byref(base), ctypes.byref(size))

for i in range(size.value):
    print(base[i].value)

lib.free_base(base)

成功输出正确的值:

复杂结构体

我们来定义一个复杂一点的结构体,嵌套包含结构体。

struct Net {
    Net *children = nullptr;
    int num_children = 0;

    // 析构函数
    ~Net() {
        delete[] children; // 释放子节点
    }
};

在这个结构体中,我们可以看到这样一个嵌套包含结构,Net之中包含Net数组。为了保证正确释放,我们为这个结构体添加上析构函数,这不会影响到python的结构体定义。

我们先写出C端的代码:

//h
void get_net_arr(Net **netArray, int *size);

void free_net(const Net *netArray);

//cpp
void get_net_arr(Net **netArray, int *size) {
    *size = 2;
    auto aArray = new Net[*size];
    for (int i = 0; i < *size; i++) {
        aArray[i].children = new Net[5];
        aArray[i].num_children = 5;
        aArray[i].value = i;
        for (int j = 0; j < aArray[i].num_children; j++) {
            aArray[i].children[j].value = 100*(j+1);
        }
    }
    *netArray = aArray;
}

void free_net(const Net *netArray) {
    delete[] netArray;
}

在这段代码中我们创建了两个Net,其中它们各自又包含五个Net。

我们再来定义一下python端的代码:

import ctypes
# 由于直接在Net中定义("children", ctypes.POINTER(Net)),会导致类型未定义
# 下面这种分开写的方式,就可以实现自己包含自己的嵌套方式
class Net(ctypes.Structure):
    pass

Net._fields_ = [
    ("value", ctypes.c_int),
    ("children", ctypes.POINTER(Net)),
    ("num_children", ctypes.c_int)
]

# 动态库路径
lib = ctypes.CDLL("dll路径")

# 定义一个待赋值的Net数组
net = ctypes.POINTER(Net)()
# 定义一个待赋值的Net数组大小
size = ctypes.c_int(0)
# 通过引用方式传递给C端
lib.get_net_arr(ctypes.byref(net), ctypes.byref(size))

# 写一个简单的递归 打印这个Net数组
def traverseNet(tnet: ctypes.POINTER(Net)):
    print("Net Value is:", tnet.value)
    print("Net has ", tnet.num_children, " children")
    for i in range(tnet.num_children):
        traverseNet(tnet.children[i])

# 遍历 Net
for i in range(size.value):
    traverseNet(net[i])
# 别忘了释放内存
lib.free_net(net)

让我们看看输出了什么:

可以看见在python端的变量也的确被正确赋值了。

可喜可贺。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值