ADR-131: A Binding for Another Language Is a Generated File Over the C Library, Written by nikaia bind, and Never a Second Artifact

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

1. Context

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.

2. Decisions

D1 — 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.

D2 — What the Python binding makes of the boundary.

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.

D3 — JavaScript is served by the WebAssembly build.

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.

D4 — A binding never widens what the library exports.

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.

3. Consequences

4. What this does not decide

5. What is built

Nothing. The order of work, after ADR-125 §5’s header:

  1. nikaia bind python, the table of D2, over the example library;
  2. the streamed and the async forms in it, with the awaitable;
  3. nikaia bind js as ADR-130’s generator, named here;
  4. a test that imports the binding and calls the example through it.

#112 carries it.

This site is open source. Improve this page.