Skip to main content

pmpx_plugin/abi/
types.rs

1//! `#[repr(C)]` data and the integers that describe it.
2//!
3//! Everything that crosses the `dlopen` boundary is here: the string, command and vtable
4//! structures, the ABI version, the error codes, the verb numbers and the build diagnostics. No
5//! type in this file may have a layout that depends on a dependency version. The marshalling
6//! helpers live in the sibling `marshal` module, and the output side in `dispatch`.
7
8// ---- Version ----
9
10/// Version of the cross-boundary layout, an independent integer, fully decoupled from the crate
11/// version. Bump it by one only when the shape of [`PmpxPluginV1`] / [`PmpxCommand`] / [`PmpxStr`],
12/// the verb numbering, or the error-code semantics really change. The host uses it as its only
13/// hard check and refuses to load when it does not match.
14pub const ABI_VERSION: u32 = 1;
15
16// ---- Error codes ----
17
18/// Success.
19pub const PMPX_OK: u32 = 0;
20
21/// This backend does not support that verb.
22/// The host treats it specially: `pmpx exec` degrades to passing through verbatim when it sees
23/// this code, while the other verbs report the error as-is, so it must stay separate from
24/// [`PMPX_ERR_INTERNAL`].
25pub const PMPX_ERR_UNSUPPORTED_VERB: u32 = 1;
26
27/// Invalid input -- an unknown verb number, a null `out`, or non-UTF-8 in `matched`.
28pub const PMPX_ERR_INVALID_ARGS: u32 = 2;
29
30/// The plugin failed internally, or it panicked (a panic is caught by [`guard`](crate::abi::guard)
31/// and mapped here, with the details on stderr).
32pub const PMPX_ERR_INTERNAL: u32 = 3;
33
34// ---- Verb numbers ----
35
36/// Number of [`Verb::Install`](crate::Verb::Install).
37pub const VERB_INSTALL: u32 = 0;
38/// Number of [`Verb::Remove`](crate::Verb::Remove).
39pub const VERB_REMOVE: u32 = 1;
40/// Number of [`Verb::Run`](crate::Verb::Run).
41pub const VERB_RUN: u32 = 2;
42/// Number of [`Verb::Build`](crate::Verb::Build).
43pub const VERB_BUILD: u32 = 3;
44/// Number of [`Verb::Test`](crate::Verb::Test).
45pub const VERB_TEST: u32 = 4;
46/// Number of [`Verb::Update`](crate::Verb::Update).
47pub const VERB_UPDATE: u32 = 5;
48/// Number of [`Verb::Exec`](crate::Verb::Exec).
49pub const VERB_EXEC: u32 = 6;
50
51// ---- Data structures ----
52
53/// A cross-boundary string: pointer + length, with no NUL terminator required.
54/// `ptr` / `len` describe raw bytes (possibly a path or a command-line argument) which on Unix
55/// need not be valid UTF-8; UTF-8 is checked only where the data is explicitly required to be
56/// text, and a failure returns [`PMPX_ERR_INVALID_ARGS`] rather than UB.
57/// It carries a length instead of relying on NUL because the pointer returned by `str::as_ptr()`
58/// is not guaranteed to be followed by a NUL.
59#[repr(C)]
60#[derive(Debug, Copy, Clone)]
61pub struct PmpxStr {
62    /// Start address. May be null when `len == 0`.
63    pub ptr: *const u8,
64    /// Length in bytes.
65    pub len: usize,
66}
67
68// SAFETY: `PmpxStr` is "a read-only byte range plus a length"; the only unsafe part of sharing it
69// across threads is that the memory `ptr` points at must still be valid. Both origins of this
70// struct are well defined:
71//   - one passed in by the host: valid for the whole call;
72//   - one produced by the plugin: points at a `Box<[u8]>` the plugin leaked, valid until
73//     `free_str`.
74// Neither is freed or written while it is shared. So marking it `Sync` holds.
75unsafe impl Sync for PmpxStr {}
76
77impl PmpxStr {
78    /// Empty. `len == 0` with a null pointer -- in `cwd` this means "no override".
79    pub const EMPTY: PmpxStr = PmpxStr {
80        ptr: std::ptr::null(),
81        len: 0,
82    };
83
84    /// Build from a `'static` string (`const` so that the `static` vtable of `export!` can be
85    /// filled in at compile time).
86    pub const fn from_static(s: &'static str) -> Self {
87        Self {
88            ptr: s.as_ptr(),
89            len: s.len(),
90        }
91    }
92
93    /// Whether it is empty.
94    pub const fn is_empty(&self) -> bool {
95        self.len == 0
96    }
97}
98
99/// A command description that crosses the boundary. Filled in by the plugin and freed by the
100/// plugin ([`free_command`](crate::abi::free_command)); the host only reads it.
101#[repr(C)]
102#[derive(Debug, Copy, Clone)]
103pub struct PmpxCommand {
104    /// Executable.
105    pub program: PmpxStr,
106    /// Argument array, with `args_len` elements.
107    pub args: *const PmpxStr,
108    /// Number of elements in `args`.
109    pub args_len: usize,
110    /// Working-directory override. `len == 0` means use the project root given by the host.
111    pub cwd: PmpxStr,
112}
113
114// SAFETY: Same as `PmpxStr` -- this struct is just "references to read-only bytes plus an array
115// length".
116unsafe impl Sync for PmpxCommand {}
117
118/// The only struct a plugin exports, and it is that table of function pointers; once the host has
119/// obtained it, every interaction goes through these pointers, with no trait object and none of
120/// the UB that comes from converting between vtables.
121#[repr(C)]
122pub struct PmpxPluginV1 {
123    /// Must equal [`ABI_VERSION`]. This is the first field the host compares.
124    pub abi_version: u32,
125
126    /// The rustc version that built this plugin, injected by `pmpx-plugin`'s build.rs.
127    /// Diagnostics only, never a hard check -- plugins built by different rustcs can be loaded
128    /// safely under this C ABI.
129    pub rustc_version: PmpxStr,
130
131    /// The target triple that built this plugin. Also diagnostics only.
132    pub target: PmpxStr,
133
134    /// Plugin name. The memory belongs to the plugin; the host frees it with
135    /// [`free_str`](crate::abi::free_str) after reading, and compares it against the name declared
136    /// in the manifest -- a mismatch means the wrong thing was installed.
137    pub name: unsafe extern "C" fn() -> PmpxStr,
138
139    /// Ecosystem family. The memory belongs to the plugin; the host frees it with
140    /// [`free_str`](crate::abi::free_str) after reading.
141    pub family: unsafe extern "C" fn() -> PmpxStr,
142
143    /// Translate "verb + arguments" into one command. When it returns [`PMPX_OK`], `out` has been
144    /// filled in and the host calls [`free_command`](crate::abi::free_command) when done; otherwise
145    /// it returns `PMPX_ERR_*` and `out` is untouched.
146    ///
147    /// # Safety
148    /// - `project_root` / `matched` / `args` must be allocated by the host, valid and read-only
149    ///   for the duration of the call;
150    /// - `out` must point at a writable [`PmpxCommand`];
151    /// - a panic must not cross this boundary: since Rust 1.81, unwinding across `extern "C"`
152    ///   aborts the process and the host's `catch_unwind` cannot save it, so `export!` wraps
153    ///   everything in `catch_unwind`.
154    pub command: unsafe extern "C" fn(
155        project_root: PmpxStr,
156        matched: *const PmpxStr,
157        matched_len: usize,
158        verb: u32,
159        args: *const PmpxStr,
160        args_len: usize,
161        out: *mut PmpxCommand,
162    ) -> u32,
163
164    /// Free the memory held by the values returned from [`PmpxPluginV1::name`] /
165    /// [`PmpxPluginV1::family`].
166    ///
167    /// # Safety
168    /// `s` must come from the same plugin and may be freed only once.
169    pub free_str: unsafe extern "C" fn(PmpxStr),
170
171    /// Free the memory filled in by [`PmpxPluginV1::command`]'s [`PmpxCommand`], without freeing
172    /// the struct itself (that struct lives on the host side).
173    ///
174    /// # Safety
175    /// `c` must come from one successful `command` call on the same plugin and may be freed only
176    /// once.
177    pub free_command: unsafe extern "C" fn(*mut PmpxCommand),
178}
179
180// SAFETY: This struct is a read-only table filled in from compile-time constants: a few integers,
181// two `'static` byte ranges, and five function pointers. It is never modified after that.
182// Function pointers are `Sync` themselves.
183unsafe impl Sync for PmpxPluginV1 {}
184
185/// Name of the single entry symbol.
186/// The symbol itself is defined inside the plugin by `pmpx_plugin::export!`, not here -- this
187/// crate gets linked into every plugin, and defining the same `#[no_mangle]` symbol itself would
188/// collide with the one `export!` generates. Its shape is
189/// `extern "C" fn() -> *const PmpxPluginV1`.
190pub const ENTRY_SYMBOL: &str = "pmpx_plugin_entry_v1";
191
192// ---- Build info (diagnostics) ----
193
194/// rustc version injected at compile time. `const fn` is deliberate: the `static` vtable that
195/// `export!` generates has to be evaluated at compile time.
196pub const fn build_rustc() -> PmpxStr {
197    PmpxStr::from_static(env!("PMPX_BUILD_RUSTC"))
198}
199
200/// Target triple injected at compile time.
201pub const fn build_target() -> PmpxStr {
202    PmpxStr::from_static(env!("PMPX_BUILD_TARGET"))
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use crate::Verb;
209
210    #[test]
211    fn verb_numbers_match_the_public_enum() {
212        // Once the numbering slips, the host and the plugin disagree about "install" and nothing
213        // fails to compile.
214        assert_eq!(Verb::Install.to_abi(), VERB_INSTALL);
215        assert_eq!(Verb::Remove.to_abi(), VERB_REMOVE);
216        assert_eq!(Verb::Run.to_abi(), VERB_RUN);
217        assert_eq!(Verb::Build.to_abi(), VERB_BUILD);
218        assert_eq!(Verb::Test.to_abi(), VERB_TEST);
219        assert_eq!(Verb::Update.to_abi(), VERB_UPDATE);
220        assert_eq!(Verb::Exec.to_abi(), VERB_EXEC);
221    }
222
223    #[test]
224    fn verb_round_trips() {
225        for v in Verb::ALL {
226            assert_eq!(Verb::from_abi(v.to_abi()), Some(*v));
227        }
228        assert_eq!(Verb::from_abi(99), None);
229    }
230
231    #[test]
232    fn build_info_is_populated() {
233        let rustc = build_rustc();
234        let target = build_target();
235        assert!(rustc.len > 0);
236        assert!(target.len > 0);
237
238        let rustc = unsafe { std::slice::from_raw_parts(rustc.ptr, rustc.len) };
239        let target = unsafe { std::slice::from_raw_parts(target.ptr, target.len) };
240        assert!(
241            std::str::from_utf8(rustc).unwrap().contains("rustc"),
242            "rustc_version should look like `rustc 1.x.y (...)`"
243        );
244        assert!(std::str::from_utf8(target).unwrap().contains('-'));
245    }
246}