Skip to content

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.

A block is a package you publish, not a folder inside one project:

Terminal window
$ 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 describes

The 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:

Terminal window
$ cd noise && magnet check
Checking host
ok 1 model (Noise), 0 functions

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.

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"

A project can consume the block in three ways:

  1. Copy it into blocks/. Every blocks/<name>/ directory is a package named <name>, with no manifest entry needed.
  2. Name it as a path dependency, noise = { path = "../noise" }. An explicit entry overrides a block in blocks/ of the same name.
  3. 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:

Terminal window
$ magnet simulate --dt 0.1 --ticks 3 --param seed=7.0
tick,t,y
0,0,0.00004406513181609687
1,0.1,0.010952103303501408
2,0.2,0.09038964072018621

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.

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.

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: Noise
impl Model for Noise<Vector<f64, 3>> { … } // v: Noise<[f64; 3]>

The declaration keeps the rest of the language checked. The checker types every call and the scheduler proves causality. Three consequences follow:

  1. An extern item lives in a named package. The root package is the generated crate itself and has no crate name to import from.
  2. inline cannot flatten an extern block, since there is no body to splice. A loop through one is broken with an explicit fby, as described in Causality, delays, and feedback.
  3. Mismatches surface as cargo errors. If NoiseParameters disagrees with the header, rustc reports it at magnet build, which exits 1.

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.