let lots: fun = rust + unity;

Ricardo Gameiro
6 min readAug 17, 2022

--

A quick guide to integrating Rust code into Unity using native plug-ins supporting multiple platforms and targets (iOS, Android, WebGL and macOS).

In a hurry

  1. Get the FunWithRustAndUnity repo.
  2. Return here if hints are needed.

Why

Unity came into play for an existing pet project mainly due to it’s cross-platform support. While C# is powerful and (kind of) fun, it is also for good or bad… traditional. Approaching Rust for the core application engine and leveraging Unity (and C#) for the user interface and scene management presented an interesting alternative that would bring back the fun to a project that had been on hold for too long.

What

The goal is to target iOS (arm64), Android (arm32 and arm64, x86 and x86_64 may come later), WebGL and also macOS (arm64 and x86_64 on a fat library), the later because it is our development platform. The future will probably also bring Windows and Linux, but that will come after the first release on the initial platforms.

How

I won’t go into explaining the basics. I’ll assume that you have some level of familiarity with Rust and Unity, including deployment of Unity projects on multiple platforms, so I’ll keep to the strictly relevant for the Rust+Unity integration. Bear in mind that everything was setup using macOS as development platform, tweaks will be necessary if using other platforms. Also, I’ll just describe the manual process, no build automation process is used.

The FunWithRustAndUnity repo contains both the Rust and Unity projects.

FunWithRustAndUnity
+ RustFun (Rust workspace)
+ fun (library to be used as Unity plug-in)
+ funcli (binary cli to use and interact with the library)
+ UnityFun (Unity 2D URP basic project)

The Rust fun library is just an example library that implements two functions (increment and decrement). These two functions are invoked every second to increment and decrement integer values that are displayed on an Unity Demo Scene using two Text Mesh Pro Labels. funcli is a command line tool that links against the library and allows us to quickly perform some interactive simulation that might not be easily feasible with Rust tests.

Just a quick forward note, inside the Unity project the plug-ins will be stored in a folder named Plugins, which resides in the Assets folder, so we better get a folder structure that goes something like this, in place:

UnityFun 
+ Assets
+ Plugins
+ Android
+ aarch64
+ armv7
+ Editor
+ iOS
+ macOS
+ WebGL

Into Rust

The no_mangle annotation is required, for every function you want to export from Rust to Unity. This gets us going with predictable names when importing into C#. Be aware that when using no_mangle, the Rust mechanism that ensures that we don’t get names clashing between functions in distinct libraries or even from the system, is disabled for these functions (for more information, check this Rust RFC). The extern "C" modifier is necessary to specify the ABI that we want to use (C# happily uses the C ABI).

Excerpt of FunWithRustAndUnity/RustFun/src/lib.rs

To limit the probability of name clashing, I’m prefixing the exported function names with fun_. Choose your alternative or ignore it at your will.

Build targets

In order to be able to build the libraries for multiple targets which will be used as Unity plug-ins, we’ll need to add support for each of those targets.

rustup target add wasm32-unknown-emscripten # WebGL with EMScripten
rustup target add aarch64-linux-android # Android on ARM 64 bits
rustup target add armv7-linux-androideabi # Android on ARM 32 bits
rustup target add aarch64-apple-darwin # macOS on Apple Silicon
rustup target add x86_64-apple-darwin # macOS on Intel
rustup target add aarch64-apple-ios # iOS

Crate type

We’ll also need to ensure that the library crate type is configured with the output library types that we’ll be using for each target.

Currently cargo doesn’t allow the definition of crate-type by target. This is a challenge because distinct targets require distinct library types. An example of this is that Android and macOS use shared libraries while iOS and WebGL use static libraries. To make things a bit worse, while cargo will build for iOS just presenting us with a warning that it doesn’t support shared libraries (if the option is present in Cargo.toml), WebGL is supported, but fails linking with Unity’s emscripten llvm

The Rust nightly toolset does now include a yet unstable flag --crate-type which will do the trick on a per target basis, so check it and build using it, if you are not fond of tampering with Cargo.toml. Check the end of the article for a quick reference on the build commands using the --crate-type flag.

For now, I’ll just show what needs to be in Cargo.toml for each target build.

Building for macOS (single architecture — Unity Editor)

If you’re in the development phase of your application, then building for macOS and specifically for the processor architecture of your development machine is enough. So…

Excerpt of RustFun/fun/Cargo.toml:[lib]
crate-type = ["cdylib", "rlib"]

We also need rlib which is a Rust Library, because we’ll be linking the funcli tool with it.

host_triplet=$(rustc -vV | sed -n 's|host: ||p')
cargo build --target ${host_triplet} --package fun

