如何在Python中传递含自定义数据类型的C结构体调用C共享库
解决方案
ctypes 本身不具备解析 C 头文件 typedef 逻辑、自动适配平台类型的能力,要实现不硬编码基础类型、跨平台兼容的目标,核心思路是不要在 Python 侧重复实现 C 侧的类型适配逻辑,直接复用 C 编译器的适配结果,以下是可落地的实现方式,按可靠性从高到低排序:
- 编译期自动探测类型映射(最稳妥,适配任意多层typedef、任意平台)
你不需要手动追溯每个自定义类型最终对应的基础类型,只需要在编译C共享库时,用完全相同的编译参数、宏定义(比如是否开启OFFSET64、32/64位编译选项、对应操作系统的头文件),额外运行一段极简的类型探测代码,直接输出每个自定义类型的长度、有符号/无符号属性、结构体字段偏移量,用这些输出自动生成Python侧的ctypes类型定义即可。
探测代码示例:
运行这段探测代码拿到输出后,在Python侧按长度匹配对应ctypes类型即可:长度为1用c_byte/c_ubyte,2用c_int16/c_uint16,4用c_int32/c_uint32,8用c_int64/c_uint64。这种方式完全不关心你的typedef嵌套了多少层,也不关心不同操作系统下类型的别名差异,只要和编译正式so用的环境一致,结果就100%准确,不会出现长度不匹配、对齐错误的问题。#include <stdio.h> #include <stddef.h> // 引入你生产环境实际使用的头文件 #include "your_prod_header.h" int main() { // 输出每个自定义类型的大小 printf("SIZEOF_PID_T=%zu\n", sizeof(pid_t)); printf("SIZEOF_FILESIZE=%zu\n", sizeof(filesize)); printf("SIZEOF_INODE_T=%zu\n", sizeof(inode_t)); printf("SIZEOF_TIMEVAL=%zu\n", sizeof(struct timeval)); // 输出结构体每个字段的偏移,彻底规避对齐问题 printf("OFFSET_INFO_PID=%zu\n", offsetof(struct info, pid)); printf("OFFSET_INFO_ATIME=%zu\n", offsetof(struct info, atime)); printf("OFFSET_INFO_SZ=%zu\n", offsetof(struct info, sz)); printf("OFFSET_INFO_INUMBER=%zu\n", offsetof(struct info, i_number)); return 0; } - 标准类型轻量映射(适合类型规则明确的POSIX标准类型)
如果你用到的是pid_t、struct timeval这类POSIX标准定义的通用类型,不需要额外探测,直接按标准规则映射即可:- pid_t 在所有兼容POSIX的系统(Linux/Solaris/AIX)下均为32位有符号整型,直接对应
ctypes.c_int32 - struct timeval 结构固定为两个long类型字段(tv_sec、tv_usec),直接按如下方式定义即可:
class timeval(ctypes.Structure): _fields_ = [ ("tv_sec", ctypes.c_long), ("tv_usec", ctypes.c_long) ] - 对于你代码里受
OFFSET64宏控制的filesize类型,可以在加载so时先做一个极小的测试调用:预留一块足够大的内存传入C函数,读取已知固定值(比如示例里C端会给sz赋值512)的位置,反推字段长度,自动切换c_uint32/c_uint64类型,不需要人工判断当前编译环境是否开了64位偏移。
- pid_t 在所有兼容POSIX的系统(Linux/Solaris/AIX)下均为32位有符号整型,直接对应
- 自动生成绑定代码(适合结构体、类型数量多的场景)
如果你的库包含大量自定义结构体和类型,可以用cffi、ctypeslib这类工具,直接解析C头文件,在安装部署阶段针对当前运行环境自动生成对应的ctypes结构体定义,全程不需要手动维护类型映射,生成的代码和C侧定义完全匹配。
适配示例(对应提供的info结构体场景)
import ctypes from ctypes import c_int32, c_uint32, c_int64, c_uint64, c_long, c_ulong, c_byte # 加载共享库 lib = ctypes.CDLL("/root/Documents/python/mylib.so") # 标准POSIX类型固定映射 class timeval(ctypes.Structure): _fields_ = [ ("tv_sec", c_long), ("tv_usec", c_long) ] pid_t = c_int32 inode_t = c_ulong # 自动探测filesize类型长度,不需要手动判断OFFSET64宏 class TestInfo(ctypes.Structure): # 预留128字节足够容纳info结构体,避免溢出 _fields_ = [("pad", c_byte * 128)] lib.did_something.argtypes = [ctypes.POINTER(TestInfo)] test_inst = TestInfo() lib.did_something(test_inst) # 已知atime字段是timeval,在pid之后,所以sz的起始偏移是sizeof(pid_t)+sizeof(timeval) sz_offset = ctypes.sizeof(pid_t) + ctypes.sizeof(timeval) # 分别按4字节、8字节读取sz位置的值,看哪个等于C端写入的固定值512,确定类型 sz_val_32 = ctypes.cast(ctypes.addressof(test_inst) + sz_offset, ctypes.POINTER(c_uint32)).contents.value sz_val_64 = ctypes.cast(ctypes.addressof(test_inst) + sz_offset, ctypes.POINTER(c_uint64)).contents.value filesize = c_uint32 if sz_val_32 == 512 else c_uint64 # 正式定义info结构体 class info(ctypes.Structure): _fields_ = [ ("pid", pid_t), ("atime", timeval), ("sz", filesize), ("i_number", inode_t) ] # 调用测试 if __name__ == "__main__": s = info() lib.did_something.argtypes = [ctypes.POINTER(info)] lib.did_something(s) print(f"pid: {s.pid}") print(f"atime: {s.atime.tv_sec}.{s.atime.tv_usec}") print(f"file size: {s.sz}") print(f"inode number: {s.i_number}")
注意:永远不要在Python侧硬编码自定义类型对应的基础类型。只要你手动写死
c_int、c_char这类映射,跨架构、跨操作系统时大概率会遇到字段读值错误、内存越界、段错误的问题。让编译器帮你做类型适配,比人工核对多层typedef的逻辑可靠得多。
内容的提问来源于stack exchange,提问作者MankPan
相关产品推荐
相关产品推荐

