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 }
| Field | Required | Meaning |
|---|---|---|
abi | Yes | 1. Any other value is refused |
file | Yes | The kernel file, relative to the package directory |
source | No | Where 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 |
commit | No | The commit source was built from. Shown and recorded like source |
[kernel.functions] | Yes | One 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 field | Meaning |
|---|---|
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 |
export | The kernel's exported function that implements it |
args | The parameter types, in order: Blob, Text, Nat64 or Bool |
result | Bool, Blob, ?Blob, Text or ?Text |
result_bytes | Only 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:
| Accepted | Refused |
|---|---|
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:
| Error | When |
|---|---|
M0291 | A [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 |
M0291 | A 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 |
M0291 | args 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 |
M0290 | The 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 type | Kernel receives |
|---|---|
Blob argument | Two parameters: ptr: *const u8, len: usize, the Blob's bytes |
Text argument | Two parameters: ptr: *const u8, len: usize, the text's UTF-8 bytes |
Nat64 argument | One u64 |
Bool argument | One 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.
| Result | The kernel |
|---|---|
Bool | Returns a u32: exactly 0 or 1. Any other value traps |
Blob with result_bytes = n | Takes one more, last parameter, out: *mut u8, with room for exactly n bytes, and writes all of them. Returns nothing |
Blob, Text | Returns the address of [len: u64][len bytes] in memory from kernel_alloc. The SDK's output and text build it |
?Blob, ?Text | As 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.
| Import | Does |
|---|---|
kernel_alloc(size: usize, align: usize) -> *mut u8 | Allocates 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.
| Check | Trap message |
|---|---|
A fixed-size result wrote past its n bytes | native kernel PACKAGE.EXPORT wrote past its N-byte result |
| A variable-size result's length is larger than the scratch it sits in | native kernel PACKAGE.EXPORT returned a result longer than its scratch |
| A variable-size result wrote past the end of its scratch | native kernel PACKAGE.EXPORT wrote past its result scratch |
A Text or ?Text result isn't UTF-8 | a native kernel returned text that is not UTF-8 |
A Blob or Text result is 0 | a native kernel returned no result |
A Bool result is neither 0 nor 1 | native 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:
| Rule | Detail |
|---|---|
| It imports anything else | Allowed 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.init | The 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 segment | Data 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 fit | The 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 with | The 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#
- No collection during a call. The garbage collector never runs while a kernel executes, so argument addresses stay valid until it returns.
- Kernels are stateless. Return everything through the result. A kernel's static data is memory, so a
static mutkeeps its value between calls, but it is reset to the kernel's initial data by every upgrade, and the Motoko body has no such state. Programs that rely on it behave differently with and without the kernel. - Shared stack. A kernel runs on the canister's stack, shared with the Motoko code that called it. Keep large buffers on the heap.
- Determinism. A kernel has no clock, no randomness and no system API. Avoid floating point: the linker doesn't refuse it, but the bit patterns of NaN results can differ between the Internet Computer and other hosts.
The kernel file#
moxzi kernel build writes a .kernel.toml file, because mops publishes only .mo, .did, .md and .toml files:
| Field | Meaning |
|---|---|
abi | 1 |
sha256 | The SHA-256 of the wasm bytes. Applications pin this value |
size | The wasm's size in bytes |
wasm | The 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#
| Limit | Value |
|---|---|
| Static data of the program, the runtime and all kernels together | 512 KB |
One kernel_alloc request | 64 MiB |
| One kernel uploaded for an on-chain build | 1,900,000 bytes |
| Instructions | A kernel call counts against the message's instruction limit like any other code |