Extensions
This is a shop-layer design note for the native extension system implemented by the canonical shop. C and C++ modules are not part of the portable ƿit language; another shop may provide no native extensions, prebuilt-only extensions, or a different native provider mechanism.
An extension is a native module, and it is named the same way every other module is —
<package>::<path>, where the path is the source file without its extension:
pitlib::json
pitlib::internal/time
shoplib::nota
gitea.example/alice/image::png
gitea.example/alice/image::formats/png
No package is spelled differently from any other, and there is no key beside the path. The file’s location is its name, so the shop finds a package’s C modules by listing the directory and nothing restates the result.
Extensions can be built as dynamic libraries for development or compiled into a static pit
binary for targets where dynamic loading is unavailable or undesirable. The shop’s
c_native list decides which packages may compile C. It is a genuine gate that blesses by
naming: a hardened shop lists exactly the packages whose C it will build, a development shop
lists broadly, and a package on neither list compiles nothing. Absence is never permission.
Package Layout
package/
package.json aliases, modules, compilation
cake.ce optional build actor
include/ automatically added include directory
source/*.c support sources
*.c native modules
dir/*.c native modules
internal/*.c native modules, package-private
Every C/C++ file outside source/ is a module of the package. source/ is the one
exception, and the rule is the folder rather than the file: if a package compiles any C
module, it is compiled with everything in that package’s source/. There are no per-file
support lists, and a helper is not distinguished from a module by anything but where it
sits.
internal/ is the package-private directory. Its modules are named from inside the owning
package with the bare, package-relative use('internal/time'), and <other>::internal/time
is refused — so pitlib/internal/time.c is reachable from pitlib/time.cm and from nowhere
else. See Shop.
The whole chain, with no table in it:
pitlib/internal/time.c provides the native functionality
pitlib/time.cm use('internal/time') — allowed: pitlib may compile C
— findable: it is right there
use('time') → pitlib, because pitlib is a fallback package
A module whose only job is to re-export a C file should not exist. If nothing wraps the C,
put the .c at the package’s top level and let the locator reach it directly; internal/
is for C that a real module wraps.
Target Model
A target is a recipe — a committed JSON file naming a platform package, the stamp
levers, the build levers, and the shop’s lists. Nothing in cake takes a target name to mean
“these things”: the name is something a recipe declares, and cake carries it without
interpreting it. shoplib::recipe is the one reader, and the schema is
Recipes; the fields matter here only in that cake reads them as
data rather than deriving them from a string.
The system token a recipe declares is what a package’s own module arms are matched
against, and that is the one place a target name legitimately appears outside a recipe — the
package declaring how it varies, matched against the name the recipe handed down. The
package speaks; cake only carries. “I want to build for ios” must never reach code that
knows ios means darwin_file.
Recipes exist for desktop, web, Apple simulator and device builds, consoles, and embedded SDKs. Whether a given recipe builds is answered by running its toolchain: missing SDKs or compilers, and headers that do not exist on that platform, are ordinary build errors. The toolchain is the gate — no recipe carries a list of what is possible, and committed recipes are worked suggestions rather than permissions.
Source Selection
The path is the name, so selection is a walk:
json.c -> <pkg>::json
net/color.c -> <pkg>::net/color
Target-specific choices are explicit and live in the manifest’s modules arms, the same
mechanism that selects a .cm per target. Nothing is guessed from a suffix such as
_posix, _darwin, _apple, _desktop, or _arm64. Below that layer, where two
platforms share no code, the package is the arm and there is nothing to select: a target
has $memfs precisely because memfs.c is in that platform’s package and in no other.
An endowment’s C file states the two things a filename cannot — where the provider must run, and whether it holds an exclusive OS slot — in the file itself:
/* canvas.c */
PIT_USE_LANE(main)
/* playdate_microphone.c */
PIT_USE_LEASE(playdate.microphone)
Both expand to nothing and are read by the same scanner that reads PIT_USE_PROBE and
PIT_USE_BOOT; the build renders the result into its output as data, and no consumer
reconstructs it. The facts a C file declares about itself are exactly these four:
| Macro | Declares |
|---|---|
PIT_USE_PROBE(<lib>) | compile this module only where that host library is detected |
PIT_USE_BOOT(<fn>) | run this function when the module is brought up |
PIT_USE_LANE(<lane>) | this provider must run on that lane |
PIT_USE_LEASE(<slot>) | this provider holds that exclusive OS slot |
lane is normal, main, or a named host-callback lane; a module declaring none runs
normal. Before boot, the
shop combines the descriptors for every endowment claimed by the executable
and pins the actor to the resulting lane. Two different fixed lanes are a
start error. Named host lanes must have a runtime runner registered by that
target. Target startup registers a named lane with
pit_execution_lane_register; the provider retains the returned opaque handle
and calls pit_execution_lane_drive only from its owning host callback. The
shop can test the name for availability, but it cannot drive the lane. Actor
top-level startup is itself queued onto a named lane, so importing and
activating a raw provider cannot accidentally run on the shop worker.
PIT_USE_LEASE names a concrete singleton OS slot; it is descriptive,
not an authority or an eager lock. Merely receiving an endowment never claims
the slot. The native provider acquires its actor-owned lease only when the
actor activates that facility, and releases it on explicit close or actor
halt. This permits, for example, many actors to use Playdate sound objects
while only one active capture owns the physical microphone callback.
There is no second selection mechanism and none is planned. A modules arm is how a module
varies by target; anything else — a magic directory, a filename suffix, a per-target
override table — would be a second answer to a question that already has one.
cake.ce Protocol
If a package contains top-level cake.ce, the builder starts it before building
the package’s extensions. Cake can fetch dependencies, inspect the target, and
return selected files, flags, and link additions. It must not require native
extensions from its own package because they do not exist yet.
The protocol has three message types:
| Message | Purpose |
|---|---|
resolve | Select the implementation file for a module on this target, and what it needs |
compile | Add or replace compile inputs for one source unit |
link | Add or replace linker inputs for a dylib, static archive, or executable |
Cake output affects cache keys through concrete outputs: flags, sources,
objects, libraries, frameworks, rpaths, and declared cache_inputs. Cake
replies do not need to be cached as separate durable artifacts.
Cake controls public build intent. It does not name modules — the path already does — and it does not choose C symbols.
Extension Symbols
One authority names symbols: shoplib::symbols. A module’s public name is its locator,
that module derives the C entry symbol from it, and the builder passes both into compilation:
-DPIT_USE_NAME=pit_pitlib_internal_time_use
-DPIT_USE_PATH="pitlib::internal/time"
-DPIT_USE_NAME=pit_gitea_example_alice_image_png_use
-DPIT_USE_PATH="gitea.example/alice/image::png"
The scheme is pit_<package>_<path>_use, with the package locator and module path converted
to C-safe identifiers.
Every rendered manifest carries the full symbol as data, and no consumer constructs one. A static extension table, a link list, a dylib lookup — each reads the symbol it was given. That is what makes the authority single: a second place that assembled a symbol from parts would be a second scheme, and the two would disagree the first time either changed.
Targets with symbol-length or naming restrictions take a deterministic shortened symbol such
as pitx_<hash> from the same authority, so the rule holds there too.
Loading
Dynamic extensions do not need constructor-based global registration. The shop opens the dylib and looks up the symbol the manifest carries.
Static binaries use an explicit generated extension table. Runtime lookup asks the current binary for its available internals. The user program sees only the locator; the provider path is shop/runtime policy.
Raw Endowment Providers
A .c file at the top level of a provider package may fulfill the raw endowment its
filename names: memfs.c can answer $memfs, and its package must be on endowments or
endowments_whole and permitted by c_native. The shop walks the loaded provider roots
while it builds the realization. If the module is already compiled into the binary it uses
that entry; otherwise the normal native builder compiles and loads it. The runner looks up
exactly what the realization row names and derives nothing.
$posix_file posix/posix_file.c
$playdate_file playdate/playdate_file.c
$memfs web/memfs.c
$iocp windows/iocp.c
$canvas web/canvas.c
$fsevents darwin/fsevents.c
Names such as posix, windows, darwin, web, and playdate are package identities in
the recipes. Their modules remain ordinary modules; provider selection adds a way to answer
claims rather than removing ordinary use() resolution.
These providers are thin descriptions of the host capability, not a
cross-platform abstraction. The uniform abstraction belongs in a Pit package
such as std; its manifest selects an implementation module for the
build target, and that implementation claims the appropriate raw provider.
The Playdate provider exposes SDFile operations directly, while the Windows
implementation consumes $iocp’s Win32 file-handle operations. Neither is
forced through a shared C “kernel” shape; std::file supplies the requestor
turn boundary and uniform behavior.
If policy approves a raw claim but the running binary has no matching registry entry, actor startup fails and names the missing endowment. Dynamic libraries may still inject a prefulfilled non-null value into the spawn context; this is the same late-binding contract, not a build-time substitution.
Build Outputs
All outputs go through the shop’s content-addressed build cache.
| Output | Key inputs |
|---|---|
| Object file | Target facts, compiler argv, source content, dependencies, runtime headers, cake hash, flags, defines, cache inputs, build type, linkage |
| Dylib | Object hashes, linker argv, libraries, frameworks, rpaths, link cache inputs |
| Static binary | Runtime object hashes, extension object hashes, generated static table, final link argv, build type |
The content hash is the authority. Filesystem stat data can only be a fast accept/reject hint.
Policy
An executable claims logical extension requirements. The policy actor decides whether the program may request them, and the shop decides whether the selected runtime can provide them. A static platform build may reject any program whose extension requirements are not present in its static table.