Skip to content

oguzhane/ninep-suite

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

99 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

nineprovider-osx

A production-grade Rust implementation of the 9P2000.L protocol — the Linux-specific dialect of the Plan 9 file protocol.

The suite provides both a client and server, each with dual I/O backends (synchronous std::net and asynchronous tokio), structured as a Cargo workspace of six crates. It enables a mountable 9P file server usable from Linux guests (via v9fs) against a macOS host filesystem, plus a client library for programmatic 9P access.

Crate Overview

Crate Description
ninep-proto Wire protocol types, serialization, and error mapping. All 29 T/R message pairs with zero-copy encode/decode via bytes::Bytes.
ninep-client Sync and async 9P2000.L client with tag multiplexer and FID allocator.
ninep-server Sync and async 9P2000.L multi-client server with pluggable Filesystem / AsyncFilesystem traits.
ninep-hostfs Host filesystem provider (HostFs / AsyncHostFs) that maps 9P operations to OS syscalls.
ninep-serve CLI tool to export a local directory over 9P2000.L (sync and async modes).
ninep-uniffi Cross-language FFI bindings via Mozilla UniFFI (Swift, Kotlin, Python).

Architecture

┌─────────────────────────────────────────────────┐
│                  ninep-uniffi                    │  UniFFI bindings (Swift/Kotlin/Python)
└────────────┬───────────────────┬────────────────┘
             │                   │
     ┌───────▼───────┐  ┌───────▼───────┐
     │  ninep-client  │  │  ninep-serve   │  CLI binary
     │  (sync/async)  │  │  (sync/async)  │
     └───────┬────────┘  └───────┬────────┘
             │                   │
             │           ┌───────▼───────┐
             │           │  ninep-hostfs  │  HostFs / AsyncHostFs
             │           └───────┬────────┘
             │                   │
             │           ┌───────▼───────┐
             │           │  ninep-server  │  Pluggable Filesystem traits
             │           │  (sync/async)  │
             │           └───────┬────────┘
             │                   │
         ┌───▼───────────────────▼───┐
         │        ninep-proto        │  Wire protocol (messages, codec, qid, stat)
         └───────────────────────────┘

Key Design Decisions

  • Zero-copy I/O: Read/write payloads use bytes::Bytes for zero-copy slicing of receive buffers.
  • Dual I/O backends: Every component has both a std::net (blocking) and tokio (async) variant, gated behind the tokio Cargo feature.
  • Thread-safe state: FID maps use DashMap for lock-free concurrent access; tag allocation is TOCTOU-free via DashMap::entry().
  • Pluggable filesystems: The server's Filesystem / AsyncFilesystem traits let you swap in any backend — ninep-hostfs provides HostFs / AsyncHostFs which maps to OS syscalls.
  • macOS portability: Tgetattr metadata and errno values are translated between macOS and Linux representations.

Quick Start

Prerequisites

  • Rust 1.75+
  • For async features: tokio 1.x

Building

cargo build --workspace

Running the Examples

Start a sync server exporting a directory:

mkdir -p /tmp/shared
cargo run --example unpfs_server -- /tmp/shared

Connect with the sync client:

cargo run --example basic_client -- 127.0.0.1:5640 /tmp/shared

Start an async server:

mkdir -p /tmp/shared
cargo run --example async_server -- /tmp/shared

Connect with the async client:

cargo run --example async_client -- 127.0.0.1:5640 /tmp/shared

Mounting from Linux (v9fs)

The primary use case is mounting a macOS directory from within a Linux guest (VM, container, etc.) using the kernel's built-in v9fs client.

TCP mount

sudo mount -t 9p -o trans=tcp,port=5640,version=9p2000.L,msize=8192 \
    <host-ip> /mnt/9p

Mount options reference

Option Description
trans=tcp Use TCP transport
port=5640 Server port (default 9P port is 564)
version=9p2000.L Protocol version (must be 9p2000.L)
msize=8192 Maximum message size in bytes
cache=none Disable client-side caching (safest for shared access)
cache=loose Loose caching (better performance, eventual consistency)
access=user Per-user access control
uname=nobody User name for the 9P attach

