moxzi
Docs / Native kernels / Kernel ABI

Kernel ABIalpha

The calling convention, manifest fields and linker rules for native kernels, ABI version 1.

This page specifies version 1 of the interface between a Motoko program and a native kernel. Writing a kernel walks through it with an example.

The kernel package's manifest#

A kernel package is a mops package whose mops.toml has a [kernel] table:

[kernel]
abi = 1
file = "kernel/crc32.kernel.toml"
source = "https://github.com/OWNER/REPO/tree/COMMIT/PATH"
commit = "COMMIT"

[kernel.functions]
"Crc32.digest" = { export = "crc32", args = ["Blob"], result = "Blob", result_bytes = 4 }
FieldRequiredMeaning
abiYes1. Any other value is refused
fileYesThe kernel file, relative to the package directory
sourceNoWhere the kernel's crate is. moxzi kernel verify rebuilds from it. Shown before a third-party kernel is accepted, and written into the module's metadata
commitNoThe commit source was built from. Shown and recorded like source
[kernel.functions]YesOne entry per Motoko function the kernel replaces

source, commit and the package's [package] version must each be one token: no spaces or control characters, at most 512 bytes. They are written into public metadata, and moxzi refuses a value that could split a line.

Each [kernel.functions] entry:

Key or fieldMeaning
Key, "Module.function"The function's file below the package's src/ directory, without .mo, then a dot and the function's name. "Crc32.digest" is digest in src/Crc32.mo; "util/Hex.encode" is encode in src/util/Hex.mo
exportThe kernel's exported function that implements it
argsThe parameter types, in order: Blob, Text, Nat64 or Bool
resultBool, Blob, ?Blob, Text or ?Text
result_bytesOnly with result = "Blob": the result's fixed length in bytes

The Motoko function#

The function must be declared at the top level of its module, with plain named parameters:

AcceptedRefused
public func digest(data : Blob) : Blob { ... }public func f((a, b) : (Blob, Blob)) : Blob (a pattern parameter)
public func mac(key : Blob, data : Blob) : Blob = ...A function inside a class, an object or another function

When the application opts in, moxzi keeps the function's name, parameters and type and replaces its body with a call to the kernel. Every declared function must be replaced, so a build that opts into a kernel either calls the kernel or stops. It never quietly runs the Motoko body instead:

ErrorWhen
M0291A [kernel.functions] key names a module the package doesn't have, such as "Sha265.digest" when the file is src/Sha256.mo. moxzi reports it before compiling, for local and remote builds
M0291A key names a function the module doesn't define, such as a typo or a renamed function. The error lists the functions the module does define
M0291args or result disagrees with the function's declared types, such as args = ["Text"] for a Blob parameter, or the number of parameters differs. The error points at the function's signature in the package's source
M0290The function takes a pattern parameter, or the name belongs to something other than a function declaration

moxzi compares the parameter and result types when they're written as built-in type names, such as Blob or ?Text. When the function uses a type alias instead, the type checker checks it against the declared signature after the rewrite, and a mismatch is a type error at the same place.

moxzi checks a declared function when the program imports its module. A function in a module the program doesn't import is never called, so it isn't checked. When the program imports none of a kernel's modules, the build says so in a note and doesn't use the kernel.

Calling convention#

Kernels are wasm64. Every pointer and length is 64 bits.

Motoko typeKernel receives
Blob argumentTwo parameters: ptr: *const u8, len: usize, the Blob's bytes
Text argumentTwo parameters: ptr: *const u8, len: usize, the text's UTF-8 bytes
Nat64 argumentOne u64
Bool argumentOne u64: 0 or 1

Argument bytes are borrowed for the duration of the call. Don't write to them, and don't keep their addresses after you return.

ResultThe kernel
BoolReturns a u32: exactly 0 or 1. Any other value traps
Blob with result_bytes = nTakes one more, last parameter, out: *mut u8, with room for exactly n bytes, and writes all of them. Returns nothing
Blob, TextReturns the address of [len: u64][len bytes] in memory from kernel_alloc. The SDK's output and text build it
?Blob, ?TextAs Blob and Text, or 0 for null

The glue copies a variable-size result into a new Motoko Blob or Text. A fixed-size result is written straight into the new Blob.

What the program provides#

A kernel may import two functions from the program. The moxzi-kernel SDK wraps both.

