[oden][oden-js] Rework modules

Damn this is a lot
This commit is contained in:
John Doty 2023-06-24 08:45:39 -07:00
parent aa90cea4a3
commit db8a5f8eed
12 changed files with 280 additions and 105 deletions

View 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
View 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
}
}

View 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"));
}
}