From 0a21ea831d6791b406eef70b3e95cfd319d8ed36 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Thu, 27 Aug 2026 15:28:21 -0300 Subject: fs_service: the transport is a ctx and three functions, not a *fuse.Fs Step 2 of the 9P chain (docs/9p.typ 12.2, docs/registry.typ 9P-2). `drain` and `step` never asked a `*fuse.Fs` for anything but `retry()`, `next()` and `reply()`, so the concrete pointer was a coupling that bought nothing and forbade a second answer. `Transport` names the three; `Fs.transport()` is the first implementor and the thunks are the entire cost. No behaviour change. The order contract -- retry() to null, then next() to null -- moves into `drain`'s doc comment, where it belongs: it is the caller's rule and every implementor inherits it, rather than a fact about FUSE. `start` and `wake` keep their `*fuse.Fs`: they are about a MOUNT, which is a FUSE thing, and a 9P listener will bring its own. Measured unchanged against zig build fs-bench -Doptimize=ReleaseFast: getattr 19 ns, lookup 40, read body 4K/1M 25/25, read ctl 385, read index 633, readdir 38, read event (empty) 22, all at zero allocations. --- src/fuse.zig | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) (limited to 'src/fuse.zig') diff --git a/src/fuse.zig b/src/fuse.zig index 311d887b..3a903e8b 100644 --- a/src/fuse.zig +++ b/src/fuse.zig @@ -58,6 +58,10 @@ const builtin = @import("builtin"); const libc = std.c; const linux = std.os.linux; const acmefs = @import("acmefs.zig"); +/// Only for `Transport`, the three-function shape this mount presents to the +/// host loop. No cycle: `fs_service` names no type from here any more, which +/// is the point of the seam. +const fs_service = @import("fs_service.zig"); /// Everything below the mount is Linux kernel ABI. Off Linux the module still /// compiles (it is imported by the shared native shell) and does nothing. @@ -1059,6 +1063,39 @@ pub const Fs = struct { gpa.destroy(fs); } + /// This mount as the three functions `fs_service` actually calls. The + /// adapter exists so that file needs no `@import("fuse.zig")` to drive a + /// filesystem: `retry`, `next` and `reply` were always its whole use of an + /// `Fs`, and naming them lets a second transport answer the same calls. + /// + /// The thunks are three lines each because a `*Fs` is not an `*anyopaque` + /// and a vtable cannot hold the typed function directly. That is the entire + /// cost of the seam. + pub fn transport(fs: *Fs) fs_service.Transport { + return .{ .ctx = fs, .vtable = &transport_vtable }; + } + + const transport_vtable: fs_service.Transport.VTable = .{ + .retry = transportRetry, + .next = transportNext, + .reply = transportReply, + }; + + fn transportRetry(ctx: *anyopaque) ?acmefs.Req { + const fs: *Fs = @ptrCast(@alignCast(ctx)); + return fs.retry(); + } + + fn transportNext(ctx: *anyopaque) ?acmefs.Req { + const fs: *Fs = @ptrCast(@alignCast(ctx)); + return fs.next(); + } + + fn transportReply(ctx: *anyopaque, r: *const acmefs.Reply, bytes: []const u8) void { + const fs: *Fs = @ptrCast(@alignCast(ctx)); + fs.reply(r, bytes); + } + // -- request pump ------------------------------------------------------- /// Parse the next pending kernel request, or null when the descriptor is -- cgit v1.3