C API & extensions

Amber has exactly two seams, and both live in one header. src/ext.h sections 1–5 are the in-process registry: drop a .c file into ext/, rebuild, and it plugs itself in without a line of src/ being patched. Section 6 is the out-of-process API on the front of libamber.so.

The in-process seam: ext/

ext/ is empty in a stock checkout. An out-of-tree package installs itself by dropping .c files there and re-running ./build.sh; they are compiled with the same flags, linked into the same binary, and register themselves from a constructor.

HookWhat it lets an extension do
am_ext_verb("xyz", fn)register a backtick verb (`xyz x) at runtime
am_ext_bsclaim a \-command before the "unknown \cmd is a shell command" fallback
am_ext_hintinline ghost text in the editor. Not called since 2.0's editor; kept so older extensions still link
am_ext_completeTab candidates. Tab completion went in 2.0, so nothing calls it; kept so older extensions still link
am_ext_startuprun once, lazily, when the REPL first reads a line
am_ext_usage / am_ext_bannerappend to --help and to the banner

The Amber-level half goes in lib/; repl.k loads lib/ext.k whole-file at startup if it exists, fully trapped, and an extension may define the optional ext.pre / ext.post / ext.err / ext.raw / ext.tag hooks.

Why this exists

Pulling a new Amber release must never conflict with a package you installed, and a user who installs nothing must pay nothing, so every hook is a null pointer and every call site is a predictable branch. tests/ext_probe.c is a complete worked example; tests/test_ext_seam.sh installs it, checks every hook, and uninstalls it again.

amber-ai is installed exactly this way:

git clone https://github.com/BonucciAndrea/amber-ai.git
cd amber-ai && ./install.sh /path/to/amber

The out-of-process seam: libamber.so

./build.sh                 # ./amber          (unchanged; identical binary, zero new cost)
./build.sh --shared        # ./amber  +  libamber.so
./build.sh --shared-only   # libamber.so only
AMBER_SHARED=1 ./build.sh  # same as --shared, for callers that cannot pass a flag
host.c
#include "ext.h"                       /* section 6 -- nothing else from src/ */

amber_init("/path/to/amber");          /* boots the engine, loads the .k stdlib */
amber_value t = amber_eval_qsql("select vwap:wavg[sz;px] by sym from trades");

int type; long long n; int bits;
const void *px = amber_get_vector_ptr(amber_table_column(t, 2), &type, &n, &bits);
/* `px` IS the engine's column payload. Not a copy of it. */

~60 entry points, all named amber_*. Boot and evaluate (amber_init, amber_eval_str, amber_eval_qsql, amber_call), reference counting (amber_retain / amber_release), the zero-copy vector seam (amber_get_vector_ptr), tables and dictionaries, constructors for pushing data back in, the Arrow C Data Interface, rendering, and amber_plugin_load for dlopening a native plugin into a running engine.

What the shared build changes, and what it does not

The executable is untouched

./build.sh with no flags produces byte-for-byte what it produced before, from the same objects, with the same flags. The shared library is a separate object set (-fPIC -Dshared): position-independent code and the global-dynamic TLS model a dlopen'd library needs both change code generation, and sharing objects between the two would silently pessimise ./amber.

Only amber_* and am_ext_* are exported

Amber's internal C is written in a terse K-derived idiom. The engine's own globals are called mr, su, us, err, run, add, sub, pk, cpl. Those are perfect inside one static binary and actively dangerous inside a library loaded next to NumPy, libarrow and libpython. An export map (src/libamber.map) makes everything else absent from the dynamic symbol table, so it cannot be bound to by accident and cannot interpose on a host's symbol of the same name.

The library records a SONAME

Two copies of the library means two engines

A satellite that dlopens a second copy of libamber.so gets a second engine: two heaps, two symbol tables, two global namespaces, and values from one that are meaningless to the other, with no error, because nothing is technically wrong. The SONAME is what lets the loader recognise an already-loaded copy and reuse it, so python-amber, libamber_arrow.so and any plugin in one process share one engine.

amber_arrow.library_path() tells you which one you got.

TLS drops to global-dynamic in the shared build only

./amber keeps the initial-exec model on the allocator's hot path. A dlopen'd library cannot: it is resolved out of the static TLS block the loader sizes before main(), and borrowing from glibc's small surplus reserve fails nondeterministically, with cannot allocate memory in static TLS block, depending on what else the host imported first.

Apache Arrow C Data Interface

The engine exports Arrow directly, in about ninety lines (src/ar.c), with no libarrow linkage at all. Export is zero-copy.

amber>
t:([]sym:`AAPL`MSFT`AAPL; px:187.3 411.2 187.4; sz:100 250 50)
p:arrow.export t                    / table  -> (schemaAddr; arrayAddr)  64-bit C-ABI pointers
arrow.import p                      / (schemaAddr; arrayAddr) -> Amber table

That is exactly the right amount of Arrow for a 10,000-line C runtime to own: it covers "hand me this table" completely. What it does not cover is "hand me this 400-million-row table in batches, and stop when I stop asking", the ArrowArrayStream, and that lives out in amber-arrow, where it can grow.

Verification

tests/test_capi.sh              # release build, then ASan + UBSan
tests/run_tests.sh --asan       # the whole suite, the C API included

tests/test_capi.c is the only consumer of libamber.so inside the engine repository, and it is written the way a satellite would write it: it includes src/ext.h and nothing else from src/, never dereferences an amber_value, and links the shared library rather than the objects. If it ever needs a second -I, the API is wrong.

81 assertions, clean under ASan + UBSan with leak detection

Because a C API whose ownership rules are only documented is a C API whose ownership rules are wrong, and LeakSanitizer is what checks the prose.