diff --git a/core/convert.rs b/core/convert.rs index a33bd7681..5f99c4998 100644 --- a/core/convert.rs +++ b/core/convert.rs @@ -2,6 +2,7 @@ use crate::runtime::ops; use deno_error::JsErrorBox; +use deno_error::JsErrorClass; use std::convert::Infallible; /// A conversion from a rust value to a v8 value. @@ -64,7 +65,7 @@ use std::convert::Infallible; /// Tuples, on the other hand, are keyed by `smi`s, which are immediates /// and don't require allocation or garbage collection. pub trait ToV8<'a> { - type Error: std::error::Error + Send + Sync + 'static; + type Error: JsErrorClass; /// Converts the value to a V8 value. fn to_v8<'i>( @@ -111,7 +112,7 @@ pub trait ToV8<'a> { /// } /// ``` pub trait FromV8<'a>: Sized { - type Error: std::error::Error + Send + Sync + 'static; + type Error: JsErrorClass; /// Converts a V8 value to a Rust value. fn from_v8<'i>( @@ -402,6 +403,18 @@ where } } +impl<'a> FromV8<'a> for *mut std::ffi::c_void { + type Error = JsErrorBox; + + fn from_v8( + _scope: &mut v8::HandleScope<'a>, + value: v8::Local<'a, v8::Value>, + ) -> Result { + crate::_ops::to_external_option(&value) + .ok_or_else(|| JsErrorBox::type_error("Invalid external option")) + } +} + impl<'s, T> ToV8<'s> for Option where T: ToV8<'s>, @@ -436,3 +449,36 @@ where } } } + +#[cfg(all(test, not(miri)))] +mod tests { + use super::FromV8 as _; + use super::Smi; + use super::ToV8 as _; + use crate::JsRuntime; + use deno_ops::FromV8; + use deno_ops::ToV8; + + #[test] + fn macros() { + #[derive(FromV8, ToV8)] + pub struct MyStruct { + a: Smi, + r#b: String, + #[v8(rename = "e")] + d: Smi, + } + + let mut runtime = JsRuntime::new(Default::default()); + let scope = &mut runtime.handle_scope(); + + let my_struct = MyStruct { + a: Smi(1), + b: "foo".to_string(), + d: Smi(2), + }; + + let value = my_struct.to_v8(scope).unwrap(); + MyStruct::from_v8(scope, value).unwrap(); + } +} diff --git a/core/fast_string.rs b/core/fast_string.rs index 65f897866..186326be2 100644 --- a/core/fast_string.rs +++ b/core/fast_string.rs @@ -1,5 +1,6 @@ // Copyright 2018-2025 the Deno authors. MIT license. +use deno_error::JsError; use serde::Deserializer; use serde::Serializer; use std::borrow::Borrow; @@ -121,7 +122,8 @@ impl Display for FastStaticString { } } -#[derive(Debug)] +#[derive(Debug, JsError)] +#[class(generic)] pub struct FastStringV8AllocationError; impl std::error::Error for FastStringV8AllocationError {} diff --git a/core/lib.rs b/core/lib.rs index 7d1473a0d..b7daa1f4e 100644 --- a/core/lib.rs +++ b/core/lib.rs @@ -34,6 +34,9 @@ pub mod webidl; // Re-exports pub use anyhow; + +pub use deno_ops::FromV8; +pub use deno_ops::ToV8; pub use deno_ops::WebIDL; pub use deno_ops::op2; pub use deno_unsync as unsync; diff --git a/ops/compile_test_runner/lib.rs b/ops/compile_test_runner/lib.rs index 57b07cb80..e94d83faf 100644 --- a/ops/compile_test_runner/lib.rs +++ b/ops/compile_test_runner/lib.rs @@ -3,6 +3,10 @@ #[macro_export] macro_rules! prelude { () => { + #[allow(unused_imports)] + use deno_ops::FromV8; + #[allow(unused_imports)] + use deno_ops::ToV8; #[allow(unused_imports)] use deno_ops::WebIDL; #[allow(unused_imports)] @@ -28,4 +32,11 @@ mod compile_tests { t.pass("../webidl/test_cases/*.rs"); t.compile_fail("../webidl/test_cases_fail/*.rs"); } + + #[test] + fn v8() { + let t = trybuild::TestCases::new(); + t.pass("../v8/test_cases/*.rs"); + t.compile_fail("../v8/test_cases_fail/*.rs"); + } } diff --git a/ops/lib.rs b/ops/lib.rs index c6857f022..121197a3c 100644 --- a/ops/lib.rs +++ b/ops/lib.rs @@ -7,6 +7,7 @@ use proc_macro::TokenStream; use std::error::Error; mod op2; +mod v8; mod webidl; /// A macro designed to provide an extremely fast V8->Rust interface layer. @@ -43,6 +44,22 @@ pub fn webidl(item: TokenStream) -> TokenStream { } } +#[proc_macro_derive(FromV8, attributes(v8))] +pub fn from_v8(item: TokenStream) -> TokenStream { + match v8::from::from_v8(item.into()) { + Ok(output) => output.into(), + Err(err) => err.into_compile_error().into(), + } +} + +#[proc_macro_derive(ToV8, attributes(v8))] +pub fn to_v8(item: TokenStream) -> TokenStream { + match v8::to::to_v8(item.into()) { + Ok(output) => output.into(), + Err(err) => err.into_compile_error().into(), + } +} + #[cfg(test)] mod infra { use std::path::PathBuf; diff --git a/ops/op2/test_cases/sync/from_v8.rs b/ops/op2/test_cases/sync/from_v8.rs index ee01f66db..04bc66685 100644 --- a/ops/op2/test_cases/sync/from_v8.rs +++ b/ops/op2/test_cases/sync/from_v8.rs @@ -2,12 +2,12 @@ #![deny(warnings)] deno_ops_compile_test_runner::prelude!(); -use deno_core::FromV8; +use deno_core::FromV8 as FromV8Trait; use deno_core::v8; struct Foo; -impl<'a> FromV8<'a> for Foo { +impl<'a> FromV8Trait<'a> for Foo { type Error = std::convert::Infallible; fn from_v8( _scope: &mut v8::PinScope<'a, '_>, diff --git a/ops/op2/test_cases/sync/to_v8.rs b/ops/op2/test_cases/sync/to_v8.rs index 51768e2e6..b67777b4f 100644 --- a/ops/op2/test_cases/sync/to_v8.rs +++ b/ops/op2/test_cases/sync/to_v8.rs @@ -2,12 +2,12 @@ #![deny(warnings)] deno_ops_compile_test_runner::prelude!(); -use deno_core::ToV8; +use deno_core::ToV8 as ToV8Trait; use deno_core::v8; struct Foo; -impl<'a> ToV8<'a> for Foo { +impl<'a> ToV8Trait<'a> for Foo { type Error = std::convert::Infallible; fn to_v8( self, diff --git a/ops/v8/from.rs b/ops/v8/from.rs new file mode 100644 index 000000000..4567f932b --- /dev/null +++ b/ops/v8/from.rs @@ -0,0 +1,92 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +use proc_macro2::TokenStream; +use quote::ToTokens; +use quote::quote; +use syn::Data; +use syn::DeriveInput; +use syn::Error; +use syn::parse2; +use syn::spanned::Spanned; + +pub fn from_v8(item: TokenStream) -> Result { + let input = parse2::(item)?; + let span = input.span(); + let ident = input.ident; + + let out = match input.data { + Data::Struct(data) => { + let (fields, pre) = super::structs::get_fields(span, data)?; + + let names = fields + .iter() + .map(|field| field.name.clone()) + .collect::>(); + + let fields = fields.into_iter().map( + |super::structs::StructField { + name, + get_key, + .. + }| { + quote! { + let #name = { + let __key = #get_key; + + if let Some(__value) = __obj.get(__scope, __key) { + ::deno_core::convert::FromV8::from_v8( + __scope, + __value, + ).map_err(::deno_error::JsErrorBox::from_err)? + } else { + let __undefined_value = ::deno_core::v8::undefined(__scope).cast::<::deno_core::v8::Value>(); + + ::deno_core::convert::FromV8::from_v8( + __scope, + __undefined_value, + ).map_err(::deno_error::JsErrorBox::from_err)? + } + }; + } + }, + ); + + create_from_impl( + ident, + quote! { + #pre + + let __obj = __value.try_cast::<::deno_core::v8::Object>() + .map_err(|_| ::deno_error::JsErrorBox::type_error("Not an object"))?; + + #(#fields)* + + Ok(Self { + #(#names),* + }) + }, + ) + } + Data::Enum(_) => { + return Err(Error::new(span, "Enums currently are not supported")); + } + Data::Union(_) => return Err(Error::new(span, "Unions are not supported")), + }; + + Ok(out) +} + +fn create_from_impl(ident: impl ToTokens, body: TokenStream) -> TokenStream { + quote! { + impl<'a> ::deno_core::convert::FromV8<'a> for #ident { + type Error = ::deno_error::JsErrorBox; + + fn from_v8( + __scope: &mut ::deno_core::v8::HandleScope<'a>, + __value: ::deno_core::v8::Local<'a, deno_core::v8::Value>, + ) -> Result { + #body + } + } + } +} diff --git a/ops/v8/mod.rs b/ops/v8/mod.rs new file mode 100644 index 000000000..24a93228d --- /dev/null +++ b/ops/v8/mod.rs @@ -0,0 +1,60 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +pub mod from; +mod structs; +pub mod to; + +#[cfg(test)] +mod tests { + use proc_macro2::Ident; + use proc_macro2::TokenStream; + use quote::ToTokens; + use std::path::PathBuf; + use syn::Item; + use syn::punctuated::Punctuated; + + fn derives_v8<'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| matches!(ident.to_string().as_str(), "FromV8" | "ToV8")) + } + }) + } + + fn expand_v8(item: impl ToTokens) -> TokenStream { + let mut tokens = super::from::from_v8(item.to_token_stream()) + .expect("Failed to generate FromV8"); + + tokens.extend( + super::to::to_v8(item.to_token_stream()) + .expect("Failed to generate ToV8"), + ); + + tokens + } + + #[testing_macros::fixture("v8/test_cases/*.rs")] + fn test_proc_macro_sync(input: PathBuf) { + crate::infra::run_macro_expansion_test(input, |file| { + file.items.into_iter().filter_map(|item| { + if let Item::Struct(struct_item) = item + && derives_v8(&struct_item.attrs) + { + return Some(expand_v8(struct_item)); + } + + None + }) + }) + } +} diff --git a/ops/v8/structs.rs b/ops/v8/structs.rs new file mode 100644 index 000000000..d1c7f9f56 --- /dev/null +++ b/ops/v8/structs.rs @@ -0,0 +1,160 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +use proc_macro2::Ident; +use proc_macro2::Span; +use proc_macro2::TokenStream; +use quote::format_ident; +use quote::quote; +use syn::DataStruct; +use syn::Error; +use syn::Field; +use syn::Fields; +use syn::LitStr; +use syn::Token; +use syn::ext::IdentExt; +use syn::parse::Parse; +use syn::parse::ParseStream; +use syn::punctuated::Punctuated; +use syn::spanned::Spanned; + +pub fn get_fields( + span: Span, + data: DataStruct, +) -> Result<(Vec, TokenStream), 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 v8_static_strings = fields + .iter() + .map(|field| { + let static_name = &field.v8_static; + let name_str = static_name.to_string(); + quote!(#static_name = #name_str) + }) + .collect::>(); + let v8_lazy_strings = fields + .iter() + .map(|field| { + let name = &field.v8_eternal; + quote! { + static #name: ::deno_core::v8::Eternal<::deno_core::v8::String> = ::deno_core::v8::Eternal::empty(); + } + }) + .collect::>(); + + let pre = quote! { + ::deno_core::v8_static_strings! { + #(#v8_static_strings),* + } + + thread_local! { + #(#v8_lazy_strings)* + } + }; + + Ok((fields, pre)) +} + +pub struct StructField { + pub name: Ident, + pub v8_static: Ident, + pub v8_eternal: Ident, + pub get_key: TokenStream, +} + +impl TryFrom for StructField { + type Error = Error; + fn try_from(value: Field) -> Result { + let span = value.span(); + let name = value.ident.unwrap(); + let mut rename = stringcase::camel_case(&name.unraw().to_string()); + + for attr in value.attrs { + if attr.path().is_ident("v8") { + let list = attr.meta.require_list()?; + let args = list.parse_args_with( + Punctuated::::parse_terminated, + )?; + + for argument in args { + match argument { + StructFieldArgument::Rename { value, .. } => { + rename = value.value(); + } + } + } + } + } + + let js_name = Ident::new(&rename, span); + + let v8_static = format_ident!("__v8_static_{js_name}"); + let v8_eternal = format_ident!("__v8_{js_name}_eternal"); + + let get_key = quote! { + #v8_eternal + .with(|__eternal| { + if let Some(__key) = __eternal.get(__scope) { + Ok(__key) + } else { + let __key = #v8_static.v8_string(__scope).map_err(::deno_error::JsErrorBox::from_err)?; + __eternal.set(__scope, __key); + Ok::<_, ::deno_error::JsErrorBox>(__key) + } + })? + .cast::<::deno_core::v8::Value>() + }; + + Ok(Self { + v8_static, + v8_eternal, + get_key, + name, + }) + } +} + +mod kw { + syn::custom_keyword!(rename); +} + +#[allow(dead_code)] +enum StructFieldArgument { + Rename { + name_token: kw::rename, + eq_token: Token![=], + value: LitStr, + }, +} + +impl Parse for StructFieldArgument { + fn parse(input: ParseStream) -> Result { + let lookahead = input.lookahead1(); + if lookahead.peek(kw::rename) { + Ok(StructFieldArgument::Rename { + name_token: input.parse()?, + eq_token: input.parse()?, + value: input.parse()?, + }) + } else { + Err(lookahead.error()) + } + } +} diff --git a/ops/v8/test_cases/struct.out b/ops/v8/test_cases/struct.out new file mode 100644 index 000000000..5d491ddfc --- /dev/null +++ b/ops/v8/test_cases/struct.out @@ -0,0 +1,166 @@ +impl<'a> ::deno_core::convert::FromV8<'a> for MyStruct { + type Error = ::deno_error::JsErrorBox; + fn from_v8( + __scope: &mut ::deno_core::v8::HandleScope<'a>, + __value: ::deno_core::v8::Local<'a, deno_core::v8::Value>, + ) -> Result { + ::deno_core::v8_static_strings! { + __v8_static_a = "__v8_static_a", __v8_static_e = "__v8_static_e", + __v8_static_b = "__v8_static_b" + } + thread_local! { + static __v8_a_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_b_eternal: ::deno_core::v8::Eternal<::deno_core::v8::String> = ::deno_core::v8::Eternal::empty(); + } + let __obj = __value + .try_cast::<::deno_core::v8::Object>() + .map_err(|_| ::deno_error::JsErrorBox::type_error("Not an object"))?; + 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(::deno_error::JsErrorBox::from_err)?; + __eternal.set(__scope, __key); + Ok::<_, ::deno_error::JsErrorBox>(__key) + } + })? + .cast::<::deno_core::v8::Value>(); + if let Some(__value) = __obj.get(__scope, __key) { + ::deno_core::convert::FromV8::from_v8(__scope, __value) + .map_err(::deno_error::JsErrorBox::from_err)? + } else { + let __undefined_value = ::deno_core::v8::undefined(__scope) + .cast::<::deno_core::v8::Value>(); + ::deno_core::convert::FromV8::from_v8(__scope, __undefined_value) + .map_err(::deno_error::JsErrorBox::from_err)? + } + }; + 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(::deno_error::JsErrorBox::from_err)?; + __eternal.set(__scope, __key); + Ok::<_, ::deno_error::JsErrorBox>(__key) + } + })? + .cast::<::deno_core::v8::Value>(); + if let Some(__value) = __obj.get(__scope, __key) { + ::deno_core::convert::FromV8::from_v8(__scope, __value) + .map_err(::deno_error::JsErrorBox::from_err)? + } else { + let __undefined_value = ::deno_core::v8::undefined(__scope) + .cast::<::deno_core::v8::Value>(); + ::deno_core::convert::FromV8::from_v8(__scope, __undefined_value) + .map_err(::deno_error::JsErrorBox::from_err)? + } + }; + let r#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(::deno_error::JsErrorBox::from_err)?; + __eternal.set(__scope, __key); + Ok::<_, ::deno_error::JsErrorBox>(__key) + } + })? + .cast::<::deno_core::v8::Value>(); + if let Some(__value) = __obj.get(__scope, __key) { + ::deno_core::convert::FromV8::from_v8(__scope, __value) + .map_err(::deno_error::JsErrorBox::from_err)? + } else { + let __undefined_value = ::deno_core::v8::undefined(__scope) + .cast::<::deno_core::v8::Value>(); + ::deno_core::convert::FromV8::from_v8(__scope, __undefined_value) + .map_err(::deno_error::JsErrorBox::from_err)? + } + }; + Ok(Self { a, d, r#b }) + } +} +impl<'a> ::deno_core::convert::ToV8<'a> for MyStruct { + type Error = ::deno_error::JsErrorBox; + fn to_v8( + self, + __scope: &mut ::deno_core::v8::HandleScope<'a>, + ) -> Result<::deno_core::v8::Local<'a, ::deno_core::v8::Value>, Self::Error> { + ::deno_core::v8_static_strings! { + __v8_static_a = "__v8_static_a", __v8_static_e = "__v8_static_e", + __v8_static_b = "__v8_static_b" + } + thread_local! { + static __v8_a_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_b_eternal: ::deno_core::v8::Eternal<::deno_core::v8::String> = ::deno_core::v8::Eternal::empty(); + } + let __obj = ::deno_core::v8::Object::new(__scope); + { + 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(::deno_error::JsErrorBox::from_err)?; + __eternal.set(__scope, __key); + Ok::<_, ::deno_error::JsErrorBox>(__key) + } + })? + .cast::<::deno_core::v8::Value>(); + let __value = ::deno_core::convert::ToV8::to_v8(self.a, __scope) + .map_err(::deno_error::JsErrorBox::from_err)?; + __obj.set(__scope, __key, __value); + } + { + 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(::deno_error::JsErrorBox::from_err)?; + __eternal.set(__scope, __key); + Ok::<_, ::deno_error::JsErrorBox>(__key) + } + })? + .cast::<::deno_core::v8::Value>(); + let __value = ::deno_core::convert::ToV8::to_v8(self.d, __scope) + .map_err(::deno_error::JsErrorBox::from_err)?; + __obj.set(__scope, __key, __value); + } + { + 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(::deno_error::JsErrorBox::from_err)?; + __eternal.set(__scope, __key); + Ok::<_, ::deno_error::JsErrorBox>(__key) + } + })? + .cast::<::deno_core::v8::Value>(); + let __value = ::deno_core::convert::ToV8::to_v8(self.r#b, __scope) + .map_err(::deno_error::JsErrorBox::from_err)?; + __obj.set(__scope, __key, __value); + } + Ok(__obj.into()) + } +} diff --git a/ops/v8/test_cases/struct.rs b/ops/v8/test_cases/struct.rs new file mode 100644 index 000000000..08f2a98db --- /dev/null +++ b/ops/v8/test_cases/struct.rs @@ -0,0 +1,12 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +#![deny(warnings)] +deno_ops_compile_test_runner::prelude!(); + +#[derive(FromV8, ToV8)] +pub struct MyStruct { + a: deno_core::convert::Smi, + r#b: String, + #[v8(rename = "e")] + d: deno_core::convert::Smi, +} diff --git a/ops/v8/to.rs b/ops/v8/to.rs new file mode 100644 index 000000000..c05c88102 --- /dev/null +++ b/ops/v8/to.rs @@ -0,0 +1,73 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +use proc_macro2::TokenStream; +use quote::ToTokens; +use quote::quote; +use syn::Data; +use syn::DeriveInput; +use syn::Error; +use syn::parse2; +use syn::spanned::Spanned; + +pub fn to_v8(item: TokenStream) -> Result { + let input = parse2::(item)?; + let span = input.span(); + let ident = input.ident; + + let out = match input.data { + Data::Struct(data) => { + let (fields, pre) = super::structs::get_fields(span, data)?; + + let fields = fields.into_iter().map( + |super::structs::StructField { name, get_key, .. }| { + quote! { + { + let __key = #get_key; + + let __value = ::deno_core::convert::ToV8::to_v8( + self.#name, + __scope, + ).map_err(::deno_error::JsErrorBox::from_err)?; + + __obj.set(__scope, __key, __value); + } + } + }, + ); + + create_from_impl( + ident, + quote! { + #pre + + let __obj = ::deno_core::v8::Object::new(__scope); + + #(#fields)* + + Ok(__obj.into()) + }, + ) + } + Data::Enum(_) => { + return Err(Error::new(span, "Enums currently are not supported")); + } + Data::Union(_) => return Err(Error::new(span, "Unions are not supported")), + }; + + Ok(out) +} + +fn create_from_impl(ident: impl ToTokens, body: TokenStream) -> TokenStream { + quote! { + impl<'a> ::deno_core::convert::ToV8<'a> for #ident { + type Error = ::deno_error::JsErrorBox; + + fn to_v8( + self, + __scope: &mut ::deno_core::v8::HandleScope<'a>, + ) -> Result<::deno_core::v8::Local<'a, ::deno_core::v8::Value>, Self::Error> { + #body + } + } + } +}