Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 48 additions & 2 deletions core/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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>(
Expand Down Expand Up @@ -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>(
Expand Down Expand Up @@ -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<Self, Self::Error> {
crate::_ops::to_external_option(&value)
.ok_or_else(|| JsErrorBox::type_error("Invalid external option"))
}
}

impl<'s, T> ToV8<'s> for Option<T>
where
T: ToV8<'s>,
Expand Down Expand Up @@ -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<u8>,
r#b: String,
#[v8(rename = "e")]
d: Smi<u32>,
}

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();
}
}
4 changes: 3 additions & 1 deletion core/fast_string.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -121,7 +122,8 @@ impl Display for FastStaticString {
}
}

#[derive(Debug)]
#[derive(Debug, JsError)]
#[class(generic)]
pub struct FastStringV8AllocationError;

impl std::error::Error for FastStringV8AllocationError {}
Expand Down
3 changes: 3 additions & 0 deletions core/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions ops/compile_test_runner/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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");
}
}
17 changes: 17 additions & 0 deletions ops/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions ops/op2/test_cases/sync/from_v8.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, '_>,
Expand Down
4 changes: 2 additions & 2 deletions ops/op2/test_cases/sync/to_v8.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
92 changes: 92 additions & 0 deletions ops/v8/from.rs
Original file line number Diff line number Diff line change
@@ -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<TokenStream, Error> {
let input = parse2::<DeriveInput>(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::<Vec<_>>();

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<Self, Self::Error> {
#body
}
}
}
}
60 changes: 60 additions & 0 deletions ops/v8/mod.rs
Original file line number Diff line number Diff line change
@@ -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<Item = &'a syn::Attribute>,
) -> 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::<Ident, syn::Token![,]>::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
})
})
}
}
Loading
Loading