Skip to content

Commit

Permalink
feat(jstz_engine): add support for creating, entering and exiting `Re…
Browse files Browse the repository at this point in the history
…alm`s
  • Loading branch information
johnyob committed Dec 11, 2024
1 parent 312043f commit 4fd7190
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 8 deletions.
102 changes: 94 additions & 8 deletions crates/jstz_engine/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,32 @@
//! functions, and variables that are available for JavaScript code to use.
//! Whenever JavaScript code does something like `window.open("http://jstz.dev/")`,
//! it is accessing a global property, in this case `window`.
//!
//! # Notes
//!
//! For more details, refer to the [ECMAScript Specification on Contexts](https://tc39.es/ecma262/#sec-global-environment-records).
use std::{marker::PhantomData, ptr::NonNull};

use mozjs::{jsapi::JSContext, rust::Runtime};
use mozjs::{
jsapi::{JSContext, JS},
rust::Runtime,
};

use crate::{compartment::Compartment, AsRawPtr};
use crate::{
compartment::{self, Compartment},
realm::Realm,
AsRawPtr,
};

/// The context of a JavaScript runtime with a state `S`.
/// Ownership of a context represents the capability to manipulate data
/// managed by the engine.
#[allow(dead_code)]
pub struct Context<S> {
raw_cx: NonNull<JSContext>,
// SAFETY: This is only `Some` if the state `S` is `Entered<'a, C, S>`.
// In this case, the old realm is guaranteed to be alive for at least as long as `'a`.
old_realm: Option<*mut JS::Realm>,
marker: PhantomData<S>,
}

Expand All @@ -40,26 +53,22 @@ pub struct Owned;
pub struct Callback;

/// A context state that has entered the compartment `C` with lifetime `'a`.
#[allow(dead_code)]
pub struct Entered<'a, C: Compartment, S> {
marker: PhantomData<(&'a (), C, S)>,
}

// The following traits are 'marker' traits that are used to enforce
// type-level invariants on the context state.
#[allow(dead_code)]
pub trait CanAlloc {}
impl CanAlloc for Owned {}
impl CanAlloc for Callback {}
impl<'a, C: Compartment, S> CanAlloc for Entered<'a, C, S> {}

#[allow(dead_code)]
pub trait CanAccess {}
impl CanAccess for Owned {}
impl CanAccess for Callback {}
impl<'a, C: Compartment, S> CanAccess for Entered<'a, C, S> {}

#[allow(dead_code)]
pub trait InCompartment<C: Compartment> {}
impl<'a, C: Compartment, S> InCompartment<C> for Entered<'a, C, S> {}

Expand All @@ -70,11 +79,62 @@ impl Context<Owned> {

Self {
raw_cx,
old_realm: None,
marker: PhantomData,
}
}
}

impl<S> Context<S> {
/// Enter an existing realm
pub fn enter_realm<'a, 'b, C: Compartment>(
&'a mut self,
realm: Realm<'b, C>,
) -> Context<Entered<'a, C, S>>
where
S: CanAlloc + CanAccess,
'a: 'b,
{
let old_realm = unsafe { JS::EnterRealm(self.as_raw_ptr(), realm.as_raw_ptr()) };

Context {
raw_cx: self.raw_cx,
old_realm: Some(old_realm),
marker: PhantomData,
}
}

/// Enter a new realm
pub fn new_realm(&mut self) -> Option<Context<Entered<'_, compartment::Ref<'_>, S>>>
where
S: CanAlloc + CanAccess,
{
let realm = Realm::new(self)?;

// TODO(https://linear.app/tezos/issue/JSTZ-196):
// Remove this `unsafe` block once rooting is implemented.
//
// SAFETY: We transmute the lifetime of the realm. This is equivalent to rooting the realm.
// This safe because entering the realm immediately roots the realm.
unsafe {
let rooted_realm: Realm<'_, compartment::Ref<'_>> =
std::mem::transmute(realm);

Some(self.enter_realm(rooted_realm))
}
}
}

impl<S> Drop for Context<S> {
fn drop(&mut self) {
if let Some(old_realm) = self.old_realm {
unsafe {
JS::LeaveRealm(self.as_raw_ptr(), old_realm);
}
}
}
}

