C# 编组非托管指针 return 类型

C# marshal unmanaged pointer return type

我有一个非托管库,其功能如下:

type* foo();

foo基本上是通过Marshal.AllocHGlobal.

在托管堆上分配一个非托管type的实例

我有 type 的托管版本。它不可 blittable,但我在成员上设置了 MarshalAs 属性,因此我可以使用 Marshal.PtrToStructure 来获取它的托管版本。但是必须通过额外的簿记来包装对 foo 的调用才能调用 Marshal.PtrToStructure 有点烦人。

我希望能够在 C# 端做这样的事情:

[DllImport("mylib", CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.LPStruct)]
type* foo();

并让 C# 的编组器在幕后处理转换,就像处理函数参数一样。我认为我应该能够做到这一点,因为 type 是在托管堆上分配的。但也许我不能?有什么方法可以让 C# 的内置编组器为我处理 return 类型上的非托管到托管转换,而无需手动调用 Marshal.PtrToStructure?

如果在 .NET 端 type 声明为 class 而不是结构,则自定义封送拆收器可以正常工作。 这在 UnmanagedType enumeration:

中有明确说明

Specifies the custom marshaler class when used with the MarshalAsAttribute.MarshalType or MarshalAsAttribute.MarshalTypeRef field. The MarshalAsAttribute.MarshalCookie field can be used to pass additional information to the custom marshaler. You can use this member on any reference type.

下面是一些应该可以正常工作的示例代码

[[DllImport("mylib", CallingConvention = CallingConvention.Cdecl)]
[return : MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef= typeof(typeMarshaler))]
private static extern type Foo();

private class typeMarshaler : ICustomMarshaler
{
    public static readonly typeMarshaler Instance = new typeMarshaler();

    public static ICustomMarshaler GetInstance(string cookie) => Instance;

    public int GetNativeDataSize() => -1;

    public object MarshalNativeToManaged(IntPtr nativeData) => Marshal.PtrToStructure<type>(nativeData);

    // in this sample I suppose the native side uses GlobalAlloc (or LocalAlloc)
    // but you can use any allocation library provided you use the same on both sides
    public void CleanUpNativeData(IntPtr nativeData) => Marshal.FreeHGlobal(nativeData);

    public IntPtr MarshalManagedToNative(object managedObj) => throw new NotImplementedException();
    public void CleanUpManagedData(object managedObj) => throw new NotImplementedException();
}

[StructLayout(LayoutKind.Sequential)]
class type
{
    /* declare fields */
};

当然,将非托管结构声明更改为 classes 可能会产生深远的影响(可能并不总是会引发编译时错误),尤其是在您有大量现有代码的情况下。

另一种解决方案是使用 Roslyn 解析您的代码,提取所有类似 Foo 的方法并为每个方法生成一个额外的 .NET 方法。我会这样做。

type* foo()

这是非常笨拙的函数签名,很难在 C 或 C++ 程序中正确使用,并且在您调用时永远不会变得更好。内存管理是最大的问题,你想和编写这段代码的程序员一起工作,让它变得更好。

您的首选签名应类似于 int foo(type* arg, size_t size)。换句话说,调用者提供内存并由本机函数填充。需要 size 参数以避免内存损坏,当 type 变化并变大。通常作为 type 的字段包含在内。 int return 值对于 return 错误代码很有用,因此您可以优雅地失败。除了使其安全之外,它还更加高效,因为根本不需要分配内存。您可以简单地传递一个局部变量。

... allocates an instance of the unmanaged type on the managed heap through Marshal.AllocHGlobal

不,这是内存管理假设变得非常危险的地方。从来没有托管堆,本机代码没有合适的方式调用 CLR。你不能假设它使用了 Marshal.AllocHGlobal() 的等价物。本机代码通常使用 malloc() 来分配存储,用于分配的堆是它链接的 CRT 的实现细节。只有 CRT 的 free() 函数才能保证可靠地释放它。您不能自己调用​​ free()。跳到底部查看为什么 AllocHGlobal() 看起来是正确的。

存在强制 pinvoke 编组器释放内存的函数签名,它通过调用 Marshal.FreeCoTaskMem() 来释放内存。请注意,这不等同于 Marshal.AllocHGlobal(),它使用不同的堆。它假定编写本机代码以很好地支持互操作并使用 CoTaskMemAlloc(),它使用专用于 COM 互操作的堆。

It's not blittable but I have MarshalAs attributes set...

这就是解释为什么 让它变得尴尬的关键细节。 pinvoke 编组器不想解决这个问题,因为它必须编组一个副本,并且自动释放对象及其成员的存储空间的风险太大。使用 [MarshalAs] 是不必要的,并且不会使代码更好,只需将 return 类型更改为 IntPtr。准备传递给 Marshal.PtrToStructure() 以及您需要的任何内存释放函数。


我不得不谈谈 Marshal.AllocHGlobal() 似乎是正确的原因。它以前不是,但在最近的 Windows 和 VS 版本中发生了变化。在 Win8 和 VS2012 中有一个很大的设计变化。 OS 不再创建 Marshal.AllocHGlobal 和 Marshal.AllocCoTaskMem 分配的单独堆。它现在是单个堆,默认进程堆(GetProcessHeap() returns 它)。 VS2012 中包含的 CRT 也有相应的变化,它现在也使用 GetProcessHeap() 而不是使用 HeapCreate() 创建自己的堆。

非常大的变化,没有广泛宣传。据我所知,Microsoft 没有为此发布任何动机,我认为基本原因是 WinRT(又名 UWP),大量内存管理使 C++、C# 和 Javascript 代码无缝协同工作。这对于必须编写互操作代码的每个人来说都非常方便,您 可以 现在假设 Marshal.FreeHGlobal() 可以完成工作。或者 Marshal.FreeCoTaskMem() 就像 pinvoke 编组器使用的那样。或者 free() 就像本机代码会使用的那样,没有区别了。

但这也是一个很大的风险,您不能再假设代码在您的开发机器上运行良好并且必须在 Win7 上重新测试时没有错误。如果您猜错了释放函数,您将得到一个 AccessViolationException。如果你还必须支持 XP 或 Win2003,那就更糟了,根本不会崩溃,但你会默默地泄漏内存。当它发生时很难处理,因为如果不更改本机代码就无法取得进展。最好早点搞定。