Skip to main content

zsh/extensions/
native_cmds.rs

1//! Host-registered native commands — builtins contributed by the *binary*
2//! rather than by this library.
3//!
4//! `EXT_BUILTIN_NAMES` (`extensions/ext_builtins.rs`) and the daemon's
5//! `ZSHRS_BUILTIN_NAMES` are both compile-time lists owned by this crate. A
6//! fat binary that links sibling runtimes into the shell's address space —
7//! `zshrs-native` links zvcs (`git`), arblang (`arb`) and strykelang
8//! (`stryke`) — has no way to extend either: they are `const` arrays, and the
9//! runtimes cannot be dependencies of this crate (zvcs depends on its own
10//! vendored gitoxide by path, which makes any dependent unpublishable).
11//!
12//! So the binary registers them here, once, before the shell starts. A
13//! registered name dispatches in-process on a direct function call: no fork,
14//! no execve, no `PATH` walk, no dynamic loader — the same treatment `cat` and
15//! `sort` already get from `reg_overridable!`.
16//!
17//! # Dispatch order
18//!
19//! Registration does not jump the queue. zsh resolves a command word as
20//! alias → function → builtin → external (c:Src/exec.c:3038-3068), and a
21//! native command sits in the *builtin* slot, after the ported builtin table:
22//!
23//! * a user `git() { … }` still wins, exactly as it shadows `cat` today;
24//! * `command git` still reaches whatever `git` is on `PATH`, because the
25//!   forced-external path never consults this registry;
26//! * `builtin git` reaches the native one.
27//!
28//! # Registration is one-shot and start-up only
29//!
30//! The table is written once by the binary's `main` before the shell runs and
31//! is read from every command dispatch after that, including from the worker
32//! threads. It is therefore an `RwLock` whose write side is expected to be
33//! uncontended: registering after startup is allowed but pointless, and no
34//! path ever removes an entry — a name that answered `whence -w` one moment
35//! must not vanish the next.
36
37use std::collections::BTreeMap;
38use std::sync::{OnceLock, RwLock};
39
40/// A native command body: the full argv (argv[0] is the command name, as
41/// invoked) in, a wait-status-style exit code out.
42///
43/// Taking argv[0] rather than only the operands is what lets a runtime keep
44/// its own `argv[0]`-dependent behaviour — zvcs dispatches `git-<verb>` off
45/// its own name (`dashed_subcommand`), and its diagnostics are prefixed with
46/// it.
47pub type NativeCmd = Box<dyn Fn(&[String]) -> i32 + Send + Sync>;
48
49fn table() -> &'static RwLock<BTreeMap<String, NativeCmd>> {
50    static TABLE: OnceLock<RwLock<BTreeMap<String, NativeCmd>>> = OnceLock::new();
51    TABLE.get_or_init(|| RwLock::new(BTreeMap::new()))
52}
53
54/// Register `name` as a native command backed by `f`.
55///
56/// Idempotent per name in the sense that the last registration wins; the
57/// binary calls this once per runtime from `main`, before the shell reads a
58/// line. A poisoned lock is ignored rather than panicking — losing a builtin
59/// registration must not take the shell down at startup.
60pub fn register<F>(name: &str, f: F)
61where
62    F: Fn(&[String]) -> i32 + Send + Sync + 'static,
63{
64    if let Ok(mut t) = table().write() {
65        t.insert(name.to_string(), Box::new(f));
66    }
67}
68
69/// Is `name` a host-registered native command?
70///
71/// The hot path: consulted on every command word that is neither a function
72/// nor a ported builtin, so it must not allocate. A read lock on a `BTreeMap`
73/// of a handful of short keys is a few compares.
74pub fn is_registered(name: &str) -> bool {
75    table()
76        .read()
77        .map(|t| t.contains_key(name))
78        .unwrap_or(false)
79}
80
81/// Registered *and* not masked by `disable NAME`.
82///
83/// The gate the two dispatch sites use. `disable` is zsh's way of taking a
84/// builtin out of the way without unsetting anything (c:Src/builtin.c:541-547
85/// toggles `DISABLED` on the node; this port tracks the same set in
86/// `BUILTINS_DISABLED`), and it has to work on a native command for the same
87/// reason it works on the `cat` shadow: it is the one escape hatch that is
88/// per-shell, reversible with `enable`, and needs no change at the call site.
89/// A disabled name falls through to the `PATH` binary.
90pub fn is_enabled(name: &str) -> bool {
91    is_registered(name)
92        && !crate::ported::builtin::BUILTINS_DISABLED
93            .lock()
94            .map(|s| s.contains(name))
95            .unwrap_or(false)
96}
97
98/// Every registered name, sorted. Feeds the `builtins` magic assoc
99/// (`${(k)builtins}`), `whence -m`, and compsys's command-position
100/// completion, so the shell reports the same set it will actually dispatch.
101pub fn names() -> Vec<String> {
102    table()
103        .read()
104        .map(|t| t.keys().cloned().collect())
105        .unwrap_or_default()
106}
107
108/// Run `name` with `argv` (argv[0] included) if it is registered.
109///
110/// Returns `None` when the name is not ours, so callers fall through to their
111/// existing next step — `PATH` lookup, or "command not found".
112///
113/// The read lock is held for the duration of the call. That is deliberate:
114/// nothing ever removes an entry, and the alternative (clone the boxed closure
115/// out) is not possible for a `dyn Fn`. Re-entrant dispatch — a native command
116/// that runs shell code that runs another native command — takes the read lock
117/// twice, which an `RwLock` grants.
118pub fn dispatch(name: &str, argv: &[String]) -> Option<i32> {
119    let t = table().read().ok()?;
120    let f = t.get(name)?;
121    Some(f(argv))
122}
123
124/// Test-only removal.
125///
126/// Production never removes an entry — a name that answered `whence -w` one
127/// moment must not vanish the next — but the table is process-global and the
128/// `${(k)builtins}` scan reads it, so a test that registers a probe name has
129/// to take it back out or the next test in the same process sees a builtin
130/// nobody registered.
131#[cfg(test)]
132pub(crate) fn unregister(name: &str) {
133    if let Ok(mut t) = table().write() {
134        t.remove(name);
135    }
136}
137
138thread_local! {
139    /// Set while a `command NAME …` precommand is dispatching NAME.
140    ///
141    /// `command` means "not the function, not the builtin — the thing on
142    /// `PATH`" (c:Src/exec.c:3275-3278, the `BINF_COMMAND && !POSIXBUILTINS`
143    /// arm that clears the builtin node). zshrs already honours that for its
144    /// own in-process shadows: `PATH= command cat` reports `command not
145    /// found: cat` rather than running the built-in `cat`.
146    ///
147    /// A native command has to answer the same way, and the site that catches
148    /// it — `execute_external_bg` — is the very site `command` dispatches
149    /// through, so the two are indistinguishable without this flag. The
150    /// `command` handler raises it for the duration of that one call.
151    ///
152    /// Thread-local because it describes one invocation in flight, and
153    /// commands run on worker threads.
154    static FORCED_EXTERNAL: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
155}
156
157/// Restores the previous `command`-prefix state on drop, so a nested
158/// dispatch (a `command` inside a function a native command ran) unwinds
159/// correctly instead of leaving the flag stuck on.
160pub struct ForcedExternalGuard(bool);
161
162impl Drop for ForcedExternalGuard {
163    fn drop(&mut self) {
164        FORCED_EXTERNAL.with(|f| f.set(self.0));
165    }
166}
167
168/// Mark the current invocation as `command`-forced for as long as the
169/// returned guard lives.
170#[must_use]
171pub fn force_external() -> ForcedExternalGuard {
172    ForcedExternalGuard(FORCED_EXTERNAL.with(|f| f.replace(true)))
173}
174
175/// True while a `command NAME` precommand is dispatching NAME, i.e. while the
176/// user has explicitly asked for the `PATH` binary rather than this table.
177pub fn is_forced_external() -> bool {
178    FORCED_EXTERNAL.with(std::cell::Cell::get)
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use std::sync::atomic::{AtomicUsize, Ordering};
185
186    /// The registry answers for a name it was given, and only for that name.
187    /// Names are test-unique because the table is process-global and the test
188    /// binary runs many tests in one process.
189    #[test]
190    fn registered_name_dispatches_with_full_argv() {
191        static SEEN: AtomicUsize = AtomicUsize::new(0);
192        register("zshrs_test_native_dispatch", |argv| {
193            SEEN.store(argv.len(), Ordering::SeqCst);
194            assert_eq!(argv[0], "zshrs_test_native_dispatch");
195            7
196        });
197
198        assert!(is_registered("zshrs_test_native_dispatch"));
199        assert!(!is_registered("zshrs_test_native_never_registered"));
200
201        let argv = [
202            "zshrs_test_native_dispatch".to_string(),
203            "--flag".to_string(),
204        ];
205        assert_eq!(dispatch("zshrs_test_native_dispatch", &argv), Some(7));
206        assert_eq!(SEEN.load(Ordering::SeqCst), 2);
207        // An unregistered name must fall through rather than answer.
208        assert_eq!(dispatch("zshrs_test_native_never_registered", &argv), None);
209
210        unregister("zshrs_test_native_dispatch");
211        assert!(!is_registered("zshrs_test_native_dispatch"));
212    }
213
214    /// `disable NAME` masks a native command without unregistering it, and
215    /// `enable NAME` takes it back — the shell falls through to `PATH` in
216    /// between. Exercised through the same `BUILTINS_DISABLED` set that
217    /// `bin_enable` writes.
218    #[test]
219    fn disable_masks_dispatch_and_enable_restores() {
220        register("zshrs_test_native_disable", |_| 0);
221        assert!(is_enabled("zshrs_test_native_disable"));
222
223        crate::ported::builtin::BUILTINS_DISABLED
224            .lock()
225            .unwrap()
226            .insert("zshrs_test_native_disable".to_string());
227        assert!(is_registered("zshrs_test_native_disable"));
228        assert!(!is_enabled("zshrs_test_native_disable"));
229
230        crate::ported::builtin::BUILTINS_DISABLED
231            .lock()
232            .unwrap()
233            .remove("zshrs_test_native_disable");
234        assert!(is_enabled("zshrs_test_native_disable"));
235
236        unregister("zshrs_test_native_disable");
237    }
238
239    /// The `command NAME` guard is scoped and nests: an inner scope restores
240    /// the outer state on drop rather than clearing the flag outright.
241    #[test]
242    fn forced_external_guard_restores_previous_state() {
243        assert!(!is_forced_external());
244        {
245            let _outer = force_external();
246            assert!(is_forced_external());
247            {
248                let _inner = force_external();
249                assert!(is_forced_external());
250            }
251            // Still inside the outer `command` dispatch.
252            assert!(is_forced_external());
253        }
254        assert!(!is_forced_external());
255    }
256}