Packages
x2c has one flat namespace, so an included .x library merges its names into
yours. A package keeps its own space. It is compiled once into an archive,
and an importing program reaches its names through an alias.
What a package looks like
A package is a directory named for the package. Only its src/ directory
holds package sources; everything else in the directory consumes it like any
other program.
packages/greet/
Makefile PACKAGE := greet, then include ../package.mk
src/greet.x the entry unit; public surface above #pragma private
tests/ consumers that import the package
examples/
That is the whole layout for a package with no native dependency. One that wraps a C library adds the pin, licence texts, and profile that Wrapping a C Library covers.
The entry unit needs no manifest and no export list:
typedef struct GreetingData {
String subject;
int count;
} *Greeting;
Greeting Greeting.new(String subject);
String Greeting.line(Greeting greeting);
#pragma private
Greeting Greeting.new(String subject) {
Greeting greeting = Scope.malloc(sizeof(struct GreetingData));
greeting.subject = subject;
greeting.count = 0;
return greeting;
}
String Greeting.line(Greeting greeting) {
greeting.count++;
return %"hello, ${greeting.subject} (${greeting.count})";
}
greet.x spells its own names bare. Compiled as a package they become
greet__Greeting, greet__Greeting_new, and greet__Greeting_line, so a
second package may publish its own Greeting in the same program.
Building one
packages/package.mk holds the recipe. A package Makefile is two lines:
PACKAGE := greet
include ../package.mk
make build translates every src/*.x in package mode and produces:
builds/greet.h the generated public header
builds/libgreet.a the archive a consumer links
builds/greet.link the one line of extra link flags, empty when none
make test builds and runs tests/test-*.x, and make clean removes
builds/. A package with a pinned native dependency adds a dependency.json
and one more Makefile line, DEPENDENCY_PREFIX_VAR := GREET_PREFIX, naming
the variable that overrides the cache prefix; make prepare then fetches and
builds the dependency into a shared cache outside the worktree and leaves a
deps symlink in the package directory.
Using one
Register the directory that holds packages and import by name:
import "greet" as g;
int main(void) {
g.Greeting greeting = g.Greeting.new("x2c");
printf("%s", %"${greeting.line()}\n");
return 0;
}
x2c build --package-dir packages --output greeter greeter.x
The driver reads the unit’s recorded dependencies, adds the package’s
builds and src directories to the C include path, and links
libgreet.a together with the line in greet.link. It never rebuilds the
package. Build it once, and a read-only package tree still serves every
consumer. A target in x2c.toml can set package-dirs instead of passing
the flag.
as is optional; import "greet"; binds the alias greet. The alias is
only a way to spell names. The generated C always uses the package’s own
prefix.
Both clauses are optional and combine:
import "<package>" [as <alias>] [with <Name> [as <Local>] {, ...}] ;
A with clause drops the alias from names you use often:
import "greet" with Greeting; // bare Greeting
import "greet" with Greeting as Hello; // bare Hello
import "greet" as g with Greeting; // g.Greeting and Greeting
int main(void) {
Greeting greeting = Greeting.new("x2c");
printf("%s", %"${greeting.line()}\n");
return 0;
}
A with name is still only a spelling: Greeting.new compiles to
greet__Greeting_new. Any public name works, functions included. The alias
stays registered, so with never takes it away, and a local variable of the
same name shadows the binding. See the
language reference for what
does and does not cross an import.
An imported package may add a method to an external type. PCRE2, for example,
adds Var.regexpmatch, so value.regexpmatch() resolves after
import "pcre2". A method declared in the consumer still wins. If two
imported packages provide the same receiver method, the call is ambiguous;
call the selected package function explicitly, such as
pcre2.Var_regexpmatch(value).
That external type is often one from the package’s own vendored header, which
crosses the import under its own name. raylib’s Image and Vector2 are
raylib’s, so a consumer writes Image.new(...), reads image.width, and uses
a + b with no with entry for either type. The types come from the header
and the methods from raylib__.
Packages that wrap a C library
packages/yyjson/ is the worked example. It publishes the vendored
src/yyjson-0.12.h because four of its options methods take yyjson’s own flag
types, so a consumer needs the upstream header on its system include path:
make -C packages/yyjson prepare
make -C packages/yyjson build
make -C packages/yyjson test run run-lisp
x2c build --package-dir packages \
--c-system-dir packages/yyjson/deps/include \
--output catalog catalog.x
prepare verifies and builds the pinned native dependency. build creates the
package archive and link file. The three checks exercise the x2c API, example
applications, raw C header, and Lisp bindings. A program imports the ordinary
API; code that needs an unwrapped option calls yyjson’s real declarations
through yyjson-0.12.h, and a runtime Lisp session installs the package’s
group with JsonLisp.install. The package README states which values own
native storage and when borrowed views expire.
A source distribution carries the package directory, including src/,
dependency.json, LICENSES/, its README, examples, and tests. It does not
carry the ignored deps symlink or builds/ output. The recipient runs the
same prepare and build commands, then registers the directory containing
the package with --package-dir; this reproduces the native profile and keeps
the generated archive, public header, and link flags together.
A package over a third-party C library also has to decide what to expose, who owns each returned value, how to preserve the library’s error codes, and whether to provide Lisp bindings. That is Wrapping a C Library.
Package examples
The packages include example applications that use their x2c interfaces without requiring public services:
packages/libcurl/examples/endpoint-report.xperforms synchronous HTTP requests against a local fixture.examples/packages/http-json-releases/composes libcurl’s response bytes with yyjson parsing.packages/termbox2/examples/incident-filter.xhandles terminal input and rendering. Its standard run is driven through a pseudo-terminal.packages/blis/examples/page-rank.xexpresses PageRank with BLIS operators;risk-report.xshows the broader matrix workflow.packages/libuv/examples/process-report.xsupervises child processes;thread-notify.x,network-report.x,ipc-report.x, anddatagram-report.xexercise wakeups, TCP, named pipes, and UDP; andrelease-checks.xreads produced artifacts through asynchronous files.- raylib’s standard examples render PNG files in memory. Use
run-interactivefor the windowed showcase; package checks do not run it.
Run the package-local examples from the repository root:
make -C packages/libcurl run
make -C examples/packages/http-json-releases test
make -C packages/termbox2 run
make -C packages/blis run
make -C packages/libuv run
make -C packages/raylib verify