Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I write a custom marshaler which allows data to flow from native to managed?

Tags:

c#

pinvoke

In attempting to write a custom marshaler related to this question (P/Invoke from C to C# without knowing size of array), I have come across something I cannot understand. This is the first ever custom marshaler that I have written so no doubt I'm missing something obvious due to my ignorance.

Here's my C# code:

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);
        }
    }
}

On the other side is a trivial native DLL, written in Delphi as it happens.

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.

The idea is that the array is passed to the DLL which then prints out the length field, and the values of the array. The native code also increments each value of the array by 1.

So, I expect to see this output:

5
1
2
3
6
12
2
3
4
7
13

But unfortunately I see this output:

5
1
2
3
6
12
1
2
3
6
12

Under the debugger I can see that MarshalNativeToManaged is executing, and that the values that it returns have been incremented. But these incremented values don't find there way back into the object that is passed to Foo.

What do I need to do to fix this?

like image 269
David Heffernan Avatar asked Aug 28 '13 21:08

David Heffernan


People also ask

What is marshalling and why do we need it?

Marshalling is the process of transforming the memory representation of an object into another format, which is suitable for storage or transmission to other software applications. Marshalling allows communication between remote objects by converting an object into serialized form.

What is .NET marshalling?

Marshalling is the process of transforming types when they need to cross between managed and native code. Marshalling is needed because the types in the managed and unmanaged code are different.

What is COM marshaler in c#?

A marshaler provides a bridge between the functionality of old and new interfaces.


1 Answers

I had a similar problem many years ago and found that there was very little documentation on Custom Marshaling. I suspect using ICustomMarshaler never really took off since it can always be done using manual marshaling in the course of your regular code. And so there was never really a need for any documentation of advanced custom marshaling scenarios.

Anyway, through a variety of sources and much trial and error I think I teased out a practical understanding of how most of Custom Marshaling works.

In your case, you have set up the ManagedToNative method correctly for [In] marshaling and the NativeToManaged method correctly for most [Out] marshaling but [In, Out] marshaling is actually a bit trickier. [In, Out] marshaling is actually in-place marshaling. So on the way back out you must marshal the data back to the same instance that was provided in the [In] side of the operation.

There are a number of small variations on this depending on whether using reference or value types, whether the call is a normal pInvoke call or a callback on a delegate, etc. But thinking about what needs to end up where is the key.

The following variation on your code works the way you want it to (and it seems to works the same way for .Net 2.0 and up):

        //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;
    }
like image 99
Stephen Martin Avatar answered Sep 27 '22 21:09

Stephen Martin