Calling Rust from C# in Unity

As mentioned before, we use the Rust programming language to implement some of the functionality of the tool in one of our games. The name of the game has now been released -Ultimechs - and we recently published a gameplay reveal trailer.

While we use Rust for the core gameplay, we still use Unity for everything else in the game. Unity has excellent support for rendering the graphics, interfacing with the audio system, reading input from different kinds of controllers, and building for different platforms. We still want to use Unity as a familiar tool for many things, so only the logic for the gameplay and network connectivity is written in Rust. The reasoning behind using Rust for those things were mentioned in the previous blog post, so now is the time to go deeper into the technical details about how we mix Rust and C# code.

This article assumes you already know how to work in a Unity project and how to build Rust code.

Building a shared library

C# can load native code from a shared library file. This file is called a "shared object" on Linux-based systems such as the Meta Quest, and has the file extension .so. On Windows, it is instead called a "dynamic link library", and has the file extension .dll. The file formats are different, but the purpose is the same: to allow a process to load code at runtime.

To build the Rust code into a shared library, add these lines to the Cargo.toml file:

The Rust code can define a function that should be exported from the shared library, using syntax like this:

The #[no_mangle] attribute tells the Rust compiler to not mangle the function name, so it will be exported exactly as written. The function name starts with fireman_ to make sure it doesn't clash with any other function. It took some time before we realized why the game crashed when we closed a network socket. It turned out that a function we named close overwrote the function of the same name in the C standard library, so when a file handle was supposed to be closed, our close function was called instead of the correct one. To avoid something like this happening again, we made sure that all exported functions have the fireman_ prefix. (Fireman is the code name of the game. During development we use code names that don't say anything about the game's content. Not only to keep it secret, but also because it shouldn't stick.)

The extern "C" declaration makes sure the function uses the C calling convention. This specifies how a caller passes the arguments to a function, and how the return value is sent back. There are many ways that this can be done, passing the values in the CPU registers and/or the stack, but the C calling convention is a standard supported by most languages, including C#.

After building with cargo build or cargo build --release, the shared library can be found in the folder target/debug or target/release respectively and should be copied over to Unity's Assets/Plugins folder. (N.b.: During development, there is a better way where we don't have to restart Unity for each change; see later in this post.)

Calling the shared library

On the C# side, we declare the function as follows:

It is important that the number and types of the parameters need to match the Rust code. There is no automatic check for this. If they don't match the result may be a crash or otherwise the wrong behavior. If you think that keeping the two function declarations matching each other sounds like a chore and a clear violation of the DRY principle, you're absolutely right. When we started this project we found no good tools for generating the C# code from the declarations in Rust. Recently a binding generator called Interoptopus was released that we hope to try out for this at some point.

Calling the function is as straightforward as calling any other static method in C#:

Replacing the DLL (or: You don't have to restart Unity!)

If you do this in Windows, you'll probably notice that once Unity has loaded the shared library, there is no way to update it. You may get errors trying to overwrite the file (like "cannot create regular file 'fireman_unity.dll': device or resource busy"), and even if you manage to delete the file and replace it with a new one, Unity will still have the old code in memory and the only way to unload it is to quit Unity. But during development, we of course don't want to restart Unity every time we change the Rust code.

To force Unity to load an updated DLL, we make sure that each time we build it, it gets a new file name. That way the operating system sees it as a completely different library and doesn't use the one already in memory. Unfortunately we have found no way to specify which file to load at runtime. The argument to the DllImport attribute has to be a constant.

Code generation to the rescue! Here is what we do: Every time we build the library, we generate a C# source file that defines a constant:

The numbers after the library name are there to make sure that the file name is unique. It is the current year, week number and clock time, but could be anything that makes sure it's a new name for every new build.

The function declarations can then use this constant instead of being hardcoded for a specific name:

We could now copy the DLL file into Assets/Plugins under the generated file name (for example fireman_unity-22-136-170916.dll), but we don't do that, because that directory would be quickly cluttered, and since Assets is under version control, it would only be a matter of time before someone accidentally committed one of the files.

Instead, at startup, before any code attempts to call functions in the DLL, we copy the DLL file to a temporary directory and explicitly load it from there using a function in the Windows kernel called LoadLibraryA :

After this, whenever we call a DllImport(libName) function, the DLL with the correct name is already loaded into memory. And when we update the Rust code and build it, FiremanNativeVersion updates and specifies a different file name, so another DLL file is loaded next time. Of course, after a while there will be several versions of the DLL in memory, which may cause problems if the Rust code allocates any resources that it doesn't free. We have had some issues with TCP sockets for example.

That should be enough to get you started with using Rust code from C#. Of course this is just an overview of how we got the core game running in Rust or how we use Unity for the user-facing parts of the game. Please let us know if you'd like a follow-up post that dives into this in more detail!

If you’re interested in using Rust and other languages to program VR and AR games, have a look at our our career site! Some current openings below:

Previous
Previous

Life at Resolution: Simon, Games Programmer

Next
Next

Demeo’s First Anniversary: A Look at the Evolving UX/UI of The Game