Search code examples
c#arraysdelphimarshallingdllexport

DllExport: Passing an array of arrays from Delphi to C#


I am using RGiesecke's "Unmanaged Exports" package to create a dll from C# that can be called from a Delphi application.

Specifically, I am looking to pass an array of arrays of a struct.

What I have made work in C# is

public struct MyVector
{
  public float X;
  public float Y;
}

[DllExport]
public static void DoStuff([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] 
  MyVector[] vectors, int count)
{
  // Do stuff
}

Which can then be called from Delphi, doing something like this:

unit MyUnit
interface
type
  TVector = array[X..Y] of single;
  TVectorCollection = array of TVector;
  procedure TDoExternalStuff(const vectors : TVectorCollection; count : integer; stdcall;
  procedure DoSomeWork;

implementation

procedure DoSomeWork;
var
  vectors : array of TVector;
  fDoExternalStuff : TDoExternalStuff;
  Handle: THandle;
begin
  // omitted: create and fill vectors
  Handle := LoadLibrary('MyExport.dll');
  @fDoExternalStuff := GetProcAddress(Handle, 'DoStuff');
  fDoExternalStuff(vectors, Length(vectors)); 
end;

end.

However, what I really need to do is to pass an array of array of TVector. An array of structs that hold an array of TVector would also do. But writing

[DllExport]
public static void DoStuff([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] 
  MyVector[][] vectors, int count)
{
  // Do stuff
}

Does not work with Delphi

...
TVectorCollection = array of array of TVector;
...

procedure DoSomeWork;
var
  vectors : array of array of TVector;
  fDoExternalStuff : TDoExternalStuff;
  Handle: THandle;
begin
  // omitted: create and fill vectors
  Handle := LoadLibrary('MyExport.dll');
  @fDoExternalStuff := GetProcAddress(Handle, 'DoStuff');
  fDoExternalStuff(vectors, Length(vectors)); //external error 
end;

And I would also be a bit surprised if it did, since I am not specifying the length of the individual elements of the jagged array anywhere.

Is there a way for me to setup my DllExport function to be able to marshal this type of element?


Solution

  • Is there a way for me to setup my DllExport function to be able to marshal this type of element?

    No, p/invoke marshalling never descends into sub-arrays. You will have to marshal this manually.

    Personally I'd pass an array of pointers to the first elements of the sub-arrays, and an array of the lengths of the sub-arrays.

    On the C# side it will look like this:

    [DllImport("kernel32.dll", EntryPoint = "CopyMemory", SetLastError = false)]
    public static extern void CopyMemory(IntPtr dest, IntPtr src, uint count);
    
    [DllExport]
    public static void DoStuff( 
        [In] 
        int arrayCount, 
        [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] 
        IntPtr[] arrays, 
        [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] 
        int[] subArrayCount, 
    )
    {
        MyVector[][] input = new MyVector[arrayCount];
        for (int i = 0; i < arrayCount; i++)
        {
            input[i] = new MyVector[subArrayCount[i]];
            GCHandle gch = GCHandle.Alloc(input[i], GCHandleType.Pinned);
            try
            {
                CopyMemory(
                    gch.AddrOfPinnedObject(), 
                    arrays[i], 
                    (uint)(subArrayCount[i]*Marshal.SizeOf(typeof(MyVector))
                );
            }
            finally
            {
                gch.Free();
            }
        }
    }
    

    It's a little messy since we can't use Marshal.Copy because it doesn't know about your struct. And there is []no simple built in way to copy from IntPtr to IntPtr](https://github.com/dotnet/corefx/issues/493). Hence the p/invoke of CopyMemory. Anyway, there's many ways to skin this one, this is just my choice. Do note that I am relying on your type being blittable. If you changed the type so that it was not blittable then you'd need to use Marshal.PtrToStructure.

    On the Delphi side you can cheat a little and take advantage of the fact that a dynamic array of dynamic arrays is actually a pointer to an array of pointers to the sub-arrays. It will look like this:

    type  
      TVectorDoubleArray = array of array of TVector;
      TIntegerArray = array of Integer;
    
    procedure DoStuff(
      arrays: TVectorDoubleArray; 
      arrayCount: Integer;
      subArrayCount: TIntegerArray
    ); stdcall; external dllname;
    
    ....
    
    procedure CallDoStuff(const arrays: TVectorDoubleArray);
    var
      i: Integer;
      subArrayCount: TIntegerArray;
    begin
      SetLength(subArrayCount, Length(arrays));
      for i := 0 to high(subArrayCount) do
        subArrayCount[i] := Length(arrays[i]);
      DoStuff(Length(Arrays), arrays, subArrayCount);
    end;