3、ctypes高级用法:结构体与联合体的映射、回调函数、指针操作、内存管理
说实话,ctypes 这玩意儿,我刚开始接触 Jetson 的时候也觉得它有点「土」——都什么年代了还手写结构体映射?但后来在做一个实时视频流处理的项目时,Python 端的性能瓶颈卡得我头皮发麻,C 库又没法重写,这才老老实实把 ctypes 捡起来。嗯,真香。
这一章咱们聊点硬核的。结构体映射、联合体、回调函数、指针操作、内存管理——这些在 Jetson 上做嵌入式 AI 开发时,几乎是绕不开的坎。我踩过的坑,你最好别踩。
3.1 结构体与联合体的映射
先说说结构体。C 语言里的结构体,在 Python 里得用 class 继承 ctypes.Structure 来定义。字段用 _fields_ 这个元组列表来描述。
举个例子,Jetson 上常见的 nvbuf_utils.h 里的 NvBufferRect:
from ctypes import *
class NvBufferRect(Structure):
_fields_ = [
("left", c_uint32),
("top", c_uint32),
("right", c_uint32),
("bottom", c_uint32),
]
你看,就这么简单。但有个坑——内存对齐。C 编译器默认会对结构体做对齐填充,而 Python 的 ctypes 默认也是对齐的。但如果你调用的 C 库用了 #pragma pack(1) 或者 __attribute__((packed)),那就得手动指定 _pack_ = 1。
联合体(Union)就更简单了,继承 ctypes.Union 就行。比如某个硬件寄存器,可以按字节读,也可以按整型读:
class RegValue(Union):
_fields_ = [
("byte", c_uint8 * 4),
("word", c_uint32),
]
我个人习惯在定义结构体时,把 _fields_ 的注释写清楚,尤其是字段的位宽和用途。Jetson 上的硬件寄存器动辄几十个字段,不写注释回头自己都看不懂。
3.2 回调函数
回调函数是 ctypes 里比较绕的一个点。说白了,就是把 Python 函数当 C 函数指针传给底层库。
在 Jetson 上做图像处理时,我经常用 libv4l2 的 VIDIOC_S_FMT 这类 ioctl 调用,但更常见的场景是——你调用的 C 库需要你注册一个回调,比如帧完成通知、错误处理等。
定义回调类型用 CFUNCTYPE:
# 定义回调函数类型:返回 void,参数是 int
CALLBACK_FUNC = CFUNCTYPE(None, c_int)
def my_callback(value):
print(f"Callback received: {value}")
# 注册回调
callback_ptr = CALLBACK_FUNC(my_callback)
lib.register_callback(callback_ptr)
这里有个关键点:回调函数对象必须保持引用。如果你把 callback_ptr 定义在局部变量里,函数一结束就被垃圾回收了,C 端再调用时直接段错误。
另外,回调函数里尽量别做耗时操作。Jetson 的 CPU 资源本来就紧张,回调里做图像解码?那画面太美我不敢看。我建议回调里只做信号通知,比如设置一个 threading.Event,主线程去轮询处理。
3.3 指针操作
ctypes 的指针操作,说白了就是 byref() 和 pointer() 的区别。
byref(obj):轻量级,只传地址,不能做指针运算。适合传参。pointer(obj):创建真正的指针对象,可以[0]取值,可以+1偏移。适合需要操作指针的场景。
举个例子,Jetson 上调用 CUDA 的 cudaMalloc:
size = c_size_t(1024 * 1024)
ptr = c_void_p()
libcuda.cuMemAlloc(byref(ptr), size)
这里用 byref 就够了。但如果你要操作指针指向的内容,就得用 pointer:
arr = (c_float * 10)()
p = pointer(arr)
p[5] = 3.14 # 直接修改数组第6个元素
嗯,这里要注意:pointer(arr) 返回的是指向整个数组的指针,不是指向第一个元素的指针。如果你需要逐元素操作,用 cast 或者直接用数组索引更安全。
3.4 内存管理
内存管理是 ctypes 里最容易出 bug 的地方,没有之一。Jetson 上的内存本来就金贵,搞不好就 OOM。
先说分配。ctypes 分配的内存,默认是 Python 管理的,会自动释放。但如果你调用了 C 库的 malloc 或 cudaMalloc,那就得手动释放。
# 分配 C 内存
buf = ctypes.create_string_buffer(1024)
# 或者用 ctypes 的 malloc
libc = CDLL("libc.so.6")
ptr = libc.malloc(1024)
# ... 使用 ...
libc.free(ptr)
你想想看,如果忘了 free,在 Jetson 上跑个几天,内存泄漏直接导致系统卡死。我有个同事曾经在推理服务里漏了 4 个字节的释放,结果跑了 72 小时后 OOM,被客户投诉到怀疑人生。
另一个常见场景是:C 库返回了一块内存,Python 端怎么接管?
# 假设 C 库返回了一个 char* 指针
c_lib.get_buffer.restype = c_void_p
ptr = c_lib.get_buffer()
# 转为可操作的 Python 对象
buf = cast(ptr, POINTER(c_char * 1024))
content = buf.contents.value
这里 cast 是 ctypes 的「万能转换器」,但用的时候要小心类型大小。我曾经把一个 c_void_p 强转成 POINTER(c_float * 100),结果实际只有 50 个 float,读后面的数据全是垃圾。
最后说一个 Jetson 特有的问题:物理内存与虚拟内存。Jetson 上的 DMA 缓冲区、NVMM 内存等,往往需要物理连续内存。ctypes 分配的普通内存是虚拟内存,不能直接给硬件 DMA 用。这时候得用 nvbuf_utils 或者 cudaHostAlloc 这类专用接口。
我个人习惯在项目里封装一个 MemoryManager 类,统一管理所有 ctypes 内存的分配和释放,用 __del__ 做兜底清理。虽然 Python 的垃圾回收不太靠谱,但至少能减少手动管理的痛苦。
小结
这一章的内容其实挺干的。结构体映射、回调函数、指针操作、内存管理——每一个都是 ctypes 的深水区。在 Jetson 上做混合编程,说白了就是跟 C 库打交道,而 ctypes 就是那座桥。
桥修好了,车跑得飞快。桥修不好,翻车是迟早的事。
下一章咱们聊聊更高级的话题:如何在 Jetson 上用 ctypes 调用 CUDA 库,实现 GPU 加速的 Python 应用。到时候我会分享一个我实际做过的视频处理案例,保证干货满满。