code-native
Write a native .so module for the Code programming
language in Rust — link "x.so" as m
emit particle to mfrom.codesource, with the handler behindmimplemented here instead of C.
This is the Rust-specific path into code's native-module ABI
(src/code_abi.h in the main repo). C — and anything else that can produce
a C-ABI shared library — still uses that header and src/runtime.c
directly; there's no package registry to publish a C-language bundle to, so
that story is unchanged. This crate exists so a Rust module doesn't need a
checkout of the code repo at all: cargo add code-native pulls in
everything required, including a real runtime.c compiled and linked in by
this crate's build.rs — not a Rust reimplementation of it, so there's no
risk of the refcounting subtly drifting from what the host trusts.
Quick start
# Cargo.toml
[]
= ["cdylib"]
[]
= "1"
// src/lib.rs
use *;
pub extern "C"
pub unsafe extern "C"
# target/release/libmymodule.so
| my_script.code
link "libmymodule.so" as m
emit Double { _class = "Double", value = 21 } to m get result
assert result.value = 42
Run it with code run my_script.code, or code build my_script.code to
compile a native binary that dlopen's the module at startup — both output
modes accept a .so the same way.
Reading a value
A CodeValue is its tag, heap and one payload the tag decides (ABI
version 2, 16 bytes). Read it through the helpers — read_str,
read_number, find_field, array_elems, object_entries — or the
methods: number(), str_ptr(), boolean(), and for an array or object
len(), items() (for slot_at) and keys(). Each answers 0 or null for
a value of another kind. An array's or object's count and keys live in its
block, not in the value.
Why only two required exports
code_module_abi_version and code_module_dispatch are it. There's no
code_module! macro generating boilerplate here (unlike the old
language's own code-native, which generated a whole descriptor table of
handlers/vars/types/emissions): the new ABI dropped that design for one
function a module dispatches through itself — see
docs/todo/native-module-linking.md
in the main repo for why. code_release needs no code from you at all — it
comes from the runtime.c this crate links in automatically.
Exported variables
Optional third export, code_module_vars, is what makes m.someConst work
alongside emit ... to m:
use *;
use OnceLock;
static VARS: = new;
pub extern "C"
See code_abi.h's own doc comment (vendored into this crate at
vendor/code_abi.h) for the full CodeVarList contract — this crate
mirrors it field-for-field rather than hiding it, since building the
'static-lifetime buffer correctly is easier to get right by following the
same shape a C module uses than behind a leaky abstraction.
Failing without ending the program
A module may never bring the application down. Report a failure by
returning an Exception — the program receives it as an ordinary value,
tests it with ∈ Exception, and may read message or ignore it entirely:
exception;
code_runtime_error is deprecated for module use and will leave this crate
once the C runtime has an error channel of its own.
guarded — and why it cannot live in the host
Wrap your dispatch in guarded so a panic becomes an Exception too:
pub unsafe extern "C"
This is not something the host could do for you. A panic escaping an
extern "C" function aborts the process rather than unwinding, so the
host's own catch_unwind never runs — the catch has to happen on this side
of the FFI boundary. tests/native_modules/test_panics exists to keep that
true.
It covers what "written wrong" usually means: unwrap/expect, index and
slice bounds, arithmetic overflow, explicit panic!/assert!, and panics
from inside dependencies. It cannot cover a deliberate exit, an infinite
loop, or undefined behaviour reached through unsafe.
This is why Rust is the recommended path for a third-party module. In C
the same guarantee does not exist: a module that forgets a NULL check
segfaults, and an integer 100 / 0 raises SIGFPE — neither is catchable by
anything, in any language, from anywhere. (Rust will not even compile the
latter.) The C path stays as the ABI's reference implementation; production
modules should take this one.
Speaking first (inbound emissions)
A module normally only answers an emit. To push particles into the
program on its own initiative — an event source, or a module reporting what
went wrong — take the optional code_module_set_inbound export and push:
use *;
// Generates `code_module_set_inbound`. A macro rather than a function in
// this crate on purpose: a `#[no_mangle]` symbol defined in a *dependency*
// is not reliably kept in the final cdylib, so the export has to be emitted
// in your crate.
declare_inbound!;
Pushed particles reach the program's handlers (not the module's own), dispatched between top-level statements. Two things worth knowing:
emit_inboundreturnsfalsewhen the host never took an inbound channel. Pushing is always best-effort, and a module has to stay correct when nobody is listening.- A pushed class the program has no handler for is dropped. That is what
lets a module report something without every program that links it having
to handle it. Since 2026-08-28 the outbound direction agrees:
emit ... to <anything>with no matching handler is null, not an error.
The queue is bounded at CODE_INBOUND_CAPACITY (256) per module, dropping
the oldest, so a module that outruns the program costs bounded memory.
.a static modules
Everything above is the .so path — code_abi.h's "primary format", the
one artifact both code run and code build accept. A .a uses a
different, simpler contract: it links straight into the host binary, so
there is no deep-copy boundary, no per-module code_release, and exactly
one runtime — the host's. In exchange it needs a symbol prefix, since every
.a linked into one program shares a flat symbol table.
Two changes to your Cargo.toml:
[]
= ["staticlib"]
[]
= { = "1", = false, = ["static-module"] }
static-module is what makes this work: without it this crate compiles the
vendored runtime.c into your archive, and linking that against a host that
already has one is multiple definition of 'code_release' — forty-one
symbols over. With it, the crate brings no runtime and calls the host's.
Then prefix your exports with a name unique among every .a the program
will link alongside:
pub extern "C"
pub unsafe extern "C"
Nothing in the language names the prefix — code build finds it by running
nm on the archive, so it only has to be unique. code_module_vars takes
the prefix too if you export it. A working example is
tests/native_modules/test_math_static/ in the main repo.
code run refuses a .a outright (there is no dlopen for an archive), so
fixtures that link one are buildonly_*.
Safety
Most of this crate's surface is safe Rust, but code_module_dispatch itself
is necessarily unsafe extern "C" fn — it's called across an FFI boundary
with a raw pointer the host guarantees is valid, which Rust has no way to
express short of unsafe. Everything you do inside the handler
(read_field_str, number, make_result, …) is safe.
code-native on crates.io already has 0.2.0 published under this same
repo + account, from the old language — a completely different API (the
macro/descriptor-table design this README's "Why only two required
exports" section explains is gone) and a different license (that one's
MIT; this one is GPL-3.0, since it links runtime.c from the main repo
directly rather than reimplementing it). That's why this package starts at
1.0.0 rather than continuing the 0.2.x line: a real break — API and
license both — deserves a major version, not one a ^0.2 pin would
silently accept. (Same call crates/code-wasm made for its own npm
package, for the same reason.)
Releasing (maintainers)
Published via crates.io Trusted Publishing (OIDC) from GitHub Actions
(.github/workflows/publish-crates-native.yml) — no CARGO_REGISTRY_TOKEN
stored anywhere, mirroring crates/code-wasm's npm Trusted Publishing setup.
One-time setup (crates.io account configuration, can't be done from CI) — normally the very first publish has to be manual, since crates.io has no equivalent to npm's Staged Packages for configuring a trusted publisher before a crate exists at all. Not needed here: the crate already exists (see above), owned by the same account doing this setup, so go straight to its Settings → Trusted Publishing page and add:
- Repository owner:
codelovesme - Repository name:
code - Workflow filename:
publish-crates-native.yml - Environment: leave blank
Every release after that is the repository's own release tag — there is
no separate one. Everything published from this repo shares one version, so
code v1.1.0, code-native 1.1.0 and every module at 1.1.0 ship together
and a consumer never has to check whether they match (tests/one_version.rs
holds every manifest to it):
The workflow sets Cargo.toml's version from the tag itself, verifies the
package builds standalone (cargo publish --dry-run), and publishes.
workflow_dispatch (the "Run workflow" button in the Actions tab) does
everything except the actual publish — a real dry run against the exact
package that would ship.
Optional borrowed metadata dispatch (unreleased)
A module may additionally export code_module_dispatch_borrowed_v1(out, api, particle) -> c_int. The interpreter tries it before constructing the full
ABI value tree. Return nonzero for a module-owned result, or zero to request
ordinary dispatch. The host releases either result, including one declined.
Keep the required ordinary exports: older hosts ignore this entry point.
The value layout and CODE_ABI_VERSION remain 2; the metadata API version is 1.
borrowed::BorrowedValue::from_raw checks the API version and exposes metadata,
first-field lookup and UTF-8 text. It does not expose array elements or scalar
number payloads. Use it only for handlers that can answer from that information
(arrays/strings Length are examples); decline everything else. See
arrays for an implementation.
All opaque pointers, field values and text slices are immutable, confined to the calling thread, and borrowed only until dispatch returns. Never retain them or pass them to CodeValue constructors/releases. Return an ordinary owned CodeValue. The host's existing transport limitations still apply. Wire types are mirrored in vendor/borrowed_abi.rs and checked against the main crate.
License
GPL-3.0 — see LICENSE. This crate links runtime.c from the
main code repo directly (vendored, not reimplemented), so it carries the
same license.