Retrieve record array from Delphi DLL with C#
Asked Answered
H

1

6

I'm trying to write a DLL in Delphi to allow my C# app to access an Advantage database (using VS2013 and not been able to access the data directly).

My issue is after I make the call, the array in C# is full of null values.

The Delphi DLL code:

TItem = record
  Id          : Int32;
  Description : PWideChar;
end;

function GetNumElements(const ATableName: PWideChar): Integer; stdcall;
var recordCount : Integer;
begin
... // code to get the number of records from ATableName
  Result := recordCount;
end;

procedure GetTableData(const ATableName: PWideChar; const AIdField: PWideChar; 
                       const ADataField: PWideChar; result: array of TItem); stdcall;
begin
  ... // ATableName, AIdField, and ADataField are used to query the specific table, then I loop through the records and add each one to result array
  index := -1;
  while not Query.Eof do begin
    Inc(index);
    result[index].Id := Query.FieldByName(AIdField).AsInteger;
    result[index].Description := PWideChar(Query.FieldByName(ADataField).AsString);
    Query.Next;
  end;
  ... // cleanup stuff (freeing created objects, etc)
end;

This appears to be working. I've used ShowMessage to show what the information going in looks like and what it looks like after.



The C# Code:

[StructLayoutAttribute(LayoutKind.Explicit)] // also tried LayoutKind.Sequential without FieldOffset
public struct TItem
{
    [FieldOffset(0)]
    public Int32 Id;

    [MarshalAs(UnmanagedType.LPWStr),FieldOffset(sizeof(Int32))]
    public string Description;
}

public static extern void GetTableData(
    [MarshalAs(UnmanagedType.LPWStr)] string tableName,
    [MarshalAs(UnmanagedType.LPWStr)] string idField,
    [MarshalAs(UnmanagedType.LPWStr)] string dataField, 
    [MarshalAs(UnmanagedType.LPArray)] TItem[] items, int high);

public void GetListItems()
{
    int numProjects = GetNumElements("Project");

    TItems[] projectItems = new TItem[numProjects];

    GetTableData("Project", "ProjectId", "ProjectName", projectItems, numProjects);
}

This code executes, no errors of any kind, but when I iterate through projectItems each one returns

Id = 0
Description = null
Haygood answered 17/9, 2014 at 10:52 Comment(0)
Z
5

There are quite a few issues that I can see. First of all, I would declare the struct like this:

[StructLayoutAttribute(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
public struct TItem
{
    public Int32 Id;
    [MarshalAs(UnmanagedType.BStr)]
    public string Description;
}

You'll need to use UnmanagedType.BStr so that the string can be allocated on the unmanaged side, and deallocated on the managed side. The alternative would be to marshal as LPWStr but then you'd have to allocate with CoTaskMemAlloc on the unmanaged side.

The Delphi record becomes:

type
  TItem = record
    Id          : Int32;
    Description : WideString;
  end;

You can clearly see that your code is wrong by looking at this line:

result[index].Description := PWideChar(Query.FieldByName(ADataField).AsString);

Here you make result[index].Description point to memory that will be deallocated when the function returns.


Trying to use a Delphi open array is risky at best. I would not do that. If you insist on doing so you should at least heed the value passed for high and not write over the end of the array. What's more, you should pass the right value for high. That is projectItems.Length-1.

Now, you are using pass by value for the array so nothing you write in the Delphi code will find its way back to the C# code. What's more, the C# code has [In] marshalling by default and so even when you switch to pass by var, the marshaller won't marshal the items back in to projectItems on the managed side.

Personally I'd stop using an open array and be explicit:

function GetTableData(
    ATableName: PWideChar; 
    AIdField: PWideChar; 
    ADataField: PWideChar; 
    Items: PItem;
    ItemsLen: Integer
): Integer; stdcall;

Here Items points to the first item in the array and ItemsLen gives the length of the supplied array. The function return value should be the number of items copied to the array.

To implement this use either pointer arithmetic, or ($POINTERMATH ON}. I prefer the latter option. I don't think I need to demonstrate that.

On the C# side you have:

[DllImport(dllname, CharSet=CharSet.Unicode)]
public static extern int GetTableData(
    string tableName,
    string idField,
    string dataField, 
    [In,Out] TItem[] items, 
    int itemsLen
);

Call it like this:

int len = GetTableData("Project", "ProjectId", "ProjectName", projectItems, 
    projectItems.Length);
// here you can check that the expected number of items were copied

Having said all of the above, I do have a doubt as to whether or not the marshaller will marshal an array of non-blittable types. I have a feeling that it won't. In which case your main options are:

  1. Switch to passing the string back as IntPtr in the record. Allocate with CoTaskMemAlloc. Destroy on the managed side with Marshal.FreeCoTaskMem.
  2. Use an open query, get next record interface, close query which would lead to multiple calls to the native code, each one returning a single item.

Personally I would opt for the latter approach.

Zootechnics answered 17/9, 2014 at 11:6 Comment(3)
Perfect answer, thanks so much. Using the pointer arithmetic was something I'd not used before (and hence not considered), but now it seems so obvious.Haygood
Will the marshaller marshal that non-blittable array in both directions?Zootechnics
OK, thanks for that. Even after all these p/invoke questions I still an unable to anticipate whether or not the marshaller will gag on a particular complex type!Zootechnics

© 2022 - 2024 — McMap. All rights reserved.