使用 C# 的 c 联合封送复杂的 C 结构

Marshaling complex C structs with c unions for C#

我迫切希望获得为 C# 正确封送的复杂 c 数据类型。我已经阅读了关于该主题的所有其他帖子,但我 运行 没有想法,尽管在我看来它非常接近解决方案。

主要问题是 c-struct 具有两种不同结构类型的联合。只有基本类型和一种包括数组,这会导致麻烦。

我创建了一个例子来展示这种情况。令我担心的结构称为 dataStreamConfiguration。 C 代码看起来像这样,有问题的结构在示例 C 代码的底部:

#include "stdint.h"
#include "stddef.h"

typedef enum viewCapEnum {
    X = 0,
}viewCapEnum;

typedef struct fraction{
    uint8_t nominator;
    uint8_t denominator;
}fraction;

typedef struct comSize{
    fraction A;
    fraction B;
}comSize;

typedef enum someEnum{
    A = 0,
    B,
    C,
    D
}someEnum;

typedef struct someSize{
    fraction X;
    fraction Y;
}someSize;


typedef struct featTemplateCap{
    someEnum A;
    someSize Size;
}featTemplateCap;

typedef struct featTypeCap{
    someEnum AB;
    someSize CD;
}featTypeCap;


typedef struct viewCap{
 uint8_t A;
 uint8_t B;
 size_t  BCount;
 viewCapEnum ViewCapEnum[50];
 comSize MinComSize;
 size_t CapaCount;
 featTemplateCap TemplCap[14];
 size_t TypeCapaCount;
 featTypeCap FeatTypeCapa[14];
 uint8_t GCount;
}viewCap;

typedef struct featX{
    uint16_t A;
    uint16_t B;
    int16_t  C;
    int16_t  D;
}featX;


typedef struct pathCap{
    uint8_t Count;
    uint8_t Size;
    featX   Feat;
}pathCap;


typedef struct dataStreamConfiguration{
  size_t FeatureSelector;
  union {
    viewCap  AsViewCap;
    pathCap  AsPathCap;
  }dataStream;
}dataStreamConfiguration;

C 和 C# 世界之间的数据类型编组对除此 dataStreamConfiguration 结构之外的几乎所有工作都有效。所以我得到了以下代码,其中不是将联合(以某种方式)映射到 c#,而是将两种数据类型一个接一个地放置。很明显这不能正常工作。看起来像这样:

public unsafe struct UInt32Struct {
    public UInt32 value;
}

public unsafe struct fraction{
    public Byte nominator;
    public Byte denominator;
}

public unsafe struct comSize{
    public fraction A;
    public fraction B;
}

public unsafe struct someSize{
    public fraction X;
    public fraction Y;
}


public unsafe struct featTemplateCap{
    public UInt32 A;
    public someSize Size;
}

public unsafe struct featTypeCap{
    public UInt32   AB;
    public someSize CD;
}


