TL;DR
OMP pushes grep, AST matching, shell/PTY, file walking, isolation, and voice into 6 independent Rust crates (pi-ast, pi-natives, pi-walker, pi-shell, pi-iso, pi-voice), then exposes a unified JavaScript interface through pi-natives — a single N-API cdylib.
- Why Rust: grep/AST need native throughput; shell/PTY need correct process control and signal handling; hashline/isolation need deterministic cross-platform behavior.
- N-API contract: Rust
#[napi]annotations →bun run build:bindingsruns napi-rs to emitindex.d.ts+.node→gen-enums.tsrewritesconst enumto runtime objects and emits explicit ESM exports → Bazel cross-compiles multi-platform.nodefiles, packaged as optional dependency leaf packages. - WASM fallback:
pi-isofalls back to recursive copy (Rcopy) when no native backend exists; the same Rust code compiles to WASM for browser/edge runtimes.
Context
OMP (oh-my-pi) is the core toolchain for an AI coding agent, providing:
- Text search: ripgrep-class regex search, fuzzy find, glob
- Code structure: tree-sitter / ast-grep syntactic search, edit, summarization
- Terminal ops: embedded shell (brush), PTY sessions, process management
- Filesystem: parallel walking, ignore rules, scan cache
- Isolation: APFS clone / Linux overlayfs / Windows ProjFS / git worktree fallback
- Voice: microphone capture, playback, WebRTC live conversation
These capabilities are invoked from Node.js / Bun main threads, but JavaScript is fundamentally unsuited to implement any of the above. OMP's solution: sink core subsystems into Rust crates, bridge via N-API.
Problem
- Performance: JS regex engine, sync FS APIs, tree walking are too slow; large-project grep/AST scans must complete in seconds.
- Correctness: Shell needs precise signal propagation, process group management, PTY master/slave coordination; JS
child_processabstraction is too thin and inconsistent. - Determinism: hashline (content-addressing), isolation (copy-on-write) need bit-identical cross-platform behavior.
- Distribution UX: Users
npm installand it works — no Rust toolchain, no compilation, support Linux/macOS/Windows x64/arm64, with AVX2/baseline CPU variants.
Attempts
Early: Everything in One Crate
Initially pi-natives contained both N-API bindings and all algorithms. Drawbacks:
- Extreme compile times (single monolithic crate)
- Subsystems couldn't be tested independently (grep, walker, shell entangled)
- Cross-platform
cfgscattered everywhere, hard to maintain
Post-Split Architecture
@oh-my-pi/pi-natives (JS entrypoint)
│
▼
crates/pi-natives (N-API cdylib, ONLY binding conversion & platform adaptation)
│
├─► pi-ast # tree-sitter registry, ast-grep matching/edit/summary
├─► pi-walker # parallel file walking, ignore, cache, glob/fuzzy candidate source
├─► pi-shell # brush shell execution, process plumbing, minimizer, in-process builtins
├─► pi-builtins # builtin commands: grep/rg/fd/ls/find/jq/sed/... (uutils ports)
├─► pi-iso # isolation backends: APFS/overlayfs/ProjFS/Rcopy + diff
└─► pi-voice # audio capture/playback, Opus/WebRTC peer
Each crate is an independent rlib exporting only Rust APIs, zero N-API dependency. pi-natives assembles them into the cdylib.
Solution
6 Crates: Responsibilities & Surfaces
| Crate | Core Responsibility | Key Rust API | N-API Surface |
|---|---|---|---|
pi-ast | Language registry, parse cache, ast-grep match/edit/summary | SupportLang, parse_with_cache, ast_grep/ast_edit/summarize_code | astGrep, astMatch, astEdit, summarizeCode |
pi-walker | Parallel dir walk, ignore, globset, cache, heartbeat cancellation | WalkRequest, WalkOptions, WalkFilter, collect/stream/for_each_file_candidate_parallel | Indirect via pi-natives glob/fd/astGrep |
pi-shell | Brush shell execution, cancel tokens, process plumbing, in-process builtin registration | Shell, execute_shell, CancelToken, Utility trait, Host view | executeShell, Shell class, PtySession |
pi-builtins | Builtin command implementations (grep/rg/fd/ls/find/jq...), Utility trait | utility_builtins(), process_builtins(), Host trait | Registered by pi-shell, not directly exposed to JS |
pi-iso | Isolation backend abstraction, probe/resolve/start/stop/diff | IsolationBackend trait, BackendKind, resolve(), default_backend() | isoBackend, isoProbe, isoResolve, isoStart/isoStop/isoDiff |
pi-voice | OS audio backends, Opus/WebRTC, callback-based live peer | AudioCapture/AudioPlayback, LivePeerCore, LiveCallbacks | AudioCapture/AudioPlayback/LiveWebRtcPeer classes |
Key design:
pi-nativescontains zero algorithms — only N-API conversion, platform adaptation, cancellation bridging, runtime installation. This lets Rust crates be consumed by non-N-API users (CLI, WASM, tests) directly.
How the N-API Binding Contract Is Written
1. Rust Side: #[napi] Annotations
// crates/pi-natives/src/grep.rs
#[napi]
pub struct GrepOptions {
pub pattern: String,
pub path: Option<String>,
pub ignore_case: Option<bool>,
pub max_count: Option<u32>,
pub max_count_per_file: Option<u32>,
pub context_before: Option<u32>,
pub context_after: Option<u32>,
pub timeout_ms: Option<u32>,
pub signal: Option<AbortSignal>,
}
#[napi]
pub async fn grep(options: GrepOptions, on_match: Option<ThreadsafeFunction<GrepMatch>>) -> GrepResult { ... }
- Naming: Rust
snake_case→ JScamelCase(automatic) - Type mapping:
Option<T>→T \| undefined,Vec<T>→T[],String→string,u32→number - Async:
async fn→ returnsPromise<T>(napi-rs usesTaskon libuv workers) - Streaming callbacks:
ThreadsafeFunction<T>→ JS callback(match) => void
2. Generate Bindings: bun run build:bindings
# packages/natives/scripts/build-bindings.ts
napi build \
--manifest-path crates/pi-natives/Cargo.toml \
--package-json-path packages/natives/package.json \
--platform --no-js --dts index.d.ts \
-o <build-output-dir> \
--profile local
Outputs:
index.d.ts: TypeScript declarations (withconst enum, interfaces, classes).node: Native addon (placed inpackages/natives/native/)
3. gen-enums.ts: Patching Runtime Enums & ESM Exports
napi-rs emits const enum — a TypeScript-only construct with no JS runtime value. gen-enums.ts does two things:
- Rewrite
const enumto runtime objects:// index.d.ts originally: export declare const enum GrepOutputMode { Content = "content", ... } // Rewritten: export declare enum GrepOutputMode { Content = "content", ... } - Emit explicit ESM exports (inside
native/index.jsmarker block):// --- generated native exports (do not edit) --- // classes export const GrepResult = nativeBindings.GrepResult; export const Shell = nativeBindings.Shell; // functions export const grep = nativeBindings.grep; export const executeShell = nativeBindings.executeShell; // string/numeric enums export const GrepOutputMode = { Content: "content", Count: "count", ... }; // --- end generated native exports ---
Why explicit ESM exports? Consumers
import { grep } from '@oh-my-pi/pi-natives'expect named exports, but napi-rs's dynamic loader returns an object that must be manually bound.
4. Release Pipeline: Bazel Cross-Compile + Leaf Packages
scripts/bazel-natives.ts linux-x64-modern linux-x64-baseline darwin-arm64 ...
│
▼
//:natives-<target> (native_addon rule)
// └─ rust_shared_library + config transition (LTO, target-cpu, strip)
│
▼
pi_natives.<platform>-<arch>[-variant].node
│
▼
gen-npm-packages.ts → @oh-my-pi/pi-natives-<platform>-<arch> (optional dependency)
- x64 variants:
modern(AVX2, x86-64-v3) /baseline(x86-64-v2) - musl/gnu share filenames: CI builds separately, installs to separate dirs
- Windows MSVC static CRT:
+crt-static+static_link_msvc, no VC++ Redistributable dependency - Loader (
loader-state.js): platform tag → variant detection → candidate ordering → sentinel version validation → load →__ompInstallTokioRuntime()installs bounded Tokio/Rayon pools
Why These 6 Subsystems Must Be Rust
grep / AST / fuzzy find: Native Throughput + Determinism
- Regex engine: Rust
regex+pcre2fallback, controllable JIT (OMP_PCRE2_JIT) - Parallel walking:
pi-walkeruses Rayon, custom heartbeat cancellation,globsetcompiled down to walker level - Memory control: Large files read only first 4 MiB (
read_owned_prefix), no mmap, avoids page faults - AST: tree-sitter grammars statically linked, parse cache reused across requests, ast-grep pattern matching zero-copy
JS alternative: shell out to
ripgrepCLI, parse stdout → high startup overhead, hard to stream, can't integrate cancellation tokens.
shell / PTY: Correct Process Control
- brush shell: Pure Rust POSIX shell (parser, expansion, interpreter), controllable builtin registration
- In-process builtins:
grep/rg/fd/ls/find/jq... run inside the shell process, no fork/exec — stdio piped, cwd synced, env isolated, cancellation token reaches directly - PTY:
portable_ptyabstracts cross-platform differences, ConPTY (Windows) / pseudotty (Unix), dedicated reader thread + incremental UTF-8 decode, resize/kill via control channel - Cancellation semantics:
timeoutMs+AbortSignal→CancelToken→ shell triggers Tokio cancellation token → sends TERM/KILL signal waves → 2s grace window → forced abort
JS
child_process.spawncannot: precise process group control, in-process builtins, unified cancellation model, PTY size sync.
walker / isolation / hashline: Cross-Platform Determinism
pi-walkercache: key = canonical root + fullWalkOptions(includingfollow_links,detail,gitignore...), TTL 1s, max 16 entries, stale-empty recheckpi-isobackend priority: macOSApfs→Zfs→Rcopy; LinuxBtrfs→Zfs→LinuxReflink→Overlayfs→Rcopy; WindowsWindowsBlockClone→Projfs→Rcopy- Diff delegates to git: When
mergedis a git repo, usesgit diffdirectly — byte-identical output forgit apply; non-git falls back to(size, mtime)→ content compare - WASM fallback:
pi-iso'sRcopybackend is pure Rust, no platform syscalls, compiles to WASM — browser/edge runtimes can run isolation logic
Why This Architecture
Rationale Behind Decisions
| Decision | Reason |
|---|---|
| Split crates | Single responsibility, independent compile/test, avoid circular deps, enable WASM/CLI reuse |
pi-natives only does bindings | Minimize N-API surface, fast compile, controllable runtime init (Windows commit limit) |
| napi-rs + manual gen-enums | napi-rs handles arg/return conversion, async tasks, ThreadsafeFunction; gen-enums handles the two things napi-rs doesn't: TS-only enums & ESM exports |
| Bazel cross-compile | Hermetic toolchains (zig cc / xwin / Xcode), musl/gnu dual support, static CRT, cache-friendly |
| Loader sentinel + variant detection | Avoid Windows file-lock update failures, AVX2 auto-detection, compiled-mode embedded addon |
| Dual-track cancellation model | Blocking APIs return errors via heartbeat(); shell/PTY use typed result flags (cancelled/timedOut) |
Pitfalls Avoided
- Windows thread commit limit:
module_initcannot spawn threads; loader post-load installs Tokio/Rayon pools, withstd::thread::Builder::spawnpre-probing how many threads the OS allows - musl/gnu basename collision: Deliberately share filenames, CI builds separately, loader never sees both simultaneously
const enumhas no runtime value: gen-enums.ts forces literal object emission- Over-partitioned cache keys: Full
WalkOptions(exceptcacheflag) participates in key, preventing different policies from sharing stale scans - In-process builtins shadowing system commands: Controllable via env vars (
PI_DISABLE_UUTILS_BUILTINSetc.), preserving user expectations
Lessons Learned
- N-API contracts should be generated, not hand-written:
#[napi]+ napi-rs guarantees type alignment, correct async bridging, safe ThreadsafeFunction. Hand-written bindings drift with versions. - Separate "algorithms" from "boundaries": Rust crates export pure Rust APIs; N-API layer only does conversion. This lets crates be used by WASM, CLI, tests directly; binding changes don't touch core logic.
- Cross-platform native behavior needs a "testable fallback":
pi-iso'sRcopybackend is both the fallback, the WASM target, and the CI verification baseline. - Release pipeline must handle "multi-platform × multi-variant": Bazel transitions + leaf package pattern solves the
optionalDependenciescombinatorial explosion with CPU variants. - Cancellation semantics belong in the API contract: Blocking uses exceptions; async commands use typed flags. Don't mix — consumers need to handle them correctly.
References
- OMP Source: crates/pi-natives
- OMP Source: packages/natives
- native-crates.md
- natives-architecture.md
- natives-binding-contract.md
- natives-build-release-debugging.md
- natives-shell-pty-process.md
- natives-text-search-pipeline.md
- fs-scan-cache-architecture.md
- natives-rust-task-cancellation.md
- napi-rs Documentation
- Bazel rules_rust
Loading...