Using Rust with Unity ... and Why ?
Maël Naccache Tüfekçi February 17, 2023 [GameDev] #Rust #UnityRust ? In my Unity ?? Turns out, it's more common easier than you think! In this article, I want to teach you how to write a library in Rust and use it in Unity. But I also want to have a small discussion about if you should do it in the first place. After all, just because you can, should you ?
Anyway, let's get started !
How to create a Rust library and call it from Unity
1. Create a library in Rust
Creating a library in Rust (using cargo) is very easy, just add the --lib
flag to the cargo new
command:
You will also need to add the following lines to your Cargo.toml
:
[]
= ["cdylib"]
In short, this make sure we compile to a dynamic library format (.so, .dylib, .dll). You can read about all the type of crate-type in the rust documentation.
2. Disable name mangling and use the C call convention
By default, Rust use name mangling. You can read why in the RFC.
This behavior is unwanted when making a library since we want our symbol to keep their name so that we can find and call them from outside. Thankfully, it is very simple to disable per-function using the #[no_mangle]
decorator.
Open the default src/lib.rs
and add the decorator just before the default add
function:
A good practice is to only add #[no_mangle]
to the functions that are supposed to be exposed to the outside. This has the added bonus of making it easy to identify which function are "internal" and which communicate with the outside world.
Another thing is to add extern "C"
to the function declaration:
pub extern "C"
This simply indicate that this function use the C calling convention which is the one C# will use.
3. Compile and copy-paste
This is technically the minimum you need to get up and running, so now you can just run cargo build
and you should find a dll/so/dylib in the target/debug
folder of your project (or in target/release
if you built with the --release
flag).
To add the library to your Unity project, you just need to copy-paste the library file somewhere in the Asset folder of your project.
Inside Unity, you might want to change the platform settings of the library to match the OS and CPU arch you compiled your lib for :
4. Create the binding and run
Now we need to tell C#/Unity how to call our function, so we need to do a binding by declaring our function definition and associating it with the library:
using ..;
public
You can put the library file pretty much anywhere in your project tree relative to your binding script. As long as the name in the DllImport
is correct, Unity should be able to find it.
Then you can call it like a normal C# sharp function in your MonoBehaviour, scriptable objects or any other C# classes. For example, we can put it in a MonoBehaviour that will call it on start:
public
Fun challenge: what do you think this code print?
Writing binding tend to be quite redundant, so this is something you can consider code-generation for. Although I know a lot of people who like to handcraft their binding file to have them properly organized and documented.
Anyway, that's it ! You now know how to "call Rust" from Unity. Unity call such library Native Plugins, you can find more info there.
But we might want to go further. After all, if we can just call function from our library, we are pretty limited in what we can do. So the second step we should try is: can we go the other direction and call Unity from our Rust library.
Turns out we can, using callback !
Calling Unity from Rust with callbacks
1. Creating our callback type in Rust
The first step we need to do is create the callback type and a function that will take one and call it in our library. This is simply achieved using what we previously learned:
// Ou callback type. Note that we have to also add 'extern "C"' for it to work
type CBFn = extern "C" fn ;
// Then we can just use this type as our function argument.
// Here we make a simple function that will call the callback with predefined argument.
pub extern "C"
And we can now just cargo build
and copy the updated DLL into our project. Note that you might have to restart Unity to force it to load the new DLL. There are ways to force Unity to reload the DLL, Resolution Games talk about in their article on rust and unity.
2. Creating a delegate in Unity
Now, we need to "copy" our callback type in Unity. This is done by declaring a C# delegate with the same argument and return value. In our case, we get the following:
;
You can put it in the same class that we put our previous declaration and add the binding for the new retcall
function:
using ..;
public
So know, we need to create or callback and call it using the retcall
binding. We can go back to our previous MonoBehaviour and add a function with the same type as our delegate and then call retcall
with it as the argument:
using ;
Note that this is a perfectly standard method, and therefor we have access to the instance member. And we can access Unity objects and change them within the callback. For example, we can change our code to move a transform to the params passed to the callback:
using ;
If you run this code, you should now see you transform move when the callback is called !
Now we are left with one more subject to talk about. Right now, everything is executed within Unity main thread. Depending on the use of the library we are programming, we might want to be able to execute tasks concurrently. So what about multithreading ?
3. Multithreading and Unity
Rust is an ideal language to program multithreaded systems. After all, it was one of its design goal. Adding multithreading is very easy with the standard library, we can add a spawn function to our library that will call a callback after 2 seconds:
pub extern "C"
Again, we can just cargo build
, copy our updated DLL, and declare our binding in Unity:
public static extern void spawn(callback cb);
And again, go to our MonoBehaviour and add a method to be our callback and call the spawn
function:
using ;
And again, if you run your game, you should see the correct log in your console.
But what if you want to modify a Unity object:
using ;
If you run this code, you will get a nasty error in your console:
Well, turns out that Unity is not thread safe, and you cannot modify anything outside its main thread. But it's okay! We can actually work around that. One easy work around is to use a class member to store our value and then update our object in a function that will be executed in Unity main thread, like Update
for example:
using ;
Of course, we might not want to update our position every frame but only when the callback as been called. To do this we could use a boolean that we only set to true in the callback, and gate our update with this boolean.
But anyway, that's pretty much everything ! You now know how to make a unity native plugin with Rust, communicate with it and even make it multithreaded. You can find even more information on unity's documentation on native plug-ins and on the mono documentation about interoperability.
Now we should talk about the elephant in the room: Why?
Now, why would I do that ?
Well, to rewrite it in Rust of course ! In all seriousness, adding a native plugin has a lot of impact:
- You introduced a new language to your stack that you (and your team) need to be fluent in.
- You now have to do some work to be cross-platform. It is easier with Rust to compile a lib for many archs and platforms, but it still adds complexity that you didn't have before.
- You have to maintain the bindings.
- And be careful about multithreading (Although this is a general consideration with Unity)!
Therefor, we should really think before starting to develop a native plugin. But there is some case where it makes a lot of sense. Let's talk about (some of) them.
1. Interacting with the outside world
This is usually the main use of native plugin. Sometimes you need to be able to call a function from the OS, another library or software (like a middleware), etc. If Unity doesn't already integrate built-in bindings, you are going to have to do your own.
Usually, this is something you would do with C (or in some case C++, Objective-C if you need to work with Apple, etc). So why would you do it in Rust ?
Naturally we could just say "Safety!", but to be completely fair, this is not a case where Rust safety will shine the most. When writing bindings to another library, unless it is also written in Rust, you will often have to use a lot of unsafe
. You might have to manipulate raw pointer, manually handling the lifecycle of some object (like calling a library function to free some resources, ...), etc. But I do find that Rust allows you to make "safer" bindings thanks to all the error management tools the language gives you (like Result and such).
But, if you are not doing just a thin-wrapper over some library's functions, you do get a lot of Rust advantages. So I find that using Rust is most useful when doing "high-level" wrapper over a library.
2. Making a library not just for Unity
Another common case is having to create a library which functionality you need to share between different engine/platform. For example, if you are making a multiplayer game, you might want to have your protocol and netcode in a common library that can easily be shared by the client and the server. I find that Rust is an awesome language to implement protocols (network or not) with its supercharged Enums, so this is a very good fit. But this can also apply for any other feature you might want to share across several platforms.
3. Going low-level
Sometimes you need to do some byte wrangling, you want to avoid the GC at all cost or need to have a very precise control over your memory. C# usually allows you to do most of what you would need, but it might just be easier to go native.
As an example, I once had to interact with a piece of hardware using a binary protocol in serial. While it would have been completely possible to do it in C#, doing it in Rust was just plain simpler and gave me more safety around the parsing and generating packets. It also means that it was very easy to integrate it in another solution later on (as mention earlier).
Conclusion and other consideration
I hope that this article taught you everything you needed to start creating and integrating your Rust library in Unity. As usual, I cannot recommend enough that you carefully consider the pros and cons before doing it (unless it's for a fun personal project, in which case, go wild!). I love Rust, but often, simplicity is best, and if you can just do it in C# directly in Unity, you probably should :) .
And to finish, here some random thought you might want to consider:
- All along this article, I use Rust with its standard library. There are many reasons why you might not want to embed the whole libstd in your own lib. If you are in such a case, you might want to read more about Rust's
#![no_std]
. - I only used primitive types, but it is also possible to pass strings and structs. See the Mono documentation on Marshalling for more info.
- You can also deal with multithreading and accessing Unity objects by separating in your own library the code that will run on the main Unity thread and the rest. This way you can pass around values between threads using all the Rust tools (Arc, channel, etc) without having to handle it on Unity's side.
- With
rustup
you can easily install toolchain and target for cross-compilation. So you should be able to cross-compile your lib for Linux and MacOS fairly easily. For iOS and Android,aarch64-apple-ios
andaarch64-linux-android
have tier 2 platform support, but the nature of those platform mean that it might involve a bit more than just dropping the lib file (although Unity take care of a lot of things for you).