summaryrefslogtreecommitdiff
path: root/9proc/docs/LIBRARY.md
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-20 01:47:28 -0300
committerGabriel Schneider <[email protected]>2026-09-20 01:47:29 -0300
commit66f2e492c348677ab3050f5e378b9eb4c04c98ce (patch)
treecf06b32309eace8eb573925e9cf14e3dfc27bd66 /9proc/docs/LIBRARY.md
parent65209217b5b68f56bc0bd5bc6c4dce33911ded59 (diff)
downloadcloud9-66f2e492c348677ab3050f5e378b9eb4c04c98ce.tar.gz
cloud9-66f2e492c348677ab3050f5e378b9eb4c04c98ce.zip
9proc core becomes a backend of cloud9.fs; engine gains optional features
9proc's own fid table, walk loop and dir-read engine are replaced by cloud9.fs.Server; the tree (static, vars, providers) is served through the engine's Req/Reply contract with node ids that keep the old qid scheme. Providers may answer later by returning error.Again (parked in the engine, retried each step, Tflush -> EINTR); no new files are exposed. Engine (backward compatible, all opt-in via Backend.features / Options): create, remove, wstat, reference accounting for backends that count handles, a salted fid index, name_capacity 0 (names from getattr), Reply.ename for backend-chosen error text, Attr.path/version/atime. Engine-level error strings and the 217-byte msize floor now apply to 9proc; tests updated accordingly. Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to '9proc/docs/LIBRARY.md')
-rw-r--r--9proc/docs/LIBRARY.md85
1 files changed, 66 insertions, 19 deletions
diff --git a/9proc/docs/LIBRARY.md b/9proc/docs/LIBRARY.md
index eef7dad..e0152e0 100644
--- a/9proc/docs/LIBRARY.md
+++ b/9proc/docs/LIBRARY.md
@@ -24,7 +24,7 @@ Design rules (non-negotiable, they mirror cloud9):
```
9proc/src/root.zig pub const core, vars, scratch, linux (linux only), Server(cfg)
- 9proc/src/core.zig Tree/Server engine on cloud9.Server: fids, walks, dir reads, providers
+ 9proc/src/core.zig the tree (static nodes, vars, providers) as a backend of cloud9.fs.Server
9proc/src/vars.zig comptime value renderers (@typeInfo) for /vars
9proc/src/scratch.zig in-memory read/write tree provider (takes an Allocator)
9proc/src/linux/probe.zig background thread + poll loop + unix/tcp/fd listeners
@@ -51,34 +51,51 @@ pub const Config = struct {
/// slots so that reads at arbitrary offsets are consistent.
snapshot_slots: u8 = 8,
snapshot_bytes: u32 = 16 * 1024,
+ max_parked: u8 = 8, // reads a provider has parked with error.Again, per connection
};
pub fn Server(comptime cfg: Config) type {
return struct {
pub const Storage = struct { // caller places this in static memory
- in: [msize]u8, out: [msize]u8, snapshots: [cfg.snapshot_slots][cfg.snapshot_bytes]u8,
+ in: [msize]u8, out: [msize]u8, data: [msize]u8, snapshots: [cfg.snapshot_slots][cfg.snapshot_bytes]u8,
};
pub const Shared = struct { // state common to all connections (providers, vars)
pub fn init(name_ctx: *anyopaque) Shared;
pub fn addProvider(s: *Shared, p: Provider) error{Full}!void;
pub fn expose(s: *Shared, name: []const u8, ptr: anytype) error{Full}!void; // typed value → /vars/<name>
};
- pub const Conn = struct { // one 9P connection, push/step/output like cloud9
+ pub const Backend = struct { ... }; // what cloud9.fs.Server knows of the tree: Req, Reply, features
+ pub const Engine = cloud9.fs.Server(Backend, .{ .fid_capacity = cfg.max_fids, .slot_capacity = cfg.max_parked, ... });
+ pub const Conn = struct { // one 9P connection: an Engine plus the tree's per-connection state
pub fn init(shared: *Shared, storage: *Storage, msize: u32) Conn;
pub fn push(c: *Conn, bytes: []const u8) usize; // feed transport bytes
- pub fn step(c: *Conn) error{Protocol}!bool; // handle ≤ 1 request; false = nothing to do
+ pub fn step(c: *Conn) error{Protocol}!bool; // serve ≤ 1 engine request; false = nothing to do
pub fn output(c: *const Conn) []const u8; // bytes to send
pub fn wrote(c: *Conn, n: usize) void;
pub fn hangup(c: *Conn) void; // drop fids, tell providers
+ pub fn fidCount(c: *const Conn) usize;
};
};
}
```
-`step` drives `cloud9.Server.receive/reply/negotiate` and the backend: the
-static tree (comptime-generated from `cfg`: `/README`, `/build/*` via a
-`build_options`-like struct passed in `cfg.build`, `/comptime/types/*`,
-`/comptime/decls`, `/runtime/fn/*`, `/ctl`, `/vars/*`) plus **providers**.
+The core is a **backend of `cloud9.fs.Server`**, the library's file-server
+engine (cloud9's `docs/design.md`, "File server engine"): one engine for
+every 9P server built on cloud9. The engine owns the fid table (indexed,
+`Options.fid_index`, so thousands of fids cost O(1) per lookup), walks,
+permission checks, directory cursors, Tflush, and the releases a dropped
+connection owes; `Conn` wraps one engine instance and answers its
+requests (`lookup`, `getattr`, `setattr`, `open`, `read`, `write`,
+`release`, `readdir`) from the tree: the static part (comptime-generated
+from `cfg`: `/README`, `/build/*` via a `build_options`-like struct passed
+in `cfg.build`, `/comptime/types/*`, `/comptime/decls`, `/runtime/fn/*`,
+`/ctl`, `/vars/*`) is served directly, **providers** through their vtable.
+The backend declares every optional engine feature (`fs.Features`:
+create, remove, wstat, references), which is how Tcreate/Tremove/Twstat
+and the one-clunk-per-handle contract below reach providers. `step`
+answers the engine's `retry()` and `next()` requests synchronously, one
+`next()` per call; a provider that cannot answer yet parks instead (see
+"Answering later").
A provider is a runtime vtable mounted at a top-level name. It owns a subtree
with its own naming (dynamic directories such as `/threads/<tid>` or
@@ -106,23 +123,53 @@ pub const Provider = struct {
};
```
-Error → Rerror text mapping lives in one place in the core, using the Plan 9
-strings 9ns's bridge already understands (`file does not exist`,
-`permission denied`, `file already exists`, `directory not empty`,
+Error → Rerror text: what the tree or a provider refuses is answered in
+`ename`'s Plan 9 strings, which 9ns's bridge understands (`file does not
+exist`, `permission denied`, `file already exists`, `directory not empty`,
`not a directory`, `is a directory`, `bad offset`, `no space`, `i/o error`,
-`not supported`).
+`not supported`, `bad command`, `bad value`, `too many open dynamic
+files`); what the engine refuses on its own carries cloud9.fs's strings,
+the ones Linux v9fs maps back to errnos (`fid unknown or out of range`,
+`fid already in use`, `Too many open files in system`, `bad use of fid`
+for I/O on an unopened fid or a walk or clone from an open one, `file
+already open for I/O`, `bad offset in directory read`, `permission denied`
+for an open the walked mode forbids (a directory for writing, OEXEC, a
+static file for writing), `wstat prohibited` for any of the fields the
+engine owns (type, dev, qid, atime, the owner names) or a length on a
+directory, `illegal name`, `Invalid argument` for an Rstat that cannot fit
+the msize or a directory read whose count holds no whole record). The
+smallest msize is the engine's `fs.msize_min` (217: one full Rwalk); a
+Tversion below it ends the connection.
Directory reads follow the 9P rule (offset 0 or previous offset+count, never
-split a record). Dynamic file reads: on open the content is generated once
-into a snapshot slot (`open` runs the generator; `read` serves the slot; a
-read at offset 0 regenerates); no free slot → Rerror `too many open dynamic
+split a record); the engine encodes the entries from the tree's records,
+with uid/gid/muid the attach uname, the modes `dirent_dir_perm`/
+`dirent_file_perm` and length 0 (a stat of the entry gives the real
+ones). Dynamic file reads: on open the content is generated once into a
+snapshot slot (`open` runs the generator; `read` serves the slot; a read
+at offset 0 regenerates); no free slot → Rerror `too many open dynamic
files`. Stats of dynamic files report length 0.
Qids: static nodes get comptime paths; provider nodes get
-`(provider index << 56) | handle`.
+`(provider index << 56) | handle` (or `NodeStat.path` in place of the
+handle). The engine's node ids are the same numbers, except that
+provider ids are offset by one in the top byte, since the engine reads
+node 0 as "the node asked about".
+
+**Answering later.** `Provider.read` may return `error.Again`: the engine
+parks the request (`Config.max_parked` per connection; a further one fails
+with EAGAIN), the connection goes on serving, and every later `Conn.step`
+asks the provider again with the same handle and offset until it answers,
+whether at once or on a later step. The fid stays open; a Tflush of a
+parked read answers it with `Interrupted system call` before the Rflush;
+hangup and Tversion drop it and close the file as usual. Nothing wakes a
+connection by itself: the platform layer steps a connection when its
+transport moves, so a provider that becomes ready has to make that happen
+(an event stream's job, not the core's). Writes cannot park.
Static memory: `Server(cfg).Storage` per connection, `Shared` once. No heap.
-The core has unit tests driven through `cloud9.Client` in memory (like today).
+The core has unit tests driven through `cloud9.Client` in memory,
+including a provider that parks.
## Value renderers (`vars.zig`)
@@ -252,8 +299,8 @@ the demo's Storage is a global.
## Verification
-* Unit tests: core (in-memory client drives every op incl. providers and
- snapshots), vars (render/set for every category), scratch, debug (capture
+* Unit tests: core (in-memory client drives every op incl. providers,
+ snapshots and a parked read), vars (render/set for every category), scratch, debug (capture
own thread and a helper thread; breakpoint pause/continue on a helper
thread; panic record path without holding).
* `zig build 9proc-check-freestanding`: compiles `core.zig` + `vars.zig`