code-native 2.18.3

Write native .so modules for the Code programming language in Rust — safe CodeValue builders/readers over the real runtime.c, no reimplementation.
Documentation
# code-native

Write a native `.so` module for the [Code programming
language](https://github.com/codelovesme/code) in Rust — `link "x.so" as m`
+ `emit particle to m` from `.code` source, with the handler behind `m`
implemented 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

```toml
# Cargo.toml
[lib]
crate-type = ["cdylib"]

[dependencies]
code-native = "1"
```

```rust
// src/lib.rs
use code_native::*;

#[no_mangle]
pub extern "C" fn code_module_abi_version() -> u32 {
    CODE_ABI_VERSION
}

#[no_mangle]
pub unsafe extern "C" fn code_module_dispatch(out: *mut CodeValue, particle: *const CodeValue) {
    let particle = &*particle;
    match read_field_str(particle, "_class") {
        Some("Double") => {
            let value = read_field_number(particle, "value").unwrap_or(0.0);
            make_result(&mut *out, c"DoubleResult", |slot| number(slot, value * 2.0));
        }
        // A class this module does not handle answers null — see
        // docs/todo/errors-as-particles.md.
        _ => null(out),
    }
}
```

```bash
cargo build --release
# 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`](https://github.com/codelovesme/code/blob/main/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`:

```rust
use code_native::*;
use std::sync::OnceLock;

static VARS: OnceLock<CodeVarList> = OnceLock::new();

#[no_mangle]
pub extern "C" fn code_module_vars() -> *const CodeVarList {
    VARS.get_or_init(|| {
        let mut buf = SlotBuffer::new(1);
        number(buf.slot_mut(0), 3.14159);
        let values = buf.slot_mut(0) as *mut CodeValue;
        // Both leaked deliberately: the ABI requires this data to stay
        // valid for the module's whole lifetime, the same requirement a C
        // module meets with `static` storage.
        std::mem::forget(buf);
        let names: &'static [*const std::ffi::c_char] = Box::leak(Box::new([c"pi".as_ptr()]));
        CodeVarList { count: 1, names: names.as_ptr(), values }
    })
}
```

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:

```rust
exception(out, "mymodule", "cannot open the door");
```

`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:

```rust
#[no_mangle]
pub unsafe extern "C" fn code_module_dispatch(
    out: *mut CodeValue,
    particle: *const CodeValue,
) {
    let particle = &*particle;
    guarded(&mut *out, "mymodule", |out| {
        match read_field_str(particle, "_class").unwrap_or("") {
            "Double" => { /* ... */ }
            _ => null(out),
        }
    })
}
```

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:

```rust
use code_native::*;

// 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.
code_native::declare_inbound!();

fn report(message: &str) {
    let mut particle = CodeValue::zeroed();
    let mut buf = SlotBuffer::new(3);
    borrowed_str(buf.slot_mut(0), c"Exception");
    borrowed_str(buf.slot_mut(1), c"mymodule");
    owned_str(buf.slot_mut(2), message);
    object(&mut particle, &[c"_class", c"source", c"message"], &mut buf);
    buf.release_all();
    emit_inbound(&particle);
    release(&mut particle);
}
```

Pushed particles reach the **program's** handlers (not the module's own),
dispatched between top-level statements. Two things worth knowing:

- `emit_inbound` returns `false` when 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`:

```toml
[lib]
crate-type = ["staticlib"]

[dependencies]
code-native = { version = "1", default-features = false, features = ["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:

```rust
#[no_mangle]
pub extern "C" fn mymath_code_module_abi_version() -> u32 { CODE_ABI_VERSION }

#[no_mangle]
pub unsafe extern "C" fn mymath_code_module_dispatch(
    out: *mut CodeValue,
    particle: *const CodeValue,
) { /* ... */ }
```

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):

```bash
git tag v1.1.0   # whatever the new version is
git push origin v1.1.0
```

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](../modules/arrays/src/lib.rs) 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](./LICENSE). This crate links `runtime.c` from the
main `code` repo directly (vendored, not reimplemented), so it carries the
same license.