public unsafe struct viewCap{
 public Byte A;
 public Byte B;
 public UInt16 BCount;
 [MarshalAs(UnmanagedType.ByValArray, SizeConst = 50] 
 public UInt32Struct[] ViewCapEnum;
 public comSize MinComSize;
 public UInt16 CapaCount;
 [MarshalAs(UnmanagedType.ByValArray, SizeConst = 14] 
 public featTemplateCap[] TemplCap;
 public UInt16 TypeCapaCount;
 [MarshalAs(UnmanagedType.ByValArray, SizeConst = 14] 
 public featTypeCap FeatTypeCapa[14];
 public Byte GCount;
}

public unsafe struct featX{
    public UInt16 A;
    public UInt16 B;
    public Int16  C;
    public Int16  D;
}


public unsafe struct pathCap{
    public Byte Count;
    public Byte Size;
    public featX Feat;
}

public unsafe struct dataStreamConfiguration{
    public UInt16 FeatureSelector;
    public viewCap AsViewCap;
    public pathCap AsPathCap;
}

所以为了让联合到 c# 我遇到了 LayoutKind.Explicit 并做了以下事情:

[StructLayout(LayoutKind.Explicit)]
public unsafe struct dataStreamConfiguration{
    [FieldOffset(0)]
    public UInt16 FeatureSelector;
    [FieldOffset(2)]
    public viewCap AsViewCap;
    [FieldOffset(2)]
    public pathCap AsPathCap;
}

由于对象类型的对齐方式无法正常工作,对象类型不正确对齐或被非对象字段重叠。我在谷歌上搜索了很多。通过 [StructLayout(LayoutKind.Explicit, Pack=4)] 将对齐方式调整为 4。但是,4、8、16、32,无论我选择什么对齐方式,我在运行时都会遇到同样的错误 - 对齐不正确或重叠问题。

我做的下一件事——我感到很幸运——是为 viewCap 结构中的所有数组展开 C# 数据类型中的所有数组。正如我所读到的,这可能会导致对齐问题。好吧,它没有用。而且我发现内存被修改了,所以我找不到我在C中看到的值现在出现在C#中。 C# 中的大多数值都是 0。好的。 为了摆脱这种内存修改的东西,我在 C# 中放入所有其他结构 [StructLayout(LayoutKind.Sequential)] 以保持元素在 C 中的顺序。遗憾的是它没有太大帮助,我找不到 c 的值-c# 中的结构。然而,当我摆脱联合并删除 AsViewCapAsPathCap (我盲目愤怒的弱点)时,它终于起作用了。好的,但这不是解决方案。

最后的帮助是尝试 IntPtr,所以我创建了一个名为 dataStreamConfigurationPtr 的新结构:

public unsafe struct dataStreamConfigurationPtr{
    public UInt16 FeatureSelector;
    public void* Ptr;
}

[StructLayout(LayoutKind.Sequential)]
public unsafe struct dataStreamConfiguration{
    public UInt16 FeatureSelector;
    public viewCap AsViewCap;
    public pathCap AsPathCap;
}

我没有使用 StructLayout.Explicit 的重叠内存,而是使用 void* 指向非托管内存位置。为此,我使用旧的结构定义来获取内存,而不是使用一个联合,我采用了第一个版本,其中两种类型都在另一个之上布局。我的想法是这样使用它:

MyFunction(dataStreamConfigurationPtr X, int Status){
    
    //Create obj and  appropraite space for data
    dataStreamConfiguration DataStream = new dataStreamConfiguration();
    DataStream.FeatureSelector = X.FeatureSelector;
    
    unsafe{
        IntPtr Ptr = new IntPtr(&X.Ptr);
        DataStream.AsViewCap = Marshal.PtrToStructure<viewCap>(Ptr);
        DataSteram.AsPathCap = Marshal.PtrToStructure<pathCap>(Ptr);
    }
    
    WCFCallback(DataStream, Status);
    
}

现在 IntPtr 指向正确的内存,但是,这仅适用于结构的第一项。因此,对于 viewCap,第一项 A 具有正确的数据,而项目 B、BCount、.. 所有其他项目似乎至少具有未对齐的值或意外值。我非常绝望该怎么做现在,我觉得我离解决方案很近了,但不知道如何从 c 到 c# 获取结构的其他数据。

非常欢迎任何建议和意见!

此致, 托比亚斯

我假设您有两个用例,并且想在 C# 端将基于 FeatureSelector 的联合部分解释为 AsViewCapAsPathCap。 这意味着我假设你不打算做 type punning.

然后可以在托管 C# 端创建两个结构:

public struct dataStreamConfigurationAsViewCap
{
    public UInt64 FeatureSelector;
    public viewCap AsViewCap;
}

public struct dataStreamConfigurationAsPathCap
{
    public UInt64 FeatureSelector;
    public pathCap AsPathCap;
}

然后您只能先检查 FeatureSelector,然后根据结果将其解释为 dataStreamConfigurationAsViewCap 或解释为 dataStreamConfigurationAsPathCap

尺码 您在 C 端(FeatureSelectorBCountCapaCountTypeCapaCount)有几个带有 size_t 的变量,您将它们全部映射到 UInt16,这是错的。 UInt16 是 C 标准中的最小大小,但通常的实现,尤其是在 运行 .NET 平台上的实现更大,另请参见 nice answer。例如在我的 macOS 机器上它是 8 个字节。

也许从一个较小的测试用例开始并逐步扩展它是个好主意,这样您就可以识别此类问题。并且当您遇到问题时,您可以更轻松地创建一个最小的、完整的和可测试的示例。

朝这个方向的一种方法可能如下:

小测试用例

some.h

#ifndef some_h
#define some_h

#include <stdio.h>

typedef struct viewCap {
    uint8_t A;
    uint8_t B;
} viewCap;

typedef struct pathCap {
    uint16_t X;
    uint16_t Y;
    size_t Num;
} pathCap;

typedef struct dataStreamConfiguration {
    size_t FeatureSelector;
    union {
        viewCap  AsViewCap;
        pathCap  AsPathCap;
    } dataStream;
} dataStreamConfiguration;

dataStreamConfiguration *dscViewCap(void);
dataStreamConfiguration *dscPathCap(void);
extern void free_struct(dataStreamConfiguration *ptr);


#endif /* some_h */

some.c

#include "some.h"
#include <stdlib.h>
#include <stdint.h>
#include <stddef.h>


dataStreamConfiguration *dscViewCap(void) {
    dataStreamConfiguration *dsc = calloc(1, sizeof(dataStreamConfiguration));
    dsc->FeatureSelector = 0;
    dsc->dataStream.AsViewCap.A = 42;
    dsc->dataStream.AsViewCap.B = 84;
    return dsc;
}

dataStreamConfiguration *dscPathCap(void) {
    dataStreamConfiguration *dsc = calloc(1, sizeof(dataStreamConfiguration));
    dsc->FeatureSelector = 1;
    dsc->dataStream.AsPathCap.X = 0xAAAA;
    dsc->dataStream.AsPathCap.Y = 0x5555;
    dsc->dataStream.AsPathCap.Num = 0x3333333333333333;
    return dsc;
}


void free_dsc(dataStreamConfiguration *ptr) {
    free(ptr);
}

int main(void) {
    return 0;
}

UnionFromC.cs

using System.Runtime.InteropServices;

namespace UnionFromC
{
    public static class Program
    {
        public struct viewCap
        {
            public Byte A;
            public Byte B;
        }

        public struct pathCap
        {
            public UInt16 X;
            public UInt16 Y;
            public UInt64 Num;
        } 
        
        public struct dataStreamConfigurationAsViewCap
        {
            public UInt64 FeatureSelector;
            public viewCap AsViewCap;
        }
        
        public struct dataStreamConfigurationAsPathCap
        {
            public UInt64 FeatureSelector;
            public pathCap AsPathCap;
        }


        [DllImport("StructLib", EntryPoint = "dscViewCap")]
        private static extern IntPtr NativeDSCViewCap();

        [DllImport("StructLib", EntryPoint = "dscPathCap")]
        private static extern IntPtr NativeDSCPathCap();

        [DllImport("StructLib", EntryPoint = "free_dsc")]
        private static extern void NativeFreeDSC(IntPtr ptr);

        static void Main()
        {
            IntPtr _intPtrViewCap = NativeDSCViewCap();
            var viewDSC = Marshal.PtrToStructure<dataStreamConfigurationAsViewCap>(_intPtrViewCap);
            Console.WriteLine("\nInterpretation as view cap:");
            Console.WriteLine($"  FeatureSelector: {viewDSC.FeatureSelector}");
            Console.WriteLine($"  A: {viewDSC.AsViewCap.A}");
            Console.WriteLine($"  B: {viewDSC.AsViewCap.B}");
            NativeFreeDSC(_intPtrViewCap);
            
            IntPtr _intPtrPathCap = NativeDSCPathCap();
            var pathDSC = Marshal.PtrToStructure<dataStreamConfigurationAsPathCap>(_intPtrPathCap);
            Console.WriteLine("\nInterpretation as path cap:");
            Console.WriteLine($"  FeatureSelector: {pathDSC.FeatureSelector}");
            Console.WriteLine($"  A: {pathDSC.AsPathCap.X:X4}");
            Console.WriteLine($"  B: {pathDSC.AsPathCap.Y:X4}");
            Console.WriteLine($"  Num: {pathDSC.AsPathCap.Num:X8}");
            NativeFreeDSC(_intPtrPathCap);
        }
    }
}

测试输出

Interpretation as view cap:
  FeatureSelector: 0
  A: 42
  B: 84

Interpretation as path cap:
  FeatureSelector: 1
  A: AAAA
  B: 5555
  Num: 3333333333333333

这是如何将数据从 C/C++ 获取到 C# 的一种解决方案。在这里我将描述我做错了什么以及需要注意的地方。

回想一下,我的要求一直是(现在仍然是)在 C/C++ 中表示为联合的任何数据都需要在 C# 中表示为联合。这意味着以下结构:

typedef struct dataStreamConfiguration{
  size_t FeatureSelector;
  union {
    viewCap  AsViewCap;
    pathCap  AsPathCap;
  }dataStream;
}dataStreamConfiguration;

AsViewCap 中的任何数据都必须在 AsPathCap 中有其类型的表示,因为它的内存简单。如果这两个中的一个被修改,另一个也是。要在 C# 中处理 C/C++ 联合,您需要提供内存布局。 正如 Stephan Schlecht 已经提到的,了解对齐很重要! 我的项目是 为 32 位编译的 对齐位于 4字节边界。因此,我在问题中的初始布局完全是错误的。您需要检查 C/C++ 项目中的布局并在 C# 结构定义中正确调整它:这是我更正的代码,两个联合成员都从第 4 个字节开始:

[StructLayout(LayoutKind.Explicit)]
public unsafe struct dataStreamConfiguration{
    [FieldOffset(0)]
    public UInt16 FeatureSelector;
    [FieldOffset(4)]
    public viewCap AsViewCap;
    [FieldOffset(4)]
    public pathCap AsPathCap;
}

这样做你就成功了一半!是的! 但是还有一件事。通过此更改,代码将编译,但您将在运行时遇到异常。是的,在运行时。很快,但就是这样。 错误消息类似于:"object-field at offset 4 is incorrectly aligned or overlapped by a non-object field" C# 是窃听是因为在 C# 中有基本类型,如 Integer 等和 引用类型 .

如果我们不正确处理这些 引用类型,它们可能会给我们带来错误。 C# 有一个非常好的工作编组,但在联合的情况下,它取决于你让它尽可能好。

解释: 我的代码中出了什么问题是 struct viewCaparrays,由 C# marshaller 编组.编组器正在履行职责并创建数组。但是,数组是引用类型 并在堆 上创建。你会得到的在栈上(数据传输C++ <-> C#)是堆上数组的引用地址。哼!因此联合中的第二个结构及其基本类型将重叠地址和从而使引用无效。很高兴运行时环境阻止我们这样做 :-) 此外,C# 正在对内存进行碎片整理。如果结构布局在内存使用方面效率不高,C# 将重新排序内容。您可以通过注释 布局类型的结构来避免这种情况: Sequential.

记住: 如果您在属于联合 (C/C++) 的类型中得到一个数组,则不能使用 C# 编组器!对联合使用 Layoutkind Explicit,对结构使用 Sequential!

解法: 可能还有其他几种我不知道的解决方案,但 100% 有效的是展开数组。是的,这是很多工作。但它有效!

所以最终的结构如下所示:

[StructLayout(LayoutKind.Sequential)]
public unsafe struct viewCap{
 public Byte A;
 public Byte B;
 public UInt16 BCount;
 public UInt32Struct ViewCapEnum_0;
 public UInt32Struct ViewCapEnum_1;
 public UInt32Struct ViewCapEnum_2;
 public UInt32Struct ViewCapEnum_3;
 [...]
 public comSize MinComSize;
 public UInt16 CapaCount;
 public featTemplateCap TemplCap_0;
 public featTemplateCap TemplCap_1;
 public featTemplateCap TemplCap_2;
 public featTemplateCap TemplCap_3;
 [...]
 public UInt16 TypeCapaCount;
 public featTypeCap FeatTypeCapa_0;
 public featTypeCap FeatTypeCapa_1;
 public featTypeCap FeatTypeCapa_2;
 public featTypeCap FeatTypeCapa_3;
 [...]
 public Byte GCount;
}

快乐的运行环境,快乐的生活!

是的,对于这个特定的解决方案,您需要调整使用数组的代码。然而,它是防弹的并且可以理解它是如何在引擎盖下工作的,这使得维护变得容易。在生成我的代码时,展开的数组没什么大不了的。