在尝试编写与此问题(P/Invoke from C to C# without knowing size of array)相关的自定义封送程序时,我遇到了一些我无法理解的问题。这是我写的第一个自定义封送器,所以毫无疑问,由于我的无知,我错过了一些明显的东西。
这是我的C#代码:
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace CustomMarshaler
{
public class MyCustomMarshaler : ICustomMarshaler
{
static MyCustomMarshaler static_instance;
public IntPtr MarshalManagedToNative(object managedObj)
{
if (managedObj == null)
return IntPtr.Zero;
if (!(managedObj is int[]))
throw new MarshalDirectiveException("VariableLengthArrayMarshaler must be used on an int array.");
int[] arr = (int[])managedObj;
int size = sizeof(int) + arr.Length * sizeof(int);
IntPtr pNativeData = Marshal.AllocHGlobal(size);
Marshal.WriteInt32(pNativeData, arr.Length);
Marshal.Copy(arr, 0, pNativeData + sizeof(int), arr.Length);
return pNativeData;
}
public object MarshalNativeToManaged(IntPtr pNativeData)
{
int len = Marshal.ReadInt32(pNativeData);
int[] arr = new int[len];
Marshal.Copy(pNativeData + sizeof(int), arr, 0, len);
return arr;
}
public void CleanUpNativeData(IntPtr pNativeData)
{
Marshal.FreeHGlobal(pNativeData);
}
public void CleanUpManagedData(object managedObj)
{
}
public int GetNativeDataSize()
{
return -1;
}
public static ICustomMarshaler GetInstance(string cookie)
{
if (static_instance == null)
{
return static_instance = new MyCustomMarshaler();
}
return static_instance;
}
}
class Program
{
[DllImport(@"MyLib.dll")]
private static extern void Foo(
[In, Out, MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(MyCustomMarshaler))]
int[] arr
);
static void Main(string[] args)
{
int[] colorTable = new int[] { 1, 2, 3, 6, 12 };
Foo(colorTable);
foreach (int value in colorTable)
Console.WriteLine(value);
}
}
}
另一方面是一个简单的本机DLL,用Delphi编写。
library MyLib;
procedure Foo(P: PInteger); stdcall;
var
i, len: Integer;
begin
len := P^;
Writeln(len);
for i := 1 to len do begin
inc(P);
Writeln(P^);
inc(P^);
end;
end;
exports
Foo;
begin
end.
这个想法是将数组传递给DLL,然后打印出长度字段和数组的值。本机代码还将数组的每个值递增1。
所以,我希望看到这个输出:
5 1 2 3 6 12 2 3 4 7 13
但不幸的是我看到了这个输出:
5 1 2 3 6 12 1 2 3 6 12
在调试器下,我可以看到MarshalNativeToManaged
正在执行,并且它返回的值已递增。但是这些递增的值并没有找到返回传递给Foo
的对象的方法。
我需要做些什么来解决这个问题?
答案 0 :(得分:8)
多年前我遇到过类似的问题,发现自定义封送的文档很少。我怀疑使用ICustomMarshaler从未真正起飞,因为它总是可以在常规代码中使用手动编组来完成。因此,从来没有真正需要任何高级自定义编组方案的文档。
无论如何,通过各种来源和大量的反复试验,我想我已经对大多数自定义封送工作的实际理解进行了解释。
在你的情况下,你已经为[In]封送正确设置了ManagedToNative方法,并为大多数[Out]封送正确设置了NativeToManaged方法,但是[In,Out]封送实际上有点棘手。 [In,Out] marshaling实际上是就地编组。因此,在退出的过程中,您必须将数据封送回操作的[In]侧提供的同一实例。
这有很多小变化,取决于使用引用或值类型,调用是普通的pInvoke调用还是委托的回调等等。但是考虑什么需要最终在哪里才是关键
您的代码的以下变体按您希望的方式工作(对于.Net 2.0及以上版本似乎也是如此):
//This must be thread static since, in theory, the marshaled
//call could be executed simultaneously on two or more threads.
[ThreadStatic] int[] marshaledObject;
public IntPtr MarshalManagedToNative(object managedObj)
{
if (managedObj == null)
return IntPtr.Zero;
if (!(managedObj is int[]))
throw new MarshalDirectiveException("VariableLengthArrayMarshaler must be used on an int array.");
//This is called on the way in so we must keep a reference to
//the original object so we can marshal to it on the way out.
marshaledObject = (int[])managedObj;
int size = sizeof(int) + marshaledObject.Length * sizeof(int);
IntPtr pNativeData = Marshal.AllocHGlobal(size);
Marshal.WriteInt32(pNativeData, marshaledObject.Length);
Marshal.Copy(marshaledObject, 0, (IntPtr)(pNativeData.ToInt64() + sizeof(int)), marshaledObject.Length);
return pNativeData;
}
public object MarshalNativeToManaged(IntPtr pNativeData)
{
if (marshaledObject == null)
throw new MarshalDirectiveException("This marshaler can only be used for in-place ([In. Out]) marshaling.");
int len = Marshal.ReadInt32(pNativeData);
if (marshaledObject.Length != len)
throw new MarshalDirectiveException("The size of the array cannot be changed when using in-place marshaling.");
Marshal.Copy((IntPtr)(pNativeData.ToInt64() + sizeof(int)), marshaledObject, 0, len);
//Reset to null for next call;
marshalledObject = null;
return marshaledObject;
}
答案 1 :(得分:3)
int len = Marshal.ReadInt32(pNativeData);
int[] arr = new int[len];
您的问题就在这里,您正在创建一个 new 数组。但您需要更新 colorTable 数组。你在MarshalManagedToNative()方法中得到了它的引用,你需要存储它,这样你就可以在MarshalNativeToManaged()方法中再次使用它。
请注意,这会产生许多后果。您的自定义marshaller对象变为有状态,您不能再使用静态实例。如果非托管代码修改了数组长度,则需要采用不同的方法。你可以通过阅读 len 来实现这一点,但实际上没有实现它,所以没关系。只是声称长度没有改变。
答案 2 :(得分:2)
非常感谢斯蒂芬和汉斯的出色答案。我现在可以清楚地看到,我必须保持传递给MarshalManagedToNative
的托管对象,然后从后续调用MarshalNativeToManaged
返回相同的对象。
框架没有提供用于管理此类状态的机制,这有点像绑定。这是因为marshaller为每次调用函数使用相同的自定义编组实例。
我相信斯蒂芬使用线程本地存储的方法会起作用。我个人不是线程本地存储的粉丝。另一种选择是使用键入非托管指针的字典。这是一个例子:using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
namespace CustomMarshaler
{
public class MyCustomMarshaler : ICustomMarshaler
{
private Dictionary<IntPtr, object> managedObjects = new Dictionary<IntPtr, object>();
public IntPtr MarshalManagedToNative(object managedObj)
{
if (managedObj == null)
return IntPtr.Zero;
if (!(managedObj is int[]))
throw new MarshalDirectiveException("MyCustomMarshaler must be used on an int array.");
int[] arr = (int[])managedObj;
int size = sizeof(int) + arr.Length * sizeof(int);
IntPtr pNativeData = Marshal.AllocHGlobal(size);
Marshal.WriteInt32(pNativeData, arr.Length);
Marshal.Copy(arr, 0, pNativeData + sizeof(int), arr.Length);
lock (managedObjects)
{
managedObjects.Add(pNativeData, managedObj);
}
return pNativeData;
}
public object MarshalNativeToManaged(IntPtr pNativeData)
{
int[] arr;
lock (managedObjects)
{
arr = (int[])managedObjects[pNativeData];
managedObjects.Remove(pNativeData);
}
int len = Marshal.ReadInt32(pNativeData);
Debug.Assert(len == arr.Length);
Marshal.Copy(pNativeData + sizeof(int), arr, 0, len);
return arr;
}
public void CleanUpNativeData(IntPtr pNativeData)
{
Marshal.FreeHGlobal(pNativeData);
}
public void CleanUpManagedData(object managedObj)
{
}
public int GetNativeDataSize()
{
return -1;
}
public static ICustomMarshaler GetInstance(string cookie)
{
return new MyCustomMarshaler();
}
}
class Program
{
[DllImport(@"MyLib.dll")]
private static extern void Foo(
[In, Out, MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(MyCustomMarshaler))]
int[] arr
);
static void Main(string[] args)
{
int[] colorTable = new int[] { 1, 2, 3, 6, 12 };
Foo(colorTable);
foreach (int value in colorTable)
Console.WriteLine(value);
}
}
}