use std::marker::PhantomData;
use std::ptr;
use boolinator::Boolinator;
use anymap::AnyMap;
use chakracore_sys::*;
use error::*;
use util::jstry;
use {value, Runtime};
struct ContextData {
promise_queue: Vec<value::Function>,
user_data: AnyMap,
}
#[derive(Debug, PartialEq)]
pub struct Context(JsContextRef);
impl Context {
pub fn new(runtime: &Runtime) -> Result<Context> {
let mut reference = JsContextRef::new();
unsafe {
jstry!(JsCreateContext(runtime.as_raw(), &mut reference));
jstry!(JsSetObjectBeforeCollectCallback(reference, ptr::null_mut(), Some(Self::collect)));
let context = Self::from_raw(reference);
context.set_data(Box::new(ContextData {
promise_queue: Vec::new(),
user_data: AnyMap::new(),
}))?;
context.exec_with(|_| {
let data = context.get_data() as *mut _ as *mut _;
jstry(JsSetPromiseContinuationCallback(Some(Self::promise_handler), data))
})
.expect("activating promise continuation callback")
.map(|_| context)
}
}
pub fn make_current<'a>(&'a self) -> Result<ContextGuard<'a>> {
let current = unsafe { Self::get_current().map(|guard| guard.current.clone()) };
self.enter().map(|_| {
ContextGuard::<'a> {
previous: current,
current: self.clone(),
phantom: PhantomData,
drop: true,
}
})
}
pub unsafe fn get_current<'a>() -> Option<ContextGuard<'a>> {
let mut reference = JsContextRef::new();
jsassert!(JsGetCurrentContext(&mut reference));
reference.0.as_ref().map(|_| ContextGuard {
previous: None,
current: Self::from_raw(reference),
phantom: PhantomData,
drop: false,
})
}
pub fn exec_with<Ret, T: FnOnce(&ContextGuard) -> Ret>(&self, callback: T) -> Result<Ret> {
self.make_current().map(|guard| callback(&guard))
}
pub fn exec_with_current<Ret, T: FnOnce(&ContextGuard) -> Ret>(callback: T) -> Option<Ret> {
unsafe { Self::get_current().as_ref().map(callback) }
}
pub(crate) fn exec_with_value<Ret, T>(value: &value::Value, callback: T) -> Result<Option<Ret>>
where T: FnOnce(&ContextGuard) -> Ret {
Context::from_value(value).map_or(Ok(None), |context| unsafe {
let guard = Context::get_current()
.and_then(|guard| (guard.context() == context).as_some(guard))
.map_or_else(|| context.make_current(), Ok);
guard.map(|guard| Some(callback(&guard)))
})
}
pub fn insert_user_data<T>(&self, value: T) -> Option<T> where T: Send + 'static {
unsafe { self.get_data().user_data.insert(value) }
}
pub fn remove_user_data<T>(&self) -> Option<T> where T: Send + 'static {
unsafe { self.get_data().user_data.remove::<T>() }
}
pub fn get_user_data<T>(&self) -> Option<&T> where T: Send + 'static {
unsafe { self.get_data().user_data.get::<T>() }
}
pub fn get_user_data_mut<T>(&self) -> Option<&mut T> where T: Send + 'static {
unsafe { self.get_data().user_data.get_mut::<T>() }
}
fn from_value(value: &value::Value) -> Option<Context> {
let mut reference = JsContextRef::new();
unsafe {
jstry(JsGetContextOfObject(value.as_raw(), &mut reference))
.ok()
.map(|_| Self::from_raw(reference))
}
}
unsafe fn set_data(&self, data: Box<ContextData>) -> Result<()> {
jstry(JsSetContextData(self.as_raw(), Box::into_raw(data) as *mut _))
}
unsafe fn get_data<'a>(&'a self) -> &'a mut ContextData {
let mut data = ptr::null_mut();
jsassert!(JsGetContextData(self.as_raw(), &mut data));
(data as *mut _)
.as_mut()
.expect("retrieving context data")
}
fn enter(&self) -> Result<()> {
jstry(unsafe { JsSetCurrentContext(self.as_raw()) })
}
fn exit(&self, previous: Option<&Context>) -> Result<()> {
jstry(unsafe {
let next = previous
.map(|context| context.as_raw())
.unwrap_or_else(JsValueRef::new);
JsSetCurrentContext(next)
})
}
unsafe extern "system" fn promise_handler(task: JsValueRef, data: *mut ::libc::c_void) {
let data = (data as *mut ContextData).as_mut().expect("retrieving promise handler stack");
data.promise_queue.push(value::Function::from_raw(task));
}
unsafe extern "system" fn collect(context: JsContextRef, _: *mut ::libc::c_void) {
let context = Self::from_raw(context);
Box::from_raw(context.get_data());
}
}
reference!(Context);
#[must_use]
#[derive(Debug)]
pub struct ContextGuard<'a> {
previous: Option<Context>,
current: Context,
phantom: PhantomData<&'a Context>,
drop: bool,
}
impl<'a> ContextGuard<'a> {
pub fn context(&self) -> Context {
self.current.clone()
}
pub fn global(&self) -> value::Object {
let mut value = JsValueRef::new();
unsafe {
jsassert!(JsGetGlobalObject(&mut value));
value::Object::from_raw(value)
}
}
pub fn execute_tasks(&self) {
let data = unsafe { self.current.get_data() };
while let Some(task) = data.promise_queue.pop() {
task.call(self, &[]).expect("executing promise task");
}
}
}
impl<'a> Drop for ContextGuard<'a> {
fn drop(&mut self) {
if self.drop {
assert!(self.current.exit(self.previous.as_ref()).is_ok())
}
}
}
#[cfg(test)]
mod tests {
use {test, value, script, Context, Property};
#[test]
fn global() {
test::run_with_context(|guard| {
let global = guard.global();
let dirname = Property::new(guard, "__dirname");
global.set(guard, &dirname, &value::String::new(guard, "FooBar"));
global.set_index(guard, 2, &value::Number::new(guard, 1337));
let result1 = script::eval(guard, "__dirname").unwrap();
let result2 = script::eval(guard, "this[2]").unwrap();
assert_eq!(result1.to_string(guard), "FooBar");
assert_eq!(result2.to_integer(guard), 1337);
});
}
#[test]
fn stack() {
let (runtime, context) = test::setup_env();
{
let get_current = || unsafe { Context::get_current().unwrap().context() };
let _guard = context.make_current().unwrap();
assert_eq!(get_current(), context);
{
let inner_context = Context::new(&runtime).unwrap();
let _guard = inner_context.make_current().unwrap();
assert_eq!(get_current(), inner_context);
}
assert_eq!(get_current(), context);
}
assert!(unsafe { Context::get_current() }.is_none());
}
#[test]
fn user_data() {
test::run_with_context(|guard| {
type Data = Vec<i32>;
let context = guard.context();
let data: Data = vec![10, 20];
context.insert_user_data(data);
let data = context.get_user_data::<Data>().unwrap();
assert_eq!(data.as_slice(), [10, 20]);
assert!(context.remove_user_data::<Data>().is_some());
assert!(context.get_user_data::<Data>().is_none());
});
}
#[test]
fn promise_queue() {
test::run_with_context(|guard| {
let result = script::eval(guard, "
var object = {};
Promise.resolve(5)
.then(val => val + 5)
.then(val => val / 5)
.then(val => object.val = val);
object;").unwrap();
guard.execute_tasks();
let value = result
.into_object()
.unwrap()
.get(guard, &Property::new(guard, "val"))
.to_integer(guard);
assert_eq!(value, 2);
});
}
#[test]
fn shared_objects() {
let (runtime, context) = test::setup_env();
let context2 = Context::new(&runtime).unwrap();
let guard1 = context.make_current().unwrap();
let object = script::eval(&guard1, "({ foo: 1337 })").unwrap();
let guard2 = context2.make_current().unwrap();
assert_eq!(object.to_json(&guard2).unwrap(), r#"{"foo":1337}"#);
}
}