Skip to main content

pmpx_plugin/
abi.rs

1//! 跨 `dlopen` 边界的**线格式**。
2//!
3//! # 这个文件是硬边界
4//!
5//! 宿主与插件是两个独立编译的世界。凡是穿过这条线的数据,其**布局必须由 ABI 决定**,
6//! 而不能由编译器、依赖版本或 rustc 版本决定。
7//!
8//! 所以这里**只有 `#[repr(C)]` 的 POD 结构与普通整数**,没有:
9//!
10//! | 禁止出现在边界上的东西 | 为什么 |
11//! | ---------------------- | ------ |
12//! | `String` / `Vec` / `Box` | 它们的内存归谁、由哪个分配器释放,取决于两边是否同源 |
13//! | `toml::Value`、`BTreeMap` | 布局随依赖的小版本变化 |
14//! | `anyhow::Error` | 同上,而且是类型擦除的 |
15//! | trait object | fat pointer 的 vtable 归属在两边之间没有保证 |
16//! | `Future` / `async` | 同上,且需要一个两边都认的 runtime |
17//!
18//! 换来的东西很大:**宿主与插件不需要同一个 rustc**,也不需要共享分配器。
19//! 唯一需要对齐的就是本文件里的结构形状,而它由 [`ABI_VERSION`] 守着。
20//!
21//! # 内存所有权的两个方向
22//!
23//! 这是最容易搞混、也最容易出 UB 的地方:
24//!
25//! | 数据 | 谁分配 | 谁释放 | 用哪个 API |
26//! | ---- | ------ | ------ | ---------- |
27//! | `project_root` / `matched` / `args`(**输入**) | 宿主 | 宿主 | 插件**只读**,绝不 free |
28//! | `out`([`PmpxCommand`] 及其内部字符串,**输出**) | 插件 | 插件 | 宿主只读,[`free_command`] |
29//! | [`PmpxPluginV1::name`] / [`PmpxPluginV1::family`] 的返回值 | 插件 | 插件 | 宿主只读,[`free_str`] |
30//!
31//! **没有任何一处内存是由"分配方之外"释放的** —— 这就是为什么不需要共享分配器。
32//!
33//! 相应地,`free_*` 函数里**绝不能**用宿主的 `Box::from_raw` 去接插件给的内存,反之亦然。
34
35use std::ffi::{OsStr, OsString};
36use std::path::PathBuf;
37
38use crate::{CommandSpec, Context, PackageManager, Verb};
39
40// ---------------------------------------------------------------------------
41// 版本
42// ---------------------------------------------------------------------------
43
44/// 跨边界布局的版本。
45///
46/// **独立整数,与 crate 版本号彻底解耦**:改 README、加日志、修 bug 都不动它;
47/// 只有 [`PmpxPluginV1`] / [`PmpxCommand`] / [`PmpxStr`] 的形状、动词编号或错误码语义
48/// 真的变了才 `+1`。
49///
50/// 宿主用它做**唯一**的硬校验:不相等就拒绝加载。这一条直接消灭了
51/// 「改一句 README 就 bump 0.1.1,导致所有 prebuilt 拒绝加载」那类问题。
52pub const ABI_VERSION: u32 = 1;
53
54// ---------------------------------------------------------------------------
55// 错误码
56// ---------------------------------------------------------------------------
57
58/// 成功。
59pub const PMPX_OK: u32 = 0;
60
61/// 这个后端不支持该动词。
62///
63/// 宿主对它有**特殊处理**:`pmpx exec` 收到这个码会退化成裸透传(见 pmpx 设计文档 6.3),
64/// 其余动词则原样报错。所以这个码必须与 [`PMPX_ERR_INTERNAL`] 分开。
65pub const PMPX_ERR_UNSUPPORTED_VERB: u32 = 1;
66
67/// 输入不合法 —— 动词编号不认识、`out` 是空指针、`matched` 里有非 UTF-8。
68pub const PMPX_ERR_INVALID_ARGS: u32 = 2;
69
70/// 插件内部出错,或者它 panic 了。
71///
72/// panic 被 [`guard`] 捕获后就归到这里 —— 宿主只需要知道"它炸了",不需要知道细节;
73/// 细节在插件的 stderr 上。
74pub const PMPX_ERR_INTERNAL: u32 = 3;
75
76// ---------------------------------------------------------------------------
77// 动词编号
78// ---------------------------------------------------------------------------
79
80/// [`Verb::Install`] 的编号。
81pub const VERB_INSTALL: u32 = 0;
82/// [`Verb::Remove`] 的编号。
83pub const VERB_REMOVE: u32 = 1;
84/// [`Verb::Run`] 的编号。
85pub const VERB_RUN: u32 = 2;
86/// [`Verb::Build`] 的编号。
87pub const VERB_BUILD: u32 = 3;
88/// [`Verb::Test`] 的编号。
89pub const VERB_TEST: u32 = 4;
90/// [`Verb::Update`] 的编号。
91pub const VERB_UPDATE: u32 = 5;
92/// [`Verb::Exec`] 的编号。
93pub const VERB_EXEC: u32 = 6;
94
95// ---------------------------------------------------------------------------
96// 数据结构
97// ---------------------------------------------------------------------------
98
99/// 跨边界字符串:**指针 + 长度**,不要求 NUL 结尾。
100///
101/// # 这是字节,不是 `String`
102///
103/// `ptr`/`len` 描述的是一段**裸字节**。它可能是路径、可能是命令行参数,
104/// 所以在 Unix 上完全可以不是合法 UTF-8 —— 转换走 `std::os::unix::ffi::OsStrExt`,
105/// 无损。
106///
107/// 只有 [`PmpxStr`] 明确要求是文本时(例如 `matched` 里的文件名)才会做 UTF-8 校验,
108/// 校验失败返回 [`PMPX_ERR_INVALID_ARGS`] 而不是 UB。
109///
110/// # 为什么带长度而不是靠 NUL
111///
112/// `str::as_ptr()` 得到的指针**不保证**后面跟着 NUL。指望它等于把 UB 写进设计。
113/// 带长度是唯一诚实的做法,顺带还省掉一次 `strlen`。
114#[repr(C)]
115#[derive(Debug, Copy, Clone)]
116pub struct PmpxStr {
117    /// 起始地址。`len == 0` 时可以是空指针。
118    pub ptr: *const u8,
119    /// 字节长度。
120    pub len: usize,
121}
122
123// SAFETY: `PmpxStr` 是"一段只读字节 + 长度",跨线程共享它 unsafe 的地方只在于
124// `ptr` 指向的内存必须仍然有效。而这个结构体的两份来源都是明确的:
125//   - 宿主传进来的:在整个调用期间有效;
126//   - 插件产出的:指向插件泄漏出来的 `Box<[u8]>`,在 `free_str` 之前一直有效。
127// 两者都不会在共享期间被释放或写入。所以把它标成 `Sync` 是成立的。
128unsafe impl Sync for PmpxStr {}
129
130impl PmpxStr {
131    /// 空。`len == 0` 且指针为空 —— 在 `cwd` 里表示"没有覆盖"。
132    pub const EMPTY: PmpxStr = PmpxStr {
133        ptr: std::ptr::null(),
134        len: 0,
135    };
136
137    /// 从一段 `'static` 文本构造。
138    ///
139    /// `const` 是关键:`export!` 生成的 `static` vtable 要在编译期填好这两个字段。
140    pub const fn from_static(s: &'static str) -> Self {
141        Self {
142            ptr: s.as_ptr(),
143            len: s.len(),
144        }
145    }
146
147    /// 是不是空的。
148    pub const fn is_empty(&self) -> bool {
149        self.len == 0
150    }
151}
152
153/// 跨边界的命令描述。
154///
155/// 由插件填充、由插件释放([`free_command`]);宿主只读。
156#[repr(C)]
157#[derive(Debug, Copy, Clone)]
158pub struct PmpxCommand {
159    /// 可执行文件。
160    pub program: PmpxStr,
161    /// 参数数组,元素个数为 `args_len`。
162    pub args: *const PmpxStr,
163    /// `args` 的元素个数。
164    pub args_len: usize,
165    /// 工作目录覆盖。`len == 0` 表示用宿主给的项目根。
166    pub cwd: PmpxStr,
167}
168
169// SAFETY: 同 `PmpxStr` —— 这个结构体只是"若干只读字节的引用 + 一个数组长度"。
170unsafe impl Sync for PmpxCommand {}
171
172/// 插件导出的**唯一**结构。
173///
174/// 宿主取到它之后,所有交互都通过这里的函数指针进行 —— 没有 trait object,
175/// 没有 vtable 互转,也就没有那类 UB。
176///
177/// # 为什么 `command` 的参数这么多
178///
179/// 每一个都是必要的:项目根、命中文件列表、动词、参数、输出。把它们打包成一个
180/// 结构体再传指针当然也行,但那样就要再定义、再版本化一个结构体;参数列表是 ABI 里
181/// 描述得最清楚的形式。
182#[repr(C)]
183pub struct PmpxPluginV1 {
184    /// 必须等于 [`ABI_VERSION`]。宿主第一件事就是比对这个字段。
185    pub abi_version: u32,
186
187    /// 编出这个插件的 rustc 版本,由 `pmpx-plugin` 的 build.rs 注入。
188    ///
189    /// **只用于诊断展示,不做硬校验。** 在这套 C ABI 下,不同 rustc 编出来的插件是
190    /// 可以安全加载的(见模块文档),硬校验只会误杀那些本来能用的组合。
191    /// 想知道值就 `pmpx plugin info` 看一眼。
192    pub rustc_version: PmpxStr,
193
194    /// 编出这个插件的 target triple。同样只做诊断。
195    pub target: PmpxStr,
196
197    /// 插件名。内存归插件,宿主读完用 [`free_str`] 释放。
198    ///
199    /// 宿主应当拿它与 manifest 里声明的名字比对 —— 不一致说明装错了东西。
200    pub name: unsafe extern "C" fn() -> PmpxStr,
201
202    /// 生态分组。内存归插件,宿主读完用 [`free_str`] 释放。
203    pub family: unsafe extern "C" fn() -> PmpxStr,
204
205    /// 把「动词 + 参数」翻译成一条命令。
206    ///
207    /// 返回 [`PMPX_OK`] 时 `out` 被填充,宿主用完调 [`free_command`];
208    /// 否则返回 `PMPX_ERR_*`,`out` 不动。
209    ///
210    /// # Safety
211    ///
212    /// - `project_root` / `matched` / `args` 必须由宿主分配、在调用期间有效且只读;
213    /// - `out` 必须指向一块可写的 [`PmpxCommand`];
214    /// - **panic 不许穿过这个边界** —— 由 `export!` 宏统一包 `catch_unwind`。
215    ///   从 Rust 1.81 起,让 panic 越过 `extern "C"` 会直接 abort,宿主侧的
216    ///   `catch_unwind` 救不了。
217    pub command: unsafe extern "C" fn(
218        project_root: PmpxStr,
219        matched: *const PmpxStr,
220        matched_len: usize,
221        verb: u32,
222        args: *const PmpxStr,
223        args_len: usize,
224        out: *mut PmpxCommand,
225    ) -> u32,
226
227    /// 释放 [`PmpxPluginV1::name`] / [`PmpxPluginV1::family`] 返回值占的内存。
228    ///
229    /// # Safety
230    ///
231    /// `s` 必须来自**同一个插件**的产出,且只能释放一次。
232    pub free_str: unsafe extern "C" fn(PmpxStr),
233
234    /// 释放 [`PmpxPluginV1::command`] 填充的 [`PmpxCommand`] 占的内存。
235    ///
236    /// 只释放结构体**指向**的内存,不释放结构体本身 —— 那个是宿主的。
237    ///
238    /// # Safety
239    ///
240    /// `c` 必须来自**同一个插件**的一次成功 `command` 调用,且只能释放一次。
241    pub free_command: unsafe extern "C" fn(*mut PmpxCommand),
242}
243
244// SAFETY: 这个结构体是编译期常量填出来的只读表:几个整数、两段 `'static` 字节、
245// 五个函数指针。填好之后从不修改。函数指针本身是 `Sync` 的。
246unsafe impl Sync for PmpxPluginV1 {}
247
248/// 唯一入口符号的名字。
249///
250/// ⚠️ **这个符号由 `pmpx_plugin::export!` 在插件里定义,不在这里。**
251/// `pmpx-plugin` 是个库,会被链接进每一个插件;如果它自己定义了同名 `#[no_mangle]`
252/// 符号,就会和 `export!` 生成的那个撞车(`#[no_mangle]` 不允许重复)。
253///
254/// 所以这个 crate 只提供名字与形状的约定,定义权在插件侧:
255///
256/// ```text
257/// #[no_mangle]
258/// pub extern "C" fn pmpx_plugin_entry_v1() -> *const PmpxPluginV1;
259/// ```
260///
261/// 名字里的 `v1` 与 [`ABI_VERSION`] 对应:将来若真的要引入形状完全不同的 v2,
262/// 就再加一个 `pmpx_plugin_entry_v2`,让 v1 插件继续能装。
263pub const ENTRY_SYMBOL: &str = "pmpx_plugin_entry_v1";
264
265// ---------------------------------------------------------------------------
266// 编出这个 crate 的那个人是谁(诊断用)
267// ---------------------------------------------------------------------------
268
269/// 编译期注入的 rustc 版本。
270///
271/// `const fn` 是刻意的:`export!` 生成的 `static` vtable 要在编译期求值。
272pub const fn build_rustc() -> PmpxStr {
273    PmpxStr::from_static(env!("PMPX_BUILD_RUSTC"))
274}
275
276/// 编译期注入的 target triple。
277pub const fn build_target() -> PmpxStr {
278    PmpxStr::from_static(env!("PMPX_BUILD_TARGET"))
279}
280
281// ---------------------------------------------------------------------------
282// 内存:分配与释放
283// ---------------------------------------------------------------------------
284
285/// 把一段字节泄漏成 [`PmpxStr`],交给边界对面去读。
286///
287/// # 统一的分配类型是 `Box<[u8]>`
288///
289/// 所有从插件流出的字符串 —— `name()`、`family()`、`program`、每个 `arg`、`cwd` ——
290/// **都用同一种分配**,这样 [`free_str`] 只有一条路径,不会出现"按 `Box<str>` 释放
291/// `Box<[u8]>`"这种 UB。
292pub fn leak_bytes(bytes: &[u8]) -> PmpxStr {
293    let boxed: Box<[u8]> = bytes.to_vec().into_boxed_slice();
294    let out = PmpxStr {
295        ptr: boxed.as_ptr(),
296        len: boxed.len(),
297    };
298    // 所有权交给边界对面,由 free_str 取回。
299    std::mem::forget(boxed);
300    out
301}
302
303/// [`leak_bytes`] 的 `&str` 版本。
304pub fn leak_str(s: &str) -> PmpxStr {
305    leak_bytes(s.as_bytes())
306}
307
308/// 释放一个由本侧 [`leak_bytes`] / [`leak_str`] 产出的 [`PmpxStr`]。
309///
310/// # Safety
311///
312/// - `s` 必须来自**本侧**的 `leak_*`,不能是宿主传来的输入;
313/// - 只能释放一次。
314///
315/// 空指针被当成"没有东西"直接返回([`PmpxStr::EMPTY`] 就是这么用的);
316/// 长度为 0 但指针非空的情况是合法分配,会正常走 `Box::from_raw`。
317pub unsafe fn free_str(s: PmpxStr) {
318    if s.ptr.is_null() {
319        return;
320    }
321    let raw = std::ptr::slice_from_raw_parts_mut(s.ptr as *mut u8, s.len);
322    // 与 leak_bytes 里的 Box<[u8]> 严格配对。
323    drop(unsafe { Box::from_raw(raw) });
324}
325
326/// 释放一个由本侧 [`write_command`] 填充的 [`PmpxCommand`] 的内容。
327///
328/// **不释放 `c` 本身** —— 那个结构体在宿主那边(通常是栈上)。
329///
330/// # Safety
331///
332/// `c` 必须来自本侧一次成功的 `command` 调用,且只能释放一次。
333pub unsafe fn free_command(c: *mut PmpxCommand) {
334    if c.is_null() {
335        return;
336    }
337    // 只读地看它一眼,然后把里面的东西逐个还回去。
338    let cmd = unsafe { &*c };
339
340    unsafe { free_str(cmd.program) };
341    unsafe { free_str(cmd.cwd) };
342
343    if !cmd.args.is_null() && cmd.args_len > 0 {
344        // 与 write_command 里的 Box<[PmpxStr]> 严格配对。
345        let raw = std::ptr::slice_from_raw_parts_mut(cmd.args as *mut PmpxStr, cmd.args_len);
346        let args = unsafe { Box::from_raw(raw) };
347        for s in args.iter() {
348            unsafe { free_str(*s) };
349        }
350        // args 在这里 drop,数组本身的内存随之归还。
351    }
352}
353
354// ---------------------------------------------------------------------------
355// 输入方向:字节 ↔ OsString
356// ---------------------------------------------------------------------------
357
358/// 把宿主传来的字节读成 `OsString`。
359///
360/// # Safety
361///
362/// `s` 必须描述一段在本次调用期间有效的只读内存,或 `len == 0`。
363///
364/// # 为什么是 `OsString` 而不是 `String`
365///
366/// Unix 上路径与命令行参数**可以不是合法 UTF-8**。用 `String` 就只能有损转换,
367/// 而 `pmpx exec some-tool /latin1/path` 这种调用会被悄悄改坏参数。
368/// `OsString` 在 Unix 上是无损的裸字节。
369///
370/// Windows 上 `OsString` 底层是 WTF-8,非 UTF-8(未配对代理项)会退化成有损替换 ——
371/// 那是 Windows 平台的边界,不是这里的取舍。
372pub unsafe fn read_os(s: PmpxStr) -> OsString {
373    if s.len == 0 {
374        return OsString::new();
375    }
376    let bytes = unsafe { std::slice::from_raw_parts(s.ptr, s.len) };
377    bytes_to_os(bytes)
378}
379
380/// 把宿主传来的字节读成 `&str`,**校验 UTF-8**。
381///
382/// 校验失败返回 [`PMPX_ERR_INVALID_ARGS`],绝不 `from_utf8_unchecked` ——
383/// 那等于假定宿主永远正确,而 ABI 的职责恰恰是不做这种假定。
384///
385/// # Safety
386///
387/// 同 [`read_os`]。
388pub unsafe fn read_str<'a>(s: PmpxStr) -> Result<&'a str, u32> {
389    if s.len == 0 {
390        return Ok("");
391    }
392    let bytes = unsafe { std::slice::from_raw_parts(s.ptr, s.len) };
393    std::str::from_utf8(bytes).map_err(|_| PMPX_ERR_INVALID_ARGS)
394}
395
396/// 把裸字节转成 `OsString`。
397///
398/// **宿主侧也需要这个**(它得把 `project_root` 与 `args` 变成字节送过边界),
399/// 所以它是公开的 —— 让两边各写一份平台 cfg 是必然漂移的那种重复。
400///
401/// - Unix:无损(`OsString` 底层就是裸字节);
402/// - 其它平台:`OsString` 底层是 WTF-8,非 UTF-8 会退化成 U+FFFD。
403#[cfg(unix)]
404pub fn bytes_to_os(bytes: &[u8]) -> OsString {
405    use std::os::unix::ffi::OsStringExt;
406    OsString::from_vec(bytes.to_vec())
407}
408
409/// 见 [`bytes_to_os`] 的平台说明。
410#[cfg(not(unix))]
411pub fn bytes_to_os(bytes: &[u8]) -> OsString {
412    String::from_utf8_lossy(bytes).into_owned().into()
413}
414
415/// 把 `OsStr` 转成裸字节。与 [`bytes_to_os`] 严格配对。
416///
417/// - Unix:无损;
418/// - 其它平台:经 `to_string_lossy`,非 UTF-8 会退化成 U+FFFD。
419#[cfg(unix)]
420pub fn os_to_bytes(s: &OsStr) -> Vec<u8> {
421    use std::os::unix::ffi::OsStrExt;
422    s.as_bytes().to_vec()
423}
424
425/// 见 [`os_to_bytes`] 的平台说明。
426#[cfg(not(unix))]
427pub fn os_to_bytes(s: &OsStr) -> Vec<u8> {
428    s.to_string_lossy().into_owned().into_bytes()
429}
430
431// ---------------------------------------------------------------------------
432// 输出方向
433// ---------------------------------------------------------------------------
434
435/// 把一个 [`CommandSpec`] 写成跨边界的形式,**内存由本侧分配**。
436///
437/// # Safety
438///
439/// `out` 必须指向一块可写的 [`PmpxCommand`]。
440pub unsafe fn write_command(out: *mut PmpxCommand, spec: CommandSpec) {
441    let program = leak_bytes(&os_to_bytes(&spec.program));
442
443    let args: Vec<PmpxStr> = spec
444        .args
445        .iter()
446        .map(|a| leak_bytes(&os_to_bytes(a)))
447        .collect();
448    let args_boxed: Box<[PmpxStr]> = args.into_boxed_slice();
449    let args_len = args_boxed.len();
450    let args_ptr = args_boxed.as_ptr();
451    std::mem::forget(args_boxed);
452
453    let cwd = match &spec.cwd {
454        Some(p) => leak_bytes(&os_to_bytes(p.as_os_str())),
455        None => PmpxStr::EMPTY,
456    };
457
458    unsafe {
459        *out = PmpxCommand {
460            program,
461            args: args_ptr,
462            args_len,
463            cwd,
464        };
465    }
466}
467
468// ---------------------------------------------------------------------------
469// 调度
470// ---------------------------------------------------------------------------
471
472/// 一次 `command` 调用的全部接线:读输入 → 调 [`crate::PackageManager::command`] → 写输出。
473///
474/// **这段逻辑住在这个 crate 里,而不是在 `export!` 宏里**,是为了它能被直接测试 ——
475/// 宏里生成的代码只能通过真的加载一个 cdylib 来覆盖。
476///
477/// # Safety
478///
479/// 见 [`PmpxPluginV1::command`] 的 Safety 段。此外 `plugin` 必须是本进程里有效的实例。
480// 参数就是 ABI 签名的那八个,一个不多一个不少 —— 这里的"参数过多"正是被镜像的东西本身。
481// 把它们打包成结构体反而要多定义、多版本化一个类型,得不偿失。
482#[allow(clippy::too_many_arguments)]
483pub unsafe fn dispatch_command(
484    plugin: &dyn PackageManager,
485    project_root: PmpxStr,
486    matched: *const PmpxStr,
487    matched_len: usize,
488    verb: u32,
489    args: *const PmpxStr,
490    args_len: usize,
491    out: *mut PmpxCommand,
492) -> u32 {
493    if out.is_null() {
494        return PMPX_ERR_INVALID_ARGS;
495    }
496
497    let Some(verb) = Verb::from_abi(verb) else {
498        return PMPX_ERR_INVALID_ARGS;
499    };
500
501    let project_root = PathBuf::from(unsafe { read_os(project_root) });
502
503    // `matched` 是**文本**(manifest 里声明的文件名),所以这里要校验 UTF-8。
504    let mut matched_names = Vec::with_capacity(matched_len);
505    for i in 0..matched_len {
506        let raw = unsafe { *matched.add(i) };
507        match unsafe { read_str(raw) } {
508            Ok(s) => matched_names.push(s.to_string()),
509            Err(code) => return code,
510        }
511    }
512
513    // `args` 是**参数**,可以是任意字节 —— 原样转成 OsString,无损。
514    let mut arg_list = Vec::with_capacity(args_len);
515    for i in 0..args_len {
516        let raw = unsafe { *args.add(i) };
517        arg_list.push(unsafe { read_os(raw) });
518    }
519
520    let ctx = Context {
521        project_root,
522        matched: matched_names,
523    };
524
525    match plugin.command(&ctx, verb, &arg_list) {
526        Ok(spec) => {
527            unsafe { write_command(out, spec) };
528            PMPX_OK
529        }
530        Err(e) => e.code(),
531    }
532}
533
534/// 把一次跨边界调用包进 `catch_unwind`。
535///
536/// # 为什么这一层不能省
537///
538/// 插件是 `dlopen` 进来的代码。它 panic 一次,宿主整个进程就没了。而从 Rust 1.81 起,
539/// **让 panic 越过 `extern "C"` 边界会直接 abort** —— 也就是说宿主那边的 `catch_unwind`
540/// 完全救不了。所以**插件必须在自己的 `extern "C` 函数里**把它兜住,这正是这里做的事。
541///
542/// 宿主侧仍然会再包一层(那份代码在 pmpx 里),但那是为了兜住"插件忘了包"
543/// 或"插件用 `panic=abort` 编的"这类情况,不是第一道防线。
544pub fn guard(f: impl FnOnce() -> u32) -> u32 {
545    // `AssertUnwindSafe`:我们只承诺"panic 不要掀翻进程",
546    // 拿到 PMPX_ERR_INTERNAL 之后调用方就会中止这次操作,不会继续碰捕获现场。
547    std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).unwrap_or(PMPX_ERR_INTERNAL)
548}
549
550#[cfg(test)]
551mod tests {
552    use super::*;
553
554    #[test]
555    fn verb_numbers_match_the_public_enum() {
556        // 这组编号一旦错位,宿主与插件对"install"的理解就会分叉 ——
557        // 而且不会有任何编译错误。所以用测试钉住。
558        assert_eq!(Verb::Install.to_abi(), VERB_INSTALL);
559        assert_eq!(Verb::Remove.to_abi(), VERB_REMOVE);
560        assert_eq!(Verb::Run.to_abi(), VERB_RUN);
561        assert_eq!(Verb::Build.to_abi(), VERB_BUILD);
562        assert_eq!(Verb::Test.to_abi(), VERB_TEST);
563        assert_eq!(Verb::Update.to_abi(), VERB_UPDATE);
564        assert_eq!(Verb::Exec.to_abi(), VERB_EXEC);
565    }
566
567    #[test]
568    fn verb_round_trips() {
569        for v in Verb::ALL {
570            assert_eq!(Verb::from_abi(v.to_abi()), Some(*v));
571        }
572        assert_eq!(Verb::from_abi(99), None);
573    }
574
575    #[test]
576    fn empty_str_reads_as_empty() {
577        assert_eq!(unsafe { read_os(PmpxStr::EMPTY) }, OsString::new());
578        assert_eq!(unsafe { read_str(PmpxStr::EMPTY) }.unwrap(), "");
579    }
580
581    #[test]
582    fn leak_and_free_round_trip() {
583        let s = leak_str("hello");
584        assert_eq!(s.len, 5);
585        assert_eq!(
586            unsafe { std::slice::from_raw_parts(s.ptr, s.len) },
587            b"hello"
588        );
589        unsafe { free_str(s) };
590    }
591
592    #[test]
593    fn free_str_tolerates_null() {
594        // EMPTY 会被 free_command 无条件传给 free_str
595        unsafe { free_str(PmpxStr::EMPTY) };
596    }
597
598    #[test]
599    fn leak_and_free_an_empty_string() {
600        // 空串的 Box<[u8]> 是一个悬垂指针(非空、len 0),必须能正常释放
601        let s = leak_str("");
602        assert_eq!(s.len, 0);
603        assert!(!s.ptr.is_null(), "空 Box 的指针是悬垂但非空的");
604        unsafe { free_str(s) };
605    }
606
607    #[test]
608    fn read_str_rejects_invalid_utf8() {
609        let bytes = [0xff, 0xfe];
610        let s = PmpxStr {
611            ptr: bytes.as_ptr(),
612            len: bytes.len(),
613        };
614        assert_eq!(unsafe { read_str(s) }, Err(PMPX_ERR_INVALID_ARGS));
615    }
616
617    /// 正常 UTF-8 在两个平台上都必须无损往返。
618    #[test]
619    fn read_os_round_trips_valid_utf8() {
620        let bytes = "/tmp/项目/ünïcode".as_bytes();
621        let s = PmpxStr {
622            ptr: bytes.as_ptr(),
623            len: bytes.len(),
624        };
625        let got = unsafe { read_os(s) };
626        assert_eq!(os_to_bytes(&got), bytes);
627    }
628
629    /// Unix 上路径与参数可以是**任意字节**,`OsString` 必须无损保住它们。
630    ///
631    /// 这正是 `args` 用 `&[OsString]` 而不是 `&[String]` 的原因:后者只能有损转换,
632    /// `pmpx exec some-tool /latin1/path` 会被悄悄改坏参数。
633    #[cfg(unix)]
634    #[test]
635    fn read_os_keeps_arbitrary_bytes_on_unix() {
636        // 0xFF 不是合法 UTF-8 起始字节,但它是个合法的路径字节
637        let bytes = [0x2f, 0x62, 0x61, 0x64, 0xff];
638        let s = PmpxStr {
639            ptr: bytes.as_ptr(),
640            len: bytes.len(),
641        };
642        let got = unsafe { read_os(s) };
643        assert_eq!(os_to_bytes(&got), bytes, "Unix 上必须无损");
644    }
645
646    /// Windows 上 `OsString` 底层是 WTF-8,非 UTF-8 会退化成 U+FFFD。
647    ///
648    /// 这是**平台边界**,不是这里的取舍:Win32 的路径本来就是 UTF-16,
649    /// 拿不到合法 UTF-8 的情况在实践中基本不存在。测试把它钉成"已知行为"而不是假装无事。
650    #[cfg(not(unix))]
651    #[test]
652    fn read_os_replaces_invalid_utf8_off_unix() {
653        let bytes = [0x2f, 0x62, 0xff];
654        let s = PmpxStr {
655            ptr: bytes.as_ptr(),
656            len: bytes.len(),
657        };
658        let got = unsafe { read_os(s) };
659        let expected = String::from_utf8_lossy(&bytes).into_owned().into_bytes();
660        assert_eq!(os_to_bytes(&got), expected);
661        assert_ne!(os_to_bytes(&got), bytes, "非 Unix 上确实是有损的");
662    }
663
664    #[test]
665    fn writes_and_frees_a_command() {
666        let spec = CommandSpec::new("cargo")
667            .arg("add")
668            .arg("serde")
669            .cwd("/tmp/project");
670
671        let mut out = std::mem::MaybeUninit::<PmpxCommand>::uninit();
672        unsafe { write_command(out.as_mut_ptr(), spec) };
673        let mut cmd = unsafe { out.assume_init() };
674
675        assert_eq!(cmd.args_len, 2);
676        let program = unsafe { std::slice::from_raw_parts(cmd.program.ptr, cmd.program.len) };
677        assert_eq!(program, b"cargo");
678
679        let arg0 = unsafe { *cmd.args.add(0) };
680        let a0 = unsafe { std::slice::from_raw_parts(arg0.ptr, arg0.len) };
681        assert_eq!(a0, b"add");
682
683        let cwd = unsafe { std::slice::from_raw_parts(cmd.cwd.ptr, cmd.cwd.len) };
684        assert_eq!(cwd, b"/tmp/project");
685
686        unsafe { free_command(&mut cmd as *mut _) };
687    }
688
689    #[test]
690    fn writes_a_command_with_no_args_and_no_cwd() {
691        let spec = CommandSpec::new("cargo");
692
693        let mut out = std::mem::MaybeUninit::<PmpxCommand>::uninit();
694        unsafe { write_command(out.as_mut_ptr(), spec) };
695        let mut cmd = unsafe { out.assume_init() };
696
697        assert_eq!(cmd.args_len, 0);
698        assert!(cmd.cwd.is_empty(), "没有 cwd 时应当是 EMPTY");
699
700        unsafe { free_command(&mut cmd as *mut _) };
701    }
702
703    #[test]
704    fn free_command_tolerates_null() {
705        unsafe { free_command(std::ptr::null_mut()) };
706    }
707
708    #[test]
709    fn guard_turns_a_panic_into_internal_error() {
710        assert_eq!(guard(|| PMPX_OK), PMPX_OK);
711        assert_eq!(guard(|| panic!("插件炸了")), PMPX_ERR_INTERNAL);
712    }
713
714    #[test]
715    fn build_info_is_populated() {
716        // 这两个值由 build.rs 注入,真实构建里不该是 "unknown"
717        let rustc = build_rustc();
718        let target = build_target();
719        assert!(rustc.len > 0);
720        assert!(target.len > 0);
721
722        let rustc = unsafe { std::slice::from_raw_parts(rustc.ptr, rustc.len) };
723        let target = unsafe { std::slice::from_raw_parts(target.ptr, target.len) };
724        assert!(
725            std::str::from_utf8(rustc).unwrap().contains("rustc"),
726            "rustc_version 应当是 `rustc 1.x.y (...)` 这种形式"
727        );
728        assert!(std::str::from_utf8(target).unwrap().contains('-'));
729    }
730}