From 7cc141a4e1605c98fb5b0cfdb632f2bbe5e74c89 Mon Sep 17 00:00:00 2001 From: Alisa Sireneva Date: Thu, 31 Oct 2024 16:45:41 +0300 Subject: [PATCH] Add Emscripten support --- .github/workflows/ci.yml | 8 +- Cargo.toml | 2 +- build.rs | 3 +- src/backend/emscripten.rs | 152 ++++++++++++++++++++++++++++++++++++++ src/backend/mod.rs | 4 + src/lib.rs | 15 ++-- 6 files changed, 174 insertions(+), 10 deletions(-) create mode 100644 src/backend/emscripten.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 14abf2e..657cdaa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -97,9 +97,13 @@ jobs: - name: Install Cross run: cargo install cross --git https://github.com/cross-rs/cross - name: Test with panic backend (debug) - run: cross test --target wasm32-unknown-emscripten + run: LITHIUM_BACKEND=panic cross test --target wasm32-unknown-emscripten + - name: Test with Emscripten backend (debug) + run: LITHIUM_BACKEND=emscripten cross test --target wasm32-unknown-emscripten - name: Test with panic backend (release) - run: cross test --target wasm32-unknown-emscripten --release + run: LITHIUM_BACKEND=panic cross test --target wasm32-unknown-emscripten --release + - name: Test with Emscripten backend (release) + run: LITHIUM_BACKEND=emscripten cross test --target wasm32-unknown-emscripten --release darwin: runs-on: ${{ matrix.os }} diff --git a/Cargo.toml b/Cargo.toml index df58555..ae82ed6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ harness = false [lints.rust.unexpected_cfgs] level = "warn" -check-cfg = ["cfg(nightly)", "cfg(backend, values(\"itanium\", \"seh\", \"panic\", \"unimplemented\"))"] +check-cfg = ["cfg(nightly)", "cfg(backend, values(\"itanium\", \"seh\", \"emscripten\", \"panic\", \"unimplemented\"))"] [profile.dev] panic = "unwind" diff --git a/build.rs b/build.rs index fda4769..f891fb9 100644 --- a/build.rs +++ b/build.rs @@ -20,9 +20,10 @@ fn main() { println!("cargo::rerun-if-env-changed=LITHIUM_BACKEND"); if let Ok(backend) = std::env::var("LITHIUM_BACKEND") { println!("cargo::rustc-cfg=backend=\"{backend}\""); + } else if cfg("target_os") == "emscripten" { + println!("cargo::rustc-cfg=backend=\"emscripten\""); } else if version_meta().unwrap().channel == Channel::Nightly && (has_cfg("unix") || (has_cfg("windows") && cfg("target_env") == "gnu")) - && cfg("target_os") != "emscripten" { println!("cargo::rustc-cfg=backend=\"itanium\""); } else if version_meta().unwrap().channel == Channel::Nightly diff --git a/src/backend/emscripten.rs b/src/backend/emscripten.rs new file mode 100644 index 0000000..4761233 --- /dev/null +++ b/src/backend/emscripten.rs @@ -0,0 +1,152 @@ +// This is partially taken from +// - https://github.com/rust-lang/rust/blob/master/library/panic_unwind/src/emcc.rs + +use super::{super::intrinsic::intercept, ThrowByPointer}; + +pub(crate) struct ActiveBackend; + +/// Emscripten unwinding. +/// +/// At the moment, the emscripten target doesn't provide a Itanium-compatible ABI, but it does +/// libcxxabi-style C++ exceptions. This is what we're going to use. +// SAFETY: C++ exceptions satisfy the requirements. +unsafe impl ThrowByPointer for ActiveBackend { + type ExceptionHeader = Header; + + fn new_header() -> Header { + Header { + reference_count: 0, + exception_type: core::ptr::null(), + exception_destructor: None, + caught: false, + rethrown: false, + adjusted_ptr: core::ptr::null_mut(), + padding: core::ptr::null(), + } + } + + #[inline] + unsafe fn throw(ex: *mut Header) -> ! { + // SAFETY: This is in-bounds for the header. + let end_of_header = unsafe { ex.add(1) }.cast(); + + // SAFETY: We provide a valid exception header. + unsafe { + __cxa_throw(end_of_header, &raw const TYPE_INFO, cleanup); + } + } + + #[inline] + fn intercept R, R>(func: Func) -> Result { + // SAFETY: The catch handler does not unwind. + let catch_data = match unsafe { intercept(func, |catch_data| catch_data) } { + Ok(value) => return Ok(value), + Err(catch_data) => catch_data, + }; + + // SAFETY: `core::intrinsics::catch_unwind` provides a pointer to this structure. + let catch_data: &CatchData = unsafe { &*catch_data.cast() }; + + // SAFETY: `ptr` was obtained from a `core::intrinsics::catch_unwind` call. + let adjusted_ptr = unsafe { __cxa_begin_catch(catch_data.ptr) }; + + // SAFETY: `adjusted_ptr` points at what the unwinder thinks is a beginning of our exception + // object. In reality, this is just the ned of header, so `sub(1)` yields the beginning of + // the header. + let ex: *mut Header = unsafe { adjusted_ptr.cast::
().sub(1) }; + + // SAFETY: `ex` points at a valid header. We're unique, so no data races are possible. + if unsafe { (*ex).exception_type } != &raw const TYPE_INFO { + // Rust panic or a foreign exception. Either way, rethrow. + // SAFETY: This function has no preconditions. + unsafe { + __cxa_rethrow(); + } + } + + // Prevent `__cxa_end_catch` from trying to deallocate the exception object with free(3) and + // corrupting the heap. + // SAFETY: We require that Lithium exceptions are not caught by foreign runtimes, so we + // assume this is still a unique reference to this exception. + unsafe { + (*ex).reference_count = 2; + } + + // SAFETY: This function has no preconditions. + unsafe { + __cxa_end_catch(); + } + + Err(ex) + } +} + +// This is __cxa_exception from emscripten sources. +#[repr(C)] +pub(crate) struct Header { + reference_count: usize, + exception_type: *const TypeInfo, + exception_destructor: Option *mut ()>, + caught: bool, + rethrown: bool, + adjusted_ptr: *mut (), + padding: *const (), +} + +// This is std::type_info. +#[repr(C)] +struct TypeInfo { + vtable: *const usize, + name: *const i8, +} + +// SAFETY: `!Sync` pointers are stupid. +unsafe impl Sync for TypeInfo {} + +#[repr(C)] +struct CatchData { + ptr: *mut (), + is_rust_panic: bool, +} + +extern "C" { + #[link_name = "\x01_ZTVN10__cxxabiv117__class_type_infoE"] + static CLASS_TYPE_INFO_VTABLE: [usize; 3]; +} + +static TYPE_INFO: TypeInfo = TypeInfo { + // Normally we would use .as_ptr().add(2) but this doesn't work in a const context. + vtable: unsafe { &CLASS_TYPE_INFO_VTABLE[2] }, + name: c"lithium_exception".as_ptr(), +}; + +extern "C-unwind" { + fn __cxa_begin_catch(thrown_exception: *mut ()) -> *mut (); + + fn __cxa_rethrow() -> !; + + fn __cxa_end_catch(); + + fn __cxa_throw( + thrown_object: *mut (), + tinfo: *const TypeInfo, + destructor: unsafe extern "C" fn(*mut ()) -> *mut (), + ) -> !; +} + +/// Destruct an exception when caught by a foreign runtime. +/// +/// # Safety +/// +/// `ex` must point at a valid exception object. +unsafe extern "C" fn cleanup(_ex: *mut ()) -> *mut () { + #[cfg(feature = "std")] + { + eprintln!( + "A Lithium exception was caught by a non-Lithium catch mechanism. This is undefined behavior. The process will now terminate.", + ); + std::process::abort(); + } + #[cfg(not(feature = "std"))] + core::intrinsics::abort(); +} diff --git a/src/backend/mod.rs b/src/backend/mod.rs index ab242d9..2a2c583 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -173,6 +173,10 @@ mod imp; #[path = "panic.rs"] mod imp; +#[cfg(backend = "emscripten")] +#[path = "emscripten.rs"] +mod imp; + #[cfg(backend = "unimplemented")] #[path = "unimplemented.rs"] mod imp; diff --git a/src/lib.rs b/src/lib.rs index 1083fd0..c58de1b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,14 +80,17 @@ #![cfg_attr(not(any(feature = "std", test)), no_std)] #![cfg_attr(not(feature = "std"), feature(thread_local))] #![cfg_attr( - any(backend = "itanium", backend = "seh"), + any(backend = "itanium", backend = "seh", backend = "emscripten"), expect( internal_features, reason = "Can't do anything about core::intrinsics::catch_unwind yet", ) )] -#![cfg_attr(backend = "itanium", feature(core_intrinsics))] -#![cfg_attr(backend = "seh", feature(core_intrinsics, fn_ptr_trait, std_internals))] +#![cfg_attr( + any(backend = "itanium", backend = "seh", backend = "emscripten"), + feature(core_intrinsics) +)] +#![cfg_attr(backend = "seh", feature(fn_ptr_trait, std_internals))] #![deny(unsafe_op_in_unsafe_fn)] #![warn( clippy::cargo, @@ -157,12 +160,12 @@ extern crate alloc; mod api; mod backend; -#[cfg(any(backend = "itanium", backend = "panic"))] +#[cfg(any(backend = "itanium", backend = "emscripten", backend = "panic"))] mod heterogeneous_stack; -#[cfg(any(backend = "itanium", backend = "panic"))] +#[cfg(any(backend = "itanium", backend = "emscripten", backend = "panic"))] mod stacked_exceptions; -#[cfg(any(backend = "itanium", backend = "seh"))] +#[cfg(any(backend = "itanium", backend = "seh", backend = "emscripten"))] mod intrinsic; pub use api::{catch, intercept, throw, InFlightException};