diff --git a/src/web_executor/js.rs b/src/web_executor/js.rs index 1d4bc469a..c5dd798de 100644 --- a/src/web_executor/js.rs +++ b/src/web_executor/js.rs @@ -6,6 +6,7 @@ use crate::web_executor::js::v8::V8Engine; pub use compile::*; pub use context::*; pub use function::*; +pub use object::*; pub use runtime::*; pub use value::*; pub use value_conversion::*; @@ -15,6 +16,7 @@ use crate::types::Result; mod compile; mod context; mod function; +mod object; mod runtime; pub mod v8; mod value; @@ -45,22 +47,6 @@ lazy_static! { pub static ref RUNTIME: Mutex> = Mutex::new(V8Engine::new()); } -pub trait JSObject { - type Value: JSValue; - type Function: JSFunction; - type FunctionVariadic: JSFunctionVariadic; - - fn set_property(&self, name: &str, value: &Self::Value) -> Result<()>; - - fn get_property(&self, name: &str) -> Result; - - fn call_method(&self, name: &str, args: &[&Self::Value]) -> Result; - - fn set_method(&self, name: &str, func: &Self::Function) -> Result<()>; - - fn set_method_variadic(&self, name: &str, func: &Self::FunctionVariadic) -> Result<()>; -} - pub trait JSArray { type Value: JSValue; diff --git a/src/web_executor/js/object.rs b/src/web_executor/js/object.rs new file mode 100644 index 000000000..432d62a0b --- /dev/null +++ b/src/web_executor/js/object.rs @@ -0,0 +1,47 @@ +use crate::web_executor::js::{JSContext, JSFunction, JSFunctionVariadic, JSValue}; + +pub trait JSObject { + type Value: JSValue; + type Function: JSFunction; + type FunctionVariadic: JSFunctionVariadic; + type GetterCB: JSGetterCallback; + type SetterCB: JSSetterCallback; + + fn set_property(&self, name: &str, value: &Self::Value) -> crate::types::Result<()>; + + fn get_property(&self, name: &str) -> crate::types::Result; + + fn call_method(&self, name: &str, args: &[&Self::Value]) -> crate::types::Result; + + fn set_method(&self, name: &str, func: &Self::Function) -> crate::types::Result<()>; + + fn set_method_variadic( + &self, + name: &str, + func: &Self::FunctionVariadic, + ) -> crate::types::Result<()>; + + #[allow(clippy::type_complexity)] + fn set_property_accessor( + &self, + name: &str, + getter: Box, + setter: Box, + ) -> crate::types::Result<()>; +} + +pub trait JSGetterCallback { + type Value: JSValue; + type Context: JSContext; + + fn context(&mut self) -> &mut Self::Context; + fn ret(&mut self, value: Self::Value); +} + +pub trait JSSetterCallback { + type Value: JSValue; + type Context: JSContext; + + fn context(&mut self) -> &mut Self::Context; + fn value(&mut self) -> &Self::Value; +} diff --git a/src/web_executor/js/v8/context.rs b/src/web_executor/js/v8/context.rs index b7f37ff0f..6a733d66d 100644 --- a/src/web_executor/js/v8/context.rs +++ b/src/web_executor/js/v8/context.rs @@ -16,10 +16,10 @@ use crate::web_executor::js::{JSContext, JSError, JSRuntime}; /// SAFETY: This is NOT thread safe, as the rest of the engine is not thread safe. /// This struct uses `NonNull` internally to store pointers to the V8Context "values" in one struct. pub struct V8Ctx<'a> { - isolate: NonNull, - handle_scope: NonNull>, - ctx: NonNull>, - context_scope: NonNull>>, + pub(crate) isolate: NonNull, + pub(crate) handle_scope: NonNull>, + pub(crate) ctx: NonNull>, + pub(crate) context_scope: NonNull>>, copied: Copied, } @@ -41,8 +41,9 @@ impl Copied { } } -enum HandleScopeType<'a> { +pub(crate) enum HandleScopeType<'a> { WithContext(HandleScope<'a>), + WithContextRef(&'a mut HandleScope<'a>), WithoutContext(HandleScope<'a, ()>), CallbackScope(CallbackScope<'a>), } @@ -52,11 +53,12 @@ impl<'a> HandleScopeType<'a> { Self::WithoutContext(HandleScope::new(isolate)) } - fn get(&mut self) -> &mut HandleScope<'a, ()> { + pub(crate) fn get(&mut self) -> &mut HandleScope<'a, ()> { match self { Self::WithContext(scope) => scope, Self::WithoutContext(scope) => scope, Self::CallbackScope(scope) => scope, + Self::WithContextRef(scope) => scope, } } } @@ -149,9 +151,13 @@ impl<'a> V8Ctx<'a> { } } -pub(crate) fn ctx_from_function_callback_info(mut scope: CallbackScope) -> Result { +pub(crate) fn ctx_from_scope_isolate<'a>( + scope: HandleScopeType<'a>, + ctx: Local<'a, v8::Context>, + isolate: NonNull, +) -> std::result::Result, (HandleScopeType<'a>, Error)> { let mut v8_ctx = V8Ctx { - isolate: NonNull::dangling(), + isolate, handle_scope: NonNull::dangling(), ctx: NonNull::dangling(), context_scope: NonNull::dangling(), @@ -160,25 +166,30 @@ pub(crate) fn ctx_from_function_callback_info(mut scope: CallbackScope) -> Resul handle_scope: false, ctx: false, context_scope: false, - }, //TODO: figure out what to deallocate + }, }; - let ctx = Box::new(scope.get_current_context()); + let ctx = Box::new(ctx); let Some(ctx) = NonNull::new(Box::into_raw(ctx)) else { - return Err(Error::JS(JSError::Compile( - "Failed to create context".to_owned(), - ))); + return Err(( + scope, + Error::JS(JSError::Compile("Failed to create context".to_owned())), + )); }; v8_ctx.ctx = ctx; - let scope = Box::new(HandleScopeType::CallbackScope(scope)); + let scope = Box::new(scope); - let Some(scope) = NonNull::new(Box::into_raw(scope)) else { - return Err(Error::JS(JSError::Compile( - "Failed to create handle scope".to_owned(), - ))); + let raw_scope = Box::into_raw(scope); + let Some(scope) = NonNull::new(raw_scope) else { + //SAFETY: we just created this, so it is safe to convert it back to a Box + let scope = unsafe { Box::from_raw(raw_scope) }; + return Err(( + *scope, + Error::JS(JSError::Compile("Failed to create handle scope".to_owned())), + )); }; v8_ctx.handle_scope = scope; @@ -189,9 +200,15 @@ pub(crate) fn ctx_from_function_callback_info(mut scope: CallbackScope) -> Resul )); let Some(ctx_scope) = NonNull::new(Box::into_raw(ctx_scope)) else { - return Err(Error::JS(JSError::Compile( - "Failed to create context scope".to_owned(), - ))); + //SAFETY: we just created this, so it is safe to convert it back to a Box + let scope = unsafe { Box::from_raw(v8_ctx.handle_scope.as_ptr()) }; + + return Err(( + *scope, + Error::JS(JSError::Compile( + "Failed to create context scope".to_owned(), + )), + )); }; v8_ctx.context_scope = ctx_scope; @@ -200,6 +217,30 @@ pub(crate) fn ctx_from_function_callback_info(mut scope: CallbackScope) -> Resul // Ok(v8_ctx) } +pub(crate) fn ctx_from_function_callback_info( + mut scope: CallbackScope, + isolate: NonNull, +) -> std::result::Result { + let ctx = scope.get_current_context(); + let scope = HandleScopeType::CallbackScope(scope); + + ctx_from_scope_isolate(scope, ctx, isolate) +} + +pub(crate) fn ctx_from<'a>( + scope: &'a mut HandleScope, + isolate: NonNull, +) -> std::result::Result, (HandleScopeType<'a>, Error)> { + let ctx = scope.get_current_context(); + + //SAFETY: This can only shorten the lifetime of the scope, which is fine. (we borrow it for 'a and it is '2, which will always be longer than 'a) + let scope = unsafe { std::mem::transmute(scope) }; + + let scope = HandleScopeType::WithContextRef(scope); + + ctx_from_scope_isolate(scope, ctx, isolate) +} + impl Drop for V8Ctx<'_> { fn drop(&mut self) { // order is important here: context scope, then handle scope (and ctx), then isolate diff --git a/src/web_executor/js/v8/function.rs b/src/web_executor/js/v8/function.rs index 518aca631..c8b7e13f2 100644 --- a/src/web_executor/js/v8/function.rs +++ b/src/web_executor/js/v8/function.rs @@ -171,9 +171,13 @@ extern "C" fn callback(info: *const FunctionCallbackInfo) { let data = unsafe { &mut *(external.value() as *mut CallbackWrapper) }; - let ctx = match ctx_from_function_callback_info(unsafe { CallbackScope::new(info) }) { + let ctx = match ctx_from_function_callback_info( + unsafe { CallbackScope::new(info) }, + data.ctx.borrow().isolate, + ) { Ok(scope) => scope, - Err(e) => { + Err((mut st, e)) => { + let scope = st.get(); let Some(e) = v8::String::new(scope, &e.to_string()) else { eprintln!("failed to create exception string\nexception was: {e}"); //TODO: replace with our own logger return; @@ -421,13 +425,13 @@ impl<'a> V8FunctionVariadic<'a> { extern "C" fn callback_variadic(info: *const FunctionCallbackInfo) { let info = unsafe { &*info }; - let scope = &mut unsafe { CallbackScope::new(info) }; + let mut scope = unsafe { CallbackScope::new(info) }; let args = FunctionCallbackArguments::from_function_callback_info(info); let rv = ReturnValue::from_function_callback_info(info); let external = match >::try_from(args.data()) { Ok(external) => external, Err(e) => { - let Some(e) = v8::String::new(scope, &e.to_string()) else { + let Some(e) = v8::String::new(&mut scope, &e.to_string()) else { eprintln!("failed to create exception string\nexception was: {e}"); //TODO: replace with our own logger return; }; @@ -438,9 +442,10 @@ extern "C" fn callback_variadic(info: *const FunctionCallbackInfo) { let data = unsafe { &mut *(external.value() as *mut CallbackWrapperVariadic) }; - let ctx = match ctx_from_function_callback_info(unsafe { CallbackScope::new(info) }) { + let ctx = match ctx_from_function_callback_info(scope, data.ctx.borrow().isolate) { Ok(scope) => scope, - Err(e) => { + Err((mut st, e)) => { + let scope = st.get(); let Some(e) = v8::String::new(scope, &e.to_string()) else { eprintln!("failed to create exception string\nexception was: {e}"); //TODO: replace with our own logger return; diff --git a/src/web_executor/js/v8/object.rs b/src/web_executor/js/v8/object.rs index decdda7c9..af428186b 100644 --- a/src/web_executor/js/v8/object.rs +++ b/src/web_executor/js/v8/object.rs @@ -1,20 +1,72 @@ -use v8::{Local, Object}; +use std::ffi::c_void; + +use v8::{ + AccessorConfiguration, External, HandleScope, Local, Name, Object, PropertyCallbackArguments, + ReturnValue, Value, +}; use crate::types::{Error, Result}; use crate::web_executor::js::v8::{ - FromContext, V8Context, V8Ctx, V8Function, V8FunctionVariadic, V8Value, + ctx_from, FromContext, V8Context, V8Ctx, V8Function, V8FunctionVariadic, V8Value, +}; +use crate::web_executor::js::{ + JSArray, JSError, JSGetterCallback, JSObject, JSSetterCallback, JSValue, }; -use crate::web_executor::js::{JSArray, JSError, JSObject, JSValue}; pub struct V8Object<'a> { ctx: V8Context<'a>, pub(crate) value: Local<'a, Object>, } +pub struct GetterCallback<'a, 'r> { + ctx: V8Context<'a>, + ret: &'r mut V8Value<'a>, +} + +impl<'a> JSGetterCallback for GetterCallback<'a, '_> { + type Value = V8Value<'a>; + type Context = V8Context<'a>; + + fn context(&mut self) -> &mut Self::Context { + &mut self.ctx + } + + fn ret(&mut self, value: Self::Value) { + *self.ret = value; + } +} + +pub struct SetterCallback<'a, 'v> { + ctx: V8Context<'a>, + value: &'v V8Value<'a>, +} + +impl<'a, 'v> JSSetterCallback for SetterCallback<'a, 'v> { + type Value = V8Value<'a>; + type Context = V8Context<'a>; + + fn context(&mut self) -> &mut Self::Context { + &mut self.ctx + } + + fn value(&mut self) -> &'v Self::Value { + self.value + } +} + +struct GetterSetter<'a, 'r> { + ctx: V8Context<'a>, + getter: Box)>, + setter: Box)>, +} + impl<'a> JSObject for V8Object<'a> { type Value = V8Value<'a>; type Function = V8Function<'a>; type FunctionVariadic = V8FunctionVariadic<'a>; + + type GetterCB = GetterCallback<'a, 'a>; + type SetterCB = SetterCallback<'a, 'a>; fn set_property(&self, name: &str, value: &Self::Value) -> Result<()> { let Some(name) = v8::String::new(self.ctx.borrow_mut().scope(), name) else { return Err(Error::JS(JSError::Generic( @@ -63,7 +115,7 @@ impl<'a> JSObject for V8Object<'a> { let function = Local::::try_from(func).unwrap(); - let args: Vec> = args.iter().map(|v| v.value).collect(); + let args: Vec> = args.iter().map(|v| v.value).collect(); let try_catch = &mut v8::TryCatch::new(self.ctx.borrow_mut().scope()); @@ -136,6 +188,132 @@ impl<'a> JSObject for V8Object<'a> { Ok(()) } } + + fn set_property_accessor( + &self, + name: &str, + getter: Box, + setter: Box, + ) -> Result<()> { + let name = v8::String::new(self.ctx.borrow_mut().scope(), name) + .ok_or_else(|| Error::JS(JSError::Generic("failed to create a string".to_owned())))?; + + let scope = self.ctx.borrow_mut().scope(); + + let gs = Box::new(GetterSetter { + ctx: self.ctx.clone(), + getter, + setter, + }); + + let data = External::new(scope, Box::into_raw(gs) as *mut c_void); + + let config = AccessorConfiguration::new( + |scope: &mut HandleScope, + name: Local, + args: PropertyCallbackArguments, + mut rv: ReturnValue| { + let external = match Local::::try_from(args.data()) { + Ok(external) => external, + Err(e) => { + let Some(e) = v8::String::new(scope, &e.to_string()) else { + eprintln!("failed to create exception string\nexception was: {e}"); //TODO: replace with our own logger + return; + }; + scope.throw_exception(Local::from(e)); + return; + } + }; + + let gs = unsafe { &*(external.value() as *const GetterSetter) }; + + let ctx = scope.get_current_context(); + + let ctx = match ctx_from(scope, gs.ctx.borrow().isolate) { + Ok(ctx) => ctx, + Err((mut st, e)) => { + let scope = st.get(); + let Some(e) = v8::String::new(scope, &e.to_string()) else { + eprintln!("failed to create exception string\nexception was: {e}"); //TODO: replace with our own logger + return; + }; + scope.throw_exception(Local::from(e)); + return; + } + }; + + let mut ret = match V8Value::new_undefined(ctx.clone()) { + Ok(ret) => ret, + Err(e) => { + let scope = ctx.borrow_mut().scope(); + let Some(e) = v8::String::new(scope, &e.to_string()) else { + eprintln!("failed to create exception string\nexception was: {e}"); //TODO: replace with our own logger + return; + }; + scope.throw_exception(Local::from(e)); + return; + } + }; + + let mut gc = GetterCallback { ctx, ret: &mut ret }; + + (gs.getter)(&mut gc); + + rv.set(ret.value); + }, + ) + .setter( + |scope: &mut HandleScope, + name: Local, + value: Local, + args: PropertyCallbackArguments, + rv: ReturnValue| { + let external = match Local::::try_from(args.data()) { + Ok(external) => external, + Err(e) => { + let Some(e) = v8::String::new(scope, &e.to_string()) else { + eprintln!("failed to create exception string\nexception was: {e}"); //TODO: replace with our own logger + return; + }; + scope.throw_exception(Local::from(e)); + return; + } + }; + + let gs = unsafe { &*(external.value() as *const GetterSetter) }; + + let mut ctx = scope.get_current_context(); + + let ctx = match ctx_from(scope, gs.ctx.borrow().isolate) { + Ok(ctx) => ctx, + Err((mut st, e)) => { + let scope = st.get(); + let Some(e) = v8::String::new(scope, &e.to_string()) else { + eprintln!("failed to create exception string\nexception was: {e}"); //TODO: replace with our own logger + return; + }; + scope.throw_exception(Local::from(e)); + return; + } + }; + + let mut val = V8Value::from_value(ctx.clone(), value); + + let mut sc = SetterCallback { + ctx, + value: &mut val, + }; + + (gs.setter)(&mut sc); + }, + ) + .data(Local::from(data)); + + self.value + .set_accessor_with_configuration(scope, name.into(), config); + + Ok(()) + } } impl<'a> FromContext<'a, Local<'a, Object>> for V8Object<'a> {