Skip to content

Commit

Permalink
Add Emscripten support
Browse files Browse the repository at this point in the history
  • Loading branch information
purplesyringa committed Oct 31, 2024
1 parent c19efb8 commit 7cc141a
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 10 deletions.
8 changes: 6 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
152 changes: 152 additions & 0 deletions src/backend/emscripten.rs
Original file line number Diff line number Diff line change
@@ -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<Func: FnOnce() -> R, R>(func: Func) -> Result<R, *mut Header> {
// 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::<Header>().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<unsafe fn(*mut ()) -> *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();
}
4 changes: 4 additions & 0 deletions src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
15 changes: 9 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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};

0 comments on commit 7cc141a

Please sign in to comment.