bsk_rust_module

struct BskRustModuleRuntime
#include <bsk_rust_module.h>

Snapshot of the SysModel runtime 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; modelTag in 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

[-] SysModel::RNGSeed

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.modelTag borrows the wrapper’s SysModel::ModelTag storage, while bskLogger refers 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

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:

  1. A plain-C config struct that holds all parameters, message ports, and optional persistent state.

  2. Three lifecycle functionsSelfInit, 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_DECL emits the matching extern "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 uses build.rs to generate the C header while the #[bsk_build::module] procedural attribute emits the extern "C" lifecycle entry points that read/write messages around the module’s own update. The user implements init, reset, and update in safe Rust with named, typed message values and BskResult return types — no FFI boilerplate by hand. A MsgReader<T> field is an input, a MsgWriter<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/cmdTorqueOutMsg above use MsgReader<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

moduleID is a unique int64_t assigned by Basilisk’s ModuleIdGenerator when the Python-visible C++ wrapper’s SysModel base 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_write call automatically.

Lifecycle context — BskRustModuleRuntime

A Rust module has no C++ base class, so this struct mirrors the relevant SysModel fields (module ID, name, …) for each lifecycle call. BskContext gives safe Rust module logic a borrowed view of that snapshot. Runtime services do not appear in the public config struct.

modelTag is 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 &str tied to that context borrow, so safe module logic cannot retain it past the lifecycle call that received it.

currentSimNanos

currentSimNanos is the current simulation time in nanoseconds [ns], passed to both Reset and Update (not SelfInit). It is also written into each outgoing message header by *_C_write. (BSK C modules call this parameter callTime; Rust modules use the more explicit C++ SysModel name currentSimNanos.)

Logging

BskContext::logger() supplies the same standard logging a hand-written C module has through the no-throw logging adapter. Its borrowed BskLoggerRef provides .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 return Err(BskError::new(...)) from a lifecycle method; they are not logging operations. The generated boundary carries that failure as data and raises BasiliskError only 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 in Reset and before each Update read; a missing connection returns an expected Rust error that the C++ wrapper translates into BasiliskError.

Optional input#[bsk(optional)] on a MsgReader<T> gives the generated input field type Option<Msg> (None when unlinked) instead of raising an error.

Output — a MsgWriter<T> is initialized automatically in SelfInit and written from the same named field returned by reset or update.

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::State associated 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_name function runs ordinary Rust drop glue for both the config and state. No Cleanup_* function, raw state pointer, or custom destructor is needed. Stateless modules use type 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-build generates Vec2’s C struct alongside myModuleConfig and 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 named name, whose config view is configType and whose opaque instance type is handleType.

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 owning BskRustError on failure.

Config_name(handle) Returns a borrowed pointer to the instance’s FFI-safe parameter and message-port view. The pointer remains valid until Destroy_name. Generated wrappers use this view only for message ports; Python-facing configuration values use the guarded accessors below.

GetConfigField_name / SetConfigField_name Copy 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_name Return static metadata used by the generated Python getter, setter, and property. Null means that the field is not deprecated.

ModuleDeprecationDate_name / ModuleDeprecationMessage_name Return 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 owning BskRustError if 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 owning BskRustError on failure.

Reset_name(handle, currentSimNanos, context) Called before the first step and on explicit resets. The generated lifecycle code checks required input connectivity, then calls BskModule::reset with a safe borrowed context. It writes outputs only after reset returns Ok. Returns null on success or an owning BskRustError on failure.

Update_name(handle, currentSimNanos, context) Called every simulation step. The generated lifecycle code reads all input messages, calls BskModule::update with a safe borrowed context, and writes output messages only after update returns Ok. Returns null on success or an owning BskRustError on 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 during SelfInit, Reset, or Update poisons that module instance because its internal invariants may be incomplete. Subsequent lifecycle calls return an error without re-entering module code. Expected BskError results do not poison the instance, and Destroy_name remains valid for poisoned instances. The guarded boundary returns the panic diagnostic through BskRustError and 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.

configType must be declared before this macro. bsk-build passes whatever struct name the crate’s impl BskModule block actually uses, which need not match name##Config.