Python调用C的那些细节问题

工作中遇到了Python性能跟不上的情况,就把其中核心计算较多的部分放到C语言中去处理,然后把结果返回到Python当中。(事后强烈建议:使用Golang来完成所有工作,或者核心部分放到Golang中,而不是C中)

在这个过程当中,遇到了一些未曾探索的领域。故此记录一下。

如何把C代码编译成.so共享库文件给Python使用

假设我们有一个sample.proto文件,使用protobuf的工具已经生成了Python和C使用的相对应的pb文件,这里编译so文件。假设我们的C代码文件名为:sourcefile.c

Bash 复制代码
gcc -c -fPIC sample.pb-c.c sample.pb-c.h sourcefile.c
gcc -shared -o a.so sample.pb-c.o sourcefile.o -L/usr/local/lib -lprotobuf-c

第一行,根据GPT的解释:

PlainText 复制代码
这条命令是使用gcc编译一组源文件,并生成一个位置无关的目标文件(.o文件)。让我们逐步解释每个选项的含义:
-c:这个选项告诉gcc只进行编译而不进行链接。它将源文件编译为目标文件,生成.o文件。
-fPIC:这个选项指示gcc生成位置无关代码(Position Independent Code)。这是为了支持动态链接库(共享库)的需求,因为共享库可以在内存中的任何位置加载。
sample.pb-c.c:这是一个源文件的名称,它包含了Protocol Buffers编译器生成的C语言代码。
sample.pb-c.h:这是一个头文件的名称,它包含了Protocol Buffers编译器生成的C语言代码的声明。
sourcefile.c:这是另一个源文件的名称,它可能包含了使用Protocol Buffers库的自定义代码。

第二行,根据GPT的解释:

SQL 复制代码
这条命令是使用gcc将多个目标文件链接为一个共享库(shared library)。让我们逐步解释每个选项的含义:
-shared:这个选项告诉gcc生成一个共享库而不是可执行文件。
-o a.so:这个选项指定生成的共享库的输出文件名为a.so。.so是共享库文件的常见扩展名。
sample.pb-c.o:这是一个目标文件的名称,它包含了Protocol Buffers编译器生成的C语言代码的实现部分。
sourcefile.o:这是另一个目标文件的名称,它可能包含了使用Protocol Buffers库的自定义代码的实现部分。
-L/usr/local/lib:这个选项指定了链接器在搜索库文件时要查找的路径。/usr/local/lib是一个常见的库文件安装路径。
-lprotobuf-c:这个选项告诉链接器要链接名为libprotobuf-c的库。-l选项用于指定要链接的库的名称。
综上所述,这条命令的作用是将sample.pb-c.o和sourcefile.o这两个目标文件链接为一个共享库,并命名为a.so。链接器会使用libprotobuf-c库来解析符号引用,以便在运行时正确地链接和加载共享库。

这样就生成了一个a.so文件,接下来在Python中使用它

Python中使用so文件

  1. 需要注意函数名和参数类型的使用,Python代码当中要指定C的函数入参类型,与C代码的函数入参一一对应,函数名也需要对应上
  1. 函数如果有复杂的返回类型,也需要注意类型当中要一一对应,比如,返回的结构体其中的某个属性是一个数组,那这个数组的长度也需要保持一致(如果不一致,会出现很多意想不到的结果,其实也不多,就是一个结果:空)

比如这里的a是一个数组,长度是MAX_CNT,如果两边的MAX_CNT不相等,那么Python当中拿到的结果将全是空。

传递protobuf序列化之后的内容

在C代码中处理由Python传来的proto序列化之后的内容,需要用到protobuf自带的__unpack()函数,这个函数的第一个入参是一个ProtobufCAllocator,也允许传入NULL,但是不建议这么写,下面是一个例子

C 复制代码
typedef struct {
    ProtobufCAllocator base;  // ProtobufCAllocator作为结构体的第一个成员
    // 自定义的其他成员
} MyProtobufCAllocator;

// 自定义的内存分配函数
static void* my_alloc(void* allocator_data, size_t size) {
    MyProtobufCAllocator* allocator = (MyProtobufCAllocator*)allocator_data;
    return malloc(size);
}

// 自定义的内存释放函数
static void my_free(void* allocator_data, void* data) {
    MyProtobufCAllocator* allocator = (MyProtobufCAllocator*)allocator_data;
    free(data);
}

// 创建自定义的ProtobufCAllocator
static ProtobufCAllocator* create_my_allocator() {
    MyProtobufCAllocator* allocator = (MyProtobufCAllocator*)malloc(sizeof(MyProtobufCAllocator));
    allocator->base.alloc = my_alloc;
    allocator->base.free = my_free;
    // 初始化其他自定义成员
    return (ProtobufCAllocator*)allocator;
}

// 销毁自定义的ProtobufCAllocator
static void destroy_my_allocator(ProtobufCAllocator* allocator) {
    MyProtobufCAllocator* my_allocator = (MyProtobufCAllocator*)allocator;
    // 清理自定义成员
    free(my_allocator);
}

使用的时候如下:

C 复制代码
ProtobufCAllocator* allocator = create_my_allocator();
Example* example = example__unpack(allocator, length, data);
// 添加一堆逻辑之后
example__free_unpacked(example, NULL);
destroy_my_allocator(allocator);

C语言常见问题

  1. malloc和free要成对出现,这是一个老生常谈的问题,但是老生经常会忘记谈到的一个问题是,malloc之后,也要记得初始化,常见的初始化方法有循环遍历赋值,或者memset()赋值,或者干脆不使用malloc,而是使用calloc函数。

  2. 本次调试遇到的另一个问题就是跟第一个问题有关,malloc了,for循环遍历赋值了,但是依然跑飞,最后发现,循环赋值的时候,只循环了一半,还有一半没有给赋值成0😭

GDB调试Python代码

  1. 常用命令如下:
SQL 复制代码
gdb python3
run xxx.py a b c args
bt
bt full
py-list
py-bt
相关推荐
Python×CATIA工业智造1 小时前
Frida RPC高级应用:动态模拟执行Android so文件实战指南
开发语言·python·pycharm
onceco1 小时前
领域LLM九讲——第5讲 为什么选择OpenManus而不是QwenAgent(附LLM免费api邀请码)
人工智能·python·深度学习·语言模型·自然语言处理·自动化
狐凄2 小时前
Python实例题:基于 Python 的简单聊天机器人
开发语言·python
悦悦子a啊3 小时前
Python之--基本知识
开发语言·前端·python
笑稀了的野生俊5 小时前
在服务器中下载 HuggingFace 模型:终极指南
linux·服务器·python·bash·gpu算力
Naiva5 小时前
【小技巧】Python+PyCharm IDE 配置解释器出错,环境配置不完整或不兼容。(小智AI、MCP、聚合数据、实时新闻查询、NBA赛事查询)
ide·python·pycharm
路来了5 小时前
Python小工具之PDF合并
开发语言·windows·python
蓝婷儿6 小时前
Python 机器学习核心入门与实战进阶 Day 3 - 决策树 & 随机森林模型实战
人工智能·python·机器学习
AntBlack6 小时前
拖了五个月 ,不当韭菜体验版算是正式发布了
前端·后端·python
.30-06Springfield6 小时前
决策树(Decision tree)算法详解(ID3、C4.5、CART)
人工智能·python·算法·决策树·机器学习