Issue #88: A library for other languages (artifact = "c-library")

Open and answer this issue on GitHub.

targets, ready · opened 2026-09-30 · updated 2026-10-10 · 9 comments

Owner’s order: second of the five big pieces (after std::db). Record: ADR-125, all of it. Nothing built.

A pub extern "C" fn with a body is an entry point, and artifact = "c-library" in [build] makes the package a library. What a C caller gets is deliberately narrow: numbers by value; text and bytes in as pointer and length, out into a buffer the caller owns (size, capacity, written; a NULL buffer asks for the size); a struct as an opaque handle with _new and _free; an enum as numbered constants; an optional as NULL or the package’s NONE. Every entry point returns an int status and puts values in out-parameters; a throws variant is a positive code, the library’s own failures are the seven negative ones, <pkg>_last_error carries site and secondary list. A caller may hand over its allocator before init. A panic is caught at the boundary and poisons the library until shutdown/init. A pausing function is exported blocking and as _async. Every handle carries a lock; a re-entrant call on the same thread is a status, not a deadlock. The header is generated from the ledger and carries its hash.

The direction ADR-124 and ADR-147 do not face: those let a program call C, and both are built.

Order of work (ADR-125 §5): the parser and the four refusals; the artifact and the safe shape at entry points; the wrapper per entry point; the runtime surface (set_allocator, init, shutdown, last_error, free, getters); blocking and async forms and the handle lock; the header generator and naming; a library called from a C program in examples/, and the test that links it.

Sub-issues carry the pieces that build on it (struct by value, symbol prefix, cancellation/streams, WebAssembly, bindings, the exported-roots analysis). The bare-metal target reuses this library’s allocator and baked settings and comes after it.

Comments

keywan-ghadami, 2026-10-08

Built so far (0.0.663 a644cb4, 0.0.664 5110b39)

The record is ADR-284, into which ADR-125 was merged. ADR-324 D6 changed the spelling to extern, without "C".

Step 1: the parser and the four refusals.

Steps 2 and 3, for D5’s first row, and the header (step 6, in part):

Next, in ADR-284 §5’s order:

Found on the way, not fixed: a function that calls panic(…) cannot be sync, because std.contracts has no entry for panic. So an entry point that panics on purpose is refused as pausing. The test triggers its panic with an index out of range instead.


Generated by Claude Code

keywan-ghadami, 2026-10-08

Progress since 0.0.664:

Still open, in ADR-284 §5 order:


Generated by Claude Code

keywan-ghadami, 2026-10-08

Progress, 0.0.668–0.0.676 (all on develop, each covered by crates/nikaia/tests/c_library.rs with a C program):

Still open, split by what it needs:

Needs a decision (ADR-284 does not say):

  1. The value of <PACKAGE>_NONE for T? of a number (D5). D7 says the boundary owns seven negative codes, and NONE is not one of them; the positive codes are the variants. Options: -8 as an eighth boundary code, or 1 with the variants starting at 2. My recommendation is -8, written into D7.
  2. Getters of an enum with payload (D5: “_kind and a getter per field”). This needs a name for a positional field (TooLarge(i64)), and a rule for the status a getter returns when the handle holds another variant. My recommendation: <prefix>_<Type>_<Variant>_<n> for positional fields, and E_ARGUMENT when the variant is wrong.
  3. Codes for std’s errors (io::Error, …) thrown by an entry point. D7 numbers the library’s own variants. My recommendation: number std’s variants after the library’s own, in ledger order.

Buildable, not built yet: String? handed back; Array[T, N] fields at the boundary; methods of an extern struct (D16); Vec of them; set_allocator (D6/D10), which needs an allocator hook that is sound for memory allocated before it was set; a library executor in place of one thread per _async call; the WebAssembly target (D22–D25); nikaia bind (D26–D29).


Generated by Claude Code

keywan-ghadami, 2026-10-08

Progress, 0.0.683 to 0.0.694:

Still open:


Generated by Claude Code

keywan-ghadami, 2026-10-08

Progress, 0.0.695 to 0.0.698:

Still open:


Generated by Claude Code

keywan-ghadami, 2026-10-10

Decision 1 of 3: an absent number is a flag, not a status (0.0.722, 1def0b9)

