如何用C#通过HDF.PInvoke读取含固定数组的复合HDF5数据集
我正在尝试用C#和HDF.PInvoke(HDF5 C API的封装)读取一个HDF5数据集,这个数据集里是复合数据类型的数组,其中复合类型包含一个9元素的double数组。运行代码时,创建GCHandle时抛出如下错误:
System.ArgumentException
HResult=0x80070057
Message=Object contains non-primitive or non-blittable data. (Parameter 'value')
Source=System.Private.CoreLib
代码在处理不含数组的复合类型时正常运行,我怀疑问题出在double[9]数组的固定大小需求和通过GCHandle传递数组的方式不匹配(可能需要用指针?),请问我哪里错了?
以下是简化后的代码:
[StructLayout(LayoutKind.Explicit, Size = 76, Pack = 1)] public struct TestData { [FieldOffset(0)] public double[] ArrayVariable = new double[9]; [FieldOffset(72)] public Single FloatVariable = 0; public TestData(){} } public static TestData[] LoadTestData(hid_t pg, string item) { hid_t memtype = H5T.create(H5T.class_t.COMPOUND, (ssize_t)76); hsize_t[] array_dims = { 9 }; hid_t compoundTypeID= H5T.array_create(H5T.NATIVE_DOUBLE, 1, array_dims); H5T.insert(memtype, "ArrayVariable", (ssize_t)0, compoundTypeID);//+72 H5T.insert(memtype, "FloatVariable", (ssize_t)72, H5T.NATIVE_FLOAT);//+4 TestData[] testData = null; if (TryOpenDataSet(pg, item, out hid_t dataset)) { hid_t dataspace = H5D.get_space(dataset); int rank = H5S.get_simple_extent_ndims(dataspace); if (rank == 1) { hsize_t[] dims = new hsize_t[1]; hsize_t[] maxdims = new hsize_t[1]; int tst = H5S.get_simple_extent_dims(dataspace, dims, maxdims); if (tst != 1) { throw new HDF5Exception("unexpected dimension number in TestData Array loading"); } int numEntries = (int)dims[0]; testData = new TestData[numEntries]; hid_t datatype = H5D.get_type(dataset); GCHandle pinnedArray = GCHandle.Alloc(testData, GCHandleType.Pinned);//****Error here**** H5D.read(dataset, memtype, H5S.ALL, H5S.ALL, H5P.DEFAULT, pinnedArray.AddrOfPinnedObject()); pinnedArray.Free(); H5T.close(datatype); } H5D.close(dataset); } return testData; } public static bool TryOpenDataSet(hid_t loc_id, string title, out hid_t datasetID) { try { datasetID = H5D.open(loc_id, title); return !(datasetID < 0); } catch (Exception) { datasetID = -1; return false; } }
问题原因与解决方法
核心原因
你的TestData结构体里的double[] ArrayVariable是引用类型,而GCHandle.Alloc的Pinned类型要求对象是blittable类型(即托管内存和非托管内存二进制表示完全一致的类型)。因为结构体包含引用类型成员,整个结构体变成非blittable,无法被直接固定,从而触发报错。
解决步骤
1. 将结构体中的数组改为固定大小缓冲区(Fixed Buffer)
使用C#的fixed关键字声明固定大小的缓冲区,这样数组会直接嵌入结构体的内存中,而非作为引用存在。修改后的结构体需要开启不安全代码支持:
[StructLayout(LayoutKind.Explicit, Size = 76, Pack = 1)] public unsafe struct TestData { [FieldOffset(0)] public fixed double ArrayVariable[9]; // 固定大小缓冲区,直接嵌入结构体内存 [FieldOffset(72)] public Single FloatVariable; public TestData() {} }
注意:需要在项目属性的"生成"选项卡中勾选"允许不安全代码"。
2. 验证内存类型定义
你原有的HDF5内存类型定义是正确的:H5T.array_create创建的9元素double数组类型,和结构体里的fixed buffer内存布局完全匹配,无需修改这部分代码。
3. 访问固定缓冲区元素
读取数据后,需要通过指针操作访问fixed buffer中的元素:
// 示例:访问第一个TestData实例的ArrayVariable第0个元素 unsafe { fixed (TestData* dataPtr = testData) { double firstValue = dataPtr[0].ArrayVariable[0]; } }
4. 无unsafe代码的替代方案(不推荐)
如果不想使用unsafe代码,可以将9元素数组拆分为9个独立的double字段:
[StructLayout(LayoutKind.Explicit, Size = 76, Pack = 1)] public struct TestData { [FieldOffset(0)] public double ArrayVar0; [FieldOffset(8)] public double ArrayVar1; [FieldOffset(16)] public double ArrayVar2; [FieldOffset(24)] public double ArrayVar3; [FieldOffset(32)] public double ArrayVar4; [FieldOffset(40)] public double ArrayVar5; [FieldOffset(48)] public double ArrayVar6; [FieldOffset(56)] public double ArrayVar7; [FieldOffset(64)] public double ArrayVar8; [FieldOffset(72)] public Single FloatVariable; }
同时需要修改HDF5内存类型定义,逐个插入这些字段。这种方式代码冗余,仅作为备选方案。
内容的提问来源于stack exchange,提问作者drG_Bristol

