Legion

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 and architecture.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)

CategorySymbol prefixCountHeaderRole
FFI subsystem bridgeslegion_ffi_*101legion_ffi_{audio,bridge,connection,display,focus,identity,js,node,objectstore}.hLanguage-binding entrypoints: opaque handles, explicit memory ownership, no internal structs. The bulk of the surface.
L4 node lifecyclelegion_node_*14l4_node.hTyped C wrappers over node operations: create_from_file, start, stop, destroy, reload, and getters (get_node_id, get_peers, format_status, …).
FFI helperslegion_params_*, legion_result_*, legion_string_*7legion_ffi.hGlue for languages without native map types: the params builder, result container free, string length.
Library baselegion_version*, legion_build_id, legion_free, legion_log_set_*, legion_config_find9legion.hVersion/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.so and call exported legion_* 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) and legion_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's legion_module_init_fn_v2 init receives a legion_plugin_manager_t * handle to register against — not from liblegion.so.

    So nm -D liblegion.so correctly shows none of legion_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.

How the boundary is enforced (defense in depth)

  1. LEGION_API decoration (legion_compat.h) — expands to __attribute__((visibility("default"))) (POSIX) / __declspec(dllexport) (Windows). Only decorated declarations are intended to export.
  2. C_VISIBILITY_PRESET hidden on the legion_shared target (CMakeLists.txt) — every symbol from liblegion's own translation units that is not LEGION_API becomes hidden. (Landed Foundation 06.04.)
  3. -Wl,--exclude-libs,ALL on legion_shared — drops every static-archive symbol from the dynamic table. Without this, the statically-linked libsodium.a re-exported ~300 crypto_*/sodium_*/randombytes_* symbols straight through (visibility=hidden only governs liblegion's own objects). Safe because every Legion binary links static sodium directly (PUBLIC on the legion target), so nothing relies on the re-export. (GNU ld + lld — Linux, Android, and other ELF targets; skipped on Apple ld64.)
  4. Header install split (CMakeLists.txt install block) — public headers are installed under ${INCLUDEDIR}/legion/; the internal headers legion_wire.h (L2 wire), legion_vm.h (consensus-VM), legion_lnmp.h (LNMP), and legion_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

GateAsserts
check_internal_symbols_hidden.shCurated 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.shThe durable L4/FFI surface is present (positive guard against accidental removal).
check_sdk_header_no_runtime_symbols.sh, check_module_includes.shThe L3 SDK header contract / module include allowlist.
check_install_files.sh, check_pkgconfig.shThe 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.so exports only legion_* (no bundled third-party, no internal symbols), guaranteed by LEGION_API + C_VISIBILITY_PRESET hidden + -Wl,--exclude-libs,ALL and the test_internal_symbols_hidden gate. 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

Copyright © 2026 Kultivator Consulting Ltd. All rights reserved.

legios.cloud