Skip to content

Commit

Permalink
Pass 64-bit integers directly across the wasm ABI boundary (#3037)
Browse files Browse the repository at this point in the history
Previously, they were passed as a pair of `i32`s; this changes them to be passed as a single `i64`, which is directly converted to a bigint on the JS end.

I think they were previously passed as a pair because `wasm-bindgen`'s support for 64-bit integers predated the WebAssembly bigint integration that translates `i64`s to bigints. However, that feature has been around for quite a while now, so I think it should be fine to use.

I also bumped the schema version - although the schema itself hasn't changed, the ABI has, so using an older version of the CLI with this version of the `wasm-bindgen` crate or vice versa won't work and I want the version-mismatch warning to show up.

Fixes RReverser/serde-wasm-bindgen#28. This doesn't rely on a global `BigInt64Array` like the previous `i32`-pair version, which means that code using 64-bit integers will no longer fail at load time in browsers without bigint support.
  • Loading branch information
Liamolucko authored Aug 18, 2022
1 parent f197b0e commit bf109de
Show file tree
Hide file tree
Showing 11 changed files with 105 additions and 292 deletions.
158 changes: 54 additions & 104 deletions crates/cli-support/src/js/binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::js::Context;
use crate::wit::InstructionData;
use crate::wit::{Adapter, AdapterId, AdapterKind, AdapterType, Instruction};
use anyhow::{anyhow, bail, Error};
use walrus::Module;
use walrus::{Module, ValType};

/// A one-size-fits-all builder for processing WebIDL bindings and generating
/// JS.
Expand Down Expand Up @@ -437,6 +437,14 @@ impl<'a, 'b> JsBuilder<'a, 'b> {
self.prelude(&format!("_assertNum({});", arg));
}

fn assert_bigint(&mut self, arg: &str) {
if !self.cx.config.debug {
return;
}
self.cx.expose_assert_bigint();
self.prelude(&format!("_assertBigInt({});", arg));
}

fn assert_bool(&mut self, arg: &str) {
if !self.cx.config.debug {
return;
Expand All @@ -455,6 +463,16 @@ impl<'a, 'b> JsBuilder<'a, 'b> {
self.prelude("}");
}

fn assert_optional_bigint(&mut self, arg: &str) {
if !self.cx.config.debug {
return;
}
self.cx.expose_is_like_none();
self.prelude(&format!("if (!isLikeNone({})) {{", arg));
self.assert_bigint(arg);
self.prelude("}");
}

fn assert_optional_bool(&mut self, arg: &str) {
if !self.cx.config.debug {
return;
Expand Down Expand Up @@ -560,9 +578,18 @@ fn instruction(js: &mut JsBuilder, instr: &Instruction, log_error: &mut bool) ->
}
}

Instruction::Standard(wit_walrus::Instruction::IntToWasm { trap: false, .. }) => {
Instruction::Standard(wit_walrus::Instruction::IntToWasm {
trap: false, input, ..
}) => {
let val = js.pop();
js.assert_number(&val);
if matches!(
input,
wit_walrus::ValType::I64 | wit_walrus::ValType::S64 | wit_walrus::ValType::U64
) {
js.assert_bigint(&val);
} else {
js.assert_number(&val);
}
js.push(val);
}

Expand All @@ -577,6 +604,7 @@ fn instruction(js: &mut JsBuilder, instr: &Instruction, log_error: &mut bool) ->
let val = js.pop();
match output {
wit_walrus::ValType::U32 => js.push(format!("{} >>> 0", val)),
wit_walrus::ValType::U64 => js.push(format!("BigInt.asUintN(64, {val})")),
_ => js.push(val),
}
}
Expand Down Expand Up @@ -618,6 +646,7 @@ fn instruction(js: &mut JsBuilder, instr: &Instruction, log_error: &mut bool) ->
Instruction::StoreRetptr { ty, offset, mem } => {
let (mem, size) = match ty {
AdapterType::I32 => (js.cx.expose_int32_memory(*mem), 4),
AdapterType::I64 => (js.cx.expose_int64_memory(*mem), 8),
AdapterType::F32 => (js.cx.expose_f32_memory(*mem), 4),
AdapterType::F64 => (js.cx.expose_f64_memory(*mem), 8),
other => bail!("invalid aggregate return type {:?}", other),
Expand All @@ -639,6 +668,7 @@ fn instruction(js: &mut JsBuilder, instr: &Instruction, log_error: &mut bool) ->
Instruction::LoadRetptr { ty, offset, mem } => {
let (mem, quads) = match ty {
AdapterType::I32 => (js.cx.expose_int32_memory(*mem), 1),
AdapterType::I64 => (js.cx.expose_int64_memory(*mem), 2),
AdapterType::F32 => (js.cx.expose_f32_memory(*mem), 1),
AdapterType::F64 => (js.cx.expose_f64_memory(*mem), 2),
other => bail!("invalid aggregate return type {:?}", other),
Expand Down Expand Up @@ -712,52 +742,6 @@ fn instruction(js: &mut JsBuilder, instr: &Instruction, log_error: &mut bool) ->
js.push(format!("ptr{}", i));
}

Instruction::I32Split64 { signed } => {
let val = js.pop();
let f = if *signed {
js.cx.expose_int64_cvt_shim()
} else {
js.cx.expose_uint64_cvt_shim()
};
let i = js.tmp();
js.prelude(&format!(
"
{f}[0] = {val};
const low{i} = u32CvtShim[0];
const high{i} = u32CvtShim[1];
",
i = i,
f = f,
val = val,
));
js.push(format!("low{}", i));
js.push(format!("high{}", i));
}

Instruction::I32SplitOption64 { signed } => {
let val = js.pop();
js.cx.expose_is_like_none();
let f = if *signed {
js.cx.expose_int64_cvt_shim()
} else {
js.cx.expose_uint64_cvt_shim()
};
let i = js.tmp();
js.prelude(&format!(
"\
{f}[0] = isLikeNone({val}) ? BigInt(0) : {val};
const low{i} = u32CvtShim[0];
const high{i} = u32CvtShim[1];
",
i = i,
f = f,
val = val,
));
js.push(format!("!isLikeNone({0})", val));
js.push(format!("low{}", i));
js.push(format!("high{}", i));
}

Instruction::I32FromOptionExternref { table_and_alloc } => {
let val = js.pop();
js.cx.expose_is_like_none();
Expand Down Expand Up @@ -803,12 +787,19 @@ fn instruction(js: &mut JsBuilder, instr: &Instruction, log_error: &mut bool) ->
js.push(format!("isLikeNone({0}) ? {1} : {0}", val, hole));
}

Instruction::FromOptionNative { .. } => {
Instruction::FromOptionNative { ty } => {
let val = js.pop();
js.cx.expose_is_like_none();
js.assert_optional_number(&val);
if *ty == ValType::I64 {
js.assert_optional_bigint(&val);
} else {
js.assert_optional_number(&val);
}
js.push(format!("!isLikeNone({0})", val));
js.push(format!("isLikeNone({0}) ? 0 : {0}", val));
js.push(format!(
"isLikeNone({val}) ? 0{n} : {val}",
n = if *ty == ValType::I64 { "n" } else { "" }
));
}

Instruction::VectorToMemory { kind, malloc, mem } => {
Expand Down Expand Up @@ -1001,29 +992,6 @@ fn instruction(js: &mut JsBuilder, instr: &Instruction, log_error: &mut bool) ->
js.push(format!("String.fromCodePoint({})", val));
}

Instruction::I64FromLoHi { signed } => {
let f = if *signed {
js.cx.expose_int64_cvt_shim()
} else {
js.cx.expose_uint64_cvt_shim()
};
let i = js.tmp();
let high = js.pop();
let low = js.pop();
js.prelude(&format!(
"\
u32CvtShim[0] = {low};
u32CvtShim[1] = {high};
const n{i} = {f}[0];
",
low = low,
high = high,
f = f,
i = i,
));
js.push(format!("n{}", i))
}

Instruction::RustFromI32 { class } => {
js.cx.require_class_wrap(class);
let val = js.pop();
Expand Down Expand Up @@ -1183,14 +1151,21 @@ fn instruction(js: &mut JsBuilder, instr: &Instruction, log_error: &mut bool) ->
js.push(format!("{0} === 0xFFFFFF ? undefined : {0}", val));
}

Instruction::ToOptionNative { ty: _, signed } => {
Instruction::ToOptionNative { ty, signed } => {
let val = js.pop();
let present = js.pop();
js.push(format!(
"{} === 0 ? undefined : {}{}",
"{} === 0 ? undefined : {}",
present,
val,
if *signed { "" } else { " >>> 0" },
if *signed {
val
} else {
match ty {
ValType::I32 => format!("{val} >>> 0"),
ValType::I64 => format!("BigInt.asUintN(64, {val})"),
_ => unreachable!("unsigned non-integer"),
}
},
));
}

Expand All @@ -1211,31 +1186,6 @@ fn instruction(js: &mut JsBuilder, instr: &Instruction, log_error: &mut bool) ->
let val = js.pop();
js.push(format!("{0} === {1} ? undefined : {0}", val, hole));
}

Instruction::Option64FromI32 { signed } => {
let f = if *signed {
js.cx.expose_int64_cvt_shim()
} else {
js.cx.expose_uint64_cvt_shim()
};
let i = js.tmp();
let high = js.pop();
let low = js.pop();
let present = js.pop();
js.prelude(&format!(
"
u32CvtShim[0] = {low};
u32CvtShim[1] = {high};
const n{i} = {present} === 0 ? undefined : {f}[0];
",
present = present,
low = low,
high = high,
f = f,
i = i,
));
js.push(format!("n{}", i));
}
}
Ok(())
}
Expand Down
48 changes: 13 additions & 35 deletions crates/cli-support/src/js/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1126,6 +1126,19 @@ impl<'a> Context<'a> {
));
}

fn expose_assert_bigint(&mut self) {
if !self.should_write_global("assert_bigint") {
return;
}
self.global(&format!(
"
function _assertBigInt(n) {{
if (typeof(n) !== 'bigint') throw new Error('expected a bigint argument');
}}
"
));
}

fn expose_assert_bool(&mut self) {
if !self.should_write_global("assert_bool") {
return;
Expand Down Expand Up @@ -2036,41 +2049,6 @@ impl<'a> Context<'a> {
);
}

fn expose_u32_cvt_shim(&mut self) -> &'static str {
let name = "u32CvtShim";
if !self.should_write_global(name) {
return name;
}
self.global(&format!("const {} = new Uint32Array(2);", name));
name
}

fn expose_int64_cvt_shim(&mut self) -> &'static str {
let name = "int64CvtShim";
if !self.should_write_global(name) {
return name;
}
let n = self.expose_u32_cvt_shim();
self.global(&format!(
"const {} = new BigInt64Array({}.buffer);",
name, n
));
name
}

fn expose_uint64_cvt_shim(&mut self) -> &'static str {
let name = "uint64CvtShim";
if !self.should_write_global(name) {
return name;
}
let n = self.expose_u32_cvt_shim();
self.global(&format!(
"const {} = new BigUint64Array({}.buffer);",
name, n
));
name
}

fn expose_is_like_none(&mut self) {
if !self.should_write_global("is_like_none") {
return;
Expand Down
28 changes: 3 additions & 25 deletions crates/cli-support/src/wit/incoming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ impl InstructionBuilder<'_, '_> {
Descriptor::U16 => self.number(WitVT::U16, WasmVT::I32),
Descriptor::I32 => self.number(WitVT::S32, WasmVT::I32),
Descriptor::U32 => self.number(WitVT::U32, WasmVT::I32),
Descriptor::I64 => self.number64(true),
Descriptor::U64 => self.number64(false),
Descriptor::I64 => self.number(WitVT::S64, WasmVT::I64),
Descriptor::U64 => self.number(WitVT::U64, WasmVT::I64),
Descriptor::F32 => {
self.get(AdapterType::F32);
self.output.push(AdapterType::F32);
Expand Down Expand Up @@ -256,17 +256,7 @@ impl InstructionBuilder<'_, '_> {
Descriptor::U32 => self.in_option_native(ValType::I32),
Descriptor::F32 => self.in_option_native(ValType::F32),
Descriptor::F64 => self.in_option_native(ValType::F64),
Descriptor::I64 | Descriptor::U64 => {
let (signed, ty) = match arg {
Descriptor::I64 => (true, AdapterType::S64.option()),
_ => (false, AdapterType::U64.option()),
};
self.instruction(
&[ty],
Instruction::I32SplitOption64 { signed },
&[AdapterType::I32, AdapterType::I32, AdapterType::I32],
);
}
Descriptor::I64 | Descriptor::U64 => self.in_option_native(ValType::I64),
Descriptor::Boolean => {
self.instruction(
&[AdapterType::Bool.option()],
Expand Down Expand Up @@ -396,18 +386,6 @@ impl InstructionBuilder<'_, '_> {
);
}

fn number64(&mut self, signed: bool) {
self.instruction(
&[if signed {
AdapterType::S64
} else {
AdapterType::U64
}],
Instruction::I32Split64 { signed },
&[AdapterType::I32, AdapterType::I32],
);
}

fn in_option_native(&mut self, wasm: ValType) {
let ty = AdapterType::from_wasm(wasm).unwrap();
self.instruction(
Expand Down
2 changes: 1 addition & 1 deletion crates/cli-support/src/wit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1646,7 +1646,7 @@ impl StructUnpacker {
fn read_ty(&mut self, ty: &AdapterType) -> Result<usize, Error> {
let (quads, alignment) = match ty {
AdapterType::I32 | AdapterType::U32 | AdapterType::F32 => (1, 1),
AdapterType::F64 => (2, 2),
AdapterType::I64 | AdapterType::U64 | AdapterType::F64 => (2, 2),
other => bail!("invalid aggregate return type {:?}", other),
};
Ok(self.append(quads, alignment))
Expand Down
Loading

0 comments on commit bf109de

Please sign in to comment.