Ports
Rho has no heap, no globals and no file I/O. That is a deliberate constraint
rather than a missing feature: it is what makes a program predictable, and it is
what makes the parts that do need a host visible instead of implicit.
A port is how a program asks for one of those parts.
Declaring what a program needs
A program declares the functions the host must supply:
extern fn read_all(path: string) string
and calls them as it calls any other function. Nothing about the call site says
"this is I/O": the declaration is the whole of the dependency, and it is in the
module, where a reader of the module sees it.
Asking for a port
A project asks the backend for the implementation by name:
# module.yml
build:
language: go
targets:
- id: go
ports:
- fsstore
The backend generates the implementation into the project, and the generated
file is ordinary code in the project: readable, and removable if a person wants
to see what it does.
The ports today
| Port | What it supplies | Used by |
|---|---|---|
fsstore | A module store on the filesystem: put, get, search, list | the registry |
logemit | Writing a log record, with the formatter's own shape | any project that logs |
docemit | The documentation generator, as a page or a site | a project that serves documentation |
gateway | A serial link, UDP delivery, a monotonic clock and a wall clock, shared state, tx-id correlation and the telemetry outbox | an edge gateway that reads a node and streams what it reads |
telemetry | Polling a gateway outbox, writing a batch through the sql port, latest and time-range queries | an aggregator that keeps a series for other applications |
A worked example
A component declares a library; a project asks for a port; the backend supplies
the implementation. Nothing in the project is generated from the component's
sources, and nothing in the component is generated for the project.
// modules/components/rho-log/src/log.rho
module rho.log
extern fn emit(level: u32, subject: string, message: string) void
fn info(subject: string, message: string) void {
emit(1, subject, message);
}
# the project's module.yml
build:
language: go
targets:
- id: go
ports:
- logemit
The project calls info(...). The logemit port supplies emit, writing the
record in the shape the formatter produces, so a device writing to a serial
console and a server writing to stdout cannot drift apart.
Why a port and not a library
A library would have to be compiled into every project that uses it, and a
library that writes files and takes locks is a library every project then
carries. A port supplies the implementation at generation time, from the
backend, so:
- the project stays pure Rho, and every dependency it has is declared in its
manifest; - the implementation is one per backend, so two projects do not drift apart;
- the boundary is visible: what came from the host is what a port named, and
nothing else did.
Note
A port is a promise the backend makes, checked when the port is generated rather
than at the call. A project that declares a port no backend supplies fails at
the call site rather than at the manifest, so the list of ports a project's
targets declare in module.yml is the honest record of what a project needs.
A service that also does something else
An HTTP service answers requests, but an edge gateway also reads a link while it
answers, and a program has no threads of its own. A server-kind module declares
fn background() void, and the generated service runs it on its own goroutine,
after init() and beside the listener:
fn init() void {
print("gateway: starting\n")
}
fn background() void {
if !gateway_serial_start() {
return
}
while true {
let frame: string = gateway_serial_read(100)
if strlen(frame) > 0 {
ingest_frame(frame)
}
}
}
The state background() writes and a handler reads is what the gateway port
is for: a value a program cannot declare as a global is one the host holds, and
the port's mutex is what makes it safe to share. The frame grammar itself stays
in Rho — the port supplies the link, not the protocol.
gateway_now_ms is a monotonic duration since the gateway started; it is not a
calendar. gateway_wall_time_ms is the host's Unix time, which is what a
service records beside a node's own uptime timestamp. A generated service's
streaming responses are not bounded by the server's read or write deadline, so a
file-following event stream stays open for the action it reports.