1
votes

I've got some old C DLLs that serve as intermediaries between my Delphi app and MATLAB compiled DLLs. These were originally developed with Delphi 7, and worked fine. Now, however, I've upgraded first to XE7 and now to 10.2.3, and I'm having problems. Basically, I have to pass a pointer to a record to the DLL; the structure includes fields that are pointers to old-fashioned C-style strings (i.e. an array of chars, where each char is a byte, and terminated by a 0 byte).

So consider this record:

MyRec = record
  id: Integer;
  name: PByte;
end;

I wrote a function like this:

function MakeCString(S: String): TBytes;
var
  I: Integer;
  len: Integer;
begin
  len := Length(S);
  SetLength(Result, len + 1);
  if len > 0 then
    begin
      for I := 0 to (len - 1) do
        begin
          Result[i] := Byte(S[I + 1]);
        end;
    end;
  Result[len] := 0;
end;

Then, my intent is to use this like this:

var r: MyRec;
r.id := 27;
r.name := MakeCString('Jimbo');
callMatlabWrapperDll(@r);

I know this is ugly and prone to memory loss - if it even works reliably at all! It seems to sort of work, but now on a different Windows 10 machine from where it worked (maybe), on my dev machine (also Windows 10), I get errors.

I've banged my head on this long enough. What I need is a way to create a C-style string, pass its address to a DLL, and clean up afterwards. (Note: This is all Win32, running on Win64).

Thanks.

2
Your code won't handle non ASCII text properly. But there's no issue with memory leaks that can be seen here. And I don't see why this should give erroslrs where delphi 7 did not. But you didn't tell us what the error is. Or show the C code. - David Heffernan

2 Answers

3
votes

Your MakeCString() is implemented wrong. It is not converting Unicode characters to ANSI, it is just truncating the characters as-is from 16-bit to 8-bit, which is not the same thing.

For that matter, you don't actually need MakeCString() at all. You can use TEncoding.GetBytes() instead.

Also, you are assigning the result of your "conversion" directly to your record's PByte field. You should first assign it to a local TBytes variable, and then get a pointer to its data.

Try this instead:

type
  MyRec = record
    id: Integer;
    name: PByte;
  end;

procedure callMatlab;
var
  r: MyRec;
  name: TBytes;
begin
  name := TEncoding.Default.GetBytes('Jimbo'#0);
  r.id := 27;
  r.name := PByte(name);
  callMatlabWrapperDll(@r);
end;

Alternatively, you can (and should) use PAnsiChar instead, which is what C-style APIs generally use for 8-bit strings, eg:

type
  MyRec = record
    id: Integer;
    name: PAnsiChar;
  end;

procedure callMatlab;
var
  r: MyRec;
  name: AnsiString;
begin
  name := AnsiString('Jimbo');
  r.id := 27;
  r.name := PAnsiChar(name);
  callMatlabWrapperDll(@r);
end;

This would be more inline with your Delphi 7 code, when String was still AnsiString.

1
votes

Two things are important to note:

  1. Since Delphi 2009, string is Unicode (that is, two bytes per character).

  2. Delphi strings are essentially C-style strings with an additional header.

The first point means that you must convert the strings from Unicode to ANSI. The second point means that you don't need to write a function that creates a C-style string from a Delphi string -- the Delphi string already is C-style if you just disregard its header. Simply casting a Delphi string to PChar gives you a pointer to the C-style string, because the header is at a negative offset from the string pointer's value.

If MyDelphiString is your Delphi string, this can be converted to an old AnsiString by a simple cast: AnsiString(MyDelphiString). This will create a new string on the heap, with the same content but now in legacy ANSI format (1 byte per char). Of course you will lose "special" characters like ⌬∮☃电脑.

Let us save this to a local variable so we know that its lifetime is (at least) as long as this variable is in scope (thanks Remy Lebeau):

var
  MyAnsiString: AnsiString;
begin
  MyAnsiString := AnsiString(MyDelphiString)

And to get a pointer to the C-style string which is a subset of this string object, just write PAnsiChar(MyAnsiString). If you insist on using a PByte type in the record, you can be explicit about this reinterpretation as well:

r.Name = PByte(PAnsiChar(MyAnsiString))

But it would be nicer if the record member's type was PAnsiChar instead of PByte.