bsk_rust_module
-
struct BskRustModuleRuntime
- #include <bsk_rust_module.h>
Snapshot of the
SysModelruntime fields a Rust module may need.Embedded in the context passed to every lifecycle call and valid only for that call. Do not retain it afterward;
modelTagin particular is a borrowed pointer.Public Members
-
int64_t moduleID
[-] unique ID assigned by ModuleIdGenerator
-
const char *modelTag
[-] SysModel::ModelTag, borrowed for this call only
-
uint64_t callCounts
[-] SysModel::CallCounts step counter
-
uint32_t rngSeed
-
int64_t moduleID
-
struct BskRustModuleContext
- #include <bsk_rust_module.h>
Borrowed framework services supplied to a Rust lifecycle call.
The wrapper owns every referenced value. Rust may use this structure only for the duration of the lifecycle call and must not retain either pointer.
runtime.modelTagborrows the wrapper’sSysModel::ModelTagstorage, whilebskLoggerrefers to the wrapper’s logging object.The shared C++ wrapper constructs this value immediately before each opaque-handle lifecycle call.
Public Members
-
BskRustModuleRuntime runtime
[-] borrowed SysModel runtime snapshot
-
BSKLogger *bskLogger
[-] borrowed Basilisk logger
-
BskRustModuleRuntime runtime
Defines
-
BSK_RUST_MODULE_ABI_VERSION
Version of the generated Rust-module C ABI used by bsk-build and bsk-sdk.
-
BSK_RUST_EXTERN_C_BEGIN
Declares Rust-owned module allocation plus the three BSK lifecycle entry points for a module implemented in Rust.
Background
Basilisk C modules consist of two parts:
A plain-C config struct that holds all parameters, message ports, and optional persistent state.
Three lifecycle functions —
SelfInit,Reset,Update— called by the Basilisk task scheduler at well-defined points.
For Rust modules the lifecycle functions are compiled into a Rust static library linked into the SWIG-generated Python module.
BSK_RUST_DECLemits the matchingextern "C"declarations so that the C compiler and the SWIG-generated glue can find them. Unlike the C-module wrapper, the Rust wrapper hides direct parameter fields and exposes generated properties backed by guarded Rust getters and setters.Macro-generated workflow
Writing raw
unsafe extern "C"Rust is error-prone. The recommended workflow usesbuild.rsto generate the C header while the#[bsk_build::module]procedural attribute emits theextern "C"lifecycle entry points that read/write messages around the module’s ownupdate. The user implementsinit,reset, andupdatein safe Rust with named, typed message values andBskResultreturn types — no FFI boilerplate by hand. AMsgReader<T>field is an input, aMsgWriter<T>field is an output, and only an input that may be unlinked needs the#[bsk(optional)]annotation. See the Basilisk documentation’s “Making Rust Modules” page for the full guide.Config struct field ordering
A Rust module config contains only Python-visible parameters and message ports. Framework metadata, logging, and internal Rust state live outside this FFI view. The suggested layout is::
typedef struct { // 1. Scalar / array parameters double K; //!< [Nm] proportional gain double P; //!< [Nm/(rad/s)] rate gain // 2. Input message ports AttGuidMsg_C attGuidInMsg; //!< [-] attitude guidance // 3. Output message ports CmdTorqueBodyMsg_C cmdTorqueOutMsg; //!< [Nm] control torque } myModuleConfig; BSK_RUST_DECL(myModule, myModuleConfig, myModuleConfigHandle)On the Rust side,
attGuidInMsg/cmdTorqueOutMsgabove useMsgReader<AttGuidMsg>/MsgWriter<CmdTorqueBodyMsg>. Those types identify the port direction without another annotation — see the “Making
Rust Modules” documentation page for the complete Rust form.
moduleID
moduleIDis a uniqueint64_tassigned by Basilisk’sModuleIdGeneratorwhen the Python-visible C++ wrapper’sSysModelbase is constructed. Task registration does not assign the ID. It is stamped onto every outgoing message header (via*_C_write) so that message recording and the logging subsystem can identify which module produced a given message. The generated lifecycle code forwards it to every*_C_writecall automatically.Lifecycle context — BskRustModuleRuntime
A Rust module has no C++ base class, so this struct mirrors the relevant
SysModelfields (module ID, name, …) for each lifecycle call.BskContextgives safe Rust module logic a borrowed view of that snapshot. Runtime services do not appear in the public config struct.modelTagis a borrowed pointer valid only for the duration of the call. On the Rust side this is enforced by the compiler, not just this comment:BskContext::model_tag()returns a&strtied to that context borrow, so safe module logic cannot retain it past the lifecycle call that received it.currentSimNanos
currentSimNanosis the current simulation time in nanoseconds [ns], passed to bothResetandUpdate(notSelfInit). It is also written into each outgoing message header by*_C_write. (BSK C modules call this parametercallTime; Rust modules use the more explicit C++SysModelnamecurrentSimNanos.)Logging
BskContext::logger()supplies the same standard logging a hand-written C module has through the no-throw logging adapter. Its borrowedBskLoggerRefprovides.debug()/.info()/.warning()methods. The shared wrapper borrows its framework-managed logger into the lifecycle context. The logger does not appear in the public config struct. A no-throw C++ adapter catches any logger exception before returning to Rust. Expected configuration, input, and runtime failures returnErr(BskError::new(...))from a lifecycle method; they are not logging operations. The generated boundary carries that failure as data and raisesBasiliskErroronly after Rust has returned normally.Message port patterns
The Rust field type identifies the port role. The procedural attribute generates named input and output value structs whose fields retain those config field names::
pub attGuidInMsg: MsgReader<AttGuidMsg>, #[bsk(optional)] pub disturbanceInMsg: MsgReader<CmdTorqueBodyMsg>, pub cmdTorqueOutMsg: MsgWriter<CmdTorqueBodyMsg>,
Required input — an unannotated
MsgReader<T>checks connectivity inResetand before eachUpdateread; a missing connection returns an expected Rust error that the C++ wrapper translates intoBasiliskError.Optional input —
#[bsk(optional)]on aMsgReader<T>gives the generated input field typeOption<Msg>(Nonewhen unlinked) instead of raising an error.Output — a
MsgWriter<T>is initialized automatically inSelfInitand written from the same named field returned byresetorupdate.Python wiring (same as any BSK C module)::
ctrl = myModule.myModule() # Python wrapper class ctrl.ModelTag = "myCtrl" ctrl.K = 5.0 # set parameters ctrl.attGuidInMsg.subscribeTo(src.attGuidOutMsg) # connect input sim.AddModelToTask("task", ctrl) # schedule the module # ctrl.cmdTorqueOutMsg is readable after InitializeSimulation()
Stateful modules — Rust-owned state
Modules that need persistent implementation state set the
BskModule::Stateassociated type. This state is stored beside the config inside the opaque Rust module instance and never crosses the FFI boundary. It may therefore contain ordinary Rust collections, strings, enums, and smart pointers::#[derive(Default)] pub struct MyState { history: Vec<f64>, status: String, } impl BskModule for myModuleConfig { type State = MyState; // reset()/update() receive &mut Self::State and return BskResult }
The generated
Destroy_namefunction runs ordinary Rust drop glue for both the config and state. NoCleanup_*function, raw state pointer, or custom destructor is needed. Stateless modules usetype State = ();.Grouping parameters — nested structs
A field may be another
#[repr(C)]struct defined in the same crate, by value (not a pointer), to group related parameters::#[repr(C)] pub struct Vec2 { pub x: f64, pub y: f64 } #[bsk_build::module] #[repr(C)] pub struct myModuleConfig { pub target: Vec2, // ... }
bsk-buildgeneratesVec2’s C struct alongsidemyModuleConfigand Python transfers the complete nested value through the generated getter and setter.A raw pointer to one of these structs (
*mut Vec2), or to any other type, is rejected. SWIG’s pointer-field setter would transfer ownership away from the Python object with nothing on the Rust side to ever free it; this applies just as much to a pointer to a primitive (*mut u8) as to a struct, so there is currently no supported field type for a persistent string or byte-buffer parameter.A field also may not be a Rust
enum, even a fieldless#[repr(u8)](or similar) one. An invalid integer discriminant copied across an FFI boundary would be undefined behavior before Rust could validate it. Use the underlying integer type as the field (e.g.pub mode: u8), validate it in the generated setter, and convert it to the Rust enum only after the value is known to be valid.
-
BSK_RUST_EXTERN_C_END
-
BSK_RUST_DECL(name, configType, handleType)
Emit
extern "C"allocation and lifecycle function declarations for a Rust-backed Basilisk module namedname, whose config view isconfigTypeand whose opaque instance type ishandleType.Create_name(&handle)Constructs the complete module instance in Rust, including arbitrary Rust-owned state, and writes its opaque owning handle. Returns null on success or an owningBskRustErroron failure.Config_name(handle)Returns a borrowed pointer to the instance’s FFI-safe parameter and message-port view. The pointer remains valid untilDestroy_name. Generated wrappers use this view only for message ports; Python-facing configuration values use the guarded accessors below.GetConfigField_name/SetConfigField_nameCopy one generated configuration value across the C boundary. The field index and byte size are generated from the same Rust struct. Setters validate the complete typed value before changing it and return an expected error without modifying the previous value when validation fails.ConfigFieldDeprecationDate_name/ConfigFieldDeprecationMessage_nameReturn static metadata used by the generated Python getter, setter, and property. Null means that the field is not deprecated.ModuleDeprecationDate_name/ModuleDeprecationMessage_nameReturn static metadata used by the generated Python module constructor. Null means that the module is not deprecated.Destroy_name(handle)Runs the config and internal state’s Rust drop glue and returns the complete allocation to Rust. Returns null on success or an owningBskRustErrorif Rust catches a panic while dropping the instance.SelfInit_name(handle, context)Called once at task registration. The generated lifecycle code initialises output message ports (*_C_init). Returns null on success or an owningBskRustErroron failure.Reset_name(handle, currentSimNanos, context)Called before the first step and on explicit resets. The generated lifecycle code checks required input connectivity, then callsBskModule::resetwith a safe borrowed context. It writes outputs only afterresetreturnsOk. Returns null on success or an owningBskRustErroron failure.Update_name(handle, currentSimNanos, context)Called every simulation step. The generated lifecycle code reads all input messages, callsBskModule::updatewith a safe borrowed context, and writes output messages only afterupdatereturnsOk. Returns null on success or an owningBskRustErroron failure.Every generated Rust definition uses the non-unwinding C ABI and catches Rust panics before returning. The caller owns every non-null error result and must release it with
Destroy_BskRustError. A panic caught duringSelfInit,Reset, orUpdatepoisons that module instance because its internal invariants may be incomplete. Subsequent lifecycle calls return an error without re-entering module code. ExpectedBskErrorresults do not poison the instance, andDestroy_nameremains valid for poisoned instances. The guarded boundary returns the panic diagnostic throughBskRustErrorand suppresses duplicate default Rust panic-hook output only on the thread executing that call. Panics outside a generated boundary continue through the previously installed application hook.configTypemust be declared before this macro. bsk-build passes whatever struct name the crate’simpl BskModuleblock actually uses, which need not matchname##Config.