views:

448

answers:

3

I'm trying to call an external Delphi function from C# which takes a Delphi set as a parameter:

Delphi code

type
  tStatus = (sIn, sOut, sAbsent, sSick);
  tStatusSet = set of tStatus;

function LoadEmployees(tStatusSet aStatusSet): tEmpList;

I need to marshall a C# array of enum values (that is elements from tStatus) into a format that Delphi will read as a tStatusSet type:

C# code

tStatusSet lStatusSet = ConvertToDelphiSet(sIn, sOut);

tEmpList lEmpList = LoadEmployees(aStatusSet);

ConvertToDelphiSet should ideally be a generic solution, able to cope with any enum. We have it defined as:

int ConvertToDelphiSet<T>(params T[] aArgs) {
  int lResult = 0;

  foreach (T lItem in aArgs)
  {
    int lValue = lItem.ToInt32();
    lValue = (int)Math.Pow(2, lValue);

    lResult |= lValue;
  }

But this isn't returning the correct value (for example passing it all four tStatus values results in Delphi only seeing the third value in the set).

Is there documentation on how Delphi internally represents a set? Is it a simple bit-field of all the values? Is there a more robust way of achieving this? Is this future proof or am I relying on undocumented internal features likely to change?

+9  A: 

I believe it is a bit-field, but probably not what you would call "simple".

A Delphi set may have up to 256 elements, or elements with values up to 255, which may not necessarily be the same thing since enum members can be assigned specific values and are not necessarily contiguous, e.g :

  TEnum = (a, b, c=255);
  TSet  = set of TEnum;

Results in a TSet with a possible maximum size of 256 bits even tho it has only 3 possible members. (and note that the member "a" has the value 0, NOT 1!)

You can see this if you use the sizeof() function on your set type or a variable of that type, which will indicate how many bytes of storage that type or variable occupies.

  TEnum = (a, b, c=255);
  TSet  = set of TEnum;

  >>> sizeof(TSet) = 32


  TEnum = (a, b, c);
  TSet  = set of TEnum;

  >>> sizeof(TSet) = 1

Any mechanism you devise that relies on the internal storage of a Delphi set type is going to be fragile and will require that you have enum types defined in both C# and Delphi that match but which cannot easily/reliably be identified as having fallen out of synch.

If that is a practical concern then I'd suggest that you instead pass your values as an array of enum member names and use RTTI in Delphi to rebuild the set on the Delphi side by converting the enum names to the corresponding enum values and adding them to your set as required (if an invalid enum member name is specified -1 is returned as the enum value from GetEnumValue):

  enumValue := TEnum( GetEnumValue(TypeInfo(TEnum), sEnumMemberName) );
  if Ord(enumValue) = -1 then
     raise Exception.Create('Invalid enum value' + sEnumMemberName);

  Include(setVar, enumValue);

GetEnumValue() and the corresponding GetEnumName() functions are part of the TypInfo unit.

In this way you will be able to detect and handle a situation where the C# code has specified an enum value (by name) that the Delphi code does not recognise (or vice versa of course), in a way that is perhaps not so reliable when making assumptions about anonymous bits in an arbitrary n-byte chunk of storage.

You do of course have to still ensure that both your C# and Delphi code are using the same, or easily mapped, names for the enum members.

A compromise might be to pass the values as a simple array of bytes = each member of an enum has an underlying byte-sized ordinal value, so as long as your enum members in C# have the same underlying ordinal value as your enum members in Delphi you can simply cast them as required, e.g, in very rough pseudocode that you should be able to adapt quite easily:

given:

     enumValues: array of Byte;
     setVar: TSet;  // set of TEnum

  for i := 0 to Length(enumValues) do
    Include(setVar, TEnum(enumValues[i]));
Deltics
Thanks, that's definitely helped clear things up for me. I'll probably try implementing your last suggestion.
Mark Pim
+2  A: 

Ideally you should never expose set involving interfaces to other languages as most languages don't have the notion but technically what you want is possible.

As you say sets are simple bitfields Delphi automatically manages for you but it's trickier than that because the size of a set variable depends on the number of set elements. Sets can have up to 256 elements and this makes it possible that a set variable can hold between 1 to 32 bytes in memory.

This in mind, you could do some bit fiddling on a byte value and pass it to a Delphi call as your set can only contain 4 elements.

utku_karatas
+4  A: 

The format of Delphi sets is of course documented, but you are painting yourself into corners when you use data types for DLL function parameters and function results that are not common to different programming languages and development environments. Stick to the types used in the Windows API, and you won't have such problems.

This does also hold true for different Delphi versions. When you use strings or objects you are at the mercy of the compiler implementing them in the same way, and using the same memory manager internally.

For your example, use a DWORD and encode each element of the set as a constant with a distinct power of 2.

mghie