Typed channels and alt for syscall hosts.
These are the Dis channel primitives that a ported Limbo userland needs,
with the semantics of the Inferno interpreter. Guests reach them through
the sys:* facade (newchan, csend, crecv, alt). Hosts call this
module directly to create, alias and close channels on a guest's behalf.
What this ports
This is the BEAM home of the Dis channel primitives from libinterp/xec.c
(isend, irecv, cqadd/cqdel, cgetb/cputb) and libinterp/alt.c
(altrdy/altcomm/altdone). In Dis those ops run inside the single-threaded
interpreter loop, so a channel operation is atomic w.r.t. every other Prog: a
send either hands its value to a waiting receiver and wakes it, or blocks the
sender on the channel's send queue; a receive is symmetric; alt commits
exactly one ready op atomically.
On the BEAM each Inferno Prog is its own BEAM process (a hyperlang guest
worker), so the atomicity that Dis got from a single interpreter thread is
recovered by making each channel a serialising OTP process (the broker).
The broker pid is the C Channel*. Every csend/crecv/alt/nbsend/
nbrecv is one GenServer.call into the broker, so all the rendezvous
bookkeeping (which Prog is parked on send/recv, the buffer ring) happens
one message at a time — exactly the Dis invariant. The calling Prog's worker
process is what blocks (it waits for the broker's reply), never a busy-wait.
Rendezvous (unbuffered, size == 0)
isend: if a receiver is already parked (c->recv->member != nil) hand the value
over directly and wake it; else park the sender on c->send and block. irecv
is the mirror. Exactly one value is handed off per matched pair — no loss, no
double-take — because the broker matches under its single-process serialisation.
Buffered (size > 0)
A ring buffer of size slots (cputb/cgetb). Send blocks only when the
buffer is full AND no receiver waits; recv blocks only when the buffer is empty
AND no sender waits. A waiting receiver still takes precedence over the buffer
for a fresh send (matching isend: it checks c->recv->member before cputb).
alt
alt(ops) is Dis altrdy+altcomm+altdone: scan the ops, collect the set
that can fire right now; if any are ready, pick one fairly (uniformly at
random over the ready set, the BEAM stand-in for Dis's run-queue order) and
perform exactly that communication. If none are ready and there is a *
default, take it without blocking; otherwise the caller blocks until one op
becomes ready, then commits exactly one. nbsend/nbrecv are alt with a
single op plus a default.
Handles cross the guest boundary as integers, not pids
A broker pid cannot cross the hyperlang term boundary — the sanitizer rejects
pids, ports and refs as forbidden terms. So,
exactly like a file descriptor, a channel is handed to the guest as an opaque
non-negative integer handle. The handle → broker-pid map lives in a registry
keyed by the caller's opaque channel scope. The OS installs the
:guest_channel_scope hook when it wants guest-facing operations to resolve
handles; kernel clients can still pass raw broker pids directly.
What is DROPPED (Dis/BEAM already provides it)
- The Dis heap
Channeltype,H2D/D2Htagged-pointer plumbing, themover/mid/Typecopy machinery, andfreechan— BEAM terms are copied by message-passing and reclaimed by GC. Tarray-of-channels alt arms (==>over an array of channels);althere takes a flat op list.
Summary
Functions
Install handle in to_scope when it is a live channel capability in
from_scope.
Non-deterministic select over ops. See module doc for the op/return shapes.
Non-deterministic select without stripping the channel payload transport envelope.
True when pid is one of this module's channel broker processes.
Close an ephemeral broker and remove every registry handle that points at it.
Release the channel set owned by scope. Drops the scope's handle->broker
registry entries and TERMINATES every broker that is no longer referenced by
any other scope (a FORKNS-shared broker survives until its last scope closes).
Without this a closed lifetime leaks both
the broker process (under BrokerSupervisor) and a stale registry entry that
still resolves. A no-op if the registry is not started.
cnewc(t, mover, len) (libinterp/xec.c) — allocate a fresh typed channel with
len buffer slots (len > 0 = buffered, as devsrv uses for its reply/request
channels). t is the Dis value Type* (here the type TAG :rread/:rwrite/
:fileio_read/:fileio_write — the heap GC type is DROPPED, only the tag is
carried); mover is movtmp(). Returns the broker PID = the C Channel*.
Receive a value from channel broker c, blocking until one is available.
Returns the value.
Receive a value without stripping the channel payload transport envelope.
Send v on channel broker c, blocking (rendezvous / buffer-full) until the
value is taken or buffered. Returns :ok.
csendalt(c, value, len) (libinterp/xec.c) — a NON-BLOCKING send used by
devsrv's srvunblock to push a sentinel request onto a server's read/write
channel without parking. In Dis it grows the channel buffer if needed
(heaparray(t, len)) so the isend it performs never blocks. Faithful
mapping: hand the value to a parked receiver / armed alt-recv waiter if one is
ready, else buffer it (growing past size as needed — the Dis buffer-grow).
Always succeeds (returns 1, as the C returns 1 on the buffered path); c is
the broker PID, len the requested buffer length (-1 = grow on demand).
Resolve the current guest channel scope through the :guest_channel_scope
app-env hook.
Start the channel registry and broker supervisor (idempotent).
pctl(FORKNS) channel carry-over. Inferno channels are heap objects shared
across a proc and its children regardless of the namespace group, and FORKNS
reshapes only the namespace (Pgrp.scopecpy), never the channel set. So when
the namespace scope is replaced by a fresh one, re-key the forking proc's live
channel handles from old_scope into new_scope (same handle ints, same broker
pids) so its existing handles still resolve in the child. A no-op if either
scope is nil or the registry is not started.
Free a channel alias, revoking it and releasing its scope quota before return. A broker shared by another scope remains alive. The last alias schedules retirement in the broker itself, so the allocator never waits on broker supervision or a close hook. The hook runs once when retirement completes. Scope closure still waits for that lifetime's orphaned brokers directly.
Expose an existing channel broker through scope's integer-handle registry.
Pair the read/write brokers of one Sys->FileIO.
The movtmp mover sentinel (libinterp/xec.c movtmp). In Dis the channel
mover is a function pointer driving the typed value copy during send/recv;
on the BEAM message passing already copies the term by value, so the mover is
an opaque marker passed to cnewc/3 (kept 1:1 so cnewc(t, movtmp(), n)
reads as in the reference). DROPPED: the memmove/freeptrs/incmem copy.
Non-blocking recv: alt with one recv op and a default. {:ok, v} | :empty.
Non-blocking send: alt with one send op and a default. true if sent.
Create a channel of size buffer slots (0 = unbuffered/rendezvous) in process
scope. Returns {handle, broker_pid}; the handle is the opaque integer the
guest holds, the pid is the broker.
Resolve a channel handle to its broker pid within scope, or nil.
retype(c, hook) — the BEAM home of devsrv's srvretype heap-type swap
(libinterp freetype(h->t); h->t = t; t->ref++; c->aux = f). In Dis the swap
installs the close-hook (Trdchan/Twrchan's free callback =
freerdchan/freewrchan) and binds c->aux = f (the SrvFile*). The Dis
heap type / refcount is DROPPED (BEAM GC); the surviving behaviour is the
close-hook binding: hook is {module, fun, aux}, invoked as
module.fun(aux, swept) when the broker is collected (its owning process
exits → the broker's terminate/2). c is the broker PID.
Same as ensure_started/0.
Functions
Install handle in to_scope when it is a live channel capability in
from_scope.
Guest channel messages can carry channel handles created after pctl(FORKNS).
The handle is still the same Inferno heap-channel capability, but the receiving
Prog may be in a different namespace scope. Aliasing only from a scope that
already owns the handle preserves the capability boundary while letting the
handle cross explicitly in a channel payload.
Non-deterministic select over ops. See module doc for the op/return shapes.
Non-deterministic select without stripping the channel payload transport envelope.
True when pid is one of this module's channel broker processes.
Close an ephemeral broker and remove every registry handle that points at it.
This is the BEAM counterpart of devsrv's destroy(rc/wc) for per-request
Rread/Rwrite reply channels after the kernel receives the reply.
Release the channel set owned by scope. Drops the scope's handle->broker
registry entries and TERMINATES every broker that is no longer referenced by
any other scope (a FORKNS-shared broker survives until its last scope closes).
Without this a closed lifetime leaks both
the broker process (under BrokerSupervisor) and a stale registry entry that
still resolves. A no-op if the registry is not started.
cnewc(t, mover, len) (libinterp/xec.c) — allocate a fresh typed channel with
len buffer slots (len > 0 = buffered, as devsrv uses for its reply/request
channels). t is the Dis value Type* (here the type TAG :rread/:rwrite/
:fileio_read/:fileio_write — the heap GC type is DROPPED, only the tag is
carried); mover is movtmp(). Returns the broker PID = the C Channel*.
Unlike newchan/2 this does NOT install the channel in a scope handle registry:
these are kernel-internal Channel*s held by devsrv directly, never crossing
the guest term boundary.
Receive a value from channel broker c, blocking until one is available.
Returns the value.
Receive a value without stripping the channel payload transport envelope.
This is only for the guest boundary, which must inspect the source scope before exposing nested channel handles back to a guest.
Send v on channel broker c, blocking (rendezvous / buffer-full) until the
value is taken or buffered. Returns :ok.
csendalt(c, value, len) (libinterp/xec.c) — a NON-BLOCKING send used by
devsrv's srvunblock to push a sentinel request onto a server's read/write
channel without parking. In Dis it grows the channel buffer if needed
(heaparray(t, len)) so the isend it performs never blocks. Faithful
mapping: hand the value to a parked receiver / armed alt-recv waiter if one is
ready, else buffer it (growing past size as needed — the Dis buffer-grow).
Always succeeds (returns 1, as the C returns 1 on the buffered path); c is
the broker PID, len the requested buffer length (-1 = grow on demand).
Resolve the current guest channel scope through the :guest_channel_scope
app-env hook.
The hook may be a zero-arity function or {module, function}. Missing or bad
hooks fail closed with {:error, :no_scope}.
Start the channel registry and broker supervisor (idempotent).
This runtime starts on first use, so channel operations do not require the OTP application tree to have been booted.
pctl(FORKNS) channel carry-over. Inferno channels are heap objects shared
across a proc and its children regardless of the namespace group, and FORKNS
reshapes only the namespace (Pgrp.scopecpy), never the channel set. So when
the namespace scope is replaced by a fresh one, re-key the forking proc's live
channel handles from old_scope into new_scope (same handle ints, same broker
pids) so its existing handles still resolve in the child. A no-op if either
scope is nil or the registry is not started.
Free a channel alias, revoking it and releasing its scope quota before return. A broker shared by another scope remains alive. The last alias schedules retirement in the broker itself, so the allocator never waits on broker supervision or a close hook. The hook runs once when retirement completes. Scope closure still waits for that lifetime's orphaned brokers directly.
Expose an existing channel broker through scope's integer-handle registry.
Used when a kernel-internal FileIO request carries a reply Channel* to guest
code: the kernel keeps the broker pid, while the guest receives a scoped int
handle and replies through the normal sys:send/alt path.
Pair the read/write brokers of one Sys->FileIO.
Inferno's freeFileIO destroys both channel fields together. The BEAM port
uses integer scope handles for the guest view, so the channel brokers need an
explicit peer link: when the server Proc that first receives from either side
exits, the broker tears down its peer too, causing both devsrv close hooks to
run.
The movtmp mover sentinel (libinterp/xec.c movtmp). In Dis the channel
mover is a function pointer driving the typed value copy during send/recv;
on the BEAM message passing already copies the term by value, so the mover is
an opaque marker passed to cnewc/3 (kept 1:1 so cnewc(t, movtmp(), n)
reads as in the reference). DROPPED: the memmove/freeptrs/incmem copy.
Non-blocking recv: alt with one recv op and a default. {:ok, v} | :empty.
Non-blocking send: alt with one send op and a default. true if sent.
Create a channel of size buffer slots (0 = unbuffered/rendezvous) in process
scope. Returns {handle, broker_pid}; the handle is the opaque integer the
guest holds, the pid is the broker.
Resolve a channel handle to its broker pid within scope, or nil.
retype(c, hook) — the BEAM home of devsrv's srvretype heap-type swap
(libinterp freetype(h->t); h->t = t; t->ref++; c->aux = f). In Dis the swap
installs the close-hook (Trdchan/Twrchan's free callback =
freerdchan/freewrchan) and binds c->aux = f (the SrvFile*). The Dis
heap type / refcount is DROPPED (BEAM GC); the surviving behaviour is the
close-hook binding: hook is {module, fun, aux}, invoked as
module.fun(aux, swept) when the broker is collected (its owning process
exits → the broker's terminate/2). c is the broker PID.
Same as ensure_started/0.