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.
What a library is, and how it differs from a program, before you set the tooling up.
The short version#
Lay the repository out
Sources in
src/, headers at the root, and anexports.txt.Write exports.txt
One exported function per line. It is an append-only ABI.
Write the manifest
A library table in
libifymode, and no build command.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#
[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 1A 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.
# DEMO library exports
# Append only. Never reorder, never remove.
demo_init
demo_draw
demo_shutdownLibLoad 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.hAdd --prefix demo_ to only consider declarations following a naming convention.
Versioning, retiring functions, and what actually breaks.
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 installThat 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
- bin
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.txtThat 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#
| Command | Purpose |
|---|---|
build | compile, process and assemble into the appvar plus the stub |
emit | generate the assembly output without running fasmg |
deps | show how each external symbol resolves |
exports | validate exports.txt against header declarations |
package | create a release zip with the appvar, the stub and the headers |
install | copy the built stub and headers into CEdev |
clean | remove object and output directories |
selfinstall | install 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.
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
.8xvlarger 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);There is no libc
Only functions the TI-OS exports are available.
snprintfand 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:
Compile and link to one assembly file
Sources go to LLVM bitcode, are linked into a single module, and emitted as one assembly file.
Strip unsupported directives
.section,.ident,public,externandprivateare removed.Re-encode string literals
Byte by byte, because fasmg stops at an embedded NUL.
Bind compiler helpers
__frameset0,__lmulu,_memcpyand similar are bound to TI-OS entry points.Provide what the OS lacks
__indcallhland_atomic_load_32are supplied by the tool.Match dependency call sites
Calls into other libraries are matched to the names those libraries export.
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:
| Route | Contents |
|---|---|
/d/<slug>/raw | the appvar body, which is what Swoop writes to the calculator |
/d/<slug>/lib | the 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.
For a library that has outgrown a generated command.