[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

@ -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<Value> {
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<Module> {
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<Module> {
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<Atom> {
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());
}

View file

@ -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)),
}
}
}

View file

@ -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<NulError> for Error {
@ -51,10 +53,26 @@ impl From<NulError> for Error {
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::IOError(e)
}
}
pub type Result<T> = core::result::Result<T, Error>;
pub type ValueResult = core::result::Result<Value, Error>;
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 {

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

@ -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<T: NativeModule>(
ctx: &ContextRef,
name: &str,
module: T,
) -> Result<()> {
) -> 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() {
@ -202,7 +203,7 @@ pub fn define_native_module<T: NativeModule>(
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<Module> {
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"));

View file

@ -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<dyn ModuleLoader>,
}
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<TLoader: ModuleLoader + 'static>(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 }

View file

@ -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<Value> {
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<Value> {
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);
}

15
src/graphics.js Normal file
View file

@ -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);
}

View file

@ -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);
}

View file

@ -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 {

View file

@ -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<f32>,
sh: Option<f32>,
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<GraphicsCommand>) -> oden_js::Result<Self> {
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<f32>,
sh: Option<f32>| {
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 })
}