From 4db501d28bbb6e48de903a877a5aa4bd7a89b512 Mon Sep 17 00:00:00 2001 From: Ingvar Stepanyan Date: Fri, 2 Sep 2022 14:31:18 +0100 Subject: [PATCH] Add `TryFrom` for `[iu](64|128)` Adds a way to convert `JsValue` or a `BigInt` to `i64`/`u64`/`i128`/`u128` with type and range checks, returning the original `JsValue` otherwise. This could be optimised a little bit further via more intrinsics, but it's good enough for the initial implementation, so leaving any optimisations for the future. Fixes #2350. --- crates/cli-support/src/intrinsic.rs | 7 ++++ crates/cli-support/src/js/mod.rs | 6 +++ crates/js-sys/src/lib.rs | 13 +++++- src/lib.rs | 63 ++++++++++++++++++++++++++++- tests/wasm/bigint.rs | 29 +++++++++++++ 5 files changed, 115 insertions(+), 3 deletions(-) diff --git a/crates/cli-support/src/intrinsic.rs b/crates/cli-support/src/intrinsic.rs index c8d4436a451..8563369525f 100644 --- a/crates/cli-support/src/intrinsic.rs +++ b/crates/cli-support/src/intrinsic.rs @@ -80,6 +80,10 @@ fn opt_f64() -> Descriptor { Descriptor::Option(Box::new(Descriptor::F64)) } +fn opt_i64() -> Descriptor { + Descriptor::Option(Box::new(Descriptor::I64)) +} + intrinsics! { pub enum Intrinsic { #[symbol = "__wbindgen_jsval_eq"] @@ -208,6 +212,9 @@ intrinsics! { #[symbol = "__wbindgen_bigint_from_u128"] #[signature = fn(U64, U64) -> Externref] BigIntFromU128, + #[symbol = "__wbindgen_bigint_get_as_i64"] + #[signature = fn(ref_externref()) -> opt_i64()] + BigIntGetAsI64, #[symbol = "__wbindgen_string_new"] #[signature = fn(ref_string()) -> Externref] StringNew, diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs index 760708191de..fb14e70910a 100644 --- a/crates/cli-support/src/js/mod.rs +++ b/crates/cli-support/src/js/mod.rs @@ -3384,6 +3384,12 @@ impl<'a> Context<'a> { format!("typeof(v) === 'boolean' ? (v ? 1 : 0) : 2") } + Intrinsic::BigIntGetAsI64 => { + assert_eq!(args.len(), 1); + prelude.push_str(&format!("const v = {};\n", args[0])); + format!("typeof(v) === 'bigint' ? v : undefined") + } + Intrinsic::Throw => { assert_eq!(args.len(), 1); format!("throw new Error({})", args[0]) diff --git a/crates/js-sys/src/lib.rs b/crates/js-sys/src/lib.rs index 1b5bfa592aa..12fef1cb7f7 100644 --- a/crates/js-sys/src/lib.rs +++ b/crates/js-sys/src/lib.rs @@ -20,7 +20,7 @@ use core::ops::{Add, BitAnd, BitOr, BitXor, Div, Mul, Neg, Not, Rem, Shl, Shr, Sub}; use std::cmp::Ordering; -use std::convert::{self, Infallible}; +use std::convert::{self, Infallible, TryFrom}; use std::f64; use std::fmt; use std::iter::{self, Product, Sum}; @@ -1046,7 +1046,7 @@ macro_rules! bigint_from { impl PartialEq<$x> for BigInt { #[inline] fn eq(&self, other: &$x) -> bool { - JsValue::from(self) == BigInt::from(*other).unchecked_into::() + JsValue::from(self) == JsValue::from(BigInt::from(*other)) } } )*) @@ -1068,6 +1068,15 @@ macro_rules! bigint_from_big { self == &BigInt::from(*other) } } + + impl TryFrom for $x { + type Error = BigInt; + + #[inline] + fn try_from(x: BigInt) -> Result { + Self::try_from(JsValue::from(x)).map_err(JsCast::unchecked_into) + } + } )*) } bigint_from_big!(i64 u64 i128 u128); diff --git a/src/lib.rs b/src/lib.rs index b286dca3f5e..e53ee90402e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,7 +18,7 @@ use core::ops::{ }; use core::u32; -use crate::convert::{FromWasmAbi, WasmOptionalF64, WasmSlice}; +use crate::convert::{FromWasmAbi, WasmOptionalF64, WasmOptionalI64, WasmSlice}; macro_rules! if_std { ($($i:item)*) => ($( @@ -885,6 +885,66 @@ macro_rules! big_numbers { )*) } +macro_rules! try_from_for_num64 { + ($ty:ty) => { + impl TryFrom for $ty { + type Error = JsValue; + + #[inline] + fn try_from(v: JsValue) -> Result { + if let WasmOptionalI64 { + present: 1, + value: as_i64, + } = unsafe { __wbindgen_bigint_get_as_i64(v.idx) } + { + // Reinterpret bits; ABI-wise this is safe to do and allows us to avoid + // having separate intrinsics per signed/unsigned types. + let as_self = as_i64 as Self; + // Double-check that we didn't truncate the bigint to 64 bits. + if v == as_self { + return Ok(as_self); + } + } + // Not a bigint or not in range. + Err(v) + } + } + }; +} + +try_from_for_num64!(i64); +try_from_for_num64!(u64); + +macro_rules! try_from_for_num128 { + ($ty:ty, $hi_ty:ty) => { + impl TryFrom for $ty { + type Error = JsValue; + + #[inline] + fn try_from(v: JsValue) -> Result { + // Truncate the bigint to 64 bits, this will give us the lower part. + let lo = unsafe { __wbindgen_bigint_get_as_i64(v.idx) }; + if lo.present == 0 { + // Not a bigint. + return Err(v); + } + // The lower part must be interpreted as unsigned in both i128 and u128. + let lo = lo.value as u64; + // Now we know it's a bigint, so we can safely use `>> 64n` without + // worrying about a JS exception on type mismatch. + let hi = v >> JsValue::from(64_u64); + // The high part is the one we want checked against a 64-bit range. + // If it fits, then our original number is in the 128-bit range. + let hi = <$hi_ty>::try_from(hi)?; + Ok(Self::from(hi) << 64 | Self::from(lo)) + } + } + }; +} + +try_from_for_num128!(i128, i64); +try_from_for_num128!(u128, u64); + big_numbers! { |n|, i64 = __wbindgen_bigint_from_i64(n), @@ -979,6 +1039,7 @@ externs! { fn __wbindgen_number_get(idx: u32) -> WasmOptionalF64; fn __wbindgen_boolean_get(idx: u32) -> u32; fn __wbindgen_string_get(idx: u32) -> WasmSlice; + fn __wbindgen_bigint_get_as_i64(idx: u32) -> WasmOptionalI64; fn __wbindgen_debug_string(ret: *mut [usize; 2], idx: u32) -> (); diff --git a/tests/wasm/bigint.rs b/tests/wasm/bigint.rs index 7f5714e2b06..30eb5c5cf7d 100644 --- a/tests/wasm/bigint.rs +++ b/tests/wasm/bigint.rs @@ -87,3 +87,32 @@ pub fn u64_slice(a: &[u64]) -> Vec { fn works() { js_works(); } + +mod try_from_works { + use super::*; + use crate::JsValue; + use core::convert::TryFrom; + + macro_rules! test_type_boundaries { + ($($ty:ident)*) => { + $( + #[wasm_bindgen_test] + fn $ty() { + // Not a bigint. + assert!($ty::try_from(JsValue::NULL).is_err()); + assert!($ty::try_from(JsValue::from_f64(0.0)).is_err()); + // Within range. + assert_eq!($ty::try_from(JsValue::from($ty::MIN)), Ok($ty::MIN)); + // Too small. + assert!($ty::try_from(JsValue::from($ty::MIN) - JsValue::from(1_i64)).is_err()); + // Within range. + assert_eq!($ty::try_from(JsValue::from($ty::MAX)), Ok($ty::MAX)); + // Too large. + assert!($ty::try_from(JsValue::from($ty::MAX) + JsValue::from(1_i64)).is_err()); + } + )* + }; + } + + test_type_boundaries!(i64 u64 i128 u128); +}