sjlj2/lib.rs
1//! # Safer[^1], cheaper and more ergonomic setjmp/longjmp in assembly.
2//!
3//! [^1]: [`long_jump`] is still unsafe and is technically UB, though.
4//!       See more about safety in [`long_jump`].
5//!
6//! - Ergonomic and safer Rusty API for typical usages. Closure API instead of multiple-return.
7//!
8//!   Multiple-return functions are undefined behaviors due to
9//!   [fatal interaction with optimizer][misopt].
10//!   This crate does not suffer from the misoptimization (covered in `tests/smoke.rs`).
11//!   If you find any misoptimization in practice, please open an issue.
12//!
13//!   See [`long_jump`] for details.
14//!
15//! - Single-use jump checkpoint.
16//!
17//!   No jump-after-jump disaster. No coroutine-at-home.
18//!
19//! - Minimal memory and performance footprint.
20//!
21//!   Single `usize` `JumpPoint`. Let optimizer save only necessary states rather than bulk saving
22//!   all callee-saved registers. Inline-able `long_jump`.
23//!
24//!   On a modern x86\_64 CPU:
25//!   - 2.7ns `catch_long_jump` without `long_jump`.
26//!   - 3.4ns `catch_long_jump`+`long_jump`.
27//!     ~416x faster than `catch_unwind`-`panic_any`.
28//!
29//! - No libc or C compiler dependency.
30//!
31//!   The low-level implementation is written in inline assembly.
32//!
33//! - `no_std` support.
34//!
35//!   By default, this crate is `#![no_std]` and does not use `alloc` either.
36//!   It is suitable for embedded environment.
37//!
38//! ```
39//! use std::ops::ControlFlow;
40//! use sjlj2::catch_long_jump;
41//!
42//! let mut a = 42;
43//! // Execute with a jump checkpoint. Both closures can return a value.
44//! let ret = catch_long_jump(|jump_point| {
45//!     a = 13;
46//!     // Jump back to the alternative path with an arbitrary `usize` payload.
47//!     // SAFETY: All frames between `catch_long_jump` and the current are POFs.
48//!     unsafe {
49//!         jump_point.long_jump(99);
50//!     }
51//! });
52//! assert_eq!(ret, ControlFlow::Break(99));
53//! ```
54//!
55//! ## Cargo features
56//!
57//! - `unwind`: Enables unwinding across [`catch_long_jump`] boundary, by
58//!   catching and resuming the panic payload. This feature requires `std`.
59//!
60//! No feature is enabled by default.
61//!
62//! ## Supported architectures
63//!
64//! - x86 (i686)
65//! - x86\_64
66//! - riscv64
67//! - riscv32, with or without E-extension
68//! - aarch64 (ARM v8)
69//! - arm
70//!
71//! ## Similar crates
72//!
73//! - [`setjmp`](https://crates.io/crates/setjmp)
74//!
75//!   - Generates from C thus needs a correctly-setup C compiler to build.
76//!   - Unknown performance because it fails to build for me. (Poor compatibility?)
77//!   - Suffers from [misoptimization][misopt].
78//!
79//! - [`sjlj`](https://crates.io/crates/sjlj)
80//!
81//!   - Only x86\_64 is supported.
82//!   - Suffers from [misoptimization][misopt] due to multi-return.
83//!   - Slower `long_jump` because of more register restoring.
84//!
85//! [misopt]: https://github.com/rust-lang/rfcs/issues/2625
86#![cfg_attr(not(any(test, feature = "unwind")), no_std)]
87use core::marker::PhantomData;
88use core::mem::{ManuallyDrop, MaybeUninit};
89use core::ops::ControlFlow;
90
91// Overridable by the next definition, and can be unused on some targets.
92#[allow(unused_macros)]
93macro_rules! maybe_strip_cfi {
94    (($($head:tt)*), $($lit1:literal,)* $([$cfi:literal], $($lit2:literal,)*)* [], $($tail:tt)*) => {
95        $($head)* (
96            $($lit1,)*
97            $($cfi, $($lit2,)*)*
98            $($tail)*
99        )
100    };
101}
102
103// Windows do not use DWARF unwind info.
104#[cfg(any(windows, panic = "abort"))]
105macro_rules! maybe_strip_cfi {
106    (($($head:tt)*), $($lit1:literal,)* $([$cfi:literal], $($lit2:literal,)*)* [], $($tail:tt)*) => {
107        $($head)* (
108            $($lit1,)*
109            $($($lit2,)*)*
110            $($tail)*
111        )
112    };
113}
114
115#[cfg(target_arch = "x86_64")]
116#[macro_use]
117#[path = "./x86_64.rs"]
118mod imp;
119
120#[cfg(all(target_arch = "x86", not(target_env = "msvc")))]
121#[macro_use]
122#[path = "./x86.rs"]
123mod imp;
124
125#[cfg(all(target_arch = "x86", target_env = "msvc"))]
126#[macro_use]
127#[path = "./x86_msvc.rs"]
128mod imp;
129
130#[cfg(target_arch = "riscv64")]
131#[macro_use]
132#[path = "./riscv64.rs"]
133mod imp;
134
135#[cfg(target_arch = "riscv32")]
136#[macro_use]
137#[path = "./riscv32.rs"]
138mod imp;
139
140#[cfg(target_arch = "aarch64")]
141#[macro_use]
142#[path = "./aarch64.rs"]
143mod imp;
144
145#[cfg(target_arch = "arm")]
146#[macro_use]
147#[path = "./arm.rs"]
148mod imp;
149
150#[cfg(not(any(
151    target_arch = "x86_64",
152    target_arch = "x86",
153    target_arch = "riscv64",
154    target_arch = "riscv32",
155    target_arch = "aarch64",
156    target_arch = "arm",
157)))]
158#[macro_use]
159mod imp {
160    compile_error!("sjlj2: unsupported platform");
161
162    macro_rules! set_jump_raw {
163        ($val:tt, $($tt:tt)*) => {
164            $val = 0 as _
165        };
166    }
167
168    pub(crate) unsafe fn long_jump_raw(_buf: *mut (), _data: usize) -> ! {
169        unimplemented!()
170    }
171}
172
173/// A jump checkpoint that you can go back to at any time.
174///
175/// It consists of a single machine word.
176#[doc(alias = "jmp_buf")]
177#[derive(Debug, Clone, Copy)]
178pub struct JumpPoint<'a>(*mut (), PhantomData<fn(&'a ()) -> &'a ()>);
179
180#[cfg(doctest)]
181/// ```compile_fail
182/// fn f(j: sjlj2::JumpPoint<'_>) -> impl Send { j }
183/// ```
184///
185/// ```compile_fail
186/// fn f(j: sjlj2::JumpPoint<'_>) -> impl Sync { j }
187/// ```
188fn _assert_not_or_sync() {}
189
190#[cfg(doctest)]
191/// ```compile_fail
192/// fn f<'a, 'b: 'a>(j: sjlj2::JumpPoint<'a>) -> sjlj2::JumpPoint<'b> { j }
193/// ```
194///
195/// ```compile_fail
196/// fn f<'a: 'b, 'b>(j: sjlj2::JumpPoint<'a>) -> sjlj2::JumpPoint<'b> { j }
197/// ```
198fn _assert_invariant() {}
199
200impl JumpPoint<'_> {
201    /// Reconstruct from a raw state.
202    ///
203    /// # Safety
204    ///
205    /// `raw` must be a valid state returned [`JumpPoint::as_raw`], and the returned type must not
206    /// outlive the lifetime of the original [`JumpPoint`] (that is, the argument closure of
207    /// [`catch_long_jump`]).
208    pub const unsafe fn from_raw(raw: *mut ()) -> Self {
209        Self(raw, PhantomData)
210    }
211
212    /// Get the underlying raw state.
213    #[must_use]
214    pub const fn as_raw(self) -> *mut () {
215        self.0
216    }
217
218    /// Alias of [`long_jump`].
219    ///
220    /// # Safety
221    ///
222    /// See [`long_jump`].
223    #[inline]
224    pub unsafe fn long_jump(self, data: usize) -> ! {
225        unsafe { long_jump(self, data) }
226    }
227}
228
229/// Invokes a closure with a jump checkpoint.
230///
231/// This function returns `Continue` if the closure returns normally. If
232/// [`long_jump`] is called on the closure argument [`JumpPoint`] inside the closure,
233/// it force unwinds the stack back to this function, and `Break` is returned
234/// with the carrying value from the argument of `long_jump`.
235///
236/// See [`long_jump`] for its safety condition.
237///
238/// # Precondition
239///
240/// The argument closure must not have a significant `Drop`, or the call frame cannot be POF.
241/// We did a best-effort detection for this with [`core::mem::needs_drop`] and a
242/// compiler error will be generated for `ordinary` with significant `Drop`.
243/// It may (but practically never) generates false positive compile errors.
244///
245/// # Safety
246///
247/// Yes, this function is safe to call. [`long_jump`] is unsafe, however.
248///
249/// # Panics
250///
251/// It is safe to panic (unwind) in `ordinary` but the behavior varies:
252/// - If cargo feature `unwind` is enabled, panic will be caught, passed through
253///   ASM boundary and resumed.
254/// - Otherwise,  it aborts the process.
255///
256/// Panics from `lander` or `Drop` of `T` are trivial because they are executed
257/// outside the ASM boundary.
258///
259/// # Nesting
260///
261/// The stack frame of `catch_long_jump` is a Plain Old Frame (POF), thus nesting
262/// `catch_long_jump` and `long_jump` across multiple levels of
263/// `catch_long_jump` is allowed.
264///
265/// ```
266/// use std::ops::ControlFlow;
267/// use sjlj2::catch_long_jump;
268///
269/// let ret = catch_long_jump(|jp1| {
270///     let _ = catch_long_jump(|_jp2| {
271///         unsafe { jp1.long_jump(42) };
272///     });
273///     unreachable!();
274/// });
275/// assert_eq!(ret, ControlFlow::Break(42));
276/// ```
277#[doc(alias = "setjmp")]
278#[inline]
279pub fn catch_long_jump<T, F>(f: F) -> ControlFlow<usize, T>
280where
281    F: FnOnce(JumpPoint<'_>) -> T,
282{
283    let mut ret = MaybeUninit::uninit();
284
285    #[cfg(feature = "unwind")]
286    match set_jump_impl(|jp| {
287        ret.write(std::panic::catch_unwind(std::panic::AssertUnwindSafe(
288            || f(jp),
289        )));
290    }) {
291        // SAFETY: `f` returns normally or caught a panic, thus `ret` is initialized.
292        ControlFlow::Continue(()) => match unsafe { ret.assume_init() } {
293            Ok(ret) => ControlFlow::Continue(ret),
294            Err(payload) => std::panic::resume_unwind(payload),
295        },
296        ControlFlow::Break(val) => ControlFlow::Break(val),
297    }
298
299    #[cfg(not(feature = "unwind"))]
300    match set_jump_impl(|jp| {
301        ret.write(f(jp));
302    }) {
303        // SAFETY: `f` returns normally, thus `ret` is initialized.
304        ControlFlow::Continue(()) => ControlFlow::Continue(unsafe { ret.assume_init() }),
305        ControlFlow::Break(val) => ControlFlow::Break(val),
306    }
307}
308
309#[inline]
310fn set_jump_impl<F>(f: F) -> ControlFlow<usize>
311where
312    F: FnOnce(JumpPoint<'_>),
313{
314    // NB: Properties expected by ASM:
315    // - `jmp_buf` is at offset 0.
316    // - On the exceptional path, the carried value is stored in `jmp_buf[0]`.
317    #[repr(C)]
318    struct Data<F> {
319        jmp_buf: MaybeUninit<imp::Buf>,
320        func: ManuallyDrop<F>,
321    }
322
323    macro_rules! gen_wrap {
324        ($abi:literal) => {
325            unsafe extern $abi fn wrap<F: FnOnce(JumpPoint<'_>)>(data: &mut Data<F>) {
326                // Non-unwinding ABI generates abort-on-unwind guard since our MSRV 1.87.
327                // No need to handle unwinding here.
328                let jp = unsafe { JumpPoint::from_raw(data.jmp_buf.as_mut_ptr().cast()) };
329                unsafe { ManuallyDrop::take(&mut data.func)(jp) };
330            }
331        };
332    }
333
334    // Linux and Windows have different C ABI. Here we choose sysv64 for simplicity.
335    #[cfg(target_arch = "x86_64")]
336    gen_wrap!("sysv64");
337
338    // x86 cdecl pass all arguments on stack, which is inconvenient under the
339    // fact that compilers also disagree on stack alignments.
340    // Here we choose fastcall to pass through ECX for simplicity.
341    #[cfg(target_arch = "x86")]
342    gen_wrap!("fastcall");
343
344    #[cfg(not(any(target_arch = "x86_64", target_arch = "x86")))]
345    gen_wrap!("C");
346
347    const {
348        assert!(
349            !core::mem::needs_drop::<F>(),
350            "catch_long_jump closure must not have a significant Drop",
351        );
352    }
353
354    let mut data = Data::<F> {
355        jmp_buf: MaybeUninit::uninit(),
356        func: ManuallyDrop::new(f),
357    };
358
359    unsafe {
360        set_jump_raw!(&raw mut data, wrap::<F>, {
361            let data = unsafe { data.jmp_buf.assume_init().0[0] };
362            return ControlFlow::Break(data);
363        });
364        ControlFlow::Continue(())
365    }
366}
367
368/// Long jump to a checkpoint, force unwinding the stack and return an arbitrary
369/// `data` to an early [`catch_long_jump`] specified by `point`.
370///
371/// Note: Unlike C `longjmp`, this function will not special case `data == 0`.
372/// `long_jump(jp, 0)` will correctly make `catch_long_jump` return `ControlFlow::Break(0)`.
373///
374/// # Safety
375///
376/// All stack frames between the current and the `catch_long_jump` specified by
377/// `point` must all be [Plain Old Frames][pof].
378///
379/// > ⚠️
380/// > It is explicitly said in [RFC2945][pof] that
381/// > > When deallocating Rust POFs: for now, this is not specified, and must be considered
382/// > > undefined behavior.
383/// >
384/// > In practice, this crate does not suffers the
385/// > [relevant optimization issue caused by return-twice][misopt],
386/// > and should be UB-free as long as only POFs are `long_jump`ed over.
387/// >
388/// > But you still need to take your own risk until force-unwinding behavior is
389/// > fully defined by Rust Reference.
390///
391/// [pof]: https://rust-lang.github.io/rfcs/2945-c-unwind-abi.html#plain-old-frames
392/// [misopt]: https://github.com/rust-lang/rfcs/issues/2625
393#[doc(alias = "longjmp")]
394#[inline]
395pub unsafe fn long_jump(point: JumpPoint<'_>, data: usize) -> ! {
396    unsafe { imp::long_jump_raw(point.0, data) }
397}