我有一个非托管库,其功能如下:
type* foo();
foo
基本上是通过type
在托管堆上分配非托管Marshal.AllocHGlobal
的实例。
我有type
的托管版本。它不是可蓝变的,但我在成员上设置了MarshalAs
属性,因此我可以使用Marshal.PtrToStructure
来获取它的托管版本。但是必须将对foo
的调用进行包装,并额外记帐才能调用Marshal.PtrToStructure
,这有点烦人。
我希望能够在C#端执行以下操作:
[DllImport("mylib", CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.LPStruct)]
type* foo();
,并让C#的编组在后台处理转换,就像对函数参数所做的那样。我认为我应该能够这样做,因为type
已分配在托管堆上。但是也许我不能?有什么方法可以让C#的内置编组为我处理返回类型上从非托管到托管的转换,而无需手动调用Marshal.PtrToStructure
?
答案 0 :(得分:6)
如果在.NET方面将type
声明为类而不是结构,则自定义封送拆收器会正常工作。
UnmanagedType enumeration中明确指出:
指定与 MarshalAsAttribute.MarshalType或MarshalAsAttribute.MarshalTypeRef 领域。 MarshalAsAttribute.MarshalCookie字段可用于传递 有关自定义封送拆收器的其他信息。 您可以使用此 任何参考类型的会员。
这里有一些示例代码应该可以正常工作
[[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 */
};
当然,将非托管结构声明更改为类可能会产生深层影响(这可能并不总是会引起编译时错误),特别是如果您有很多现有代码的话。
另一种解决方案是使用Roslyn解析代码,提取所有类似Foo的方法,并为每个方法生成一个附加的.NET方法。我会这样做。
答案 1 :(得分:2)
type* foo()
这是非常的尴尬函数签名,很难在C或C ++程序中正确使用,并且在您单击时不会变得更好。内存管理是最大的问题,您想与编写此代码的程序员合作以使其更好。
您的首选签名应类似于int foo(type* arg, size_t size)
。换句话说,调用者提供了内存,并由本机函数填充了内存。 size 参数是必需的,以避免内存损坏,这是当 type 的版本更改并获取时所必需的。更大。通常包含为 type 字段。 int 返回值对于返回错误代码很有用,因此您可以正常地失败。除了使其安全之外,它还更加有效,因为根本不需要分配内存。您只需传递局部变量即可。
...通过Marshal.AllocHGlobal在托管堆上分配非托管类型的实例
否,这是内存管理假设变得非常危险的地方。从来没有托管堆,本机代码无法调用CLR。而且,您不能假设它使用了Marshal.AllocHGlobal()的等效项。本机代码通常使用malloc()分配存储,用于分配的堆是其链接的CRT的实现细节。仅保证CRT的free()函数可靠地释放它。您不能自己调用free()。跳到底部查看为什么AllocHGlobal()看起来正确。
有一些函数签名会强制调用pinvoke marshaller释放内存,这是通过调用Marshal.FreeCoTaskMem()来实现的。请注意,这不等同于Marshal.AllocHGlobal(),它使用了一个不同的堆。假定编写本机代码以很好地支持互操作并且使用了CoTaskMemAlloc(),它使用了专用于COM互操作的堆。
它不是可蓝调的,但我设置了MarshalAs属性...
这是棘手的细节,解释了为什么必须使它尴尬。 Pinvoke编组器不希望解决此问题,因为它必须封送副本,并且自动释放对象及其成员的存储空间的风险太大。使用[MarshalAs]是不必要的,并且不会使代码更好,只需将返回类型更改为IntPtr
。准备传递给Marshal.PtrToStructure()以及您需要的任何内存释放功能。
我必须谈论Marshal.AllocHGlobal()似乎正确的原因。它过去不是,但是在最近的Windows和VS版本中已更改。 Win8和VS2012中发生了很大的设计更改。操作系统不再创建Marshal.AllocHGlobal和Marshal.AllocCoTaskMem从中分配的单独堆。现在它是一个堆,默认为进程堆(GetProcessHeap()返回它)。 VS2012附带的CRT进行了相应的更改,它现在还使用GetProcessHeap()而不是使用HeapCreate()创建自己的堆。
很大的变化,尚未广泛宣传。微软对此并没有发布任何动机,我认为基本原因是WinRT(又名UWP),大量的内存管理难题以使C ++,C#和Javascript代码无缝协作。这对于每个必须编写互操作代码的人来说都非常方便,您现在可以 假设Marshal.FreeHGlobal()完成了工作。或像pinvoke marshaller一样使用Marshal.FreeCoTaskMem()。或像本机代码将使用的free()一样,不再有任何区别。
但这也是一个巨大的风险,当您的开发机器上的代码运行良好并且必须在Win7上进行重新测试时,您不能再假定该代码没有错误。如果您猜错发布函数,则会收到AccessViolationException。更糟糕的是,如果还必须支持XP或Win2003,则完全不会崩溃,但是会无声地泄漏内存。这种情况很难解决,因为如果不更改本机代码就无法继续前进。最好早点做。