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"]
[]
= "0.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.
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.
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.