Skip to content

Modules & shared libraries

On every target with a dynamic loader — the native hosts (arm64, x86_64, win64) and arm9 (XTOS) — a program can be split into a shared library and its clients. The library is a real .dylib / .so / .dll; the client is type-checked against the actual binary, not against a header that might have drifted from it.

Terminal window
# build the library (native host — add -A arm9 etc. to cross-compile)
xcc --emit-lib -o libXtg.dylib xtg.xc
# build a client against it
xcc -L . -o app app.xc

--emit-lib also writes a sibling .xtc.iface describing the classes, protocols, structs and enums the library exports; that is what #import <Lib> reads. The 6502 and m68k targets have no dynamic loader and remain whole-program.

#import <Xtg> // resolves to libXtg.so on the -L search path
i16 main(void)
{
XGButton* b = new XGButton(); // a class from the library
b.setAction(&onClick); // …taking a bound method from HERE
return 0;
}

--emit-lib embeds a description of the library’s public API in the .so itself (an .xtc.iface section). #import <Lib> reads it back. There is no header file to keep in sync, and nothing to get out of step: the types you compile against are the types the library was built with.

Everything a library realistically exports:

classeswith inheritance, static methods, and virtual dispatch — including the library calling back into a client subclass
protocolswith working dispatch across the .so — see below
structsby value, in and out of a library method
enumsboth the constants and the type name
free functions, typedefs
weak: fieldsincluding weak: act_t^ — a weak bound method, i.e. target/action
bound methods (^)widened plain functions and bound &obj.method alike
C types from another libraryby reference, not by copy — see below

A protocol method’s dispatch index is its position in the protocol’s own declaration, so every module derives it identically with no coordination — and the protocol’s identity is a hash of its name, a value rather than an address.

That matters because two libraries built independently — neither importing the other — have no way to agree on a shared slot number. They don’t have to. A class conforming to a protocol from each dispatches correctly through both.

A binding library — one whose job is to expose a C library — hits a subtle problem. If libXtg exposes GEM’s OBJECT, that type belongs to libGEM, not to Xtg, and Xtg has no business describing it.

So it doesn’t. The interface records only where the type lives ("cImports": ["GEM"]), and the client re-imports the same libGEM.so through the same DWARF reader. One source of truth. Copying the layout instead would let two libraries silently disagree about OBJECT after a header change — and a layout disagreement across a .so is the worst failure there is, because nothing type-checks it and nothing reports it.

The client does not have to know: importing libXtg pulls libGEM in for you.

A global belongs to the module it was compiled in. To refer to one that lives in another module, say so:

extern u16 gCounter; // DEFINED in the library; reserve no storage here
i16 main(void)
{
gCounter = (u16)10; // the app writes it…
bump(); // …the library increments it…
return (i16)gCounter; // …and both see the same variable. 11.
}

Without extern, a plain declaration would define a second copy in the client, and writes through it would never reach the library’s — silently. extern is the word for that distinction, and an imported library’s globals are not injected without it: referring to one you have not declared extern is an honest “Undefined identifier”.

extern globals may not have an initialiser — the defining module owns that.

#import <Foo> also works for a plain C .so. There is no .xtc.iface in one, so xcc reads its DWARF instead and brings in its functions, its types, and its enum constants:

#import <GEM>
if (obj.ob_state == OS_DISABLED) { … } // OS_DISABLED comes from the .so
  • 6502 and m68k are whole-program — they have no dynamic loader, so --emit-lib does not apply there.
  • On macOS, a dylib currently caps at 255 exported symbols. The Mach-O export trie the linker writes is a single flat node, and childCount is one byte. A library that imports Foundation exceeds this, and the build warns (>255 exports, trie truncated) but still produces a .dylib — whose missing symbols then surface as a dyld: Symbol not found in the client, at run time. Keep a macOS library small until compiler bug 042 is closed; ELF and PE have no such limit.
  • A library and its dependencies compose, transitively. Two libraries built in complete ignorance of each other also compose — that is what the protocol design above is for.