DEBUG.WIKI
野草云;香港 200Mbps 峰值;不限月流量 · BGP VPS;查看套餐 →
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

NumPy 读取 ctypes 继承结构,字段缺失且偏移错位

np.dtype 遍历子类 _fields_ 时可能漏掉父类字段。对已知简单结构显式声明字段、实际偏移与总大小,先验布局再读缓冲区。

一个 ctypes 结构体继承了父类字段,传给 np.dtype() 后却只剩子类新增字段;再按这个 dtype 读取缓冲区,数值来自错误的位置。先比较字段偏移与记录大小,不要只看数组形状是否正确。

NumPy 2.5.4 与 2.4.6 有这类明确记录。相关转换函数遍历当前类型的 _fields_,而 ctypes 子类的这份列表不重复列出父类字段。于是自动构造的 NumPy dtype 可能缺少继承字段,甚至把子类字段放到偏移零;添加 _pack_ 也不自动补全继承布局。

对已知简单结构,按 ctypes 的实际偏移声明 dtype

下面只处理固定的、原生字节序的普通数值字段,没有位域、联合体或指针:

import ctypes as ct
import numpy as np

class Base(ct.Structure):
    _fields_ = [("a", ct.c_int32), ("b", ct.c_int32)]

class Record(Base):
    _fields_ = [("c", ct.c_double)]

record_dtype = np.dtype({
    "names": ["a", "b", "c"],
    "formats": [np.int32, np.int32, np.float64],
    "offsets": [Record.a.offset, Record.b.offset, Record.c.offset],
    "itemsize": ct.sizeof(Record),
})

字段顺序、类型与总大小都由这个具体结构确定。不要把两个 _fields_ 列表直接拼接后假设没有填充字节,也不要把某台机器上的 c_long 一概写成 int64;结构布局会受平台与 ABI 影响。

这里建立 dtype 不会读取任何业务内存。真正使用 np.frombuffer() 等入口时,缓冲区必须仍然存活,记录数量与字节长度应匹配,写入还要遵守原内存的可写性和所有权。正确的 dtype 也不能使一个已释放的指针变安全。

先验证布局,再验证字段值

逐项比较 record_dtype.fields[name][1] 与 ctypes 字段的 offset,并比较 record_dtype.itemsize 与 ct.sizeof(Record)。准备每个字段都不同的隔离样本,按原入口读回,能发现“字段存在但位置错了”的情况。

位域、嵌套结构、数组字段、非原生字节序和指针需要各自的明确映射,不能把本例扩展成任意 ctypes 类型的通用转换器。遇到 PEP3118 buffer format string does not match the actual itemsize 时,也应保留原错误,不能通过缩短记录大小或截掉字节来迎合它。

正式 2.5.4 仍使用上述 _fields_ 遍历路径。后续升级应以真实结构的偏移、大小与读回值验收,不只检查导入成功;调用方自行维护的显式 dtype 也应随结构定义一起更新。