DEV Community

Cover image for Call into Rust from C# and Unity
Emanuele Manzione
Emanuele Manzione

Posted on • Originally published at mhlab.tech

Call into Rust from C# and Unity

Rust is a safe and fast low-level language and recently I got enthusiastic about it. So I would like to add it in my game-dev pipeline. Today I will try to call from Unity3D and C# some functions written in Rust.

Getting started

I assume that you already know a bit how to work with Rust and Cargo, so I will not speak about how to install the compiler and its environment.

I create a new lib project with cargo init --lib and specify in Cargo.toml that I want a dynamic library as output:

[lib]
name = "unity_rust"
crate-type = ["dylib"]
Enter fullscreen mode Exit fullscreen mode

To know more about this topic, click here.

I also create a new Unity project for the sake of testing.

Writing some Rust

Now it's time to write some Rust. I want start simple: just a function to generate a random int between 0 and 100. I am using the rand crate, so be sure to add it as dependency.

#[no_mangle]
pub extern fn get_random_int() -> i32 {
    let mut rng = rand::thread_rng();
    rng.gen_range(0, 100)
}
Enter fullscreen mode Exit fullscreen mode

There are two interesting aspects to point out. The first one is the #[no_mangle] attribute: it informs the compiler not to mangle the symbol name for my function, in this way I can easily refer to it by name later.
The second one is the extern keyword: it specifies that the function will be exported with the C function call convention.

Now I run cargo build or cargo build --release and the compiled library will be generated in my target folder.

On Unity side

I create a Plugins folder in the Unity project and put there the compiled native DLL. So I create the C# interop code:

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

public static class RustRandom
{
    [DllImport("unity_rust")]
    private static extern int get_random_int();

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static int GetRandomInt()
    {
        return get_random_int();
    }
}
Enter fullscreen mode Exit fullscreen mode

Something to point out here too. I used DllImport to inform the compiler about the name of the assembly that contains the following symbol. I used extern to inform the compiler that it is an external function. I wrapped the imported function in a public GetRandomInt that matches the C# naming convention. I used MethodImpl to suggest an aggressive inlining, since the function is just a wrapper.

Time to test

Now it is time to test what I coded. I create a simple MonoBehaviour to display the random generated int:

using UnityEngine;

public class RustRandomTest : MonoBehaviour
{
    private void Start()
    {
        Debug.Log("Random number: " + RustRandom.GetRandomInt());
    }
}
Enter fullscreen mode Exit fullscreen mode

Attach this component to a game object and hit the "Play" button. The console should log a random number like: Random number: 73.

Advanced usage

Well, what I described until now is really basic and it has probably not so many usages. Let's try to dive deeper!

Instantiating a ThreadRng everytime I want a random number is not efficient. The proper way would be to instantiate it once and retrieve it on demand. Also, if I instantiate something I also have to free it when I don't need it anymore.

pub struct RandomGeneratorParameters {
    min: i32,
    max: i32
}

pub struct RandomGenerator {
    rng: rand::prelude::ThreadRng,
    parameters: RandomGeneratorParameters
}

#[no_mangle]
pub extern fn create_random_generator(params: RandomGeneratorParameters) -> *mut RandomGenerator {
    unsafe { transmute(Box::new(RandomGenerator {
        rng: rand::thread_rng(),
        parameters: params
    })) }
}

#[no_mangle]
pub extern fn get_random_int(rng_ptr: *mut RandomGenerator) -> i32 {
    let rng = unsafe { &mut *rng_ptr };
    rng.rng.gen_range(rng.parameters.min, rng.parameters.max)
}

#[no_mangle]
pub extern fn destroy_random_generator(rng_ptr: *mut RandomGenerator) {
    let rng : Box<RandomGenerator> = unsafe { transmute(rng_ptr) };
}
Enter fullscreen mode Exit fullscreen mode

The interesting part here is the use of std::mem::transmute (you can find additional info about it here), that basically works like memcpy in C.

create_random_generator creates a RandomGenerator instance, box it on the heap, then transmutes it to a *mut.

get_random_int now accepts a mutable pointer to a RandomGenerator instance.

destroy_random_generator accepts the mutable pointer to a RandomGenerator instance, transmutes it back to the boxed RandomGenerator instance and lets Rust deallocate it when it goes out of scope.

Now it is C#'s turn. Its interop code should look like this:

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential)]
public struct RustRandomParameters
{
    public int Min;
    public int Max;
}

public static class RustRandom
{
    private static IntPtr RngPtr;

    [DllImport("unity_rust")]
    private static extern IntPtr create_random_generator(RustRandomParameters parameters);

    [DllImport("unity_rust")]
    private static extern int get_random_int(IntPtr rngPtr);

    [DllImport("unity_rust")]
    private static extern void destroy_random_generator(IntPtr rngPtr);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void Initialize()
    {
        RngPtr = create_random_generator(new RustRandomParameters()
        {
            Min = 0,
            Max = 100
        });
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static int GetRandomInt()
    {
        return get_random_int(RngPtr);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void Dispose()
    {
        destroy_random_generator(RngPtr);
    }
}

Enter fullscreen mode Exit fullscreen mode

Nothing relevant, same logic as before. The only thing worth point out is the use of IntPtr to represent a native pointer and the StructLayout(LayoutKind.Sequential) attribute to make the sequential memory layout of that struct explicit.

And now, again, the test:

public class RustRandomTest : MonoBehaviour
{
    private void Awake()
    {
        RustRandom.Initialize();
    }

    private void Start()
    {
        Debug.Log("Random number: " + RustRandom.GetRandomInt());
    }

    private void OnDestroy()
    {
        RustRandom.Dispose();
    }
}
Enter fullscreen mode Exit fullscreen mode

It does work! Also, remember that calling into Rust functions has the same cost of calling into C functions from C#.

And that's it. I don't know if it will be useful for anything I will do in the future, but it should be enough to fully interoperate from Unity/C# into Rust.

Source code

Here you can find the source code: GitHub

External resources

Top comments (0)