From e3736396c9be2c0c6ca53d304531b40b7d8ff8ae Mon Sep 17 00:00:00 2001 From: Leo Kettmeir Date: Thu, 2 Jan 2025 04:57:32 -0800 Subject: [PATCH] feat: WebIDL derive macro (#1003) - adds a `WebIdlConverter` trait - implemented for Vec, to have sequence converters - implemented for Option, to have nullable converters - implemented for HashMap, to have record converters - implemented for integers - implemented for floats, and creates `Unrestricted` structs for unrestricted float and unrestricted double - implemented for booleans - implemented for string - creates new `ByteString` struct for bytestring conversion - creates new `BigInt` struct for bigint conversion - adds a `WebIDL` derive macro, current only allowed on structs as a dictionary converter and on enums as enum converters --- Cargo.lock | 21 +- Cargo.toml | 3 +- core/Cargo.toml | 2 +- core/error.rs | 9 +- core/lib.rs | 5 +- core/runtime/mod.rs | 2 +- core/runtime/v8_static_strings.rs | 3 +- core/webidl.rs | 1403 +++++++++++++++++ dcore/Cargo.toml | 1 + ops/Cargo.toml | 1 + ops/compile_test_runner/src/lib.rs | 29 +- ops/lib.rs | 9 + ops/op2/dispatch_fast.rs | 6 +- ops/op2/dispatch_slow.rs | 74 +- ops/op2/signature.rs | 80 +- ops/op2/test_cases/sync/object_wrap.out | 4 +- ops/op2/test_cases/sync/webidl.out | 130 ++ ops/op2/test_cases/sync/webidl.rs | 8 + ops/webidl/dictionary.rs | 360 +++++ ops/webidl/enum.rs | 103 ++ ops/webidl/mod.rs | 219 +++ ops/webidl/test_cases/dict.out | 310 ++++ ops/webidl/test_cases/dict.rs | 18 + ops/webidl/test_cases/enum.out | 40 + ops/webidl/test_cases/enum.rs | 12 + ops/webidl/test_cases_fail/enum_fields.rs | 10 + ops/webidl/test_cases_fail/enum_fields.stderr | 5 + 27 files changed, 2814 insertions(+), 53 deletions(-) create mode 100644 core/webidl.rs create mode 100644 ops/op2/test_cases/sync/webidl.out create mode 100644 ops/op2/test_cases/sync/webidl.rs create mode 100644 ops/webidl/dictionary.rs create mode 100644 ops/webidl/enum.rs create mode 100644 ops/webidl/mod.rs create mode 100644 ops/webidl/test_cases/dict.out create mode 100644 ops/webidl/test_cases/dict.rs create mode 100644 ops/webidl/test_cases/enum.out create mode 100644 ops/webidl/test_cases/enum.rs create mode 100644 ops/webidl/test_cases_fail/enum_fields.rs create mode 100644 ops/webidl/test_cases_fail/enum_fields.stderr diff --git a/Cargo.lock b/Cargo.lock index b336305bd..f9b3416c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -485,7 +485,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -650,6 +650,7 @@ dependencies = [ name = "deno_ops" version = "0.203.0" dependencies = [ + "indexmap", "pretty_assertions", "prettyplease", "proc-macro-rules", @@ -991,6 +992,12 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + [[package]] name = "heck" version = "0.4.1" @@ -1018,7 +1025,7 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96274be293b8877e61974a607105d09c84caebe9620b47774aa8a6b942042dd4" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", "new_debug_unreachable", "once_cell", "phf", @@ -1128,12 +1135,12 @@ checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" [[package]] name = "indexmap" -version = "2.2.6" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.2", ] [[package]] @@ -2748,9 +2755,9 @@ dependencies = [ [[package]] name = "v8" -version = "130.0.2" +version = "130.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee0be58935708fa4d7efb970c6cf9f2d9511d24ee24246481a65b6ee167348d" +checksum = "8b61316a57fcd7e5f3840fe085f13e6dfd37e92d73b040033d2f598c7a1984c3" dependencies = [ "bindgen", "bitflags", diff --git a/Cargo.toml b/Cargo.toml index b7952d5f9..2a6865bb2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ serde_v8 = { version = "0.236.0", path = "./serde_v8" } deno_ast = { version = "=0.40.0", features = ["transpiling"] } deno_core_icudata = "0.74.0" deno_unsync = "0.4.1" -v8 = { version = "130.0.2", default-features = false } +v8 = { version = "130.0.4", default-features = false } anyhow = "1" bencher = "0.1" @@ -40,6 +40,7 @@ cooked-waker = "5" criterion = "0.5" fastrand = "2" futures = "0.3.21" +indexmap = "2.1.0" libc = "0.2.126" memoffset = ">=0.9" num-bigint = { version = "0.4", features = ["rand"] } diff --git a/core/Cargo.toml b/core/Cargo.toml index b7cfdbe7e..fe92837ca 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -35,7 +35,7 @@ deno_core_icudata = { workspace = true, optional = true } deno_ops.workspace = true deno_unsync.workspace = true futures.workspace = true -indexmap = "2.1.0" +indexmap.workspace = true libc.workspace = true memoffset.workspace = true parking_lot.workspace = true diff --git a/core/error.rs b/core/error.rs index 1a2763638..9b6d699ae 100644 --- a/core/error.rs +++ b/core/error.rs @@ -97,7 +97,14 @@ impl std::error::Error for CustomError {} /// If this error was crated with `custom_error()`, return the specified error /// class name. In all other cases this function returns `None`. pub fn get_custom_error_class(error: &Error) -> Option<&'static str> { - error.downcast_ref::().map(|e| e.class) + error + .downcast_ref::() + .map(|e| e.class) + .or_else(|| { + error + .downcast_ref::() + .map(|_| "TypeError") + }) } /// A wrapper around `anyhow::Error` that implements `std::error::Error` diff --git a/core/lib.rs b/core/lib.rs index 11ce501d0..344465ad9 100644 --- a/core/lib.rs +++ b/core/lib.rs @@ -32,9 +32,12 @@ mod runtime; mod source_map; mod tasks; mod web_timeout; +pub mod webidl; // Re-exports pub use anyhow; +pub use deno_ops::op2; +pub use deno_ops::WebIDL; pub use deno_unsync as unsync; pub use futures; pub use parking_lot; @@ -51,8 +54,6 @@ pub use sourcemap; pub use url; pub use v8; -pub use deno_ops::op2; - pub use crate::async_cancel::CancelFuture; pub use crate::async_cancel::CancelHandle; pub use crate::async_cancel::CancelTryFuture; diff --git a/core/runtime/mod.rs b/core/runtime/mod.rs index c01bdc809..f6616569e 100644 --- a/core/runtime/mod.rs +++ b/core/runtime/mod.rs @@ -10,7 +10,7 @@ pub mod ops_rust_to_v8; mod setup; mod snapshot; pub mod stats; -pub(crate) mod v8_static_strings; +pub mod v8_static_strings; #[cfg(all(test, not(miri)))] mod tests; diff --git a/core/runtime/v8_static_strings.rs b/core/runtime/v8_static_strings.rs index 1bdc0a750..e448283f2 100644 --- a/core/runtime/v8_static_strings.rs +++ b/core/runtime/v8_static_strings.rs @@ -1,4 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +#[macro_export] macro_rules! v8_static_strings { ($($ident:ident = $str:literal),* $(,)?) => { $( @@ -7,7 +8,7 @@ macro_rules! v8_static_strings { }; } -pub(crate) use v8_static_strings; +pub use v8_static_strings; v8_static_strings!( BUILD_CUSTOM_ERROR = "buildCustomError", diff --git a/core/webidl.rs b/core/webidl.rs new file mode 100644 index 000000000..e9174f188 --- /dev/null +++ b/core/webidl.rs @@ -0,0 +1,1403 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::borrow::Cow; +use std::collections::HashMap; +use v8::HandleScope; +use v8::Local; +use v8::Value; + +#[derive(Debug)] +pub struct WebIdlError { + pub prefix: Cow<'static, str>, + pub context: Cow<'static, str>, + pub kind: WebIdlErrorKind, +} + +impl WebIdlError { + pub fn new( + prefix: Cow<'static, str>, + context: &impl Fn() -> Cow<'static, str>, + kind: WebIdlErrorKind, + ) -> Self { + Self { + prefix, + context: context(), + kind, + } + } + + pub fn other( + prefix: Cow<'static, str>, + context: &impl Fn() -> Cow<'static, str>, + other: T, + ) -> Self { + Self::new(prefix, context, WebIdlErrorKind::Other(Box::new(other))) + } +} + +impl std::fmt::Display for WebIdlError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}: {} ", self.prefix, self.context)?; + + match &self.kind { + WebIdlErrorKind::ConvertToConverterType(kind) => { + write!(f, "can not be converted to a {kind}") + } + WebIdlErrorKind::DictionaryCannotConvertKey { converter, key } => { + write!( + f, + "can not be converted to '{converter}' because '{key}' is required in '{converter}'", + ) + } + WebIdlErrorKind::NotFinite => write!(f, "is not a finite number"), + WebIdlErrorKind::IntRange { lower_bound, upper_bound } => write!(f, "is outside the accepted range of ${lower_bound} to ${upper_bound}, inclusive"), + WebIdlErrorKind::InvalidByteString => write!(f, "is not a valid ByteString"), + WebIdlErrorKind::Precision => write!(f, "is outside the range of a single-precision floating-point value"), + WebIdlErrorKind::InvalidEnumVariant { converter, variant } => write!(f, "can not be converted to '{converter}' because '{variant}' is not a valid enum value"), + WebIdlErrorKind::Other(other) => std::fmt::Display::fmt(other, f), + } + } +} + +impl std::error::Error for WebIdlError {} + +#[derive(Debug)] +pub enum WebIdlErrorKind { + ConvertToConverterType(&'static str), + DictionaryCannotConvertKey { + converter: &'static str, + key: &'static str, + }, + NotFinite, + IntRange { + lower_bound: f64, + upper_bound: f64, + }, + Precision, + InvalidByteString, + InvalidEnumVariant { + converter: &'static str, + variant: String, + }, + Other(Box), +} + +pub trait WebIdlConverter<'a>: Sized { + type Options: Default; + + fn convert( + scope: &mut HandleScope<'a>, + value: Local<'a, Value>, + prefix: Cow<'static, str>, + context: C, + options: &Self::Options, + ) -> Result + where + C: Fn() -> Cow<'static, str>; +} + +// any converter +impl<'a> WebIdlConverter<'a> for Local<'a, Value> { + type Options = (); + + fn convert( + _scope: &mut HandleScope<'a>, + value: Local<'a, Value>, + _prefix: Cow<'static, str>, + _context: C, + _options: &Self::Options, + ) -> Result + where + C: Fn() -> Cow<'static, str>, + { + Ok(value) + } +} + +// nullable converter +impl<'a, T: WebIdlConverter<'a>> WebIdlConverter<'a> for Option { + type Options = T::Options; + + fn convert( + scope: &mut HandleScope<'a>, + value: Local<'a, Value>, + prefix: Cow<'static, str>, + context: C, + options: &Self::Options, + ) -> Result + where + C: Fn() -> Cow<'static, str>, + { + if value.is_null_or_undefined() { + Ok(None) + } else { + Ok(Some(WebIdlConverter::convert( + scope, value, prefix, context, options, + )?)) + } + } +} + +crate::v8_static_strings! { + NEXT = "next", + DONE = "done", + VALUE = "value", +} + +thread_local! { + static NEXT_ETERNAL: v8::Eternal = v8::Eternal::empty(); + static DONE_ETERNAL: v8::Eternal = v8::Eternal::empty(); + static VALUE_ETERNAL: v8::Eternal = v8::Eternal::empty(); +} + +// sequence converter +impl<'a, T: WebIdlConverter<'a>> WebIdlConverter<'a> for Vec { + type Options = T::Options; + + fn convert( + scope: &mut HandleScope<'a>, + value: Local<'a, Value>, + prefix: Cow<'static, str>, + context: C, + options: &Self::Options, + ) -> Result + where + C: Fn() -> Cow<'static, str>, + { + let Some(obj) = value.to_object(scope) else { + return Err(WebIdlError::new( + prefix, + &context, + WebIdlErrorKind::ConvertToConverterType("sequence"), + )); + }; + + let iter_key = v8::Symbol::get_iterator(scope); + let Some(iter) = obj + .get(scope, iter_key.into()) + .and_then(|iter| iter.try_cast::().ok()) + .and_then(|iter| iter.call(scope, obj.cast(), &[])) + .and_then(|iter| iter.to_object(scope)) + else { + return Err(WebIdlError::new( + prefix, + &context, + WebIdlErrorKind::ConvertToConverterType("sequence"), + )); + }; + + let mut out = vec![]; + + let next_key = NEXT_ETERNAL + .with(|eternal| { + if let Some(key) = eternal.get(scope) { + Ok(key) + } else { + let key = NEXT + .v8_string(scope) + .map_err(|e| WebIdlError::other(prefix.clone(), &context, e))?; + eternal.set(scope, key); + Ok(key) + } + })? + .into(); + + let done_key = DONE_ETERNAL + .with(|eternal| { + if let Some(key) = eternal.get(scope) { + Ok(key) + } else { + let key = DONE + .v8_string(scope) + .map_err(|e| WebIdlError::other(prefix.clone(), &context, e))?; + eternal.set(scope, key); + Ok(key) + } + })? + .into(); + + let value_key = VALUE_ETERNAL + .with(|eternal| { + if let Some(key) = eternal.get(scope) { + Ok(key) + } else { + let key = VALUE + .v8_string(scope) + .map_err(|e| WebIdlError::other(prefix.clone(), &context, e))?; + eternal.set(scope, key); + Ok(key) + } + })? + .into(); + + loop { + let Some(res) = iter + .get(scope, next_key) + .and_then(|next| next.try_cast::().ok()) + .and_then(|next| next.call(scope, iter.cast(), &[])) + .and_then(|res| res.to_object(scope)) + else { + return Err(WebIdlError::new( + prefix, + &context, + WebIdlErrorKind::ConvertToConverterType("sequence"), + )); + }; + + if res.get(scope, done_key).is_some_and(|val| val.is_true()) { + break; + } + + let Some(iter_val) = res.get(scope, value_key) else { + return Err(WebIdlError::new( + prefix, + &context, + WebIdlErrorKind::ConvertToConverterType("sequence"), + )); + }; + + out.push(WebIdlConverter::convert( + scope, + iter_val, + prefix.clone(), + || format!("{}, index {}", context(), out.len()).into(), + options, + )?); + } + + Ok(out) + } +} + +// record converter +// the Options only apply to the value, not the key +impl< + 'a, + K: WebIdlConverter<'a> + Eq + std::hash::Hash, + V: WebIdlConverter<'a>, + > WebIdlConverter<'a> for HashMap +{ + type Options = V::Options; + + fn convert( + scope: &mut HandleScope<'a>, + value: Local<'a, Value>, + prefix: Cow<'static, str>, + context: C, + options: &Self::Options, + ) -> Result + where + C: Fn() -> Cow<'static, str>, + { + let Ok(obj) = value.try_cast::() else { + return Err(WebIdlError::new( + prefix, + &context, + WebIdlErrorKind::ConvertToConverterType("record"), + )); + }; + + let obj = if let Ok(proxy) = obj.try_cast::() { + if let Ok(obj) = proxy.get_target(scope).try_cast() { + obj + } else { + return Ok(Default::default()); + } + } else { + obj + }; + + let Some(keys) = obj.get_own_property_names( + scope, + v8::GetPropertyNamesArgs { + mode: v8::KeyCollectionMode::OwnOnly, + property_filter: Default::default(), + index_filter: v8::IndexFilter::IncludeIndices, + key_conversion: v8::KeyConversionMode::ConvertToString, + }, + ) else { + return Ok(Default::default()); + }; + + let mut out = HashMap::with_capacity(keys.length() as _); + + for i in 0..keys.length() { + let key = keys.get_index(scope, i).unwrap(); + let value = obj.get(scope, key).unwrap(); + + let key = WebIdlConverter::convert( + scope, + key, + prefix.clone(), + &context, + &Default::default(), + )?; + let value = WebIdlConverter::convert( + scope, + value, + prefix.clone(), + &context, + options, + )?; + + out.insert(key, value); + } + + Ok(out) + } +} + +#[derive(Debug, Default)] +pub struct IntOptions { + pub clamp: bool, + pub enforce_range: bool, +} + +// https://webidl.spec.whatwg.org/#abstract-opdef-converttoint +macro_rules! impl_ints { + ($($t:ty: $unsigned:tt = $name:literal: $min:expr => $max:expr),*) => { + $( + impl<'a> WebIdlConverter<'a> for $t { + type Options = IntOptions; + + #[allow(clippy::manual_range_contains)] + fn convert( + scope: &mut HandleScope<'a>, + value: Local<'a, Value>, + prefix: Cow<'static, str>, + context: C, + options: &Self::Options, + ) -> Result + where + C: Fn() -> Cow<'static, str>, + { + const MIN: f64 = $min as f64; + const MAX: f64 = $max as f64; + + if value.is_big_int() { + return Err(WebIdlError::new(prefix, &context, WebIdlErrorKind::ConvertToConverterType($name))); + } + + let Some(mut n) = value.number_value(scope) else { + return Err(WebIdlError::new(prefix, &context, WebIdlErrorKind::ConvertToConverterType($name))); + }; + if n == -0.0 { + n = 0.0; + } + + if options.enforce_range { + if !n.is_finite() { + return Err(WebIdlError::new(prefix, &context, WebIdlErrorKind::NotFinite)); + } + + n = n.trunc(); + if n == -0.0 { + n = 0.0; + } + + if n < MIN || n > MAX { + return Err(WebIdlError::new(prefix, &context, WebIdlErrorKind::IntRange { + lower_bound: MIN, + upper_bound: MAX, + })); + } + + return Ok(n as Self); + } + + if !n.is_nan() && options.clamp { + return Ok( + n.clamp(MIN, MAX) + .round_ties_even() as Self + ); + } + + if !n.is_finite() || n == 0.0 { + return Ok(0); + } + + n = n.trunc(); + if n == -0.0 { + n = 0.0; + } + + if n >= MIN && n <= MAX { + return Ok(n as Self); + } + + let bit_len_num = 2.0f64.powi(Self::BITS as i32); + + n = { + let sign_might_not_match = n % bit_len_num; + if n.is_sign_positive() != bit_len_num.is_sign_positive() { + sign_might_not_match + bit_len_num + } else { + sign_might_not_match + } + }; + + impl_ints!(@handle_unsigned $unsigned n bit_len_num); + + Ok(n as Self) + } + } + )* + }; + + (@handle_unsigned false $n:ident $bit_len_num:ident) => { + if $n >= MAX { + return Ok(($n - $bit_len_num) as Self); + } + }; + + (@handle_unsigned true $n:ident $bit_len_num:ident) => {}; +} + +// https://webidl.spec.whatwg.org/#js-integer-types +impl_ints!( + i8: false = "byte": i8::MIN => i8::MAX, + u8: true = "octet": u8::MIN => u8::MAX, + i16: false = "short": i16::MIN => i16::MAX, + u16: true = "unsigned short": u16::MIN => u16::MAX, + i32: false = "long": i32::MIN => i32::MAX, + u32: true = "unsigned long": u32::MIN => u32::MAX, + i64: false = "long long": ((-2i64).pow(53) + 1) => (2i64.pow(53) - 1), + u64: true = "unsigned long long": u64::MIN => (2u64.pow(53) - 1) +); + +// float +impl<'a> WebIdlConverter<'a> for f32 { + type Options = (); + + fn convert( + scope: &mut HandleScope<'a>, + value: Local<'a, Value>, + prefix: Cow<'static, str>, + context: C, + _options: &Self::Options, + ) -> Result + where + C: Fn() -> Cow<'static, str>, + { + let Some(n) = value.number_value(scope) else { + return Err(WebIdlError::new( + prefix, + &context, + WebIdlErrorKind::ConvertToConverterType("float"), + )); + }; + + if !n.is_finite() { + return Err(WebIdlError::new( + prefix, + &context, + WebIdlErrorKind::NotFinite, + )); + } + + let n = n as f32; + + if !n.is_finite() { + return Err(WebIdlError::new( + prefix, + &context, + WebIdlErrorKind::Precision, + )); + } + + Ok(n) + } +} + +#[derive(Debug, Copy, Clone)] +pub struct UnrestrictedFloat(pub f32); +impl std::ops::Deref for UnrestrictedFloat { + type Target = f32; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'a> WebIdlConverter<'a> for UnrestrictedFloat { + type Options = (); + + fn convert( + scope: &mut HandleScope<'a>, + value: Local<'a, Value>, + prefix: Cow<'static, str>, + context: C, + _options: &Self::Options, + ) -> Result + where + C: Fn() -> Cow<'static, str>, + { + let Some(n) = value.number_value(scope) else { + return Err(WebIdlError::new( + prefix, + &context, + WebIdlErrorKind::ConvertToConverterType("float"), + )); + }; + + Ok(UnrestrictedFloat(n as f32)) + } +} + +// double +impl<'a> WebIdlConverter<'a> for f64 { + type Options = (); + + fn convert( + scope: &mut HandleScope<'a>, + value: Local<'a, Value>, + prefix: Cow<'static, str>, + context: C, + _options: &Self::Options, + ) -> Result + where + C: Fn() -> Cow<'static, str>, + { + let Some(n) = value.number_value(scope) else { + return Err(WebIdlError::new( + prefix, + &context, + WebIdlErrorKind::ConvertToConverterType("float"), + )); + }; + + if !n.is_finite() { + return Err(WebIdlError::new( + prefix, + &context, + WebIdlErrorKind::NotFinite, + )); + } + + Ok(n) + } +} + +#[derive(Debug, Copy, Clone)] +pub struct UnrestrictedDouble(pub f64); +impl std::ops::Deref for UnrestrictedDouble { + type Target = f64; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'a> WebIdlConverter<'a> for UnrestrictedDouble { + type Options = (); + + fn convert( + scope: &mut HandleScope<'a>, + value: Local<'a, Value>, + prefix: Cow<'static, str>, + context: C, + _options: &Self::Options, + ) -> Result + where + C: Fn() -> Cow<'static, str>, + { + let Some(n) = value.number_value(scope) else { + return Err(WebIdlError::new( + prefix, + &context, + WebIdlErrorKind::ConvertToConverterType("float"), + )); + }; + + Ok(UnrestrictedDouble(n)) + } +} + +#[derive(Debug)] +pub struct BigInt { + pub sign: bool, + pub words: Vec, +} + +impl<'a> WebIdlConverter<'a> for BigInt { + type Options = (); + + fn convert( + scope: &mut HandleScope<'a>, + value: Local<'a, Value>, + prefix: Cow<'static, str>, + context: C, + _options: &Self::Options, + ) -> Result + where + C: Fn() -> Cow<'static, str>, + { + let Some(bigint) = value.to_big_int(scope) else { + return Err(WebIdlError::new( + prefix, + &context, + WebIdlErrorKind::ConvertToConverterType("bigint"), + )); + }; + + let mut words = vec![]; + let (sign, _) = bigint.to_words_array(&mut words); + Ok(Self { sign, words }) + } +} + +impl<'a> WebIdlConverter<'a> for bool { + type Options = (); + + fn convert( + scope: &mut HandleScope<'a>, + value: Local<'a, Value>, + _prefix: Cow<'static, str>, + _context: C, + _options: &Self::Options, + ) -> Result + where + C: Fn() -> Cow<'static, str>, + { + Ok(value.to_boolean(scope).is_true()) + } +} + +#[derive(Debug, Default)] +pub struct StringOptions { + treat_null_as_empty_string: bool, +} + +// DOMString and USVString, since we treat them the same +impl<'a> WebIdlConverter<'a> for String { + type Options = StringOptions; + + fn convert( + scope: &mut HandleScope<'a>, + value: Local<'a, Value>, + prefix: Cow<'static, str>, + context: C, + options: &Self::Options, + ) -> Result + where + C: Fn() -> Cow<'static, str>, + { + let str = if value.is_string() { + value.try_cast::().unwrap() + } else if value.is_null() && options.treat_null_as_empty_string { + return Ok(String::new()); + } else if value.is_symbol() { + return Err(WebIdlError::new( + prefix, + &context, + WebIdlErrorKind::ConvertToConverterType("string"), + )); + } else if let Some(str) = value.to_string(scope) { + str + } else { + return Err(WebIdlError::new( + prefix, + &context, + WebIdlErrorKind::ConvertToConverterType("string"), + )); + }; + + Ok(str.to_rust_string_lossy(scope)) + } +} + +#[derive(Debug, Clone)] +pub struct ByteString(pub String); +impl std::ops::Deref for ByteString { + type Target = String; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl<'a> WebIdlConverter<'a> for ByteString { + type Options = StringOptions; + + fn convert( + scope: &mut HandleScope<'a>, + value: Local<'a, Value>, + prefix: Cow<'static, str>, + context: C, + options: &Self::Options, + ) -> Result + where + C: Fn() -> Cow<'static, str>, + { + let str = if value.is_string() { + value.try_cast::().unwrap() + } else if value.is_null() && options.treat_null_as_empty_string { + return Ok(Self(String::new())); + } else if value.is_symbol() { + return Err(WebIdlError::new( + prefix, + &context, + WebIdlErrorKind::ConvertToConverterType("string"), + )); + } else if let Some(str) = value.to_string(scope) { + str + } else { + return Err(WebIdlError::new( + prefix, + &context, + WebIdlErrorKind::ConvertToConverterType("string"), + )); + }; + + if !str.contains_only_onebyte() { + return Err(WebIdlError::new( + prefix, + &context, + WebIdlErrorKind::InvalidByteString, + )); + } + + Ok(Self(str.to_rust_string_lossy(scope))) + } +} + +// TODO: +// object +// ArrayBuffer +// DataView +// Array buffer types +// ArrayBufferView + +#[cfg(all(test, not(miri)))] +mod tests { + use super::*; + use crate::JsRuntime; + + #[test] + fn integers() { + let mut runtime = JsRuntime::new(Default::default()); + let scope = &mut runtime.handle_scope(); + + macro_rules! test_integer { + ($t:ty: $($val:expr => $expected:literal$(, $opts:expr)?);+;) => { + $( + let val = v8::Number::new(scope, $val as f64); + let converted = <$t>::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &test_integer!(@opts $($opts)?), + ); + assert_eq!(converted.unwrap(), $expected); + )+ + }; + + ($t:ty: $($val:expr => ERR$(, $opts:expr)?);+;) => { + $( + let val = v8::Number::new(scope, $val as f64); + let converted = <$t>::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &test_integer!(@opts $($opts)?), + ); + assert!(converted.is_err()); + )+ + }; + + (@opts $opts:expr) => { $opts }; + (@opts) => { Default::default() }; + } + + test_integer!( + i8: + 50 => 50; + -10 => -10; + 130 => -126; + -130 => 126; + 130 => 127, IntOptions { clamp: true, enforce_range: false }; + ); + test_integer!( + i8: + f64::INFINITY => ERR, IntOptions { clamp: false, enforce_range: true }; + -f64::INFINITY => ERR, IntOptions { clamp: false, enforce_range: true }; + f64::NAN => ERR, IntOptions { clamp: false, enforce_range: true }; + 130 => ERR, IntOptions { clamp: false, enforce_range: true }; + ); + + test_integer!( + u8: + 50 => 50; + -10 => 246; + 260 => 4; + 260 => 255, IntOptions { clamp: true, enforce_range: false }; + ); + test_integer!( + u8: + f64::INFINITY => ERR, IntOptions { clamp: false, enforce_range: true }; + f64::NAN => ERR, IntOptions { clamp: false, enforce_range: true }; + 260 => ERR, IntOptions { clamp: false, enforce_range: true }; + ); + + let val = v8::String::new(scope, "3").unwrap(); + let converted = u8::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert_eq!(converted.unwrap(), 3); + + let val = v8::String::new(scope, "test").unwrap(); + let converted = u8::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert_eq!(converted.unwrap(), 0); + + let val = v8::BigInt::new_from_i64(scope, 0); + let converted = u8::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert!(converted.is_err()); + + let val = v8::Symbol::new(scope, None); + let converted = u8::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert!(converted.is_err()); + + let val = v8::undefined(scope); + let converted = u8::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert_eq!(converted.unwrap(), 0); + } + + #[test] + fn float() { + let mut runtime = JsRuntime::new(Default::default()); + let scope = &mut runtime.handle_scope(); + + let val = v8::Number::new(scope, 3.0); + let converted = f32::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert_eq!(converted.unwrap(), 3.0); + + let val = v8::Number::new(scope, f64::INFINITY); + let converted = f32::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert!(converted.is_err()); + + let val = v8::Number::new(scope, f64::MAX); + let converted = f32::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert!(converted.is_err()); + } + + #[test] + fn unrestricted_float() { + let mut runtime = JsRuntime::new(Default::default()); + let scope = &mut runtime.handle_scope(); + + let val = v8::Number::new(scope, 3.0); + let converted = UnrestrictedFloat::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert_eq!(*converted.unwrap(), 3.0); + + let val = v8::Number::new(scope, f32::INFINITY as f64); + let converted = UnrestrictedFloat::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert_eq!(*converted.unwrap(), f32::INFINITY); + + let val = v8::Number::new(scope, f64::NAN); + let converted = UnrestrictedFloat::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + + assert!(converted.unwrap().is_nan()); + + let val = v8::Number::new(scope, f64::MAX); + let converted = UnrestrictedFloat::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert!(converted.unwrap().is_infinite()); + } + + #[test] + fn double() { + let mut runtime = JsRuntime::new(Default::default()); + let scope = &mut runtime.handle_scope(); + + let val = v8::Number::new(scope, 3.0); + let converted = f64::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert_eq!(converted.unwrap(), 3.0); + + let val = v8::Number::new(scope, f64::INFINITY); + let converted = f64::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert!(converted.is_err()); + + let val = v8::Number::new(scope, f64::MAX); + let converted = f64::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert_eq!(converted.unwrap(), f64::MAX); + } + + #[test] + fn unrestricted_double() { + let mut runtime = JsRuntime::new(Default::default()); + let scope = &mut runtime.handle_scope(); + + let val = v8::Number::new(scope, 3.0); + let converted = UnrestrictedDouble::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert_eq!(*converted.unwrap(), 3.0); + + let val = v8::Number::new(scope, f64::INFINITY); + let converted = UnrestrictedDouble::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert_eq!(*converted.unwrap(), f64::INFINITY); + + let val = v8::Number::new(scope, f64::NAN); + let converted = UnrestrictedDouble::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + + assert!(converted.unwrap().is_nan()); + + let val = v8::Number::new(scope, f64::MAX); + let converted = UnrestrictedDouble::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert_eq!(*converted.unwrap(), f64::MAX); + } + + #[test] + fn string() { + let mut runtime = JsRuntime::new(Default::default()); + let scope = &mut runtime.handle_scope(); + + let val = v8::String::new(scope, "foo").unwrap(); + let converted = String::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert_eq!(converted.unwrap(), "foo"); + + let val = v8::Number::new(scope, 1.0); + let converted = String::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert_eq!(converted.unwrap(), "1"); + + let val = v8::Symbol::new(scope, None); + let converted = String::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert!(converted.is_err()); + + let val = v8::null(scope); + let converted = String::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert_eq!(converted.unwrap(), "null"); + + let val = v8::null(scope); + let converted = String::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &StringOptions { + treat_null_as_empty_string: true, + }, + ); + assert_eq!(converted.unwrap(), ""); + + let val = v8::Object::new(scope); + let converted = String::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &StringOptions { + treat_null_as_empty_string: true, + }, + ); + assert_eq!(converted.unwrap(), "[object Object]"); + + let val = v8::String::new(scope, "生").unwrap(); + let converted = String::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert_eq!(converted.unwrap(), "生"); + } + + #[test] + fn byte_string() { + let mut runtime = JsRuntime::new(Default::default()); + let scope = &mut runtime.handle_scope(); + + let val = v8::String::new(scope, "foo").unwrap(); + let converted = ByteString::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert_eq!(*converted.unwrap(), "foo"); + + let val = v8::Number::new(scope, 1.0); + let converted = ByteString::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert_eq!(*converted.unwrap(), "1"); + + let val = v8::Symbol::new(scope, None); + let converted = ByteString::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert!(converted.is_err()); + + let val = v8::null(scope); + let converted = ByteString::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert_eq!(*converted.unwrap(), "null"); + + let val = v8::null(scope); + let converted = ByteString::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &StringOptions { + treat_null_as_empty_string: true, + }, + ); + assert_eq!(*converted.unwrap(), ""); + + let val = v8::Object::new(scope); + let converted = ByteString::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &StringOptions { + treat_null_as_empty_string: true, + }, + ); + assert_eq!(*converted.unwrap(), "[object Object]"); + + let val = v8::String::new(scope, "生").unwrap(); + let converted = ByteString::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert!(converted.is_err()); + } + + #[test] + fn any() { + let mut runtime = JsRuntime::new(Default::default()); + let scope = &mut runtime.handle_scope(); + + let val = v8::Object::new(scope); + let converted = v8::Local::::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert!(converted.unwrap().is_object()); + } + + #[test] + fn sequence() { + let mut runtime = JsRuntime::new(Default::default()); + let scope = &mut runtime.handle_scope(); + + let a = v8::Number::new(scope, 1.0); + let b = v8::String::new(scope, "2").unwrap(); + let val = v8::Array::new_with_elements(scope, &[a.into(), b.into()]); + let converted = Vec::::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert_eq!(converted.unwrap(), vec![1, 2]); + } + + #[test] + fn nullable() { + let mut runtime = JsRuntime::new(Default::default()); + let scope = &mut runtime.handle_scope(); + + let val = v8::undefined(scope); + let converted = Option::::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert_eq!(converted.unwrap(), None); + + let val = v8::Number::new(scope, 1.0); + let converted = Option::::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert_eq!(converted.unwrap(), Some(1)); + } + + #[test] + fn record() { + let mut runtime = JsRuntime::new(Default::default()); + let scope = &mut runtime.handle_scope(); + + let key = v8::String::new(scope, "foo").unwrap(); + let val = v8::Number::new(scope, 1.0); + let obj = v8::Object::new(scope); + obj.set(scope, key.into(), val.into()); + + let converted = HashMap::::convert( + scope, + obj.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert_eq!( + converted.unwrap(), + HashMap::from([(String::from("foo"), 1)]) + ); + } + + #[test] + fn dictionary() { + #[derive(deno_ops::WebIDL, Debug, Eq, PartialEq)] + #[webidl(dictionary)] + pub struct Dict { + a: u8, + #[options(clamp = true)] + b: Vec, + #[webidl(default = Some(3))] + c: Option, + #[webidl(rename = "e")] + d: u16, + f: HashMap, + #[webidl(required)] + g: Option, + } + + let mut runtime = JsRuntime::new(Default::default()); + let val = runtime + .execute_script( + "", + "({ a: 1, b: [70000], e: 70000, f: { 'foo': 1 }, g: undefined })", + ) + .unwrap(); + + let scope = &mut runtime.handle_scope(); + let val = Local::new(scope, val); + + let converted = Dict::convert( + scope, + val, + "prefix".into(), + || "context".into(), + &Default::default(), + ); + + assert_eq!( + converted.unwrap(), + Dict { + a: 1, + b: vec![65535], + c: Some(3), + d: 4464, + f: HashMap::from([(String::from("foo"), 1)]), + g: None, + } + ); + } + + #[test] + fn r#enum() { + #[derive(deno_ops::WebIDL, Debug, Eq, PartialEq)] + #[webidl(enum)] + pub enum Enumeration { + FooBar, + Baz, + #[webidl(rename = "hello")] + World, + } + + let mut runtime = JsRuntime::new(Default::default()); + let scope = &mut runtime.handle_scope(); + + let val = v8::String::new(scope, "foo-bar").unwrap(); + let converted = Enumeration::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert_eq!(converted.unwrap(), Enumeration::FooBar); + + let val = v8::String::new(scope, "baz").unwrap(); + let converted = Enumeration::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert_eq!(converted.unwrap(), Enumeration::Baz); + + let val = v8::String::new(scope, "hello").unwrap(); + let converted = Enumeration::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert_eq!(converted.unwrap(), Enumeration::World); + + let val = v8::String::new(scope, "unknown").unwrap(); + let converted = Enumeration::convert( + scope, + val.into(), + "prefix".into(), + || "context".into(), + &Default::default(), + ); + assert!(converted.is_err()); + } +} diff --git a/dcore/Cargo.toml b/dcore/Cargo.toml index bf02f8fb6..cd6cbbdb5 100644 --- a/dcore/Cargo.toml +++ b/dcore/Cargo.toml @@ -4,6 +4,7 @@ name = "dcore" version = "0.1.0" authors.workspace = true +default-run = "dcore" edition.workspace = true license.workspace = true publish = false diff --git a/ops/Cargo.toml b/ops/Cargo.toml index 5d18fffe5..8446d0324 100644 --- a/ops/Cargo.toml +++ b/ops/Cargo.toml @@ -15,6 +15,7 @@ path = "./lib.rs" proc-macro = true [dependencies] +indexmap.workspace = true proc-macro-rules.workspace = true proc-macro2.workspace = true quote.workspace = true diff --git a/ops/compile_test_runner/src/lib.rs b/ops/compile_test_runner/src/lib.rs index ed6e3619a..0666717db 100644 --- a/ops/compile_test_runner/src/lib.rs +++ b/ops/compile_test_runner/src/lib.rs @@ -2,14 +2,17 @@ #[macro_export] macro_rules! prelude { () => { + #[allow(unused_imports)] use deno_ops::op2; + #[allow(unused_imports)] + use deno_ops::WebIDL; pub fn main() {} }; } #[cfg(test)] -mod tests { +mod op2_tests { use std::path::PathBuf; // TODO(mmastrac): It's faster to do things with testing_macros::fixture? @@ -40,3 +43,27 @@ mod tests { } } } + +#[cfg(test)] +mod webidl_tests { + #[rustversion::nightly] + #[test] + fn compile_test_all() { + // Run all the tests on a nightly build (which should take advantage of cargo's --keep-going to + // run in parallel: https://github.com/dtolnay/trybuild/pull/168) + let t = trybuild::TestCases::new(); + t.pass("../webidl/test_cases/*.rs"); + t.compile_fail("../webidl/test_cases_fail/*.rs"); + } + + #[rustversion::not(nightly)] + #[test] + fn compile_test_all() { + // Run all the tests if we're in the CI + if let Ok(true) = std::env::var("CI").map(|s| s == "true") { + let t = trybuild::TestCases::new(); + t.compile_fail("../webidl/test_cases_fail/*.rs"); + t.pass("../webidl/test_cases/*.rs"); + } + } +} diff --git a/ops/lib.rs b/ops/lib.rs index 2b7d4c9cb..938a8e4c2 100644 --- a/ops/lib.rs +++ b/ops/lib.rs @@ -5,6 +5,7 @@ use proc_macro::TokenStream; use std::error::Error; mod op2; +mod webidl; /// A macro designed to provide an extremely fast V8->Rust interface layer. #[doc = include_str!("op2/README.md")] @@ -31,3 +32,11 @@ fn op2_macro(attr: TokenStream, item: TokenStream) -> TokenStream { } } } + +#[proc_macro_derive(WebIDL, attributes(webidl, options))] +pub fn webidl(item: TokenStream) -> TokenStream { + match webidl::webidl(item.into()) { + Ok(output) => output.into(), + Err(err) => err.into_compile_error().into(), + } +} diff --git a/ops/op2/dispatch_fast.rs b/ops/op2/dispatch_fast.rs index c8fb19ede..3c952113d 100644 --- a/ops/op2/dispatch_fast.rs +++ b/ops/op2/dispatch_fast.rs @@ -903,6 +903,7 @@ fn map_arg_to_v8_fastcall_type( | Arg::OptionBuffer(..) | Arg::SerdeV8(_) | Arg::FromV8(_) + | Arg::WebIDL(_, _) | Arg::Ref(..) => return Ok(None), // We don't support v8 global arguments Arg::V8Global(_) | Arg::OptionV8Global(_) => return Ok(None), @@ -953,7 +954,10 @@ fn map_retval_to_v8_fastcall_type( arg: &Arg, ) -> Result, V8MappingError> { let rv = match arg { - Arg::OptionNumeric(..) | Arg::SerdeV8(_) | Arg::ToV8(_) => return Ok(None), + Arg::OptionNumeric(..) + | Arg::SerdeV8(_) + | Arg::ToV8(_) + | Arg::WebIDL(_, _) => return Ok(None), Arg::Void => V8FastCallType::Void, Arg::Numeric(NumericArg::bool, _) => V8FastCallType::Bool, Arg::Numeric(NumericArg::u32, _) diff --git a/ops/op2/dispatch_slow.rs b/ops/op2/dispatch_slow.rs index 3e3ec1b84..e7eee170d 100644 --- a/ops/op2/dispatch_slow.rs +++ b/ops/op2/dispatch_slow.rs @@ -21,6 +21,7 @@ use super::signature::RefType; use super::signature::RetVal; use super::signature::Special; use super::signature::Strings; +use super::signature::WebIDLPairs; use super::V8MappingError; use super::V8SignatureMappingError; use proc_macro2::Ident; @@ -234,6 +235,22 @@ pub(crate) fn with_fn_args( ) } +pub(crate) fn get_prefix(generator_state: &mut GeneratorState) -> String { + if generator_state.needs_self { + format!( + "Failed to execute '{}' on '{}'", + generator_state.name, generator_state.self_ty + ) + } else if generator_state.use_this_cppgc { + format!("Failed to construct '{}'", generator_state.self_ty) + } else { + format!( + "Failed to execute '{}.{}'", + generator_state.self_ty, generator_state.name + ) + } +} + pub(crate) fn with_required_check( generator_state: &mut GeneratorState, required: u8, @@ -245,24 +262,12 @@ pub(crate) fn with_required_check( "argument" }; - let prefix = if generator_state.needs_self { - format!( - "Failed to execute '{}' on '{}': ", - generator_state.name, generator_state.self_ty - ) - } else if generator_state.use_this_cppgc { - format!("Failed to construct '{}': ", generator_state.self_ty) - } else { - format!( - "Failed to execute '{}.{}': ", - generator_state.self_ty, generator_state.name - ) - }; + let prefix = get_prefix(generator_state); gs_quote!(generator_state(fn_args, scope) => (if #fn_args.length() < #required as i32 { let msg = format!( - "{}{} {} required, but only {} present", + "{}: {} {} required, but only {} present", #prefix, #required, #arguments_lit, @@ -646,6 +651,47 @@ pub fn from_arg( }; } } + Arg::WebIDL(ty, options) => { + *needs_scope = true; + let ty = + syn::parse_str::(ty).expect("Failed to reparse state type"); + let scope = scope.clone(); + let err = format_ident!("{}_err", arg_ident); + let throw_exception = throw_type_error_string(generator_state, &err)?; + let prefix = get_prefix(generator_state); + let context = format!("Argument {}", index + 1); + + let options = if options.is_empty() { + quote!(Default::default()) + } else { + let inner = options + .iter() + .map(|WebIDLPairs(k, v)| quote!(#k: #v)) + .collect::>(); + + quote! { + <#ty as deno_core::webidl::WebIdlConverter>::Options { + #(#inner),* + ..Default::default() + } + } + }; + + quote! { + let #arg_ident = match <#ty as deno_core::webidl::WebIdlConverter>::convert( + &mut #scope, + #arg_ident, + #prefix.into(), + || std::borrow::Cow::Borrowed(#context), + &#options, + ) { + Ok(t) => t, + Err(#err) => { + #throw_exception; + } + }; + } + } Arg::CppGcResource(ty) => { *needs_scope = true; let scope = scope.clone(); diff --git a/ops/op2/signature.rs b/ops/op2/signature.rs index a2261f3c9..8aa67cfcc 100644 --- a/ops/op2/signature.rs +++ b/ops/op2/signature.rs @@ -1,5 +1,6 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use proc_macro2::Ident; +use proc_macro2::Literal; use proc_macro2::Span; use proc_macro2::TokenStream; use proc_macro_rules::rules; @@ -247,6 +248,16 @@ pub enum NumericFlag { Number, } +// its own struct to facility Eq & PartialEq on other structs +#[derive(Clone, Debug)] +pub struct WebIDLPairs(pub Ident, pub Literal); +impl PartialEq for WebIDLPairs { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 && self.1.to_string() == other.1.to_string() + } +} +impl Eq for WebIDLPairs {} + /// Args are not a 1:1 mapping with Rust types, rather they represent broad classes of types that /// tend to have similar argument handling characteristics. This may need one more level of indirection /// given how many of these types have option variants, however. @@ -278,6 +289,7 @@ pub enum Arg { OptionCppGcResource(String), FromV8(String), ToV8(String), + WebIDL(String, Vec), VarArgs, } @@ -732,7 +744,7 @@ pub enum BufferSource { Any, } -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum AttributeModifier { /// #[serde], for serde_v8 types. Serde, @@ -740,6 +752,8 @@ pub enum AttributeModifier { ToV8, /// #[from_v8] for types that impl `FromV8` FromV8, + /// #[webidl], for types that impl `WebIdlConverter` + WebIDL(Vec), /// #[smi], for non-integral ID types representing small integers (-2³¹ and 2³¹-1 on 64-bit platforms, /// see https://medium.com/fhinkel/v8-internals-how-small-is-a-small-integer-e0badc18b6da). Smi, @@ -773,6 +787,7 @@ impl AttributeModifier { AttributeModifier::Buffer(..) => "buffer", AttributeModifier::Smi => "smi", AttributeModifier::Serde => "serde", + AttributeModifier::WebIDL(_) => "webidl", AttributeModifier::String(_) => "string", AttributeModifier::State => "state", AttributeModifier::Global => "global", @@ -873,7 +888,7 @@ pub enum RetError { AttributeError(#[from] AttributeError), } -#[derive(Copy, Clone, Default)] +#[derive(Clone, Default)] pub(crate) struct Attributes { primary: Option, } @@ -1198,7 +1213,7 @@ fn parse_attributes( return Ok(Attributes::default()); } Ok(Attributes { - primary: Some(*attrs.first().unwrap()), + primary: Some((*attrs.first().unwrap()).clone()), }) } @@ -1231,6 +1246,8 @@ fn parse_attribute( (#[bigint]) => Some(AttributeModifier::Bigint), (#[number]) => Some(AttributeModifier::Number), (#[serde]) => Some(AttributeModifier::Serde), + (#[webidl]) => Some(AttributeModifier::WebIDL(vec![])), + (#[webidl($($key: ident = $value: literal),*)]) => Some(AttributeModifier::WebIDL(key.into_iter().zip(value.into_iter()).map(|v| WebIDLPairs(v.0, v.1)).collect())), (#[smi]) => Some(AttributeModifier::Smi), (#[string]) => Some(AttributeModifier::String(StringMode::Default)), (#[string(onebyte)]) => Some(AttributeModifier::String(StringMode::OneByte)), @@ -1357,10 +1374,10 @@ fn parse_type_path( ( v8 :: Local < $( $_scope:lifetime , )? v8 :: $v8:ident $(,)? >) => Ok(CV8Local(TV8(parse_v8_type(&v8)?))), ( v8 :: Global < $( $_scope:lifetime , )? v8 :: $v8:ident $(,)? >) => Ok(CV8Global(TV8(parse_v8_type(&v8)?))), ( v8 :: $v8:ident ) => Ok(CBare(TV8(parse_v8_type(&v8)?))), - ( $( std :: rc :: )? Rc < RefCell < $ty:ty $(,)? > $(,)? > ) => Ok(CRcRefCell(TSpecial(parse_type_special(position, attrs, &ty)?))), - ( $( std :: rc :: )? Rc < $ty:ty $(,)? > ) => Ok(CRc(TSpecial(parse_type_special(position, attrs, &ty)?))), + ( $( std :: rc :: )? Rc < RefCell < $ty:ty $(,)? > $(,)? > ) => Ok(CRcRefCell(TSpecial(parse_type_special(position, attrs.clone(), &ty)?))), + ( $( std :: rc :: )? Rc < $ty:ty $(,)? > ) => Ok(CRc(TSpecial(parse_type_special(position, attrs.clone(), &ty)?))), ( Option < $ty:ty $(,)? > ) => { - match parse_type(position, attrs, &ty)? { + match parse_type(position, attrs.clone(), &ty)? { Arg::Special(special) => Ok(COption(TSpecial(special))), Arg::String(string) => Ok(COption(TString(string))), Arg::Numeric(numeric, _) => Ok(COption(TNumeric(numeric))), @@ -1528,7 +1545,7 @@ pub(crate) fn parse_type( use ParsedType::*; use ParsedTypeContainer::*; - if let Some(primary) = attrs.primary { + if let Some(primary) = attrs.clone().primary { match primary { AttributeModifier::Ignore => { unreachable!(); @@ -1558,17 +1575,23 @@ pub(crate) fn parse_type( } AttributeModifier::Serde | AttributeModifier::FromV8 - | AttributeModifier::ToV8 => { - let make_arg = match primary { - AttributeModifier::Serde => Arg::SerdeV8, - AttributeModifier::FromV8 => Arg::FromV8, - AttributeModifier::ToV8 => Arg::ToV8, + | AttributeModifier::ToV8 + | AttributeModifier::WebIDL(_) => { + let make_arg: Box Arg> = match primary { + AttributeModifier::Serde => Box::new(Arg::SerdeV8), + AttributeModifier::FromV8 => Box::new(Arg::FromV8), + AttributeModifier::ToV8 => Box::new(Arg::ToV8), + AttributeModifier::WebIDL(ref options) => { + Box::new(move |s| Arg::WebIDL(s, options.clone())) + } _ => unreachable!(), }; match ty { Type::Tuple(of) => return Ok(make_arg(stringify_token(of))), Type::Path(of) => { - if better_alternative_exists(position, of) { + if !matches!(primary, AttributeModifier::WebIDL(_)) + && better_alternative_exists(position, of) + { return Err(ArgError::InvalidAttributeType( primary.name(), stringify_token(ty), @@ -1617,7 +1640,12 @@ pub(crate) fn parse_type( } AttributeModifier::Number => match ty { Type::Path(of) => { - match parse_type_path(position, attrs, TypePathContext::None, of)? { + match parse_type_path( + position, + attrs.clone(), + TypePathContext::None, + of, + )? { COption(TNumeric( n @ (NumericArg::u64 | NumericArg::usize @@ -1685,8 +1713,8 @@ pub(crate) fn parse_type( } numeric => { let res = CBare(TBuffer(BufferType::Slice(mut_type, numeric))); - res.validate_attributes(position, attrs, &of)?; - Arg::from_parsed(res, attrs).map_err(|_| { + res.validate_attributes(position, attrs.clone(), &of)?; + Arg::from_parsed(res, attrs.clone()).map_err(|_| { ArgError::InvalidType(stringify_token(ty), "for slice") }) } @@ -1696,7 +1724,12 @@ pub(crate) fn parse_type( } } Type::Path(of) => { - match parse_type_path(position, attrs, TypePathContext::Ref, of)? { + match parse_type_path( + position, + attrs.clone(), + TypePathContext::Ref, + of, + )? { CBare(TString(Strings::RefStr)) => Ok(Arg::String(Strings::RefStr)), COption(TString(Strings::RefStr)) => { Ok(Arg::OptionString(Strings::RefStr)) @@ -1720,14 +1753,19 @@ pub(crate) fn parse_type( }; match &*of.elem { Type::Path(of) => { - match parse_type_path(position, attrs, TypePathContext::Ptr, of)? { + match parse_type_path( + position, + attrs.clone(), + TypePathContext::Ptr, + of, + )? { CBare(TNumeric(NumericArg::__VOID__)) => { Ok(Arg::External(External::Ptr(mut_type))) } CBare(TNumeric(numeric)) => { let res = CBare(TBuffer(BufferType::Ptr(mut_type, numeric))); - res.validate_attributes(position, attrs, &of)?; - Arg::from_parsed(res, attrs).map_err(|_| { + res.validate_attributes(position, attrs.clone(), &of)?; + Arg::from_parsed(res, attrs.clone()).map_err(|_| { ArgError::InvalidType( stringify_token(ty), "for numeric pointer", @@ -1747,7 +1785,7 @@ pub(crate) fn parse_type( } } Type::Path(of) => Arg::from_parsed( - parse_type_path(position, attrs, TypePathContext::None, of)?, + parse_type_path(position, attrs.clone(), TypePathContext::None, of)?, attrs, ) .map_err(|_| ArgError::InvalidType(stringify_token(ty), "for path")), diff --git a/ops/op2/test_cases/sync/object_wrap.out b/ops/op2/test_cases/sync/object_wrap.out index 714646406..dc5b2c883 100644 --- a/ops/op2/test_cases/sync/object_wrap.out +++ b/ops/op2/test_cases/sync/object_wrap.out @@ -422,8 +422,8 @@ impl Foo { ); if args.length() < 1u8 as i32 { let msg = format!( - "{}{} {} required, but only {} present", - "Failed to execute 'call' on 'Foo': ", + "{}: {} {} required, but only {} present", + "Failed to execute 'call' on 'Foo'", 1u8, "argument", args.length(), diff --git a/ops/op2/test_cases/sync/webidl.out b/ops/op2/test_cases/sync/webidl.out new file mode 100644 index 000000000..8d06ddb47 --- /dev/null +++ b/ops/op2/test_cases/sync/webidl.out @@ -0,0 +1,130 @@ +#[allow(non_camel_case_types)] +const fn op_webidl() -> ::deno_core::_ops::OpDecl { + #[allow(non_camel_case_types)] + struct op_webidl { + _unconstructable: ::std::marker::PhantomData<()>, + } + impl ::deno_core::_ops::Op for op_webidl { + const NAME: &'static str = stringify!(op_webidl); + const DECL: ::deno_core::_ops::OpDecl = ::deno_core::_ops::OpDecl::new_internal_op2( + ::deno_core::__op_name_fast!(op_webidl), + false, + false, + false, + 2usize as u8, + false, + Self::v8_fn_ptr as _, + Self::v8_fn_ptr_metrics as _, + ::deno_core::AccessorType::None, + None, + None, + ::deno_core::OpMetadata { + ..::deno_core::OpMetadata::default() + }, + ); + } + impl op_webidl { + pub const fn name() -> &'static str { + ::NAME + } + fn slow_function_impl<'s>( + info: &'s deno_core::v8::FunctionCallbackInfo, + ) -> usize { + #[cfg(debug_assertions)] + let _reentrancy_check_guard = deno_core::_ops::reentrancy_check( + &::DECL, + ); + let mut scope = unsafe { deno_core::v8::CallbackScope::new(info) }; + let mut rv = deno_core::v8::ReturnValue::from_function_callback_info(info); + let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info( + info, + ); + let result = { + let arg0 = args.get(0usize as i32); + let arg0 = match ::convert( + &mut scope, + arg0, + "Failed to execute 'UNINIT.call'".into(), + || std::borrow::Cow::Borrowed("Argument 1"), + &Default::default(), + ) { + Ok(t) => t, + Err(arg0_err) => { + let msg = deno_core::v8::String::new( + &mut scope, + &format!("{}", deno_core::anyhow::Error::from(arg0_err)), + ) + .unwrap(); + let exc = deno_core::v8::Exception::type_error(&mut scope, msg); + scope.throw_exception(exc); + return 1; + } + }; + let arg1 = args.get(1usize as i32); + let arg1 = match ::convert( + &mut scope, + arg1, + "Failed to execute 'UNINIT.call'".into(), + || std::borrow::Cow::Borrowed("Argument 2"), + &Default::default(), + ) { + Ok(t) => t, + Err(arg1_err) => { + let msg = deno_core::v8::String::new( + &mut scope, + &format!("{}", deno_core::anyhow::Error::from(arg1_err)), + ) + .unwrap(); + let exc = deno_core::v8::Exception::type_error(&mut scope, msg); + scope.throw_exception(exc); + return 1; + } + }; + Self::call(arg0, arg1) + }; + deno_core::_ops::RustToV8RetVal::to_v8_rv(result, &mut rv); + return 0; + } + extern "C" fn v8_fn_ptr<'s>(info: *const deno_core::v8::FunctionCallbackInfo) { + let info: &'s _ = unsafe { &*info }; + Self::slow_function_impl(info); + } + extern "C" fn v8_fn_ptr_metrics<'s>( + info: *const deno_core::v8::FunctionCallbackInfo, + ) { + let info: &'s _ = unsafe { &*info }; + let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info( + info, + ); + let opctx: &'s _ = unsafe { + &*(deno_core::v8::Local::< + deno_core::v8::External, + >::cast_unchecked(args.data()) + .value() as *const deno_core::_ops::OpCtx) + }; + deno_core::_ops::dispatch_metrics_slow( + opctx, + deno_core::_ops::OpMetricsEvent::Dispatched, + ); + let res = Self::slow_function_impl(info); + if res == 0 { + deno_core::_ops::dispatch_metrics_slow( + opctx, + deno_core::_ops::OpMetricsEvent::Completed, + ); + } else { + deno_core::_ops::dispatch_metrics_slow( + opctx, + deno_core::_ops::OpMetricsEvent::Error, + ); + } + } + } + impl op_webidl { + #[inline(always)] + fn call(s: String, _n: u32) -> u32 { + s.len() as _ + } + } + ::DECL +} diff --git a/ops/op2/test_cases/sync/webidl.rs b/ops/op2/test_cases/sync/webidl.rs new file mode 100644 index 000000000..3a8a8345f --- /dev/null +++ b/ops/op2/test_cases/sync/webidl.rs @@ -0,0 +1,8 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +#![deny(warnings)] +deno_ops_compile_test_runner::prelude!(); + +#[op2] +fn op_webidl(#[webidl] s: String, #[webidl] _n: u32) -> u32 { + s.len() as _ +} diff --git a/ops/webidl/dictionary.rs b/ops/webidl/dictionary.rs new file mode 100644 index 000000000..8beae3e3b --- /dev/null +++ b/ops/webidl/dictionary.rs @@ -0,0 +1,360 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use super::kw; +use proc_macro2::Ident; +use proc_macro2::Span; +use proc_macro2::TokenStream; +use quote::format_ident; +use quote::quote; +use quote::ToTokens; +use syn::parse::Parse; +use syn::parse::ParseStream; +use syn::punctuated::Punctuated; +use syn::spanned::Spanned; +use syn::DataStruct; +use syn::Error; +use syn::Expr; +use syn::Field; +use syn::Fields; +use syn::LitStr; +use syn::MetaNameValue; +use syn::Token; +use syn::Type; + +pub fn get_body( + ident_string: String, + span: Span, + data: DataStruct, +) -> Result<(TokenStream, Vec, Vec), Error> { + let fields = match data.fields { + Fields::Named(fields) => fields, + Fields::Unnamed(_) => { + return Err(Error::new( + span, + "Unnamed fields are currently not supported", + )) + } + Fields::Unit => { + return Err(Error::new(span, "Unit fields are currently not supported")) + } + }; + + let mut fields = fields + .named + .into_iter() + .map(TryInto::try_into) + .collect::, Error>>()?; + fields.sort_by(|a, b| a.name.cmp(&b.name)); + + let names = fields + .iter() + .map(|field| field.name.clone()) + .collect::>(); + let v8_static_strings = fields + .iter() + .map(|field| { + let name = field.get_name(); + let new_ident = format_ident!("__v8_static_{name}"); + let name_str = name.to_string(); + quote!(#new_ident = #name_str) + }) + .collect::>(); + let v8_lazy_strings = fields + .iter() + .map(|field| { + let name = field.get_name(); + let v8_eternal_name = format_ident!("__v8_{name}_eternal"); + quote! { + static #v8_eternal_name: ::deno_core::v8::Eternal<::deno_core::v8::String> = ::deno_core::v8::Eternal::empty(); + } + }) + .collect::>(); + + let fields = fields.into_iter().map(|field| { + let name = field.get_name(); + let string_name = name.to_string(); + let original_name = field.name; + let v8_static_name = format_ident!("__v8_static_{name}"); + let v8_eternal_name = format_ident!("__v8_{name}_eternal"); + + let options = if field.converter_options.is_empty() { + quote!(Default::default()) + } else { + let inner = field.converter_options + .into_iter() + .map(|(k, v)| quote!(#k: #v)) + .collect::>(); + + let ty = field.ty; + + // Type-alias to workaround https://github.com/rust-lang/rust/issues/86935 + quote! { + { + type Alias<'a> = <#ty as ::deno_core::webidl::WebIdlConverter<'a>>::Options; + Alias { + #(#inner),*, + ..Default::default() + } + } + } + }; + + let new_context = format!("'{string_name}' of '{ident_string}'"); + + let convert = quote! { + let val = ::deno_core::webidl::WebIdlConverter::convert( + __scope, + __value, + __prefix.clone(), + || format!("{} ({})", #new_context, __context()).into(), + &#options, + )?; + }; + + let convert_body = if field.option_is_required { + quote! { + if __value.is_undefined() { + None + } else { + #convert + Some(val) + } + } + } else { + let val = if field.is_option { + quote!(Some(val)) + } else { + quote!(val) + }; + + quote! { + #convert + #val + } + }; + + let undefined_as_none = if field.default_value.is_some() { + quote! { + .and_then(|__value| { + if __value.is_undefined() { + None + } else { + Some(__value) + } + }) + } + } else { + quote!() + }; + + let required_or_default = if let Some(default) = field.default_value { + default.to_token_stream() + } else { + quote! { + return Err(::deno_core::webidl::WebIdlError::new( + __prefix, + &__context, + ::deno_core::webidl::WebIdlErrorKind::DictionaryCannotConvertKey { + converter: #ident_string, + key: #string_name, + }, + )); + } + }; + + quote! { + let #original_name = { + let __key = #v8_eternal_name + .with(|__eternal| { + if let Some(__key) = __eternal.get(__scope) { + Ok(__key) + } else { + let __key = #v8_static_name + .v8_string(__scope) + .map_err(|e| ::deno_core::webidl::WebIdlError::other(__prefix.clone(), &__context, e))?; + __eternal.set(__scope, __key); + Ok(__key) + } + })? + .into(); + + if let Some(__value) = __obj.as_ref().and_then(|__obj| __obj.get(__scope, __key))#undefined_as_none { + #convert_body + } else { + #required_or_default + } + }; + } + }).collect::>(); + + let body = quote! { + let __obj: Option<::deno_core::v8::Local<::deno_core::v8::Object>> = if __value.is_undefined() || __value.is_null() { + None + } else { + if let Ok(obj) = __value.try_into() { + Some(obj) + } else { + return Err(::deno_core::webidl::WebIdlError::new( + __prefix, + &__context, + ::deno_core::webidl::WebIdlErrorKind::ConvertToConverterType("dictionary") + )); + } + }; + + #(#fields)* + + Ok(Self { #(#names),* }) + }; + + Ok((body, v8_static_strings, v8_lazy_strings)) +} + +struct DictionaryField { + span: Span, + name: Ident, + rename: Option, + default_value: Option, + is_option: bool, + option_is_required: bool, + converter_options: std::collections::HashMap, + ty: Type, +} + +impl DictionaryField { + fn get_name(&self) -> Ident { + Ident::new( + &self + .rename + .clone() + .unwrap_or_else(|| stringcase::camel_case(&self.name.to_string())), + self.span, + ) + } +} + +impl TryFrom for DictionaryField { + type Error = Error; + fn try_from(value: Field) -> Result { + let span = value.span(); + let mut default_value: Option = None; + let mut rename: Option = None; + let mut option_is_required = false; + let mut converter_options = std::collections::HashMap::new(); + + for attr in value.attrs { + if attr.path().is_ident("webidl") { + let list = attr.meta.require_list()?; + let args = list.parse_args_with( + Punctuated::::parse_terminated, + )?; + + for argument in args { + match argument { + DictionaryFieldArgument::Default { value, .. } => { + default_value = Some(value) + } + DictionaryFieldArgument::Rename { value, .. } => { + rename = Some(value.value()) + } + DictionaryFieldArgument::Required { .. } => { + option_is_required = true + } + } + } + } else if attr.path().is_ident("options") { + let list = attr.meta.require_list()?; + let args = list.parse_args_with( + Punctuated::::parse_terminated, + )?; + + let args = args + .into_iter() + .map(|kv| { + let ident = kv.path.require_ident()?; + Ok((ident.clone(), kv.value)) + }) + .collect::, Error>>()?; + + converter_options.extend(args); + } + } + + let is_option = if let Type::Path(path) = &value.ty { + if let Some(last) = path.path.segments.last() { + last.ident == "Option" + } else { + false + } + } else { + false + }; + + if option_is_required && !is_option { + return Err(Error::new( + span, + "Required option can only be used with an Option", + )); + } + + if option_is_required && default_value.is_some() { + return Err(Error::new( + span, + "Required option and default value cannot be used together", + )); + } + + Ok(Self { + span, + name: value.ident.unwrap(), + rename, + default_value, + is_option, + option_is_required, + converter_options, + ty: value.ty, + }) + } +} + +#[allow(dead_code)] +enum DictionaryFieldArgument { + Default { + name_token: kw::default, + eq_token: Token![=], + value: Expr, + }, + Rename { + name_token: kw::rename, + eq_token: Token![=], + value: LitStr, + }, + Required { + name_token: kw::required, + }, +} + +impl Parse for DictionaryFieldArgument { + fn parse(input: ParseStream) -> Result { + let lookahead = input.lookahead1(); + if lookahead.peek(kw::default) { + Ok(DictionaryFieldArgument::Default { + name_token: input.parse()?, + eq_token: input.parse()?, + value: input.parse()?, + }) + } else if lookahead.peek(kw::rename) { + Ok(DictionaryFieldArgument::Rename { + name_token: input.parse()?, + eq_token: input.parse()?, + value: input.parse()?, + }) + } else if lookahead.peek(kw::required) { + Ok(DictionaryFieldArgument::Required { + name_token: input.parse()?, + }) + } else { + Err(lookahead.error()) + } + } +} diff --git a/ops/webidl/enum.rs b/ops/webidl/enum.rs new file mode 100644 index 000000000..3f0f06187 --- /dev/null +++ b/ops/webidl/enum.rs @@ -0,0 +1,103 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use super::kw; +use proc_macro2::Ident; +use proc_macro2::TokenStream; +use quote::quote; +use syn::parse::Parse; +use syn::parse::ParseStream; +use syn::punctuated::Punctuated; +use syn::spanned::Spanned; +use syn::DataEnum; +use syn::Error; +use syn::LitStr; +use syn::Token; +use syn::Variant; + +pub fn get_body( + ident_string: String, + data: DataEnum, +) -> Result { + let variants = data + .variants + .into_iter() + .map(get_variant_name) + .collect::, _>>()?; + + let variants = variants + .into_iter() + .map(|(name, ident)| quote!(#name => Ok(Self::#ident))) + .collect::>(); + + Ok(quote! { + let Ok(str) = __value.try_cast::<::deno_core::v8::String>() else { + return Err(::deno_core::webidl::WebIdlError::new( + __prefix, + &__context, + ::deno_core::webidl::WebIdlErrorKind::ConvertToConverterType("enum"), + )); + }; + + match str.to_rust_string_lossy(__scope).as_str() { + #(#variants),*, + s => Err(::deno_core::webidl::WebIdlError::new(__prefix, &__context, ::deno_core::webidl::WebIdlErrorKind::InvalidEnumVariant { converter: #ident_string, variant: s.to_string() })) + } + }) +} + +fn get_variant_name(value: Variant) -> Result<(String, Ident), Error> { + let mut rename: Option = None; + + if !value.fields.is_empty() { + return Err(Error::new( + value.fields.span(), + "variants with fields are not allowed for enum converters", + )); + } + + for attr in value.attrs { + if attr.path().is_ident("webidl") { + let list = attr.meta.require_list()?; + let args = list.parse_args_with( + Punctuated::::parse_terminated, + )?; + + for argument in args { + match argument { + EnumVariantArgument::Rename { value, .. } => { + rename = Some(value.value()) + } + } + } + } + } + + Ok(( + rename.unwrap_or_else(|| stringcase::kebab_case(&value.ident.to_string())), + value.ident, + )) +} + +#[allow(dead_code)] +enum EnumVariantArgument { + Rename { + name_token: kw::rename, + eq_token: Token![=], + value: LitStr, + }, +} + +impl Parse for EnumVariantArgument { + fn parse(input: ParseStream) -> Result { + let lookahead = input.lookahead1(); + if lookahead.peek(kw::rename) { + Ok(EnumVariantArgument::Rename { + name_token: input.parse()?, + eq_token: input.parse()?, + value: input.parse()?, + }) + } else { + Err(lookahead.error()) + } + } +} diff --git a/ops/webidl/mod.rs b/ops/webidl/mod.rs new file mode 100644 index 000000000..bbbd41528 --- /dev/null +++ b/ops/webidl/mod.rs @@ -0,0 +1,219 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +mod dictionary; +mod r#enum; + +use proc_macro2::Ident; +use proc_macro2::TokenStream; +use quote::quote; +use syn::parse::Parse; +use syn::parse::ParseStream; +use syn::parse2; +use syn::spanned::Spanned; +use syn::Attribute; +use syn::Data; +use syn::DeriveInput; +use syn::Error; +use syn::Token; + +pub fn webidl(item: TokenStream) -> Result { + let input = parse2::(item)?; + let span = input.span(); + let ident = input.ident; + let ident_string = ident.to_string(); + let converter = input + .attrs + .into_iter() + .find_map(|attr| ConverterType::from_attribute(attr).transpose()) + .ok_or_else(|| Error::new(span, "missing #[webidl] attribute"))??; + + let out = match input.data { + Data::Struct(data) => match converter { + ConverterType::Dictionary => { + let (body, v8_static_strings, v8_lazy_strings) = + dictionary::get_body(ident_string, span, data)?; + + let implementation = create_impl(ident, body); + + quote! { + ::deno_core::v8_static_strings! { + #(#v8_static_strings),* + } + + thread_local! { + #(#v8_lazy_strings)* + } + + #implementation + } + } + ConverterType::Enum => { + return Err(Error::new(span, "Structs do not support enum converters")); + } + }, + Data::Enum(data) => match converter { + ConverterType::Dictionary => { + return Err(Error::new( + span, + "Enums currently do not support dictionary converters", + )); + } + ConverterType::Enum => { + create_impl(ident, r#enum::get_body(ident_string, data)?) + } + }, + Data::Union(_) => return Err(Error::new(span, "Unions are not supported")), + }; + + Ok(out) +} + +mod kw { + syn::custom_keyword!(dictionary); + syn::custom_keyword!(default); + syn::custom_keyword!(rename); + syn::custom_keyword!(required); +} + +enum ConverterType { + Dictionary, + Enum, +} + +impl ConverterType { + fn from_attribute(attr: Attribute) -> Result, Error> { + if attr.path().is_ident("webidl") { + let list = attr.meta.require_list()?; + let value = list.parse_args::()?; + Ok(Some(value)) + } else { + Ok(None) + } + } +} + +impl Parse for ConverterType { + fn parse(input: ParseStream) -> syn::Result { + let lookahead = input.lookahead1(); + + if lookahead.peek(kw::dictionary) { + input.parse::()?; + Ok(Self::Dictionary) + } else if lookahead.peek(Token![enum]) { + input.parse::()?; + Ok(Self::Enum) + } else { + Err(lookahead.error()) + } + } +} + +fn create_impl(ident: Ident, body: TokenStream) -> TokenStream { + quote! { + impl<'a> ::deno_core::webidl::WebIdlConverter<'a> for #ident { + type Options = (); + + fn convert( + __scope: &mut ::deno_core::v8::HandleScope<'a>, + __value: ::deno_core::v8::Local<'a, ::deno_core::v8::Value>, + __prefix: std::borrow::Cow<'static, str>, + __context: C, + __options: &Self::Options, + ) -> Result + where + C: Fn() -> std::borrow::Cow<'static, str>, + { + #body + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + use quote::ToTokens; + use std::path::PathBuf; + use syn::punctuated::Punctuated; + use syn::File; + use syn::Item; + + fn derives_webidl<'a>( + attrs: impl IntoIterator, + ) -> bool { + attrs.into_iter().any(|attr| { + attr.path().is_ident("derive") && { + let list = attr.meta.require_list().unwrap(); + let idents = list + .parse_args_with(Punctuated::::parse_terminated) + .unwrap(); + idents.iter().any(|ident| ident == "WebIDL") + } + }) + } + + #[testing_macros::fixture("webidl/test_cases/*.rs")] + fn test_proc_macro_sync(input: PathBuf) { + test_proc_macro_output(input) + } + + fn expand_webidl(item: impl ToTokens) -> String { + let tokens = + webidl(item.to_token_stream()).expect("Failed to generate WebIDL"); + println!("======== Raw tokens ========:\n{}", tokens.clone()); + let tree = syn::parse2(tokens).unwrap(); + let actual = prettyplease::unparse(&tree); + println!("======== Generated ========:\n{}", actual); + actual + } + + fn test_proc_macro_output(input: PathBuf) { + let update_expected = std::env::var("UPDATE_EXPECTED").is_ok(); + + let source = + std::fs::read_to_string(&input).expect("Failed to read test file"); + + const PRELUDE: &str = r"// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +#![deny(warnings)] +deno_ops_compile_test_runner::prelude!();"; + + if !source.starts_with(PRELUDE) { + panic!("Source does not start with expected prelude:]n{PRELUDE}"); + } + + let file = + syn::parse_str::(&source).expect("Failed to parse Rust file"); + let mut expected_out = vec![]; + for item in file.items { + match item { + Item::Struct(struct_item) => { + if derives_webidl(&struct_item.attrs) { + expected_out.push(expand_webidl(struct_item)); + } + } + Item::Enum(enum_item) => { + dbg!(); + if derives_webidl(&enum_item.attrs) { + expected_out.push(expand_webidl(enum_item)); + } + } + _ => {} + } + } + + let expected_out = expected_out.join("\n"); + + if update_expected { + std::fs::write(input.with_extension("out"), expected_out) + .expect("Failed to write expectation file"); + } else { + let expected = std::fs::read_to_string(input.with_extension("out")) + .expect("Failed to read expectation file"); + assert_eq!( + expected, expected_out, + "Failed to match expectation. Use UPDATE_EXPECTED=1." + ); + } + } +} diff --git a/ops/webidl/test_cases/dict.out b/ops/webidl/test_cases/dict.out new file mode 100644 index 000000000..04f4cfc33 --- /dev/null +++ b/ops/webidl/test_cases/dict.out @@ -0,0 +1,310 @@ +::deno_core::v8_static_strings! { + __v8_static_a = "a", __v8_static_b = "b", __v8_static_c = "c", __v8_static_e = "e", + __v8_static_f = "f", __v8_static_g = "g" +} +thread_local! { + static __v8_a_eternal: ::deno_core::v8::Eternal<::deno_core::v8::String> = ::deno_core::v8::Eternal::empty(); + static __v8_b_eternal: ::deno_core::v8::Eternal<::deno_core::v8::String> = ::deno_core::v8::Eternal::empty(); + static __v8_c_eternal: ::deno_core::v8::Eternal<::deno_core::v8::String> = ::deno_core::v8::Eternal::empty(); + static __v8_e_eternal: ::deno_core::v8::Eternal<::deno_core::v8::String> = ::deno_core::v8::Eternal::empty(); + static __v8_f_eternal: ::deno_core::v8::Eternal<::deno_core::v8::String> = ::deno_core::v8::Eternal::empty(); + static __v8_g_eternal: ::deno_core::v8::Eternal<::deno_core::v8::String> = ::deno_core::v8::Eternal::empty(); +} +impl<'a> ::deno_core::webidl::WebIdlConverter<'a> for Dict { + type Options = (); + fn convert( + __scope: &mut ::deno_core::v8::HandleScope<'a>, + __value: ::deno_core::v8::Local<'a, ::deno_core::v8::Value>, + __prefix: std::borrow::Cow<'static, str>, + __context: C, + __options: &Self::Options, + ) -> Result + where + C: Fn() -> std::borrow::Cow<'static, str>, + { + let __obj: Option<::deno_core::v8::Local<::deno_core::v8::Object>> = if __value + .is_undefined() || __value.is_null() + { + None + } else { + if let Ok(obj) = __value.try_into() { + Some(obj) + } else { + return Err( + ::deno_core::webidl::WebIdlError::new( + __prefix, + &__context, + ::deno_core::webidl::WebIdlErrorKind::ConvertToConverterType( + "dictionary", + ), + ), + ); + } + }; + let a = { + let __key = __v8_a_eternal + .with(|__eternal| { + if let Some(__key) = __eternal.get(__scope) { + Ok(__key) + } else { + let __key = __v8_static_a + .v8_string(__scope) + .map_err(|e| ::deno_core::webidl::WebIdlError::other( + __prefix.clone(), + &__context, + e, + ))?; + __eternal.set(__scope, __key); + Ok(__key) + } + })? + .into(); + if let Some(__value) = __obj + .as_ref() + .and_then(|__obj| __obj.get(__scope, __key)) + { + let val = ::deno_core::webidl::WebIdlConverter::convert( + __scope, + __value, + __prefix.clone(), + || format!("{} ({})", "'a' of 'Dict'", __context()).into(), + &Default::default(), + )?; + val + } else { + return Err( + ::deno_core::webidl::WebIdlError::new( + __prefix, + &__context, + ::deno_core::webidl::WebIdlErrorKind::DictionaryCannotConvertKey { + converter: "Dict", + key: "a", + }, + ), + ); + } + }; + let b = { + let __key = __v8_b_eternal + .with(|__eternal| { + if let Some(__key) = __eternal.get(__scope) { + Ok(__key) + } else { + let __key = __v8_static_b + .v8_string(__scope) + .map_err(|e| ::deno_core::webidl::WebIdlError::other( + __prefix.clone(), + &__context, + e, + ))?; + __eternal.set(__scope, __key); + Ok(__key) + } + })? + .into(); + if let Some(__value) = __obj + .as_ref() + .and_then(|__obj| __obj.get(__scope, __key)) + { + let val = ::deno_core::webidl::WebIdlConverter::convert( + __scope, + __value, + __prefix.clone(), + || format!("{} ({})", "'b' of 'Dict'", __context()).into(), + &{ + type Alias<'a> = as ::deno_core::webidl::WebIdlConverter<'a>>::Options; + Alias { + clamp: true, + ..Default::default() + } + }, + )?; + val + } else { + return Err( + ::deno_core::webidl::WebIdlError::new( + __prefix, + &__context, + ::deno_core::webidl::WebIdlErrorKind::DictionaryCannotConvertKey { + converter: "Dict", + key: "b", + }, + ), + ); + } + }; + let c = { + let __key = __v8_c_eternal + .with(|__eternal| { + if let Some(__key) = __eternal.get(__scope) { + Ok(__key) + } else { + let __key = __v8_static_c + .v8_string(__scope) + .map_err(|e| ::deno_core::webidl::WebIdlError::other( + __prefix.clone(), + &__context, + e, + ))?; + __eternal.set(__scope, __key); + Ok(__key) + } + })? + .into(); + if let Some(__value) = __obj + .as_ref() + .and_then(|__obj| __obj.get(__scope, __key)) + .and_then(|__value| { + if __value.is_undefined() { None } else { Some(__value) } + }) + { + let val = ::deno_core::webidl::WebIdlConverter::convert( + __scope, + __value, + __prefix.clone(), + || format!("{} ({})", "'c' of 'Dict'", __context()).into(), + &Default::default(), + )?; + Some(val) + } else { + Some(3) + } + }; + let d = { + let __key = __v8_e_eternal + .with(|__eternal| { + if let Some(__key) = __eternal.get(__scope) { + Ok(__key) + } else { + let __key = __v8_static_e + .v8_string(__scope) + .map_err(|e| ::deno_core::webidl::WebIdlError::other( + __prefix.clone(), + &__context, + e, + ))?; + __eternal.set(__scope, __key); + Ok(__key) + } + })? + .into(); + if let Some(__value) = __obj + .as_ref() + .and_then(|__obj| __obj.get(__scope, __key)) + { + let val = ::deno_core::webidl::WebIdlConverter::convert( + __scope, + __value, + __prefix.clone(), + || format!("{} ({})", "'e' of 'Dict'", __context()).into(), + &Default::default(), + )?; + val + } else { + return Err( + ::deno_core::webidl::WebIdlError::new( + __prefix, + &__context, + ::deno_core::webidl::WebIdlErrorKind::DictionaryCannotConvertKey { + converter: "Dict", + key: "e", + }, + ), + ); + } + }; + let f = { + let __key = __v8_f_eternal + .with(|__eternal| { + if let Some(__key) = __eternal.get(__scope) { + Ok(__key) + } else { + let __key = __v8_static_f + .v8_string(__scope) + .map_err(|e| ::deno_core::webidl::WebIdlError::other( + __prefix.clone(), + &__context, + e, + ))?; + __eternal.set(__scope, __key); + Ok(__key) + } + })? + .into(); + if let Some(__value) = __obj + .as_ref() + .and_then(|__obj| __obj.get(__scope, __key)) + { + let val = ::deno_core::webidl::WebIdlConverter::convert( + __scope, + __value, + __prefix.clone(), + || format!("{} ({})", "'f' of 'Dict'", __context()).into(), + &Default::default(), + )?; + val + } else { + return Err( + ::deno_core::webidl::WebIdlError::new( + __prefix, + &__context, + ::deno_core::webidl::WebIdlErrorKind::DictionaryCannotConvertKey { + converter: "Dict", + key: "f", + }, + ), + ); + } + }; + let g = { + let __key = __v8_g_eternal + .with(|__eternal| { + if let Some(__key) = __eternal.get(__scope) { + Ok(__key) + } else { + let __key = __v8_static_g + .v8_string(__scope) + .map_err(|e| ::deno_core::webidl::WebIdlError::other( + __prefix.clone(), + &__context, + e, + ))?; + __eternal.set(__scope, __key); + Ok(__key) + } + })? + .into(); + if let Some(__value) = __obj + .as_ref() + .and_then(|__obj| __obj.get(__scope, __key)) + { + if __value.is_undefined() { + None + } else { + let val = ::deno_core::webidl::WebIdlConverter::convert( + __scope, + __value, + __prefix.clone(), + || format!("{} ({})", "'g' of 'Dict'", __context()).into(), + &Default::default(), + )?; + Some(val) + } + } else { + return Err( + ::deno_core::webidl::WebIdlError::new( + __prefix, + &__context, + ::deno_core::webidl::WebIdlErrorKind::DictionaryCannotConvertKey { + converter: "Dict", + key: "g", + }, + ), + ); + } + }; + Ok(Self { a, b, c, d, f, g }) + } +} diff --git a/ops/webidl/test_cases/dict.rs b/ops/webidl/test_cases/dict.rs new file mode 100644 index 000000000..0f68c5a90 --- /dev/null +++ b/ops/webidl/test_cases/dict.rs @@ -0,0 +1,18 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +#![deny(warnings)] +deno_ops_compile_test_runner::prelude!(); + +#[derive(WebIDL)] +#[webidl(dictionary)] +pub struct Dict { + a: u8, + #[options(clamp = true)] + b: Vec, + #[webidl(default = Some(3))] + c: Option, + #[webidl(rename = "e")] + d: u64, + f: std::collections::HashMap, + #[webidl(required)] + g: Option, +} diff --git a/ops/webidl/test_cases/enum.out b/ops/webidl/test_cases/enum.out new file mode 100644 index 000000000..dc09959d4 --- /dev/null +++ b/ops/webidl/test_cases/enum.out @@ -0,0 +1,40 @@ +impl<'a> ::deno_core::webidl::WebIdlConverter<'a> for Enumeration { + type Options = (); + fn convert( + __scope: &mut ::deno_core::v8::HandleScope<'a>, + __value: ::deno_core::v8::Local<'a, ::deno_core::v8::Value>, + __prefix: std::borrow::Cow<'static, str>, + __context: C, + __options: &Self::Options, + ) -> Result + where + C: Fn() -> std::borrow::Cow<'static, str>, + { + let Ok(str) = __value.try_cast::<::deno_core::v8::String>() else { + return Err( + ::deno_core::webidl::WebIdlError::new( + __prefix, + &__context, + ::deno_core::webidl::WebIdlErrorKind::ConvertToConverterType("enum"), + ), + ); + }; + match str.to_rust_string_lossy(__scope).as_str() { + "foo-bar" => Ok(Self::FooBar), + "baz" => Ok(Self::Baz), + "hello" => Ok(Self::World), + s => { + Err( + ::deno_core::webidl::WebIdlError::new( + __prefix, + &__context, + ::deno_core::webidl::WebIdlErrorKind::InvalidEnumVariant { + converter: "Enumeration", + variant: s.to_string(), + }, + ), + ) + } + } + } +} diff --git a/ops/webidl/test_cases/enum.rs b/ops/webidl/test_cases/enum.rs new file mode 100644 index 000000000..bdb0eaeca --- /dev/null +++ b/ops/webidl/test_cases/enum.rs @@ -0,0 +1,12 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +#![deny(warnings)] +deno_ops_compile_test_runner::prelude!(); + +#[derive(WebIDL)] +#[webidl(enum)] +pub enum Enumeration { + FooBar, + Baz, + #[webidl(rename = "hello")] + World, +} diff --git a/ops/webidl/test_cases_fail/enum_fields.rs b/ops/webidl/test_cases_fail/enum_fields.rs new file mode 100644 index 000000000..b2a991e77 --- /dev/null +++ b/ops/webidl/test_cases_fail/enum_fields.rs @@ -0,0 +1,10 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +#![deny(warnings)] +deno_ops_compile_test_runner::prelude!(); + +#[derive(WebIDL)] +#[webidl(enum)] +pub enum Enumeration { + FooBar(u32), + Baz, +} diff --git a/ops/webidl/test_cases_fail/enum_fields.stderr b/ops/webidl/test_cases_fail/enum_fields.stderr new file mode 100644 index 000000000..a590fd892 --- /dev/null +++ b/ops/webidl/test_cases_fail/enum_fields.stderr @@ -0,0 +1,5 @@ +error: variants with fields are not allowed for enum converters + --> ../webidl/test_cases_fail/enum_fields.rs:8:9 + | +8 | FooBar(u32), + | ^^^^^