Skip to content

Commit 7c7cd51

Browse files
Optimized ABI performance for Option<[all 32-bit primitives]> (#4183)
1 parent e37a1dd commit 7c7cd51

File tree

8 files changed

+223
-56
lines changed

8 files changed

+223
-56
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
* Deprecate `autofocus`, `tabIndex`, `focus()` and `blur()` bindings in favor of bindings on the inherited `Element` class.
2323
[#4143](https://github.com/rustwasm/wasm-bindgen/pull/4143)
2424

25+
* Optimized ABI performance for `Option<{i32,u32,isize,usize,f32,*const T,*mut T}>`.
26+
[#4183](https://github.com/rustwasm/wasm-bindgen/pull/4183)
27+
2528
### Fixed
2629

2730
* Fixed methods with `self: &Self` consuming the object.

crates/cli-support/src/js/binding.rs

+60-4
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ pub struct JsBuilder<'a, 'b> {
3737
/// JS functions, etc.
3838
cx: &'a mut Context<'b>,
3939

40+
/// A debug name for the function being generated, used for error messages
41+
debug_name: &'a str,
42+
4043
/// The "prelude" of the function, or largely just the JS function we've
4144
/// built so far.
4245
prelude: String,
@@ -121,6 +124,7 @@ impl<'a, 'b> Builder<'a, 'b> {
121124
asyncness: bool,
122125
variadic: bool,
123126
generate_jsdoc: bool,
127+
debug_name: &str,
124128
) -> Result<JsFunction, Error> {
125129
if self
126130
.cx
@@ -138,7 +142,7 @@ impl<'a, 'b> Builder<'a, 'b> {
138142
// If this is a method then we're generating this as part of a class
139143
// method, so the leading parameter is the this pointer stored on
140144
// the JS object, so synthesize that here.
141-
let mut js = JsBuilder::new(self.cx);
145+
let mut js = JsBuilder::new(self.cx, debug_name);
142146
if let Some(consumes_self) = self.method {
143147
let _ = params.next();
144148
if js.cx.config.debug {
@@ -184,7 +188,12 @@ impl<'a, 'b> Builder<'a, 'b> {
184188
)?;
185189
}
186190

187-
assert_eq!(js.stack.len(), adapter.results.len());
191+
assert_eq!(
192+
js.stack.len(),
193+
adapter.results.len(),
194+
"stack size mismatch for {}",
195+
debug_name
196+
);
188197
match js.stack.len() {
189198
0 => {}
190199
1 => {
@@ -422,9 +431,10 @@ impl<'a, 'b> Builder<'a, 'b> {
422431
}
423432

424433
impl<'a, 'b> JsBuilder<'a, 'b> {
425-
pub fn new(cx: &'a mut Context<'b>) -> JsBuilder<'a, 'b> {
434+
pub fn new(cx: &'a mut Context<'b>, debug_name: &'a str) -> JsBuilder<'a, 'b> {
426435
JsBuilder {
427436
cx,
437+
debug_name,
428438
args: Vec::new(),
429439
tmp: 0,
430440
pre_try: String::new(),
@@ -463,7 +473,10 @@ impl<'a, 'b> JsBuilder<'a, 'b> {
463473
}
464474

465475
fn pop(&mut self) -> String {
466-
self.stack.pop().unwrap()
476+
match self.stack.pop() {
477+
Some(s) => s,
478+
None => panic!("popping an empty stack in {}", self.debug_name),
479+
}
467480
}
468481

469482
fn push(&mut self, arg: String) {
@@ -920,6 +933,44 @@ fn instruction(
920933
js.push(format!("isLikeNone({0}) ? {1} : {0}", val, hole));
921934
}
922935

936+
Instruction::F64FromOptionSentinelInt { signed } => {
937+
let val = js.pop();
938+
js.cx.expose_is_like_none();
939+
js.assert_optional_number(&val);
940+
941+
// We need to convert the given number to a 32-bit integer before
942+
// passing it to the ABI for 2 reasons:
943+
// 1. Rust's behavior for `value_f64 as i32/u32` is different from
944+
// the WebAssembly behavior for values outside the 32-bit range.
945+
// We could implement this behavior in Rust too, but it's easier
946+
// to do it in JS.
947+
// 2. If we allowed values outside the 32-bit range, the sentinel
948+
// value itself would be allowed. This would make it impossible
949+
// to distinguish between the sentinel value and a valid value.
950+
//
951+
// To perform the actual conversion, we use JS bit shifts. Handily,
952+
// >> and >>> perform a conversion to i32 and u32 respectively
953+
// to apply the bit shift, so we can use e.g. x >>> 0 to convert to
954+
// u32.
955+
956+
let op = if *signed { ">>" } else { ">>>" };
957+
js.push(format!("isLikeNone({val}) ? 0x100000001 : ({val}) {op} 0"));
958+
}
959+
Instruction::F64FromOptionSentinelF32 => {
960+
let val = js.pop();
961+
js.cx.expose_is_like_none();
962+
js.assert_optional_number(&val);
963+
964+
// Similar to the above 32-bit integer variant, we convert the
965+
// number to a 32-bit *float* before passing it to the ABI. This
966+
// ensures consistent behavior with WebAssembly and makes it
967+
// possible to use a sentinel value.
968+
969+
js.push(format!(
970+
"isLikeNone({val}) ? 0x100000001 : Math.fround({val})"
971+
));
972+
}
973+
923974
Instruction::FromOptionNative { ty } => {
924975
let val = js.pop();
925976
js.cx.expose_is_like_none();
@@ -1277,6 +1328,11 @@ fn instruction(
12771328
));
12781329
}
12791330

1331+
Instruction::OptionF64Sentinel => {
1332+
let val = js.pop();
1333+
js.push(format!("{0} === 0x100000001 ? undefined : {0}", val));
1334+
}
1335+
12801336
Instruction::OptionU32Sentinel => {
12811337
let val = js.pop();
12821338
js.push(format!("{0} === 0xFFFFFF ? undefined : {0}", val));

crates/cli-support/src/js/mod.rs

+12-15
Original file line numberDiff line numberDiff line change
@@ -2655,6 +2655,16 @@ __wbg_set_wasm(wasm);"
26552655
ContextAdapterKind::Adapter => {}
26562656
}
26572657

2658+
// an internal debug name to help with error messages
2659+
let debug_name = match kind {
2660+
ContextAdapterKind::Import(i) => {
2661+
let i = builder.cx.module.imports.get(i);
2662+
format!("import of `{}::{}`", i.module, i.name)
2663+
}
2664+
ContextAdapterKind::Export(e) => format!("`{}`", e.debug_name),
2665+
ContextAdapterKind::Adapter => format!("adapter {}", id.0),
2666+
};
2667+
26582668
// Process the `binding` and generate a bunch of JS/TypeScript/etc.
26592669
let binding::JsFunction {
26602670
ts_sig,
@@ -2674,22 +2684,9 @@ __wbg_set_wasm(wasm);"
26742684
asyncness,
26752685
variadic,
26762686
generate_jsdoc,
2687+
&debug_name,
26772688
)
2678-
.with_context(|| match kind {
2679-
ContextAdapterKind::Export(e) => {
2680-
format!("failed to generate bindings for `{}`", e.debug_name)
2681-
}
2682-
ContextAdapterKind::Import(i) => {
2683-
let i = builder.cx.module.imports.get(i);
2684-
format!(
2685-
"failed to generate bindings for import of `{}::{}`",
2686-
i.module, i.name
2687-
)
2688-
}
2689-
ContextAdapterKind::Adapter => {
2690-
"failed to generates bindings for adapter".to_string()
2691-
}
2692-
})?;
2689+
.with_context(|| "failed to generates bindings for ".to_string() + &debug_name)?;
26932690

26942691
self.typescript_refs.extend(ts_refs);
26952692

crates/cli-support/src/wit/incoming.rs

+22-8
Original file line numberDiff line numberDiff line change
@@ -276,13 +276,13 @@ impl InstructionBuilder<'_, '_> {
276276
&[AdapterType::I32],
277277
);
278278
}
279-
Descriptor::I8 => self.in_option_sentinel(AdapterType::S8),
280-
Descriptor::U8 => self.in_option_sentinel(AdapterType::U8),
281-
Descriptor::I16 => self.in_option_sentinel(AdapterType::S16),
282-
Descriptor::U16 => self.in_option_sentinel(AdapterType::U16),
283-
Descriptor::I32 => self.in_option_native(ValType::I32),
284-
Descriptor::U32 => self.in_option_native(ValType::I32),
285-
Descriptor::F32 => self.in_option_native(ValType::F32),
279+
Descriptor::I8 => self.in_option_sentinel32(AdapterType::S8),
280+
Descriptor::U8 => self.in_option_sentinel32(AdapterType::U8),
281+
Descriptor::I16 => self.in_option_sentinel32(AdapterType::S16),
282+
Descriptor::U16 => self.in_option_sentinel32(AdapterType::U16),
283+
Descriptor::I32 => self.in_option_sentinel64_int(AdapterType::I32, true),
284+
Descriptor::U32 => self.in_option_sentinel64_int(AdapterType::U32, false),
285+
Descriptor::F32 => self.in_option_sentinel64_f32(AdapterType::F32),
286286
Descriptor::F64 => self.in_option_native(ValType::F64),
287287
Descriptor::I64 | Descriptor::U64 => self.in_option_native(ValType::I64),
288288
Descriptor::Boolean => {
@@ -459,11 +459,25 @@ impl InstructionBuilder<'_, '_> {
459459
);
460460
}
461461

462-
fn in_option_sentinel(&mut self, ty: AdapterType) {
462+
fn in_option_sentinel32(&mut self, ty: AdapterType) {
463463
self.instruction(
464464
&[ty.option()],
465465
Instruction::I32FromOptionU32Sentinel,
466466
&[AdapterType::I32],
467467
);
468468
}
469+
fn in_option_sentinel64_int(&mut self, ty: AdapterType, signed: bool) {
470+
self.instruction(
471+
&[ty.option()],
472+
Instruction::F64FromOptionSentinelInt { signed },
473+
&[AdapterType::F64],
474+
);
475+
}
476+
fn in_option_sentinel64_f32(&mut self, ty: AdapterType) {
477+
self.instruction(
478+
&[ty.option()],
479+
Instruction::F64FromOptionSentinelF32,
480+
&[AdapterType::F64],
481+
);
482+
}
469483
}

crates/cli-support/src/wit/outgoing.rs

+16-8
Original file line numberDiff line numberDiff line change
@@ -257,15 +257,15 @@ impl InstructionBuilder<'_, '_> {
257257
&[AdapterType::NamedExternref(name.clone()).option()],
258258
);
259259
}
260-
Descriptor::I8 => self.out_option_sentinel(AdapterType::S8),
261-
Descriptor::U8 => self.out_option_sentinel(AdapterType::U8),
262-
Descriptor::I16 => self.out_option_sentinel(AdapterType::S16),
263-
Descriptor::U16 => self.out_option_sentinel(AdapterType::U16),
264-
Descriptor::I32 => self.option_native(true, ValType::I32),
265-
Descriptor::U32 => self.option_native(false, ValType::I32),
260+
Descriptor::I8 => self.out_option_sentinel32(AdapterType::S8),
261+
Descriptor::U8 => self.out_option_sentinel32(AdapterType::U8),
262+
Descriptor::I16 => self.out_option_sentinel32(AdapterType::S16),
263+
Descriptor::U16 => self.out_option_sentinel32(AdapterType::U16),
264+
Descriptor::I32 => self.out_option_sentinel64(AdapterType::S32),
265+
Descriptor::U32 => self.out_option_sentinel64(AdapterType::U32),
266266
Descriptor::I64 => self.option_native(true, ValType::I64),
267267
Descriptor::U64 => self.option_native(false, ValType::I64),
268-
Descriptor::F32 => self.option_native(true, ValType::F32),
268+
Descriptor::F32 => self.out_option_sentinel64(AdapterType::F32),
269269
Descriptor::F64 => self.option_native(true, ValType::F64),
270270
Descriptor::Boolean => {
271271
self.instruction(
@@ -576,11 +576,19 @@ impl InstructionBuilder<'_, '_> {
576576
);
577577
}
578578

579-
fn out_option_sentinel(&mut self, ty: AdapterType) {
579+
fn out_option_sentinel32(&mut self, ty: AdapterType) {
580580
self.instruction(
581581
&[AdapterType::I32],
582582
Instruction::OptionU32Sentinel,
583583
&[ty.option()],
584584
);
585585
}
586+
587+
fn out_option_sentinel64(&mut self, ty: AdapterType) {
588+
self.instruction(
589+
&[AdapterType::F64],
590+
Instruction::OptionF64Sentinel,
591+
&[ty.option()],
592+
);
593+
}
586594
}

crates/cli-support/src/wit/standard.rs

+10
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,14 @@ pub enum Instruction {
214214
I32FromOptionEnum {
215215
hole: u32,
216216
},
217+
/// Pops an `externref` from the stack, pushes either a sentinel value if it's
218+
/// "none" or the integer value of it if it's "some"
219+
F64FromOptionSentinelInt {
220+
signed: bool,
221+
},
222+
/// Pops an `externref` from the stack, pushes either a sentinel value if it's
223+
/// "none" or the f32 value of it if it's "some"
224+
F64FromOptionSentinelF32,
217225
/// Pops any externref from the stack and then pushes two values. First is a
218226
/// 0/1 if it's none/some and second is `ty` value if it was there or 0 if
219227
/// it wasn't there.
@@ -324,6 +332,8 @@ pub enum Instruction {
324332
kind: VectorKind,
325333
mem: walrus::MemoryId,
326334
},
335+
/// pops f64, pushes it viewed as an optional value with a known sentinel
336+
OptionF64Sentinel,
327337
/// pops i32, pushes it viewed as an optional value with a known sentinel
328338
OptionU32Sentinel,
329339
/// pops an i32, then `ty`, then pushes externref

0 commit comments

Comments
 (0)