4

The following code snippet is from Unities Bonjour client example, which demonstrates how to interact with native code from C#. It's a simple C function that returns a string back to C# (Unity):

char* MakeStringCopy (const char* string)
{
    if (string == NULL)
        return NULL;

    char* res = (char*)malloc(strlen(string) + 1);
    strcpy(res, string);
    return res;
}

const char* _GetLookupStatus ()
{
    // By default mono string marshaler creates .Net string for returned UTF-8 C string 
    // and calls free for returned value, thus returned strings should be allocated on heap
    return MakeStringCopy([[delegateObject getStatus] UTF8String]);
}

The C# declaration of the function looks like:

[DllImport ("__Internal")]
private static extern string _GetLookupStatus ();

There are a few things that puzzle me here:

  1. Is this the right way to return a string from iOS native code to C#?
  2. How does the returned string ever get freed?
  3. Is there a better way to do it?

Any insights in this matter are appreciated. Thank you.

Programmer
  • 121,791
  • 22
  • 236
  • 328
SePröbläm
  • 5,142
  • 6
  • 31
  • 45

1 Answers1

2

1.No.

2.You have to do that yourself.

3.Yes

If you allocate memory inside a function on the C or C++ side, you must free it. I don't see any code allocating memory on the side but I assume you left that part. Also, do not return a variable declared on the stack to C#. You will end up with undefined behavior including crashes.

Here is a C++ solution for this.

For the C solution:

char* getByteArray() 
{
    //Create your array(Allocate memory)
    char * arrayTest = malloc( 2 * sizeof(char) );

    //Do something to the Array
    arrayTest[0]=3;
    arrayTest[1]=5;

    //Return it
    return arrayTest;
}


int freeMem(char* arrayPtr){
    free(arrayPtr);
    return 0;
}

The only difference is that the C version uses malloc and free function to allocate and de-allocate memory.

C#:

[DllImport("__Internal", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr getByteArray();

[DllImport("__Internal", CallingConvention = CallingConvention.Cdecl)]
public static extern int freeMem(IntPtr ptr);

//Test
void Start() 
{
 //Call and return the pointer
 IntPtr returnedPtr = getIntArray();

 //Create new Variable to Store the result
 byte[] returnedResult = new byte[2];

 //Copy from result pointer to the C# variable
 Marshal.Copy(returnedPtr, returnedResult, 0, 2);

 //Free native memory
 freeMem(returnedPtr);

 //The returned value is saved in the returnedResult variable
 byte val1 = returnedResult[0];
 byte val2 = returnedResult[1];
}

Note that this is only a test that uses char with 2 characters only. You can make the size of the string dynamic by adding a out int outValue parameter to the C# function then adding int* outValue parameter to the C function. You can then write to this parameter on the C side the size of the character is and access that size from the C# side.

This size can then be passed to the last argument of the Marshal.Copy function and remove the current hard-coded 2 value limit. I will leave this for you to do but if confused, see this post for example of that.


The better solution is to pass StringBuilder to the native side then write to it. The bad side is that you have to declare the size of the StringBuilder on time.

C++:

void __cdecl  _GetLookupStatus (char* data, size_t size) 
{ 
    strcpy_s(data, size, "Test"); 
}

C#:

[DllImport("__Internal", CallingConvention = CallingConvention.Cdecl)]
public static extern int _GetLookupStatus(StringBuilder data, int size);

//Test
void Start() 
{
    StringBuilder buffer = new StringBuilder(500);
    _GetLookupStatus (buffer, buffer.Capacity);
    string result = buffer.ToString();
}

If you are looking for the fastest way then you should use char array on the C# side, pin it on C# side then send it to C as IntPtr. On the C side, you can use strcpy_s to modify the char array. That way, no memory is allocated on the C side. You are just re-using the memory of the char array from C#. You can see the float[] example at the end of the answer here.

Programmer
  • 121,791
  • 22
  • 236
  • 328
  • Thank you very much for the detailed clarification! I've added MakeStringCopy() to the code snippet. – SePröbläm Jun 09 '17 at 20:24
  • One more question about not returning variables that are passed on the stack: Does it also apply to primitive data types? I did some tests and strings instantly fail, but int seems to work. – SePröbläm Jun 09 '17 at 22:59
  • if it is int,float, bool, that's fine. If you are returning a reference of it with `&` or a pointer of it with `*` then its not fine. If it is an array such as `int []` or `int *` then it's not fine too since you are returning a memory address of it. It is fine if it is a variable that created with `malloc` in C or `new` keyword in C++ because they will be there even when you are outside the function. I think you should learn about returning references or pointers in a function. You will fully understand these. – Programmer Jun 09 '17 at 23:13
  • 1
    Ok, I got it! Thanks again to the clarification. C# marshaling is a complex beast if you just started with it. Your help is greatly appreciated! Thank you. – SePröbläm Jun 10 '17 at 00:26
  • Actually the answer on the 1st question is YES, it is a valid way to return a string from native function. According to Unity's doc: "String values returned from a native method should be UTF–8 encoded and allocated on the heap. Mono marshalling calls free for strings like this." – dmitry1100 Dec 01 '20 at 19:03