Returning structs from C#

You are not limited to primitives when passing values through the FFI barrier. C# can return C-compatible structs that are readable from Rust. However, the struct must only contain primitive types and IntPtr

[StructLayout(LayoutKind.Sequential)]

public struct MyStruct

{

...

}

On Rust, this proceeds as if you were calling a C FFI function.

#[repr(C)]

pub struct MyStruct {

pub ...,

}

Strings

Strings in C# are not C compatible and must be manually marshalled into an IntPtr, like so.

[NativeCallable(EntryPoint = "hello_world", CallingConvention = CallingConvention.Cdecl)]

public IntPtr HelloWorld()

{

return Marshal.StringToCoTaskMemUTF8("Hello World");

}

This allocates unmanaged memory for a null-terminated UTF8 C compatible char* string, and returns a pointer to that memory. On Rust, you can treat this as a *const c_char , and deal with it as you would a normal C string. However, you can not free this string with libc::free . Instead, expose the CLR free method like so:

[NativeCallable(EntryPoint = "free_corert", CallingConvention = CallingConvention.Cdecl)]

public static void Free(IntPtr ptr) {

Marshal.FreeCoTaskMem(ptr);

}

And call free_corert(ptr: *mut c_void) in Rust to free memory once you’re finished with the string.

Reflection, or more appropriately RTTI

C# has a powerful reflection API that is often used by many libraries. Since the .NET compiler will now emit native code, you need to manually specify which namespaces to include runtime type information (RTTI) for. Unlike C++, this isn’t done automatically, and if you fail to specify the correct namespaces, reflection will fail spectacularly. Fortunately, it’s not that difficult.

First, under the root folder of your C# project, create rd.xml with the following contents.

<Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata">

<Application>

<Assembly Name="[name of your assembly]" Dynamic="Required All"/>

</Application>

</Directives>

If you use any C# libraries that use reflection, such as Newtonsoft.Json , add an entry for those libraries as well, like so

<Assembly Name="Newtonsoft.Json" Dynamic="Required All"/>

Finally, include rd.xml as a resource in your csproj

<ItemGroup>

<RdXmlFile Include="rd.xml" /> <!--Include stack trace data -->

<IlcArg Include="--stacktracedata" />

</ItemGroup>

Putting it all together

Without a proper build script, compiling C# and Rust separately is extremely unwieldy. The build script will need to discover the .NET Core and ILCompiler SDK versions, call the .NET compiler, and direct cargo to link to those libraries. See how seiri wraps everything up with this build script. I’ve had a redditor approach me in adapting this build script to something more generic, so if that pans out, that would make things much easier to get started.

Conclusion

It works? I was surprised and shocked as you may or may not be that it was possible for such an abomination to exist. Surprisingly, the biggest drawback was a bloated executable; having to link not only the entire .NET Runtime Library as well as the CLR garbage collector resulted in an executable size of about 16 megabytes. Not only did it just work, it was reasonably performant, on par or even better than normal, JITted C#. It was really the best of both worlds.

However, when I last messed with this, anything more complex than returning primitives would completely blow up during the linking step on Linux, no matter what I tried. The redditor that contacted me about generalizing the build script managed to get everything working on Linux though, so YMMV. Theoretically there is nothing stopping Linux compatibility, but I suspect that there may be a conflict with the version of clang Rust uses, and the version of clang that CoreRT uses, but that may have been fixed by now.

As for seiri, I’ve only really written all this up for posterity. The main barrier that prevented me from using the C++ version of TagLib was the horrible, horrible, perhaps even more Lovecraftian than this unholy matrimony between garbage collectors and borrow checkers, build system that is CMake. It must mean something that trying to get TagLib’s CMake build files to cooperate with me drove me to instead give up, and use an experimental, alpha technology instead.

All good things must come to an end, and as much as it saddens me, after dipping my toes in some C++ hellfire and coming back to this project in a few months, I’ve finally figured out how to get CMake and the C++ version of TagLib to work with cargo’s build system and am working on replacing taglib-sharp with TagLib proper, for proper Linux support if nothing else at all. This blog post serves as a reminder and as an instructional for anyone willing to go as far as I did to avoid CMake and C++ build systems that they’d be willing to create their own Frankenstein’s Monster.

Final Remarks

I’ve noticed that this blog post is now linked as an example on the CoreRT docs as a reference. I’ve since switched seiri over to a pure C++ approach, but for anyone looking for a real life example, this would be the tree where I still used C# in seiri. This includes a build script (that works only on Windows) that will link required libraries automatically, but may be outdated as of time of writing.