Reification and the macros
A #[uclass] struct becomes a real UClass when its library loads: Rusteal reifies it. This chapter follows one class from the macro to the engine and back: what the macros generate, where an object’s Rust data lives, how the engine calls a Rust function, and how the C++ side builds the class. The order types are registered in, and enums, interfaces and booleans, are in Types declared in Rust.
The macros (rusteal-macros)
Only four procedural macros exist, and rusteal-runtime re-exports them: #[uclass], #[uclass_impl], #[ustruct] and #[uenum]. #[uproperty], #[component], #[ufunction], #[class_defaults] and #[udelegate] are inert attributes that the outer macro parses and strips.
Names (prop_type.rs): a Rust name becomes the Unreal one in PascalCase (move_speed → MoveSpeed), with b_ turned into UE’s bool prefix (b_can_dash → bCanDash); name = "..." overrides it. A type’s id is the FNV-1a hash of its identifier (not its module path), and a parent’s id the hash of the parent path’s last segment: two Rust classes with the same name in one library collide.
#[uclass] (uclass.rs)
| Argument | Meaning |
|---|---|
parent = Path | Required: the engine or Rust class it derives from. |
implements = "..." / [...] | Interfaces by class path, starting with /. |
config = "Name" | The config category its Config properties are saved in. |
Other arguments are ignored without an error, and so are attributes written on the struct itself.
Fields are of three kinds:
#[uproperty(...)]: a UE property. Flags:BlueprintReadWrite,BlueprintReadOnly,EditAnywhere,EditDefaultsOnly,VisibleAnywhere,SaveGame,Config,Transient,BindWidget,BindWidgetOptional,BindWidgetAnimandBindWidgetAnimOptional(bothTransient); values:default = expr(scalars and enums only),name,category. Anything else is a compile error.#[component(...)]: a default subobject:root,attach = "field",socket = "..."(needsattach),name = "..."(the subobject’s name, PascalCase of the field otherwise).- anything else: a Rust-private field, never seen by the engine.
The flags map to CPF_* as UHT’s do:
| Argument | Flags |
|---|---|
BlueprintReadWrite, BlueprintReadOnly | CPF_BlueprintVisible |
BlueprintReadOnly | CPF_BlueprintReadOnly, and CPF_Edit + CPF_EditConst unless an edit flag is given |
EditAnywhere, BlueprintReadWrite | CPF_Edit |
EditDefaultsOnly | CPF_Edit, CPF_DisableEditOnInstance |
VisibleAnywhere | CPF_Edit, CPF_EditConst |
SaveGame / Config / Transient | CPF_SaveGame / CPF_Config / CPF_Transient |
category and the BindWidget* flags become property metadata, which only exists in editor builds (WITH_EDITORONLY_DATA).
A property’s type is one of bool, i32, i64, u8, f32, f64, UObjectRef<T>, SubclassOf<T>, SoftObjectRef<T>, OwnedStruct<T>, FName, String, an engine or Rust enum (any capitalized path without generics), or a UeArray of scalars, objects, classes, FNames or OwnedStructs. A const _ block asserts at compile time that the parent and every component type are Unreal classes.
The expansion, in order:
- The struct is rewritten to
{ __obj: UObjectHandle, __rust_data: *mut __<Name>RustData }: a handle to the engine object and a pointer to the Rust-private fields, which move to__<Name>RustData. That type’sDefaultcallsDefault::default()on each field, so private fields must implementDefault. UeClass(static_class(), which panics if the class was never registered),HasParent,Inherits<U>through the parent, andDeref<Target = UObjectRef<Parent>>(a pointer cast, relying onUObjectRef’s#[repr(transparent)]layout): a Rust class has its parent’s methods.- For each property, a getter and a
set_setter that look up theFPropertyHandleonce (astatic OnceLockper accessor) and then callproperty_get_*/property_set_*; an array gets a getter returning aUeArrayview. For each private field,x()(a clone),set_x()andx_mut(). For each component, a getter returning it fromreify_find_default_subobject. as_ref(),from_obj(obj)(validity,IsA, then the instance data; null data means the library was reloaded or unloaded),get_default()(the default object).- Hidden registration functions:
__rusteal_register_<name>(find the parent, register theRustTypeInfowhose construct isBox::into_raw(Box::new(Default::default())), callreify_create_class),__rusteal_members_<name>(properties, interfaces, components),__rusteal_finalize_<name>(reify_finalize_class, then thedefault =values on the default object),__rusteal_after_defaults_<name>(reify_set_class_configforconfig). inventory::submit!(ClassRegistration { type_id, shape, depends_on, create, register, finalize, after_defaults }),depends_onbeing the parent’s and the components’ ids.
#[uclass_impl] (uclass_impl.rs)
It takes no arguments and relies on the names #[uclass] generated, so it sits in the same module as the struct.
#[ufunction(...)] accepts BlueprintCallable (the default), BlueprintPure, Override, BlueprintImplementableEvent (at most one of the last three), Exec and name:
| Kind | Flags |
|---|---|
| Callable | FUNC_Native | FUNC_BlueprintCallable | FUNC_Public |
| Pure | the same, | FUNC_BlueprintPure |
| Override | FUNC_Native | FUNC_BlueprintEvent | FUNC_Public |
| ImplementableEvent | FUNC_Event | FUNC_BlueprintEvent | FUNC_BlueprintCallable | FUNC_Public, no FUNC_Native |
Exec | adds FUNC_Exec |
Parameters are scalars, UObjectRef, SubclassOf, UStructRef, enums and OutRef<T>; an override’s parameters come from the parent’s function, not from Rust. A return type of RustealResult<T> (or Result<T, RustealError>) makes the function fallible: Unreal sees T, and an Err is raised as a script error (see Errors). An ImplementableEvent has an empty body, which the macro replaces with a DynamicCall to the Blueprint’s implementation.
#[class_defaults] (one per block, &mut self) runs on the class default object after finalize; a RustealResult<()> it returns is logged on error. #[udelegate] (an empty &self method, name optional) declares a multicast delegate: its body becomes a broadcast, and the macro adds <name>_add(closure) and <name>_add_ufunction(target, "Function").
The expansion registers each function with a closure (register_function, whose returned index is the callback id), then reify_add_function and its parameters with CPF_Parm (and a ReturnValue with CPF_ReturnParm); events get callback id 0. It submits a ClassFunctionRegistration { type_id, shape, register_functions, class_defaults }.
#[ustruct] and #[uenum]
#[ustruct] requires every field to be a #[uproperty] (no default; a nested struct is OwnedStruct<T>). The Rust struct becomes a unit marker type; the fields are reached through a <Name>Ext trait implemented for UStructRef<Name> (memory Unreal owns) and OwnedStruct<Name> (a buffer Rust owns). #[uenum] takes unit variants with discriminants in 0..=254 and makes the enum #[repr(u8)]; see Types declared in Rust.
The shape (shape.rs)
Each macro hashes its declarations: the sorted list of the attribute tokens with ident: Type for fields, or the signature for methods, joined and FNV-1a hashed. Bodies, doc comments, private fields and order do not count. reify_registry::class_shape combines a class’s shape with its impl blocks’, and the plugin compares it on hot reload to keep or replace the class.
The registry (rusteal-core/src/reify_registry.rs)
| Store | What it holds |
|---|---|
TYPE_REGISTRY | type_id → RustTypeInfo (construct and drop functions). |
FUNC_REGISTRY | A Vec of FunctionEntry { callback, type_id }; the callback id is the index. |
INSTANCE_DATA | The object’s address → one InstanceEntry { data, type_id } per reified class in its chain (a Blueprint child of a Rust class of a Rust class has two). |
construct_instance(obj, type_id)builds the data and stores it, replacing an existing entry of that type;drop_instance(obj)drops every entry of the address;clear_alldrops everything when the library shuts down.- A call from the engine reaches
invoke_function(id, obj, params): the entry is cloned, the instance data found by type, and the generated closure runs. It caches the parameters’ offsets (reflection, once), reads each withnative_mem_read(or as aUStructRef/OutRefinto the buffer), zero-initializes the return slot, builds a temporarySelf { __obj, __rust_data }, calls the method, raises anErr, and writes the return value back (reflection_copy_structfor a struct).
The C++ side
URustealReifiedClass
A UBlueprintGeneratedClass subclass, the class behind every #[uclass]: RustTypeId, its Library, its Shape, its first native ancestor (NativeSuperClass) and its component definitions. Being a Blueprint-generated class is what lets the engine create and destroy it at runtime, and Blueprints derive from it; it is also why editor Python sees Rust classes by their Unreal names.
RustealClassConstructor, its ClassConstructor:
- finds every reified class in the object’s class chain, root first (
ReifiedChain), which is how a Blueprint child of a Rust class is constructed; - calls the native parent’s constructor;
- creates each component definition of the chain with
CreateDefaultSubobject, sets its property, and makes it the root or attaches it (to a component of this pass, or an inherited one found by name); - calls
construct_rust_instance(obj, RustTypeId, bIsCDO)for each class of the chain, each through its own library.
RustealReifyApiImpl.cpp
CreateClassImpl: in the library’s package (/Script/Rustealor/Script/<Plugin>, created rooted withPKG_CompiledIn), aNewObject<URustealReifiedClass>(RF_Public | RF_Standalone)with the parent as super struct, itsClassWithin, the inherited class flags without the config ones plusCLASS_CompiledFromBlueprint, andRustealClassConstructor. In the editor, a stubUBlueprintnamedRS_<Name>is itsClassGeneratedBy, up to date and rooted, so the editor’s Blueprint machinery accepts the class. The class is rooted. An existing class is kept or retired by its shape (see Hot reload).AddPropertyImpl: an existing property of the same name is reused (hot reload); otherwise one is created by type and added withAddCppProperty. An enum property is anFEnumPropertyover anFByteProperty.AddFunctionImpl: an existingURustealReifiedFunctionkeeps its object and gets the new callback id and library; a new one is linked into the class’sChildren. A native function getsexecCallRustFunctionas its native thunk; an override copies its parameters from the parent’s function of the same name (CopyParamsFromParentFunction).FinalizeClassImpl: binds and links the functions and the class, assembles the reference token stream, notifies the registration, and creates the default object, which runs the class constructor and therefore the default object’s Rust data. An actor class with its ownReceiveTickgetsbCanEverTickandbStartWithTickEnabled.- Components (
AddDefaultSubobjectImpl) are stored asFRustealComponentDefs with an object property (EditConst,BlueprintReadOnly,InstancedReference…). Interfaces (AddInterfaceImpl) are loaded by path, after engine init when needed, and added asFImplementedInterface(I, 0, true), as for a Blueprint. Config (SetClassConfigImpl) sets the config name and flags and loads the default object’s config.
URustealReifiedFunction::execCallRustFunction
- Finds the reified function:
Stack.CurrentNativeFunction,Stack.Node, the super function chain, or by name in the object’s reified classes. - Gets the parameters: from
ProcessEvent, they areStack.Locals; from the Blueprint VM, they are stepped into a zeroedFMemory_Allocabuffer, with the out parameters’ addresses recorded. - Calls
invoke_rust_function(CallbackId, Self, Params)with the library current. - A pending script error becomes a Blueprint runtime error, on the VM path.
- Copies out parameters and the return value back, and destroys the VM path’s parameters.
Lifetime
The engine owns the object; Rust owns its data. FRustealDeleteListener::NotifyUObjectDeleted calls drop_rust_instance once per library of the object’s reified chain when the garbage collector deletes it. Private fields live on the Rust heap and are not reflected, so the garbage collector does not see objects they reference: a private UObjectRef does not keep its object alive (a #[uproperty] or a Pinned does).
Structs, delegates and subsystems
- Structs.
CreateStructImplmakes aUScriptStruct(BlueprintType), reused on reload;FinalizeStructImplfinalizes nested structs first, then binds, links and prepares the struct ops.UStructRef<T>borrows memory Unreal owns (a parameter buffer);OwnedStruct<T>owns a buffer, initialized and destroyed through reflection. - Delegates.
#[udelegate]adds a signature function<Name>__DelegateSignatureand anFMulticastInlineDelegateProperty(BlueprintAssignable). A broadcast is aDynamicCall::for_delegate. Binding a Rust closure stores it indelegate_registryunder an id and creates aURustealDelegateProxy(outer: the owner) whoseProcessEventforwards the broadcast toinvoke_delegate_callback. ADelegateBindingunbinds when dropped, unlessdetach()ed; a closure re-entered while it runs is skipped. - Subsystems. The engine only creates subsystems of native classes, so
RustealSubsystems.hdeclares abstract native parents that turn the virtuals into events Rust overrides:URustealGameInstanceSubsystem,URustealWorldSubsystem(a tickable world subsystem withReceivePostInitialize,ReceiveWorldBeginPlay,ReceiveTick,bWantsTick,bCreateInEditorWorlds),URustealEngineSubsystem,URustealLocalPlayerSubsystem(ReceivePlayerControllerChanged), and, in the editor module,URustealEditorSubsystem. Each forwardsShouldCreateSubsystem,InitializeandDeinitializetoReceiveShouldCreateSubsystem,ReceiveInitializeandReceiveDeinitialize.
Object references
| Type | What it is | Contract |
|---|---|---|
UObjectRef<T> | #[repr(transparent)] over UObjectHandle | Copy, no ownership, no GC root; is_valid() is ::IsValid; checked() before use; cast() checks IsA, upcast is checked at compile time. The Deref chain casts pointers, so the layout must stay transparent. |
TWeakObjectPtr<T> | An FWeakObjectPtr’s bytes | get() is None once the object is gone. |
SoftObjectRef<T> | A path | get() finds a loaded object; load_synchronous() loads it. |
SubclassOf<T> | #[repr(transparent)] over UClassHandle | base() is T::static_class(). |
Pinned<T> | A GC root (AddToRoot) | Not reference counted: dropping one of two Pinned of the same object unroots it. Pins are released when their library unloads. |