Custom Rust blocks
Some blocks cannot be written in magscript. A pseudo-random source needs wrapping integer arithmetic, and a Kalman filter may already exist in Rust. For these you write the Rust and declare its signature in magscript, the way a C header declares a function.
This guide builds a pseudo-random Noise block and uses it from a project made in
the quickstart.
Scaffold a block
Section titled “Scaffold a block”A block is a package you publish, not a folder inside one project:
$ magnet new --rust noise Created noise
Use it from a project, either way:
blocks/noise/ copy or submodule it in noise = { path = "../noise" } in Magnet.toml [dependencies]
Then in your package's own module (`src/<package>.mag`):
use noise::noise::Noise; model <Package> { …, g: Noise, } step <Package>(u: f64) -> (y: f64) { y = self.g(u); } params <Package>() { Self { …, g: Noise(2.0) } }noise/├── .git/├── .gitignore├── Cargo.toml the crate, with the magnetite runtime already resolved└── src/ ├── lib.rs `pub mod noise;` ├── noise.mag the header Magnetite reads └── noise.rs the Rust it describesThe header and the Rust are one module in two files, so a consumer imports the block as
noise::noise::Noise: package, module, model. The scaffold is a working gain block, and
magnet check type-checks its header:
$ cd noise && magnet check Checking host ok 1 model (Noise), 0 functionsThe header
Section titled “The header”Replace src/noise.mag:
model Noise { seed: f64,}
extern step Noise() -> (y: f64);extern marks the step and never the model. The header leaves out only the body, since
Magnetite still needs every field name and type to:
- check the constructor;
- drive the parameter panel;
- build the parameters struct.
A second block is another model and extern step pair in its own .mag file, with its own .rs
beside it and one more pub mod line in src/lib.rs, so one crate can export a whole library.
The Rust
Section titled “The Rust”Replace src/noise.rs. The one requirement is to implement Model:
use magnetite::prelude::{Ctx, Model};
#[repr(C)]pub struct NoiseParameters { pub seed: f64,}
pub struct Noise { state: u32,}
impl Model for Noise { type Parameters = NoiseParameters; type Inputs = (); type Outputs = f64;
fn init(params: &Self::Parameters) -> Self { Self { state: params.seed as u32 | 1 } }
fn step(&mut self, _ctx: &Ctx, _params: &Self::Parameters, _inputs: Self::Inputs) -> Self::Outputs { // xorshift32: wrapping integer arithmetic magscript has no syntax for. self.state ^= self.state << 13; self.state ^= self.state >> 17; self.state ^= self.state << 5; f64::from(self.state) / f64::from(u32::MAX) }}NoiseParameters must match the header’s fields by name, because Magnetite writes the values a
constructor computes, and any --param routed into one, into it. #[repr(C)] keeps its layout stable for calibration.
Cargo.toml needs no edits. It depends on the public magnetite runtime from crates.io, the same
line the generated crate carries, so cargo sees one copy of the Model trait on both sides:
[dependencies]magnetite = "0.2"Use the block
Section titled “Use the block”A project can consume the block in three ways:
- Copy it into
blocks/. Everyblocks/<name>/directory is a package named<name>, with no manifest entry needed. - Name it as a path dependency,
noise = { path = "../noise" }. An explicit entry overrides a block inblocks/of the same name. - Name its repository,
noise = { git = "…", tag = "v1.0" }, exactly as for a magscript library.
Then import and step it like any model:
use noise::noise::Noise;
model Noisy { amplitude: f64, n: Noise,}
step Noisy(u: f64) -> (y: f64) { let jitter: f64 = self.n(); y = u + self.amplitude * jitter;}
params Noisy(seed: f64 = 2024.0) { Self { amplitude: 0.1, n: Noise(seed) }}A --param sets an argument of the entry’s constructor, and Noisy routes seed into its Noise
call, so the seed is one flag away:
$ magnet simulate --dt 0.1 --ticks 3 --param seed=7.0tick,t,y0,0,0.000044065131816096871,0.1,0.0109521033035014082,0.2,0.09038964072018621In the editor
Section titled “In the editor”Every package a project depends on is a group in the block library’s Extensions section. Each
model and step pair in it is a block, with ports from the step’s signature and parameters from
the model’s fields. There is no descriptor file to maintain.
The Extensions section also manages dependencies. It removes a package row, and adds any of these in one click:
- a repository;
- a local path;
- the reference library.
magnet add and magnet remove do the same from a terminal.
Probe the interface
Section titled “Probe the interface”An extern step has no body, so @probe may record its inputs and outputs and nothing else:
@probe(u, y)extern step Noise(u: f64) -> (y: f64);The caller builds the telemetry at the call site, so the columns arrive as probe.<instance>.u
and probe.<instance>.y without any change to the block’s crate.
Generic blocks
Section titled “Generic blocks”A Rust block can leave a signal type open, as a generic model does:
model Noise<T = f64> { scale: T,}
extern step Noise(u: T) -> (y: T);Your crate declares the same defaults, pub struct Noise<T = f64>, and writes one impl Model
per argument a caller binds:
impl Model for Noise { … } // n: Noiseimpl Model for Noise<Vector<f64, 3>> { … } // v: Noise<[f64; 3]>What the boundary costs
Section titled “What the boundary costs”The declaration keeps the rest of the language checked. The checker types every call and the scheduler proves causality. Three consequences follow:
- An
externitem lives in a named package. The root package is the generated crate itself and has no crate name to import from. inlinecannot flatten an extern block, since there is no body to splice. A loop through one is broken with an explicitfby, as described in Causality, delays, and feedback.- Mismatches surface as cargo errors. If
NoiseParametersdisagrees with the header, rustc reports it atmagnet build, which exits 1.
Share it
Section titled “Share it”Blocks are shared by repository. There is no Magnetite registry for customer code, so your blocks
stay in your own infrastructure. The one crate published centrally is the MIT-licensed
magnetite runtime, which every block depends on.
magnetite-ext is the recommended reference
library of small, general blocks such as pseudo-random sources. The Extensions section adds it
pinned to its current release. To add it by hand, pick a tag from the repository and
depend on it like any other.