Returning a string from a C# callback to C
I had a pretty difficult time finding a good example of returning a string from C# to C in a callback. For our application, we have the core game mechanism written in a C library, but I would like to implement as much non-game functionality in C# (MonoTouch, etc.) since C# is a much richer language.
One feature I want to move from the C library to C# is the logic for determining the path for files to assets. There’s a good deal of logic I’d like to apply on where assets live, and I don’t want that logic to have to be implemented in C. To that end, I defined a callback in the C library. The header looks like:
typedef void (*ContentFilePathProvider)(const char* file, char* buffer, int bufferSize);
extern void SetContentFilePathProvider(ContentFilePathProvider contentFilePathProvider);
void GetContentFilePath(const char* file, char* buffer, int bufferSize);
Here we expect the C# code to import SetContentFilePathProvider and invoke it passing it a (static) method defined in C# to implement calculating the fully qualified path to an arbitrary file. The type of the delegate is dictated by the ContentFilePathProvider typedef. And on the C side, whenever we want the fully qualified path, we just call GetContentFilePath.
Before looking at the C# implementation, let’s finish with the C implementation. First we need to create a global variable to store the callback:
static ContentFilePathProvider _ContentFilePathProvider = NULL;
The function to set the callback (called from C#):
void SetContentFilePathProvider(ContentFilePathProvider contentFilePathProvider)
{
_ContentFilePathProvider = contentFilePathProvider;
}
This function is called anywhere in C we need to get the content file path for an asset:
void GetContentFilePath(const char* file, char* buffer, int bufferSize)
{
_ContentFilePathProvider(file, buffer, bufferSize);
}
For example, elsewhere in our code, we have:
char fullPath[512] = {0};
GetContentFilePath(fileName, fullPath, 512);
return Foo(fullPath);
We initialize the buffer to 512 bytes. This means the callback must ensure it does not stuff more than 512 bytes of path into it. Obviously this number might need to be tweaked. That is life in the unmanaged world.
Now, let’s take a look at the C# impementation of the above contract. You’ll need to import the C function for setting the callback, but first we need to specify the delegate type for the callback:
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void ContentFilePathProvider(string file, IntPtr buffer, int bufferLength);
I’m using a standard unmanaged C lib produced in Xcode, so the calling convention is Cdecl. The second parameter, buffer, is what really tripped me up.
The Google seemed to indicate it should be of type StringBuilder. I tried this but the result on the C side was that the text was gobbledygook. Clearly something wasn’t right. Eventaully I found a terrific answer by Olivier Levrey on codeproject that led me to a working solution that uses IntPtr.
The last parameter indicates to the callee how much space there actually is in the buffer. You generally need the length of buffers in order to operate on them, if for overflow checking if nothing else.
Finally, the callback setter itself:
[DllImport(ImportSource)]
private static unsafe extern void SetContentFilePathProvider(
ContentFilePathProvider contentFilePathProvider);
The code to implement this is a bit cumbersome, but works fine:
[MonoPInvokeCallback(typeof(ContentFilePathProvider))]
internal static void GetContentFilePath(string file, IntPtr buffer, int bufferLength)
{
var s = Config.ContentDir + Path.DirectorySeparatorChar + file;
if (s.Length > bufferLength)
throw new InvalidOperationException("File path is greater than length of buffer (" +
bufferLength + ")");
// Convert from managed to unmanaged string
IntPtr sPtr = Marshal.StringToHGlobalAnsi(s);
// Create a byte array to receive the bytes of the unmanaged string
var sBytes = new byte[s.Length + 1];
// Copy the the bytes in the unmanaged string into the byte array
Marshal.Copy(sPtr, sBytes, 0, s.Length);
// Copy the bytes from the byte array into the buffer passed into this callback
Marshal.Copy(sBytes, 0, buffer, sBytes.Length);
// Free the unmanaged string
Marshal.FreeHGlobal(sPtr);
}
The attribute up top (MonoPInvokeCallback) is extremely important if you intend to deploy to iOS. Without it you will get nasty AOT errors that will make you freak out. Especially since you will have otherwise tested this successfully in the simulator. And there is nothing that ruins my day more as when something works in the simulator but doesn’t work on the device.
And to assign the callback:
SetContentFilePathProvider(GetContentFilePath);
And that’s it! Now whenever our C code wants a file (and it might be located in one of many possible directories) the logic for finding that is happily implemented in C#. Once we figure out what the path is, we stuff it into a character array by way of explicit marshalling.