.. _rustModules:
:beta:`Making Rust Modules`
===========================
.. important::
Rust module support requires Rust 1.89 or newer. The current stable Rust
toolchain is recommended.
.. sidebar:: Rust Module Support
Rust module support is experimental. Its interface may change between
Basilisk releases.
Rust modules are compiled Basilisk modules whose lifecycle methods are written
in Rust instead of C or C++. They use the normal ``SysModel`` lifecycle,
message system, Python configuration, module documentation, and unit-test
structure. A Python user imports and schedules a Rust module in exactly the
same way as another compiled Basilisk module.
This guide is for developers who already understand C or C++ Basilisk modules.
It introduces the Rust-specific terms and files needed to create a module
inside the Basilisk source tree. The :ref:`rustModuleTemplate` is the complete
working example and should be used as the starting point for a new module.
Rust Terms Used in This Guide
-----------------------------
Rust uses different names for several familiar build concepts:
``rustc``
The Rust compiler.
``Cargo``
Rust's build tool and dependency manager. Cargo reads ``Cargo.toml``, runs
the module's ``build.rs`` script, compiles the Rust source, and runs
Rust-native tests. The standard Rust installer supplies both ``rustc`` and
Cargo.
Cargo package
A directory described by one ``Cargo.toml`` manifest. Each Basilisk Rust
module directory is a Cargo package.
crate
A Rust compilation unit. A Cargo package can contain one or more crates.
Each Basilisk Rust module contains one library crate, so this guide
occasionally uses *module crate* to mean the Rust library compiled from the
module directory.
Cargo workspace
A group of related Cargo packages that share dependency resolution and one
lockfile. All Rust packages inside the Basilisk repository belong to the
workspace defined by ``src/Cargo.toml``.
``Cargo.lock``
The file recording the exact dependency versions selected for a workspace.
Basilisk commits one shared lockfile at ``src/Cargo.lock`` so local and CI
builds use the same versions.
attribute
Metadata written above a Rust item with ``#[...]``. For example,
``#[bsk_build::module]`` identifies the configuration struct and asks
Basilisk's Rust tooling to generate its lifecycle interface.
trait
A Rust interface defining behavior that a type must provide. It is similar
in purpose to a C++ abstract base-class interface. A module implements the
``BskModule`` trait to provide its lifecycle methods.
The official `Rust Book `__ provides an
introduction to general Rust syntax. The
`Cargo Book `__ covers Cargo terminology and
commands in more detail.
Comparison with C and C++ Modules
---------------------------------
The Rust tooling generates the C header and SWIG interface needed to join the
existing Basilisk build. The module author writes the Rust behavior and the
normal Basilisk documentation and tests.
.. list-table:: Mapping familiar Basilisk concepts to Rust
:header-rows: 1
:widths: 31 31 38
* - Basilisk concept
- C or C++
- Rust
* - Implementation
- ``myModule.c/.cpp`` and usually ``myModule.h``
- ``myModule.rs``
* - Build description
- CMake source lists
- ``Cargo.toml`` plus workspace registration
* - SWIG and C boundary
- Hand-written ``myModule.i`` and public declarations
- Generated from the marked Rust configuration
* - Python-visible configuration
- Config struct or public class fields
- ``#[repr(C)]`` struct marked with ``#[bsk_build::module]``
* - Private implementation state
- Private C++ members or fields hidden from SWIG
- ``BskModule::State``
* - Initialization
- Constructor and ``SelfInit()``
- Rust ``Default``/``init()`` and generated ``SelfInit()``
* - Reset and update
- ``Reset()`` and ``UpdateState()``
- ``reset()`` and ``update()`` methods of ``BskModule``
* - Message I/O
- Reader/writer fields and explicit read/write calls
- Annotated ports and generated named input/output values
* - Logging
- ``bskLogger``
- ``context.logger()``
* - Expected failure
- ``BasiliskError`` or ``BSK_ERROR``
- Return ``Err(BskError::new(...))``
Install and Build with Rust Support
-----------------------------------
Install the stable toolchain with the official
`rustup installer `__. The
:ref:`Linux `, :ref:`macOS `, and
:ref:`Windows ` setup pages contain platform-specific
instructions.
Open a new terminal after installing Rust and verify both tools:
.. code-block:: console
rustc --version
cargo --version
Rust module discovery is disabled in a normal Basilisk build, so users who do
not need Rust modules do not need these tools. Enable Rust modules with:
.. code-block:: console
python3 conanfile.py --rustModules True
On Windows, use ``python`` instead of ``python3`` if that is the command for
the active Python installation.
``rustModules`` enables discovery and compilation of in-tree Rust modules and
Rust modules supplied through ``pathToExternalModules``. Cargo builds the Rust
code, while Corrosion connects Cargo packages to Basilisk's CMake build as
ordinary CMake targets. Basilisk downloads a pinned Corrosion version
automatically during the first Rust-enabled configuration, so developers only
need to install Rust and Cargo.
After the build, verify the template through its normal Python unit test:
.. code-block:: console
python3 -m pytest src/moduleTemplates/rustModuleTemplate/_UnitTest -v
Quick-Start from ``rustModuleTemplate``
---------------------------------------
Copy ``src/moduleTemplates/rustModuleTemplate`` into the desired
``src/fswAlgorithms`` or ``src/simulation`` category. Then:
#. Rename the directory to the new module name.
#. Rename ``rustModuleTemplate.rs`` and ``rustModuleTemplate.rst`` so each
filename matches the new module directory.
#. Update the package name and ``[lib] path`` in ``Cargo.toml``.
#. Rename the marked configuration struct in the Rust source.
#. Pass that exact struct name to ``generate_bindings()`` in ``build.rs``.
#. Replace the template implementation, documentation, and Python unit test.
#. Add the new module to the workspace as described in
`Register the Module in the Workspace`_.
The template intentionally demonstrates more than a minimal control law. It
contains individual and fixed-size arrays of input and output messages,
Python-visible parameters, Rust-owned private state, logging, expected errors,
Rust-native tests, and a test-only panic used to verify the language boundary.
Module Layout
-------------
Place the files in a normal Basilisk module directory:
.. code-block:: text
src/fswAlgorithms//myModule/
|-- Cargo.toml
|-- build.rs
|-- myModule.rs
|-- myModule.rst
`-- _UnitTest/
`-- test_myModule.py
``Cargo.toml``
Describes the module package, Rust source path, output library, and
dependencies. It fills the role that Cargo expects from a package manifest.
``build.rs``
A short Cargo build script that runs before compilation. It asks
``bsk-build`` to generate the module's C header and SWIG interface.
``myModule.rs``
Contains the configuration struct, lifecycle implementation, private state,
and optional Rust-native tests.
``myModule.rst``
The normal Basilisk module documentation page.
``_UnitTest/test_myModule.py``
Tests the public Python interface and message behavior in a Basilisk
simulation.
No hand-written ``.h`` or ``.i`` file is required. Generated files are build
products placed under ``dist3`` and are removed by the normal clean build.
Configure the Module Package
----------------------------
The module's ``Cargo.toml`` should follow this pattern:
.. code-block:: toml
[package]
name = "myModule"
version = "0.1.0"
edition = "2021"
rust-version.workspace = true
[package.metadata.basilisk]
module = true
[lib]
path = "myModule.rs"
crate-type = ["staticlib"]
[dependencies]
bsk-messages = { path = "../../../architecture/rust/bsk_messages" }
bsk-build = { path = "../../../architecture/rust/bsk_build" }
[build-dependencies]
bsk-build = { path = "../../../architecture/rust/bsk_build",
default-features = false, features = ["codegen"] }
The important entries are:
``rust-version.workspace = true``
Uses Basilisk's minimum Rust version instead of declaring a different
version for each module.
``[package.metadata.basilisk]``
Marks this Cargo package as a Basilisk module. Support libraries under
``src/architecture/rust`` are Cargo packages too, but they do not have this
marker and are not exposed as Python modules. During CMake configuration,
Basilisk asks Cargo for workspace metadata and reads this typed Boolean
marker from Cargo's package model; Basilisk does not interpret
``Cargo.toml`` syntax itself.
``[lib] path``
Selects ``myModule.rs`` as the library source. Cargo would otherwise expect
the conventional Rust path ``src/lib.rs``.
``crate-type = ["staticlib"]``
Produces a native static library that Basilisk links into the generated
Python module.
``[dependencies]``
Lists Rust libraries used by the module:
* ``bsk-messages`` supplies Basilisk message value types, message ports,
lifecycle types, context, logging, and errors.
* ``bsk-build`` supplies the ``#[bsk_build::module]`` attribute and runtime
support.
``[build-dependencies]``
Lists code used only by ``build.rs``. Disabling default features prevents
the host build script from trying to link Basilisk runtime symbols; the
``codegen`` feature enables binding generation.
The dependency paths are relative to the module directory. Adjust them when a
module is at a different directory depth.
Generate the Language Boundary
------------------------------
The complete ``build.rs`` file is normally:
.. code-block:: rust
fn main() {
bsk_build::generate_bindings("MyModuleConfig");
}
Pass the exact name of the struct marked with ``#[bsk_build::module]``.
Cargo compiles and runs ``build.rs`` as a separate host program before it
compiles the module crate, so the build script cannot refer to
``MyModuleConfig`` as a Rust type. ``generate_bindings()`` passes the selected
identifier into the subsequent Rust compilation, where the procedural
attribute verifies that both names match. A stale or misspelled name therefore
produces a compile error.
During the build:
#. Cargo runs ``build.rs``.
#. ``bsk-build`` uses ``cbindgen`` to generate a C-compatible header for the
selected configuration.
#. ``bsk-build`` generates the module-specific SWIG interface.
#. Rust compiles the module, and ``#[bsk_build::module]`` verifies the selected
configuration name while generating the lifecycle boundary.
#. CMake links the Rust static library and generated wrapper into the normal
Basilisk Python package.
The generated header and SWIG interface are build products; do not edit them.
Register the Module in the Workspace
------------------------------------
All in-tree Rust packages belong to the Cargo workspace rooted at
``src/Cargo.toml``. The workspace gives Basilisk one dependency resolution and
one committed ``src/Cargo.lock``. Workspace registration is also required for
module discovery: CMake considers the packages returned by ``cargo metadata``
and selects those whose ``[package.metadata.basilisk]`` ``module`` value is
``true``.
Add the new module's path, relative to ``src``, to the explicit member list:
.. code-block:: toml
[workspace]
resolver = "2"
members = [
"architecture/rust/bsk_build",
"architecture/rust/bsk_macros",
"architecture/rust/bsk_messages",
"architecture/rust/bsk_utilities",
"fswAlgorithms//myModule",
"moduleTemplates/rustModuleTemplate",
]
After adding the member or changing dependencies, let Cargo update the shared
lockfile deliberately:
.. code-block:: console
cargo check --manifest-path src/fswAlgorithms//myModule/Cargo.toml
git diff -- src/Cargo.lock
Review and commit both ``src/Cargo.toml`` and ``src/Cargo.lock`` with the new
module. Do not add a ``Cargo.lock`` inside the module directory. Cargo uses
the workspace lockfile, and module-local lockfiles are ignored.
Normal Basilisk and CI builds pass ``--locked`` to Cargo. A locked build fails
when ``Cargo.toml`` and ``src/Cargo.lock`` disagree instead of silently
selecting new dependency versions.
Integrated External Rust Modules
--------------------------------
Rust modules can remain in a separate source repository while participating
in an integrated Basilisk build through ``pathToExternalModules``. Place each
module directly under the external project's ``ExternalModules`` directory:
.. code-block:: text
External/
|-- Cargo.toml
|-- Cargo.lock
`-- ExternalModules/
`-- myRustModule/
|-- Cargo.toml
|-- build.rs
|-- myRustModule.rs
|-- myRustModule.rst
`-- _UnitTest/
`-- test_myRustModule.py
A Cargo workspace at ``External/Cargo.toml`` is recommended so all Rust
modules in that project share one dependency resolution and lockfile:
.. code-block:: toml
[workspace]
resolver = "2"
members = ["ExternalModules/myRustModule"]
[workspace.package]
rust-version = "1.89"
[profile.dev]
panic = "unwind"
[profile.release]
panic = "unwind"
An external module uses the same package marker, static-library crate type,
source layout, and ``build.rs`` as an in-tree module. Its dependencies must
point to the support crates in the Basilisk checkout used for the integrated
build. For example:
.. code-block:: toml
[dependencies]
bsk-messages = { path = "/path/to/basilisk/src/architecture/rust/bsk_messages" }
bsk-build = { path = "/path/to/basilisk/src/architecture/rust/bsk_build" }
[build-dependencies]
bsk-build = { path = "/path/to/basilisk/src/architecture/rust/bsk_build",
default-features = false, features = ["codegen"] }
Use paths relative to the external project when its location relative to the
Basilisk checkout is controlled; this keeps the project portable between
machines. Until the Basilisk Rust support crates are distributed separately,
the Cargo dependency paths couple the external module to that checkout.
Generate and commit the external workspace's lockfile, then enable both build
options:
.. code-block:: console
cargo generate-lockfile --manifest-path /path/to/External/Cargo.toml
python3 conanfile.py --clean --rustModules True \
--pathToExternalModules "/path/to/External"
The resulting module is imported from ``Basilisk.ExternalModules``. External
C payload headers under ``External/msgPayloadDefC`` are included in the normal
C-message generation step and are therefore available through
``bsk-messages``. Rerun the same ``conanfile.py`` command after adding or
editing one of those payloads.
For a project containing only one Rust module, the module may instead be an
independent Cargo package with its own committed ``Cargo.lock`` and no
workspace manifest at the external root. Basilisk discovers marked
``Cargo.toml`` files directly under ``ExternalModules/``. A shared
external workspace is preferred when the project contains multiple Rust
packages.
Write the Module
----------------
The following module has one Python-visible gain, one required input, one
output, and no private state:
.. code-block:: rust
use bsk_messages::*;
#[bsk_build::module]
#[repr(C)]
pub struct MyModuleConfig {
/// [Nm] Proportional gain
#[bsk(validate = validate_gain)]
pub K: f64,
/// [-] Attitude guidance input
pub attGuidInMsg: MsgReader,
/// [Nm] Commanded body torque output
pub cmdTorqueOutMsg: MsgWriter,
}
fn validate_gain(
_config: &MyModuleConfig,
proposed_gain: &f64,
) -> BskResult<()> {
if !proposed_gain.is_finite() || *proposed_gain <= 0.0 {
return Err(BskError::new("K must be finite and positive"));
}
Ok(())
}
impl BskModule for MyModuleConfig {
type State = ();
type Inputs = MyModuleInputs;
type Outputs = MyModuleOutputs;
fn init(&mut self, _state: &mut Self::State) -> BskResult<()> {
self.K = 1.0; // [Nm]
Ok(())
}
fn reset(
&mut self,
_state: &mut Self::State,
_context: &BskContext<'_>,
_current_sim_nanos: u64,
) -> BskResult {
// Retain reset-time validation for defaults, internal changes,
// and relationships involving multiple fields.
validate_gain(self, &self.K)?;
Ok(Self::Outputs::default())
}
fn update(
&mut self,
_state: &mut Self::State,
_context: &BskContext<'_>,
inputs: Self::Inputs,
_current_sim_nanos: u64,
) -> BskResult {
let guidance = inputs.attGuidInMsg;
Ok(MyModuleOutputs {
cmdTorqueOutMsg: Some(CmdTorqueBodyMsg {
torqueRequestBody: [
-self.K * guidance.sigma_BR[0],
-self.K * guidance.sigma_BR[1],
-self.K * guidance.sigma_BR[2],
],
}),
})
}
}
The Rust-specific declarations mean:
``use bsk_messages::*;``
Imports the Basilisk Rust message values and module support types used in
the file.
``#[repr(C)]``
Requests a C-compatible field layout for the Python-visible configuration.
The generated ABI copies complete typed values between the C++/SWIG wrapper
and the Rust-owned module.
``#[bsk_build::module]``
Identifies the one top-level module configuration, validates its supported
field types, and generates the lifecycle entry points.
``pub``
Makes a configuration field visible to ``cbindgen``. The generated wrapper
hides direct field access and exposes a Rust-backed Python property plus
``getX`` and ``setX`` methods.
``type State``, ``Inputs``, and ``Outputs``
Select the private state type and the named message-value structs used by
the lifecycle methods. The input and output types are generated from the
annotated ports. For ``MyModuleConfig`` they are named
``MyModuleInputs`` and ``MyModuleOutputs``.
``BskResult``
Represents either success, ``Ok(T)``, or an expected module failure,
``Err(BskError)``.
Configuration Getters, Setters, and Validation
----------------------------------------------
Every non-port configuration field receives generated getter and setter
methods. For a field named ``increment``, Python can use either the familiar
property or the explicit methods:
.. code-block:: python
module.increment = 2.0
value = module.increment
module.setIncrement(2.0)
value = module.getIncrement()
Both forms call the same guarded Rust accessor. Fixed-size arrays are copied
and validated as complete values. Assigning the wrong number of elements
raises ``BasiliskError`` without changing the field.
Use ``#[bsk(validate = function_name)]`` when an assignment requires immediate
validation. The named function receives the current configuration and a
borrowed proposed value:
.. code-block:: rust
#[bsk(validate = validate_gain)]
pub K: f64,
fn validate_gain(
_config: &MyModuleConfig,
proposed_gain: &f64,
) -> BskResult<()> {
if !proposed_gain.is_finite() || *proposed_gain <= 0.0 {
return Err(BskError::new("K must be finite and positive"));
}
Ok(())
}
The generated setter calls the validator before assignment. Returning
``Err(BskError)`` raises Python ``BasiliskError`` and preserves the previous
value. The first argument provides the current configuration when checks need
to involve other fields.
Module code can still update ``self.K`` directly inside Rust. Such internal
assignments do not call the external setter. Retain appropriate reset-time
validation for required values, relationships involving several fields, and
configuration that may be changed internally.
Data arriving through an input message also does not pass through a
configuration setter. Validate such data after reading it in ``reset`` or
``update``. For example, :ref:`mrpPDRust` validates its message-delivered
spacecraft inertia during reset.
Module Lifecycle
----------------
The generated wrapper provides ``SelfInit()``, ``Reset()``, and
``UpdateState()`` to Basilisk. The Rust implementation supplies these
``BskModule`` methods:
``init(state) -> BskResult<()>``
Runs once while Rust constructs the module, before Python configures it.
Override it to set non-zero configuration defaults or initial private
state. Rust first initializes every configuration and state field through
its ``Default`` implementation. The default ``init`` returns ``Ok(())``.
``Default`` provides the familiar initial values for primitive
configuration fields: numeric fields start at zero, booleans at ``false``,
arrays are initialized element-by-element, and message ports start empty.
Custom Rust-owned state uses its own ``Default`` implementation and may
therefore begin with non-zero values, allocated collections, strings, or
enum variants. Use ``init`` for any non-zero Python-visible configuration
defaults that should be applied before the user configures the module.
``reset(state, context, current_sim_nanos) -> BskResult``
Runs at simulation start and on every Basilisk ``Reset()``. Use it for
parameter validation and private-state reset. The default returns an
``Outputs`` value containing ``None`` for every port and therefore does not
publish during reset. Override it to return ``Some(payload)`` for any port
that requires an explicit reset value.
``update(state, context, inputs, current_sim_nanos) -> BskResult``
Runs on each task update. It receives message values through ``inputs`` and
selects output messages through ``outputs``. The generated wrapper performs
the actual message reads and writes. ``update`` has no default and must be
implemented.
If ``reset`` or ``update`` returns ``Err(BskError::new("..."))``, the wrapper
raises the normal Python ``BasiliskError`` after Rust returns. No output
message is written for the failed lifecycle call.
Unused Lifecycle Parameters
---------------------------
The ``BskModule`` trait fixes the lifecycle method signatures. Keep an unused
argument in the implementation and prefix its binding with an underscore to
tell the Rust compiler that it is intentionally unused:
.. code-block:: rust
fn reset(
&mut self,
_state: &mut Self::State,
_context: &BskContext<'_>,
_current_sim_nanos: u64,
) -> BskResult {
Ok(Self::Outputs::default())
}
Use a descriptive underscore-prefixed name instead of a bare ``_`` so the
argument's purpose remains clear. Remove the underscore if the implementation
later uses the value.
Message Ports and Values
------------------------
``bsk-messages`` provides a Rust value type for each built-in Basilisk message.
For example, the Rust ``AttGuidMsg`` corresponds to the familiar
``AttGuidMsgPayload`` data. A port uses one of two generic types:
.. important::
Rust ports use Basilisk's generated C message interfaces, such as
``AttGuidMsg_C``; Rust does not bind directly to the C++
``Message`` classes. This does not prevent C++ interoperability.
A Rust port can connect to a C or C++ module port with the same payload
type, just as existing C and C++ Basilisk modules connect to each other.
In Rust source, always declare ports with ``MsgReader`` or
``MsgWriter`` from ``bsk-messages``.
``MsgReader``
An input port that reads an ``AttGuidMsg`` value.
``MsgWriter``
An output port that writes a ``CmdTorqueBodyMsg`` value.
The port type declares its direction: ``MsgReader`` is an input and
``MsgWriter`` is an output. Required readers and all writers need no port
annotation. Add ``#[bsk(optional)]`` only when an input may be unconnected:
.. code-block:: rust
/// [-] Required attitude input
pub navAttInMsg: MsgReader,
/// [Nm] Optional disturbance estimate
#[bsk(optional)]
pub disturbanceInMsg: MsgReader,
/// [Nm] Commanded body torque
pub cmdTorqueOutMsg: MsgWriter,
A required input causes ``Reset`` or ``UpdateState`` to fail with
``BasiliskError`` when it is not connected. The generated input field for an
optional port has type ``Option``: it is ``Some(value)`` when connected
and ``None`` when unconnected.
The generated input and output structs use the same field names as the ports.
Any number of inputs and outputs is supported, and declaration order does not
control message routing. Each output field has type ``Option``:
``Some(payload)`` publishes that port and ``None`` leaves it unchanged for the
current lifecycle call.
Writing Output Messages
~~~~~~~~~~~~~~~~~~~~~~~
Rust module logic returns message payload values in its generated ``Outputs``
struct. It does not call ``MsgWriter`` or fill in a Basilisk message header
directly. Each returned field is matched by name to the corresponding
``MsgWriter`` configuration field. For example, the return value
.. code-block:: rust
Ok(RustModuleTemplateOutputs {
dataOutMsg: Some(data_out_msg),
dataOutMsgs: data_out_msgs.map(Some),
})
writes ``data_out_msg`` to ``dataOutMsg`` and each wrapped element of
``data_out_msgs`` to the same-index element of ``dataOutMsgs``. Return ``None``
for any individual field or array element that should not publish. Additional
output ports are handled by adding their named optional payloads to the same
generated output struct. ``Outputs::default()`` sets every field and array
element to ``None``, which is useful when a lifecycle call publishes nothing.
After ``reset`` or ``update`` returns ``Ok(...)``, the generated lifecycle
code publishes each selected ``Some(payload)`` output and completes its
message header automatically:
* ``moduleID`` is the unique ID assigned when the Python/C++ module wrapper is
constructed. Every output published by that module receives this ID.
* ``timeWritten`` is the ``current_sim_nanos`` value for that ``Reset`` or
``UpdateState`` call [ns]. All outputs published by one lifecycle call
receive the same timestamp.
* ``isWritten`` is set to true by the normal Basilisk C message write
interface.
Thus, these header values do not appear in the Rust ``Outputs`` value and the
module developer should not set them. ``SelfInit`` initializes the output
ports but does not publish payloads. A lifecycle call that returns ``Err(...)``
does not write any output. A successful call may publish all outputs, only a
subset, or none. A skipped port retains its previous payload and message-header
state.
Fixed-Size Arrays of Message Ports
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Use a Rust array when a module has a fixed number of ports with the same
message type. The array length is part of the module interface:
.. code-block:: rust
/// [-] Required navigation inputs
pub navAttInMsgs: [MsgReader; 2],
/// [-] Optional navigation inputs
#[bsk(optional)]
pub optionalNavAttInMsgs: [MsgReader; 2],
/// [Nm] Commanded body torques
pub cmdTorqueOutMsgs: [MsgWriter; 2],
The generated lifecycle value fields preserve the same length. In this
example, ``inputs.navAttInMsgs`` has type ``[NavAttMsg; 2]``,
``inputs.optionalNavAttInMsgs`` has type ``[Option; 2]``, and the
output field has type ``cmdTorqueOutMsgs: [Option; 2]``.
Every element of a required input array must be connected. An unconnected
required element reports its array index in the resulting ``BasiliskError``.
Process and return the arrays with normal Rust array operations:
.. code-block:: rust
let cmd_torques = inputs.navAttInMsgs.map(|navigation| {
CmdTorqueBodyMsg {
torqueRequestBody: [
-self.K * navigation.sigma_BN[0],
-self.K * navigation.sigma_BN[1],
-self.K * navigation.sigma_BN[2],
],
}
});
Ok(MyModuleOutputs {
cmdTorqueOutMsgs: cmd_torques.map(Some),
})
Construct the ``[Option; N]`` value with a literal array,
``core::array::from_fn``, or the array ``map`` method. Each element is
independent; for example, ``[Some(first_torque), None]`` publishes only array
element zero.
Python returns a list containing the fixed set of normal message interfaces:
.. code-block:: python
for input_port, source_message in zip(module.navAttInMsgs, source_messages):
input_port.subscribeTo(source_message)
output_recorders = [
output_port.recorder() for output_port in module.cmdTorqueOutMsgs
]
Each list element is a live port, so calling ``subscribeTo()`` or
``recorder()`` affects the module directly. The property cannot be assigned.
Adding, removing, or replacing entries in the returned Python list only
changes that temporary list, not the module's fixed set of ports.
An optional input array can represent a variable number of connected inputs
up to a fixed maximum, as a C module commonly does with a fixed-capacity
message array. Its lifecycle value is an array of ``Option`` values, so
the Rust implementation can process the connected ``Some`` entries and skip
the unconnected ``None`` entries.
Only fixed-size message-port arrays are supported in the Python-visible
configuration. A Rust ``Vec>`` or ``Vec>`` does not
have a stable C layout and is rejected. The dynamically sized C++
``std::vector`` message pattern does not yet have a Rust-module equivalent.
Adding or Changing a Basilisk Message
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
After adding or editing a Basilisk message payload, rerun the build with Rust
module support enabled:
.. code-block:: console
python conanfile.py --rustModules True
This first regenerates the normal C message interfaces and then generates the
matching Rust bindings in Cargo's build directory. No separate Rust generator
must be run, and the generated bindings are not committed.
When Rust modules are disabled, the Rust binding generator does not run.
Python-Visible Configuration
----------------------------
The marked configuration struct is the equivalent of a C module config struct
or the public fields of a C++ module. It can contain:
* scalar integer, floating-point, and Boolean parameters;
* fixed-size arrays such as ``[f64; 3]`` and ``[[f64; 3]; 3]``;
* nested, by-value ``#[repr(C)]`` parameter structs that derive
``bsk_build::BskConfigValue``; and
* ``MsgReader`` and ``MsgWriter`` ports, individually or in fixed-size
arrays.
Every configuration field must implement Rust's ``Default`` behavior because
Rust constructs the complete module before calling ``init``. Every non-port
field must also implement ``BskConfigValue``. This safety contract restricts
the generated raw-copy boundary to types with matching Rust and C++ layouts,
valid bit patterns, and no Rust ownership or borrowed references.
Basilisk provides ``BskConfigValue`` for Boolean, fixed-width integer, and
floating-point scalars and recursively for fixed-size arrays. A type alias for
one of these types inherits the implementation. Module-defined nested structs
must use plain ``#[repr(C)]`` and derive ``Clone``, ``Copy``, ``Default``, and
``bsk_build::BskConfigValue``. The derive verifies that the struct and all of
its fields satisfy the same boundary contract.
Configuration fields cannot contain raw pointers, Rust enums, dynamically
sized strings, ``Vec`` collections, references, characters, or other owning
Rust types. These types do not have a safe, general Python/C representation.
Put them in private Rust state instead. Do not manually implement the unsafe
``BskConfigValue`` trait merely to bypass a compiler error; a manual
implementation assumes responsibility for every cross-language layout and
bit-validity guarantee enforced by the derive.
Nested parameters can be grouped by value:
.. code-block:: rust
#[repr(C)]
#[derive(Clone, Copy, Default, bsk_build::BskConfigValue)]
pub struct ControllerGains {
/// [Nm] Proportional gain
pub K: f64,
/// [Nm/(rad/s)] Rate gain
pub P: f64,
}
#[bsk_build::module]
#[repr(C)]
pub struct MyModuleConfig {
/// [-] Controller parameters
pub gains: ControllerGains,
// Message ports follow.
}
The getter returns a copy of a nested value. Modify that copy and assign the
complete value through the setter:
.. code-block:: python
gains = module.gains
gains.K = 0.1 # [Nm]
module.gains = gains
Fixed-size arrays map to normal C arrays and appear as Python lists:
.. code-block:: rust
/// [Nm] Maximum torque on each body axis
pub maxTorques: [f64; 3],
/// [-] Direction cosine matrix
pub dcm_BR: [[f64; 3]; 3],
.. code-block:: python
module.maxTorques = [0.001, 0.001, 0.001] # [Nm]
module.dcm_BR = [[1.0, 0.0, 0.0], # [-]
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0]]
Reading an array field from Python returns a copy. Reassign the list after
changing an element:
.. code-block:: python
values = module.maxTorques
values[0] = 0.005 # [Nm]
module.maxTorques = values
The :ref:`rustModuleTemplate` module exercises both a nested configuration
struct and a multidimensional fixed-size array through the complete generated
Rust, C++, SWIG, and Python interface.
Rust-Owned Private State
------------------------
``BskModule::State`` stores implementation details that must persist between
lifecycle calls but should not appear in Python. Unlike the configuration,
this state never crosses the C interface and can use normal safe Rust types
such as ``Vec``, ``String``, enums, and smart pointers:
.. code-block:: rust
#[derive(Default)]
pub struct MyState {
history: Vec,
status: String,
mode: InternalMode,
}
#[derive(Default)]
enum InternalMode {
#[default]
Idle,
Running,
}
impl BskModule for MyModuleConfig {
type State = MyState;
type Inputs = MyModuleInputs;
type Outputs = MyModuleOutputs;
fn update(
&mut self,
state: &mut Self::State,
_context: &BskContext<'_>,
_inputs: Self::Inputs,
_current_sim_nanos: u64,
) -> BskResult {
state.history.push(self.K);
state.status = String::from("updated");
state.mode = InternalMode::Running;
Ok(Self::Outputs::default())
}
}
Use ``type State = ();`` when the module is stateless. Rust allocates and
destroys the complete module instance, so ordinary Rust cleanup releases
private state automatically.
Basilisk C Utilities
--------------------
The ``bsk-utilities`` package provides safe Rust wrappers around selected
Basilisk utilities that already have a C interface. An in-tree module enables
the shared workspace dependency in ``Cargo.toml``:
.. code-block:: toml
[dependencies]
bsk-utilities.workspace = true
[dev-dependencies]
bsk-utilities = { workspace = true, features = ["ffi-tests"] }
Import and call the wrappers with ordinary Rust values:
.. code-block:: rust
use bsk_utilities::attitude;
let sigma = attitude::dcm_to_mrp(dcm); // [-]
let wrapped_angle = attitude::wrap_to_pi(angle); // [rad]
The safe wrappers convert fixed-size Rust arrays to the pointers expected by
the existing C functions. The Basilisk build links their implementation from
``ArchitectureUtilities``. The ``ffi-tests`` development feature compiles the
same C sources into Rust test executables, allowing ``cargo test`` to execute
utility-backed module code without a configured CMake build. Module code
should not call functions in the generated ``bsk_utilities::raw`` module
directly. See
:ref:`rustModuleTemplate` for an in-tree module that calls a safe utility
wrapper from its update method.
Linear Algebra with ``nalgebra``
--------------------------------
Basilisk's Rust workspace provides
`nalgebra `__ for vector and matrix operations. In-tree
modules select the shared, reviewed version with
``nalgebra.workspace = true`` when they need linear algebra:
.. code-block:: toml
[dependencies]
nalgebra.workspace = true
This is the Rust equivalent of using Eigen in a C++ Basilisk module. Common
fixed-size types include ``Vector3`` and ``Matrix3``:
.. code-block:: rust
use nalgebra::{Matrix3, Vector3};
let inertia = Matrix3::from_row_slice(&vehicle_config.ISCPntB_B); // [kg*m^2]
let omega = Vector3::from_column_slice(&guidance.omega_BR_B); // [rad/s]
let angular_momentum = inertia * omega; // [N*m*s]
Keep ``nalgebra`` types inside lifecycle methods or Rust-owned private state.
Python-visible configuration and Basilisk C message payloads must retain their
C-compatible array types. Convert an array to a matrix with
``Matrix3::from_row_slice`` because Basilisk stores matrix payload fields in
row-major order. Convert a result back to an array when constructing an output
message:
.. code-block:: rust
let torque: Vector3 = calculate_torque(); // [N*m]
let output = CmdTorqueBodyMsg {
torqueRequestBody: [torque[0], torque[1], torque[2]],
};
See :ref:`mrpPDRust` for a complete controller that uses ``nalgebra`` while
keeping its generated C/Python interface unchanged.
Simulation Time
---------------
Basilisk passes the current simulation time to ``reset()`` and ``update()`` as
the ``u64`` value ``current_sim_nanos`` in nanoseconds. Convert an absolute
timestamp to seconds with ``NANO2SEC``:
.. code-block:: rust
use bsk_messages::BskError;
use bsk_utilities::constants::NANO2SEC;
let absolute_time_sec = current_sim_nanos as f64 * NANO2SEC; // [s]
This conversion remains finite during simulations longer than
:math:`2^{53}` nanoseconds, approximately 104 days, although an ``f64`` can no
longer preserve every individual nanosecond at that scale.
For a relative time, subtract the integer timestamps before converting. Use
``checked_sub()`` when the stored timestamp is expected to precede the current
time:
.. code-block:: rust
let elapsed_nanos = current_sim_nanos
.checked_sub(previous_sim_nanos)
.ok_or_else(|| BskError::new("simulation time moved backwards"))?;
let elapsed_time_sec = elapsed_nanos as f64 * NANO2SEC; // [s]
Performing the subtraction first retains the precision of a small elapsed
interval even when the absolute simulation time is large. Store the previous
timestamp as a ``u64`` and update it only after the elapsed-time calculation.
Runtime Context and Logging
---------------------------
``BskContext`` gives each lifecycle call a temporary view of Basilisk runtime
information and services:
* ``module_id()`` returns the module identifier;
* ``model_tag()`` returns the Python-visible model tag;
* ``call_counts()`` returns the update-call count;
* ``rng_seed()`` returns the module random-number seed; and
* ``logger()`` returns the Basilisk logger.
The context is borrowed only for the current call. Do not store references
obtained from it in the configuration or private state.
Use the logger for nonfatal diagnostics:
.. code-block:: rust
context.logger().debug("Starting update.");
context.logger().info("Module parameters are valid.");
context.logger().warning("Using a fallback input.");
For an expected failure that must stop the lifecycle call, return an error:
.. code-block:: rust
if self.K <= 0.0 {
return Err(BskError::new("K must be positive"));
}
Do not deliberately use a Rust panic for validation or normal error control.
The generated boundary catches unexpected panics so they do not unwind into
C++. A caught panic becomes ``BasiliskError`` and poisons that module instance
because its private state may be partially updated. Later lifecycle calls on
that instance fail, but Rust can still destroy it safely. An expected
``BskError`` does not poison the module.
Use the Module from Python
--------------------------
The generated wrapper is imported, configured, connected, and scheduled like
another compiled module:
.. code-block:: python
from Basilisk.fswAlgorithms import myModule
module = myModule.myModule()
module.ModelTag = "myRustModule"
module.K = 0.25 # [Nm]
module.attGuidInMsg.subscribeTo(attitudeGuidanceMessage)
simulation.AddModelToTask("taskName", module)
The Python object exposes generated properties for the marked configuration
fields along with the normal ``SysModel`` fields. Each property uses its Rust
getter or setter. The generated ``MyModuleConfig`` proxy is an implementation
detail and should not be constructed separately.
Document the Module
-------------------
Keep the user-facing description in ``myModule.rst`` and follow the same
documentation structure used by C and C++ modules. Use the
``.. bsk-module-io::`` directive for the message diagram and table. The
:ref:`rustModuleTemplate` documentation provides a complete example.
Rust uses ``///`` for documentation attached to the following field, struct,
or function. It uses ``//!`` for documentation about the containing source
module. Follow Basilisk's unit-first convention:
.. code-block:: rust
/// [m] Inertial position vector
pub position: [f64; 3],
/// [m/s] Inertial velocity vector
pub velocity: [f64; 3],
/// [-] Dimensionless gain
pub gain: f64,
``cbindgen`` copies these field comments into the generated C header, and the
normal Doxygen/Breathe pipeline includes the generated module configuration
API in the Sphinx page.
Build Basilisk with Rust support before generating documentation:
.. code-block:: console
python3 conanfile.py --rustModules True
cd docs
make html
The generated HTML is stored under ``docs/build/html``. The Rust API section
is omitted when its generated header is unavailable.
Test the Module
---------------
Every in-tree Rust module must have the normal Basilisk Python unit test. Add
Rust-native tests when they provide useful direct coverage of private state,
validation, numerical helpers, or other internal Rust behavior.
Rust-Native Tests
~~~~~~~~~~~~~~~~~
Place ``#[cfg(test)]`` tests in ``myModule.rs`` or in the package's Rust test
layout. These tests do not link the compiled Basilisk library, so that library
does not need to be built with ``--rustModules True``. However,
``bsk-messages`` generates its Rust bindings from the normal C-message headers
under ``dist3/autoSource/cMsgCInterface``. Configure Basilisk at least once to
generate those headers; either a normal or Rust-enabled configuration works:
.. code-block:: console
python3 conanfile.py
Then run the Rust-native tests from the module directory:
.. code-block:: console
cargo test --locked
The module attribute omits generated C lifecycle entry points during
``cargo test``, so these tests can call ``init``, ``reset``, and ``update``
without linking Basilisk.
To test code that calls ``context.logger()``, add the test logger feature:
.. code-block:: toml
[dev-dependencies]
bsk-build = { path = "../../../architecture/rust/bsk_build",
features = ["test_logger"] }
Rust-native logger calls then print to ``stderr`` instead of calling Basilisk
C symbols. Use ``BskModuleRuntime::for_testing()`` and
``BskContext::for_testing()`` to create a context, as demonstrated by
``rustModuleTemplate.rs``.
Python Unit Test
~~~~~~~~~~~~~~~~
Add ``_UnitTest/test_myModule.py`` to validate the public Python attributes,
message connections, lifecycle behavior, and numerical results:
.. code-block:: console
python3 -m pytest src/fswAlgorithms//myModule/_UnitTest -v
Guard the test before importing its generated wrapper. This skips the test in
a Rust-disabled Basilisk build, while still reporting a missing wrapper as a
failure when Rust support was requested:
.. code-block:: python
import pytest
from Basilisk import hasBuildFeature
rustModulesEnabled = hasBuildFeature("rustModules")
pytestmark = pytest.mark.skipif(
not rustModulesEnabled,
reason="Requires Basilisk built with --rustModules True",
)
if rustModulesEnabled:
from Basilisk.fswAlgorithms import myModule
After a Rust-enabled Basilisk build, the normal ``python3 run_all_test.py``
workflow includes the module's Python test. The same command also runs all
Cargo workspace tests when ``cargo`` is installed and available on ``PATH``.
It reports that Rust tests were skipped, without failing, when Rust support is
disabled or ``cargo`` is not available. The Cargo tests exercise the Rust
packages directly without linking the generated Basilisk module library, but
they still require the C-message headers produced by the Rust-enabled
Basilisk configuration performed before ``run_all_test.py``.
Contributor Checks
~~~~~~~~~~~~~~~~~~
Before submitting a Rust module, run the complete workspace checks from the
repository root:
.. code-block:: console
cargo fmt --all --check --manifest-path src/Cargo.toml
cargo test --workspace --all-features --locked --manifest-path src/Cargo.toml
cargo test -p bsk-build --no-default-features --locked --manifest-path src/Cargo.toml
cargo clippy --workspace --all-targets --all-features --locked \
--manifest-path src/Cargo.toml -- -D warnings
The repository's local pre-commit hook runs the same Rust formatting check
whenever Rust source is changed. The local hook reports a skip instead of
failing when Cargo or ``rustfmt`` is not installed, keeping Rust optional for
non-Rust development. Pull-request CI installs and always runs the formatter,
checks the minimum supported Rust version, and verifies that generated
language boundaries retain panic containment. Follow the
:ref:`bskModuleCheckoutList` for the remaining module review requirements.
Current Limitations
-------------------
* Rust module support is experimental, and its generated interface may change
between Basilisk releases.
* Python-visible configuration is limited to supported C-compatible types.
Put dynamic Rust data in ``BskModule::State``.
* Arbitrary Rust methods are not exported to Python. Configuration fields
receive generated getters, setters, and properties; other public methods
remain the standard Basilisk lifecycle and message interfaces.
* The Rust utility bindings cover C-compatible Basilisk utilities and
constants. C++-only utilities require a C-compatible wrapper before Rust can
use them; use ``nalgebra`` for native Rust vector and matrix operations.
* After adding or changing a message payload, rerun
``python3 conanfile.py --rustModules True`` to regenerate the matching Rust
bindings in the build directory.
Reference
---------
The generated ABI is described in
:download:`bsk_rust_module.h <../../../../src/architecture/_GeneralModuleFiles/bsk_rust_module.h>`.
The Rust runtime and generation API are documented in
``src/architecture/rust/bsk_build/src/lib.rs``. For a working implementation,
configuration, Python test, and Rust-native tests, see
:ref:`rustModuleTemplate`.