ARCHITECTURE · 03
The VFS model.
TFS is a virtual filesystem with pluggable backends, spoken through one small C ABI. That ABI is the seam of the whole architecture: patched Ruby above it, backends below it, and either a C++ or a Rust implementation behind it.
The seam: tebako_fs_*.
SHIPPED · libtfs v0.13.0Since libtfs v0.13.0 the surface is modern-only: the legacy tebako C/C++ API (a ~16k-line libdwarfs-wr lineage) was removed, leaving exactly 24 exports, declared in include/tebako/fs/c_api.h. Because the contract is a C ABI, consumers don't care which implementation — or which repository — ships it. Two implementations now exist: the C++ one below, and the Rust tfs crate in tebako-rs — a drop-in libtfs.so/.dylib/.dll/.a exporting exactly 33 tebako_* symbols, nm-verified (the 24-function contract surface plus tebako_fs_abi_version and eight compat/introspection helpers — nothing else leaks).
LIFECYCLE — SINGLE-MOUNT COMPAT
tebako_fs_inittebako_fs_init_from_filetebako_fs_init_from_file_attebako_fs_unmount
The original entry points, kept byte-compatible: init* stays single-mount (second init → EALREADY); unmount() tears down everything.
LIFECYCLE — MULTI-MOUNT
tebako_fs_mount_from_filetebako_fs_mount_from_file_attebako_fs_mount_from_memorytebako_fs_unmount_handle
The mount-table API: each mount returns an opaque handle and attaches at its own mount point — an app image, data images, and more, concurrently.
FILE I/O
tebako_fs_opentebako_fs_readtebako_fs_preadtebako_fs_lseektebako_fs_closetebako_fs_stattebako_fs_fstat
pread never moves the fd position (lseek+read emulation was rejected as racy).
DIRECTORY I/O
tebako_fs_opendirtebako_fs_readdirtebako_fs_closedirtebako_fs_rewinddirtebako_fs_telldirtebako_fs_seekdirtebako_fs_dir_is_embedded
Index-based telldir/seekdir cookies; dir_is_embedded is the registry test the patched runtime uses to dispatch between memfs and host.
UTILITIES
tebako_fs_extract_alltebako_fs_dlmap2file
extract_all powers --tebako-extract; dlmap2file extracts a memfs file to a host path so native extensions can be dlopen’ed.
The full behavior contract is executable: the c_api test suite registers 493 tests under ctest on the C++ side — the parity oracle. The Rust workspace runs the ported cases through its own C ABI (164 today, the full sweep following the same pattern; the audit is in tebako-rs's docs/parity.md).
Multi-mount.
SHIPPED · v0.13.0One process can attach many TFS images at once — the app image plus data images plus anything else the package carries. A mount table maps opaque handles to (mount point, backend instance); path resolution is longest-prefix-wins on mount-point boundaries, and every fd/dir handle records its owning mount, so unmounting one image force-closes only its own handles and leaves the rest fully usable.
open("/tebako/fs/app/config/boot.rb", …)
│
▼
┌───────────────────────────────────────────────────────┐
│ path dispatch — longest mount-point prefix wins │
│ │
│ mount 0 /tebako/fs/app → dwarfs backend │
│ mount 1 /tebako/fs/data → zip backend │
│ (nested mount points allowed; duplicates → EEXIST) │
└───────────────────────────────────────────────────────┘
│ path inside a mount │ path outside every mount
▼ ▼
tebako_fs_open() host open()
read-only image, owning mount cwd, writes, mkdir — untouchedThe backends are instance-based — dwarfs (via dwarfs-t), squashfs, zip — with no global mount state, which is exactly what makes N concurrent mounts safe. Images can be mounted from a package slot (file + offset + length), from a standalone file, or from memory.
Read-only memfs, host cwd and writes.
The packaged filesystem is read-only by design. Paths inside a mount are served from the image; everything else — the process working directory, every write, mkdir, unlink — passes through to the host filesystem unchanged. There is deliberately no tebako_fs_chdir and no write surface in the ABI: a packaged app can corrupt neither its own image nor another package's mount. The one sanctioned escape hatch is tebako_fs_dlmap2file, which extracts a file to the host so the OS loader can map a native extension.
IO routing in patched Ruby.
SHIPPEDThe patches published by tamatebako/ruby reroute Ruby's file calls to the ABI — and only to the ABI. A stat-based "is this path inside a mount?" check decides routing; non-mount paths fall through to the host unchanged.
| RUBY / LIBC CALL | ROUTES TO |
|---|---|
| open / read / lseek / close | tebako_fs_open / read / lseek / close |
| stat / fstat / lstat / access-as-exists | tebako_fs_stat / fstat (+ stat-as-exists) |
| opendir / readdir / closedir | tebako_fs_opendir / readdir / closedir |
| Dir#tell / Dir#seek / Dir#each | tebako_fs_telldir / seekdir / rewinddir |
| IO#pread | tebako_fs_pread (fd position untouched) |
| fd / dir introspection | tebako_fs_dir_is_embedded and the fd registry |
| writes, cwd, mkdir, unlink | never routed — they go to the host FS (read-only memfs) |
| native extension load (dlopen) | tebako_fs_dlmap2file → host path → host dlopen |