diff --git a/oden-js/src/context.rs b/oden-js/src/context.rs index 8f2dd651..300a5915 100644 --- a/oden-js/src/context.rs +++ b/oden-js/src/context.rs @@ -1,24 +1,12 @@ use crate::{ - callback::new_fn, conversion::RustFunction, Atom, ClassID, Error, Result, Runtime, Value, - ValueRef, ValueResult, + callback::new_fn, conversion::RustFunction, module::Module, Atom, ClassID, Error, Result, + Runtime, Value, ValueRef, ValueResult, }; use bitflags::bitflags; use oden_js_sys as sys; use std::ffi::CString; use std::ops::{Deref, DerefMut}; -/// Different ways to evaluate JavaScript. See the various `eval` methods on -/// `ContextRef`. -pub enum EvalType { - /// Global code, i.e., just plain old JavaScript running in the most - /// boring, traditional context. - Global, - - /// Module code, i.e., code running under the context of an `import`, or - /// referenced in HTML as `type=module`. - Module, -} - bitflags! { pub struct EvalFlags: u32 { const NONE = 0; @@ -31,8 +19,8 @@ bitflags! { const STRIP = sys::JS_EVAL_FLAG_STRIP; /// Compile but do not run. The result is a value with a value type - /// of `ValueType::Module` or `ValueType::FunctionBytecode`, and - /// which can be executed with `value.eval_function`. + /// of `ValueType::FunctionBytecode`, and which can be executed with + /// `value.eval_function`. const COMPILE_ONLY = sys::JS_EVAL_FLAG_COMPILE_ONLY; /// Don't include the stack frames before this eval in the Error() @@ -51,6 +39,10 @@ impl ContextRef { ContextRef { ctx } } + pub fn get_runtime(&self) -> Runtime { + Runtime::from_raw(unsafe { sys::JS_GetRuntime(self.ctx) }) + } + pub fn is_registered_class(&self, id: &ClassID) -> bool { let id = id.get(); let is_registered = unsafe { sys::JS_IsRegisteredClass(sys::JS_GetRuntime(self.ctx), id) }; @@ -106,30 +98,42 @@ 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()); - val.eval_function(self)?; - - Ok(val) + /// Evaluate the specified JavaScript code. + pub fn eval(&self, input: &str, filename: &str, flags: EvalFlags) -> ValueResult { + self.eval_internal(input, filename, sys::JS_EVAL_TYPE_GLOBAL, flags) } - /// Evaluate the specified JavaScript code. - pub fn eval( + /// Evaluate the specified JavaScript code as a module. + pub fn eval_module(&self, input: &str, filename: &str) -> Result { + let val = self.eval_internal( + input, + filename, + sys::JS_EVAL_TYPE_MODULE, + EvalFlags::COMPILE_ONLY, + )?; + assert!(val.is_module()); + unsafe { + // NOTE: This might be stupid but we're trying to + // intentionally leak the value here; since the module + // itself has a lifetime unconstrained by traditional value + // semantics. (This is a weird edge in QuickJS.) + sys::JS_DupValue(self.ctx, val.val); + Ok(Module::from_raw( + sys::JS_VALUE_GET_PTR(val.val) as *mut sys::JSModuleDef, + self.get_runtime(), + )) + } + } + + fn eval_internal( &self, input: &str, filename: &str, - eval_type: EvalType, + eval_type: u32, flags: EvalFlags, ) -> ValueResult { 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, - EvalType::Module => sys::JS_EVAL_TYPE_MODULE, - }; let flags_bits: i32 = (eval_type | flags.bits).try_into().unwrap(); unsafe { @@ -143,6 +147,18 @@ impl ContextRef { } } + /// Import a module by name. + pub fn import_module(&self, name: &str, base: &str) -> Result { + let name = CString::new(name)?; + let base = CString::new(base)?; + let module = unsafe { sys::JS_RunModule(self.ctx, name.as_ptr(), base.as_ptr()) }; + if module.is_null() { + return Err(self.exception_error()); + } + + Ok(Module::from_raw(module, self.get_runtime())) + } + /// Construct a new string atom. pub fn new_atom(&self, value: &str) -> Result { let c_value = CString::new(value)?; @@ -377,9 +393,7 @@ mod tests { let rt = Runtime::new(); let ctx = Context::new(rt); - let val = ctx - .eval("1+1", "script", EvalType::Global, EvalFlags::NONE) - .unwrap(); + let val = ctx.eval("1+1", "script", EvalFlags::NONE).unwrap(); assert_eq!(String::from("2"), val.to_string(&ctx).unwrap()); } @@ -390,7 +404,7 @@ mod tests { let ctx = Context::new(rt); let compiled = ctx - .eval("1 + 1", "script", EvalType::Global, EvalFlags::COMPILE_ONLY) + .eval("1 + 1", "script", EvalFlags::COMPILE_ONLY) .expect("Unable to compile code"); let result = compiled .eval_function(&ctx) @@ -410,9 +424,7 @@ mod tests { let val = ctx.new_i32(12).unwrap(); go.set_property(&ctx, "foo", &val).unwrap(); - let result = ctx - .eval("foo + 3", "script", EvalType::Global, EvalFlags::NONE) - .unwrap(); + let result = ctx.eval("foo + 3", "script", EvalFlags::NONE).unwrap(); assert_eq!(String::from("15"), result.to_string(&ctx).unwrap()); } @@ -454,9 +466,7 @@ mod tests { let val = vr.as_ref().unwrap(); go.set_property(&ctx, "val", val).unwrap(); - let result = ctx - .eval(expr, "script", EvalType::Global, EvalFlags::NONE) - .unwrap(); + let result = ctx.eval(expr, "script", EvalFlags::NONE).unwrap(); assert_eq!(String::from(*expected), result.to_string(&ctx).unwrap()); } } @@ -479,12 +489,7 @@ mod tests { } let result = ctx - .eval( - "foo().toUpperCase()", - "script", - EvalType::Global, - EvalFlags::NONE, - ) + .eval("foo().toUpperCase()", "script", EvalFlags::NONE) .unwrap(); assert_eq!(String::from("UNSAFE"), result.to_string(&ctx).unwrap()); } @@ -493,17 +498,16 @@ mod tests { fn modules_with_exports() { let ctx = Context::new(Runtime::new()); let module = ctx - .load_module("const foo = 123; export { foo };", "main.js") + .eval_module("const foo = 123; export { foo };", "main.js") .expect("Could not load!"); - assert_eq!(module.value_type(), ValueType::Module); let foo = module - .get_module_export(&ctx, "foo") + .get_export(&ctx, "foo") .expect("Could not get export"); assert_eq!(String::from("123"), foo.to_string(&ctx).unwrap()); let bar = module - .get_module_export(&ctx, "bar") + .get_export(&ctx, "bar") .expect("Could not get export"); assert!(bar.is_undefined()); } diff --git a/oden-js/src/conversion/into.rs b/oden-js/src/conversion/into.rs index 3dd5915f..5eab2909 100644 --- a/oden-js/src/conversion/into.rs +++ b/oden-js/src/conversion/into.rs @@ -118,6 +118,7 @@ impl TryIntoValue for Error { Error::RustFunctionError(e) => Err(Error::RustFunctionError(e)), Error::Exception(v, d) => Err(Error::Exception(v.dup(ctx), d)), Error::OutOfMemory => Err(Error::OutOfMemory), + Error::IOError(e) => Err(Error::IOError(e)), } } } diff --git a/oden-js/src/lib.rs b/oden-js/src/lib.rs index 746bfd78..bbcaa840 100644 --- a/oden-js/src/lib.rs +++ b/oden-js/src/lib.rs @@ -13,7 +13,7 @@ mod value; pub use atom::{Atom, AtomRef}; pub use class::{Class, ClassID}; -pub use context::{Context, ContextRef, EvalFlags, EvalType}; +pub use context::{Context, ContextRef, EvalFlags}; pub use conversion::*; pub use runtime::Runtime; pub use value::{Value, ValueRef, ValueType}; @@ -43,6 +43,8 @@ pub enum Error { Exception(Value, String), #[error("out of memory")] OutOfMemory, + #[error("an io error occurred: {0}")] + IOError(std::io::Error), } impl From for Error { @@ -51,10 +53,26 @@ impl From for Error { } } +impl From for Error { + fn from(e: std::io::Error) -> Self { + Error::IOError(e) + } +} + pub type Result = core::result::Result; pub type ValueResult = core::result::Result; -pub fn throw_string(context: &ContextRef, message: String) -> sys::JSValue { +pub(crate) fn throw_error(context: &ContextRef, error: Error) -> sys::JSValue { + match error { + Error::Exception(v, _) => unsafe { + sys::JS_DupValue(context.ctx, v.val); + sys::JS_Throw(context.ctx, v.val) + }, + other => throw_string(context, other.to_string()), + } +} + +pub(crate) fn throw_string(context: &ContextRef, message: String) -> sys::JSValue { let ctx = context.ctx; match context.new_string(&message) { Ok(e) => unsafe { diff --git a/oden-js/src/module/loader.rs b/oden-js/src/module/loader.rs new file mode 100644 index 00000000..f21c61f1 --- /dev/null +++ b/oden-js/src/module/loader.rs @@ -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; +} + +pub struct DefaultModuleLoader {} + +impl DefaultModuleLoader { + pub fn new() -> DefaultModuleLoader { + DefaultModuleLoader {} + } +} + +impl ModuleLoader for DefaultModuleLoader { + fn load(&mut self, _context: &ContextRef, name: &str) -> Result { + // 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, +) -> *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(); + } + } +} diff --git a/oden-js/src/module/mod.rs b/oden-js/src/module/mod.rs new file mode 100644 index 00000000..1934e733 --- /dev/null +++ b/oden-js/src/module/mod.rs @@ -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 { + 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 + } +} diff --git a/oden-js/src/module.rs b/oden-js/src/module/native.rs similarity index 97% rename from oden-js/src/module.rs rename to oden-js/src/module/native.rs index 4171ae59..c65bda95 100644 --- a/oden-js/src/module.rs +++ b/oden-js/src/module/native.rs @@ -1,3 +1,4 @@ +use super::Module; use crate::{ throw_string, Class, ClassID, ContextRef, Error, Result, TryIntoValue, Value, ValueRef, }; @@ -186,7 +187,7 @@ pub fn define_native_module( ctx: &ContextRef, name: &str, module: T, -) -> Result<()> { +) -> 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() { @@ -202,7 +203,7 @@ pub fn define_native_module( let mut import_meta = ctx.check_exception(unsafe { sys::JS_GetImportMeta(ctx.ctx, m) })?; import_meta.set_property(ctx, "native_module", &native_value)?; - Ok(()) + Ok(Module::from_raw(m, ctx.get_runtime())) } struct GenericNativeModule { @@ -256,7 +257,7 @@ impl<'ctx> NativeModuleBuilder<'ctx> { } /// Finish constructing the native module. - pub fn build(&self, name: &str) -> Result<()> { + pub fn build(&self, name: &str) -> Result { let context = self.exports.context; let mut exports = Vec::new(); for export in self.exports.exports.iter() { @@ -298,7 +299,7 @@ mod tests { .expect("Module load should succeed"); let js_module = context - .load_module( + .eval_module( r#" import { foo } from "the_test"; export const my_foo = foo; @@ -308,7 +309,7 @@ export const my_foo = foo; .expect("Evaluation of the test script should succeed"); let my_foo = js_module - .get_module_export(&context, "my_foo") + .get_export(&context, "my_foo") .expect("Retrieving JS export should succeed"); assert_eq!(my_foo.to_string(&context).unwrap(), String::from("23")); @@ -328,7 +329,7 @@ export const my_foo = foo; .expect("Module build should succeed"); let js_module = context - .load_module( + .eval_module( r#" import { foo, bar } from "the_test"; export const my_foo = foo + bar; @@ -338,7 +339,7 @@ export const my_foo = foo + bar; .expect("Evaluation of the test script should succeed"); let my_foo = js_module - .get_module_export(&context, "my_foo") + .get_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/runtime.rs b/oden-js/src/runtime.rs index ca21535b..4250ffff 100644 --- a/oden-js/src/runtime.rs +++ b/oden-js/src/runtime.rs @@ -1,8 +1,29 @@ +use crate::module::loader::{load_module, DefaultModuleLoader, ModuleLoader}; +use crate::ContextRef; use oden_js_sys as sys; use std::cell::RefCell; +use std::ffi::CStr; struct PrivateState { refs: u64, + loader: Box, +} + +impl PrivateState { + unsafe extern "C" fn module_loader( + ctx: *mut sys::JSContext, + path: *const i8, + opaque: *mut std::os::raw::c_void, + ) -> *mut sys::JSModuleDef { + let path = match CStr::from_ptr(path).to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + + let state = opaque as *mut PrivateState; + let context = ContextRef::from_raw(ctx); + load_module(&context, path, &mut (*state).loader) + } } #[derive(Debug)] @@ -12,10 +33,19 @@ pub struct Runtime { impl Runtime { pub fn new() -> Runtime { - let state = Box::new(RefCell::new(PrivateState { refs: 1 })); + Self::with_loader(DefaultModuleLoader::new()) + } + + pub fn with_loader(loader: TLoader) -> Runtime { + let state = Box::new(RefCell::new(PrivateState { + refs: 1, + loader: Box::new(loader), + })); let rt = unsafe { let rt = sys::JS_NewRuntime(); - sys::JS_SetRuntimeOpaque(rt, Box::into_raw(state) as *mut _); + let state = Box::into_raw(state) as *mut _; + sys::JS_SetRuntimeOpaque(rt, state); + sys::JS_SetModuleLoaderFunc(rt, None, Some(PrivateState::module_loader), state); rt }; Runtime { rt } diff --git a/oden-js/src/value.rs b/oden-js/src/value.rs index 87450366..52180e8d 100644 --- a/oden-js/src/value.rs +++ b/oden-js/src/value.rs @@ -1,6 +1,6 @@ use crate::{AtomRef, ContextRef, Error, Result, Runtime, RustFunction}; use oden_js_sys as sys; -use std::ffi::{CStr, CString}; +use std::ffi::CStr; use std::fmt; use std::ops::{Deref, DerefMut}; @@ -312,21 +312,6 @@ impl ValueRef { Ok(result) } - pub fn get_module_export(&self, ctx: &ContextRef, export: &str) -> Result { - if self.value_type() != ValueType::Module { - return Err(Error::InvalidType { - expected: ValueType::Bool, - found: self.value_type(), - }); - } - - let c_value = CString::new(export)?; - unsafe { - 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())) - } - } - pub fn call(&self, ctx: &ContextRef) -> Result { unsafe { ctx.check_exception(sys::JS_Call( @@ -414,7 +399,7 @@ impl fmt::Debug for Value { #[cfg(test)] mod tests { use super::*; - use crate::{Context, EvalFlags, EvalType, Runtime}; + use crate::{Context, EvalFlags, Runtime}; #[test] fn value_type() { @@ -438,9 +423,7 @@ mod tests { ]; for (expr, expected) in tests.into_iter() { - let val = ctx - .eval(expr, "script", EvalType::Global, EvalFlags::STRICT) - .unwrap(); + let val = ctx.eval(expr, "script", EvalFlags::STRICT).unwrap(); assert_eq!(*expected, val.value_type(), "for {}", expr); } diff --git a/src/graphics.js b/src/graphics.js new file mode 100644 index 00000000..39e08156 --- /dev/null +++ b/src/graphics.js @@ -0,0 +1,15 @@ +import * as core from "graphics-core"; + +function cls(r, g, b) { + core.cls(r, g, b); +} + +function print(...args) { + core.print(args.join(" ")); +} + +function spr(x, y, w, h, sx, sy, sw = undefined, sh = undefined) { + sw = sw | w; + sh = sh | h; + core.spr(xy, w, h, sx, sy, sw, sh); +} diff --git a/src/main.js b/src/main.js index 8b21b959..bf72eb7a 100644 --- a/src/main.js +++ b/src/main.js @@ -1,4 +1,4 @@ -import { cls, print, spr } from "graphics"; +import { cls, print, spr } from "./src/graphics"; export function init() { print("Hello world!"); @@ -8,5 +8,5 @@ export function update() {} export function draw() { cls(0.1, 0.2, 0.3); - spr(0, 0, 0.5, 0.5, 0, 0, 0.5, 0.5); + spr(0, 0, 0.5, 0.5, 0, 0); } diff --git a/src/script.rs b/src/script.rs index a34d149d..320d1c25 100644 --- a/src/script.rs +++ b/src/script.rs @@ -30,17 +30,17 @@ impl ScriptContext { let js = include_str!("main.js"); let module = context - .load_module(js, "main.js") + .eval_module(js, "main.js") .expect("Unable to load main"); let init = module - .get_module_export(&context, "init") + .get_export(&context, "init") .expect("Unable to fetch init"); let update = module - .get_module_export(&context, "update") + .get_export(&context, "update") .expect("Unable to fetch update"); let draw = module - .get_module_export(&context, "draw") + .get_export(&context, "draw") .expect("Unable to fetch draw"); ScriptContext { diff --git a/src/script/graphics.rs b/src/script/graphics.rs index 4ad1560a..af708126 100644 --- a/src/script/graphics.rs +++ b/src/script/graphics.rs @@ -1,4 +1,4 @@ -use oden_js::{module, ContextRef, Value, ValueRef, ValueResult}; +use oden_js::{module, ContextRef, Value, ValueResult}; use std::sync::mpsc::Sender; use std::sync::Arc; @@ -37,13 +37,7 @@ impl GraphicsImpl { GraphicsImpl { sender } } - fn print_fn(&self, ctx: &ContextRef, args: &[&ValueRef]) -> ValueResult { - let mut text = String::with_capacity(128); - for arg in args { - let v = arg.to_string(ctx)?; - text.push_str(&v); - } - + fn print_fn(&self, ctx: &ContextRef, text: String) -> ValueResult { let _ = self .sender .send(GraphicsCommand::Print(PrintCommand { text })); @@ -67,8 +61,8 @@ impl GraphicsImpl { h: f32, u: f32, v: f32, - sw: Option, - sh: Option, + sw: f32, + sh: f32, ) -> ValueResult { let _ = self.sender.send(GraphicsCommand::Sprite(SpriteCommand { x, @@ -77,8 +71,8 @@ impl GraphicsImpl { h, u, v, - sw: sw.unwrap_or(w), - sh: sh.unwrap_or(h), + sw, + sh, })); Ok(Value::undefined(ctx)) } @@ -91,12 +85,12 @@ pub struct GraphicsAPI { impl GraphicsAPI { pub fn define(ctx: &ContextRef, sender: Sender) -> oden_js::Result { let gfx = Arc::new(GraphicsImpl::new(sender)); - let mut builder = module::NativeModuleBuilder::new(ctx); + let mut builder = module::native::NativeModuleBuilder::new(ctx); { let gfx = gfx.clone(); builder.export( "print", - ctx.new_dynamic_fn(move |ctx, _, args| gfx.print_fn(ctx, args))?, + ctx.new_fn(move |ctx: &ContextRef, t: String| gfx.print_fn(ctx, t))?, )?; } { @@ -120,14 +114,14 @@ impl GraphicsAPI { h: f32, u: f32, v: f32, - sw: Option, - sh: Option| { + sw: f32, + sh: f32| { gfx.spr_fn(ctx, x, y, w, h, u, v, sw, sh) }, )?, )?; } - builder.build("graphics")?; + builder.build("graphics-core")?; Ok(GraphicsAPI { gfx }) }