nikaia bind, and Never a Second ArtifactStatus: Superseded by ADR-284 — its D1-D4 are D26-D29 there (D3 together with ADR-153)
Date: September 19, 2026
Target Version: Nikaia 0.0.9
Related: ADR-125 (the library boundary), ADR-130 (the WebAssembly library and its .js), ADR-104 (nikaia describe, the other generated file), ADR-129 (cancel and stream)
ADR-125 left open “Python and Node artifacts, which generate
native modules from the same entry points”. The question is whether the
compiler should emit a second kind of artifact per host language — a
Python extension module, a Node add-on — or one artifact and a thin file
that each host loads with its own standard tooling. A native module per
language is what PyO3 and napi-rs do, and each is a second translation
table (Python objects, JavaScript values) beside the C one, a second build
dependency, and a second thing that can be out of date.
The ledger already knows everything a binding needs: the signatures, the error variants, the enum numbering, which struct is a handle and which a value, which function pauses. That is what generated the header; it can generate the wrapper.
nikaia bind <language> writes a binding from the ledger over the C library; the artifact stays one.nikaia bind python # writes <package>/__init__.py beside the shared library
nikaia bind js # the .js and .d.ts of ADR-130, for a WebAssembly build
A binding is a generated source file the host language loads with what it
ships — ctypes for Python, the WebAssembly API for JavaScript — and it
depends on nothing else. It is regenerated with the header and carries the
same ledger hash in its head comment, so a binding and a library that do not
match say so. There is no artifact = "python-module"; the one artifact is
ADR-125’s.
| at the boundary | in Python |
|---|---|
a status that is not OK |
an exception: hello.Error with one subclass per error variant, carrying last_error’s text; the boundary’s negative codes are hello.BoundaryError subclasses |
text or bytes out (out, cap, written) |
the wrapper does the size query and the second call; the caller receives str or bytes |
| text or bytes in | str encoded UTF-8, or bytes; never a char* the caller manages |
| a handle | a class with the methods, a property per getter, close() and a context manager; __del__ frees a handle nobody closed |
an extern "C" struct |
a ctypes.Structure subclass with the fields, usable as a value |
an enum |
an enum.IntEnum |
T? |
None |
| a callback | a Python callable, wrapped by CFUNCTYPE and kept alive for the call |
the _async form |
a function taking a callable, and — where an event loop is running — an async def that awaits it, resolved through call_soon_threadsafe; ADR-129’s ticket is a cancel() on the returned awaitable |
| a streamed function (ADR-129 D2) | a generator; returning from the consumer’s loop stops the stream |
The wrapper holds the module’s lock while calling init and shutdown,
and otherwise adds nothing the boundary does not do: the handle lock is
ADR-125 D8’s, the copies are D3’s.
For JavaScript, nikaia bind js is ADR-130’s generated .js
and .d.ts, and the module runs in a browser and in Node alike without a
native compile. There is no native Node add-on: what a native library
could reach on Node that a WebAssembly module cannot — the filesystem, a
socket — is §4’s, and a program that needs it today has a C library and
Node’s own means of loading one.
A binding exposes the entry points and nothing else: no function the ledger
does not mark extern, no field the header does not read, no constructor the
library does not have. What a host language cannot express — an integer
optional is None, a char is a one-character str — is decided in the
table above and not per program.
pip install of a directory with one shared
library and one generated file, and reads exceptions, strings and classes.nikaia bind with one generator per language, reading
the ledger the header generator reads.Nothing. The order of work, after ADR-125 §5’s header:
nikaia bind python, the table of D2, over the example library;nikaia bind js as ADR-130’s generator, named here;#112 carries it.