You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过Python ctypes调用CFUNCTYPE类型的结构体函数字段?

问题:Python调用Kotlin Native生成的DLL中嵌套结构体函数失败

编辑说明
简而言之:我更新了头文件节选和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时指向了错误的内存地址。

正确的结构体定义步骤:

  1. 补充所有前置字段的类型定义
    虽然你认为这些字段细节不重要,但它们的内存大小直接决定了结构体的布局。需要先定义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)
    
  2. 完整定义嵌套结构体
    按照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)
        ]
    
  3. 正确加载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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 17:05:59