Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to call C# delegate to pass array of strings from native C simplest way?

I know this can be done by mallocing in C, passing malloced pointer to delegate with parameter type IntPtr, marshalling to string[] and then freeing malloced memory with separate, exported C-function from managed code.

My question is: Can this be done simpler way? E.g. :

  • C# delegate parameter is of type string[]?
  • no separate free function to call from managed code

EDIT: I tried with delegate signature:

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
MyManagedDelegate(string[] values, int valueCount)

and fucntion in C:

void NativeCallDelegate(char *pStringValues[], int nValues)
{
    if (gSetStringValuesCB)
        gSetStringValuesCB(pStringValues, nValues);
}

calling it in C:

char *Values[]= {"One", "Two", "Three"};
NativeCallDelegate(Values, 3);

This results in that i can use only 1st string in array.

like image 561
char m Avatar asked Mar 13 '23 22:03

char m


1 Answers

Here's how to do it properly, I'll give a full example so it's reproductible.

The C side

typedef void(*setStringValuesCB_t)(char *pStringValues[], int nValues);

static setStringValuesCB_t gSetStringValuesCB;

void NativeCallDelegate(char *pStringValues[], int nValues)
{
    if (gSetStringValuesCB)
        gSetStringValuesCB(pStringValues, nValues);
}

__declspec(dllexport) void NativeLibCall(setStringValuesCB_t callback)
{
    gSetStringValuesCB = callback;
    char *Values[] = { "One", "Two", "Three" };
    NativeCallDelegate(Values, 3);
}

Nothing fancy here, I just added the necessary glue code and left the rest alone.

The C# side

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void MyManagedDelegate(
    [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr, SizeParamIndex = 1)]
    string[] values,
    int valueCount);

[DllImport("NativeTemp", CallingConvention = CallingConvention.Cdecl)]
public static extern void NativeLibCall(MyManagedDelegate callback);

public static void Main()
{
    NativeLibCall(PrintReceivedData);
}

public static void PrintReceivedData(string[] values, int valueCount)
{
    foreach (var item in values)
        Console.WriteLine(item);
}

The trick lies in the marshaling part:

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void MyManagedDelegate(
    [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr, SizeParamIndex = 1)]
    string[] values,
    int valueCount);

The MarshalAs attribute tells the .NET marshaler the following:

  • UnmanagedType.LPArray You're getting an array...
  • ArraySubType = UnmanagedType.LPStr ...of standard C strings...
  • SizeParamIndex = 1 ...and the size of that array is specified by the second parameter.

The C strings are copied and converted to System.String instances by the .NET marshaler before the invocation of your C# method. So if you need to pass dynamically generated strings to C#, you malloc them, then you call gSetStringValuesCB, and you can free them immediately afterwards, all from the C code, as .NET has its own copy of the data.


You can refer to the docs:

UnmanagedType.LPArray:

A pointer to the first element of a C-style array. When marshaling from managed to unmanaged code, the length of the array is determined by the length of the managed array. When marshaling from unmanaged to managed code, the length of the array is determined from the MarshalAsAttribute.SizeConst and MarshalAsAttribute.SizeParamIndex fields, optionally followed by the unmanaged type of the elements within the array when it is necessary to differentiate among string types.

UnmanagedType.LPStr:

A single byte, null-terminated ANSI character string. You can use this member on the System.String and System.Text.StringBuilder data types.

MarshalAs.SizeParamIndex:

Indicates the zero-based parameter that contains the count of array elements, similar to size_is in COM.

like image 94
Lucas Trzesniewski Avatar answered Apr 06 '23 20:04

Lucas Trzesniewski