…will build:

target/${host_triplet}/debug/libfun.dylib

This file should be copied into the Assets/Plugins/Editor folder and will allow you to use the library in the Unity Editor.

Note that the Unity Editor doesn’t reload libraries when they get updated, so you’ll need to restart the Editor whenever you build your Rust library, or alternatively…… well let’s leave that for another post.

Building for macOS (fat library)

For this we need to first build both macOS architectures:

Excerpt of RustFun/fun/Cargo.toml:[lib]
crate-type = ["cdylib"]

macOS goes happily with a shared library.

cargo build --target aarch64-apple-darwin --package fun
cargo build --target x86_64-apple-darwin --package fun

This will get us two libraries, one per architecture, that we now need to join into a single bundle:

lipo -create -output target/fun.bundle \
target/aarch64-apple-darwin/debug/libfun.dylib \
target/x86_64-apple-darwin/debug/libfun.dylib

When using a macOS bundle, you should not prefix it with lib. The fun.bundle file can then be copied into Assets/Plugins/macOS folder.

Building for iOS

iOS is in fact, pretty straightforward.

Excerpt of RustFun/fun/Cargo.toml:[lib]
crate-type = ["staticlib"]

On iOS we use a static library because Rust doesn’t yet support shared libraries on the iOS target.

cargo build --target aarch64-apple-ios --package fun

Cargo will build a static library located at:

target/aarch64-apple-ios/debug/libfun.a

This library should be copied into Assets/Plugins/iOS.

Building for Android

Android is a bit trickier because we do need to use the linker from Android NDK. Fortunately, given that we are using Unity, we can simplify this by using the one bundled with Unity (ensure that you have the corresponding modules installed).

Excerpt of RustFun/fun/Cargo.toml:[lib]
crate-type = ["cdylib"]

Android is happy with a shared library.

# Android ARMV7
export CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="$(ls /Applications/Unity/Hub/Editor/*/PlaybackEngines/AndroidPlayer/NDK/toolchains/llvm/prebuilt/*/bin/armv7a-linux-androideabi*-clang | sort | tail -n 1)"
cargo build --target armv7-linux-androideabi --package fun
# Android ARM64
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$(ls /Applications/Unity/Hub/Editor/*/PlaybackEngines/AndroidPlayer/NDK/toolchains/llvm/prebuilt/*/bin/aarch64-linux-android*-clang | sort | tail -n 1)"
cargo build --target aarch64-linux-android --package fun

Predictably the output files will come at:

target/armv7-linux-androideabi/debug/libfun.so
target/aarch64-linux-android/debug/libfun.so

The first library should be copied into Assets/Plugins/Android/armv7 and the second into Assets/Plugins/Android/aarch64.

Building for WebGL

With WebGL we are back to needing a linker, and again Unity to the rescue (ensure that you have the corresponding modules installed).

Excerpt of RustFun/fun/Cargo.toml:[lib]
crate-type = ["staticlib"]

We are using a static lib, because it looks that it’s the way to go with Unity, even though a shared library might also work.

export CARGO_TARGET_WASM32_UNKNOWN_EMSCRIPTEN_LINKER="$(ls /Applications/Unity/Hub/Editor/*/PlaybackEngines/WebGLSupport/BuildTools/Emscripten/llvm/clang | sort | tail -n 1)"
cargo build --target wasm32-unknown-emscripten --package fun

Cargo will build a static library located at:

target/wasm32-unknown-emscripten/debug/libfun.a

This library should be copied into Assets/Plugins/WebGL.

Unity

After copying all the plug-ins into the respective folders, just right-click the Plugins folder on Unity and Refresh. All the plugins should become visible. Click on the macOS/fun.bundle and on the Unity Inspector, unselect Editor (remember that we have a specific library for the Editor).

One final step before going to C# code is ensuring that we are targeting both armv7 and arm64 on the Android build. For this, goto Edit -> Project Settings then select Player, click on the Android Tab, scroll down to Other Settings and expand it, scroll down to Configuration and change Script Backend to ILCPP. Further down check both the ARMv7 and ARM64 checkboxes.

And finaly the code to invoke Rust from C#.

FunWithRustAndUnity/UnityFun/Assets/Scripts/DemoSceneManager.cs

All the DemoScene connections to the Text Mesh Pro Labels are already setup so, you’re hopefully good to go!

Go ahead and enjoy your Unity builds using Rust code. The full project can be found at FunWithRustAndUnity.

Addenda: Building with Rust nightly (crate-type)

If using Rust nightly the quick and dry commands to build (instead of cargo) would be:

--

--