13

この質問に関連するカスタム マーシャラーを作成しようとすると ( P/Invoke from C to C# without know 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);
        }
    }
}

反対側には、たまたま Delphi で書かれた簡単なネイティブ DLL があります。

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 に渡され、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

Stephen と Hans の優れた回答に感謝します。MarshalManagedToNativeに渡された管理対象オブジェクトを保持し、その後の への呼び出しから同じオブジェクトを返す必要があることが明確にわかりましたMarshalNativeToManaged

フレームワークがそのような状態を管理するためのメカニズムを提供していないのは、ちょっとした束縛です。これは、マーシャラーが関数の呼び出しごとにカスタム マーシャラーの同じインスタンスを使用するためです。

スレッド ローカル ストレージを使用する Stephen のアプローチはうまくいくと思います。私は個人的にスレッド ローカル ストレージのファンではありません。もう 1 つのオプションは、アンマネージ ポインターをキーとするディクショナリを使用することです。以下に図を示します。

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 に答える