580 lines
19 KiB
Rust
580 lines
19 KiB
Rust
use crate::{
|
|
callback::new_fn, conversion::RustFunction, module::Module, Atom, ClassID, Error, Promise,
|
|
Result, Runtime, Value, ValueRef, ValueResult,
|
|
};
|
|
use bitflags::bitflags;
|
|
use oden_js_sys as sys;
|
|
use std::ffi::CString;
|
|
use std::ops::{Deref, DerefMut};
|
|
|
|
bitflags! {
|
|
pub struct EvalFlags: u32 {
|
|
const NONE = 0;
|
|
|
|
/// Force evaluation in "strict" mode, as if "use strict" were at the
|
|
/// top of the file.
|
|
const STRICT = sys::JS_EVAL_FLAG_STRICT;
|
|
|
|
/// Force "strip" mode.
|
|
const STRIP = sys::JS_EVAL_FLAG_STRIP;
|
|
|
|
/// Compile but do not run. The result is a value with a value type
|
|
/// 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()
|
|
/// backtraces.
|
|
const BACKTRACE_BARRIER = sys::JS_EVAL_FLAG_BACKTRACE_BARRIER;
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct ContextRef {
|
|
pub(crate) ctx: *mut sys::JSContext,
|
|
}
|
|
|
|
// TODO: Should all these require mutability to enforce single-threadedness?
|
|
impl ContextRef {
|
|
pub(crate) fn from_raw(ctx: *mut sys::JSContext) -> Self {
|
|
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) };
|
|
is_registered != 0
|
|
}
|
|
|
|
pub fn add_intrinsic_base_objects(&mut self) {
|
|
unsafe { sys::JS_AddIntrinsicBaseObjects(self.ctx) }
|
|
}
|
|
pub fn add_intrinsic_date(&mut self) {
|
|
unsafe { sys::JS_AddIntrinsicDate(self.ctx) }
|
|
}
|
|
pub fn add_intrinsic_eval(&mut self) {
|
|
unsafe { sys::JS_AddIntrinsicEval(self.ctx) }
|
|
}
|
|
pub fn add_intrinsic_string_normalize(&mut self) {
|
|
unsafe { sys::JS_AddIntrinsicStringNormalize(self.ctx) }
|
|
}
|
|
pub fn add_intrinsic_regexp_compiler(&mut self) {
|
|
unsafe { sys::JS_AddIntrinsicRegExpCompiler(self.ctx) }
|
|
}
|
|
pub fn add_intrinsic_regexp(&mut self) {
|
|
unsafe { sys::JS_AddIntrinsicRegExp(self.ctx) }
|
|
}
|
|
pub fn add_intrinsic_json(&mut self) {
|
|
unsafe { sys::JS_AddIntrinsicJSON(self.ctx) }
|
|
}
|
|
pub fn add_intrinsic_proxy(&mut self) {
|
|
unsafe { sys::JS_AddIntrinsicProxy(self.ctx) }
|
|
}
|
|
pub fn add_intrinsic_map_set(&mut self) {
|
|
unsafe { sys::JS_AddIntrinsicMapSet(self.ctx) }
|
|
}
|
|
pub fn add_intrinsic_typed_arrays(&mut self) {
|
|
unsafe { sys::JS_AddIntrinsicTypedArrays(self.ctx) }
|
|
}
|
|
pub fn add_intrinsic_promise(&mut self) {
|
|
unsafe { sys::JS_AddIntrinsicPromise(self.ctx) }
|
|
}
|
|
pub fn add_intrinsic_bigint(&mut self) {
|
|
unsafe { sys::JS_AddIntrinsicBigInt(self.ctx) }
|
|
}
|
|
pub fn add_intrinsic_bigfloat(&mut self) {
|
|
unsafe { sys::JS_AddIntrinsicBigFloat(self.ctx) }
|
|
}
|
|
pub fn add_intrinsic_bigdecimal(&mut self) {
|
|
unsafe { sys::JS_AddIntrinsicBigDecimal(self.ctx) }
|
|
}
|
|
pub fn add_intrinsic_operators(&mut self) {
|
|
unsafe { sys::JS_AddIntrinsicOperators(self.ctx) }
|
|
}
|
|
pub fn enable_bignum_ext(&mut self, enable: bool) {
|
|
unsafe { sys::JS_EnableBignumExt(self.ctx, if enable { 1 } else { 0 }) }
|
|
}
|
|
|
|
/// 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 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: u32,
|
|
flags: EvalFlags,
|
|
) -> ValueResult {
|
|
let c_input = CString::new(input)?;
|
|
let c_filename = CString::new(filename)?;
|
|
let flags_bits: i32 = (eval_type | flags.bits).try_into().unwrap();
|
|
|
|
unsafe {
|
|
self.check_exception(sys::JS_Eval(
|
|
self.ctx,
|
|
c_input.into_raw(),
|
|
input.len(),
|
|
c_filename.into_raw(),
|
|
flags_bits,
|
|
))
|
|
}
|
|
}
|
|
|
|
/// 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, base.as_ptr(), name.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)?;
|
|
|
|
let atom = unsafe { sys::JS_NewAtomLen(self.ctx, c_value.as_ptr(), value.len()) };
|
|
if atom == sys::JS_ATOM_NULL {
|
|
return Err(self.exception_error());
|
|
}
|
|
|
|
Ok(Atom::from_raw(atom, self))
|
|
}
|
|
|
|
/// Construct a new value of type object.
|
|
pub fn new_object(&self) -> ValueResult {
|
|
self.check_exception(unsafe { sys::JS_NewObject(self.ctx) })
|
|
}
|
|
|
|
/// Construct a new value from a boolean.
|
|
pub fn new_bool<T>(&self, value: T) -> ValueResult
|
|
where
|
|
T: Into<bool>,
|
|
{
|
|
self.check_exception(unsafe { sys::JS_NewBool(self.ctx, value.into()) })
|
|
}
|
|
|
|
/// Construct a new value that wraps a strongly-typed closure.
|
|
pub fn new_fn<F>(&self, func: impl RustFunction<F> + 'static) -> ValueResult {
|
|
self.new_dynamic_fn(move |c, _, a| func.call(c, a))
|
|
}
|
|
|
|
/// Construct a new value that wraps a dynamically-typed closure.
|
|
pub fn new_dynamic_fn<F>(&self, func: F) -> ValueResult
|
|
where
|
|
F: 'static + Fn(&ContextRef, &ValueRef, &[&ValueRef]) -> ValueResult,
|
|
{
|
|
// Constructing a new function is complicated enough that it needs to
|
|
// be out of line.
|
|
self.check_exception(new_fn(self, func))
|
|
}
|
|
|
|
/// Construct a new value from an int32.
|
|
pub fn new_i32<T>(&self, value: T) -> ValueResult
|
|
where
|
|
T: Into<i32>,
|
|
{
|
|
self.check_exception(unsafe { sys::JS_NewInt32(self.ctx, value.into()) })
|
|
}
|
|
|
|
/// Construct a new value from a u32.
|
|
///
|
|
/// This returns a value of type Int32 if the value fits into an int32,
|
|
/// otherwise it returns a value of type Float64.
|
|
pub fn new_u32<T>(&self, value: T) -> ValueResult
|
|
where
|
|
T: Into<u32>,
|
|
{
|
|
self.check_exception(unsafe { sys::JS_NewUint32(self.ctx, value.into()) })
|
|
}
|
|
|
|
/// Construct a new value from a float64.
|
|
pub fn new_f64<T>(&self, value: T) -> ValueResult
|
|
where
|
|
T: Into<f64>,
|
|
{
|
|
self.check_exception(unsafe { sys::JS_NewFloat64(self.ctx, value.into()) })
|
|
}
|
|
|
|
/// Construct a new BigInt from an i64
|
|
pub fn new_i64<T>(&self, value: T) -> ValueResult
|
|
where
|
|
T: Into<i64>,
|
|
{
|
|
self.check_exception(unsafe { sys::JS_NewBigInt64(self.ctx, value.into()) })
|
|
}
|
|
|
|
/// Construct a new BigInt from an u64
|
|
pub fn new_u64<T>(&self, value: T) -> ValueResult
|
|
where
|
|
T: Into<u64>,
|
|
{
|
|
self.check_exception(unsafe { sys::JS_NewBigUint64(self.ctx, value.into()) })
|
|
}
|
|
|
|
/// Construct a new array value.
|
|
pub fn new_array(&self) -> ValueResult {
|
|
self.check_exception(unsafe { sys::JS_NewArray(self.ctx) })
|
|
}
|
|
|
|
/// Construct a new value from a string.
|
|
pub fn new_string(&self, value: &str) -> ValueResult {
|
|
let c_value = CString::new(value)?;
|
|
self.check_exception(unsafe {
|
|
sys::JS_NewStringLen(self.ctx, c_value.as_ptr(), value.len())
|
|
})
|
|
}
|
|
|
|
/// Get the null value.
|
|
pub fn null(&self) -> Value {
|
|
let v = sys::JSValue {
|
|
u: sys::JSValueUnion {
|
|
ptr: std::ptr::null_mut(),
|
|
},
|
|
tag: sys::JS_TAG_NULL as i64,
|
|
};
|
|
Value::from_raw(v, self)
|
|
}
|
|
|
|
/// Get the undefined value.
|
|
pub fn undefined(&self) -> Value {
|
|
let v = sys::JSValue {
|
|
u: sys::JSValueUnion {
|
|
ptr: std::ptr::null_mut(),
|
|
},
|
|
tag: sys::JS_TAG_UNDEFINED as i64,
|
|
};
|
|
Value::from_raw(v, self)
|
|
}
|
|
|
|
/// Construct a new promise.
|
|
pub fn new_promise(&self) -> Result<(Value, Promise)> {
|
|
self.get_runtime().new_promise(self)
|
|
}
|
|
|
|
/// Construct a new exception object, suitable for throwing.
|
|
pub fn new_error(&self, message: &str) -> Value {
|
|
let e = match self.new_string(message) {
|
|
Ok(e) => e,
|
|
Err(_) => match self.new_string("INTERNAL ERROR: Embedded NUL in message") {
|
|
Ok(e) => e,
|
|
|
|
// Faulting this hard is inexcusable.
|
|
Err(_) => return self.exception(),
|
|
},
|
|
};
|
|
unsafe {
|
|
let err = Value::from_raw(sys::JS_NewError(self.ctx), self);
|
|
|
|
// NOTE: Throughout this function we work at the lower-level
|
|
// error handling stuff because the errors are easier to
|
|
// manage. (We know how it can fail!)
|
|
if sys::JS_ValueGetTag(err.val) == sys::JS_TAG_EXCEPTION {
|
|
// GIVE UP; This is out of memory anyway things probably
|
|
// went wrong because of that. We'll return *that*
|
|
// exception.
|
|
return self.exception();
|
|
}
|
|
|
|
sys::JS_DupValue(self.ctx, e.val); // SetProperty takes ownership.
|
|
let prop = CString::new("message").unwrap();
|
|
if sys::JS_SetPropertyStr(self.ctx, err.val, prop.as_ptr(), e.val) == -1 {
|
|
// As before, we're just going to take the exception from
|
|
// the context, and drop the one we were trying to create
|
|
// on the floor.
|
|
return self.exception();
|
|
}
|
|
|
|
// We put the message in, we can return the value.
|
|
err
|
|
}
|
|
}
|
|
|
|
/// Fetch the global object for the context.
|
|
pub fn global_object(&self) -> ValueResult {
|
|
self.check_exception(unsafe { sys::JS_GetGlobalObject(self.ctx) })
|
|
}
|
|
|
|
/// Check the value to see if it is the special marker for an exception,
|
|
/// and if so grab the exception from the context and return it in an
|
|
/// error value. Otherwise, just return success with the value, wrapped.
|
|
pub(crate) fn check_exception(&self, val: sys::JSValue) -> ValueResult {
|
|
if unsafe { sys::JS_ValueGetTag(val) } == sys::JS_TAG_EXCEPTION {
|
|
Err(self.exception_error())
|
|
} else {
|
|
Ok(Value::from_raw(val, self))
|
|
}
|
|
}
|
|
|
|
/// Fetch the exception value from the context, if any. This is not
|
|
/// public because anything that might raise an exception should be
|
|
/// returning a Result<> instead, to separate the exception flow from the
|
|
/// value flow.
|
|
pub(crate) fn exception(&self) -> Value {
|
|
Value::from_raw(unsafe { sys::JS_GetException(self.ctx) }, self)
|
|
}
|
|
|
|
/// Fetch the exception value from the context, if any. This is not
|
|
/// public because anything that might raise an exception should be
|
|
/// returning a Result<> instead, to separate the exception flow from the
|
|
/// value flow.
|
|
pub(crate) fn exception_error(&self) -> Error {
|
|
let exc = self.exception();
|
|
let desc = exc.to_string(&self).unwrap_or_else(|_| String::new());
|
|
let stack = exc
|
|
.get_property(&self, "stack")
|
|
.and_then(|stack| stack.to_string(&self))
|
|
.unwrap_or_else(|_| String::new());
|
|
|
|
Error::Exception(exc, desc, stack)
|
|
}
|
|
|
|
/// Process all pending async jobs. This includes all promise resolutions.
|
|
pub fn process_all_jobs(&self) -> Result<()> {
|
|
// TODO: SAFETY
|
|
// This is unsafe because multiple contexts can be sharing the same runtime and cause
|
|
// a race condition on the underlying runtime.
|
|
loop {
|
|
let mut ctx1: *mut sys::JSContext = std::ptr::null_mut();
|
|
let err = unsafe { sys::JS_ExecutePendingJob(sys::JS_GetRuntime(self.ctx), &mut ctx1) };
|
|
if err == 0 {
|
|
break;
|
|
} else if err < 0 {
|
|
return Err(ContextRef::from_raw(ctx1).exception_error());
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct Context {
|
|
value: ContextRef,
|
|
runtime: Runtime,
|
|
}
|
|
|
|
impl Context {
|
|
/// Construct a new JavaScript context with many of the useful instrinisc
|
|
/// functions defined. Objects created by this context can be shared
|
|
/// across contexts belonging to the same runtime.
|
|
///
|
|
/// This function behaves as if the following methods have been called:
|
|
///
|
|
/// - `add_intrinsic_base_objects`
|
|
/// - `add_intrinsic_date`
|
|
/// - `add_intrinsic_eval`
|
|
/// - `add_intrinsic_string_normalize`
|
|
/// - `add_intrinsic_regexp`
|
|
/// - `add_intrinsic_json`
|
|
/// - `add_intrinsic_proxy`
|
|
/// - `add_intrinsic_map_set`
|
|
/// - `add_intrinsic_typed_arrays`
|
|
/// - `add_intrinsic_promise`
|
|
/// - `add_intrinsic_bignum`
|
|
///
|
|
/// If you don't want those objects, call `new_raw`, and then add the
|
|
/// intrinsics that you want.
|
|
pub fn new(runtime: Runtime) -> Context {
|
|
let value = unsafe { ContextRef::from_raw(sys::JS_NewContext(runtime.rt)) };
|
|
Context { value, runtime }
|
|
}
|
|
|
|
/// Construct a new JavaScript context without any intrinsic
|
|
/// objects. Objects created by this context can be shared across
|
|
/// contexts belonging to the same runtime.
|
|
///
|
|
/// You will probably want to call one or more of the `add_intrinsic_`
|
|
/// functions to add useful types to the runtime.
|
|
pub fn new_raw(runtime: Runtime) -> Context {
|
|
let value = unsafe { ContextRef::from_raw(sys::JS_NewContextRaw(runtime.rt)) };
|
|
Context { value, runtime }
|
|
}
|
|
|
|
/// Get the runtime underlying this context.
|
|
pub fn runtime(&self) -> &Runtime {
|
|
&self.runtime
|
|
}
|
|
}
|
|
|
|
impl Deref for Context {
|
|
type Target = ContextRef;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.value
|
|
}
|
|
}
|
|
|
|
impl DerefMut for Context {
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
&mut self.value
|
|
}
|
|
}
|
|
|
|
impl Drop for Context {
|
|
fn drop(&mut self) {
|
|
unsafe {
|
|
sys::JS_FreeContext(self.value.ctx);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::{ValueRef, ValueType};
|
|
|
|
#[test]
|
|
fn basic_evaluation() {
|
|
let rt = Runtime::new();
|
|
let ctx = Context::new(rt);
|
|
|
|
let val = ctx.eval("1+1", "script", EvalFlags::NONE).unwrap();
|
|
|
|
assert_eq!(String::from("2"), val.to_string(&ctx).unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn eval_compiled() {
|
|
let rt = Runtime::new();
|
|
let ctx = Context::new(rt);
|
|
|
|
let compiled = ctx
|
|
.eval("1 + 1", "script", EvalFlags::COMPILE_ONLY)
|
|
.expect("Unable to compile code");
|
|
let result = compiled
|
|
.eval_function(&ctx)
|
|
.expect("Unable to execute compiled function");
|
|
|
|
assert_eq!(String::from("2"), result.to_string(&ctx).unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn global_object() {
|
|
let rt = Runtime::new();
|
|
let ctx = Context::new(rt);
|
|
|
|
let mut go = ctx.global_object().unwrap();
|
|
assert_eq!(ValueType::Object, go.value_type());
|
|
|
|
let val = ctx.new_i32(12).unwrap();
|
|
go.set_property(&ctx, "foo", &val).unwrap();
|
|
|
|
let result = ctx.eval("foo + 3", "script", EvalFlags::NONE).unwrap();
|
|
assert_eq!(String::from("15"), result.to_string(&ctx).unwrap());
|
|
}
|
|
|
|
fn cb(ctx: &ContextRef, _this: &ValueRef, args: &[&ValueRef]) -> ValueResult {
|
|
Ok(args[1].dup(ctx))
|
|
}
|
|
|
|
#[test]
|
|
fn new_objects() {
|
|
let rt = Runtime::new();
|
|
let ctx = Context::new(rt);
|
|
|
|
let mut go = ctx.global_object().unwrap();
|
|
|
|
let tests = &[
|
|
(ctx.new_u32(300u32), "val - 2", "298"),
|
|
(ctx.new_i32(12), "val + 10", "22"),
|
|
(ctx.new_f64(12.3), "val + .2", "12.5"),
|
|
(ctx.new_string("asdf"), "val.toUpperCase()", "ASDF"),
|
|
(ctx.new_bool(true), "val ? 'yes' : 'no'", "yes"),
|
|
(ctx.new_bool(false), "val ? 'yes' : 'no'", "no"),
|
|
(ctx.new_i64(-222), "val", "-222"),
|
|
(ctx.new_u64(222u64), "val", "222"),
|
|
(ctx.new_array(), "val.length", "0"),
|
|
(
|
|
ctx.new_dynamic_fn(|c, _, a: &[&ValueRef]| Ok(a[1].dup(c))),
|
|
"val(1,2)",
|
|
"2",
|
|
),
|
|
(ctx.new_dynamic_fn(cb), "val(1,2)", "2"),
|
|
(
|
|
ctx.new_fn(|_: &ContextRef, x: i32, y: i32| x * y),
|
|
"val(3,3)",
|
|
"9",
|
|
),
|
|
];
|
|
|
|
for (vr, expr, expected) in tests.into_iter() {
|
|
let val = vr.as_ref().unwrap();
|
|
go.set_property(&ctx, "val", val).unwrap();
|
|
|
|
let result = ctx.eval(expr, "script", EvalFlags::NONE).unwrap();
|
|
assert_eq!(String::from(*expected), result.to_string(&ctx).unwrap());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn real_closures() {
|
|
let rt = Runtime::new();
|
|
let ctx = Context::new(rt);
|
|
|
|
let return_value = ctx.new_string("unsafe").unwrap();
|
|
{
|
|
let mut go = ctx.global_object().unwrap();
|
|
go.set_property(
|
|
&ctx,
|
|
"foo",
|
|
&ctx.new_dynamic_fn(move |c, _, _| Ok(return_value.dup(c)))
|
|
.unwrap(),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
let result = ctx
|
|
.eval("foo().toUpperCase()", "script", EvalFlags::NONE)
|
|
.unwrap();
|
|
assert_eq!(String::from("UNSAFE"), result.to_string(&ctx).unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn modules_with_exports() {
|
|
let ctx = Context::new(Runtime::new());
|
|
let module = ctx
|
|
.eval_module("const foo = 123; export { foo };", "main.js")
|
|
.expect("Could not load!");
|
|
|
|
let foo = module
|
|
.get_export(&ctx, "foo")
|
|
.expect("Could not get export");
|
|
assert_eq!(String::from("123"), foo.to_string(&ctx).unwrap());
|
|
|
|
let bar = module
|
|
.get_export(&ctx, "bar")
|
|
.expect("Could not get export");
|
|
assert!(bar.is_undefined());
|
|
}
|
|
}
|