7

I am experiencing some very strange things right now. When I am passing a struct from C++ to a Delphi DLL as a parameter everything works fine. However, as soon as I want to receive a record as a result I either get wrong values or exceptions. I deactivated the alignment of the record so that passing them should work! Heres the code!

Delphi DLL:

TSimpleRecord = packed record
  Nr1 : Integer;
  Nr2 : Integer;
end;

//...

function TTest() : TSimpleRecord; cdecl;
begin
  Result.Nr1 := 1;
  Result.Nr2 := 201;
  ShowMessage(IntToStr(SizeOf(Result)));
end;

C++ call :

#pragma pack(1)
struct TSimpleRecord
{
    int Nr1;
    int Nr2;
};

//...

    typedef TSimpleRecord (__cdecl TestFunc)(void);
    TestFunc* Function;
    HINSTANCE hInstLibrary = LoadLibrary("Reactions.dll");
    if (hInstLibrary)
    {
        Function = (TestFunc*)GetProcAddress(hInstLibrary, "TTest");
        if (Function)
        {
            TSimpleRecord Result = {0};
            Result = Function();
            printf("%d - %d - %d", sizeof(Result), Result.Nr1, Result.Nr2);
            cin.get();
        }
    }

I have got no idea why passing this record as a parameter works but not as a result of a function!?

Can anybody help me?`

Thanks

PS: As I said, both C++ and Delphi show that the record is 8 bytes large.

Henry
  • 727
  • 1
  • 6
  • 25

2 Answers2

5

Some compilers will return struct types (possibly depending on the size) in registers, others will add a hidden extra parameter where the result should be stored. Unfortunately, it looks like you're dealing two compilers that do not agree on how to return these.

You should be able to avoid the problem by explicitly using an out parameter instead.

procedure TTest(out Result: TSimpleRecord); cdecl;
begin
  Result.Nr1 := 1;
  Result.Nr2 := 201;
end;

Do not forget to update the C++ code accordingly.

Rudy Velthuis has written about this:

This showed me that the ABCVar struct was returned in the registers EDX:EAX (EDX with the top 32 bits, and EAX with the lower ones). This is not what Delphi does with records at all, not even with records of this size. Delphi treats such return types as extra var parameters, and does not return anything (so the function is actually a procedure).

[...]

The only type which Delphi returns as EDX:EAX combination is Int64.

which suggests that an alternative way to avoid the problem is

function TTest() : Int64; cdecl;
begin
  TSimpleRecord(Result).Nr1 := 1;
  TSimpleRecord(Result).Nr2 := 201;
end;

Note that Delphi allows such type punning even in situations where the behaviour would be undefined in C++.

Community
  • 1
  • 1
  • Thanks, exactly what I was looking for! Even though the example using Int64 doesn't work for me – Henry Apr 20 '13 at 10:47
  • @Henry In what way does it not work? Does it give you an error message, or do you still get incorrect behaviour? I ask because I've verified that the result is returned in registers (as intended) in a standalone application. Glad that the other way to avoid the problem is working, anyway :) –  Apr 20 '13 at 10:56
  • Although I agree that the Int64 trick should work. That's how I read all the docs. – David Heffernan Apr 20 '13 at 11:41
  • @DavidHeffernan Technically you're right, but I really don't think it's a good idea to rely on that unnecessarily. If the C++ code calls a procedure, the Delphi code should provide a procedure, only to keep the code readable. Even if the result of the compilation may be bitwise identical to Delphi code that continues to provide a function. –  Apr 20 '13 at 11:41
  • Agreed 100% But both our answers are about the ABI. – David Heffernan Apr 20 '13 at 11:54
  • Right, we just took a slightly different approach. You're modifying the C++ code to accept the Delphi function, I'm suggesting to change the signature to something the two compilers agree on binary-wise. Both approaches will give identical C++ source code, just different Delphi code. And both approaches should work just fine. –  Apr 20 '13 at 12:02
  • Trying to use Int64 as you've shown above is giving me a "E2089 invalid typecast" – Henry Apr 20 '13 at 12:20
  • @Henry No, that's not true. The Int64 trick compiles and works. FWIW, you should not pack records. – David Heffernan Apr 20 '13 at 13:31
  • @Henry You can get that error message if the record is not exactly the same size as `Int64`, and in that case returning it as `Int64` would be wrong. But with the definition in your question, the sizes do match. At least, it works from Delphi 7 to XE, with the exception of Delphi .NET (which you're not using, or you'd be having far bigger problems). –  Apr 20 '13 at 13:43
  • @David Heffernan If it only works with "standard" records then this method will be useless since it wouldn't have the same size as the c-struct or am I missing something? – Henry Apr 20 '13 at 13:53
  • @hvd I am done with Delphi, no need to start with an imo even worse version of it :D – Henry Apr 20 '13 at 13:53
  • What are standard records? That trick can only work if the sizes match. And they do. – David Heffernan Apr 20 '13 at 13:54
  • They only do because I am using packed records.. The keyword packed disables the alignment just as #pragma pack(1) does on the C-Side – Henry Apr 20 '13 at 13:55
  • @Henry For this specific record (containing two `Integer`s), the size would be the same with or without packing. In C++ too. –  Apr 20 '13 at 13:56
  • Exactly. By packing you may mis align your record. I see no need for packing here. – David Heffernan Apr 20 '13 at 13:57
  • Oh ok, didn't notice that since I am only using these integers in this example. The record I am actually using got many different var-types and the size varied a lot between c and delphi so that I needed to disable the alignment. Learned a lot, thanks :) – Henry Apr 20 '13 at 14:01
  • 1
    @Henry That explains why you can't use the `Int64` cast too, then: that relies on the fact that with the definition in your question, the structure is exactly 8 bytes. For larger structures, Delphi and C++ should already agree on how to return it, so you shouldn't need tweaking. –  Apr 20 '13 at 14:05
  • Yes. You really don't want to be packing records. Doing so will lead to poor memory access due to mis alignment. – David Heffernan Apr 20 '13 at 14:08
1

Delphi does not follow the platform standard ABI for return values. The standard ABI passes return values to the caller by value. Delphi treats the return value as an implicit extra var parameter, passed after all other parameters. The documentation describes the rules.

You can change your calling code to match that. Pass an extra reference to struct parameter in your C++ function.

typedef void (__cdecl TestFunc)(TSimpleRecord&);     

If you are going to do this on the C++ side, you would be best doing the same change on the Delphi side for clarity.

Since Delphi does not follow the platform standards for return values I suggest you restrict yourself to types that are compatible with other tools. That means integral values up to 32 bits, pointers and floating point values.

As a general rule of thumb, do not pack records. If you do so you will have mis-alignment which affects performance. For the record in the question, there will be no padding anyway since both fields are the same size.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490