Example: QEMU virtio-9p

When running a Linux VM with QEMU, you can use virtio-9p for shared folders:

# Start the 9P server on the host
cargo run --example unpfs_server -- /path/to/shared

# In QEMU, forward port 5640 and mount inside the guest
# Or use QEMU's built-in 9pfs passthrough instead:
qemu-system-x86_64 \
    -virtfs local,path=/path/to/shared,mount_tag=hostshare,security_model=mapped-xattr \
    ...

# Inside the guest:
sudo mount -t 9p -o trans=virtio,version=9p2000.L hostshare /mnt/shared

Library Usage

Sync Client

use ninep_client::{NineClient, SyncNineClient, TcpTransport};
use ninep_proto::wire::NOFID;

let transport = TcpTransport::connect("127.0.0.1:5640")?;
let mut client = SyncNineClient::new(Box::new(transport));

// Handshake
let msize = client.version(8192)?;

// Attach to root
let root_fid = 0;
client.attach(root_fid, NOFID, "nobody", "/tmp/shared", u32::MAX)?;

// Walk to a file
let (file_fid, _) = client.walk_auto(root_fid, &["myfile.txt".into()])?;

// Open and read
client.open(file_fid, 0)?;
let data = client.read(file_fid, 0, 4096)?;

// Cleanup
client.clunk(file_fid)?;
client.clunk(root_fid)?;
client.shutdown()?;

Async Client (tokio)

use ninep_client::{AsyncNineClient, AsyncTcpTransport};
use ninep_proto::wire::NOFID;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let transport = AsyncTcpTransport::connect("127.0.0.1:5640").await?;
    let mut client = AsyncNineClient::new(transport);

    let msize = client.version(8192).await?;
    client.attach(0, NOFID, "nobody", "/tmp/shared", u32::MAX).await?;

    let (fid, _) = client.walk_auto(0, &["myfile.txt".into()]).await?;
    client.open(fid, 0).await?;
    let data = client.read(fid, 0, 4096).await?;

    client.clunk(fid).await?;
    client.shutdown().await?;
    Ok(())
}

Sync Server

use std::sync::Arc;
use std::sync::atomic::Ordering;
use ninep_server::{NineServer, Filesystem, new_shutdown_token};
use ninep_hostfs::HostFs;

let shutdown = new_shutdown_token();
let listener = std::net::TcpListener::bind("127.0.0.1:5640")?;

let mut server = NineServer::new(shutdown.clone());
// serve_tcp blocks until shutdown
server.serve_tcp(listener, || Arc::new(HostFs::new(8192)) as Arc<dyn Filesystem>)?;

Async Server (tokio)

use std::sync::Arc;
use tokio_util::sync::CancellationToken;
use ninep_server::{AsyncNineServer, AsyncFilesystem};
use ninep_hostfs::AsyncHostFs;

let cancel = CancellationToken::new();
let listener = tokio::net::TcpListener::bind("127.0.0.1:5640").await?;

let server = AsyncNineServer::new(cancel.clone());
server.serve_tcp(listener, || {
    Arc::new(AsyncHostFs::new(8192)) as Arc<dyn AsyncFilesystem>
}).await?;

Documentation

Generate the full API documentation:

cargo doc --workspace --no-deps --open

Cargo Features

Crate Feature Description
ninep-client tokio Enable async client (AsyncNineClient, AsyncTcpTransport)
ninep-server tokio Enable async server (AsyncNineServer, AsyncFilesystem)
ninep-hostfs tokio Enable async host filesystem provider (AsyncHostFs)

All three crates default to sync-only. Enable the tokio feature for async support:

[dependencies]
ninep-client = { path = "crates/ninep-client", features = ["tokio"] }
ninep-server = { path = "crates/ninep-server", features = ["tokio"] }
ninep-hostfs = { path = "crates/ninep-hostfs", features = ["tokio"] }

Protocol Reference

License

This project is dual-licensed:

About

FUSE-less 9P on macOS using FSKit

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors