将具有引用成员的C#类封送到C++

本文关键字:C++ 引用 成员 | 更新日期: 2023-09-27 18:01:01

我们有一个任务,要将一个引用了另一个类的类从C#传递到C++。C#类看起来像这样:

[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#类传递给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结构中,我们没有引用,而是嵌入了一个类。内存布局如下:

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

类嵌入会导致额外的数据复制,这让我们非常不安。我们试图在Marshal.StructureToPtr()的帮助下将引用传递为IntPtr,但MSDN表示

"StructureToPtr将结构的内容复制到预先分配的内存块">

以便它也复制数据。

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

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

编译器表示"无法获取的地址、获取的大小或声明指向托管类型的指针">

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

那么,有没有什么方法可以像通常的引用一样传递引用呢?

将具有引用成员的C#类封送到C++

那么,有没有什么方法可以像通常的引用一样传递引用呢?

不,由于垃圾收集器的原因,引用不稳定,必须首先固定对象。pinvoke-mashaller拒绝为非平凡对象图这样做。您必须将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();
}

注意我是如何将两个class"合并"在一起的。。。IntPtr现在指向class的"内部"。

请注意,只有在handle.Free() 之前,IntPtr Image才会"正确">

但在这一点上,我会用另一种方法封装一切,比如:

[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不必重新固定它。