Skip to main content
Test Double company logo
Services
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Management
Launch modern product orgs
Legacy Modernization
Renovate legacy software systems
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech & product teams
Technical Assessments
Uncover root causes & improvements
Case Studies
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say Hello
Test Double logo
Menu
Services
BackGrid of dots icon
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Management
Launch modern product orgs
Legacy Modernization
Renovate legacy software systems
Cycle icon
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech & product teams
Technical Assessments
Uncover root causes & improvements
Case Studies
Solutions
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say hello
Developers
Developers
Developers
Accelerate quality software

Getting started with FFI: Rust & Unity

Explore how Rust can take your Unity game to the next level with enhanced performance and robust features. Dive in to find out more.
Michael Schoonmaker
|
January 1, 2018
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

In my slack time, I’ve started writing games in Unity again.

As I’ve been going, though, I found myself wanting some of the tools and features from Rust. I’ve grown accustomed to Rust for performance-sensitive applications; it has several tools and features that make writing those applications easier for me. More and more I found myself thinking, “Wouldn’t it be better to be able to write this feature in Rust?”

Time to break out some FFI magic.

  • slack-time
  • unity
  • rust

What is FFI?

The term “Foreign-Function Interface” refers to any mechanism that allows code written in one language to directly invoke code written in another language.

If “microservices” allow you to write individual networked services in whatever language you want as long as they understand a shared network protocol, FFI implementations allow you to write libraries in whatever language you want as long as the programs that use those libraries understand the foreign-function interface provided.

If you’ve ever used a native module in Ruby or Node, you’ve benefitted from an FFI provided by C or C++. [Native modules typically control the virtual machine directly (e.g. using V8’s C++ API), though both Node and Ruby have FFI modules available for binding to dynamic libraries.]

In Rust’s case, the name also refers to the ffi module of Rust’s standard library, which provides tools to make writing those interfaces from Rust simpler.

  • node-ffi
  • ruby-ffi
  • rust-ffi

Why might I do this?

Unity is a fantastic game engine, loaded with an incredibly rich set of features for a game engine that is freely available. Where Unity lacks, Rust shines:

  • Test-driven development: If the simulation logic is written in Rust, the same safety and testability that Rust provides can be applied to your simulation. While Unity continues to tinker with an in-editor Test Runner, it’s fragile, with a slow feedback loop.
  • Memory management: .NET relies on garbage collection, which can cause performance issues. Rust, on the other hand, requires manual memory management, which allows you to more finely control its use over time.
  • Complex network protocols: Rust’s design for enums, match, and the std::io::Cursor type all make writing low-level network protocols simpler and more reliable. In tandem with the other two points, you can write predictable, tested protocols with well-understood, verifiable performance and memory implications.

Risks

I’ve come across two major risks to this approach: thread panics and memory ownership.

The first risk is specific to Unity, and not an issue with FFI in general: if the Rust code panics (throws an unhandled error), Unity crashes. Hard. There may be a way to defend against this from Unity, but I haven’t found it. Instead, avoid panics at all costs. match, Result, etc. are your friends.

The second risk is common in FFI applications: Rust’s semantics around allocating and deallocating memory are specific to Rust, so the only way to correctly free memory allocated in Rust is to allow Rust to deallocate that memory. You need to keep track of any and all memory you allocate in Rust, and ensure that Rust knows when it’s safe to deallocate that memory. The simplest way to do that is with what I call a “baton”.

A baton is a single, opaque pointer provided to the host (e.g. Unity) when your library’s internal state is initialized. All internal allocations are attached to the data structure behind the baton. Every operation requested by the host comes with the baton, to which you continue to affix data as required. Once the host is finished, they pass the baton back one last time, and you deallocate all remaining memory in use.

Partial example

I’m assuming you already have a Rust library and a Unity project. In your Rust project’s Cargo.toml file, tell Cargo you want to build a dynamic library instead of a static one:

[lib]
name = "ffi_example"
crate-type = ["dylib"]

Once you’ve done that, future builds of the library will place a .dylib file in the target directory (e.g. target/debug/ffi_example.dylib). This DLL needs to be moved into your Unity project’s Asset/Plugins directory as a .bundle to be discoverable and loadable via Unity:

cp rust/target/debug/ffi_example.dylib unity/Assets/Plugins/ffi_example.bundle

I highly recommend scripting this: every time the Rust library is built, it needs to be copied into Unity. [In the full example that can be seen in build.sh.]

Detangling Unmangling names

The next major step is to provide a publicly consumable API from Rust. This is as simple as marking a function extern "C" within the top-level module:

# In src/lib.rs:
pub extern "C" fn get_answer() -> u32 {
  42
}

[Note: The function is marked pub, too. Try dropping that and see what happens.]

We can double-check that the symbol (the name of the function) is actually exported with a tool like objdump:

objdump -t rust/target/debug/libffi_example.dylib | grep get_answer
0000000000001260 g     F __TEXT,__text  __ZN11ffi_example10get_answer17h684b44aa9cba5ce3E

Unfortunately for us the Rust compiler has “mangled” the name we provided. This process is really good at ensuring we don’t collide with some other get_answer function, but we need to disable mangling for this function to call it from Unity:

# In src/lib.rs:
#[no_mangle]
pub extern "C" fn get_answer() -> u32 {
  42
}

Double-checking with objdump, we can see that the #[no_mangle] attribute did its job (the underscore at the beginning is normal):

objdump -t rust/target/debug/libffi_example.dylib | grep get_answer
0000000000001260 g     F __TEXT,__text  _get_answer

Calling from Unity

Now that we can find our get_answer function, we should call it from C#. While we could bind to this function from a MonoBehaviour, it’s better to create a separate class:

# In Assets/Plugins/FFI.cs
using System.Runtime.InteropServices;

public class FFI
{
    [DllImport("libffi_example")]
    public static extern int get_answer();
}

Then, from any MonoBehaviour, we can call our newly-loaded Rust function:

Debug.Log(String.format("The answer is: {0}", FFI.get_answer()));

Full example: Memory management, error handling, etc.

A full example of this is available on GitHub. This example includes solutions to the risks mentioned above, as well as doing something a little more interesting: sending UDP datagrams to a waiting Node server.

In particular, check out the example for:

  • Memory management using a baton.
  • A well-factored Baton type in Rust.
  • An FFI class in C# that provides a more idiomatic interface.
  • Sending basic data from C# to Rust.

Going further

Now that you have the basics of FFI down, the next steps are to pass more complicated data structures around. For example, let’s take the “network protocol” use case, above. If you’re trying to represent domain objects on the wire, you need to get the full domain object into Unity. How do we do that?

While the complete answer is a blog post in and of itself, here are a few hints:

  • Keep to single-layer structs.
  • Use [StructLayout(LayoutKind.Sequential)] from C#.
  • Use #[repr(C)] from Rust.

Thanks to Jim Fleming for inspiration and some helpful information while I started exploring this concept.

Related Insights

🔗
Guide to zero-copy FFI with Rust and Unity

Explore our insights

See all insights
Developers
Developers
Developers
You’re holding it wrong! The double loop model for agentic coding

Joé Dupuis has noticed an influx of videos and blog posts about the "correct" way of working with AI agents. Joé thinks most of it is bad advice, and has a better approach he wants to show you.

by
Joé Dupuis
Leadership
Leadership
Leadership
Don't play it safe: Improve your continuous discovery process to reduce risk

We often front-load discovery to feel confident before building—but that’s not real agility. This post explores how continuous learning reduces risk better than perfect plans ever could.

by
Doc Norton
Leadership
Leadership
Leadership
How an early-stage startup engineering team improved the bottom line fast

A fast-growing startup was burning cash faster than it could scale. Here’s how smart engineering decisions helped them improve the bottom line.

by
Jonathon Baugh
Letter art spelling out NEAT

Join the conversation

Technology is a means to an end: answers to very human questions. That’s why we created a community for developers and product managers.

Explore the community
Test Double Executive Leadership Team

Learn about our team

Like what we have to say about building great software and great teams?

Get to know us
Test Double company logo
Improving the way the world builds software.
What we do
Services OverviewSoftware DeliveryProduct ManagementLegacy ModernizationDevOpsUpgrade RailsTechnical RecruitmentTechnical Assessments
Who WE ARE
About UsCulture & CareersGreat CausesEDIOur TeamContact UsNews & AwardsN.E.A.T.
Resources
Case StudiesAll InsightsLeadership InsightsDeveloper InsightsProduct InsightsPairing & Office Hours
NEWSLETTER
Sign up hear about our latest innovations.
Your email has been added!
Oops! Something went wrong while submitting the form.
Standard Ruby badge
614.349.4279hello@testdouble.com
Privacy Policy
© 2020 Test Double. All Rights Reserved.