Skip to content

Commit

Permalink
feat(core): Add initial support for realms (denoland#14019)
Browse files Browse the repository at this point in the history
This commit adds tentative support for multiple realms in "deno_core". 

It adds the "JsRealm" API that adds methods like "JsRuntime"'s 
"handle_scope", "global_object" and "execute_script" specific to the realm.
  • Loading branch information
andreubotella committed Apr 17, 2022
1 parent bd6494d commit 19bb82a
Show file tree
Hide file tree
Showing 2 changed files with 209 additions and 39 deletions.
1 change: 1 addition & 0 deletions core/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ pub use crate::runtime::CompiledWasmModuleStore;
pub use crate::runtime::CrossIsolateStore;
pub use crate::runtime::GetErrorClassFn;
pub use crate::runtime::JsErrorCreateFn;
pub use crate::runtime::JsRealm;
pub use crate::runtime::JsRuntime;
pub use crate::runtime::RuntimeOptions;
pub use crate::runtime::SharedArrayBufferStore;
Expand Down
247 changes: 208 additions & 39 deletions core/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ pub struct JsRuntime {
inspector: Option<Box<JsRuntimeInspector>>,
snapshot_creator: Option<v8::SnapshotCreator>,
has_snapshotted: bool,
built_from_snapshot: bool,
allocations: IsolateAllocations,
extensions: Vec<Extension>,
event_loop_middlewares: Vec<Box<OpEventLoopFn>>,
Expand Down Expand Up @@ -145,7 +146,7 @@ pub type CompiledWasmModuleStore = CrossIsolateStore<v8::CompiledWasmModule>;
/// Internal state for JsRuntime which is stored in one of v8::Isolate's
/// embedder slots.
pub(crate) struct JsRuntimeState {
pub global_context: Option<v8::Global<v8::Context>>,
global_realm: Option<JsRealm>,
pub(crate) js_recv_cb: Option<v8::Global<v8::Function>>,
pub(crate) js_macrotask_cbs: Vec<v8::Global<v8::Function>>,
pub(crate) js_nexttick_cbs: Vec<v8::Global<v8::Function>>,
Expand Down Expand Up @@ -373,7 +374,7 @@ impl JsRuntime {
.unwrap_or_else(|| Rc::new(NoopModuleLoader));

isolate.set_slot(Rc::new(RefCell::new(JsRuntimeState {
global_context: Some(global_context),
global_realm: Some(JsRealm(global_context)),
pending_promise_exceptions: HashMap::new(),
pending_dyn_mod_evaluate: vec![],
pending_mod_evaluate: None,
Expand Down Expand Up @@ -406,6 +407,7 @@ impl JsRuntime {
inspector: Some(inspector),
snapshot_creator: maybe_snapshot_creator,
has_snapshotted: false,
built_from_snapshot: has_startup_snapshot,
allocations: IsolateAllocations::default(),
event_loop_middlewares: Vec::with_capacity(options.extensions.len()),
extensions: options.extensions,
Expand All @@ -414,7 +416,8 @@ impl JsRuntime {
// TODO(@AaronO): diff extensions inited in snapshot and those provided
// for now we assume that snapshot and extensions always match
if !has_startup_snapshot {
js_runtime.init_extension_js().unwrap();
let realm = js_runtime.global_realm();
js_runtime.init_extension_js(&realm).unwrap();
}
// Init extension ops
js_runtime.init_extension_ops().unwrap();
Expand All @@ -425,9 +428,7 @@ impl JsRuntime {
}

pub fn global_context(&mut self) -> v8::Global<v8::Context> {
let state = Self::state(self.v8_isolate());
let state = state.borrow();
state.global_context.clone().unwrap()
self.global_realm().0
}

pub fn v8_isolate(&mut self) -> &mut v8::OwnedIsolate {
Expand All @@ -438,9 +439,38 @@ impl JsRuntime {
self.inspector.as_mut().unwrap()
}

pub fn global_realm(&mut self) -> JsRealm {
let state = Self::state(self.v8_isolate());
let state = state.borrow();
state.global_realm.clone().unwrap()
}

pub fn create_realm(&mut self) -> Result<JsRealm, Error> {
let realm = {
// SAFETY: Having the scope tied to self's lifetime makes it impossible to
// reference self.ops while the scope is alive. Here we turn it into an
// unbound lifetime, which is sound because 1. it only lives until the end
// of this block, and 2. the HandleScope only has access to the isolate,
// and nothing else we're accessing from self does.
let scope = &mut v8::HandleScope::new(unsafe {
&mut *(self.v8_isolate() as *mut v8::OwnedIsolate)
});
let context = bindings::initialize_context(
scope,
&Self::state(self.v8_isolate()).borrow().op_ctxs,
self.built_from_snapshot,
);
JsRealm::new(v8::Global::new(scope, context))
};

if !self.built_from_snapshot {
self.init_extension_js(&realm)?;
}
Ok(realm)
}

pub fn handle_scope(&mut self) -> v8::HandleScope {
let context = self.global_context();
v8::HandleScope::with_context(self.v8_isolate(), context)
self.global_realm().handle_scope(self)
}

fn setup_isolate(mut isolate: v8::OwnedIsolate) -> v8::OwnedIsolate {
Expand All @@ -465,16 +495,16 @@ impl JsRuntime {
module_map.clone()
}

/// Initializes JS of provided Extensions
fn init_extension_js(&mut self) -> Result<(), Error> {
/// Initializes JS of provided Extensions in the given realm
fn init_extension_js(&mut self, realm: &JsRealm) -> Result<(), Error> {
// Take extensions to avoid double-borrow
let mut extensions: Vec<Extension> = std::mem::take(&mut self.extensions);
for m in extensions.iter_mut() {
let js_files = m.init_js();
for (filename, source) in js_files {
let source = source()?;
// TODO(@AaronO): use JsRuntime::execute_static() here to move src off heap
self.execute_script(filename, &source)?;
realm.execute_script(self, filename, &source)?;
}
}
// Restore extensions
Expand Down Expand Up @@ -630,33 +660,7 @@ impl JsRuntime {
name: &str,
source_code: &str,
) -> Result<v8::Global<v8::Value>, Error> {
let scope = &mut self.handle_scope();

let source = v8::String::new(scope, source_code).unwrap();
let name = v8::String::new(scope, name).unwrap();
let origin = bindings::script_origin(scope, name);

let tc_scope = &mut v8::TryCatch::new(scope);

let script = match v8::Script::compile(tc_scope, source, Some(&origin)) {
Some(script) => script,
None => {
let exception = tc_scope.exception().unwrap();
return exception_to_err_result(tc_scope, exception, false);
}
};

match script.run(tc_scope) {
Some(value) => {
let value_handle = v8::Global::new(tc_scope, value);
Ok(value_handle)
}
None => {
assert!(tc_scope.has_caught());
let exception = tc_scope.exception().unwrap();
exception_to_err_result(tc_scope, exception, false)
}
}
self.global_realm().execute_script(self, name, source_code)
}

/// Takes a snapshot. The isolate should have been created with will_snapshot
Expand All @@ -682,7 +686,7 @@ impl JsRuntime {

let state = Self::state(self.v8_isolate());

state.borrow_mut().global_context.take();
state.borrow_mut().global_realm.take();

self.inspector.take();

Expand Down Expand Up @@ -1769,6 +1773,101 @@ impl JsRuntime {
}
}

/// A representation of a JavaScript realm tied to a [`JsRuntime`], that allows
/// execution in the realm's context.
///
/// A [`JsRealm`] instance does not hold ownership of its corresponding realm,
/// so they can be created and dropped as needed. And since every operation on
/// them requires passing a mutable reference to the [`JsRuntime`], multiple
/// [`JsRealm`] instances won't overlap.
///
/// # Panics
///
/// Every method of [`JsRealm`] will panic if you call if with a reference to a
/// [`JsRuntime`] other than the one that corresponds to the current context.
///
/// # Lifetime of the realm
///
/// A [`JsRealm`] instance will keep the underlying V8 context alive even if it
/// would have otherwise been garbage collected.
#[derive(Clone)]
pub struct JsRealm(v8::Global<v8::Context>);
impl JsRealm {
pub fn new(context: v8::Global<v8::Context>) -> Self {
JsRealm(context)
}

pub fn context(&self) -> &v8::Global<v8::Context> {
&self.0
}

pub fn handle_scope<'s>(
&self,
runtime: &'s mut JsRuntime,
) -> v8::HandleScope<'s> {
v8::HandleScope::with_context(runtime.v8_isolate(), &self.0)
}

pub fn global_object<'s>(
&self,
runtime: &'s mut JsRuntime,
) -> v8::Local<'s, v8::Object> {
let scope = &mut self.handle_scope(runtime);
self.0.open(scope).global(scope)
}

/// Executes traditional JavaScript code (traditional = not ES modules) in the
/// realm's context.
///
/// `name` can be a filepath or any other string, eg.
///
/// - "/some/file/path.js"
/// - "<anon>"
/// - "[native code]"
///
/// The same `name` value can be used for multiple executions.
///
/// `Error` can be downcast to a type that exposes additional information
/// about the V8 exception. By default this type is `JsError`, however it may
/// be a different type if `RuntimeOptions::js_error_create_fn` has been set.
pub fn execute_script(
&self,
runtime: &mut JsRuntime,
name: &str,
source_code: &str,
) -> Result<v8::Global<v8::Value>, Error> {
let scope = &mut self.handle_scope(runtime);

let source = v8::String::new(scope, source_code).unwrap();
let name = v8::String::new(scope, name).unwrap();
let origin = bindings::script_origin(scope, name);

let tc_scope = &mut v8::TryCatch::new(scope);

let script = match v8::Script::compile(tc_scope, source, Some(&origin)) {
Some(script) => script,
None => {
let exception = tc_scope.exception().unwrap();
return exception_to_err_result(tc_scope, exception, false);
}
};

match script.run(tc_scope) {
Some(value) => {
let value_handle = v8::Global::new(tc_scope, value);
Ok(value_handle)
}
None => {
assert!(tc_scope.has_caught());
let exception = tc_scope.exception().unwrap();
exception_to_err_result(tc_scope, exception, false)
}
}
}

// TODO(andreubotella): `mod_evaluate`, `load_main_module`, `load_side_module`
}

#[inline]
pub fn queue_async_op(
scope: &v8::Isolate,
Expand Down Expand Up @@ -3200,4 +3299,74 @@ assertEquals(1, notify_return_value);
)
.unwrap();
}

#[test]
fn js_realm_simple() {
let mut runtime = JsRuntime::new(Default::default());
let main_context = runtime.global_context();
let main_global = {
let scope = &mut runtime.handle_scope();
let local_global = main_context.open(scope).global(scope);
v8::Global::new(scope, local_global)
};

let realm = runtime.create_realm().unwrap();
assert_ne!(realm.context(), &main_context);
assert_ne!(realm.global_object(&mut runtime), main_global);

let main_object = runtime.execute_script("", "Object").unwrap();
let realm_object =
realm.execute_script(&mut runtime, "", "Object").unwrap();
assert_ne!(main_object, realm_object);
}

#[test]
fn js_realm_init() {
#[op]
fn op_test() -> Result<String, Error> {
Ok(String::from("Test"))
}

let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![Extension::builder().ops(vec![op_test::decl()]).build()],
..Default::default()
});
let realm = runtime.create_realm().unwrap();
let ret = realm
.execute_script(&mut runtime, "", "Deno.core.opSync('op_test')")
.unwrap();

let scope = &mut realm.handle_scope(&mut runtime);
assert_eq!(ret, serde_v8::to_v8(scope, "Test").unwrap());
}

#[test]
fn js_realm_init_snapshot() {
let snapshot = {
let mut runtime = JsRuntime::new(RuntimeOptions {
will_snapshot: true,
..Default::default()
});
let snap: &[u8] = &*runtime.snapshot();
Vec::from(snap).into_boxed_slice()
};

#[op]
fn op_test() -> Result<String, Error> {
Ok(String::from("Test"))
}

let mut runtime = JsRuntime::new(RuntimeOptions {
startup_snapshot: Some(Snapshot::Boxed(snapshot)),
extensions: vec![Extension::builder().ops(vec![op_test::decl()]).build()],
..Default::default()
});
let realm = runtime.create_realm().unwrap();
let ret = realm
.execute_script(&mut runtime, "", "Deno.core.opSync('op_test')")
.unwrap();

let scope = &mut realm.handle_scope(&mut runtime);
assert_eq!(ret, serde_v8::to_v8(scope, "Test").unwrap());
}
}

0 comments on commit 19bb82a

Please sign in to comment.