Rusteal
Rust bindings for Unreal Engine 5.8
Rusteal lets you write Unreal Engine gameplay in Rust. Your Rust code compiles to a shared library that is loaded by a small UE C++ plugin. All UE API calls cross the FFI boundary through a function pointer table — no C++ compilation required during Rust iteration.
⚠️ Early Stage Project — Rusteal is under active development and not ready for production use. APIs will change without notice, documentation is incomplete, and many UE features are not yet covered. Contributions and feedback are welcome, but please do not use this for shipping projects.
Acknowledgments
Rusteal is a hard fork of uika by VioletHelianthus, who designed and wrote the foundation this project stands on: the reflection-driven code generator, the UHT exporter plugin, the reification of Rust structs as UClasses, and the runtime. Thank you.
Rusteal does not intend to stay compatible with the original uika, unless its author wants to port the improvements back. From here on it develops the Unreal bindings independently.
Example
examples/gem_collector
in this repository is a full working demo of the runtime API: copy the
directory into a project's Rust/ and add it to the workspace members.
Getting Started
Prerequisites
- Unreal Engine 5.8 (source or installed build)
- Rust (stable, latest recommended)
- Linux: the clang toolchain and .NET runtime bundled with the engine
- Windows: Visual Studio 2022 with the C++ workload
Install
That installs the rusteal binary; the engine plugins and the project
templates travel inside it. The first command that needs the engine asks where
it is and keeps the answer in ~/.config/rusteal/config.toml — the only
per-machine setting Rusteal has.
A new project
rusteal new creates a UE project from the engine's Blank C++ template,
installs the Rusteal plugins, writes the Rust workspace and runs the whole
build pipeline. Then open MyGame.uproject, drop a HelloActor into the level
and press Play: the Output Log shows [MyGame] Hello from Rust.
It does not run git init — versioning is your call — but it does write a
.gitignore covering the Unreal and Rust build output, so a later git init
picks up the right files.
Two flags: --no-build stops after writing the project, leaving the pipeline
for a later rusteal build; --runtime-path makes the project depend on a
Rusteal checkout instead of the published crates, which is for working on
Rusteal itself — see Contributing.
An existing project
setup does not write the Rust workspace; create it as below, or copy the
layout rusteal new produces.
Versions
A project is tied to one Rusteal version: Rust/Cargo.toml pins
rusteal-runtime, rusteal-core and rusteal-ffi to it ("=x.y.z"), and the
plugins in Plugins/ carry it. Before doing anything, build, generate and
setup check it against the CLI's own version:
- the CLI is newer:
rusteal upgrademoves the project to it — the pins, the plugins (Plugins/RustealandPlugins/RustealGeneratorare replaced wholesale) and a full build; - the project is newer: install the CLI it uses,
cargo install rusteal@x.y.z.
The plugin checks the library as well: a librusteal built against another
version is refused at load, with both versions in the Output Log.
Project layout
Everything lives in one repository; the only thing outside it is the engine path in the CLI's own configuration.
YourProject/
├── YourProject.uproject
├── rusteal.toml # project configuration (versioned)
├── Source/YourProject/ # the C++ module UBT needs
├── Plugins/Rusteal/ # runtime plugin + generated C++ wrappers
├── Plugins/RustealGenerator/ # UHT exporter
├── Intermediate/Rusteal/uht/ # reflection JSON (build output)
└── Rust/
├── Cargo.toml # workspace
├── your-game/ # your code (cdylib)
└── bindings/ # generated by `rusteal build`, committed
Rust/Cargo.toml — rusteal new pins the version of the CLI that created
the project:
[]
= ["your-game", "bindings"]
= "3"
[]
= "=x.y.z"
= "=x.y.z"
= "=x.y.z"
= "0.33"
Rust/your-game/Cargo.toml:
[]
= ["cdylib"]
[]
= { = true }
= { = "../bindings" }
= { = true }
Rust/your-game/src/lib.rs:
entry!;
The runtime types come from rusteal_runtime::prelude, the engine types from
the generated crate's own bindings::prelude.
Build Pipeline
The CLI orchestrates a 5-step build:
| Step | Command | What it does |
|---|---|---|
| 1 | UE Build | Compiles UE project, triggers RustealGenerator → JSON reflection data |
| 2 | Codegen | Reads JSON → generates the bindings crate + C++ wrappers |
| 3 | UE Rebuild | Compiles the generated C++ wrappers into the UE module |
| 4 | Cargo Build | cargo build --release on your cdylib crate |
| 5 | Deploy | Copies the library to Plugins/Rusteal/Binaries/<Platform>/ (rusteal.dll, librusteal.so) |
Commands take the project directory, or find it by walking up from the current one:
# Full build
# Rust-only rebuild (skip the UE steps)
# Codegen and everything after
# Just regenerate the bindings crate and the C++ wrappers
Key Concepts
Object References
// Lightweight handle (8 bytes, Copy). May become invalid if UE GCs the object.
let actor: = world.spawn_actor?;
// RAII GC root. Prevents garbage collection until dropped.
let pinned: = actor.pin?;
// Checked access — verifies the object is still alive before use.
let checked = actor.checked?;
checked.k2_get_actor_location;
Defining UE Classes
Dynamic Calls
For Blueprint-defined functions or APIs not covered by generated bindings:
let mut call = new?;
call.set?;
call.call?;
Hot Reload
During development, rebuild your Rust library and reload without restarting the editor:
Then in the UE console:
Rusteal.Reload
Function implementations update immediately. Adding/removing uproperty or ufunction requires an editor restart.
Platform Support
| Platform | Status |
|---|---|
| Linux (x64) | Supported (Unreal Engine 5.8.2) |
| Windows (x64) | Supported |
| macOS | Not yet tested |
Contributing
Working on Rusteal itself, rather than on a game:
# The workspace builds, tests and lints without the engine
# A throwaway project that depends on this checkout
# After changing the runtime, the macros or a game crate
In the editor, Rusteal.Reload swaps the library in without restarting; adding
or removing a uproperty/ufunction still needs a restart.
A project made with --runtime-path follows that checkout: the CLI acts on it
only when built from the same checkout, so drive it with cargo run -p rusteal --
from there. After pulling, cargo run -p rusteal -- setup /tmp/Probe reinstalls
the plugins at the checkout's version.
--runtime-path
It decides where the generated project takes Rusteal from:
| without it | --runtime-path <checkout> |
|
|---|---|---|
Rust/Cargo.toml says |
rusteal-runtime = "=x.y.z" |
{ path = "<checkout>/rusteal-runtime" } |
| Crates come from | crates.io | the local checkout |
| A change in the runtime reaches the game | on the next published version | on the next build |
| The project builds on another machine | yes | no (the path is this machine's) |
So: without it for a real game, with it while working on Rusteal — a change in
rusteal-core shows up in the probe project on the next build --from 4,
with nothing to publish in between.
What lives where
| Crate | Role |
|---|---|
rusteal-ue-flags |
UE reflection flag constants |
rusteal-ffi |
#[repr(C)] types and the API table: the Rust ↔ C++ contract |
rusteal-core |
the safe runtime: object references, lifecycle, containers, math |
rusteal-macros |
#[uclass], #[uclass_impl] and their attributes |
rusteal-runtime |
what a game depends on; re-exports the above |
rusteal-codegen |
reflection JSON → the bindings crate and the C++ wrappers; owns manual/ |
rusteal-cli |
the rusteal binary; owns templates/ and embeds ue_plugin/ |
Changing the C++ plugin means running cargo run -p rusteal -- sync-plugin
before publishing, which refreshes the snapshot the binary embeds.
Engine APIs Rusteal uses that Unreal has deprecated are tracked in
docs/ue-deprecations.md: what, since which UE
version, until when and where. A new engine version means checking its
warnings against that list.
Commits are small — one per fix — and never mention AI authorship.
Releases and versions
Everything Rusteal ships carries one version: the seven crates (they inherit
[workspace.package].version through version.workspace = true), the two UE
plugins and the FFI contract between them.
Where it comes from. The release is cut from a develop → main PR. The
prepare-release workflow takes the last released version from crates.io
(max_version of the rusteal crate, the one source that cannot drift) and
bumps it by the commits the PR adds: BREAKING CHANGE: → major, feat: →
minor, anything else → patch. It never reads the version from the repository's
files. If nothing that goes into a package changed since the last tag, it skips
the release.
Where it is written. The same bump commit writes it everywhere at once:
| File | Field | 0.2.1 becomes |
|---|---|---|
Cargo.toml |
[workspace.package].version and the internal { version, path } dependencies |
0.2.1 |
ue_plugin/*/*.uplugin |
VersionName |
"0.2.1" |
ue_plugin/*/*.uplugin |
Version |
2001 |
Tags (v0.2.1), crates.io and the manifests only ever use major.minor.patch.
The integer form. Two places cannot hold 0.2.1: the .uplugin Version
field, which UE requires to be an integer, and RustealApiTable::version, a
u32. Both use the same encoding, major * 1_000_000 + minor * 1_000 + patch
(rusteal_ffi::encode_version). It is always derived from the version, never
a source of it. Minor and patch must stay below 1000, or the number would be
ambiguous; encode_version refuses such a version at compile time, so it cannot
slip through.
How it is kept in line.
rusteal-cli/tests/plugin_version.rsfails if either.uplugindisagrees with the workspace version, so a descriptor edited by hand turns CI red.- At load, the plugin compares its own version with the library's
rusteal_version()export and refuses a library built against another one;rusteal_runtime::initchecks the table'sversionagain. - Before
build,generateandsetup, the CLI compares the project's pins and plugins with its own version (see Versions).
Cargo.lock is not versioned: every build would rewrite the internal versions
in it. A dependency release that breaks the workspace shows up in CI, where it
gets fixed. cargo install rusteal --locked still works, because cargo packs a
lock file of its own into the published crate.
License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.