简体   繁体   中英

Recreating C# `Struct` version of Delphi `Record` - to pass into DLL as a parameter

I'm building a DLL in Delphi, and it needs to work similar to how the Windows API works. This DLL only has one exported function...

function DoSomething(var MyRecord: TMyRecord): Integer; stdcall;

...where TMyRecord = my record I will need to re-create in C#. If I'm not mistaken, this is exactly how the standard Windows API works. This record also contains a reference to another record type...

TMyOtherRecord = record
  SomeDC: HDC;
  SomeOtherInt: Integer;
end;

TMyRecord = record
  SomeInteger: Integer;
  SomeColor: TColor;
  SomeText: PChar;
  SomeTextSize: Integer;
  MyOtherRecord: TMyOtherRecord;
end;

Question part 1:

I'd like to see if I can avoid using PChar, if at all possible. I don't expect anything over 255 characters to be passed through. Is there another type I can use instead which won't require me to use a size of string ?


Question part 2:

I need to double check that I am declaring this C# struct class correctly, because it needs to perfectly match the Record declared in Delphi...

public struct MyOtherRecord
{
  public IntPtr SomeDC;
  public int SomeOtherInt;
}

public struct MyRecord
{
  public int SomeInteger;
  public Color SomeColor;
  public string SomeText;
  public int SomeTextSize;
  public MyOtherRecord OtherReord = new MyOtherRecord();
}

Question part 3:

Is it safe in this case to have a record inside of a record (or struct inside of a struct)? Pretty sure it is, but I need to make sure.

I'm going to assume that the information is flowing from C# to Delphi and not the other way, largely because that makes life a lot easier when writing the answer, and you didn't state otherwise!

In that case the Delphi function declaration should be:

function DoSomething(const MyRecord: TMyRecord): Integer; stdcall;

The first point is that you can't expect System.Drawing.Color to be handled by the P/invoke marshaller. Declare the color as int and use ColorTranslator.ToWin32 and ColorTranslator.FromWin32 to handle the conversion.


There's nothing to be afraid of with PChar . You don't need a field with the string length since the length is implicit in a PChar due to the null-terminator. Just declare the field as string in the C# struct, PChar in the Delphi record and let the P/invoke marshaller do its magic. Don't try to write to the PChar content from Delphi. That will end in tears. If you want to pass a string back to the C# code then there are ways, but I won't address them here.


It's perfectly fine to have inline structs. Nothing to worry about there. Don't allocate them with new . Just treat them as value types (which they are) like int , double etc.


In due course you will need to add StructLayout attributes and so on, declare your DLL function with DllImport and so on.


To summarise, I would declare your structs like this:

Delphi

TMyOtherRecord = record
  SomeDC: HDC;
  SomeOtherInt: Integer;
end;

TMyRecord = record
  SomeInteger: Integer;
  SomeColor: TColor;
  SomeText: PChar;
  MyOtherRecord: TMyOtherRecord;
end;

function DoSomething(const MyRecord: TMyRecord): Integer; stdcall;

C#

[StructLayout(LayoutKind.Sequential)]
public struct MyOtherRecord
{
  public IntPtr SomeDC;
  public int SomeOtherInt;
}

[StructLayout(LayoutKind.Sequential)]
public struct MyRecord
{
  public int SomeInteger;
  public int SomeColor;
  public string SomeText;
  public MyOtherRecord MyOtherRecord;
}

[DllImport("mydll.dll")]
static extern int DoSomething([In] ref MyRecord MyRecord);

I've not marked the string with a MarshalAs since the default is to marshal it as a LPSTR which is the same as a Delphi 7 PChar .

I've only compiled this in my head so there may be a few wrinkles.

If you don't want to use a PChar on the Delphi side, your best bet is a fixed-length char array. However, the PChar type is specifically built to handle these situations: it's a C-style NULL-terminated string. For clarity in your C# definition, you can use the MarshalAs attribute to indicate exactly what kind of 'string' you are expecting on the call site. The default for a string inside a struct depends on which version of the Framework you're using: the Compact Framework only supports Unicode strings (LPWSTR), otherwise it will be LPSTR. Since there's 7 different options for string marshaling, I always specify the one I want even if it's the default, but I think in your case it's optional.

Also, as noted above, the C# Color type is not the same as the Delphi TColor type. TColor is just an integer in a strange mixed format, while Color has a bunch of additional properties beyond just the RGB colors. You have a couple of options: define a new C# struct to match the TColor definition, or just use int and build the values manually. Here's a better description of the structure of a TColor .

Finally, for value types like struct, you don't actually need to instantiate them with new ; if you just declare a variable of a struct type the space is allocated for you. The only benefit of using new to instantiate a struct is that your constructor will run (you don't have one), and all the fields will be initialized to their default values. If you plan to fill in all the fields anyway, it's just overhead you don't need.

Overall, this is what I would probably use:

public struct MyOtherRecord
{
  public IntPtr SomeDC;
  public int SomeOtherInt;
}

public struct MyRecord
{
  public int SomeInteger;
  public int SomeColor;
  [MarshalAs(UnmanagedType.LPSTR)] public string SomeText;
  public int SomeTextSize;
  public MyOtherRecord OtherRecord;
}

One thing I am not sure of is the record alignment. I think I recall reading that Delphi "in theory" used 8-byte alignment by default but that "in practice" it aligned fields based on their type; this is controlled by the ${A} directive. In C#, unless you use an explicit [StructLayout] your fields will get alignment based on their size. Everything in your records is an integer-sized value, so you ought to be safe, but if you see what looks like data corruption, check the "sizeof" values for both the Delphi and C# structures and make sure they are the same.

If not, you can use [StructLayout(LayoutKind.Explicit)] and [FieldOffset] attributes to exactly specify the alignment of your C# structure.

UPDATE:

Thanks to @David Heffernan for pointing out that PChar in Delphi 7 is LPSTR (in which case, my personal preference would be to use PWideChar in Delphi, since the .NET CF don't support ANSI, and Windows uses UTF-16 internally anyway, but whichever works.) Answer updated to match.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM