Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Retrieve record array from Delphi DLL with C#

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
like image 725
Ranky Avatar asked Sep 17 '14 10:09

Ranky


People also ask

How to write and call DLL's within Delphi?

How to Write and Call DLL's within Delphi. The following code creates a DLL containing two functions, Min and Max, with the objective of returning the larger of two integers. Start a new DLL project in Delphi (Click File −> New, select DLL). Save the project as delhpdll. Fill in the code in the library as given below.

How to use C++ libraries in Delphi?

In commercial C++ libraries, it is common to get only a few C++ headers and the static library file (.lib) without any of the accompanying .cpp source files. So, in this case, when we want to use those C++ libraries in our Delphi application, we can use a Proxy DLL to make it possible.

How to create a DLL with Min and max functions in Delphi?

The following code creates a DLL containing two functions, Min and Max, with the objective of returning the larger of two integers. Start a new DLL project in Delphi (Click File −> New, select DLL). Save the project as delhpdll. Fill in the code in the library as given below.

How to use C++ proxy DLL in a Delphi application?

The first function will get an index of the object and set the value of the variable. In the second function, it takes the index of the object, calls the “ getSquare ” function of the object, and store the value in the value variable. How to use the C++ Proxy DLL in a Delphi Application? We can link DLLs either statically or dynamically.


1 Answers

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.

like image 130
David Heffernan Avatar answered Oct 24 '22 01:10

David Heffernan