pmpx_plugin/shell.rs
1//! The plugin side of the ABI: everything between `export!`'s shims and the trait a plugin writes.
2//!
3//! This is the only module in the crate that touches raw memory. It owns three jobs:
4//!
5//! - **Answer the host's questions** through the accessors (`dispatch` is what `export!` points the
6//! `command` capability at),
7//! - **Build the typed [`Context`]** out of them, so a plugin never sees a key or a pointer,
8//! - **Hand the answer back** in the ABI's shape, with the memory this side owns.
9//!
10//! Everything here runs *inside* the plugin, on data the host lent it. The rules it obeys:
11//!
12//! - Nothing lent may be kept past the call.
13//! - Memory is freed by the side that allocated it: what this module leases out for the answer is
14//! reclaimed by `free_command` / `free_str`, never by the host.
15//! - A panic must not cross: the shims in `export!` wrap every entry point in `catch_unwind`.
16
17use std::ffi::{OsStr, OsString};
18
19use pmpx_plugin_abi::{
20 PmpxCommand, PmpxContext, PmpxStr, PMPX_ERR_INTERNAL, PMPX_ERR_INVALID_ARGS,
21 PMPX_ERR_UNSUPPORTED_VERB, PMPX_MAX_ITEMS, PMPX_OK,
22};
23
24use crate::{CommandSpec, Context, PackageManager, Verb};
25
26/// What `name` / `family` answer when the plugin panicked before it could answer at all.
27///
28/// A marker rather than an empty string: the host refuses to load a plugin whose name disagrees with
29/// its manifest, and this makes that refusal say what happened instead of showing an empty pair of
30/// quotes.
31pub const PANIC_MARKER: &str = "<the plugin panicked>";
32
33/// Write a [`CommandSpec`] in its cross-boundary form, with the memory allocated by this side.
34///
35/// # Safety
36/// `out` must point at writable memory for a [`PmpxCommand`].
37pub unsafe fn write_command(out: *mut PmpxCommand, spec: CommandSpec) {
38 let program = leak_bytes(&os_to_bytes(&spec.program));
39
40 let args: Vec<PmpxStr> = spec
41 .args
42 .iter()
43 .map(|arg| leak_bytes(&os_to_bytes(arg)))
44 .collect();
45 let args = args.into_boxed_slice();
46 let args_len = args.len();
47 let args = Box::into_raw(args) as *const PmpxStr;
48
49 let cwd = match &spec.cwd {
50 Some(dir) => leak_bytes(&os_to_bytes(dir.as_os_str())),
51 None => PmpxStr::EMPTY,
52 };
53
54 // SAFETY: the caller promises writable memory; every pointer above belongs to this side and is
55 // reclaimed by `free_command`.
56 unsafe {
57 (*out).size = std::mem::size_of::<PmpxCommand>();
58 (*out).program = program;
59 (*out).args = pmpx_plugin_abi::PmpxSlice::new(args, args_len);
60 (*out).cwd = cwd;
61 }
62}
63
64/// Release everything [`write_command`] allocated, without touching the struct itself (it lives on
65/// the host's stack).
66///
67/// # Safety
68/// `command` must come from one successful [`write_command`] on this side, and must be released
69/// once.
70pub unsafe fn free_command(command: *mut PmpxCommand) {
71 if command.is_null() {
72 return;
73 }
74
75 // SAFETY: the caller vouches for the pointer.
76 let command = unsafe { &*command };
77
78 free_str(command.program);
79 free_str(command.cwd);
80
81 if !command.args.is_absent() {
82 // SAFETY: `write_command` leaked exactly this box.
83 let args = unsafe {
84 Box::from_raw(std::ptr::slice_from_raw_parts_mut(
85 command.args.ptr as *mut PmpxStr,
86 command.args.len,
87 ))
88 };
89 for arg in args.iter() {
90 free_str(*arg);
91 }
92 }
93}
94
95/// Give one string back to this side's allocator.
96///
97/// # Safety
98/// `s` must come from [`leak_bytes`] or [`leak_str`], and must be released once.
99pub unsafe fn free_str(s: PmpxStr) {
100 if s.ptr.is_null() {
101 return;
102 }
103 // SAFETY: the caller vouches for the provenance and the single release.
104 let bytes =
105 unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(s.ptr as *mut u8, s.len)) };
106 drop(bytes);
107}
108
109/// Lease out bytes for the host to read.
110pub fn leak_bytes(bytes: &[u8]) -> PmpxStr {
111 let boxed = bytes.to_vec().into_boxed_slice();
112 let len = boxed.len();
113 PmpxStr::new(Box::into_raw(boxed) as *const u8, len)
114}
115
116/// Lease out text for the host to read.
117pub fn leak_str(s: &str) -> PmpxStr {
118 leak_bytes(s.as_bytes())
119}
120
121/// Read a borrowed C string as text, checking UTF-8.
122///
123/// # Safety
124/// `s` must be valid for the duration of the call.
125pub unsafe fn read_str(s: PmpxStr) -> Option<&'static str> {
126 // SAFETY: the caller vouches for the memory; the lifetime is deliberately tied to the call site
127 // by making the caller keep the value inside it.
128 let bytes = unsafe { s.as_bytes() }?;
129 std::str::from_utf8(bytes).ok().map(|text| {
130 // The bytes belong to the host and are valid for this call; saying `'static` here is what
131 // lets the shells read a key without copying it. Every caller uses the value immediately,
132 // inside the same call.
133 unsafe { std::mem::transmute::<&str, &'static str>(text) }
134 })
135}
136
137/// Read a borrowed C string as an owned `OsString`, losslessly where the platform allows it.
138///
139/// # Safety
140/// As [`read_str`].
141pub unsafe fn read_os(s: PmpxStr) -> OsString {
142 // SAFETY: the caller vouches for the memory.
143 let bytes = unsafe { s.as_bytes() }.unwrap_or(&[]);
144 pmpx_plugin_abi::bytes_to_os(bytes)
145}
146
147/// The plugin-side entry point `export!` exposes as the `command` capability.
148///
149/// # Safety
150/// See the safety notes of the ABI's `command` table: the context must be the host's, valid and
151/// read-only for this call, and `out` must be writable.
152pub unsafe fn dispatch(
153 plugin: &dyn PackageManager,
154 context: *const PmpxContext,
155 out: *mut PmpxCommand,
156) -> u32 {
157 if out.is_null() || context.is_null() {
158 return PMPX_ERR_INVALID_ARGS;
159 }
160
161 // SAFETY: non-null, and the host keeps it read-only for the call.
162 let raw = unsafe { &*context };
163
164 // The host says how much of the struct it built; reading past that would be reading whatever
165 // happens to be there.
166 if raw.size < std::mem::size_of::<PmpxContext>() {
167 return PMPX_ERR_INVALID_ARGS;
168 }
169
170 // A verb this build does not know is answered as *unsupported*, not as invalid arguments: that
171 // is what keeps a new verb additive, because it leaves the host's degradation path open.
172 let Some(verb) = Verb::from_abi(raw.verb) else {
173 return PMPX_ERR_UNSUPPORTED_VERB;
174 };
175
176 let Ok(args) = (unsafe { read_args(context) }) else {
177 return PMPX_ERR_INVALID_ARGS;
178 };
179
180 // SAFETY: the context is the host's, valid for this call; the typed context borrows it only for
181 // as long as this function runs.
182 let context = unsafe { Context::from_host(context) };
183
184 // Bracketed so that anything the plugin logs can say what it was working with, and so that a
185 // plugin logging outside a call finds nothing rather than a stale one.
186 let description = context.describe(verb, args.len());
187 let answer = crate::debug::with_call(description, || plugin.command(&context, verb, &args));
188
189 match answer {
190 Ok(spec) => {
191 // SAFETY: `out` was checked non-null, and the host promises it is writable.
192 unsafe { write_command(out, spec) };
193 PMPX_OK
194 }
195 Err(error) => error.code(),
196 }
197}
198
199/// Read the arguments the host sent.
200///
201/// # Safety
202/// `context` must be a valid host context.
203unsafe fn read_args(context: *const PmpxContext) -> Result<Vec<OsString>, ()> {
204 let raw = unsafe { &*context };
205 let key = PmpxStr::new(
206 pmpx_plugin_abi::PMPX_KEY_ARGS.as_ptr(),
207 pmpx_plugin_abi::PMPX_KEY_ARGS.len(),
208 );
209
210 let count = unsafe { (raw.count)(context, key) };
211 // A host that answers an absurd count is refused rather than allocated for.
212 if count > PMPX_MAX_ITEMS {
213 return Err(());
214 }
215
216 let mut args = Vec::with_capacity(count);
217 for index in 0..count {
218 let value = unsafe { (raw.get)(context, key, index) };
219 if value.is_absent() {
220 return Err(());
221 }
222 args.push(unsafe { read_os(value) });
223 }
224 Ok(args)
225}
226
227/// The `free_str` shim's implementation.
228///
229/// # Safety
230/// As [`free_str`].
231pub unsafe fn dispatch_free_str(s: PmpxStr) {
232 // SAFETY: the caller (the host) only passes back what this side leased out.
233 unsafe { free_str(s) };
234}
235
236/// The `free_command` shim's implementation.
237///
238/// # Safety
239/// As [`free_command`].
240pub unsafe fn dispatch_free_command(command: *mut PmpxCommand) {
241 // SAFETY: as above.
242 unsafe { free_command(command) };
243}
244
245/// Wrap one cross-boundary call in `catch_unwind`.
246///
247/// Since Rust 1.81, letting a panic cross an `extern "C"` boundary aborts the process outright, and
248/// the host cannot help at all -- so the plugin has to catch it itself, here.
249pub fn guard(f: impl FnOnce() -> u32) -> u32 {
250 // `AssertUnwindSafe`: once the caller sees an error code it aborts the operation and never looks
251 // at the borrowed state again, so there is nothing for the unwind-safety check to protect.
252 std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).unwrap_or(PMPX_ERR_INTERNAL)
253}
254
255/// The same, for one of the two `-> PmpxStr` shims.
256pub fn guard_str(f: impl FnOnce() -> PmpxStr) -> PmpxStr {
257 std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).unwrap_or_else(|_| {
258 let marker = leak_str(PANIC_MARKER);
259 // The host frees what it is handed, including this marker.
260 marker
261 })
262}
263
264/// Read the arguments a command table was given, converting paths and arguments both ways.
265///
266/// Re-exported shape helpers: they exist so the shells and the tests do not each write their own
267/// platform `cfg`.
268pub(crate) fn os_to_bytes(s: &OsStr) -> Vec<u8> {
269 pmpx_plugin_abi::os_to_bytes(s)
270}
271
272/// The rustc version this crate was built with, injected by `build.rs`. Diagnostics only.
273pub const BUILD_RUSTC: &str = env!("PMPX_BUILD_RUSTC");
274
275/// The target this crate was built for, injected by `build.rs`. Diagnostics only.
276pub const BUILD_TARGET: &str = env!("PMPX_BUILD_TARGET");
277
278/// Remember the plugin's own name, for the no-host fallback.
279pub fn remember_name(name: &str) {
280 crate::debug::remember_name(name);
281}
282
283/// Install the host's logging table, after the caller has checked its size.
284///
285/// # Safety
286/// As [`debug`](mod@crate::debug)'s own note: the table must be the host's, large enough, and alive for the
287/// process.
288pub unsafe fn install_log_table(table: *const pmpx_plugin_abi::PmpxLog) {
289 // SAFETY: the caller checked the size and the lifetime.
290 unsafe { crate::debug::set_log_table(table) };
291}