如何通过Python ctypes调用CFUNCTYPE类型的结构体函数字段?
编辑说明
简而言之:我更新了头文件节选和Python代码,因为之前遗漏了太多关键内容。
详情:我之前忽略了一些自认为不重要但实际影响内存布局的声明(后续采纳的答案指出了这一点)。此外,我曾简化结构体嵌套并重命名结构体以减少冗余(原路径kotlin.root.com.marcoeckstein.klib曾简化为myLib)。由于采纳的答案基于实际DLL创建,我也修订了头文件节选和Python代码,使其与该DLL及答案保持一致。
我有一个由Kotlin Multiplatform/Native自动生成的DLL(Windows为.dll,Linux为.so)及其C头文件,节选如下:
typedef struct { // #region // 以下大多数声明的细节不重要,但它们在二进制布局中的内存占用至关重要: void (*DisposeStablePointer)(libnative_KNativePtr ptr); void (*DisposeString)(const char* string); libnative_KBoolean (*IsInstance)(libnative_KNativePtr ref, const libnative_KType* type); libnative_kref_kotlin_Byte (*createNullableByte)(libnative_KByte); libnative_kref_kotlin_Short (*createNullableShort)(libnative_KShort); libnative_kref_kotlin_Int (*createNullableInt)(libnative_KInt); libnative_kref_kotlin_Long (*createNullableLong)(libnative_KLong); libnative_kref_kotlin_Float (*createNullableFloat)(libnative_KFloat); libnative_kref_kotlin_Double (*createNullableDouble)(libnative_KDouble); libnative_kref_kotlin_Char (*createNullableChar)(libnative_KChar); libnative_kref_kotlin_Boolean (*createNullableBoolean)(libnative_KBoolean); libnative_kref_kotlin_Unit (*createNullableUnit)(void); // #endregion struct { struct { struct { struct { struct { const char* (*createMessage)(); // 此处有更多嵌套结构体 // (与本问题无关) } klib; } marcoeckstein; } com; } root; } kotlin; } libnative_ExportedSymbols; extern libnative_ExportedSymbols* libnative_symbols(void);
我希望从Python中调用其中的createMessage函数,尝试了以下代码:
import ctypes class Klib(ctypes.Structure): _fields_ = [("createMessage", ctypes.CFUNCTYPE(ctypes.c_char_p))] class Marcoeckstein(ctypes.Structure): _fields_ = [("klib", Klib)] class Com(ctypes.Structure): _fields_ = [("marcoeckstein", Marcoeckstein)] class Root(ctypes.Structure): _fields_ = [("com", Com)] class Kotlin(ctypes.Structure): _fields_ = [("root", Root)] class Libnative_ExportedSymbols(ctypes.Structure): _fields_ = [("kotlin", Kotlin)] dll = ctypes.CDLL("path/to/dll") #region #以下两行已被证实错误,见下文。 dll.libnative_symbols.restype = Libnative_ExportedSymbols libnative = dll.libnative_symbols() #endregion libnative.kotlin.root.com.marcoeckstein.klib.createMessage()
运行后出现错误:
- Windows:
OSError: exception: access violation writing 0x00007FFA51556340 - Linux:
Segmentation fault (core dumped)
我在Windows上尝试用WINFUNCTYPE替代CFUNCTYPE,但错误相同。另外,包装器生成器ctypesgen为createMessage生成的类型和我手动定义的一致。
后来根据提示,我修改了指针相关的代码:
原错误代码:
dll.libnative_symbols.restype = Libnative_ExportedSymbols libnative = dll.libnative_symbols()
修改为:
dll.libnative_symbols.restype = ctypes.POINTER(Libnative_ExportedSymbols) libnative = dll.libnative_symbols().contents
现在不再报错,但createMessage返回空bytes实例,而该函数本应始终返回非空字符串。
请问问题出在哪?如何正确调用这个CFUNCTYPE类型的字段?
解决方案
问题的核心在于结构体的内存布局不匹配。你定义的Libnative_ExportedSymbols结构体只包含了kotlin字段,但原C结构体在kotlin之前还有大量函数指针字段——这些字段的内存占用会直接影响后续嵌套结构体的偏移量,导致你访问createMessage时指向了错误的内存地址。
正确的结构体定义步骤:
补充所有前置字段的类型定义
虽然你认为这些字段细节不重要,但它们的内存大小直接决定了结构体的布局。需要先定义C头中所有前置的函数指针类型,确保结构体的内存偏移完全匹配。import ctypes # 定义基础类型,匹配Kotlin Native生成的类型 libnative_KNativePtr = ctypes.c_void_p libnative_KBoolean = ctypes.c_bool libnative_KByte = ctypes.c_char libnative_KShort = ctypes.c_short libnative_KInt = ctypes.c_int libnative_KLong = ctypes.c_longlong libnative_KFloat = ctypes.c_float libnative_KDouble = ctypes.c_double libnative_KChar = ctypes.c_wchar libnative_KType = ctypes.c_void_p # 无需具体实现,只需占位 # 定义前置的函数指针类型 CFUNCTYPE_DisposeStablePointer = ctypes.CFUNCTYPE(None, libnative_KNativePtr) CFUNCTYPE_DisposeString = ctypes.CFUNCTYPE(None, ctypes.c_char_p) CFUNCTYPE_IsInstance = ctypes.CFUNCTYPE(libnative_KBoolean, libnative_KNativePtr, ctypes.POINTER(libnative_KType)) CFUNCTYPE_createNullableByte = ctypes.CFUNCTYPE(ctypes.c_void_p, libnative_KByte) CFUNCTYPE_createNullableShort = ctypes.CFUNCTYPE(ctypes.c_void_p, libnative_KShort) CFUNCTYPE_createNullableInt = ctypes.CFUNCTYPE(ctypes.c_void_p, libnative_KInt) CFUNCTYPE_createNullableLong = ctypes.CFUNCTYPE(ctypes.c_void_p, libnative_KLong) CFUNCTYPE_createNullableFloat = ctypes.CFUNCTYPE(ctypes.c_void_p, libnative_KFloat) CFUNCTYPE_createNullableDouble = ctypes.CFUNCTYPE(ctypes.c_void_p, libnative_KDouble) CFUNCTYPE_createNullableChar = ctypes.CFUNCTYPE(ctypes.c_void_p, libnative_KChar) CFUNCTYPE_createNullableBoolean = ctypes.CFUNCTYPE(ctypes.c_void_p, libnative_KBoolean) CFUNCTYPE_createNullableUnit = ctypes.CFUNCTYPE(ctypes.c_void_p)完整定义嵌套结构体
按照C头的顺序,先定义前置字段,再定义嵌套的kotlin结构体:# 嵌套结构体从内到外定义 class Klib(ctypes.Structure): _fields_ = [("createMessage", ctypes.CFUNCTYPE(ctypes.c_char_p))] class Marcoeckstein(ctypes.Structure): _fields_ = [("klib", Klib)] class Com(ctypes.Structure): _fields_ = [("marcoeckstein", Marcoeckstein)] class Root(ctypes.Structure): _fields_ = [("com", Com)] class Kotlin(ctypes.Structure): _fields_ = [("root", Root)] # 完整定义ExportedSymbols结构体,包含所有前置字段 class Libnative_ExportedSymbols(ctypes.Structure): _fields_ = [ ("DisposeStablePointer", CFUNCTYPE_DisposeStablePointer), ("DisposeString", CFUNCTYPE_DisposeString), ("IsInstance", CFUNCTYPE_IsInstance), ("createNullableByte", CFUNCTYPE_createNullableByte), ("createNullableShort", CFUNCTYPE_createNullableShort), ("createNullableInt", CFUNCTYPE_createNullableInt), ("createNullableLong", CFUNCTYPE_createNullableLong), ("createNullableFloat", CFUNCTYPE_createNullableFloat), ("createNullableDouble", CFUNCTYPE_createNullableDouble), ("createNullableChar", CFUNCTYPE_createNullableChar), ("createNullableBoolean", CFUNCTYPE_createNullableBoolean), ("createNullableUnit", CFUNCTYPE_createNullableUnit), ("kotlin", Kotlin) ]正确加载DLL并调用函数
# 加载DLL dll = ctypes.CDLL("path/to/dll") # 指定返回类型为结构体指针 dll.libnative_symbols.restype = ctypes.POINTER(Libnative_ExportedSymbols) libnative = dll.libnative_symbols().contents # 调用createMessage并解码结果 result = libnative.kotlin.root.com.marcoeckstein.klib.createMessage() print(result.decode("utf-8")) # 转换为Python字符串
为什么之前的修改不充分?
你只修正了指针的问题,但结构体的内存布局完全不对——跳过前置字段后,kotlin字段的偏移量和C中的实际偏移不匹配,导致你访问的createMessage并不是正确的函数地址,自然返回错误结果。只有完整匹配结构体的所有字段,才能保证内存偏移正确,从而调用到正确的函数。
内容的提问来源于stack exchange,提问作者Marco Eckstein

