use super::Module; 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(Module::from_raw(m, ctx.get_runtime())) } 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 .eval_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_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 .eval_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_export(&context, "my_foo") .expect("Retrieving JS export should succeed"); assert_eq!(my_foo.to_string(&context).unwrap(), String::from("444")); } }