Skip to content

Setting up ce-libify

Turn ordinary C sources into a LibLoad library — an appvar, a linker stub and public headers — locally and in Flight.

ce-libify compiles ordinary C sources into a LibLoad library for the TI-84 Plus CE. It produces two files: an appvar, NAME.8xv, which is the library body the calculator loads, and a stub, NAME.lib, which the linker resolves other programs against.

Flight has ce-libify installed in the build image, so a library project usually needs no local installation at all. Install it locally when you want to compile and test before pushing.

LibLoad libraries

What a library is, and how it differs from a program, before you set the tooling up.

Read on

The short version#

  1. Lay the repository out

    Sources in src/, headers at the root, and an exports.txt.

  2. Write exports.txt

    One exported function per line. It is an append-only ABI.

  3. Write the manifest

    A library table in libify mode, and no build command.

  4. Push

    Flight runs ce-libify for you and publishes the appvar, the stub and the headers.

Repository layout#

The defaults assume this shape, and following it means you write almost no configuration.

  • your-library
    • .ceagle-flightthe manifest
    • exports.txtthe ABI, append only
    • demo.hthe public header
    • src
      • demo.c
      • internal.c
    • binproduced by the build, gitignored

Every one of those paths is configurable, but if you match the layout you only have to name the library.

The manifest#

.ceagle-flight
[program]
name = "Demo Library"
version = "1.0.0"
summary = "Two functions other programs can call."
description = """
A LibLoad library built from ordinary C sources.

Programs list it under depends and Swoop sends it along.
"""
categories = ["libs"]
tags = ["libload"]
author = "Ada Lovelace"

[library]
mode = "libify"
name = "DEMO"
version = 1
src = "src"
include = "."
exports = "exports.txt"
headers = ["demo.h"]
optimize = "-Oz"

[build]
timeout = "10m"

The command Flight generates#

From the manifest above, Flight runs:

ce-libify build \
  --name DEMO \
  --src 'src' \
  --headers '.' \
  --exports 'exports.txt' \
  --bin bin \
  --optimize -Oz \
  --lib-version 1

A library this one depends on adds --cedev and one --depend per stub, pointing at the copies Flight staged for the build. The outputs are always bin/DEMO.8xv and bin/DEMO.lib, which is why appvar and stub are not read in this mode.

exports.txt#

The export table is the library's ABI. One name per line; blank lines and lines starting with # are ignored.

exports.txt
# DEMO library exports
# Append only. Never reorder, never remove.
demo_init
demo_draw
demo_shutdown

LibLoad resolves imports by ordinal, meaning by position in this file. That has one consequence worth stating plainly:

To retire a function, leave its line in place and make the implementation a stub. To check the file against your header:

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

Add --prefix demo_ to only consider declarations following a naming convention.

The export table is an ABI

Versioning, retiring functions, and what actually breaks.

Read on

Installing it locally#

You need Go 1.26 and Python 3. Python is used only to fetch fasmg.

git clone https://github.com/alessiodam/toolchain_libify
cd toolchain_libify
make install

That builds the tool, retrieves fasmg and its ez80 macro packages, and installs them into your CEdev directory:

  • $CEDEV
    • bin
      • ce-libify
      • fasmg
    • meta
      • libify*.inc and *.alm

Point it somewhere else with make install CEDEV=/path/to/CEdev, and undo it with make uninstall.

Building locally#

ce-libify build --name DEMO --src src --headers . --exports exports.txt

That writes bin/DEMO.8xv and bin/DEMO.lib.

To use the library from an ordinary CE program on your own machine, copy the stub and the headers into the toolchain, then send the appvar to the calculator:

cp bin/DEMO.lib "$CEDEV/lib/libload/"
cp demo.h "$CEDEV/include/"

Depending on other libraries#

A library may link against other LibLoad libraries. They are declared once, under [program], by Roost address:

[program]
name = "Demo Library"
version = "1.0.0"
depends = ["usb-driver", "my-graphics-library@2.1.0"]

[library]
mode = "libify"
name = "DEMO"

Flight downloads the developer archive each address resolves to and unpacks the stub and the headers before the build starts. Nothing is rebuilt, and a bare address takes the newest release while address@version pins one.

Locally, where you hold the stub yourself:

ce-libify build --name DEMO --src src --headers . \
  --exports exports.txt --depend usbdrvce

--depend takes a library name from $CEDEV/lib/libload/ or a direct file path, and is repeatable. ce-libify rewrites call sites to match the dependency's definitions.

Every ce-libify command#

CommandPurpose
buildcompile, process and assemble into the appvar plus the stub
emitgenerate the assembly output without running fasmg
depsshow how each external symbol resolves
exportsvalidate exports.txt against header declarations
packagecreate a release zip with the appvar, the stub and the headers
installcopy the built stub and headers into CEdev
cleanremove object and output directories
selfinstallinstall or remove ce-libify itself

ce-libify <command> --help prints the flags for one command, and ce-libify --version prints build information.

Constraints that will bite you#

These are properties of LibLoad libraries, not of the tool, and they are the two things that most often surprise people writing their first one.

  1. There is no BSS heap

    Every static buffer is carried inside the appvar and copied into user RAM when the library loads. A large static buffer therefore makes the .8xv larger and costs RAM on every load.

    Prefer caller-supplied buffers when size matters:

    // Costs 4 KiB in the appvar and 4 KiB of RAM, always.
    static uint8_t scratch[4096];
    
    // Costs nothing until a caller decides it wants it.
    void demo_render(uint8_t *scratch, size_t length);
  2. There is no libc

    Only functions the TI-OS exports are available. snprintf and friends have to be avoided or implemented by hand.

    Check what is actually resolvable:

    ce-libify deps

What the tool does to the assembly#

Worth knowing when a build fails in an unfamiliar way. The C compiler does not emit something fasmg can assemble, so ce-libify rewrites it:

  1. Compile and link to one assembly file

    Sources go to LLVM bitcode, are linked into a single module, and emitted as one assembly file.

  2. Strip unsupported directives

    .section, .ident, public, extern and private are removed.

  3. Re-encode string literals

    Byte by byte, because fasmg stops at an embedded NUL.

  4. Bind compiler helpers

    __frameset0, __lmulu, _memcpy and similar are bound to TI-OS entry points.

  5. Provide what the OS lacks

    __indcallhl and _atomic_load_32 are supplied by the tool.

  6. Match dependency call sites

    Calls into other libraries are matched to the names those libraries export.

  7. Emit the export table

    Export aliases and the table itself go after the code body.

An unresolved symbol stops the build with the location that needs it, rather than a fasmg error a long way from the cause.

What Flight publishes#

A successful library build writes two things into Roost:

RouteContents
/d/<slug>/rawthe appvar body, which is what Swoop writes to the calculator
/d/<slug>/libthe developer archive: the appvar, the stub and every public header, flat

The archive is staged by Flight from the files the manifest names, so both library modes leave exactly the same thing behind.

Building a library yourself

For a library that has outgrown a generated command.

Read on
CEagle documentation