2、Python调用C基础:ctypes库入门、加载动态链接库(.so文件)、基本数据类型映射(int, float, char*)

好,咱们正式开始动手了。

这一章,我会带你走通Python调用C语言的第一步。说白了,就是让Python能直接跑C代码。你想想看,Python跑得慢,但开发快;C跑得快,但开发慢。把两者结合起来,就是Jetson上做AI加速的常规操作。

我个人习惯,在Jetson上做性能敏感的部分,比如图像预处理、矩阵运算,都用C写好,编译成.so文件,然后Python通过ctypes去调。这样既保留了Python的灵活性,又拿到了C的执行效率。

2.1 ctypes库是什么?

ctypes是Python标准库里的一个模块。不需要额外安装,装好Python就有。它的作用,就是让Python能直接调用C语言写的动态链接库。

我在项目中遇到过不少新手,一上来就用subprocess去跑C程序,或者用文件读写来传数据。嗯,那效率太低了。直接用ctypes,函数调用就像调用Python函数一样自然。

核心概念:ctypes把C的数据类型映射成Python的数据类型,然后通过加载.so文件,让Python能直接调用里面的函数。

2.2 加载动态链接库(.so文件)

在Linux系统上,C编译出来的动态库后缀是.so。在Jetson上,我们用的就是这种。

加载方式很简单,就一行代码:

import ctypes

# 加载当前目录下的 libmylib.so
my_lib = ctypes.CDLL("./libmylib.so")

这里要注意几个坑,我曾经踩过:

避坑指南:

  • 路径问题:./表示当前目录。如果库在别处,要用绝对路径或相对路径。
  • 权限问题:确保.so文件有可执行权限。我遇到过编译好了,但忘了chmod +x,结果加载失败。
  • 依赖问题:如果.so依赖其他库,那些库也得在系统路径里。可以用ldd libmylib.so检查。

加载成功后,my_lib就是一个对象。你可以通过它调用里面的函数。

2.3 基本数据类型映射

这是最基础,也是最容易出错的地方。C和Python的数据类型不一样,必须手动指定映射关系。

我直接给你一张表,这是我做项目时经常参考的:

C语言类型 ctypes类型 Python类型
int ctypes.c_int int
float ctypes.c_float float
double ctypes.c_double float
char* ctypes.c_char_p bytesstr(需编码)
void None None

举个例子,假设C函数是这样:

// C代码
int add(int a, int b) {
    return a + b;
}

float multiply(float x, float y) {
    return x * y;
}

void greet(char* name) {
    printf("Hello, %s!\n", name);
}

在Python里调用时,需要这样写:

import ctypes

# 加载库
lib = ctypes.CDLL("./libmylib.so")

# 指定函数参数和返回值类型
lib.add.argtypes = [ctypes.c_int, ctypes.c_int]
lib.add.restype = ctypes.c_int

lib.multiply.argtypes = [ctypes.c_float, ctypes.c_float]
lib.multiply.restype = ctypes.c_float

lib.greet.argtypes = [ctypes.c_char_p]
lib.greet.restype = None

# 调用
result_int = lib.add(10, 20)
print(result_int)  # 输出 30

result_float = lib.multiply(3.14, 2.0)
print(result_float)  # 输出 6.28

# 注意:字符串要转成bytes
lib.greet(b"Jetson")  # 输出 Hello, Jetson!

个人经验:我建议你每次写Python调用C时,都显式指定argtypesrestype。不指定的话,ctypes默认所有参数都是int,返回值也是int。你想想看,如果C函数返回float,Python却当int读,那结果肯定不对。

2.4 字符串传递的细节

字符串这块,坑比较多。我详细说说。

C的char*,在Python里对应ctypes.c_char_p。但Python的字符串是str类型,需要先编码成bytes

# 正确做法
name = "Jetson"
lib.greet(name.encode('utf-8'))  # 转成bytes

# 或者直接传bytes
lib.greet(b"Jetson")

反过来,如果C函数返回一个char*,Python拿到的是bytes,需要解码:

lib.get_version.restype = ctypes.c_char_p
version_bytes = lib.get_version()
version_str = version_bytes.decode('utf-8')
print(version_str)

注意:如果C函数返回的char*是静态字符串(比如"1.0.0"),没问题。但如果返回的是动态分配的内存,你得负责释放它。否则会内存泄漏。这个后面章节会讲。

2.5 一个完整的例子

咱们把上面的内容串起来,写一个完整的流程。

首先,C代码(mylib.c):

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

float multiply(float x, float y) {
    return x * y;
}

void greet(char* name) {
    printf("Hello, %s!\n", name);
}

char* get_version() {
    return "1.0.0";
}

编译成.so

gcc -shared -o libmylib.so mylib.c -fPIC

然后,Python调用:

import ctypes

lib = ctypes.CDLL("./libmylib.so")

# 配置add
lib.add.argtypes = [ctypes.c_int, ctypes.c_int]
lib.add.restype = ctypes.c_int

# 配置multiply
lib.multiply.argtypes = [ctypes.c_float, ctypes.c_float]
lib.multiply.restype = ctypes.c_float

# 配置greet
lib.greet.argtypes = [ctypes.c_char_p]
lib.greet.restype = None

# 配置get_version
lib.get_version.restype = ctypes.c_char_p

# 测试
print(lib.add(5, 3))          # 8
print(lib.multiply(2.5, 4.0)) # 10.0
lib.greet(b"Jetson")          # Hello, Jetson!
print(lib.get_version().decode())  # 1.0.0

运行一下,应该能看到输出。

2.6 常见错误与调试

我刚开始用ctypes时,经常遇到段错误(Segmentation Fault)。说白了,就是内存访问越界了。原因通常是参数类型没指定对。

  • 段错误:检查argtypesrestype是否和C函数声明一致。
  • 返回值不对:比如C返回float,Python没设restype,默认当int读,结果就是乱码。
  • 字符串乱码:检查编码和解码是否一致。C里用UTF-8,Python里也要用UTF-8

调试技巧:可以用print(dir(lib))查看库里有啥函数。如果函数名找不到,可能是C代码里没extern "C"(C++编译时名字会变)。这个后面章节会讲。

2.7 小结

这一章,咱们走通了Python调用C的基础流程。你学会了:

  • ctypes.CDLL加载.so文件
  • 映射intfloatchar*等基本数据类型
  • 指定argtypesrestype
  • 处理字符串的编码和解码

下一章,咱们会深入一点,讲结构体和指针的传递。那才是真正发挥C性能的地方。

记住:ctypes是Python和C之间的桥梁。桥搭好了,两边才能顺畅通信。别嫌麻烦,参数类型一定要指定清楚。


公众号:蓝海资料掘金营,微信deep3321