Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The runtime

rusteal-core is the safe runtime: every unsafe call through the API table is confined to it, so the generated bindings and game code only see safe types. rusteal-runtime is the one crate a game depends on. This chapter is a map of both, and of the editor module that completes them on the C++ side.

rusteal-core, file by file

FileRole
lib.rsModules and re-exports; lock_or_recover and friends, which recover a poisoned lock instead of panicking.
api.rsThe table pointer (static API: OnceLock): init_api (once), api() (panics when not initialized), is_api_initialized().
ffi_dispatchGenerated by build.rs, one function per table entry (see The Rust–C++ boundary).
object_ref.rsUObjectRef<T>, Checked<T>, casts, upcasts.
weak_ptr.rs, soft_ref.rs, subclass_of.rs, pinned.rsWeak and soft references, class references, GC roots (see Reification).
traits.rsUeClass, UeStruct, UeEnum, HasParent, Inherits, UeHandle, ValidHandle.
reify_registry.rsRegistering the library’s types, instance data, function dispatch (see Reification).
delegate_registry.rsClosures bound to delegates and console commands, by id; DelegateBinding.
dynamic_call.rsCalling a function, or broadcasting a delegate, by name through reflection.
containers.rsUeArray, UeMap, UeSet views; OwnedStruct<T>.
struct_ref.rsUStructRef<T> and OutRef<T> into parameter buffers.
fname.rsFName.
ue_math.rsRotator, Transform, LinearColor, Color, Plane, Ray, Sphere, boxes and bounds, on glam.
world.rsSpawning, finding, loading and creating objects, actors of a class, data table rows.
console.rs, input.rs, widget.rs, logging.rsConsole commands and variables, Enhanced Input binding, widgets, ulog!.
task.rsBackground work, the game-thread queue, tick hooks.
error.rs, ffi_guard.rsRustealError and panics (see Errors).

Threads and ticks (task.rs)

