diff --git a/oden-js-sys/src/static-functions.rs b/oden-js-sys/src/static-functions.rs index 6419d46e..fb40a012 100644 --- a/oden-js-sys/src/static-functions.rs +++ b/oden-js-sys/src/static-functions.rs @@ -52,7 +52,9 @@ extern "C" { fn JS_MakeException_real() -> JSValue; fn JS_MakeNull_real() -> JSValue; fn JS_MakeUndefined_real() -> JSValue; - fn JS_ValueGetPtr_real(v: JSValue) -> *mut ::std::os::raw::c_void; + fn JS_VALUE_GET_PTR_real(v: JSValue) -> *mut ::std::os::raw::c_void; + fn JS_VALUE_GET_INT_real(v: JSValue) -> ::std::os::raw::c_int; + fn JS_VALUE_GET_BOOL_real(v: JSValue) -> ::std::os::raw::c_int; } pub unsafe fn JS_ValueGetTag(v: JSValue) -> i32 { @@ -228,6 +230,14 @@ pub unsafe fn JS_MakeUndefined() -> JSValue { JS_MakeUndefined_real() } -pub unsafe fn JS_ValueGetPtr(v: JSValue) -> *mut ::std::os::raw::c_void { - JS_ValueGetPtr_real(v) +pub unsafe fn JS_VALUE_GET_PTR(v: JSValue) -> *mut ::std::os::raw::c_void { + JS_VALUE_GET_PTR_real(v) +} + +pub unsafe fn JS_VALUE_GET_INT(v: JSValue) -> ::std::os::raw::c_int { + JS_VALUE_GET_INT_real(v) +} + +pub unsafe fn JS_VALUE_GET_BOOL(v: JSValue) -> ::std::os::raw::c_int { + JS_VALUE_GET_BOOL_real(v) } diff --git a/oden-js-sys/static-functions.c b/oden-js-sys/static-functions.c index 26922beb..9917813e 100644 --- a/oden-js-sys/static-functions.c +++ b/oden-js-sys/static-functions.c @@ -137,6 +137,15 @@ JSValue JS_MakeUndefined_real() { return JS_UNDEFINED; } -void *JS_ValueGetPtr_real(JSValue val) { +void *JS_VALUE_GET_PTR_real(JSValue val) { return JS_VALUE_GET_PTR(val); } + +int JS_VALUE_GET_INT_real(JSValue v) { + return JS_VALUE_GET_INT(v); +} + +int JS_VALUE_GET_BOOL_real(JSValue v) { + return JS_VALUE_GET_BOOL(v); +} + diff --git a/oden-js/src/callback.rs b/oden-js/src/callback.rs index e7bbd345..55e7b03a 100644 --- a/oden-js/src/callback.rs +++ b/oden-js/src/callback.rs @@ -1,6 +1,6 @@ -use crate::{Class, ClassID, ContextRef, Error, ValueRef, ValueResult}; +use crate::{throw_string, Class, ClassID, ContextRef, Error, ValueRef, ValueResult}; use oden_js_sys as sys; -use std::ffi::{c_int, CString}; +use std::ffi::c_int; use std::panic::catch_unwind; pub trait Callback: Fn(&ContextRef, &ValueRef, &[&ValueRef]) -> ValueResult {} @@ -25,40 +25,6 @@ impl Class for CallbackObject { } } -fn throw_string(context: &ContextRef, message: String) -> sys::JSValue { - let ctx = context.ctx; - match context.new_string(&message) { - Ok(e) => unsafe { - // Because context.new_string yields an owned Value, and will - // clean it up on the way out, we need to explicitly DupValue a - // reference for the `Throw` to own. - let err = sys::JS_NewError(ctx); - if sys::JS_ValueGetTag(err) == sys::JS_TAG_EXCEPTION { - // GIVE UP; this is out of memory anyway things probably went - // wrong because of that. - return err; - } - - sys::JS_DupValue(ctx, e.val); // SetProperty takes ownership. - let prop = CString::new("message").unwrap(); - if sys::JS_SetPropertyStr(ctx, err, prop.as_ptr(), e.val) == -1 { - // Also an out of memory but we need to free the error object - // on our way out. - sys::JS_FreeValue(ctx, err); - return sys::JS_MakeException(); // JS_EXCEPTION - } - - sys::JS_Throw(ctx, err) - }, - Err(_) => unsafe { - sys::JS_Throw( - ctx, - sys::JS_NewString(ctx, "Errors within errors: embedded nulls in the description of the error that occurred".as_bytes().as_ptr() as *const i8), - ) - }, - } -} - fn callback_impl( ctx: *mut sys::JSContext, _this: sys::JSValue, @@ -103,7 +69,7 @@ where *ret } } - Err(Error::Exception(e)) => unsafe { + Err(Error::Exception(e, _)) => unsafe { // If we returned `Error::Exception` then we're propagating an // exception through the JS stack, just flip it. let exc = &e.val; diff --git a/oden-js/src/context.rs b/oden-js/src/context.rs index 8a748755..5e0f167c 100644 --- a/oden-js/src/context.rs +++ b/oden-js/src/context.rs @@ -106,6 +106,7 @@ impl ContextRef { unsafe { sys::JS_EnableBignumExt(self.ctx, if enable { 1 } else { 0 }) } } + /// Evaluate the specified JavaScript code as a module. pub fn load_module(&self, input: &str, filename: &str) -> Result { let val = self.eval(input, filename, EvalType::Module, EvalFlags::COMPILE_ONLY)?; assert!(val.is_module()); @@ -122,14 +123,8 @@ impl ContextRef { eval_type: EvalType, flags: EvalFlags, ) -> ValueResult { - let c_input = match CString::new(input) { - Ok(cs) => Ok(cs), - Err(_) => Err(Error::UnexpectedNul), - }?; - let c_filename = match CString::new(filename) { - Ok(cs) => Ok(cs), - Err(_) => Err(Error::UnexpectedNul), - }?; + let c_input = CString::new(input)?; + let c_filename = CString::new(filename)?; let eval_type = match eval_type { EvalType::Global => sys::JS_EVAL_TYPE_GLOBAL, @@ -150,14 +145,11 @@ impl ContextRef { /// Construct a new string atom. pub fn new_atom(&self, value: &str) -> Result { - let c_value = match CString::new(value) { - Ok(cs) => Ok(cs), - Err(_) => Err(Error::UnexpectedNul), - }?; + let c_value = CString::new(value)?; - let atom = unsafe { sys::JS_NewAtomLen(self.ctx, c_value.into_raw(), value.len()) }; + let atom = unsafe { sys::JS_NewAtomLen(self.ctx, c_value.as_ptr(), value.len()) }; if atom == sys::JS_ATOM_NULL { - return Err(Error::Exception(self.exception())); + return Err(self.exception_error()); } Ok(Atom::from_raw(atom, self)) @@ -241,13 +233,9 @@ impl ContextRef { /// Construct a new value from a string. pub fn new_string(&self, value: &str) -> ValueResult { - let c_value = match CString::new(value) { - Ok(cs) => Ok(cs), - Err(_) => Err(Error::UnexpectedNul), - }?; - + let c_value = CString::new(value)?; self.check_exception(unsafe { - sys::JS_NewStringLen(self.ctx, c_value.into_raw(), value.len()) + sys::JS_NewStringLen(self.ctx, c_value.as_ptr(), value.len()) }) } @@ -283,7 +271,7 @@ impl ContextRef { /// error value. Otherwise, just return success with the value, wrapped. pub(crate) fn check_exception(&self, val: sys::JSValue) -> ValueResult { if unsafe { sys::JS_ValueGetTag(val) } == sys::JS_TAG_EXCEPTION { - Err(Error::Exception(self.exception())) + Err(self.exception_error()) } else { Ok(Value::from_raw(val, self)) } @@ -296,6 +284,16 @@ impl ContextRef { pub(crate) fn exception(&self) -> Value { Value::from_raw(unsafe { sys::JS_GetException(self.ctx) }, self) } + + /// Fetch the exception value from the context, if any. This is not + /// public because anything that might raise an exception should be + /// returning a Result<> instead, to separate the exception flow from the + /// value flow. + pub(crate) fn exception_error(&self) -> Error { + let exc = self.exception(); + let desc = exc.to_string(&self).unwrap_or_else(|_| String::new()); + Error::Exception(exc, desc) + } } #[derive(Debug)] diff --git a/oden-js/src/conversion/into.rs b/oden-js/src/conversion/into.rs index c57e4671..dbb5cf81 100644 --- a/oden-js/src/conversion/into.rs +++ b/oden-js/src/conversion/into.rs @@ -116,7 +116,8 @@ impl TryIntoValue for Error { } Error::ConversionError(e) => Err(Error::ConversionError(e)), Error::RustFunctionError(e) => Err(Error::RustFunctionError(e)), - Error::Exception(v) => Err(Error::Exception(v.dup(ctx))), + Error::Exception(v, d) => Err(Error::Exception(v.dup(ctx), d)), + Error::OutOfMemory => Err(Error::OutOfMemory), } } } diff --git a/oden-js/src/lib.rs b/oden-js/src/lib.rs index 95ee5547..746bfd78 100644 --- a/oden-js/src/lib.rs +++ b/oden-js/src/lib.rs @@ -1,3 +1,5 @@ +use oden_js_sys as sys; +use std::ffi::{CString, NulError}; use thiserror::Error; mod atom; @@ -5,6 +7,7 @@ mod callback; mod class; mod context; mod conversion; +pub mod module; mod runtime; mod value; @@ -36,8 +39,51 @@ pub enum Error { ConversionError(String), #[error("an error occurred calling a rust function: {0}")] RustFunctionError(String), - #[error("an exception was thrown during evaluation")] - Exception(Value), + #[error("an exception was thrown during evaluation: {1}")] + Exception(Value, String), + #[error("out of memory")] + OutOfMemory, } + +impl From for Error { + fn from(_: NulError) -> Self { + Error::UnexpectedNul + } +} + pub type Result = core::result::Result; pub type ValueResult = core::result::Result; + +pub fn throw_string(context: &ContextRef, message: String) -> sys::JSValue { + let ctx = context.ctx; + match context.new_string(&message) { + Ok(e) => unsafe { + // Because context.new_string yields an owned Value, and will + // clean it up on the way out, we need to explicitly DupValue a + // reference for the `Throw` to own. + let err = sys::JS_NewError(ctx); + if sys::JS_ValueGetTag(err) == sys::JS_TAG_EXCEPTION { + // GIVE UP; this is out of memory anyway things probably went + // wrong because of that. + return err; + } + + sys::JS_DupValue(ctx, e.val); // SetProperty takes ownership. + let prop = CString::new("message").unwrap(); + if sys::JS_SetPropertyStr(ctx, err, prop.as_ptr(), e.val) == -1 { + // Also an out of memory but we need to free the error object + // on our way out. + sys::JS_FreeValue(ctx, err); + return sys::JS_MakeException(); // JS_EXCEPTION + } + + sys::JS_Throw(ctx, err) + }, + Err(_) => unsafe { + sys::JS_Throw( + ctx, + sys::JS_NewString(ctx, "Errors within errors: embedded nulls in the description of the error that occurred".as_bytes().as_ptr() as *const i8), + ) + }, + } +} diff --git a/oden-js/src/module.rs b/oden-js/src/module.rs new file mode 100644 index 00000000..4171ae59 --- /dev/null +++ b/oden-js/src/module.rs @@ -0,0 +1,346 @@ +use crate::{ + throw_string, Class, ClassID, ContextRef, Error, Result, TryIntoValue, Value, ValueRef, +}; +use oden_js_sys as sys; +use std::collections::HashSet; +use std::ffi::CString; + +/// A helper structure for declaring values. Use this in the implementation +/// of a NativeModule. (See the documentation for `NativeModule` for more +/// information.) +pub struct Declarations { + declarations: HashSet, +} + +impl Declarations { + pub(crate) fn new() -> Self { + Declarations { + declarations: HashSet::new(), + } + } + + /// Declare that a native module will eventually export a value named + /// `name`. + pub fn declare(&mut self, name: S) -> Result<&mut Self> + where + S: Into>, + { + self.declarations.insert(CString::new(name)?); + Ok(self) + } + + /// Apply all the declarations to the module. (Don't let user code see + /// the JSModuleDef, right?) + pub(crate) unsafe fn apply(self, ctx: &ContextRef, m: *mut sys::JSModuleDef) -> Result<()> { + for k in self.declarations { + let res = unsafe { sys::JS_AddModuleExport(ctx.ctx, m, k.into_raw()) }; + if res < 0 { + return Err(Error::OutOfMemory); + } + } + Ok(()) + } +} + +struct Export { + name: CString, + value: Value, +} + +/// A helper structure for exporting values. Use this in the implementation +/// of a NativeModule. (See the documentation for `NativeModule` for more +/// information.) +pub struct Exports<'ctx> { + context: &'ctx ContextRef, + exports: Vec, +} + +impl<'ctx> Exports<'ctx> { + pub(crate) fn new(context: &'ctx ContextRef) -> Self { + Exports { + context, + exports: Vec::new(), + } + } + + /// Export a value named `name` from the current module. You *must* have + /// provided the same name in a previous call to `Declarations::declare`. + pub fn export>, T: TryIntoValue>( + &mut self, + name: N, + value: T, + ) -> Result<&mut Self> { + let name = CString::new(name.into())?; + let value = value.try_into_value(&self.context)?; + self.export_value(name, &value) + } + + /// Export a value named `name` from the current module. You *must* have + /// provided the same name in a previous call to `Declarations::declare`. + pub fn export_value(&mut self, name: CString, value: &ValueRef) -> Result<&mut Self> { + self.exports.push(Export { + name, + value: value.dup(self.context), + }); + Ok(self) + } + + /// Actually export the values in the module. (Don't let user code see + /// the JSModuleDef!) + pub(crate) fn apply(self, module: *mut sys::JSModuleDef) -> Result<()> { + for export in self.exports { + let name = export.name; + let value = export.value; + + let res = unsafe { + // Ownership of name is retained + // Ownership of value is transfered. + sys::JS_DupValue(self.context.ctx, value.val); + sys::JS_SetModuleExport(self.context.ctx, module, name.as_ref().as_ptr(), value.val) + }; + + if res < 0 { + return Err(Error::OutOfMemory); + } + } + Ok(()) + } +} + +/// Implement this trait to implement a JavaScript module in Rust. +/// +/// JavaScript modules proceed in two phases. First, we resolve all the +/// imports, and once the import graph has been determined, then "execute" +/// the body of the modules to actually generate values. We reflect these +/// two phases here, in the `declare` and `define` methods. +/// +/// (You might find the `NativeModuleBuilder` structure easier to use.) +pub trait NativeModule { + /// Phase 1: Declare all the names you're going to export. Call + /// `declarations.declare` once for each value you will eventually export + /// in phase 2. + fn declare(&self, declarations: &mut Declarations) -> Result<()>; + + /// Phase 2: Define all the values you're going to define. Call + /// `exports.export` once for each value you declared in phase 1. + fn define<'ctx>(&self, context: &'ctx ContextRef, exports: &mut Exports<'ctx>) -> Result<()>; +} + +struct NativeModuleState { + module: T, +} + +impl NativeModuleState { + fn new(module: T) -> Self { + NativeModuleState { module } + } + + fn define(context: &ContextRef, m: *mut sys::JSModuleDef) -> Result<()> { + let import_meta = + context.check_exception(unsafe { sys::JS_GetImportMeta(context.ctx, m) })?; + let native_value = import_meta.get_property(context, "native_module")?; + let native = Self::try_from_value(&native_value)?; + + let mut exports = Exports::new(context); + native + .module + .define(context, &mut exports) + .and_then(|_| exports.apply(m)) + } +} + +impl Class for NativeModuleState { + fn class_id() -> &'static ClassID { + static ID: ClassID = ClassID::new("NativeModuleState"); + &ID + } +} + +unsafe extern "C" fn init_func( + ctx: *mut sys::JSContext, + m: *mut sys::JSModuleDef, +) -> std::os::raw::c_int { + let context = ContextRef::from_raw(ctx); + match NativeModuleState::::define(&context, m) { + Ok(_) => 0, + Err(Error::Exception(e, _)) => unsafe { + // If we returned `Error::Exception` then we're propagating an + // exception through the JS stack, just flip it. + let exc = &e.val; + sys::JS_DupValue(ctx, *exc); + sys::JS_Throw(ctx, *exc); + -1 + }, + Err(err) => { + throw_string(&context, err.to_string()); + -1 + } + } +} + +/// Define a new native module, with the provided name and +/// implementation. Once this succeeds, the specified module is ready to be +/// consumed by javascript, although the `define` method will not be called +/// until the first time it gets successfully imported somewhere. +pub fn define_native_module( + ctx: &ContextRef, + name: &str, + module: T, +) -> Result<()> { + let c_name = CString::new(name)?; + let m = unsafe { sys::JS_NewCModule(ctx.ctx, c_name.as_ptr(), Some(init_func::)) }; + if m.is_null() { + return Err(ctx.exception_error()); + } + + let mut declarations = Declarations::new(); + module + .declare(&mut declarations) + .and_then(|_| unsafe { declarations.apply(&ctx, m) })?; + + let native_value = NativeModuleState::new(module).into_value(ctx)?; + let mut import_meta = ctx.check_exception(unsafe { sys::JS_GetImportMeta(ctx.ctx, m) })?; + import_meta.set_property(ctx, "native_module", &native_value)?; + + Ok(()) +} + +struct GenericNativeModule { + exports: Vec, +} + +impl GenericNativeModule { + fn new(exports: Vec) -> Self { + GenericNativeModule { exports } + } +} + +impl NativeModule for GenericNativeModule { + fn declare(&self, declarations: &mut Declarations) -> Result<()> { + for e in self.exports.iter() { + declarations.declare(e.name.clone())?; + } + Ok(()) + } + + fn define<'ctx>(&self, _: &'ctx ContextRef, exports: &mut Exports<'ctx>) -> Result<()> { + for e in self.exports.iter() { + exports.export_value(e.name.clone(), &e.value)?; + } + Ok(()) + } +} + +/// A helper to define a native module, by defining it export-by-export. +pub struct NativeModuleBuilder<'ctx> { + exports: Exports<'ctx>, +} + +impl<'ctx> NativeModuleBuilder<'ctx> { + /// Construct a new native module builder, which will use the given + /// context to define members. + pub fn new(context: &'ctx ContextRef) -> Self { + NativeModuleBuilder { + exports: Exports::new(context), + } + } + + /// Define a new export that will be in the native module. + pub fn export>, T: TryIntoValue>( + &mut self, + name: N, + value: T, + ) -> Result<&mut Self> { + self.exports.export(name, value)?; + Ok(self) + } + + /// Finish constructing the native module. + pub fn build(&self, name: &str) -> Result<()> { + let context = self.exports.context; + let mut exports = Vec::new(); + for export in self.exports.exports.iter() { + exports.push(Export { + name: export.name.clone(), + value: export.value.dup(context), + }); + } + let generic_module = GenericNativeModule::new(exports); + define_native_module(context, name, generic_module) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Context, Runtime}; + + struct TestingNativeModule {} + + impl NativeModule for TestingNativeModule { + fn declare(&self, declarations: &mut Declarations) -> Result<()> { + declarations.declare("foo")?; + Ok(()) + } + + fn define<'ctx>(&self, _: &'ctx ContextRef, exports: &mut Exports<'ctx>) -> Result<()> { + exports.export("foo", 23)?; + Ok(()) + } + } + + #[test] + fn test_define_native_module() { + let runtime = Runtime::new(); + let context = Context::new(runtime); + + define_native_module(&context, "the_test", TestingNativeModule {}) + .expect("Module load should succeed"); + + let js_module = context + .load_module( + r#" +import { foo } from "the_test"; +export const my_foo = foo; +"#, + "test", + ) + .expect("Evaluation of the test script should succeed"); + + let my_foo = js_module + .get_module_export(&context, "my_foo") + .expect("Retrieving JS export should succeed"); + + assert_eq!(my_foo.to_string(&context).unwrap(), String::from("23")); + } + + #[test] + fn test_native_module_builder() { + let runtime = Runtime::new(); + let context = Context::new(runtime); + + NativeModuleBuilder::new(&context) + .export("foo", 123) + .expect("define should succeed") + .export("bar", 321) + .expect("define should succeed") + .build("the_test") + .expect("Module build should succeed"); + + let js_module = context + .load_module( + r#" +import { foo, bar } from "the_test"; +export const my_foo = foo + bar; +"#, + "test", + ) + .expect("Evaluation of the test script should succeed"); + + let my_foo = js_module + .get_module_export(&context, "my_foo") + .expect("Retrieving JS export should succeed"); + + assert_eq!(my_foo.to_string(&context).unwrap(), String::from("444")); + } +} diff --git a/oden-js/src/value.rs b/oden-js/src/value.rs index b6d88405..f8ce5ee3 100644 --- a/oden-js/src/value.rs +++ b/oden-js/src/value.rs @@ -125,7 +125,7 @@ impl ValueRef { let mut res: i32 = 0; let ret = sys::JS_ToInt32(ctx.ctx, &mut res, self.val); if ret < 0 { - Err(Error::Exception(ctx.exception())) + Err(ctx.exception_error()) } else { Ok(res) } @@ -137,7 +137,9 @@ impl ValueRef { let mut res: u32 = 0; let ret = sys::JS_ToUint32(ctx.ctx, &mut res, self.val); if ret < 0 { - Err(Error::Exception(ctx.exception())) + let exc = ctx.exception(); + let desc = exc.to_string(&ctx).unwrap_or_else(|_| String::new()); + Err(Error::Exception(exc, desc)) } else { Ok(res) } @@ -149,7 +151,7 @@ impl ValueRef { let mut res: i64 = 0; let ret = sys::JS_ToInt64(ctx.ctx, &mut res, self.val); if ret < 0 { - Err(Error::Exception(ctx.exception())) + Err(ctx.exception_error()) } else { Ok(res) } @@ -189,7 +191,7 @@ impl ValueRef { let mut res: f64 = 0.0; let ret = sys::JS_ToFloat64(ctx.ctx, &mut res, self.val); if ret < 0 { - Err(Error::Exception(ctx.exception())) + Err(ctx.exception_error()) } else { Ok(res) } @@ -257,7 +259,7 @@ impl ValueRef { sys::JS_DupValue(ctx.ctx, val.val); let result = sys::JS_SetProperty(ctx.ctx, self.val, prop.atom, val.val); if result == -1 { - Err(Error::Exception(ctx.exception())) + Err(ctx.exception_error()) } else { Ok(()) } @@ -290,7 +292,7 @@ impl ValueRef { let cstr = unsafe { let ptr = sys::JS_ToCStringLen2(ctx.ctx, std::ptr::null_mut(), self.val, 0); if ptr.is_null() { - return Err(Error::Exception(ctx.exception())); + return Err(ctx.exception_error()); } CStr::from_ptr(ptr) }; @@ -318,14 +320,10 @@ impl ValueRef { }); } - let c_value = match CString::new(export) { - Ok(cs) => Ok(cs), - Err(_) => Err(Error::UnexpectedNul), - }?; - + let c_value = CString::new(export)?; unsafe { - let module = sys::JS_ValueGetPtr(self.val) as *mut sys::JSModuleDef; - ctx.check_exception(sys::JS_GetModuleExport(ctx.ctx, module, c_value.into_raw())) + let module = sys::JS_VALUE_GET_PTR(self.val) as *mut sys::JSModuleDef; + ctx.check_exception(sys::JS_GetModuleExport(ctx.ctx, module, c_value.as_ptr())) } } @@ -342,7 +340,7 @@ impl ValueRef { } } -impl<'ctx> fmt::Debug for ValueRef { +impl fmt::Debug for ValueRef { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Value") .field("v", &self.val)