Hyperlang.Chan (Hyperlang v0.1.0)

Copy Markdown View Source

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 Channel type, H2D/D2H tagged-pointer plumbing, the mover/mid/Type copy machinery, and freechan — BEAM terms are copied by message-passing and reclaimed by GC.
  • Tarray-of-channels alt arms (==> over an array of channels); alt here 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.

Functions

alias_handle(from_scope, to_scope, handle)

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.

alt(ops)

Non-deterministic select over ops. See module doc for the op/return shapes.

alt_raw(ops)

Non-deterministic select without stripping the channel payload transport envelope.

broker?(pid)

True when pid is one of this module's channel broker processes.

close_broker(broker)

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.

close_scope(scope)

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)

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.

crecv(c)

Receive a value from channel broker c, blocking until one is available. Returns the value.

crecv_raw(c)

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.

csend(c, v)

Send v on channel broker c, blocking (rendezvous / buffer-full) until the value is taken or buffered. Returns :ok.

csendalt(c, value, len)

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).

current_scope()

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}.

ensure_started()

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.

fork_scope(new_scope, old_scope)

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.

freechan(scope, handle, broker)

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.

guest_channel_value(value, from_scope, to_scope)

handle_for(scope, broker)

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.

movtmp()

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.

nbrecv(c)

Non-blocking recv: alt with one recv op and a default. {:ok, v} | :empty.

nbsend(c, v)

Non-blocking send: alt with one send op and a default. true if sent.

newchan(size)

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.

newchan(size, classification)

newchan(scope, size, classification)

resolve(scope, handle)

Resolve a channel handle to its broker pid within scope, or nil.

retype(c, hook)

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.

start_registry()

Same as ensure_started/0.

with_broker(scope, handle, fun)