将 C# class 与 C++ 的引用成员一起编组

Marshal C# class with reference members to C++

我们的任务是将一个 class 与对另一个 class 的引用从 C# 传递到 C++。 C# class 看起来像这样:

[StructLayout(LayoutKind.Sequential, Pack=1)]
public class ImageInfo
{
    public uint Width;
    public uint Height;
    public uint Stride;
}

[StructLayout(LayoutKind.Sequential, Pack=1)]
internal class FrameInfo
{
    public int FrameNumber;
    public double TimeStamp;
    public ImageInfo Image; // This is the problem
}

C++ 结构如下所示(包也是 1):

struct FrameInfo
{
public:

    unsigned int    frameNumber;
    double          timeStamp;
    ImageInfo       *image;
};

struct ImageInfo
{
public:

    unsigned int     m_width;
    unsigned int     m_height;
    unsigned int     m_stride;
};

我们把C#class传给C++是这样的:

[DllImport("ourLib.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)]
public static extern int OnFrame(int pId, FrameInfo pFrame);

并在 C++ 中接受它:

extern "C" __declspec(dllexport) int OnFrame(int pId, const FrameInfo *pFrame);

因此在 FrameInfo 结构中我们没有引用而是嵌入的 class。内存布局如下所示:

0..3 bytes:   uint   frameNumber

4..11 bytes:  double timeStamp

12..15 bytes: uint   width

16..19 bytes: uint height

etc... 

而不是:

0..3 bytes:   uint   frameNumber

4..11 bytes:  double timeStamp

12..15 bytes: ImageInfo* image  // pointer to the image

Class 嵌入会导致额外的数据复制,这让我们很不爽。我们试图在 Marshal.StructureToPtr() 的帮助下将引用作为 IntPtr 传递,但 MSDN 说

"StructureToPtr copies the contents of structure to the pre-allocated block of memory"

以便它也复制数据。

然后我们尝试在C#中使用C风格的指针:

[StructLayout(LayoutKind.Sequential, Pack=1)]
internal unsafe class FrameInfo
{
    public int FrameNumber;
    public double TimeStamp;
    public ImageInfo *Image;
}

编译器说 "Cannot take the address of, get the size of, or declare a pointer to a managed type"

好的。然后我们尝试了一个奇怪的未记录的函数 __makeref() 然后将它转换为 IntPtr 但它也有相当奇怪的内存布局。

那么,有没有办法像往常引用一样传递引用呢?

So, is there any way to pass the reference just as usual reference?

不,由于垃圾收集器,引用不稳定,必须先固定一个对象。 pinvoke 编组器拒绝为非平凡的对象图这样做。您必须将 FrameInfo.Image 成员声明为 IntPtr 并使用 GC.Alloc + AddrOfPinnedObject

你可以在某处得到 在这种情况下,ImageInfo 应该是一个结构类型。这允许你声明一个指针:

[StructLayout(LayoutKind.Sequential)]
public struct ImageInfo {
    public uint Width;
    public uint Height;
    public uint Stride;
}

[StructLayout(LayoutKind.Sequential)]
internal unsafe class FrameInfo {
    public int FrameNumber;
    public double TimeStamp;        
    public ImageInfo* Image;
}

只要结构存储在栈中(局部变量或方法参数),就可以轻松初始化FrameInfo.Image:

var info = new ImageInfo { Width = 5, Height = 6, Stride = 7 };
var frame = new FrameInfo { FrameNumber = 1, TimeStamp = 2, Image = &info };
OnFrame(42, frame);

Pack可能是错误的。 Visual C++ 没有 Pack=1,它通常 Pack=0...

现在...你可以稍微作弊:

[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal class FrameInfo
{
    public int FrameNumber;
    public double TimeStamp;
    public IntPtr Image; // This is the problem
    public uint Width;
    public uint Height;
    public uint Stride;
}

[DllImport("NativeLibrary", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)]
public static extern int OnFrame(int pId, FrameInfo fi);

然后

var fi = new FrameInfo { FrameNumber = 1, TimeStamp = 2, Width = 3, Height = 4, Stride = 5 };

var handle = GCHandle.Alloc(fi, GCHandleType.Pinned);

try
{
    fi.Image = handle.AddrOfPinnedObject() + 2 * sizeof(double) + IntPtr.Size;

    OnFrame(1, fi);
}
finally
{
    handle.Free();
}

请注意我是如何 "merged" 将两个 class 放在一起的...IntPtr 现在指向 "inside" class

请注意,IntPtr Image 只会 "correct" 直到 handle.Free()

但在这一点上,我会用另一种方法封装所有内容,例如:

[DllImport("NativeLibrary", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)]
public static extern int OnFrame(int pId, IntPtr fi);

public static int OnFrameSharp(int pId, FrameInfo fi)
{
    var handle = GCHandle.Alloc(fi, GCHandleType.Pinned);

    try
    {
        IntPtr ptr = handle.AddrOfPinnedObject();
        fi.Image = ptr + 2 * sizeof(double) + IntPtr.Size;

        return OnFrame(pId, ptr);
    }
    finally
    {
        handle.Free();
    }
}

通过这种方式,我会获得一些速度,因为我固定了 FrameInfo 而 CLR 不必重新固定它。