impl<S> AsRawPtr for Context<S> {
type Ptr = *mut JSContext;

Expand All @@ -85,7 +145,10 @@ impl<S> AsRawPtr for Context<S> {

#[cfg(test)]
mod test {
use mozjs::rust::{JSEngine, Runtime};
use mozjs::{
jsapi::JS,
rust::{JSEngine, Runtime},
};

use crate::AsRawPtr;

Expand All @@ -101,4 +164,27 @@ mod test {

assert_eq!(raw_cx, unsafe { cx.as_raw_ptr() })
}

#[test]
fn entering_and_leaving_realm() {
let engine = JSEngine::init().unwrap();
let rt = Runtime::new(engine.handle());
let cx = &mut Context::from_runtime(&rt);

// Enter a new realm to evaluate the script in.
let mut cx1 = cx.new_realm().unwrap();
let ptr = unsafe { cx1.as_raw_ptr() };
let global1 = unsafe { JS::CurrentGlobalOrNull(cx1.as_raw_ptr()) };
assert_eq!(global1, unsafe { JS::CurrentGlobalOrNull(ptr) });

let cx2 = cx1.new_realm().unwrap();
let global2 = unsafe { JS::CurrentGlobalOrNull(cx2.as_raw_ptr()) };
assert_ne!(global1, global2);
assert_eq!(global2, unsafe { JS::CurrentGlobalOrNull(ptr) });

drop(cx2);

// Dropping the entered realm should restore the previous realm
assert_eq!(global1, unsafe { JS::CurrentGlobalOrNull(ptr) });
}
}
1 change: 1 addition & 0 deletions crates/jstz_engine/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod compartment;
mod context;
mod realm;

#[allow(dead_code)]
pub(crate) trait AsRawPtr {
Expand Down
103 changes: 103 additions & 0 deletions crates/jstz_engine/src/realm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//! This module provides the interface for JavaScript Realms in SpiderMonkey.
//! A realm represents a distinct execution environment for JavaScript code,
//! encapsulating global objects, intrinsic objects, and a separate environment
//! for executing scripts and modules.
//!
//! Realms are fundamental to the JavaScript specification and enable features such as:
//! - **Isolation**: Code execution in one realm cannot directly affect another realm's
//! execution environment, making realms ideal for sandboxing.
//! - **Multiple Global Environments**: Each realm has its own `global` object and associated
//! built-ins like `Array`, `Object`, and `Function`.
//!
//! # Key Concepts
//!
//! - **Global Object**: Each realm contains its unique global object, which is the root
//! of the scope chain for all scripts executed within that realm.
//! - **Intrinsics**: Realms maintain their own set of intrinsic objects, such as
//! `Object.prototype` and `Array.prototype`, ensuring isolation at the object level.
//! - **Compartments**: Realms exist within compartments, which group related
//! realms
//!
//! # Notes
//!
//! For more details, refer to the [ECMAScript Specification on Realms](https://tc39.es/ecma262/#sec-code-realms).
use std::{marker::PhantomData, ptr::NonNull};

use mozjs::{
jsapi::{JSObject, JS_NewGlobalObject, OnNewGlobalHookOption, JS},
rust::{RealmOptions, SIMPLE_GLOBAL_CLASS},
};

use crate::{
compartment::{self, Compartment},
context::{CanAccess, CanAlloc, Context, InCompartment},
AsRawPtr,
};

/// A JavaScript realm with lifetime of at least `'a` allocated in compartment `C`.
/// A realm is a global object.
pub struct Realm<'a, C: Compartment> {
global_object: NonNull<JSObject>,
marker: PhantomData<(&'a (), C)>,
}

impl<'a, C: Compartment> Copy for Realm<'a, C> {}

impl<'a, C: Compartment> Clone for Realm<'a, C> {
fn clone(&self) -> Self {
*self
}
}

impl<'a> Realm<'a, compartment::Ref<'a>> {
pub fn new<S>(cx: &'a mut Context<S>) -> Option<Self>
where
S: CanAlloc + CanAccess,
{
// NOTE: [RealmOptions::default()] enables the creation of a new compartment for this
// realm. If we want to use an existing compartment, this will need to be altered.
let mut realm_options = RealmOptions::default();
realm_options.creationOptions_.sharedMemoryAndAtomics_ = true;
realm_options
.creationOptions_
.defineSharedArrayBufferConstructor_ = true;

let global_object = unsafe {
JS_NewGlobalObject(
cx.as_raw_ptr(),
&SIMPLE_GLOBAL_CLASS,
std::ptr::null_mut(),
OnNewGlobalHookOption::FireOnNewGlobalHook,
&*realm_options,
)
};

Some(Self {
global_object: NonNull::new(global_object)?,
marker: PhantomData,
})
}
}

impl<'a, C: Compartment> Realm<'a, C> {
pub fn from_context<S>(cx: &'a mut Context<S>) -> Option<Self>
where
S: InCompartment<C>,
{
let global_object = unsafe { JS::CurrentGlobalOrNull(cx.as_raw_ptr()) };

Some(Self {
global_object: NonNull::new(global_object)?,
marker: PhantomData,
})
}
}

impl<'a, C: Compartment> AsRawPtr for Realm<'a, C> {
type Ptr = *mut JSObject;

unsafe fn as_raw_ptr(&self) -> Self::Ptr {
self.global_object.as_ptr()
}
}

0 comments on commit 4fd7190

Please sign in to comment.