madsbirk
madsbirk

Reputation: 73

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?

Upvotes: 4

Views: 1552

Answers (1)

David Heffernan
David Heffernan

Reputation: 613461

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;

Upvotes: 4

Related Questions