5 个版本

0.1.4 2020年5月13日
0.1.3 2020年5月12日
0.1.2 2020年5月11日
0.1.1 2020年5月11日
0.1.0 2020年5月10日

#1273Rust 模式

Download history 550/week @ 2024-03-13 489/week @ 2024-03-20 520/week @ 2024-03-27 506/week @ 2024-04-03 518/week @ 2024-04-10 616/week @ 2024-04-17 488/week @ 2024-04-24 229/week @ 2024-05-01 218/week @ 2024-05-08 252/week @ 2024-05-15 448/week @ 2024-05-22 223/week @ 2024-05-29 281/week @ 2024-06-05 426/week @ 2024-06-12 560/week @ 2024-06-19 303/week @ 2024-06-26

1,592 每月下载量
用于 4 个crate(2 直接)

PostgreSQL OR MIT

15KB
168

setjmp / longjmp

警告:此crate为实验性质,即使是谨慎使用也可能产生未定义的行为。

此crate公开了四个C标准库函数给Rust

pub fn setjmp(env: *mut jmp_buf) -> c_int;
pub fn sigsetjmp(env: *mut sigjmp_buf, savesigs: c_int) -> c_int;
pub fn longjmp(env: *mut jmp_buf, val: c_int) -> c_void;
pub fn siglongjmp(env: *mut sigjmp_buf, val: c_int) -> c_void;

以及使用它们所需的 jmp_bufsigjmp_buf 类型。

有关详细信息和建议,请参阅 setjmp(3)

另请参阅 RFC #2625

动机

为了更好地与可能使用 setjmp()/longjmp() 的C代码交互。

  • 如果C代码调用Rust代码,而Rust代码又调用C代码,并且发生了 longjmp(),您可能希望Rust代码捕获该 longjmp(),将其转换为panic(以安全地回滚),然后catch_unwind(),然后再将其转换回 longjmp() 以返回到C代码中的某个地方(最后调用 setjmp() 的地方)。
  • 如果Rust代码调用C代码,Rust代码可能想要捕获C代码中的 longjmp() 并对其进行某种处理。
  • Rust代码可能希望 longjmp() 将控制权返回给C代码。

在 Rust 中可以使用 setjmp()/longjmp() 来管理控制流(无需与 C 交互),但这非常危险,并且没有明确的使用场景。

为什么需要“setjmp”包?

通常,从 Rust 中使用 C 函数很简单:你只需声明它。为什么要费心制作一个特殊的包?

  1. 如本文档所述,记录许多问题和注意事项。
  2. 深入研究问题空间,以便 Rust 语言团队可能觉得定义行为(至少在某些狭窄情况下)是舒适的。
  3. 提供测试以查看是否有明显的问题。
  4. 处理一些平台问题
    1. jmp_bufsigjmp_buf 类型并非微不足道,最好使用 bindgen 在系统的 <setjmp.h> 头文件中定义。
    2. libc 实现通常使用宏来更改实际引用的符号;而且这在不同的平台上有所不同。例如,实际 libc 符号可能是 __sigsetjmp,并且可能有一个宏将 sigsetjmp() 调用重写为 __sigsetjmp()

用法

setjmp 的调用只能在以下上下文中出现(参见评论)

  • match 的整个控制表达式,例如 match setjmp(env) { ... }
  • if setjmp(env) $integer_relational_operator $integer_constant_expression { ... }
  • 表达式语句的整个表达式: setjmp(env);

请参阅测试示例。

问题

除了在 C 中使用 setjmp/longjmp 的许多挑战之外,还有使用 Rust 的额外挑战。

  1. 这些函数的行为是用 C 定义的,因此任何应用到 Rust 的应用都是通过类比(直到 Rust 定义行为)。
  2. Rust 有析构函数,而 C 没有。任何 longjmp() 都必须小心,不要跳过拥有引用到具有析构函数的变量的任何栈帧。
  3. Rust 没有函数多次返回的概念,如 fork()setjmp(),因此可以想象 Rust 可能会在这样的函数周围生成不正确的代码。
  4. Rust 在编译时使用 LLVM,这需要通过使用 returns_twice 属性来让 LLVM 知道函数会多次返回;但是 Rust 没有方法将此属性传播到 LLVM。如果没有此属性,LLVM 本身可能生成不正确的代码(参见评论)。
  5. 跳转可以中断良好的括号化控制流,绕过有关已运行代码的保证。
  6. 跳转可以返回到值被移动之前的位置,从而允许使用后释放的漏洞。
  7. 跳转释放变量而不销毁它们(这不仅仅是泄露它们)。

替代方案

鉴于这些问题,您应该认真考虑替代方案。

一个替代方案是在从C进入Rust堆栈帧或从Rust堆栈帧进入C时使用C包装器。如果需要,包装器可以将Rust的特殊返回值转换为C的 longjmp(),或者捕获C的 longjmp()并将其转换为Rust的 panic!()。然而,这并不总是实用的,因此有时从Rust调用 setjmp()/longjmp()仍然是最佳解决方案。

建议

  • 使用 #[inline(never)] 标记调用 setjmp() 的任何函数,以减少误优化的可能性。
  • setjmp() 返回 0 和可能的 longjmp() 之间的代码应尽可能简单。通常,这可能是保存/设置全局变量和调用C FFI函数(这可能会导致 longjmp())。应避免使用此代码在堆上分配内存、使用实现 Drop 特性的类型或可能导致误优化的复杂代码。
  • longjmp() 或任何父堆栈帧之前的代码也应尽可能简单。通常,这可能是从被调用者检索返回值或捕获恐慌(使用 catch_unwind())所需的最少代码。应避免使用此代码在堆上分配内存、使用实现 Drop 特性的类型或可能导致误优化的复杂代码。

依赖

~0–1.8MB
~35K SLoC