Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using rust with wasm error handling #19612

Open
hoodmane opened this issue Jun 13, 2023 · 4 comments
Open

Using rust with wasm error handling #19612

hoodmane opened this issue Jun 13, 2023 · 4 comments

Comments

@hoodmane
Copy link
Collaborator

hoodmane commented Jun 13, 2023

I've been working on getting rust panics to work with wasm error handling. It is possible to compile the rust code and get valid generated code, but panics cannot be caught with catch_unwind. My best guess is that the problem is that the Rust compiler try intrinsic is wrong. The generated wasm has a throw instruction but no try or catch.

@aheejin

I am compiling rust code with the following environment variables:

export CARGO_BUILD_TARGET=wasm32-unknown-emscripten
export CARGO_TARGET_WASM32_UNKNOWN_EMSCRIPTEN_LINKER=emcc

export RUSTFLAGS= \
	-C link-arg=-sSIDE_MODULE=2 \
	-C link-arg=-sWASM_BIGINT \
	-C link-arg=-fwasm-exceptions \
	-C link-arg=-sDISABLE_EXCEPTION_CATCHING=1 \
	-C llvm-args=-enable-emscripten-cxx-exceptions=0 \
	-C llvm-args=-wasm-enable-sjlj \
	-Z link-native-libraries=no

my rust library looks like:

#[no_mangle]
pub extern "C" fn panic_test(n: i32) -> i32 {
    println!("panic test start {}", n);
    let result = panic::catch_unwind(|| {
        f(n)
    });
    println!("panic test end {}, {:?}", n, result);
    if let Ok(k) = result {
        return k - 1;
    } else {
        return -66;
    }
}

#[inline(never)]
fn f(n: i32) -> i32 {
    println!("f start {}", n);
    let result = g(n);
    println!("f end {}", n);
    return result - 1;
}

#[inline(never)]
fn g(n: i32) -> i32 {
    println!("g start {}", n);
    if n > 20 {
        panic!("oops!");
    }
    println!("g end {}", n);
    return n - 1;
}

The try intrinsic is here:
https://github.com/rust-lang/rust/blob/master/compiler/rustc_codegen_llvm/src/intrinsic.rs#L683-L707
it seems to be missing at least calls to __cxa_begin_catch and __cxa_end_catch, and the landingpad looks like it's specific to js exception handling.

@hoodmane
Copy link
Collaborator Author

I tried to work on this here:
rust-lang/rust@master...hoodmane:rust:wasm-eh
My approach may be a bit misguided, there is certainly no evidence that it is working. My model IR the following, which does seem to work correctly when compiled with Emscripten:

Details
define i32 @rust_try(
  ptr nocapture noundef readonly %try_func, 
  ptr noundef %data, 
  ptr nocapture noundef readonly %catch_func
)
personality ptr @__gxx_wasm_personality_v0
 {
  invoke void %try_func(ptr noundef %data)
          to label %normal unwind label %catchswitch

normal:
  ret i32 0

catchswitch:
  %cs = catchswitch within none [label %catchpad_all] unwind to caller

catchpad_all:
  %funclet = catchpad within %cs [ptr @_ZTI10rust_panic, ptr null]
  %ptr = tail call ptr @llvm.wasm.get.exception(token %funclet)
  %selector = tail call i32 @llvm.wasm.get.ehselector(token %funclet)
  %rust_typeid = tail call i32 @llvm.eh.typeid.for(ptr nonnull @_ZTI10rust_panic)
  %is_rust_panic = icmp eq i32 %selector, %rust_typeid
  %adjusted_ptr = call ptr @__cxa_begin_catch(ptr %ptr) #3 [ "funclet"(token %funclet) ]
  br i1 %is_rust_panic, label %caught_rust, label %caught_foreign

caught_rust:
  call void %catch_func(ptr noundef %data, ptr noundef %adjusted_ptr) #3 [ "funclet"(token %funclet) ]
  call void @__cxa_end_catch() #3 [ "funclet"(token %funclet) ]
  catchret from %funclet to label %caught

caught_foreign:
  call void %catch_func(ptr noundef %data, ptr noundef null) #3 [ "funclet"(token %funclet) ]
  call void @__cxa_end_catch() #3 [ "funclet"(token %funclet) ]
  catchret from %funclet to label %caught

caught:
  ret i32 1
}

I don't know how to build quite this IR since the rust IR Builder gives %funclet type Funclet and function calls expect type Value so I have no idea how to pass it to a function like llvm.wasm.get.exception.

@tlively
Copy link
Member

tlively commented Jun 14, 2023

cc @aheejin, does that LLVM look correct for Wasm EH?

@hoodmane
Copy link
Collaborator Author

If I pass -C llvm-args=-wasm-enable-eh to rustc, compilation fails with signal: 4, SIGILL: illegal instruction. I think when compiling with clang, -fwasm-exceptions is translated into -target-feature +exception-handling -mllvm -wasm-enable-eh which in rust speak I think is spelled -C target-feature=+exception-handling -C llvm-args=-wasm-enable-eh. But passing these makes it fail weirdly.

In other cases, the generated wasm has no try/catch instructions.

@aheejin
Copy link
Member

aheejin commented Jun 15, 2023

We process llvm.wasm.get.exception and llvm.wasm.get.ehselector intrinsics in https://github.com/llvm/llvm-project/blob/main/llvm/lib/CodeGen/WasmEHPrepare.cpp pass. In a nutshell, llvm.wasm.get.exception turns into wasm.catch intrinsic, which is lowered down to a real Wasm catch instruction. llvm.wasm.get.ehselector turns into a load, but we need to do some stores and a call into the personality function before that. (See the long comments in the pass for details.) In x86, the personality function is called within libunwind while stack unwinding, but we instrument user functions to call it from there because we don't have control over stack unwinding ourselves.

If you use our Wasm backend, running opt or llc on bc/ll files will run WasmEHPrepare and all the backend passes for you, but WasmEHPrepare assumes the existence some C++ library functions and data structures, so it looks Rust would need its similar counterparts. Once you get through that I think it can also just share our backend pipeline.

I don't know Rust so I really don't have any idea on what your rust code does. I think I can help more if you give me the LLVM IR generated from it (which you did too).

If I pass -C llvm-args=-wasm-enable-eh to rustc, compilation fails with signal: 4, SIGILL: illegal instruction. I think when compiling with clang, -fwasm-exceptions is translated into -target-feature +exception-handling -mllvm -wasm-enable-eh which in rust speak I think is spelled -C target-feature=+exception-handling -C llvm-args=-wasm-enable-eh. But passing these makes it fail weirdly.

In other cases, the generated wasm has no try/catch instructions.

I'm not sure what this means. What's the actual command line are you running? I am not very familiar with Rust Wasm toolchain. For C++, emcc -fwasm-exceptions is supposed to take care of everything without the need to separately passing -wasm-enable-eh and such. But I guess Rust has its own frontend, right? Then from which point do you run LLVM? Which tool in LLVM do you run? (clang? opt? llc?)

And what's the command line for that? Can you give me an input ll file and a command that crashes?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants