[oden][oden-js] Rework modules
Damn this is a lot
This commit is contained in:
parent
aa90cea4a3
commit
db8a5f8eed
12 changed files with 280 additions and 105 deletions
53
oden-js/src/module/loader.rs
Normal file
53
oden-js/src/module/loader.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
use super::ModuleRef;
|
||||
use crate::{throw_error, ContextRef, Error, Result};
|
||||
use oden_js_sys as sys;
|
||||
use std::path::Path;
|
||||
|
||||
pub enum ModuleSource<'a> {
|
||||
Native(&'a ModuleRef),
|
||||
JavaScript(String),
|
||||
}
|
||||
|
||||
pub trait ModuleLoader {
|
||||
fn load(&mut self, context: &ContextRef, name: &str) -> Result<ModuleSource>;
|
||||
}
|
||||
|
||||
pub struct DefaultModuleLoader {}
|
||||
|
||||
impl DefaultModuleLoader {
|
||||
pub fn new() -> DefaultModuleLoader {
|
||||
DefaultModuleLoader {}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModuleLoader for DefaultModuleLoader {
|
||||
fn load(&mut self, _context: &ContextRef, name: &str) -> Result<ModuleSource> {
|
||||
// Attempt to open the file.
|
||||
let path = Path::new(name);
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(str) => Ok(ModuleSource::JavaScript(str)),
|
||||
Err(e) => Err(Error::IOError(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn load_module(
|
||||
context: &ContextRef,
|
||||
name: &str,
|
||||
loader: &mut Box<dyn ModuleLoader>,
|
||||
) -> *mut sys::JSModuleDef {
|
||||
match loader.load(context, name) {
|
||||
Ok(ModuleSource::Native(native)) => native.module,
|
||||
Ok(ModuleSource::JavaScript(js)) => match context.eval_module(&js, name) {
|
||||
Ok(v) => v.module,
|
||||
Err(e) => {
|
||||
throw_error(context, e);
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
throw_error(context, e);
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
}
|
||||
}
|
||||
76
oden-js/src/module/mod.rs
Normal file
76
oden-js/src/module/mod.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
use crate::{ContextRef, Result, Runtime, Value};
|
||||
use oden_js_sys as sys;
|
||||
use std::ffi::CString;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
pub mod loader;
|
||||
pub mod native;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ModuleRef {
|
||||
module: *mut sys::JSModuleDef,
|
||||
}
|
||||
|
||||
impl ModuleRef {
|
||||
pub(crate) fn from_raw(module: *mut sys::JSModuleDef) -> ModuleRef {
|
||||
ModuleRef { module }
|
||||
}
|
||||
|
||||
fn eval_self(&self, context: &ContextRef) -> Result<()> {
|
||||
let result = unsafe {
|
||||
let v = sys::JSValue {
|
||||
u: sys::JSValueUnion {
|
||||
ptr: self.module as *mut _,
|
||||
},
|
||||
tag: sys::JS_TAG_MODULE as i64,
|
||||
};
|
||||
|
||||
sys::JS_DupValue(context.ctx, v);
|
||||
sys::JS_EvalFunction(context.ctx, v)
|
||||
};
|
||||
context.check_exception(result)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_export(&self, context: &ContextRef, export: &str) -> Result<Value> {
|
||||
self.eval_self(context)?;
|
||||
let c_value = CString::new(export)?;
|
||||
unsafe {
|
||||
context.check_exception(sys::JS_GetModuleExport(
|
||||
context.ctx,
|
||||
self.module,
|
||||
c_value.as_ptr(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Module {
|
||||
m: ModuleRef,
|
||||
|
||||
#[allow(dead_code)]
|
||||
runtime: Runtime,
|
||||
}
|
||||
|
||||
impl Module {
|
||||
pub(crate) fn from_raw(module: *mut sys::JSModuleDef, runtime: Runtime) -> Module {
|
||||
Module {
|
||||
m: ModuleRef::from_raw(module),
|
||||
runtime,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Module {
|
||||
type Target = ModuleRef;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.m
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Module {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.m
|
||||
}
|
||||
}
|
||||
347
oden-js/src/module/native.rs
Normal file
347
oden-js/src/module/native.rs
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
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<CString>,
|
||||
}
|
||||
|
||||
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<S>(&mut self, name: S) -> Result<&mut Self>
|
||||
where
|
||||
S: Into<Vec<u8>>,
|
||||
{
|
||||
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<Export>,
|
||||
}
|
||||
|
||||
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<N: Into<Vec<u8>>, 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<T: NativeModule> {
|
||||
module: T,
|
||||
}
|
||||
|
||||
impl<T: NativeModule> NativeModuleState<T> {
|
||||
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<T: NativeModule> Class for NativeModuleState<T> {
|
||||
fn class_id() -> &'static ClassID {
|
||||
static ID: ClassID = ClassID::new("NativeModuleState");
|
||||
&ID
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn init_func<T: NativeModule>(
|
||||
ctx: *mut sys::JSContext,
|
||||
m: *mut sys::JSModuleDef,
|
||||
) -> std::os::raw::c_int {
|
||||
let context = ContextRef::from_raw(ctx);
|
||||
match NativeModuleState::<T>::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<T: NativeModule>(
|
||||
ctx: &ContextRef,
|
||||
name: &str,
|
||||
module: T,
|
||||
) -> Result<Module> {
|
||||
let c_name = CString::new(name)?;
|
||||
let m = unsafe { sys::JS_NewCModule(ctx.ctx, c_name.as_ptr(), Some(init_func::<T>)) };
|
||||
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<Export>,
|
||||
}
|
||||
|
||||
impl GenericNativeModule {
|
||||
fn new(exports: Vec<Export>) -> 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<N: Into<Vec<u8>>, 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<Module> {
|
||||
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"));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue