13

在尝试编写与此问题相关的自定义编组器(P/Invoke from C to C# without known 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

我需要做什么来解决这个问题?

4

3 回答 3

9

很多年前我也遇到过类似的问题,发现关于自定义编组的文档很少。我怀疑使用 ICustomMarshaler 从来没有真正起飞,因为它总是可以在您的常规代码过程中使用手动编组来完成。因此,从来没有真正需要任何高级自定义编组方案的文档。

无论如何,通过各种来源和反复试验,我认为我对大多数自定义编组的工作原理有了一个实际的理解。

在您的情况下,您已经为 [In] 编组正确设置了 ManagedToNative 方法,并为大多数 [Out] 编组正确设置了 NativeToManaged 方法,但 [In, Out] 编组实际上有点棘手。[In, Out] 编组实际上是就地编组。因此,在退出的路上,您必须将数据编组回操作的 [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;
    }
于 2013-09-06T18:19:32.543 回答
3
    int len = Marshal.ReadInt32(pNativeData);
    int[] arr = new int[len];

您的问题位于此处,您正在创建一个数组。但是您需要改为更新colorTable数组。您在 MarshalManagedToNative() 方法中获得了对它的引用,您需要存储它以便可以在 MarshalNativeToManaged() 方法中再次使用它。

请注意,这会产生许多后果。您的自定义编组器对象变为有状态,您不能再使用静态实例。如果非托管代码修改了数组长度,您将需要一种不同的方法。您可以通过回读len来实现这一点,但实际上并没有实现它,所以没关系。只要断言长度没有改变。

于 2013-09-06T19:31:00.210 回答
3

非常感谢 Stephen 和 Hans 的出色回答。我现在可以清楚地看到,我必须保留传递给 的托管对象,MarshalManagedToNative然后从后续调用中返回相同的对象MarshalNativeToManaged

框架没有提供管理这种状态的机制,这有点束缚。这是因为编组器对函数的每次调用都使用相同的自定义编组器实例。

我相信斯蒂芬使用线程本地存储的方法会奏效。我个人不是线程本地存储的粉丝。另一种选择是使用以非托管指针为键的字典。这是一个插图:

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);
        }
    }
}
于 2013-09-10T07:53:21.993 回答