//! Native utilities exported via N-API for the Oh My Pi toolchain. //! //! # Overview //! High-performance primitives for clipboard access, grep, file discovery, //! ANSI-aware text measurement, syntax highlighting, HTML/PDF-to-Markdown //! conversion, and terminal SIXEL encoding. //! //! # Example //! ```ignore //! use pi_natives::text::visible_width; //! //! let width = visible_width("hello"); //! assert_eq!(width, 5); //! ``` //! //! # Architecture //! ```text //! JS (packages/natives) -> N-API -> Rust modules (clipboard/fd/glob/grep/html/pdf/highlight/sixel/text) //! ``` #![allow(clippy::trailing_empty_array, reason = "generated by napi macro")] #![allow(clippy::trivially_copy_pass_by_ref, reason = "napi env idiom")] #![feature(alloc_error_hook)] pub mod appearance; pub mod ast; pub mod audio; pub mod block; pub mod clipboard; pub mod crash_handler; pub mod desktop; pub mod devicecheck; pub mod diff; pub mod fd; pub mod file_lock; pub mod glob; pub mod glob_util; pub mod grep; pub mod highlight; pub mod html; pub mod iofs; pub mod js; pub mod keys; pub mod live; /// PDF inspection and Markdown conversion. pub mod pdf; pub mod sixel; pub mod snapcompact; pub mod utok; pub use pi_ast::language; pub mod power; pub mod iso; pub mod prof; pub mod ps; pub mod pty; pub mod shell; pub mod summary; pub mod task; #[cfg(test)] pub(crate) mod testing; pub mod text; pub mod tokens; pub(crate) mod utils; pub mod vectors; pub mod workspace; #[cfg(target_os = "windows")] use std::sync::{ Arc, atomic::{AtomicBool, Ordering}, }; #[cfg(target_os = "windows")] use napi::bindgen_prelude::create_custom_tokio_runtime; use napi_derive::{module_init, napi}; /// Upper bound on Windows Tokio *scheduler* workers. These only drive async I/O /// futures (shell/process/PTY/ISO) and light glue tasks; all CPU-heavy and /// blocking native work runs elsewhere — libuv tasks (`task::blocking`), Rayon, /// or Tokio's separate blocking pool via `spawn_blocking` — so a handful of /// async workers is plenty regardless of core count. #[cfg(target_os = "windows")] const NAPI_TOKIO_MAX_WORKER_THREADS: usize = 4; /// Cap on Tokio's lazily-grown blocking pool (used by `spawn_blocking` offloads /// such as `iso_start`/`iso_stop`/`pty.start`/`walk_diff`). Threads here are /// created on demand, not at load, so this only bounds peak fan-out. #[cfg(target_os = "windows")] const NAPI_TOKIO_MAX_BLOCKING_THREADS: usize = 8; /// Upper bound on Rayon's global pool on Windows. Rayon is used for CPU-bound /// helpers (`count_tokens`, vendored `sort`), but the global pool cannot /// recover if its first lazy initialization fails after Windows refuses worker /// threads. #[cfg(any(target_os = "windows", test))] const RAYON_MAX_THREADS: usize = 8; #[cfg(any(target_os = "windows", test))] const RAYON_RESERVED_NON_RAYON_THREADS: usize = 1; /// Windows worker count we'd *like*, before checking what the OS will actually /// grant: the Tokio default (one per core) clamped to /// [`NAPI_TOKIO_MAX_WORKER_THREADS`]. #[cfg(target_os = "windows")] fn desired_worker_threads() -> usize { std::thread::available_parallelism() .map_or(1, |threads| threads.get()) .clamp(1, NAPI_TOKIO_MAX_WORKER_THREADS) } #[cfg(target_os = "windows")] fn desired_rayon_threads() -> usize { clamped_rayon_threads(std::thread::available_parallelism().map_or(1, |threads| threads.get())) } #[cfg(any(target_os = "windows", test))] fn clamped_rayon_threads(threads: usize) -> usize { threads.clamp(1, RAYON_MAX_THREADS) } #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[cfg(any(target_os = "windows", test))] enum RayonPoolPlan { WorkerThreads(usize), SkipGlobalPool, } #[cfg(any(target_os = "windows", test))] fn rayon_pool_plan(desired: usize, spawnable: usize) -> RayonPoolPlan { let desired = clamped_rayon_threads(desired); let workers = spawnable .saturating_sub(RAYON_RESERVED_NON_RAYON_THREADS) .min(desired); if workers > 0 { RayonPoolPlan::WorkerThreads(workers) } else { RayonPoolPlan::SkipGlobalPool } } /// Probe how many worker threads Windows will let us hold alive /// *simultaneously*, up to `target`. Returns the count actually spawned (0 when /// not even one extra thread is possible). /// /// `Builder::build()` for a multi-thread runtime spawns every worker eagerly /// and **panics** (not `Err`) when Windows refuses one — on a /// memory-constrained host (tiny pagefile / commit limit, `os error 1455`) that /// aborts the whole process at addon load before any JS error can surface — and /// a panic thrown that deep in runtime construction is not something we can /// usefully catch and recover from at module-init time. We instead pre-flight /// with `std::thread::Builder::spawn`, which returns an /// `io::Result`, holding each probe thread alive (so their stacks are committed /// concurrently, matching how real workers coexist) until we know the safe /// count. Probe threads use the std default stack, exactly like Tokio's workers /// (it leaves `thread_stack_size` unset), so the probe is representative. /// /// Keep this Windows-only. On Linux, spawning probe threads from `module_init` /// can deadlock while Bun is loading the `.node`; napi-rs's default runtime /// loads cleanly there and avoids any custom loader-time thread probe. #[cfg(target_os = "windows")] fn probe_spawnable_workers(target: usize) -> usize { let keep_running = Arc::new(AtomicBool::new(true)); let mut handles = Vec::with_capacity(target); for _ in 0..target { let keep = Arc::clone(&keep_running); match std::thread::Builder::new().spawn(move || { while keep.load(Ordering::Relaxed) { std::thread::park_timeout(std::time::Duration::from_millis(1)); } }) { Ok(handle) => handles.push(handle), Err(_) => break, } } let spawned = handles.len(); keep_running.store(false, Ordering::Relaxed); for handle in handles { handle.thread().unpark(); let _ = handle.join(); } spawned } /// Install Rayon's global pool before any `par_iter` or vendored uutils sort /// path can lazily initialize it with Rayon's default one-thread-per-core /// policy. When the probe sees fewer workers than requested, reserve capacity /// for native code that must still perform its own `thread::spawn` (notably /// vendored `sort`'s external-sort helper) and build the global pool with the /// remaining spawnable count; when no worker remains after that reserve, leave /// the global pool untouched and keep patched Rayon callsites on sequential /// paths. /// Rayon stores global initialization in a `Once`, so a failed /// `build_global()` call would permanently poison the process for later /// parallel work. #[cfg(any(target_os = "windows", test))] fn rayon_probe_target(desired: usize) -> usize { clamped_rayon_threads(desired).saturating_add(RAYON_RESERVED_NON_RAYON_THREADS) } #[cfg(target_os = "windows")] fn configure_rayon_pool() { let desired = desired_rayon_threads(); let plan = rayon_pool_plan(desired, probe_spawnable_workers(rayon_probe_target(desired))); let result = match plan { RayonPoolPlan::WorkerThreads(threads) => rayon::ThreadPoolBuilder::new() .num_threads(threads) .build_global(), RayonPoolPlan::SkipGlobalPool => { pi_shell::set_rayon_global_pool_available(false); return; }, }; if result.is_ok() { pi_shell::set_rayon_global_pool_available(true); } } /// Build the custom Tokio runtime napi-rs uses on Windows, sized to what the /// host can actually spawn. Never panics: backs off from /// [`desired_worker_threads`] to whatever the probe allows, and falls back to a /// current-thread runtime (which spawns no workers at build time, so it can't /// abort under commit-limit pressure) when not even one worker is available. /// Returns `None` only if even that fails, in which case we leave napi-rs to /// construct its own default. #[cfg(target_os = "windows")] fn create_windows_napi_tokio_runtime() -> Option { let workers = probe_spawnable_workers(desired_worker_threads()); let multi_thread = (workers > 0) .then(|| { tokio::runtime::Builder::new_multi_thread() .worker_threads(workers) .max_blocking_threads(NAPI_TOKIO_MAX_BLOCKING_THREADS) .enable_all() .build() .ok() }) .flatten(); multi_thread.or_else(|| { tokio::runtime::Builder::new_current_thread() .enable_all() .build() .ok() }) } /// Version sentinel — exists solely so the JS loader can prove at load time /// that the `.node` file on disk is from the same package release as the /// `index.js` ESM wrapper invoking it. /// /// The `js_name` is bumped by `scripts/release.ts` to match the new /// `Cargo.toml` / `package.json` version on every release. The JS loader /// computes the expected name from `package.json#version` and refuses to use /// a `.node` that doesn't expose it, turning the silent /// ` is not a function` crash from a locked-file update (the canonical /// Windows `bun install -g` failure mode) into a clear load-time error. /// /// Bump policy: `__piNativesV{major}_{minor}_{patch}` — non-alphanumerics in /// the version string are mapped to `_` to keep it a valid JS identifier. /// MUST stay in sync with `VERSION_SENTINEL_EXPORT` in /// `packages/natives/native/index.js` (which derives the name from /// `package.json#version`). #[napi(js_name = "__piNativesV17_3_8")] pub const fn pi_natives_version_sentinel() {} /// Native module entry point: install crash diagnostics before any tool can /// invoke a panicking or allocating native call. This runs during `.node` /// load, while the dynamic-loader lock is held, so it MUST NOT spawn threads — /// the Tokio runtime is installed afterwards on Windows by /// [`omp_install_tokio_runtime`], which the JS loader calls once `dlopen` has /// returned. /// /// On Windows, the custom Tokio runtime is host-sized to prevent aborts under /// memory limits (see [`create_windows_napi_tokio_runtime`]). Non-Windows /// builds intentionally use napi-rs's default path. Linux source builds can /// deadlock if this module initializer or post-load setup performs its own /// thread probe, so we keep the probe and custom runtime Windows-only. #[module_init] fn install_native_crash_handler() { crash_handler::install(); } /// Guards [`omp_install_tokio_runtime`] so the runtime is built at most once /// per process even if the loader invokes it more than once. #[cfg(target_os = "windows")] static TOKIO_RUNTIME_INSTALLED: AtomicBool = AtomicBool::new(false); /// Install the bounded Tokio runtime napi-rs adopts for async exports and the /// bounded Rayon global pool used by native parallel iterators. /// /// The JS loader calls this exactly once, synchronously, right *after* `dlopen` /// returns and *before* any async native or parallel iterator runs — never from /// `#[module_init]`. Building a multi-thread runtime eagerly spawns worker /// threads, and doing that during module init (while the dynamic-loader lock is /// held) deadlocks on some hosts: a fresh worker blocks acquiring the loader /// lock that the init thread still owns. napi-rs only materializes its runtime /// on the first async call (`RT` is a `LazyLock`) and /// `create_custom_tokio_runtime` merely records the runtime in a `OnceLock`, so /// installing it post-load is still honored. /// /// Without the Tokio override napi builds its own default (one worker per CPU, /// spawned eagerly), which aborts the process (`os error 1455`) on a /// memory-constrained Windows host before any JS error can surface; /// [`create_windows_napi_tokio_runtime`] pre-flights the spawn instead. Rayon /// has the same one-thread-per-core lazy default, so [`configure_rayon_pool`] /// installs a probed global pool before `count_tokens` or vendored `sort` can /// trigger it across a N-API nounwind boundary. If no worker thread is /// spawnable, patched Rayon callsites stay sequential rather than registering a /// current-thread-only global pool that cannot steal work from later native /// calls. Idempotent. #[napi(js_name = "__ompInstallTokioRuntime")] #[allow(clippy::missing_const_for_fn, reason = "napi macro is incompatible with const fn")] pub fn omp_install_tokio_runtime() { #[cfg(target_os = "windows")] if TOKIO_RUNTIME_INSTALLED.swap(true, Ordering::SeqCst) { return; } #[cfg(target_os = "windows")] if let Some(runtime) = create_windows_napi_tokio_runtime() { create_custom_tokio_runtime(runtime); } #[cfg(target_os = "windows")] configure_rayon_pool(); } #[cfg(test)] mod tests { use super::{ RAYON_MAX_THREADS, RayonPoolPlan, clamped_rayon_threads, rayon_pool_plan, rayon_probe_target, }; #[test] fn rayon_threads_are_capped_for_windows_commit_pressure() { assert_eq!(clamped_rayon_threads(0), 1); assert_eq!(clamped_rayon_threads(1), 1); assert_eq!(clamped_rayon_threads(RAYON_MAX_THREADS + 1), RAYON_MAX_THREADS); } #[test] fn rayon_probe_includes_reserved_non_rayon_capacity() { assert_eq!(rayon_probe_target(4), 5); assert_eq!( rayon_probe_target(RAYON_MAX_THREADS + 4), RAYON_MAX_THREADS + super::RAYON_RESERVED_NON_RAYON_THREADS ); } #[test] fn rayon_uses_requested_pool_when_probe_covers_request_and_reserve() { assert_eq!(rayon_pool_plan(4, 5), RayonPoolPlan::WorkerThreads(4)); assert_eq!( rayon_pool_plan(RAYON_MAX_THREADS + 4, usize::MAX), RayonPoolPlan::WorkerThreads(RAYON_MAX_THREADS) ); } #[test] fn rayon_uses_worker_pool_after_reserving_non_rayon_capacity() { assert_eq!(rayon_pool_plan(4, 3), RayonPoolPlan::WorkerThreads(2)); assert_eq!(rayon_pool_plan(1, 2), RayonPoolPlan::WorkerThreads(1)); } #[test] fn rayon_skips_global_pool_when_only_reserved_capacity_can_spawn() { assert_eq!(rayon_pool_plan(4, 1), RayonPoolPlan::SkipGlobalPool); assert_eq!(rayon_pool_plan(1, 0), RayonPoolPlan::SkipGlobalPool); } }