Skip to content

The export table is an ABI

LibLoad resolves imports by ordinal, so exports.txt is append only. Reordering it breaks every program already compiled against the library.

exports.txt lists the functions a LibLoad library makes available. It looks like a convenience file. It is not: it is the library's binary interface.

How resolution actually works#

LibLoad does not resolve imports by name at runtime. It resolves them by ordinal, meaning by position in the export table.

When a program is compiled against your library, the linker records "call entry 2 of DEMO". When it runs, LibLoad loads the appvar and jumps to whatever entry 2 is now.

exports.txt
demo_init      # ordinal 0
demo_draw      # ordinal 1
demo_shutdown  # ordinal 2

The rule#

Safealways

Appending a new name to the end. Existing ordinals do not move.

Neverbreaks callers

Reordering, removing, or inserting anywhere but the end.

File format#

One name per line. Blank lines and lines starting with # are ignored, so comments are free.

exports.txt
# DEMO library exports.
#
# APPEND ONLY. Never reorder, never remove a line.
# Retired entries stay in place as stubs.

demo_init
demo_draw
demo_shutdown

# --- added in library version 2 ---
demo_draw_scaled
demo_set_palette

Retiring a function#

You cannot remove the line. Keep it, and make the implementation harmless:

src/demo.c
// Retired in library version 3. The ordinal has to stay so that programs
// compiled against versions 1 and 2 keep resolving correctly.
void demo_old_draw(void) {
}

Callers that were compiled against it keep working, badly but predictably. New callers do not see it because you removed the declaration from the header.

Checking the file against your header#

ce-libify will compare the two for you:

ce-libify exports --exports exports.txt --header demo.h

That catches the two ordinary mistakes: a function declared in the header but never exported, which fails to link for callers, and a name in the export table that no longer exists, which fails to build for you.

Filter by naming convention when the header also declares internal helpers:

ce-libify exports --exports exports.txt --header demo.h --prefix demo_

The library version number#

[library] version in the manifest is passed to ce-libify as the library version. It is metadata that travels with the appvar, not something LibLoad enforces, and it is independent of the [program] version that names the release.

A reasonable convention:

  1. Bump the program version on every release

    1.0.0, 1.0.1, 1.1.0. This is what people see on Roost.

  2. Bump the library version when you append exports

    So that a caller can tell at a glance whether the appvar on the calculator is new enough to have the function it wants.

  3. Never bump the library version to signal a break

    There is no mechanism that would act on it. If you truly must break the ABI, publish a new library at a new address.

Breaking it anyway#

If the ABI has to change incompatibly, the honest move is a new project with a new address and a new LibLoad name. Both are permanent for exactly this reason: demo-library keeps meaning what it meant, and demo-library-2 is a thing people opt into.

Addresses and slugs

Why nothing in CEagle is ever released and reissued.

Read on
CEagle documentation