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:

MacroDeclares
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:

MessagePurpose
resolveSelect the implementation file for a module on this target, and what it needs
compileAdd or replace compile inputs for one source unit
linkAdd 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.

OutputKey inputs
Object fileTarget facts, compiler argv, source content, dependencies, runtime headers, cake hash, flags, defines, cache inputs, build type, linkage
DylibObject hashes, linker argv, libraries, frameworks, rpaths, link cache inputs
Static binaryRuntime 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.