Skip to main content

pmpx_plugin/abi/
dispatch.rs

1//! The output side: writing a command into the boundary, the dispatch entry point, and the panic
2//! guard around it.
3//!
4//! This logic lives here rather than inside the `export!` macro so that it can be tested directly.
5
6use std::path::PathBuf;
7
8use crate::{CommandSpec, Context, PackageManager, Verb};
9
10use super::marshal::{free_str, leak_bytes, os_to_bytes, read_os, read_str};
11use super::types::{PmpxCommand, PmpxStr, PMPX_ERR_INTERNAL, PMPX_ERR_INVALID_ARGS, PMPX_OK};
12
13/// Write a [`CommandSpec`] in its cross-boundary form, with the memory allocated by this side.
14/// # Safety
15/// `out` must point at a writable [`PmpxCommand`].
16pub unsafe fn write_command(out: *mut PmpxCommand, spec: CommandSpec) {
17    let program = leak_bytes(&os_to_bytes(&spec.program));
18
19    let args: Vec<PmpxStr> = spec
20        .args
21        .iter()
22        .map(|a| leak_bytes(&os_to_bytes(a)))
23        .collect();
24    let args_boxed: Box<[PmpxStr]> = args.into_boxed_slice();
25    let args_len = args_boxed.len();
26    let args_ptr = args_boxed.as_ptr();
27    std::mem::forget(args_boxed);
28
29    let cwd = match &spec.cwd {
30        Some(p) => leak_bytes(&os_to_bytes(p.as_os_str())),
31        None => PmpxStr::EMPTY,
32    };
33
34    unsafe {
35        *out = PmpxCommand {
36            program,
37            args: args_ptr,
38            args_len,
39            cwd,
40        };
41    }
42}
43
44/// Free the contents of a [`PmpxCommand`] filled in by this side's [`write_command`], without
45/// freeing `c` itself (that struct lives on the host side, usually on the stack).
46/// # Safety
47/// `c` must come from one successful `command` call on this side and may be freed only once.
48pub unsafe fn free_command(c: *mut PmpxCommand) {
49    if c.is_null() {
50        return;
51    }
52    let cmd = unsafe { &*c };
53
54    unsafe { free_str(cmd.program) };
55    unsafe { free_str(cmd.cwd) };
56
57    if !cmd.args.is_null() && cmd.args_len > 0 {
58        // Strictly paired with the Box<[PmpxStr]> in write_command.
59        let raw = std::ptr::slice_from_raw_parts_mut(cmd.args as *mut PmpxStr, cmd.args_len);
60        let args = unsafe { Box::from_raw(raw) };
61        for s in args.iter() {
62            unsafe { free_str(*s) };
63        }
64    }
65}
66
67/// All the wiring of one `command` call: read the inputs, call
68/// [`crate::PackageManager::command`], write the output.
69/// This logic lives here rather than in the `export!` macro so that it can be tested directly.
70/// # Safety
71/// See the Safety section of [`PmpxPluginV1::command`](crate::abi::PmpxPluginV1::command). In
72/// addition, `plugin` must be a valid instance in this process.
73#[allow(clippy::too_many_arguments)]
74pub unsafe fn dispatch_command(
75    plugin: &dyn PackageManager,
76    project_root: PmpxStr,
77    matched: *const PmpxStr,
78    matched_len: usize,
79    verb: u32,
80    args: *const PmpxStr,
81    args_len: usize,
82    out: *mut PmpxCommand,
83) -> u32 {
84    if out.is_null() {
85        return PMPX_ERR_INVALID_ARGS;
86    }
87
88    let Some(verb) = Verb::from_abi(verb) else {
89        return PMPX_ERR_INVALID_ARGS;
90    };
91
92    let project_root = PathBuf::from(unsafe { read_os(project_root) });
93
94    // `matched` is text (file names declared in the manifest), so UTF-8 is checked here.
95    let mut matched_names = Vec::with_capacity(matched_len);
96    for i in 0..matched_len {
97        let raw = unsafe { *matched.add(i) };
98        match unsafe { read_str(raw) } {
99            Ok(s) => matched_names.push(s.to_string()),
100            Err(code) => return code,
101        }
102    }
103
104    // `args` are arguments and may be arbitrary bytes -- converted to OsString as-is, losslessly.
105    let mut arg_list = Vec::with_capacity(args_len);
106    for i in 0..args_len {
107        let raw = unsafe { *args.add(i) };
108        arg_list.push(unsafe { read_os(raw) });
109    }
110
111    let ctx = Context {
112        project_root,
113        matched: matched_names,
114    };
115
116    match plugin.command(&ctx, verb, &arg_list) {
117        Ok(spec) => {
118            unsafe { write_command(out, spec) };
119            PMPX_OK
120        }
121        Err(e) => e.code(),
122    }
123}
124
125/// Wrap one cross-boundary call in `catch_unwind`.
126/// Since Rust 1.81, letting a panic cross an `extern "C"` boundary aborts the process outright,
127/// and the host's `catch_unwind` cannot help at all, so the plugin has to catch it itself. The
128/// host side wraps one more layer, for the cases where "the plugin forgot to wrap" or "the plugin
129/// was built with `panic=abort`".
130pub fn guard(f: impl FnOnce() -> u32) -> u32 {
131    // `AssertUnwindSafe`: once the caller has PMPX_ERR_INTERNAL it aborts the operation and never
132    // touches the caught state again.
133    std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).unwrap_or(PMPX_ERR_INTERNAL)
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn writes_and_frees_a_command() {
142        let spec = CommandSpec::new("cargo")
143            .arg("add")
144            .arg("serde")
145            .cwd("/tmp/project");
146
147        let mut out = std::mem::MaybeUninit::<PmpxCommand>::uninit();
148        unsafe { write_command(out.as_mut_ptr(), spec) };
149        let mut cmd = unsafe { out.assume_init() };
150
151        assert_eq!(cmd.args_len, 2);
152        let program = unsafe { std::slice::from_raw_parts(cmd.program.ptr, cmd.program.len) };
153        assert_eq!(program, b"cargo");
154
155        let arg0 = unsafe { *cmd.args.add(0) };
156        let a0 = unsafe { std::slice::from_raw_parts(arg0.ptr, arg0.len) };
157        assert_eq!(a0, b"add");
158
159        let cwd = unsafe { std::slice::from_raw_parts(cmd.cwd.ptr, cmd.cwd.len) };
160        assert_eq!(cwd, b"/tmp/project");
161
162        unsafe { free_command(&mut cmd as *mut _) };
163    }
164
165    #[test]
166    fn writes_a_command_with_no_args_and_no_cwd() {
167        let spec = CommandSpec::new("cargo");
168
169        let mut out = std::mem::MaybeUninit::<PmpxCommand>::uninit();
170        unsafe { write_command(out.as_mut_ptr(), spec) };
171        let mut cmd = unsafe { out.assume_init() };
172
173        assert_eq!(cmd.args_len, 0);
174        assert!(
175            cmd.cwd.is_empty(),
176            "cwd without an override should be EMPTY"
177        );
178
179        unsafe { free_command(&mut cmd as *mut _) };
180    }
181
182    #[test]
183    fn free_command_tolerates_null() {
184        unsafe { free_command(std::ptr::null_mut()) };
185    }
186
187    #[test]
188    fn guard_turns_a_panic_into_internal_error() {
189        assert_eq!(guard(|| PMPX_OK), PMPX_OK);
190        assert_eq!(guard(|| panic!("the plugin blew up")), PMPX_ERR_INTERNAL);
191    }
192}