Skip to content

Commit 7c47541

Browse files
committed
Replace setjmp/longjmp usage in Wasmtime
Since Wasmtime's inception it's used the `setjmp` and `longjmp` functions in C to implement handling of traps. While this solution was easy to implement, relatively portable, and performant enough, there are a number of downsides that have evolved over time to make this an unattractive approach in the long run: * Using `setjmp` fundamentally requires using C because Rust does not understand a function that returns twice. It's fundamentally unsound to invoke `setjmp` in Rust meaning that Wasmtime has forever needed a C compiler configured and set up to build. This notably means that `cargo check` cannot check other targets easily. * Using `longjmp` means that Rust function frames are unwound on the stack without running destructors. This is a dangerous operation of which we get no protection from the compiler about. Both frames entering wasm and frames exiting wasm are all skipped. Absolutely minimizing this has been beneficial for portability to platforms such as Pulley. * Currently the no_std implementation of Wasmtime requires embedders to provide `wasmtime_{setjmp,longjmp}` which is a thorn in the side of what is otherwise a mostly entirely independent implementation of Wasmtime. * There is a performance floor to using `setjmp` and `longjmp`. Calling `setjmp` requires using C but Wasmtime is otherwise written in Rust meaning that there's a Rust->C->Rust->Wasm boundary which fundamentally can't be inlined without cross-language LTO which is difficult to configure. * With the implementation of the WebAssembly exceptions proposal Wasmtime now has two means of unwinding the stack. Ideally Wasmtime would only have one, and the more general one is the method of exceptions. * Jumping out of a signal handler on Unix is tricky business. While we've made it work it's generally most robust of the signal handler simply returns which it now does. With all of that in mind the purpose of this commit is to replace the setjmp/longjmp mechanism of handling traps with the recently implemented support for exceptions in Cranelift. That is intended to resolve all of the above points in one swoop. One point in particular though that's nice about setjmp/longjmp is that unwinding the stack on a trap is an O(1) operation. For situations such as stack overflow that's a particularly nice property to have as we can guarantee embedders that traps are a constant time (albeit somewhat expensive with signals) operation. Exceptions naively require unwinding the entire stack, and although frame pointers mean we're just traversing a linked list I wanted to preserve the O(1) property here nonetheless. To achieve this a solution is implemented where the array-to-wasm (host-to-wasm) trampolines setup state in `VMStoreContext` so looking up the current trap handler frame is an O(1) operation. Namely the sp/fp/pc values for a `Handler` are stored inline. Implementing this feature required supporting relocations-to-offsets-in-functions which was not previously supported by Wasmtime. This required Cranelift refactorings such as bytecodealliance#11570, bytecodealliance#11585, and bytecodealliance#11576. This then additionally required some more refactoring in this commit which was difficult to split out as it otherwise wouldn't be tested. Apart from the relocation-related business much of this change is about updating the platform signal handlers to use exceptions instead of longjmp to return. For example on Unix this means updating the `ucontext_t` with register values that the handler specifies. Windows involves updating similar contexts, and macOS mach ports ended up not needing too many changes. In terms of overall performance the relevant benchmark from this repository, compared to before this commit, is: sync/no-hook/core - host-to-wasm - typed - nop time: [10.552 ns 10.561 ns 10.571 ns] change: [−7.5238% −7.4011% −7.2786%] (p = 0.00 < 0.05) Performance has improved. Closes bytecodealliance#3927 cc bytecodealliance#10923 prtest:full
1 parent 4c01ee2 commit 7c47541

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+795
-1040
lines changed

.github/workflows/main.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ jobs:
318318
micro_checks:
319319
name: Check ${{matrix.name}}
320320
strategy:
321-
fail-fast: true
321+
fail-fast: ${{ github.event_name != 'pull_request' }}
322322
matrix:
323323
include:
324324
- name: wasmtime
@@ -504,7 +504,7 @@ jobs:
504504
name: "Platform: ${{ matrix.target }}"
505505
runs-on: ${{ matrix.os }}
506506
strategy:
507-
fail-fast: true
507+
fail-fast: ${{ github.event_name != 'pull_request' }}
508508
matrix:
509509
include:
510510
- target: x86_64-unknown-freebsd
@@ -627,7 +627,7 @@ jobs:
627627
if: needs.determine.outputs.test-capi
628628

629629
strategy:
630-
fail-fast: true
630+
fail-fast: ${{ github.event_name != 'pull_request' }}
631631
matrix:
632632
os: [ubuntu-24.04, macos-15, windows-2025]
633633

