4
\$\begingroup\$

I have an array allocated in C# which I am passing into unmanaged code to be modified. The following code works, but I am not sure if there are any other more 'correct' or 'efficient' methods that I am not aware of.

Code.c

extern "C" {
    class MyObject
    {
    public:
        int32_t X;
    };

    __declspec(dllexport) void ArrayTest(MyObject* a, int length);

    void ArrayTest(MyObject* a, int length)
    {
        for (int i = 0; i < length; i++)
        {
            a[i].X = i + 1;
        }
    }
}

Code.cs

class Program
{
    [StructLayout(LayoutKind.Sequential)]
    public struct MyObject
    {
        public Int32 X;
    }

    [DllImport(@"MyDll.dll", CallingConvention = CallingConvention.Cdecl)]
    public extern static void ArrayTest(IntPtr objects, int length);

    static void Main(string[] args)
    {
        var objects = new MyObject[3];

        var handle = GCHandle.Alloc(objects, GCHandleType.Pinned);
        var ptr = GCHandle.ToIntPtr(handle);

        ArrayTest(ptr, objects.Length);

        for (int i = 0; i < objects.Length; i++)
        {
            var offset = Marshal.SizeOf<MyObject>() * i;

            objects[i] = Marshal.PtrToStructure<MyObject>(IntPtr.Add(ptr, offset));
        }
    }
}
\$\endgroup\$
  • \$\begingroup\$ extern "C" class? C only has struct. \$\endgroup\$ – Cacahuete Frito Jun 29 at 10:04
5
\$\begingroup\$

You must definitely call handle.Free() when finished using it so GC can do the cleaning.


Note that GCHandle.Alloc(objects, GCHandleType.Pinned); only works for structs with pure primitive or to be more precise: blittable types. String fields etc. must be handled differently.


A little optimization:

You repeatedly calculate this

var offset = Marshal.SizeOf<MyObject>() * i;

Instead you can calculate offset once:

var offset = Marshal.Sizeof<MyObject>();

and in the loop do:

ptr = IntPtr.Add(ptr, offset);

If you are willing/allowed to run in unsafe mode you can do:

unsafe public void RunUnsafe()
{
  var objects = new MyObject[3];

  var handle = GCHandle.Alloc(objects, GCHandleType.Pinned);
  var ptr = GCHandle.ToIntPtr(handle);

  ArrayTest(ptr, objects.Length);
  MyObject* pobj = (MyObject*)ptr;

  for (int i = 0; i < objects.Length; i++, pobj++)
  {
    Console.WriteLine((*pobj).X);
  }

  handle.Free();
}

If you are entitled to modify the signature of ArrayTest, you can changed it to:

    public extern static void ArrayTest([In, Out] MyObject[] objects, int length);

The [In, Out] attributes determines that the array should be marshaled both ways so that ArrayTest can work on the provided instances in the array [In], and changes made in ArrayTest are reflected in the objects when the function returns [Out].

Then your method can be simplified to:

  var objects = new MyObject[3];

  ArrayTest(objects, objects.Length);

  foreach (var obj in objects)
  {
    Console.WriteLine(obj.X);
  }
\$\endgroup\$
  • \$\begingroup\$ Thanks. So using [In, Out] won't require pinning? \$\endgroup\$ – rhughes Jun 29 at 9:02
  • \$\begingroup\$ @rhughes: No, the marshaler handles that - as far as I know. \$\endgroup\$ – Henrik Hansen Jun 29 at 9:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.