The thread that runs rusteal_init is recorded as the game thread. Then:

  • run_on_game_thread(f) pushes f onto a queue; it runs on the next tick.
  • spawn(work, done) runs work on a rusteal-task thread and queues done(value) for the game thread. A panicking work is logged on the game thread; a thread that cannot start is logged too. Running tasks are counted.
  • on_tick(f) registers a hook called every tick with the delta time, until its TickHandle is removed (the handle is #[must_use]: dropping it keeps the hook forever).
  • tick(delta), the on_tick callback: drains the queue, then calls the hooks, each under catch_unwind, without holding the lock while user code runs, so a hook may add or remove hooks.
  • shutdown(), from on_shutdown: waits for running tasks (warning after a second), then clears everything, so no thread runs code that is unloaded.

On the C++ side, StartupModule adds TickLibraries to the core ticker (FTSTicker::GetCoreTicker()), which calls every library’s on_tick each frame.

Calling by name (dynamic_call.rs)

DynamicCall is what the bindings do not cover: functions defined in Blueprints, a BlueprintImplementableEvent’s implementation, delegates.

  1. DynamicCall::new(obj, "Function") checks the object, finds the function by reflection (function_not_found otherwise) and allocates its parameter buffer; for_delegate(obj, property) takes the delegate’s signature instead.
  2. set(name, value) finds the parameter and its offset and writes the value (an FName through the property, to keep its display index); set_struct copies a struct.
  3. call() runs reflection_call_function; broadcast() a multicast delegate. The result’s get(name) reads an out parameter or the return value.
  4. The buffer is freed when the call or its result is dropped.

The macros generate DynamicCalls for BlueprintImplementableEvent bodies and #[udelegate] broadcasts, logging a failure as a warning and returning a default.

Data

  • Containers. UeArray<T>, UeMap<K, V> and UeSet<T> are Copy views, an owner and a property: nothing is cached, each call crosses the boundary (len, get, set, push…), and to_vec, bulk_iter and set_all copy everything in one call, growing the buffer once when it is too small. Elements implement ContainerElement: primitives (raw-copyable), handles and references, String (length then UTF-8), OwnedStruct<T> (bytes).
  • Structs. OwnedStruct<T> owns a buffer sized, initialized and destroyed through reflection (new); copies made from bytes do not destroy. UStructRef<T> borrows memory Unreal owns; OutRef<T> writes an out parameter.
  • Math. Rotator is in degrees and converts to and from DQuat (pitch clamped at ±90°); Transform is a DQuat, a DVec3 translation and a DVec3 scale; LinearColor converts to and from Vec4. The engine’s large world coordinates are doubles, hence the D types.

Services

  • Console (console.rs): register_command(name, help, closure) stores the closure in delegate_registry and registers the command, which receives its arguments and a CommandContext whose world::<W>() checks the world’s handle into a typed reference; a name already registered is refused. ConsoleVariable<T> (bool, i32, f32, String) registers a variable and reads and writes it as text. The plugin forgets a library’s commands when it unloads.
  • Input (input.rs): bind_action_raw(actor, action, trigger, "Function") binds an Enhanced Input action to a #[ufunction], whose parameters must be a prefix of (FInputActionValue, ElapsedSeconds, TriggeredSeconds, UInputAction*); the actor needs an UEnhancedInputComponent. should_display_touch_interface() is the Slate check the templates use for touch controls.
  • Widgets (widget.rs): create_widget_raw(owner, class) picks the owner as CreateWidget does (player controller, world, game instance).
  • Logging: games log through the log crate. rusteal-runtime/src/ue_log.rs installs a log::Log when the library initializes (init), with Info as the default maximum level, and writes each record to the LogRusteal category: error! as an error, warn! as a warning, the rest at display level. Each library has its own log instance, so the game’s and every plugin’s loggers are independent. Underneath, ulog!(level, ...) (logging.rs, LOG_DISPLAY, LOG_WARNING, LOG_ERROR) is the raw call, kept for older projects; it needs the API initialized.

rusteal-runtime

What a game crate depends on, and nothing else:

  • re-exports: rusteal_core as runtime, rusteal_ffi as ffi, rusteal_ue_flags as ue_flags, glam, the macros, and inventory (hidden) for the macros’ registrations;
  • __CALLBACKS: the seven callbacks, each in ffi_boundary;
  • init(table): the checks and the registration described in The Rust–C++ boundary;
  • entry!(): the four exports (rusteal_init, which first records the crate’s source root for error locations, rusteal_shutdown, rusteal_version, rusteal_callbacks);
  • on_load!(function): a hook run at the end of every init, hot reloads included;
  • prelude: the references, containers, structs, errors, math types, handles, uclass/uclass_impl/ustruct and the glam types a game uses most. uenum, ulog!, bail!, ensure! and task are reached through the crate (rusteal_runtime::runtime::ulog).

The engine’s types are not here: they come from the project’s own bindings crate.

The editor module (ue_plugin/Rusteal/Source/RustealEditor/)

Rusteal.uplugin declares RustealEditor as an Editor module: it depends on editor-only modules (UnrealEd, BlueprintGraph, DirectoryWatcher, MessageLog, EditorSubsystem), which a game target cannot link, so the runtime module stays free of them and exposes plain hooks the editor module fills in (RustealSetClassReinstancer, RustealSetSourceOpener). Its StartupModule:

  1. registers the Rusteal Message Log listing (filters, no pages, clearable);
  2. sets the source opener: FSourceCodeNavigation::OpenSourceFile(file, line, column), or the default application;
  3. sets the class reinstancer (see Hot reload);
  4. starts the auto reload.

It also declares URustealEditorSubsystem, the native parent of Rust editor subsystems, which the editor plugin template derives from. ShutdownModule undoes all four.

Testing the runtime

There is no test double of the API table: a test that reaches ffi_dispatch panics in api(). The unit tests (inline #[cfg(test)] modules) exercise the code paths that do not need the engine, and the runtime keeps those paths separate on purpose (is_api_initialized() guards):

FileTests
error.rsCodes, formatting, contexts, locations, std conversions, bail!/ensure!, source paths, size and Send/Sync.
ffi_guard.rsA panic returns the default.
task.rsspawn, on_tick, removal, shutdown, driven by calling tick() by hand.
console.rsValue parsing.
ue_math.rsRotator and color conversions.
subclass_of.rsHand-written UeClass probes.
reify_registry.rsShape combination and the dependency order.

What needs the engine is tested in a project (see Testing a change). CI runs cargo build --workspace, cargo test --workspace and cargo clippy --workspace --all-targets --no-deps -- -D warnings, all without Unreal.