I am using Robert Giesecke Unmanaged Exports 1.2.6 in VS2010 and my goal is to pass an array of structs from c# (.NET 3.5) to delphi (D7).
I have to admit, that I'm not that familiar with delphi.
I've already read this post, but the suggested answer didn't work for me:
When calling func in delphi the CPU-debugging-window opens and if I continue the app exits without exception and without the desired result.
Here is the code I tried:
C# platform x86
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using RGiesecke.DllExport;
namespace ArrayTest
{
public class Class1
{
public struct Sample
{
[MarshalAs(UnmanagedType.BStr)]
public string Name;
}
[DllExport]
public static int func(
[Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)]
Sample[] samples,
ref int len
)
{
// len holds the length of the array on input
// len is assigned the number of items that have been assigned values
// use the return value to indicate success or failure
for (int i = 0; i < len; i++)
samples[i].Name = "foo: " + i.ToString();
return 0;
}
}
}
Delphi7
program DelphiApp;
{$APPTYPE CONSOLE}
uses
SysUtils,
ActiveX;
type
TSample = record
Name: WideString;
end;
PSample = ^TSample;
function func(samples: PSample; var len: Integer): Integer; stdcall;
external 'ArrayTest.dll';
procedure Test2;
var
samples: array of TSample;
i, len: Integer;
begin
len := 10;
SetLength(samples, len);
if func(PSample(samples), len)=0 then
for i := 0 to len-1 do
Writeln(samples[i].Name);
end;
begin
Test2();
end.
As mentioned earlier, the debugger opens the CPU-Window and if I continue the app exits without exception or error message.
If I run it without dbugger, Windows tells me the app isn't working any more and the app closes.
What am I missing?
Update
Modified code:
[DllExport]
public static int func(
[Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)]
Sample[] samples,
ref int len
)
{
Console.WriteLine("return 0");
return 0;
}
procedure Test2;
var
samples: array of TSample;
i, len: Integer;
begin
len := 10;
SetLength(samples, len);
if func(PSample(samples), len)=0 then
for i := 0 to len-1 do
Writeln('D7: ', i);
end;
Even if I don't access the array on either side, the behaviour still is the same.
Console-output: return 0
Seems like I found the issue:
The code runs fine if .NET 4.0 or higher is used. If you use .NET 3.5 or lower the len-parameter has to be passed by value.
See MSDN-documentation SizeParamIndex v3.5:
The parameter containing the size must be an integer that is passed by value.
See MSDN-documentation SizeParamIndex v4.0:
When arrays are passed as C-style arrays, the marshaler cannot
determine the size of the array. Therefore, to pass an managed array
to an unmanaged function or method, you must provide two arguments:
The array, defined by reference or value.
The array size, defined by reference or value.
Code working with .NET 3.5:
C#
[DllExport]
public static int func(
[Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)]
Sample[] samples,
int len,
ref int outLen
)
{
// len holds the length of the array on input
// outLen is assigned the number of items that have been assigned values
// use the return value to indicates success and the required array size (>=0) or failure (<0)
int requiredSize = 20;
if (requiredSize < len)
{
len = requiredSize;
}
for (outLen = 0; outLen < len; outLen++)
{
samples[outLen].Name = "foo: " + outLen.ToString();
}
return requiredSize;
}
Delphi7
function func(samples: PSample; len: Integer; var outLen: Integer): Integer; stdcall;
external 'ArrayTest.dll';
procedure Test2;
var
samples: array of TSample;
i, len: Integer;
begin
len := 0;
// query the required array size
i := func(PSample(samples), len, len);
if i>0 then
begin
len := i;
SetLength(samples, len);
if func(PSample(samples), len, len)>=0 then
for i := 0 to len-1 do
Writeln(samples[i].Name);
end;
end;
Conclusion:
The code posted in my question and posted by David Heffernan here only works with .NET >= 4.0!
If you have to use .NET <= 3.5 you must pass the arraysize by value and not by reference!
Related
I call a dll "plcommpro.dll" for a specific operations on Access Controls
the following C# code is working perfect and retrieve data correctly in buffer
[DllImport("plcommpro.dll", EntryPoint = "GetDeviceData")]
public static extern int GetDeviceData(IntPtr h, ref byte buffer,
int buffersize, string tablename, string filename, string filter, string
options);
Now, I need to to write same operation from Delphi, So I tried the following:
TGetDeviceData = Function(iDevID : NativeInt; buffer : Pbyte ; iSize :
Integer;
tablename, filename, strFilter, strOptions : PAnsiChar) : Int64;stdcall;
and I call the function as follows:
var
myBuffer : TBytes;
iRetLog : Integer;
bufferSize : Integer;
sConnect : TConnect;
GetDeviceData : TGetDeviceData;
dllHandle : THandle;
iDevID : Integer;
begin
dllHandle := LoadLibrary('plcommpro.dll') ;
if dllHandle <> 0 then
begin
#sConnect := GetProcAddress(dllHandle, 'Connect');
if #sConnect <> Nil then
begin
strParams := PChar('protocol=TCP,ipaddress=' + grd_Machines.Cells[cl_Machine_IP, iLoop] + ',port=4370,timeout=2000,passwd=');
iDevID := sConnect(strParams);
strTableName := PAnsiChar(AnsiString(('user')));
strDatas := PAnsiChar(AnsiString(''));
strFileName := PAnsiChar(AnsiString(''));
strFilter := PAnsiChar(AnsiString(''));
strOptions := PAnsiChar(AnsiString(''));
#GetDeviceData := GetProcAddress(dllHandle, 'GetDeviceData');
if #GetDeviceData <> Nil then
begin
try
buffersize := 1024*1024;
//bufferSize := MaxInt - 1;
SetLength(myBuffer, 1024*1024);
mem_AttLogs.Lines.Add('buffer Size : ' + IntToStr(buffersize) );
iRetLogs := GetDeviceData(iDevID, PByte(myBuffer[0]), buffersize, strTableName, strFileName, strFilter, strOptions);
if iRetLogs > 0 then
begin
....
//Here: I need to read the returned values from the function; but it always fails
end
The code is modified to explain my case more clearly. Can you help?
The C# declaration you give is obviously flawed: ref byte buffer makes no sense. A buffer is not one byte. It should probably be something like [out] byte[] buffer (thanks to David Heffernan). Both translate an underlying pointer, but the conversion done on the C# side is different.
Since this seems to be interop code to interface with a plain Windows DLL, I can see what the original must have been: a pointer to bytes, which is best translated as PByte in Delphi (but no var, that would introduce one level of indirection too many).
So now it should be something like:
var
GetDeviceData: function(h: THandle; buffer: PByte; buffersize: Integer;
tablename, filename, filter, options: PAnsiChar): Integer stdcall;
Now you finally updated your code, the error you get is quite obvious:
iRetLog := GetDeviceData(iDevID, PByte(myBuffer[0]), buffersize,
strTableName, strFileName, strFilter, strOptions);
That is wrong. You are casting an AnsiChar (i.e. myBuffer[0]) to a pointer. You need a pointer to the first element of myBuffer, so do:
iRetLog := GetDeviceData(iDevID, #myBuffer[0], buffersize,
strTableName, strFileName, strFilter, strOptions); // Note the #
FWIW, since you seem to be using constant strings, you can just do:
iRetLog := GetDeviceData(iDevID, #myBuffer[0], buffersize, 'user', '', '', '');
And to make this easier to maintain, don't use literal numbers if you already have a variable that has this number (and must be the exact same size anyway), so do this instead:
buffersize := 1024*1024;
SetLength(myBuffer, buffersize);
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?
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;
I have just asked and obtained an answer to my question that was : "can't return custom type instance with unmanaged export (Robert Giesecke)" -> can't return custom type instance with unmanaged export (Robert Giesecke)
I wonder if (and how) it is possible to pass arrays of struct from .NET to Delphi using unmanaged export (Robert Giesecke):
Returning arrays directly like
[DllExport] public static void CreateSampleInstance(out Sample[] sample)
using array member in a returned struct
[DllExport] public static void CreateSampleInstance(out Sample sample)
and
`public struct Sample
{
Other[] Others;
}`
My question here is how to write the Delphi side and what attribute to set in the .NET one.
Thanks a lot.
Arrays are more tricky because you need to take more care over where the array is allocated and destroyed. The cleanest approach is always to allocate at the caller, pass the array to the callee to let it fill out the array. That approach would look like this in your context:
public struct Sample
{
[MarshalAs(UnmanagedType.BStr)]
public string Name;
}
[DllExport]
public static int func(
[Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex=1)]
Sample[] samples,
ref int len
)
{
// len holds the length of the array on input
// len is assigned the number of items that have been assigned values
// use the return value to indicate success or failure
for (int i = 0; i < len; i++)
samples[i].Name = "foo: " + i.ToString();
return 0;
}
You need to specify that the array needs to be marshalled in the out direction. If you wanted values marshalled both ways then you would use In, Out instead of Out. You also need to use MarshalAs with UnmanagedType.LPArray to indicate how to marshal the array. And you do need to specify the size param so that the marshaller knows how many items to marshal back to the unmanaged code.
And then on the Delphi side you declare the function like this:
type
TSample = record
Name: WideString;
end;
PSample = ^TSample;
function func(samples: PSample; var len: Integer): Integer; stdcall;
external dllname;
Call it like this:
var
samples: array of TSample;
i, len: Integer;
....
len := 10;
SetLength(samples, len);
if func(PSample(samples), len)=0 then
for i := 0 to len-1 do
Writeln(samples[i].Name);
Update
As AlexS discovered (see comments below), passing the size param index by reference is only supported on .net 4. On earlier versions you need to pass the size param index by value.
The reason I chose to pass it by reference here is to allow for the following protocol:
The caller passes in a value indicating how large the array is.
The callee passes out a value indicating how many elements have been populated.
This works well on .net 4, but on earlier versions you would need to use an extra parameter for step 2.
I've been trying to invoke a method that have been created in Delphi in the following way:
function _Func1(arrParams: array of TParams): Integer;stdcall;
type
TParams = record
Type: int;
Name: string;
Amount : Real;
end;
My code is:
[DllImport("some.dll", EntryPoint = "_Func1", CallingConvention = CallingConvention.StdCall)]
public static extern int Func(
[MarshalAs(UnmanagedType.LPArray, ArraySubType=UnmanagedType.Struct)] TParams[] arrParams)
And the struct is:
[StructLayoutAttribute(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct TParams
{
public int Type;
[MarshalAs(UnmanagedType.AnsiBStr)]
public string Name;
public double Amount;
}
When I am calling this method I'm getting the error:
Cannot marshal field 'Name' of type 'TParams': Invalid managed/unmanaged type combination (String fields must be paired with LPStr, LPWStr, BStr or ByValTStr).
However none of those combinations works, as Delphi's strings are prefixed with its length and it is Ansi for sure (I've tried it with other string parameters). Does anyone have a clue how to solve this?
There are two main problems with this, use of open arrays and use of Delphi string.
Open arrays
Delphi open arrays are implemented by passing a pointer to the first element of the array and an extra parameter specifying the index of the last item, high in Delphi terminology. For more information see this answer.
Delphi strings
The C# marshaller cannot interop with a Delphi string. Delphi strings are private types, only to be used internally to a Delphi module. Instead you should use a null-terminated string, PAnsiChar.
Putting it all together you can write it like this:
Delphi
type
TParams = record
_Type: Integer;//Type is a reserved word in Delphi
Name: PAnsiChar;
Amount: Double;
end;
function Func(const arrParams: array of TParams): Integer; stdcall;
C#
[StructLayoutAttribute(LayoutKind.Sequential)]
public struct TParams
{
public int Type;
public string Name;
public double Amount;
}
[DllImport("some.dll")]
public static extern int Func(TParams[] arrParams, int high);
TParams[] params = new TParams[len];
...populate params
int retval = Func(params, params.Length-1);
To compliment David's answer, you can marshal to a Delphi string, but it's ugly. In C#, you have to replace all of your strings in the struct with IntPtr.
private static IntPtr AllocDelphiString(string str)
{
byte[] unicodeData = Encoding.Unicode.GetBytes(str);
int bufferSize = unicodeData.Length + 6;
IntPtr hMem = Marshal.AllocHGlobal(bufferSize);
Marshal.WriteInt32(hMem, 0, unicodeData.Length); // prepended length value
for (int i = 0; i < unicodeData.Length; i++)
Marshal.WriteByte(hMem, i + 4, unicodeData[i]);
Marshal.WriteInt16(hMem, bufferSize - 2, 0); // null-terminate
return new IntPtr(hMem.ToInt64() + 4);
}
This can directly be sent to Delphi, where it'll properly be read as a string.
Remember that you must free this string when you're done with it. However, GlobalFree() can't be called directly on the pointer to the string, because it doesn't point to the start of the allocation. You'll have to cast that pointer to a long, then subtract 4, then cast it back to a pointer. This compensates for the length prefix.
I have a function in C++ written as:
MyFunc(const double* pArray, int length);
I need to pass a non-constant array into it:
//C#
double[] myDoubleArray = new double[] { 1, 2, 3, 4, 5 };
MyFunc(myDoubleArray, 5);
The program is breaking when I do this.
edit:
//C# declaration
[DllImport(#"RTATMATHLIB.dll", EntryPoint = "?MyFunc##YANPBNHHHH#Z")]
public static extern double MyFunc(double[] data, int length);
//C# usage
public static double MyFunc(double[] data)
{
return MyFunc(data, data.Length);
}
//C++ export
__declspec(dllexport) double MyFunc(const double* data, int length);
//C++ signature
double MyFunc(const double* data, int length)
{
return 0; //does not quite matter what it returns...
}
You can always pass a non-constant array to a function requiring a constant array. The const qualifier to the array implies that the function will not modify the contents of the array. The array does not need to be constant before passing to the function; the function will not modify the contents, due to the const in the declaration
Constness can be trivially added. Your program is not wrong. You must have missed some other detail.