cranelift/codegen/src/isa/aarch64/abi.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1097,10 +1097,13 @@ impl ABIMachineSpec for AArch64MachineDeps {
10971097
}
10981098

10991099
fn get_regs_clobbered_by_call(call_conv: isa::CallConv, is_exception: bool) -> PRegSet {
1100-
match call_conv {
1101-
isa::CallConv::Winch => WINCH_CLOBBERS,
1102-
isa::CallConv::Tail if is_exception => ALL_CLOBBERS,
1103-
_ => DEFAULT_AAPCS_CLOBBERS,
1100+
match (call_conv, is_exception) {
1101+
(isa::CallConv::Tail, true) => ALL_CLOBBERS,
1102+
(isa::CallConv::Winch, true) => ALL_CLOBBERS,
1103+
(isa::CallConv::Winch, false) => WINCH_CLOBBERS,
1104+
(isa::CallConv::SystemV, _) => DEFAULT_AAPCS_CLOBBERS,
1105+
(_, false) => DEFAULT_AAPCS_CLOBBERS,
1106+
(_, true) => panic!("unimplemented clobbers for exn abi of {call_conv:?}"),
11041107
}
11051108
}
11061109

cranelift/codegen/src/isa/call_conv.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ impl CallConv {
8484
/// Does this calling convention support exceptions?
8585
pub fn supports_exceptions(&self) -> bool {
8686
match self {
87-
CallConv::Tail | CallConv::SystemV => true,
87+
CallConv::Tail | CallConv::SystemV | CallConv::Winch => true,
8888
_ => false,
8989
}
9090
}

cranelift/codegen/src/isa/pulley_shared/inst.isle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -709,9 +709,9 @@
709709

710710
;; Helper for creating `MInst.LoadExtName*` instructions.
711711
(decl load_ext_name (BoxExternalName i64 RelocDistance) XReg)
712-
(rule 1 (load_ext_name name offset (RelocDistance.Near))
712+
(rule (load_ext_name name offset (RelocDistance.Near))
713713
(load_ext_name_near name offset))
714-
(rule 0 (load_ext_name name offset (RelocDistance.Far))
714+
(rule (load_ext_name name offset (RelocDistance.Far))
715715
(load_ext_name_far name offset))
716716

717717
;;;; Helpers for Emitting Calls ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

cranelift/codegen/src/isa/x64/abi.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -887,11 +887,13 @@ impl ABIMachineSpec for X64ABIMachineSpec {
887887
call_conv_of_callee: isa::CallConv,
888888
is_exception: bool,
889889
) -> PRegSet {
890-
match call_conv_of_callee {
891-
CallConv::Winch => ALL_CLOBBERS,
892-
CallConv::WindowsFastcall => WINDOWS_CLOBBERS,
893-
CallConv::Tail if is_exception => ALL_CLOBBERS,
894-
_ => SYSV_CLOBBERS,
890+
match (call_conv_of_callee, is_exception) {
891+
(isa::CallConv::Tail, true) => ALL_CLOBBERS,
892+
(isa::CallConv::Winch, _) => ALL_CLOBBERS,
893+
(isa::CallConv::SystemV, _) => SYSV_CLOBBERS,
894+
(isa::CallConv::WindowsFastcall, false) => WINDOWS_CLOBBERS,
895+
(_, false) => SYSV_CLOBBERS,
896+
(call_conv, true) => panic!("unimplemented clobbers for exn abi of {call_conv:?}"),
895897
}
896898
}
897899

crates/cranelift/src/compiler.rs

Lines changed: 103 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -373,9 +373,13 @@ impl wasmtime_environ::Compiler for Compiler {
373373
let array_call_sig = array_call_signature(isa);
374374

375375
let mut compiler = self.function_compiler();
376-
let func = ir::Function::with_name_signature(Default::default(), array_call_sig);
376+
let func = ir::Function::with_name_signature(key_to_name(key), array_call_sig);
377377
let (mut builder, block0) = compiler.builder(func);
378378

379+
let try_call_block = builder.create_block();
380+
builder.ins().jump(try_call_block, []);
381+
builder.switch_to_block(try_call_block);
382+
379383
let (vmctx, caller_vmctx, values_vec_ptr, values_vec_len) = {
380384
let params = builder.func.dfg.block_params(block0);
381385
(params[0], params[1], params[2], params[3])
@@ -391,41 +395,95 @@ impl wasmtime_environ::Compiler for Compiler {
391395
args.insert(0, caller_vmctx);
392396
args.insert(0, vmctx);
393397

394-
// Just before we enter Wasm, save our stack pointer.
398+
// Just before we enter Wasm, save our context information.
395399
//
396400
// Assert that we were really given a core Wasm vmctx, since that's
397401
// what we are assuming with our offsets below.
398402
self.debug_assert_vmctx_kind(&mut builder, vmctx, wasmtime_environ::VMCONTEXT_MAGIC);
399403
let offsets = VMOffsets::new(isa.pointer_bytes(), &translation.module);
400404
let vm_store_context_offset = offsets.ptr.vmctx_store_context();
401-
save_last_wasm_entry_fp(
405+
save_last_wasm_entry_context(
402406
&mut builder,
403407
pointer_type,
404408
&offsets.ptr,
405409
vm_store_context_offset.into(),
406410
vmctx,
411+
try_call_block,
407412
);
408413

414+
// Create the invocation of wasm, which is notably done with a
415+
// `try_call` with an exception handler that's used to handle traps.
416+
let normal_return = builder.create_block();
417+
let exceptional_return = builder.create_block();
418+
let normal_return_values = wasm_call_sig
419+
.returns
420+
.iter()
421+
.map(|ty| {
422+
builder
423+
.func
424+
.dfg
425+
.append_block_param(normal_return, ty.value_type)
426+
})
427+
.collect::<Vec<_>>();
428+
409429
// Then call the Wasm function with those arguments.
410430
let callee_key = FuncKey::DefinedWasmFunction(module_index, def_func_index);
411-
let call = declare_and_call(&mut builder, wasm_call_sig, callee_key, &args);
412-
let results = builder.func.dfg.inst_results(call).to_vec();
431+
let signature = builder.func.import_signature(wasm_call_sig.clone());
432+
let callee = {
433+
let (namespace, index) = callee_key.into_raw_parts();
434+
let name = ir::ExternalName::User(
435+
builder
436+
.func
437+
.declare_imported_user_function(ir::UserExternalName { namespace, index }),
438+
);
439+
builder.func.dfg.ext_funcs.push(ir::ExtFuncData {
440+
name,
441+
signature,
442+
colocated: true,
443+
})
444+
};
413445

414-
// Then store the results back into the array.
446+
let dfg = &mut builder.func.dfg;
447+
let exception_table = dfg.exception_tables.push(ir::ExceptionTableData::new(
448+
signature,
449+
ir::BlockCall::new(
450+
normal_return,
451+
(0..wasm_call_sig.returns.len())
452+
.map(|i| ir::BlockArg::TryCallRet(i.try_into().unwrap())),
453+
&mut dfg.value_lists,
454+
),
455+
[ir::ExceptionTableItem::Default(ir::BlockCall::new(
456+
exceptional_return,
457+
None,
458+
&mut dfg.value_lists,
459+
))],
460+
));
461+
builder.ins().try_call(callee, &args, exception_table);
462+
463+
builder.seal_block(try_call_block);
464+
builder.seal_block(normal_return);
465+
builder.seal_block(exceptional_return);
466+
467+
// On the normal return path store all the results in the array we were
468+
// provided and return "true" for "returned successfully".
469+
builder.switch_to_block(normal_return);
415470
self.store_values_to_array(
416471
&mut builder,
417472
wasm_func_ty.returns(),
418-
&results,
473+
&normal_return_values,
419474
values_vec_ptr,
420475
values_vec_len,
421476
);
422-
423-
// At this time wasm functions always signal traps with longjmp or some
424-
// similar sort of routine, so if we got this far that means that the
425-
// function did not trap, so return a "true" value here to indicate that
426-
// to satisfy the ABI of the array-call signature.
427477
let true_return = builder.ins().iconst(ir::types::I8, 1);
428478
builder.ins().return_(&[true_return]);
479+
480+
// On the exceptional return path just return "false" for "did not
481+
// succeed". Note that register restoration is part of the `try_call`
482+
// and handler implementation.
483+
builder.switch_to_block(exceptional_return);
484+
let false_return = builder.ins().iconst(ir::types::I8, 0);
485+
builder.ins().return_(&[false_return]);
486+
429487
builder.finalize();
430488

431489
Ok(CompiledFunctionBody {
@@ -448,7 +506,7 @@ impl wasmtime_environ::Compiler for Compiler {
448506
let array_call_sig = array_call_signature(isa);
449507

450508
let mut compiler = self.function_compiler();
451-
let func = ir::Function::with_name_signature(Default::default(), wasm_call_sig);
509+
let func = ir::Function::with_name_signature(key_to_name(key), wasm_call_sig);
452510
let (mut builder, block0) = compiler.builder(func);
453511

454512
let args = builder.func.dfg.block_params(block0).to_vec();
@@ -702,7 +760,7 @@ impl wasmtime_environ::Compiler for Compiler {
702760
let host_sig = sigs.host_signature(builtin_func_index);
703761

704762
let mut compiler = self.function_compiler();
705-
let func = ir::Function::with_name_signature(Default::default(), wasm_sig.clone());
763+
let func = ir::Function::with_name_signature(key_to_name(key), wasm_sig.clone());
706764
let (mut builder, block0) = compiler.builder(func);
707765
let vmctx = builder.block_params(block0)[0];
708766

@@ -724,7 +782,7 @@ impl wasmtime_environ::Compiler for Compiler {
724782
let call = self.call_builtin(&mut builder, vmctx, &args, builtin_func_index, host_sig);
725783
let results = builder.func.dfg.inst_results(call).to_vec();
726784

727-
// Libcalls do not explicitly `longjmp` for example but instead return a
785+
// Libcalls do not explicitly jump/raise on traps but instead return a
728786
// code indicating whether they trapped or not. This means that it's the
729787
// responsibility of the trampoline to check for an trapping return
730788
// value and raise a trap as appropriate. With the `results` above check
@@ -1127,9 +1185,9 @@ impl Compiler {
11271185
/// This helper is used when the host returns back to WebAssembly. The host
11281186
/// returns a `bool` indicating whether the call succeeded. If the call
11291187
/// failed then Cranelift needs to unwind back to the original invocation
1130-
/// point. The unwind right now is then implemented in Wasmtime with a
1131-
/// `longjmp`, but one day this might be implemented differently with an
1132-
/// unwind inside of Cranelift.
1188+
/// point. The unwind right now is then implemented in Wasmtime with an
1189+
/// exceptional resume, one day this might be implemented differently with
1190+
/// an unwind inside of Cranelift.
11331191
///
11341192
/// Additionally in the future for pulley this will emit a special trap
11351193
/// opcode for Pulley itself to cease interpretation and exit the
@@ -1402,33 +1460,13 @@ fn clif_to_env_exception_tables<'a>(
14021460
builder.add_func(CodeOffset::try_from(range.start).unwrap(), call_sites)
14031461
}
14041462

1405-
fn declare_and_call(
1406-
builder: &mut FunctionBuilder,
1407-
signature: ir::Signature,
1408-
callee_key: FuncKey,
1409-
args: &[ir::Value],
1410-
) -> ir::Inst {
1411-
let (namespace, index) = callee_key.into_raw_parts();
1412-
let name = ir::ExternalName::User(
1413-
builder
1414-
.func
1415-
.declare_imported_user_function(ir::UserExternalName { namespace, index }),
1416-
);
1417-
let signature = builder.func.import_signature(signature);
1418-
let callee = builder.func.dfg.ext_funcs.push(ir::ExtFuncData {
1419-
name,
1420-
signature,
1421-
colocated: true,
1422-
});
1423-
builder.ins().call(callee, &args)
1424-
}
1425-
1426-
fn save_last_wasm_entry_fp(
1463+
fn save_last_wasm_entry_context(
14271464
builder: &mut FunctionBuilder,
14281465
pointer_type: ir::Type,
14291466
ptr_size: &impl PtrSize,
14301467
vm_store_context_offset: u32,
14311468
vmctx: Value,
1469+
block: ir::Block,
14321470
) {
14331471
// First we need to get the `VMStoreContext`.
14341472
let vm_store_context = builder.ins().load(
@@ -1438,14 +1476,33 @@ fn save_last_wasm_entry_fp(
14381476
i32::try_from(vm_store_context_offset).unwrap(),
14391477
);
14401478

1441-
// Then store our current stack pointer into the appropriate slot.
1479+
// Save the current fp/sp of the entry trampoline into the `VMStoreContext`.
14421480
let fp = builder.ins().get_frame_pointer(pointer_type);
14431481
builder.ins().store(
14441482
MemFlags::trusted(),
14451483
fp,
14461484
vm_store_context,
14471485
ptr_size.vmstore_context_last_wasm_entry_fp(),
14481486
);
1487+
let sp = builder.ins().get_stack_pointer(pointer_type);
1488+
builder.ins().store(
1489+
MemFlags::trusted(),
1490+
sp,
1491+
vm_store_context,
1492+
ptr_size.vmstore_context_last_wasm_entry_sp(),
1493+
);
1494+
1495+
// Also save the address of this function's exception handler. This is used
1496+
// as a resumption point for traps, for example.
1497+
let trap_handler = builder
1498+
.ins()
1499+
.get_exception_handler_address(pointer_type, block, 0);
1500+
builder.ins().store(
1501+
MemFlags::trusted(),
1502+
trap_handler,
1503+
vm_store_context,
1504+
ptr_size.vmstore_context_last_wasm_entry_trap_handler(),
1505+
);
14491506
}
14501507

14511508
fn save_last_wasm_exit_fp_and_pc(
@@ -1474,3 +1531,8 @@ fn save_last_wasm_exit_fp_and_pc(
14741531
ptr.vmstore_context_last_wasm_exit_pc(),
14751532
);
14761533
}
1534+
1535+
fn key_to_name(key: FuncKey) -> ir::UserFuncName {
1536+
let (namespace, index) = key.into_raw_parts();
1537+
ir::UserFuncName::User(ir::UserExternalName { namespace, index })
1538+
}

crates/cranelift/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ fn mach_reloc_to_reloc(
302302
// in the Wasm-to-Cranelift translator.
303303
panic!("unexpected libcall {libcall:?}");
304304
}
305-
_ => panic!("unrecognized external name"),
305+
_ => panic!("unrecognized external name {target:?}"),
306306
};
307307
Relocation {
308308
reloc: kind,

crates/environ/src/vmoffsets.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,14 +218,24 @@ pub trait PtrSize {
218218
self.vmstore_context_last_wasm_exit_trampoline_fp() + self.size()
219219
}
220220

221+
/// Return the offset of the `last_wasm_entry_sp` field of `VMStoreContext`.
222+
fn vmstore_context_last_wasm_entry_sp(&self) -> u8 {
223+
self.vmstore_context_last_wasm_exit_pc() + self.size()
224+
}
225+
221226
/// Return the offset of the `last_wasm_entry_fp` field of `VMStoreContext`.
222227
fn vmstore_context_last_wasm_entry_fp(&self) -> u8 {
223-
self.vmstore_context_last_wasm_exit_pc() + self.size()
228+
self.vmstore_context_last_wasm_entry_sp() + self.size()
229+
}
230+
231+
/// Return the offset of the `last_wasm_entry_trap_handler` field of `VMStoreContext`.
232+
fn vmstore_context_last_wasm_entry_trap_handler(&self) -> u8 {
233+
self.vmstore_context_last_wasm_entry_fp() + self.size()
224234
}
225235

226236
/// Return the offset of the `stack_chain` field of `VMStoreContext`.
227237
fn vmstore_context_stack_chain(&self) -> u8 {
228-
self.vmstore_context_last_wasm_entry_fp() + self.size()
238+
self.vmstore_context_last_wasm_entry_trap_handler() + self.size()
229239
}
230240

231241
// Offsets within `VMMemoryDefinition`

crates/wasmtime/build.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,13 @@ fn build_c_helpers() {
8585
build.define("VERSIONED_SUFFIX", Some(versioned_suffix!()));
8686
if std::env::var("CARGO_FEATURE_DEBUG_BUILTINS").is_ok() {
8787
build.define("FEATURE_DEBUG_BUILTINS", None);
88-
}
89-
90-
// On MinGW targets work around a bug in the MinGW compiler described at
91-
// https://github.com/bytecodealliance/wasmtime/pull/9688#issuecomment-2573367719
92-
if cfg("windows") && cfg_is("target_env", "gnu") {
93-
build.define("__USE_MINGW_SETJMP_NON_SEH", None);
88+
} else if cfg("windows") {
89+
// If debug builtins are disabled and this target is for Windows then
90+
// there's no need to build the C helpers file.
91+
//
92+
// TODO: should skip this on Unix targets as well but needs a solution
93+
// for `wasmtime_using_libunwind`.
94+
return;
9495
}
9596

9697
println!("cargo:rerun-if-changed=src/runtime/vm/helpers.c");

0 commit comments

Comments
 (0)