Legion ABI — the public dynamic surface of liblegion.so
Status: the export boundary is locked + CI-enforced (Foundation 09, 2026-06-18) by 3 build mechanisms + a gate (
check_internal_symbols_hidden.sh). The symbol set itself is a pre-release DRAFT — the L3/L4 surfaces are under review and will change before any release (see Stability & draft status andarchitecture.md).
TL;DR
liblegion.so exports exactly 131 legion_* symbols and nothing else — no
bundled libsodium, no internal Legion symbols. That set is the L4 dynamic
ABI: the stable native-C surface any consumer links against — native desktop
applications, embedded / microcontroller targets, and language bindings alike.
It is the sovereignty boundary — everything an app or binding is allowed to
call, and nothing more. (The Android NDK app was the first proof-of-concept
consumer that exercised L1–L4; Android is not a constraint.)
To see it:
nm -D --defined-only build/liblegion.so | awk '{print $3}' | grep '^legion_' | sort
What's in the ABI (131 symbols)
| Category | Symbol prefix | Count | Header | Role |
|---|---|---|---|---|
| FFI subsystem bridges | legion_ffi_* | 101 | legion_ffi_{audio,bridge,connection,display,focus,identity,js,node,objectstore}.h | Language-binding entrypoints: opaque handles, explicit memory ownership, no internal structs. The bulk of the surface. |
| L4 node lifecycle | legion_node_* | 14 | l4_node.h | Typed C wrappers over node operations: create_from_file, start, stop, destroy, reload, and getters (get_node_id, get_peers, format_status, …). |
| FFI helpers | legion_params_*, legion_result_*, legion_string_* | 7 | legion_ffi.h | Glue for languages without native map types: the params builder, result container free, string length. |
| Library base | legion_version*, legion_build_id, legion_free, legion_log_set_*, legion_config_find | 9 | legion.h | Version/build introspection, library-allocated-memory free, logging callback + level, config-file lookup. |
| 131 |
The exact count is informational (it grows as the L4 surface grows). The
invariant the CI gate enforces is "exports only legion_*" — never the
specific number.
L3 vs L4 — why L3 is not in the dynamic ABI
Legion's API is layered L1 (mesh/transport) → L2 (membership/identity) → L3 (capability provider surface) → L4 (consumer/app surface). Only L4 is the dynamic ABI:
- L4 = dynamic ABI. Apps and language bindings load
liblegion.soand call exportedlegion_*symbols. This is the 129-symbol set above. - L3 = in-process header contract.
sdk.h(module register/dispatch, the L3 KV wrapper, caps/membership queries) andlegion_identity.h(cert/identity primitives) are headers for module authors, not dynamic-ABI symbols. They are deliberately hidden in the.so:- Built-in modules and tools that link the static
liblegion.a(default visibility) resolve them directly. - A dynamically-loaded (
dlopen) module resolves them against the host executable — the runtime/tools link the static archive with--whole-archive --export-dynamic, putting the L3 symbols in the executable's dynamic table; the module'slegion_module_init_fn_v2init receives alegion_plugin_manager_t *handle to register against — not fromliblegion.so.
Sonm -D liblegion.socorrectly shows none oflegion_module_register,legion_l3_kv_set,legion_l3_caps_query, etc. That is by design — the L3 contract lives in headers + CI gates (check_sdk_header_no_runtime_symbols.sh,check_module_includes.sh), not in the dynamic symbol table. - Built-in modules and tools that link the static
How the boundary is enforced (defense in depth)
LEGION_APIdecoration (legion_compat.h) — expands to__attribute__((visibility("default")))(POSIX) /__declspec(dllexport)(Windows). Only decorated declarations are intended to export.C_VISIBILITY_PRESET hiddenon thelegion_sharedtarget (CMakeLists.txt) — every symbol from liblegion's own translation units that is notLEGION_APIbecomes hidden. (Landed Foundation 06.04.)-Wl,--exclude-libs,ALLonlegion_shared— drops every static-archive symbol from the dynamic table. Without this, the statically-linkedlibsodium.are-exported ~300crypto_*/sodium_*/randombytes_*symbols straight through (visibility=hidden only governs liblegion's own objects). Safe because every Legion binary links static sodium directly (PUBLICon thelegiontarget), so nothing relies on the re-export. (GNU ld + lld — Linux, Android, and other ELF targets; skipped on Apple ld64.)- Header install split (
CMakeLists.txtinstall block) — public headers are installed under${INCLUDEDIR}/legion/; the internal headerslegion_wire.h(L2 wire),legion_vm.h(consensus-VM),legion_lnmp.h(LNMP), andlegion_route_reply.h(route-query wire DTO — on the L4 include allow-list for in-tree tools but not shipped) are deliberately not installed — they carry below-L4 internals.
CI gates that keep it locked
| Gate | Asserts |
|---|---|
check_internal_symbols_hidden.sh | Curated internal symbols are absent, and the .so exports only legion_* (the positive boundary assertion — catches a reintroduced libsodium leak or a dropped --exclude-libs). |
check_ffi_symbols.sh, check_l4_node_symbols.sh, check_l4_util_symbols.sh | The durable L4/FFI surface is present (positive guard against accidental removal). |
check_sdk_header_no_runtime_symbols.sh, check_module_includes.sh | The L3 SDK header contract / module include allowlist. |
check_install_files.sh, check_pkgconfig.sh | The installed-header set and legion.pc pkg-config surface. |
Stability & draft status
The L3/L4 public surfaces are drafts, not release candidates. They are documented and locked-in-shape now so they can be reviewed; names, signatures, and groupings may still change.
Two different things are "locked" to different degrees:
- The export boundary — LOCKED (and CI-enforced).
liblegion.soexports onlylegion_*(no bundled third-party, no internal symbols), guaranteed byLEGION_API+C_VISIBILITY_PRESET hidden+-Wl,--exclude-libs,ALLand thetest_internal_symbols_hiddengate. This invariant will not regress. - The symbol set — PRE-RELEASE DRAFT. Which symbols are exported and their exact signatures are expected to change as the L3/L4 APIs are reviewed. No formal "no removal / no signature change" promise applies yet.
The formal ABI-stability promise below takes effect from the first tagged release; until then, treat the surface as movable:
- From the first release, within a major version: symbols are not removed, signatures do not change, and new symbols may be added.
- Hidden symbols (everything not exported) carry no stability contract and may change or disappear at any time.
- Internal headers (
legion_wire.h,legion_vm.h,legion_lnmp.h,legion_route_reply.h) are not part of the contract even if present in a source checkout.
Auditing / regenerating
# Current exported public ABI (should be only legion_*):
nm -D --defined-only build/liblegion.so | awk '{print $3}' | grep '^legion_' | sort
# Prove nothing else leaks (the CI gate, runnable locally):
BUILD_DIR=build bash tests/check_internal_symbols_hidden.sh
# Count by category:
nm -D --defined-only build/liblegion.so | awk '{print $3}' | grep '^legion_' \
| sed -E 's/^(legion_ffi)_.*/\1_*/; s/^(legion_node)_.*/\1_*/' | sort | uniq -c
