Writing a kernelalpha
Turn a Rust crate into a native kernel for a mops package, test it against its Motoko body, and publish it.
In this tutorial you give a Motoko CRC-32 function a native kernel built from the crc32fast crate. You build the kernel reproducibly, use it from an application, check that it gives the same answers as the Motoko code on hundreds of inputs, and measure the difference: 18 million instructions for a megabyte instead of 246 million.
Prerequisites:
- moxzi and mops
- rustup with the toolchain kernels build with:
rustup toolchain install nightly-2026-07-25 --profile minimal --component rust-src - Node.js, for the comparison at the end
The finished example has two directories: the package, crc32, and an application that uses it, app.
crc32/
mops.toml the package, with its [kernel] table
README.md mops publish requires one
src/Crc32.mo the Motoko function and its body
rust/ the kernel's crate
kernel/ the built kernel (generated)
app/
mops.toml
main.mo
moxzi-kernel/ the kernel SDK (generated)
1. Write the Motoko function#
Start with an ordinary Motoko function that does the job. Its body is what runs under moc, and in every application that doesn't opt into the kernel:
/// The CRC-32 of `data` (the IEEE polynomial that zlib, gzip and PNG use), as 4 big-endian
/// bytes. Runs the `crc32` native kernel when the application opts in; otherwise this body.
public func digest(data : Blob) : Blob {
var crc : Nat32 = 0xFFFFFFFF;
for (byte in data.vals()) {
crc := table[Nat32.toNat((crc ^ Nat8.toNat32(byte)) & 0xFF)] ^ (crc >> 8);
};
crc ^= 0xFFFFFFFF;
Blob.fromArray([
Nat32.toNat8(crc >> 24),
Nat32.toNat8((crc >> 16) & 0xFF),
Nat32.toNat8((crc >> 8) & 0xFF),
Nat32.toNat8(crc & 0xFF),
])
};
Keep this body correct and simple. It's also your test oracle: the kernel must return exactly what it returns, for every input. The file's 256-entry table is at the bottom of the file.
A function a kernel can replace sits at the top level of its module, takes plain named parameters, and uses only these types: Blob, Text, Nat64 and Bool for arguments; Bool, Blob, ?Blob, Text and ?Text for the result. See the Kernel ABI for the details.
2. Write the kernel crate#
A kernel is a no_std Rust static library that exports one C function per Motoko function:
[package]
name = "crc32-kernel"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
[lib]
crate-type = ["staticlib"]
[dependencies]
moxzi-kernel = { path = "../../moxzi-kernel" }
crc32fast = { version = "=1.5.3", default-features = false }
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"
#![no_std]
use moxzi_kernel::bytes;
/// `Crc32.digest : Blob -> Blob`, a 4-byte result: `out` has room for exactly 4 bytes.
#[no_mangle]
pub unsafe extern "C" fn crc32(data: *const u8, len: usize, out: *mut u8) {
let crc = crc32fast::hash(bytes(data, len));
core::ptr::copy_nonoverlapping(crc.to_be_bytes().as_ptr(), out, 4);
}
The Blob argument arrives as a pointer and a length. The result is a Blob of exactly 4 bytes, so the function takes one more parameter, out, with room for those 4 bytes, and writes them. A result of varying length would instead be returned with the SDK's output.
The crate follows the rules every kernel follows:
crate-type = ["staticlib"]and#![no_std].- The SDK as a path dependency,
moxzi-kernel = { path = "../../moxzi-kernel" }. It gives the crate an allocator, a panic handler that traps the message, and thebytes,output,textandtraphelpers. - Each export is a
#[no_mangle] pub extern "C" fnorpub unsafe extern "C" fninsrc/lib.rs. moxzi exports every such function in that file, and only those. - Dependencies without
std, usually throughdefault-features = false.
Pin each dependency to an exact version (=1.5.3). An update then shows up as a change to Cargo.toml that you review.
3. Lock the dependencies#
moxzi kernel build builds with --locked, so the crate needs a Cargo.lock. Write the SDK where the path dependency expects it, then generate the lock:
moxzi kernel sdk moxzi-kernel
cargo +nightly-2026-07-25 generate-lockfile --manifest-path crc32/rust/Cargo.toml
moxzi-kernel: the moxzi-kernel SDK (sources sha256:e055dee34c8e2dd6662fe25e7cb7b7113b648a127122d25a0197d26cc1da3529)
Updating crates.io index
Locking 3 packages to latest compatible versions
Review the lock file and commit it. It decides every crate version in the kernel.
4. Build the kernel#
moxzi kernel build crc32/rust -o crc32/kernel/crc32.kernel.toml
crc32/kernel/crc32.kernel.toml: 22815 B, sha256:6ccd7b5b0125b20c95e23cea3fcb3c8f9a9cddc41ed50749f034c3bd8fc73c06
The kernel file holds the wasm in base64, with its size and SHA-256. The same crate and Cargo.lock build to the same bytes on every machine, so anyone can check that a kernel file came from your source. To make that possible, the build:
- uses exactly
nightly-2026-07-25and its linker, checked by commit; - builds in
/tmp/moxzi-kernel-build, one build at a time, against the SDK moxzi carries; - ignores
RUSTFLAGS,RUSTC_WRAPPERand the other cargo and rustc settings in your environment, and names each one it ignores. To pass a--cfgto a dependency, list it inCargo.tomlunder[package.metadata.moxzi-kernel] cfg = ['name="value"'].
5. Declare the kernel in the package#
The package's mops.toml names the kernel file and maps each Motoko function to its export:
[package]
name = "crc32"
version = "0.1.0"
description = "CRC-32 with a native moxzi kernel; the Motoko body runs under moc or without the opt-in."
license = "Apache-2.0"
files = ["src/**/*.mo", "kernel/*.kernel.toml"]
[dependencies]
core = "2.6.0"
[kernel]
abi = 1
file = "kernel/crc32.kernel.toml"
[kernel.functions]
"Crc32.digest" = { export = "crc32", args = ["Blob"], result = "Blob", result_bytes = 4 }
"Crc32.digest" is the function digest in src/Crc32.mo. args and result must match the Motoko declaration. Every application that opts in checks them: a key that names no module or no function, or types that don't match, stop its build with error M0291. See the Motoko function. files lists the kernel file: without it, mops publishes only the .mo files, mops.toml, README.md and licence files.
Check that the file is what the crate builds to:
moxzi kernel verify crc32 --crate crc32/rust
MATCH kernel/crc32.kernel.toml (the package's kernel): sha256:6ccd7b5b0125b20c95e23cea3fcb3c8f9a9cddc41ed50749f034c3bd8fc73c06
MATCH crc32: rebuilt sha256:6ccd7b5b0125b20c95e23cea3fcb3c8f9a9cddc41ed50749f034c3bd8fc73c06 (22815 B)
kernel verify: every pin MATCHES the rebuilt bytes
Anyone with your source can run the same check, which is what lets an application trust a pin.
6. Use it from an application#
The application depends on the package and calls Crc32.digest:
[dependencies]
core = "2.6.0"
crc32 = "../crc32"
import Array "mo:core/Array";
import Blob "mo:core/Blob";
import IC "mo:core/InternetComputer";
import Nat8 "mo:core/Nat8";
import Crc32 "mo:crc32/Crc32";
actor {
// The CRC-32 of `data`.
public query func crc(data : Blob) : async Blob { Crc32.digest(data) };
// The CRC-32 of `size` bytes, and the instructions it took.
public func measure(size : Nat) : async { crc : Blob; instructions : Nat64 } {
let data = Blob.fromArray(Array.tabulate<Nat8>(size, func i = Nat8.fromNat(i % 256)));
var crc : Blob = "";
let instructions = IC.countInstructions(func() { crc := Crc32.digest(data) });
{ crc; instructions }
};
};
Install the dependencies and opt into the kernel:
cd app
mops install
moxzi kernel add crc32
kernel crc32 0.1.0
package ./crc32
file kernel/crc32.kernel.toml (its sha256 matches its bytes)
sha256 6ccd7b5b0125b20c95e23cea3fcb3c8f9a9cddc41ed50749f034c3bd8fc73c06
abi 1
size 22815 B
source (not declared)
commit (not declared)
functions (1):
Crc32.digest -> crc32
rebuilt no: `moxzi kernel verify ./crc32` rebuilds it from its source and compares
third-party: moxzi does not build or review it
A native kernel is compiled code that runs inside your canister and shares its memory.
It can read and write anything in the canister's heap -- every user's data, keys, and
the state that survives upgrades -- and return what it read. The linker keeps the
system API away from it (it cannot send messages, spend cycles or set timers) and the
glue checks what it returns, but nothing checks what it does to memory. The sha256 pin
guarantees which bytes run, not that they are correct or benign. Accept a kernel only
if you trust its author or have reviewed its source at the commit it names, and take
a canister snapshot before deploying a new one.
./mops.toml: added [kernels] crc32 = "sha256:6ccd7b5b0125b20c95e23cea3fcb3c8f9a9cddc41ed50749f034c3bd8fc73c06"
The first build that links it asks you to accept these bytes (or pass --accept-kernel crc32=sha256:6ccd7b5b0125b20c95e23cea3fcb3c8f9a9cddc41ed50749f034c3bd8fc73c06).
The kernel is yours, so to moxzi it's third-party. The first build shows what you're about to link and stops:
moxzi build main.mo -o kernel.wasm
warning: native kernel crc32 is third-party: moxzi does not build or review it
package crc32 0.1.0
sha256 6ccd7b5b0125b20c95e23cea3fcb3c8f9a9cddc41ed50749f034c3bd8fc73c06 (your [kernels] pin)
source (not declared)
commit (not declared)
size 22815 B, 1 function(s)
A native kernel is compiled code that runs inside your canister and shares its memory.
It can read and write anything in the canister's heap -- every user's data, keys, and
the state that survives upgrades -- and return what it read. The linker keeps the
system API away from it (it cannot send messages, spend cycles or set timers) and the
glue checks what it returns, but nothing checks what it does to memory. The sha256 pin
guarantees which bytes run, not that they are correct or benign. Accept a kernel only
if you trust its author or have reviewed its source at the commit it names, and take
a canister snapshot before deploying a new one.
Error: this build links 1 third-party native kernel(s) that this machine has not accepted (see the warning above). Nothing was built. If you trust them, rerun with
--accept-kernel crc32=sha256:6ccd7b5b0125b20c95e23cea3fcb3c8f9a9cddc41ed50749f034c3bd8fc73c06
The acceptance is recorded in ./accepted-kernels, so later builds of the same bytes do not ask again.
Accept those exact bytes. At a terminal you can type yes at the prompt; this passes the flag the error printed, as a script would:
moxzi build main.mo -o kernel.wasm --accept-kernel crc32=sha256:6ccd7b5b0125b20c95e23cea3fcb3c8f9a9cddc41ed50749f034c3bd8fc73c06
accepted native kernel crc32 sha256:6ccd7b5b0125b20c95e23cea3fcb3c8f9a9cddc41ed50749f034c3bd8fc73c06; recorded in ./accepted-kernels (builds of these bytes on this machine will not ask again; a new version will)
kernel: crc32 sha256:6ccd7b5b0125b20c (22815 B, 1 function(s)) [third-party, accepted on this machine]
main.mo -> kernel.wasm + kernel.did (281542 bytes, 18 file(s), 155 steps, 2.25s)
compiled by ~/.local/lib/moxzi/compiler.wasm (sha256 9294817c6e9069cbd0e712d0e9dde0ec1b6893d0e7b42d798fa3dd4c8cd772cb), moxzi 0.1.0-alpha.10
Later builds of the same bytes on this machine don't ask again.
7. Test the kernel against the Motoko body#
Build the application again without the opt-in. Delete the [kernels] line from app/mops.toml, then:
moxzi build main.mo -o motoko.wasm
note: package crc32 provides a native kernel; it runs its Motoko code unless you opt in under [kernels] in mops.toml (`moxzi kernel add crc32`)
main.mo -> motoko.wasm + motoko.did (264580 bytes, 18 file(s), 155 steps, 2.72s)
compiled by ~/.local/lib/moxzi/compiler.wasm (sha256 9294817c6e9069cbd0e712d0e9dde0ec1b6893d0e7b42d798fa3dd4c8cd772cb), moxzi 0.1.0-alpha.10
Now install both builds on a local replica with PocketIC, call each with the same inputs, and compare. The Motoko body is the reference: any difference is a bug in the kernel, or in the declaration that connects them.
// Install both builds on a local replica, check that they agree, and compare their cost.
import { readFileSync } from "node:fs";
import { randomBytes } from "node:crypto";
import { PocketIc, PocketIcServer } from "@dfinity/pic";
const idlFactory = ({ IDL }) => IDL.Service({
crc: IDL.Func([IDL.Vec(IDL.Nat8)], [IDL.Vec(IDL.Nat8)], ["query"]),
measure: IDL.Func([IDL.Nat], [IDL.Record({ crc: IDL.Vec(IDL.Nat8), instructions: IDL.Nat64 })], []),
});
const server = await PocketIcServer.start();
const pic = await PocketIc.create(server.getUrl());
const install = async (file) =>
(await pic.setupCanister({ idlFactory, wasm: readFileSync(file) })).actor;
const kernel = await install("kernel.wasm");
const motoko = await install("motoko.wasm");
const hex = (b) => Buffer.from(b).toString("hex");
// The standard check value: CRC-32 of the ASCII digits 1 to 9 is cbf43926.
const check = new TextEncoder().encode("123456789");
console.log(`check value: kernel ${hex(await kernel.crc(check))}, Motoko ${hex(await motoko.crc(check))}`);
// The Motoko body is the oracle: the kernel must agree with it on every input.
let inputs = 0;
for (const size of [0, 1, 2, 3, 4, 7, 8, 15, 16, 63, 64, 65, 255, 256, 1000, 4096]) {
for (let i = 0; i < 25; i++) {
const data = randomBytes(size);
const [k, m] = [await kernel.crc(data), await motoko.crc(data)];
if (hex(k) !== hex(m)) throw new Error(`differ on ${hex(data)}: ${hex(k)} vs ${hex(m)}`);
inputs++;
}
}
console.log(`${inputs} random inputs: the kernel and the Motoko body agree`);
const col = (x) => x.toLocaleString("en-US").padStart(13);
console.log("\n bytes with kernel Motoko body");
for (const size of [64, 1_000, 1_000_000]) {
const [k, m] = [await kernel.measure(BigInt(size)), await motoko.measure(BigInt(size))];
if (hex(k.crc) !== hex(m.crc)) throw new Error("results differ");
console.log(`${col(size)} ${col(k.instructions)} ${col(m.instructions)}`);
}
await pic.tearDown();
await server.stop();
npm install @dfinity/pic@0.23.0
node compare.mjs
check value: kernel cbf43926, Motoko cbf43926
400 random inputs: the kernel and the Motoko body agree
bytes with kernel Motoko body
64 1,405 17,175
1,000 17,864 247,431
1,000,000 17,672,149 246,001,431
Both builds give the standard check value for CRC-32, agree on 400 random inputs of awkward lengths, and the kernel costs about a fourteenth of the Motoko body. For your own kernels, test edge cases (empty input, the largest input you accept, invalid input) and published test vectors, such as Wycheproof for cryptography. An input the Motoko body rejects with a trap should trap in the kernel too.
8. Publish the package#
Add the source the kernel was built from to [kernel], so users can rebuild it and see where it comes from:
[kernel]
abi = 1
file = "kernel/crc32.kernel.toml"
source = "https://github.com/OWNER/crc32/tree/COMMIT/rust"
commit = "COMMIT"
source is the repository's URL, with /tree/REV/PATH when the crate isn't at the root. Name a commit rather than a branch: a branch moves. Commit the crate, its Cargo.lock and the kernel file together, push them, and publish as usual:
mops publish
The kernel file is a .toml file because mops publishes only .mo, .did, .md and .toml files. Users add your package with mops add crc32 and opt in with moxzi kernel add crc32. Each of them is asked to accept your kernel the first time, and sees your declared source and commit.
Any change to the crate or its lock changes the kernel's bytes, and so every user's pin. Their next build stops until they review the new version and run moxzi kernel add again. Say in your changelog when a release changes the kernel.
Next steps#
- Security for kernel authors: constant time, and what to check in the compiled wasm.
- Kernel ABI: every argument and result type, and what the linker refuses.
- Using kernels: what your users see.