ImportDoes
kernel_alloc(size: usize, align: usize) -> *mut u8Allocates scratch memory for the rest of the call. align must be a power of two up to 65,536 and size at most 64 MiB, or the message traps. Nothing is freed during the call; the garbage collector reclaims all of it after the glue has copied the result
kernel_trap(ptr: *const u8, len: usize) -> !Traps the message with this text as the reject message

The SDK makes kernel_alloc the crate's global allocator, so Vec and alloc work. Its dealloc does nothing, so a growing Vec keeps every buffer it outgrew until the call ends. Reserve capacity up front when you know the size.

The SDK's panic handler traps with native kernel panicked: followed by the panic message, truncated to 256 bytes.

Checks on results#

The glue checks what a kernel returns before Motoko code sees it. Each failed check traps the message, and the IC rolls the message back.

CheckTrap message
A fixed-size result wrote past its n bytesnative kernel PACKAGE.EXPORT wrote past its N-byte result
A variable-size result's length is larger than the scratch it sits innative kernel PACKAGE.EXPORT returned a result longer than its scratch
A variable-size result wrote past the end of its scratchnative kernel PACKAGE.EXPORT wrote past its result scratch
A Text or ?Text result isn't UTF-8a native kernel returned text that is not UTF-8
A Blob or Text result is 0a native kernel returned no result
A Bool result is neither 0 nor 1native kernel PACKAGE.EXPORT returned a Bool that is neither 0 nor 1

An overrun is detected by a guard word after the buffer, so it catches writes that run off the end. It doesn't catch writes anywhere else in memory. The check costs about 30 instructions for a fixed-size result and about 50 for a variable-size one.

What the linker refuses#

The linker checks every kernel before linking it, both locally and on the compiler canister. A kernel is refused if:

RuleDetail
It imports anything elseAllowed imports: env.memory, env.__indirect_function_table, env.__stack_pointer, env.__memory_base, env.__table_base, env.kernel_alloc, env.kernel_trap. No system API, no other kernel, no function from the program
It uses memory.grow or memory.initThe runtime owns the memory and loads the kernel's data
It has a start function, or defines its own memory or table
It has a data or element segment at an absolute address, or a passive data segmentData is placed relative to __memory_base, function pointers relative to __table_base
It imports through a global offset table (GOT)ABI 1 kernels have none
It doesn't export a function the program calls
Its static data doesn't fitThe program's static data, every kernel's and the runtime's share one 512 KB (524,288-byte) reserve. The error names each kernel's share
Its bytes don't match the pin the program was compiled withThe pin travels in the module's moxzi:kernels section

A kernel the program never calls isn't linked. Kernels are linked in order of package name, so the output doesn't depend on the order of [kernels].

State, memory and garbage collection#

The kernel file#

moxzi kernel build writes a .kernel.toml file, because mops publishes only .mo, .did, .md and .toml files:

FieldMeaning
abi1
sha256The SHA-256 of the wasm bytes. Applications pin this value
sizeThe wasm's size in bytes
wasmThe wasm, base64

The comment lines at the top name the toolchain and linker that built it. moxzi checks that sha256 matches the decoded bytes every time it reads the file.

Metadata in a linked module#

A module built with kernels opted in has a public custom section, moxzi:kernels, with one line per kernel the module calls, and so contains. A kernel the program never calls isn't linked and isn't listed. When the program calls no kernel at all, the section is empty, or absent if the program imports none of the kernels' modules.

PACKAGE sha256:HEX abi=1 version=VERSION source=URL commit=COMMIT

The first two fields are the pin. version=, source= and commit= appear when the package declares them. The linker reads the section to refuse kernel bytes other than the pinned ones, and anyone can read it from a deployed canister. As wasm-tools print shows it:

(@custom "icp:public moxzi:kernels" (after data) "moxzi-hash sha256:d9e07eabd5c53e4567f6ce132857b52f7b0d3546f2443b73bdb3b07cf2244a2e abi=1 version=0.1.0 source=https://github.com/icdevsorg/moxzi-kernels/tree/main/kernels/moxzi-hash/rust commit=445b558869a5552956229d704011c40480e1854c\0a")

Limits#

LimitValue
Static data of the program, the runtime and all kernels together512 KB
One kernel_alloc request64 MiB
One kernel uploaded for an on-chain build1,900,000 bytes
InstructionsA kernel call counts against the message's instruction limit like any other code
On this pageThe kernel package's manifestThe Motoko functionCalling conventionWhat the program providesChecks on resultsWhat the linker refusesState, memory and garbage collectionThe kernel fileMetadata in a linked moduleLimits