Question (from the comment on 0.0.668–0.0.676): which value does the status <PACKAGE>_NONE get for a T? of a number? ADR-284 D5 named the status but gave it no value.

Options weighed:

Decided (owner): the flag. ADR-284 D30 (D5’s table and D16 changed); Part III 15.1.

To build

  1. A T? of a number or of an extern struct, as an entry point’s result, is exported with the value’s out-parameter followed by bool* present. The status is OK either way. Where present is false, the value’s out-parameter is left untouched. The header writes both parameters.
    • Test in tests/c_library.rs: fn find(key: ref String) -> i64? returns OK with present = true and the value, and OK with present = false and n unchanged.
  2. The same for a method or getter of a handle that returns T? of a number.
  3. nikaia bind python makes it None. nikaia bind node makes it null.

Decisions 2 (getters of an enum with payload) and 3 (codes for std’s errors) follow, one at a time.


Generated by Claude Code

keywan-ghadami, 2026-10-10

Decision 2 of 3: an enum with payload (0.0.723, 69b79af)

Question: ADR-284 D5 gives an enum with payload a handle, with _kind and “a getter per field”. It does not say how a positional field is named, or what a getter answers on a handle that holds another variant.

Decided (owner):

To build

  1. Handle getters. _kind, plus one getter per field per variant, named as above.
    • Tests in tests/c_library.rs:
      • Shape::Rect { w, h } read through lib_Shape_Rect_w;
      • Circle(f64) read through lib_Shape_Circle_0;
      • lib_Shape_Rect_w on a Circle is E_ARGUMENT, and last_error names Circle.
  2. pub extern enum.
    • Parser: extern before enum.
    • Checker: NK1145 for a field that is no C value.
    • Emitter: #[repr(C)] on the enum.
    • Header: the _Tag enum and the typedef struct { tag; union { … }; }. A variant without fields gets no member, and a member is the variant’s name in lower case.
    • Test: a C program builds a Rect, passes it in, gets a Circle back, and reads tag and circle._0. A tag of 99 is E_ARGUMENT.
  3. The same in the places D16 covers: a Vec of them in and out, methods with self as const T* or T*, and use as a field of an extern struct.
  4. Bindings.
    • Python: a ctypes.Structure with the union.
    • Node: { kind: "Rect", w, h }, converted both ways.

Decision 3 (codes for std’s errors) follows.


Generated by Claude Code

keywan-ghadami, 2026-10-10

Decision 3 of 3: codes for std’s errors (0.0.724, c12e561)

Question: which status an entry point returns for an error it throws from std. Today such a function is not exported (not_yet in cexport.rs). That covers every library that reads a file, since 38 ledger entries throw io::IoError.

Options weighed:

Decided: ADR-284 D33, with D7 changed. Part III 15.1 holds the table.

To build

  1. Renumber the library’s own variants from 100 000. This changes examples/c-library, its use.c, the Python and Node bindings, and tests/c_library.rs.
  2. Export std’s errors.
    • An entry point that throws io::IoError, or several types, is exported. The header writes <PACKAGE>_E_IO_NOT_FOUND and so on, with the target’s errno value (from libc at build time) or the fixed number.
    • Test: an entry point that reads a missing file returns ENOENT, and the C test compares the status with <errno.h>’s ENOENT.
  3. last_error carries the message of Other(String).
  4. Bindings.
    • Python: ENOENT raises FileNotFoundError and EACCES raises PermissionError. Both are subclasses of the package’s Error as well, if that can be done cleanly; otherwise the package’s Error, with errno set.
    • Node: code is 'ENOENT', and errno is the number.
  5. WASI target: the header uses WASI’s errno values.

All three decisions on this issue are now taken.


Generated by Claude Code

keywan-ghadami, 2026-10-10

The three decisions are built (0.0.725–0.0.728)

Decision 1, D30: an absent number is a flag (0.0.725, 13555d8)

Decision 2, D31 and D32

Decision 3, D33: error codes (0.0.728, dda8b62)

Still open on this issue: the WebAssembly target and nikaia bind js (D22–D25, #111). What blocks it is on #276.


Generated by Claude Code

Back to Open Issues