How to properly marshal strings from Unity to C/C+

2020-03-04 08:18发布

问题:

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.

回答1:

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.