Providers and Lanes

An endowment is the authority an actor holds; a provider is the C that actually does the work behind it. This page is the low level: how a provider hands results back to an actor, which thread it runs on, and how it owns a scarce OS slot. The user-facing side is in Endowments.

Providers are direct platform hooks

A provider is a thin, honest C wrapper over the target’s real surface — $playdate_file, $iocp, $kqueue, $coreaudio — on the same module interface every other C module uses. There is no portable abstraction layer beneath them and no pretending. Portability is assembled above providers, in pit, as packages — std::file over the per-target file endowment, networking packages over their raw sockets.

“Honest” has a precise boundary, and it is the compilation boundary. A provider exists on a build when its package compiles there, so a recipe that names no package holding a socket genuinely has no socket endowment and a program claiming one fails to build. Within a package that does compile, a call answers for itself: $posix_spawn is present on iOS and returns -1 exactly as the operating system does. Passing the OS’s own answer through is the honest thing — a stub that lied about succeeding would not be, and neither would pretending the surface was absent when it compiled. See Endowments.

This is the two-sided rule stated in Capabilities, seen from the C side: pit code arrives through use and rides inside the bundle; native power arrives through an endowment and belongs to the host. After shipping, the endowment is the only lever — which is why providers are granted, never use-able.

Four delivery shapes

A provider hands work back to an actor in one of four ways, and every provider on every target uses one of them.

One-shot completion. Submit now, the provider answers once, later — a file read, a socket connect, a name resolution. The result arrives as a single signal the actor handles in a later turn.

Event stream. Recurring events delivered as messages: button presses, a crank, window events, filesystem-watch notifications. The provider raises a signal each time, and the actor handles each in its own turn.

Actor call gate. A host callback that needs a value back from pit — a synchronous answer — goes through a retained, actor-serialized gate. The call runs on the actor’s own lane so it can enter the VM safely; a hard-realtime callback never takes this path, because it cannot wait for a turn.

Realtime pull. Some platform surfaces pull data on a realtime thread the VM can never run on — an audio render callback, a synth source. For these the boundary is a ring buffer: pit pushes samples in, the realtime callback drains them, and a low-water mark fires an event-stream signal meaning “feed me.” One shared single-producer/single-consumer ring primitive serves all of them, so providers do not hand-roll lock-free code. Where a platform’s real surface is push rather than pull, the provider endows push directly and skips the ring.

The first two are the common cases; the last two exist for host callbacks that need a return value or that run somewhere the VM cannot.

Execution lanes

A provider runs on a lane — a scheduling context — and the lane is part of the endowment’s honest description, not something the program arranges. The provider’s own C file declares it with PIT_USE_LANE(main), beside PIT_USE_LEASE for an exclusive slot, because this shim has to be pinned to a thread is a fact about that C. Most work is normal. Some is main, because the platform demands it: an AppKit or Win32 window, a Web DOM surface, and Web Audio must run on the main thread. A few targets add their own host-callback lanes.

Because a lane is a fact of the endowment, holding an endowment can pin an actor to a thread. An actor granted $appkit_window is pinned to the main thread at spawn, and every lane an actor’s endowments claim is combined before it boots. The idiomatic result is a small window-owner actor that runs on the UI thread, receives ordinary messages, and drives the window — the UI-thread-plus-message-passing pattern, written in pit instead of a C-side main-thread pump. A lane is scheduling policy; it is not a fifth delivery shape.

A target names its own lanes. On desktop the main-thread lane carries the window, graphics, audio, and input providers that the platform requires there. The Playdate registers every raw provider on a single playdate.frame host-callback lane; its hard-realtime sound callback touches only the provider’s preallocated ring and signals that lane, never entering the VM on the audio thread.

Leases

Authority to hold an endowment is separate from ownership of an actual scarce OS slot. Most providers need no lease: two actors can each open files, each open sockets, each render ordinary audio, and their handles are simply actor-owned. There is no blanket “one live actor per endowment” rule.

A lease applies only when a provider activates a genuinely exclusive slot — a single frame hook, a microphone capture, one exclusive device. The lease is actor-owned and process-atomic: acquired when the concrete slot is opened, released on close or when the actor halts. It protects the slot, not the endowment. Merely receiving an endowment never creates one.

A slot that the host supplies exactly once carries a lease for that reason. $watchkit_interface holds the watchkit.interface lease because the watch offers one concrete interface, and the sealed-mobile window endowments — $uikit_window, $watchkit_interface — expose only a reduced surface (create, destroy, size) rather than the title, position, and resize setters a desktop window provider carries, because the platform itself offers no more.

Handles are actor-owned

Every resource a provider hands out — a file handle, a socket, a window, a subscription, an operation token — is an actor-owned opaque record. It belongs to one actor’s heap, and its finalizer closes the underlying OS resource when the record becomes unreachable or the actor stops. That is what lets an actor’s teardown release its providers cleanly: stopping the actor drops its handles, and dropping a handle closes what it held. Each provider owns its own handle representation rather than sharing a universal one, so a target’s real surface shows through instead of being flattened into a common shape.