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日 |
#1273 在 Rust 模式
1,592 每月下载量
用于 4 个crate(2 直接)
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_buf
和 sigjmp_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 函数很简单:你只需声明它。为什么要费心制作一个特殊的包?
- 如本文档所述,记录许多问题和注意事项。
- 深入研究问题空间,以便 Rust 语言团队可能觉得定义行为(至少在某些狭窄情况下)是舒适的。
- 提供测试以查看是否有明显的问题。
- 处理一些平台问题
jmp_buf
和sigjmp_buf
类型并非微不足道,最好使用 bindgen 在系统的<setjmp.h>
头文件中定义。- 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 的额外挑战。
- 这些函数的行为是用 C 定义的,因此任何应用到 Rust 的应用都是通过类比(直到 Rust 定义行为)。
- Rust 有析构函数,而 C 没有。任何
longjmp()
都必须小心,不要跳过拥有引用到具有析构函数的变量的任何栈帧。 - Rust 没有函数多次返回的概念,如
fork()
或setjmp()
,因此可以想象 Rust 可能会在这样的函数周围生成不正确的代码。 - Rust 在编译时使用 LLVM,这需要通过使用
returns_twice
属性来让 LLVM 知道函数会多次返回;但是 Rust 没有方法将此属性传播到 LLVM。如果没有此属性,LLVM 本身可能生成不正确的代码(参见此评论)。 - 跳转可以中断良好的括号化控制流,绕过有关已运行代码的保证。
- 跳转可以返回到值被移动之前的位置,从而允许使用后释放的漏洞。
- 跳转释放变量而不销毁它们(这不仅仅是泄露它们)。
替代方案
鉴于这些问题,您应该认真考虑替代方案。
一个替代方案是在从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