.NET host, with (params object[] args)

Hello,

we have a .NET function that needs to be exposed to a Delphi-Plugin.

The interface that needs to be exposed has a method like:

void SomeFunction(params object[] args);

the args array can only contain:

  • int
  • double
  • string
  • bool

Think of this as a logger method.

How do you recommend we expose something like this over a IHYCrossPlatformInterface?

The best I can come up with is to have several overloads:

void SomeFunction(string arg1, string arg2);
void SomeFunction(string arg1, int arg2);

The big drawback, is that we will have many, many overloads, since the number of params is not known, and also the type and order can vary…

Of course, we would just add the combinations that are required, but new combinations will constantly be needed over time. It would not be nice to have to add a new overload, everytime someone needs to to call that function with different parameters.

Thank you!

Hello

The params keyword is just a syntax sugar. The function in question still receives an array. So for the Delphi plugin the same method can be exposed as void SomeFunction(object[] args);

The second part is more complex. To mimic the object[] behavior we need to be able to pass value and its type pairs from Delphi to .NET code.

There are 2 possible solutions. 1st is to define an interface that would expose Type and Value properties like

public interface IValue: IHYCrossPlatformInterface
{
  int Type {get; set;}
  int ValueAsInteger {get; set;}
  double ValueAsDouble {get; set;}
  string ValueAsString {get; set;}
  bool ValueAsBool {get; set;}
}

Then expose a method like

void SomeFunction(IValue[] args)

in the plugin interface. This method should accept IValue[] array, convert them to object[] and call the “real” SomeMethod.

Implement this interface Delphi-side and you will be able to send any arbitrary sets of values.

This would work for a relatively rarely called methods, because marshalling such complex values and accessing them .NET-side will have its performance costs.

2nd approach requires more code but is way faster in terms of Delphi->.NET call.

The idea is to serialize the array of Variant values Delphi-side and to pass the entire array as a binary array from Delphi to .NET. So the plugin would expose method as

void SomeFunction(Byte[] args)

Delphi-side you will need a wrapper method accepting array of Variant

This method should do the following:

  1. Create new TMemoryStream instance
  2. Write array length as integer of the array to the stream
  3. For each array element
    3.1. Write VarType to the stream as integer
    3.2. Write variant value to the stream. Integer and Double values can be written AS IS. For Boolean values write 1 for TRUE and 0 for FALSE. Fro String values convert them to Byte array using a call like TEncoding.Default.GetBytes(aValue) and write down this array length and then the array itself
  4. Once the array is serialized convert stream to byte array and call the plugin method
  5. Put the received byte array into a MemoryStream instance and deserialize it back.

Some snippets to make this task easier:

Reading Int32 from stream:

		Byte[] buffer = new Byte[4];
		Int32 bytesRead = this.Stream.Read(buffer, 0, 4);
		return buffer[0] + (buffer[1] * 0x100) + (buffer[2] * 0x10000) + (buffer[3] * 0x1000000);

Reading Double from stream:

		Byte[] buffer = new Byte[8];
		this.Stream.Read(buffer, 0, 8);
		return System.BitConverter.ToDouble(buffer, 0);

Reading ANSI String from stream:

		Int32 stringLength = ...read integer...
		Byte[] buffer = new Byte[stringLength];
		Int32 bytesRead = this.Stream.Read(buffer, 0, stringLength);
		return Encoding.Default.GetString(buffer, 0, buffer.Length);

Thi approach requires more code, but its performance is way better. Serialization/Deserialization costs are way lover than the costs of a dozen COM interop calls required to read array in the 1st approach.

Regards

Thank you for the very detailed answer :slight_smile:
I had forgotten to mention that our requirement was also performance sensitive, but you already addressed that.
Thx!!!

@antonk,

Here is how we solved it. Might be useful to someone else.

At the end we did not use the Variant array.
Our c# now looks like this:

void SomeFunction(IntPtr buffer, int bufferSize);

From Delphi we send just the address of the MemoryStream buffer and its size:

SomeFunction(Stream.Memory, Stream.Size); // this is in delphi, Stream is TMemoryStream

In c# we construct the MemoryStream as such:

  // copy the delphi bytes to a managed byte array
  var managedByteArray = new byte[bufferSize];
  Marshal.Copy(buffer, managedByteArray, 0, bufferSize);

  // deserialize bytes to object[]
  using (MemoryStream ms = new MemoryStream(managedByteArray))
  {
    args = DeserializeArgs(ms);
  }

In this case we are copying the unmanaged bytes to a managed byte[].
We could be even more performant if we would use unsafe code in c# and just use a byte* instead.
We might end up doing this.

Also, our Delphi wrapper function uses array of const. Same as SysUtils.Format function.

Thanks!

As a final note to this thread.

This approach needs to be used only when the incoming parameter types can vary and the call performance is an issue. In the other cases it is enough to stick with Hydra-generated interfaces.

I’ll log an issue to add an article based on this thread to the Hydra docs.

Thanks, logged as bugs://83446

@antonk
Do you happen to have code how to read an Int64 from the Stream?
It is not quite clear to me what you are doing in the snippet for reading an Int32.

Thanks!

Ah, probably a better notation is needed here.

This

means take 1st byte + (2nd byte shifted left 8 bits) + (3rd byte shifted left 16 bits) + (4th byte shifted left 24 bits)

This code gathers back an 32-bit integer value from 4 bytes

Perhaps this notation would be more clear:

buffer[0] + (buffer[1] << 8) + (buffer[2] << 16) + (buffer[3] << 24);

Similar approach for 64-bit integer would be

buffer[0] + (buffer[1] << 8) + (buffer[2] << 16) + (buffer[3] << 24) + 
   (buffer[4] << 32) + (buffer[5] << 40) + (buffer[6] << 48) + (buffer[7] << 56)