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}