Repository: mun-lang/mun (language: rust)

Issue:
Using Mun in a threaded environment
The `mun_runtime::Runtime` is not `Sync` nor `Send` since the `RuntimeBuilder::spawn` function returns a `Rc<RefCell<Runtime>>`. This makes it hard to use in other programs that make use of threads. The reason we wrap the `Runtime` in an `Rc` is because the runtime (or better yet, the memory model) is not thread safe. Wrapping the `Runtime` in a mutex will also not work because the memory model is shared between other structs (like `StructRef`). Sending the `Runtime` to a thread and doing modifications while also accessing a `StructRef` from another thread will cause threading issues. 

We've already talked about this a lot. We currently have no proper plan to support multithreading. However, our current solution is also not very ergonomic. I propose we implement a blocking solution that still doesnt do multithreading of the runtime but does allow the runtime and derived data to be shared across threads. In a later version we can then add proper multithreading hopefully using the same API we design now.

A straighforward solution is to wrap all memory modifying operations inside a mutex. This will block any operation performed by other threads but will enable the use of the Runtime and derived data to be send to different threads.

Curious to your thoughts!


Discussion / hints:
Looking at it closer, the `StructRef` is already safe because it is tied to the lifetime of the runtime. If we ensure that you can only use a rooted struct by first converting it to a `StructRef` (the `RootedStruct::as_ref`), which is again tied to the runtime, we can make the runtime `Send`. The `as_ref` method is currently unsafe. I would prefer to have an API like:

```
let rooted_struct:RootedStruct = // ...
let ref:StructRef<'r> = runtime.get_ref(rooted_struct).unwrap();
invoke_fn!("main", ref)
```

This involves making sure that the `get_ref` method can be safely called. WDYT @Wodann?
IIRC, the API can be made safe if we can guarantee that the rooted struct was created in the runtime that we are using to convert to a `StructRef`. That could also be achieved by storing a "unique identifier" that matches that of a runtime.

Fix patch:
diff --git a/book/listings/ch01-getting-started/listing04.rs b/book/listings/ch01-getting-started/listing04.rs
index 489ae575e..e7984d16b 100644
--- a/book/listings/ch01-getting-started/listing04.rs
+++ b/book/listings/ch01-getting-started/listing04.rs
@@ -1,20 +1,17 @@
-use mun_runtime::RuntimeBuilder;
+use mun_runtime::Runtime;
 use std::{cell::RefCell, env, rc::Rc};
 
 fn main() {
     let lib_path = env::args().nth(1).expect("Expected path to a Mun library.");
 
-    let mut runtime = RuntimeBuilder::new(lib_path)
-        .spawn()
+    let mut runtime = Runtime::builder(lib_path)
+        .finish()
         .expect("Failed to spawn Runtime");
 
     loop {
-        {
-            let runtime_ref = runtime.borrow();
-            let arg: i64 = runtime_ref.invoke("arg", ()).unwrap();
-            let result: i64 = runtime_ref.invoke("fibonacci", (arg,)).unwrap();
-            println!("fibonacci({}) = {}", arg, result);
-        }
-        runtime.borrow_mut().update();
+        let arg: i64 = runtime.invoke("arg", ()).unwrap();
+        let result: i64 = runtime.invoke("fibonacci", (arg,)).unwrap();
+        println!("fibonacci({}) = {}", arg, result);
+        runtime.update();
     }
 }
diff --git a/book/listings/ch02-basic-concepts/listing02.rs b/book/listings/ch02-basic-concepts/listing02.rs
index d72ff69cc..47b04cc18 100644
--- a/book/listings/ch02-basic-concepts/listing02.rs
+++ b/book/listings/ch02-basic-concepts/listing02.rs
@@ -1,12 +1,11 @@
-use mun_runtime::RuntimeBuilder;
+use mun_runtime::Runtime;
 use std::{cell::RefCell, rc::Rc};
 
 fn main() {
-    let runtime = RuntimeBuilder::new("main.munlib")
-        .spawn()
+    let runtime = Runtime::builder("main.munlib")
+        .finish()
         .expect("Failed to spawn Runtime");
 
-    let runtime_ref = runtime.borrow();
-    let result: bool = runtime_ref.invoke("random_bool", ()).unwrap();
+    let result: bool = runtime.invoke("random_bool", ()).unwrap();
     println!("random bool: {}", result);
 }
diff --git a/book/listings/ch02-basic-concepts/listing03.rs b/book/listings/ch02-basic-concepts/listing03.rs
index c1a8967a7..1264c2473 100644
--- a/book/listings/ch02-basic-concepts/listing03.rs
+++ b/book/listings/ch02-basic-concepts/listing03.rs
@@ -1,4 +1,4 @@
-use mun_runtime::RuntimeBuilder;
+use mun_runtime::Runtime;
 use std::{cell::RefCell, rc::Rc};
 
 extern "C" fn random() -> i64 {
@@ -8,12 +8,11 @@ extern "C" fn random() -> i64 {
 }
 
 fn main() {
-    let runtime = RuntimeBuilder::new("main.munlib")
+    let runtime = Runtime::builder("main.munlib")
         .insert_fn("random", random as extern "C" fn() -> i64)
-        .spawn()
+        .finish()
         .expect("Failed to spawn Runtime");
 
-    let runtime_ref = runtime.borrow();
-    let result: bool = runtime_ref.invoke("random_bool", ()).unwrap();
+    let result: bool = runtime.invoke("random_bool", ()).unwrap();
     println!("random_bool: {}", result);
 }
diff --git a/book/listings/ch03-structs/listing11.rs b/book/listings/ch03-structs/listing11.rs
index 874d69507..fb95b194f 100644
--- a/book/listings/ch03-structs/listing11.rs
+++ b/book/listings/ch03-structs/listing11.rs
@@ -1,16 +1,15 @@
 # extern crate mun_runtime;
-use mun_runtime::{RuntimeBuilder, StructRef};
+use mun_runtime::{Runtime, StructRef};
 use std::{cell::RefCell, env, rc::Rc};
 
 fn main() {
     let lib_path = env::args().nth(1).expect("Expected path to a Mun library.");
 
-    let runtime = RuntimeBuilder::new(lib_path)
-        .spawn()
+    let runtime = Runtime::builder(lib_path)
+        .finish()
         .expect("Failed to spawn Runtime");
 
-    let runtime_ref = runtime.borrow();
-    let a: StructRef = runtime_ref.invoke("vector2_new", (-1.0f32, 1.0f32)).unwrap();
-    let b: StructRef = runtime_ref.invoke("vector2_new", (1.0f32, -1.0f32)).unwrap();
-    let added: StructRef = runtime_ref.invoke("vector2_add", (a, b)).unwrap();
+    let a: StructRef = runtime.invoke("vector2_new", (-1.0f32, 1.0f32)).unwrap();
+    let b: StructRef = runtime.invoke("vector2_new", (1.0f32, -1.0f32)).unwrap();
+    let added: StructRef = runtime.invoke("vector2_add", (a, b)).unwrap();
 }
diff --git a/book/listings/ch03-structs/listing12.rs b/book/listings/ch03-structs/listing12.rs
index d88e0f512..51965c345 100644
--- a/book/listings/ch03-structs/listing12.rs
+++ b/book/listings/ch03-structs/listing12.rs
@@ -1,17 +1,16 @@
 # extern crate mun_runtime;
-# use mun_runtime::{RuntimeBuilder, StructRef};
+# use mun_runtime::{Runtime, StructRef};
 # use std::{cell::RefCell, env, rc::Rc};
 #
 # fn main() {
 #     let lib_path = env::args().nth(1).expect("Expected path to a Mun library.");
 #
-#     let mut runtime = 
-#         RuntimeBuilder::new(lib_path)
-#             .spawn()
+#     let runtime =
+#         Runtime::builder(lib_path)
+#             .finish()
 #             .expect("Failed to spawn Runtime");
 #
-    let runtime_ref = runtime.borrow();
-    let mut xy: StructRef = runtime_ref.invoke("vector2_new", (-1.0f32, 1.0f32)).unwrap();
+    let mut xy: StructRef = runtime.invoke("vector2_new", (-1.0f32, 1.0f32)).unwrap();
     let x: f32 = xy.get("x").unwrap();
     xy.set("x", x * x).unwrap();
     let y = xy.replace("y", -1.0f32).unwrap();
diff --git a/book/listings/ch03-structs/listing14.rs b/book/listings/ch03-structs/listing14.rs
index cc83a763e..fe2965f52 100644
--- a/book/listings/ch03-structs/listing14.rs
+++ b/book/listings/ch03-structs/listing14.rs
@@ -1,5 +1,5 @@
 # extern crate mun_runtime;
-use mun_runtime::{RuntimeBuilder, StructRef};
+use mun_runtime::{Runtime, StructRef};
 use std::{env, time};
 
 extern "C" fn log_f32(value: f32) {
@@ -9,16 +9,12 @@ extern "C" fn log_f32(value: f32) {
 fn main() {
     let lib_dir = env::args().nth(1).expect("Expected path to a Mun library.");
 
-    let runtime = RuntimeBuilder::new(lib_dir)
+    let mut runtime = Runtime::builder(lib_dir)
         .insert_fn("log_f32", log_f32 as extern "C" fn(f32))
-        .spawn()
+        .finish()
         .expect("Failed to spawn Runtime");
 
-    let ctx = {
-        let runtime_ref = runtime.borrow();
-        let ctx: StructRef = runtime_ref.invoke("new_sim", ()).unwrap();
-        ctx.root(runtime.clone())
-    };
+    let ctx = runtime.invoke::<StructRef, ()>("new_sim", ()).unwrap().root();
 
     let mut previous = time::Instant::now();
     const FRAME_TIME: time::Duration = time::Duration::from_millis(40);
@@ -33,12 +29,9 @@ fn main() {
             elapsed.as_secs_f32()
         };
 
-        {
-            let runtime_ref = runtime.borrow();
-            let _: () = runtime_ref.invoke("sim_update", (unsafe { ctx.as_ref(&runtime_ref) }, elapsed_secs)).unwrap();
-        }
+        let _: () = runtime.invoke("sim_update", (ctx.as_ref(&runtime), elapsed_secs)).unwrap();
         previous = now;
 
-        runtime.borrow_mut().update();
+        runtime.update();
     }
 }
diff --git a/crates/mun/src/ops/start.rs b/crates/mun/src/ops/start.rs
index 27f86d198..8cd9bf7bd 100644
--- a/crates/mun/src/ops/start.rs
+++ b/crates/mun/src/ops/start.rs
@@ -1,18 +1,15 @@
-use std::{cell::RefCell, rc::Rc};
-
 use anyhow::anyhow;
 use clap::ArgMatches;
-use mun_runtime::{ReturnTypeReflection, Runtime, RuntimeBuilder};
+use mun_runtime::{ReturnTypeReflection, Runtime};
 
 use crate::ExitStatus;
 
 /// Starts the runtime with the specified library and invokes function `entry`.
-pub fn start(matches: &ArgMatches) -> Result<ExitStatus, anyhow::Error> {
+pub fn start(matches: &ArgMatches) -> anyhow::Result<ExitStatus> {
     let runtime = runtime(matches)?;
 
-    let borrowed = runtime.borrow();
     let entry_point = matches.value_of("entry").unwrap_or("main");
-    let fn_definition = borrowed
+    let fn_definition = runtime
         .get_function_definition(entry_point)
         .ok_or_else(|| {
             std::io::Error::new(
@@ -24,19 +21,19 @@ pub fn start(matches: &ArgMatches) -> Result<ExitStatus, anyhow::Error> {
     if let Some(ret_type) = fn_definition.prototype.signature.return_type() {
         let type_guid = &ret_type.guid;
         if *type_guid == bool::type_guid() {
-            let result: bool = borrowed
+            let result: bool = runtime
                 .invoke(entry_point, ())
                 .map_err(|e| anyhow!("{}", e))?;
 
             println!("{}", result)
         } else if *type_guid == f64::type_guid() {
-            let result: f64 = borrowed
+            let result: f64 = runtime
                 .invoke(entry_point, ())
                 .map_err(|e| anyhow!("{}", e))?;
 
             println!("{}", result)
         } else if *type_guid == i64::type_guid() {
-            let result: i64 = borrowed
+            let result: i64 = runtime
                 .invoke(entry_point, ())
                 .map_err(|e| anyhow!("{}", e))?;
 
@@ -50,17 +47,16 @@ pub fn start(matches: &ArgMatches) -> Result<ExitStatus, anyhow::Error> {
         Ok(ExitStatus::Success)
     } else {
         #[allow(clippy::unit_arg)]
-        borrowed
+        runtime
             .invoke(entry_point, ())
             .map(|_: ()| ExitStatus::Success)
             .map_err(|e| anyhow!("{}", e))
     }
 }
 
-fn runtime(matches: &ArgMatches) -> Result<Rc<RefCell<Runtime>>, anyhow::Error> {
-    let builder = RuntimeBuilder::new(
+fn runtime(matches: &ArgMatches) -> anyhow::Result<Runtime> {
+    Runtime::builder(
         matches.value_of("LIBRARY").unwrap(), // Safe because its a required arg
-    );
-
-    builder.spawn()
+    )
+    .finish()
 }
diff --git a/crates/mun_memory/src/gc/root_ptr.rs b/crates/mun_memory/src/gc/root_ptr.rs
index 905c312ec..8874e7652 100644
--- a/crates/mun_memory/src/gc/root_ptr.rs
+++ b/crates/mun_memory/src/gc/root_ptr.rs
@@ -36,6 +36,11 @@ impl<T: TypeMemory + TypeTrace, G: GcRuntime<T>> GcRootPtr<T, G> {
         }
     }
 
+    /// Returns the runtime that owns the memory
+    pub fn runtime(&self) -> &Weak<G> {
+        &self.runtime
+    }
+
     /// Returns the handle of this instance
     pub fn handle(&self) -> GcPtr {
         self.handle
diff --git a/crates/mun_runtime/benches/benchmarks.rs b/crates/mun_runtime/benches/benchmarks.rs
index 62d00627b..177580655 100644
--- a/crates/mun_runtime/benches/benchmarks.rs
+++ b/crates/mun_runtime/benches/benchmarks.rs
@@ -17,9 +17,8 @@ pub fn fibonacci_benchmark(c: &mut Criterion) {
     for i in [100i64, 200i64, 500i64, 1000i64, 4000i64, 8000i64].iter() {
         // Run Mun fibonacci
         group.bench_with_input(BenchmarkId::new("mun", i), i, |b, i| {
-            let runtime_ref = runtime.borrow();
             b.iter(|| {
-                let _: i64 = runtime_ref.invoke("main", (*i,)).unwrap();
+                let _: i64 = runtime.invoke("main", (*i,)).unwrap();
             })
         });
 
@@ -78,9 +77,8 @@ pub fn empty_benchmark(c: &mut Criterion) {
     let mut group = c.benchmark_group("empty");
 
     group.bench_function("mun", |b| {
-        let runtime_ref = runtime.borrow();
         b.iter(|| {
-            let _: i64 = runtime_ref.invoke("empty", (black_box(20i64),)).unwrap();
+            let _: i64 = runtime.invoke("empty", (black_box(20i64),)).unwrap();
         })
     });
     group.bench_function("rust", |b| b.iter(|| empty(black_box(20))));
@@ -114,9 +112,8 @@ struct RustParent<'a> {
 pub fn get_struct_field_benchmark(c: &mut Criterion) {
     // Perform setup (not part of the benchmark)
     let runtime = util::runtime_from_file("struct.mun");
-    let runtime_ref = runtime.borrow_mut();
-    let mun_gc_parent: StructRef = runtime_ref.invoke("make_gc_parent", ()).unwrap();
-    let mun_value_parent: StructRef = runtime_ref.invoke("make_value_parent", ()).unwrap();
+    let mun_gc_parent: StructRef = runtime.invoke("make_gc_parent", ()).unwrap();
+    let mun_value_parent: StructRef = runtime.invoke("make_value_parent", ()).unwrap();
 
     let rust_child = RustChild(-2.0, -1.0, 1.0, 2.0);
     let rust_parent = RustParent {
@@ -203,9 +200,8 @@ pub fn get_struct_field_benchmark(c: &mut Criterion) {
 pub fn set_struct_field_benchmark(c: &mut Criterion) {
     // Perform setup (not part of the benchmark)
     let runtime = util::runtime_from_file("struct.mun");
-    let runtime_ref = runtime.borrow();
-    let mut mun_gc_parent: StructRef = runtime_ref.invoke("make_value_parent", ()).unwrap();
-    let mut mun_value_parent: StructRef = runtime_ref.invoke("make_value_parent", ()).unwrap();
+    let mut mun_gc_parent: StructRef = runtime.invoke("make_value_parent", ()).unwrap();
+    let mut mun_value_parent: StructRef = runtime.invoke("make_value_parent", ()).unwrap();
 
     let rust_child = RustChild(-2.0, -1.0, 1.0, 2.0);
     let mut rust_child2 = rust_child.clone();
diff --git a/crates/mun_runtime/benches/util/mod.rs b/crates/mun_runtime/benches/util/mod.rs
index b18177e02..1e16d5f9f 100644
--- a/crates/mun_runtime/benches/util/mod.rs
+++ b/crates/mun_runtime/benches/util/mod.rs
@@ -1,11 +1,7 @@
 use compiler::{Config, DisplayColor, Driver, OptimizationLevel, PathOrInline};
 use mlua::Lua;
-use mun_runtime::RuntimeBuilder;
-use std::{
-    cell::RefCell,
-    path::{Path, PathBuf},
-    rc::Rc,
-};
+use mun_runtime::Runtime;
+use std::path::{Path, PathBuf};
 use wasmer_runtime::{instantiate, Instance};
 
 fn compute_resource_path<P: AsRef<Path>>(p: P) -> PathBuf {
@@ -14,7 +10,7 @@ fn compute_resource_path<P: AsRef<Path>>(p: P) -> PathBuf {
         .join(p)
 }
 
-pub fn runtime_from_file<P: AsRef<Path>>(p: P) -> Rc<RefCell<mun_runtime::Runtime>> {
+pub fn runtime_from_file<P: AsRef<Path>>(p: P) -> Runtime {
     let path = PathOrInline::Path(compute_resource_path(p));
     let (mut driver, file_id) = Driver::with_file(
         Config {
@@ -33,7 +29,7 @@ pub fn runtime_from_file<P: AsRef<Path>>(p: P) -> Rc<RefCell<mun_runtime::Runtim
 
     let out_path = driver.assembly_output_path_from_file(file_id);
     driver.write_all_assemblies(false).unwrap();
-    RuntimeBuilder::new(out_path).spawn().unwrap()
+    Runtime::builder(out_path).finish().unwrap()
 }
 
 pub fn lua_from_file<P: AsRef<Path>>(p: P) -> Lua {
diff --git a/crates/mun_runtime/examples/buoyancy.rs b/crates/mun_runtime/examples/buoyancy.rs
index 67980a929..5d5b37537 100644
--- a/crates/mun_runtime/examples/buoyancy.rs
+++ b/crates/mun_runtime/examples/buoyancy.rs
@@ -1,4 +1,4 @@
-use mun_runtime::{RuntimeBuilder, StructRef};
+use mun_runtime::{Runtime, StructRef};
 use std::{env, time};
 
 extern "C" fn log_f32(value: f32) {
@@ -12,13 +12,15 @@ extern "C" fn log_f32(value: f32) {
 fn main() {
     let lib_dir = env::args().nth(1).expect("Expected path to a Mun library.");
 
-    let runtime = RuntimeBuilder::new(lib_dir)
+    let mut runtime = Runtime::builder(lib_dir)
         .insert_fn("log_f32", log_f32 as extern "C" fn(f32))
-        .spawn()
+        .finish()
         .expect("Failed to spawn Runtime");
 
-    let runtime_ref = runtime.borrow();
-    let ctx: StructRef = runtime_ref.invoke("new_sim", ()).unwrap();
+    let ctx = runtime
+        .invoke::<StructRef, ()>("new_sim", ())
+        .unwrap()
+        .root();
 
     let mut previous = time::Instant::now();
     const FRAME_TIME: time::Duration = time::Duration::from_millis(40);
@@ -33,12 +35,11 @@ fn main() {
             elapsed.as_secs_f32()
         };
 
-        let runtime_ref = runtime.borrow();
-        let _: () = runtime_ref
-            .invoke("sim_update", (ctx.clone(), elapsed_secs))
+        let _: () = runtime
+            .invoke("sim_update", (ctx.as_ref(&runtime), elapsed_secs))
             .unwrap();
         previous = now;
 
-        runtime.borrow_mut().update();
+        runtime.update();
     }
 }
diff --git a/crates/mun_runtime/examples/fibonacci.rs b/crates/mun_runtime/examples/fibonacci.rs
index 03a0d30f7..fddbc7312 100644
--- a/crates/mun_runtime/examples/fibonacci.rs
+++ b/crates/mun_runtime/examples/fibonacci.rs
@@ -1,4 +1,4 @@
-use mun_runtime::RuntimeBuilder;
+use mun_runtime::Runtime;
 use std::env;
 
 // How to run?
@@ -9,20 +9,18 @@ fn main() {
     let lib_dir = env::args().nth(1).expect("Expected path to a Mun library.");
     println!("lib: {}", lib_dir);
 
-    let runtime = RuntimeBuilder::new(lib_dir)
-        .spawn()
+    let mut runtime = Runtime::builder(lib_dir)
+        .finish()
         .expect("Failed to spawn Runtime");
 
-    let mut runtime_ref = runtime.borrow_mut();
-
     loop {
-        let n: i64 = runtime_ref
+        let n: i64 = runtime
             .invoke("nth", ())
-            .unwrap_or_else(|e| e.wait(&mut runtime_ref));
-        let result: i64 = runtime_ref
+            .unwrap_or_else(|e| e.wait(&mut runtime));
+        let result: i64 = runtime
             .invoke("fibonacci", (n,))
-            .unwrap_or_else(|e| e.wait(&mut runtime_ref));
+            .unwrap_or_else(|e| e.wait(&mut runtime));
         println!("fibonacci({}) = {}", n, result);
-        runtime_ref.update();
+        runtime.update();
     }
 }
diff --git a/crates/mun_runtime/src/adt.rs b/crates/mun_runtime/src/adt.rs
index a6cbd562c..00bdcdc45 100644
--- a/crates/mun_runtime/src/adt.rs
+++ b/crates/mun_runtime/src/adt.rs
@@ -4,17 +4,12 @@ use crate::{
     reflection::{
         equals_argument_type, equals_return_type, ArgumentReflection, ReturnTypeReflection,
     },
-    Runtime,
+    GarbageCollector, Runtime,
 };
 use memory::gc::{GcRuntime, HasIndirectionPtr};
 use once_cell::sync::OnceCell;
-use std::cell::{Ref, RefCell};
 use std::{
-    marker::PhantomPinned,
-    mem::MaybeUninit,
-    pin::Pin,
     ptr::{self, NonNull},
-    rc::Rc,
     sync::Arc,
 };
 
@@ -53,8 +48,8 @@ impl<'s> StructRef<'s> {
     }
 
     /// Roots the `StructRef`.
-    pub fn root(self, runtime: Rc<RefCell<Runtime>>) -> RootedStruct {
-        RootedStruct::new(&self.runtime.gc, runtime, self.raw)
+    pub fn root(self) -> RootedStruct {
+        RootedStruct::new(&self.runtime.gc, self.raw)
     }
 
     /// Returns the type information of the struct.
@@ -291,87 +286,23 @@ impl<'s> Marshal<'s> for StructRef<'s> {
 #[derive(Clone)]
 pub struct RootedStruct {
     handle: GcRootPtr,
-    runtime: Rc<RefCell<Runtime>>,
 }
 
 impl RootedStruct {
     /// Creates a `RootedStruct` that wraps a raw Mun struct.
-    fn new<G: GcRuntime<UnsafeTypeInfo>>(
-        gc: &Arc<G>,
-        runtime: Rc<RefCell<Runtime>>,
-        raw: RawStruct,
-    ) -> Self {
-        let handle = {
-            let runtime_ref = runtime.borrow();
-            // Safety: The type returned from `ptr_type` is guaranteed to live at least as long as
-            // `Runtime` does not change. As we hold a shared reference to `Runtime`, this is safe.
-            assert!(unsafe { gc.ptr_type(raw.0).into_inner().as_ref().data.is_struct() });
-
-            GcRootPtr::new(&runtime_ref.gc, raw.0)
-        };
-
-        Self { handle, runtime }
+    fn new(gc: &Arc<GarbageCollector>, raw: RawStruct) -> Self {
+        // Safety: The type returned from `ptr_type` is guaranteed to live at least as long as
+        // `Runtime` does not change. As we hold a shared reference to `Runtime`, this is safe.
+        assert!(unsafe { gc.ptr_type(raw.0).into_inner().as_ref().data.is_struct() });
+        Self {
+            handle: GcRootPtr::new(gc, raw.0),
+        }
     }
 
     /// Converts the `RootedStruct` into a `StructRef`, using an external shared reference to a
     /// `Runtime`.
-    ///
-    /// # Safety
-    ///
-    /// The `RootedStruct` should have been allocated by the `Runtime`.
-    pub unsafe fn as_ref<'r>(&self, runtime: &'r Runtime) -> StructRef<'r> {
+    pub fn as_ref<'r>(&self, runtime: &'r Runtime) -> StructRef<'r> {
+        assert_eq!(Arc::as_ptr(&runtime.gc), self.handle.runtime().as_ptr());
         StructRef::new(RawStruct(self.handle.handle()), runtime)
     }
-
-    /// Converts the `RootedStruct` to a pinned `RootedStructRef` that can be used just like a
-    /// `StructRef`.
-    pub fn by_ref(&self) -> Pin<Box<RootedStructRef>> {
-        RootedStructRef::new(RawStruct(self.handle.handle()), self.borrow_runtime())
-    }
-
-    /// Borrows the struct's runtime.
-    pub fn borrow_runtime(&self) -> Ref<Runtime> {
-        self.runtime.borrow()
-    }
-}
-
-/// Type-agnostic wrapper for safely obtaining a `StructRef` from a `RootedStruct`.
-pub struct RootedStructRef<'s> {
-    runtime: Ref<'s, Runtime>,
-    struct_ref: MaybeUninit<StructRef<'s>>,
-    _pin: PhantomPinned,
-}
-
-impl<'s> RootedStructRef<'s> {
-    fn new(raw: RawStruct, runtime: Ref<'s, Runtime>) -> Pin<Box<Self>> {
-        let struct_ref = RootedStructRef {
-            runtime,
-            struct_ref: MaybeUninit::uninit(),
-            _pin: PhantomPinned,
-        };
-        let mut boxed = Box::pin(struct_ref);
-
-        let runtime = NonNull::from(&boxed.runtime);
-
-        // Safety: Modifying a field doesn't move the whole struct
-        unsafe {
-            let struct_ref = StructRef::new(raw, &*runtime.as_ptr());
-            let mut_ref: Pin<&mut Self> = Pin::as_mut(&mut boxed);
-            Pin::get_unchecked_mut(mut_ref)
-                .struct_ref
-                .as_mut_ptr()
-                .write(struct_ref);
-        }
-
-        boxed
-    }
-}
-
-impl<'s> std::ops::Deref for RootedStructRef<'s> {
-    type Target = StructRef<'s>;
-
-    fn deref(&self) -> &Self::Target {
-        // Safety: We always guarantee to set the `struct_ref` upon construction.
-        unsafe { &*self.struct_ref.as_ptr() }
-    }
 }
diff --git a/crates/mun_runtime/src/lib.rs b/crates/mun_runtime/src/lib.rs
index 12d1b956a..9f39e6f8a 100644
--- a/crates/mun_runtime/src/lib.rs
+++ b/crates/mun_runtime/src/lib.rs
@@ -19,12 +19,10 @@ use mun_project::LOCKFILE_NAME;
 use notify::{RawEvent, RecommendedWatcher, RecursiveMode, Watcher};
 use rustc_hash::FxHashMap;
 use std::{
-    cell::RefCell,
     collections::{HashMap, VecDeque},
     ffi, io, mem,
     path::{Path, PathBuf},
     ptr::NonNull,
-    rc::Rc,
     string::ToString,
     sync::{
         mpsc::{channel, Receiver},
@@ -87,7 +85,7 @@ pub struct RuntimeBuilder {
 
 impl RuntimeBuilder {
     /// Constructs a new `RuntimeBuilder` for the shared library at `library_path`.
-    pub fn new<P: Into<PathBuf>>(library_path: P) -> Self {
+    fn new<P: Into<PathBuf>>(library_path: P) -> Self {
         Self {
             options: RuntimeOptions {
                 library_path: library_path.into(),
@@ -106,9 +104,9 @@ impl RuntimeBuilder {
         self
     }
 
-    /// Spawns a [`Runtime`] with the builder's options.
-    pub fn spawn(self) -> anyhow::Result<Rc<RefCell<Runtime>>> {
-        Runtime::new(self.options).map(|runtime| Rc::new(RefCell::new(runtime)))
+    /// Constructs a [`Runtime`] with the builder's options.
+    pub fn finish(self) -> anyhow::Result<Runtime> {
+        Runtime::new(self.options)
     }
 }
 
@@ -203,6 +201,11 @@ pub struct Runtime {
 }
 
 impl Runtime {
+    /// Constructs a new [`RuntimeBuilder`] to construct a new [`Runtime`] instance.
+    pub fn builder<P: Into<PathBuf>>(library_path: P) -> RuntimeBuilder {
+        RuntimeBuilder::new(library_path)
+    }
+
     /// Constructs a new `Runtime` that loads the library at `library_path` and its
     /// dependencies. The `Runtime` contains a file watcher that is triggered with an interval
     /// of `dur`.
@@ -438,25 +441,25 @@ impl Runtime {
 }
 
 /// An error that might occur when calling a mun function from Rust.
-pub struct InvokeErr<Name: AsRef<str>, T> {
+pub struct InvokeErr<'name, T> {
     msg: String,
-    function_name: Name,
+    function_name: &'name str,
     arguments: T,
 }
 
-impl<Name: AsRef<str>, T> Debug for InvokeErr<Name, T> {
+impl<'name, T> Debug for InvokeErr<'name, T> {
     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
         write!(f, "{}", &self.msg)
     }
 }
 
-impl<Name: AsRef<str>, T> Display for InvokeErr<Name, T> {
+impl<'name, T> Display for InvokeErr<'name, T> {
     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
         write!(f, "{}", &self.msg)
     }
 }
 
-impl<Name: AsRef<str>, T: InvokeArgs> InvokeErr<Name, T> {
+impl<'name, T: InvokeArgs> InvokeErr<'name, T> {
     /// Retries a function invocation once, resulting in a potentially successful
     /// invocation.
     // FIXME: `unwrap_or_else` does not compile for `StructRef`, due to
@@ -577,27 +580,24 @@ impl Runtime {
     pub fn invoke<
         'runtime,
         'ret,
+        'name,
         ReturnType: ReturnTypeReflection + Marshal<'ret> + 'ret,
         ArgTypes: InvokeArgs,
-        Name: AsRef<str>,
     >(
         &'runtime self,
-        function_name: Name,
+        function_name: &'name str,
         arguments: ArgTypes,
-    ) -> Result<ReturnType, InvokeErr<Name, ArgTypes>>
+    ) -> Result<ReturnType, InvokeErr<'name, ArgTypes>>
     where
         'runtime: 'ret,
     {
         // Get the function information from the runtime
-        let function_name_str = function_name.as_ref();
-        let function_info = match self
-            .get_function_definition(function_name_str)
-            .ok_or_else(|| {
-                format!(
-                    "failed to obtain function '{}', no such function exists.",
-                    function_name_str
-                )
-            }) {
+        let function_info = match self.get_function_definition(function_name).ok_or_else(|| {
+            format!(
+                "failed to obtain function '{}', no such function exists.",
+                function_name
+            )
+        }) {
             Ok(function_info) => function_info,
             Err(msg) => {
                 return Err(InvokeErr {
diff --git a/crates/mun_skeptic/src/runtime.rs b/crates/mun_skeptic/src/runtime.rs
index 8c99531f2..8953b8955 100644
--- a/crates/mun_skeptic/src/runtime.rs
+++ b/crates/mun_skeptic/src/runtime.rs
@@ -1,7 +1,7 @@
 //! Code to perform tests on Mun code.
 
 use mun_compiler::{Config, DisplayColor, PathOrInline, RelativePathBuf};
-use mun_runtime::RuntimeBuilder;
+use mun_runtime::Runtime;
 
 /// The type of test to create
 #[derive(Copy, Clone)]
@@ -72,18 +72,17 @@ pub fn run_test(code: &str, mode: TestMode) {
 
     // Create a runtime
     let assembly_path = driver.assembly_output_path_from_file(file_id);
-    let runtime = RuntimeBuilder::new(assembly_path)
-        .spawn()
+    let runtime = Runtime::builder(assembly_path)
+        .finish()
         .expect("error creating runtime for test assembly");
 
     // Find the main function
-    if runtime.borrow().get_function_definition("main").is_none() {
+    if runtime.get_function_definition("main").is_none() {
         panic!("Could not find `main` function");
     }
 
     // Call the main function
     let _: () = runtime
-        .borrow_mut()
         .invoke("main", ())
         .expect("error calling main function");
 }


Test patch:
diff --git a/crates/mun/tests/integration.rs b/crates/mun/tests/integration.rs
index a68138c15..c4d55763d 100644
--- a/crates/mun/tests/integration.rs
+++ b/crates/mun/tests/integration.rs
@@ -1,5 +1,5 @@
 use mun::run_with_args;
-use mun_runtime::RuntimeBuilder;
+use mun_runtime::Runtime;
 use std::ffi::OsString;
 use std::path::Path;
 
@@ -73,8 +73,7 @@ fn build_and_run(project: &Path) {
     let library_path = project.join("target/mod.munlib");
     assert!(library_path.is_file());
 
-    let runtime = RuntimeBuilder::new(&library_path).spawn().unwrap();
-    let runtime_ref = runtime.borrow();
-    let result: f64 = runtime_ref.invoke("main", ()).unwrap();
+    let runtime = Runtime::builder(&library_path).finish().unwrap();
+    let result: f64 = runtime.invoke("main", ()).unwrap();
     assert_eq!(result, 3.14159);
 }
diff --git a/crates/mun_runtime/tests/hot_reloading.rs b/crates/mun_runtime/tests/hot_reloading.rs
index 82b571df7..78376e985 100644
--- a/crates/mun_runtime/tests/hot_reloading.rs
+++ b/crates/mun_runtime/tests/hot_reloading.rs
@@ -14,9 +14,7 @@ fn hotreloadable() {
     .expect("Failed to build test driver");
     assert_invoke_eq!(i32, 5, driver, "main");
 
-    let runtime = driver.runtime();
     driver.update(
-        runtime.borrow(),
         "mod.mun",
         r"
     pub fn main() -> i32 { 10 }
@@ -46,9 +44,7 @@ fn hotreload_struct_decl() {
     )
     .expect("Failed to build test driver");
 
-    let runtime = driver.runtime();
     driver.update(
-        runtime.borrow(),
         "mod.mun",
         r#"
     pub struct(gc) Args {
diff --git a/crates/mun_runtime/tests/marshalling.rs b/crates/mun_runtime/tests/marshalling.rs
index f2a78f61a..573ce24fd 100644
--- a/crates/mun_runtime/tests/marshalling.rs
+++ b/crates/mun_runtime/tests/marshalling.rs
@@ -256,9 +256,7 @@ fn compiler_valid_utf8() {
     )
     .expect("Failed to build test driver");
 
-    let runtime = driver.runtime();
-    let runtime_ref = runtime.borrow();
-    let foo_func = runtime_ref.get_function_definition("foo").unwrap();
+    let foo_func = driver.runtime.get_function_definition("foo").unwrap();
     assert_eq!(
         unsafe { CStr::from_ptr(foo_func.prototype.name) }
             .to_str()
@@ -389,20 +387,19 @@ fn marshal_struct() {
         assert_eq!(Ok(data.0), s.get::<T>(field_name));
     }
 
-    let runtime = driver.runtime();
-    let runtime_ref = runtime.borrow();
-
     let int_data = TestData(3i32, 6i32);
     let bool_data = TestData(true, false);
 
     // Verify that struct marshalling works for fundamental types
-    let mut foo: StructRef = runtime_ref
+    let mut foo: StructRef = driver
+        .runtime
         .invoke("foo_new", (int_data.0, bool_data.0))
         .unwrap();
     test_field(&mut foo, &int_data, "a");
     test_field(&mut foo, &bool_data, "b");
 
-    let mut bar: StructRef = runtime_ref
+    let mut bar: StructRef = driver
+        .runtime
         .invoke("bar_new", (int_data.0, bool_data.0))
         .unwrap();
     test_field(&mut bar, &int_data, "0");
@@ -435,37 +432,43 @@ fn marshal_struct() {
     }
 
     // Verify that struct marshalling works for struct types
-    let mut baz: StructRef = runtime_ref.invoke("baz_new", (foo,)).unwrap();
-    let c1: StructRef = runtime_ref
+    let mut baz: StructRef = driver.runtime.invoke("baz_new", (foo,)).unwrap();
+    let c1: StructRef = driver
+        .runtime
         .invoke("foo_new", (int_data.0, bool_data.0))
         .unwrap();
-    let c2: StructRef = runtime_ref
+    let c2: StructRef = driver
+        .runtime
         .invoke("foo_new", (int_data.1, bool_data.1))
         .unwrap();
     test_struct(&mut baz, c1, c2);
 
-    let mut qux: StructRef = runtime_ref.invoke("qux_new", (bar,)).unwrap();
-    let c1: StructRef = runtime_ref
+    let mut qux: StructRef = driver.runtime.invoke("qux_new", (bar,)).unwrap();
+    let c1: StructRef = driver
+        .runtime
         .invoke("bar_new", (int_data.0, bool_data.0))
         .unwrap();
-    let c2: StructRef = runtime_ref
+    let c2: StructRef = driver
+        .runtime
         .invoke("bar_new", (int_data.1, bool_data.1))
         .unwrap();
     test_struct(&mut qux, c1, c2);
 
     // Verify the dispatch table works when a marshallable wrapper function exists alongside the
     // original function.
-    let mut baz2: StructRef = runtime_ref
+    let mut baz2: StructRef = driver
+        .runtime
         .invoke("baz_new_transitive", (int_data.0, bool_data.0))
         .unwrap();
     // TODO: Find an ergonomic solution for this:
     // .unwrap_or_else(|e| e.wait(&mut runtime_ref));
 
-    let runtime_ref = runtime.borrow();
-    let c1: StructRef = runtime_ref
+    let c1: StructRef = driver
+        .runtime
         .invoke("foo_new", (int_data.0, bool_data.0))
         .unwrap();
-    let c2: StructRef = runtime_ref
+    let c2: StructRef = driver
+        .runtime
         .invoke("foo_new", (int_data.1, bool_data.1))
         .unwrap();
     test_struct(&mut baz2, c1, c2);
@@ -541,11 +544,11 @@ fn marshal_struct() {
     assert!(bar_err.is_err());
 
     // Specify invalid return type
-    let bar_err: Result<i64, _> = runtime_ref.invoke("baz_new", (foo,));
+    let bar_err: Result<i64, _> = driver.runtime.invoke("baz_new", (foo,));
     assert!(bar_err.is_err());
 
     // Pass invalid struct type
-    let bar_err: Result<StructRef, _> = runtime_ref.invoke("baz_new", (bar,));
+    let bar_err: Result<StructRef, _> = driver.runtime.invoke("baz_new", (bar,));
     assert!(bar_err.is_err());
 }
 
@@ -670,10 +673,8 @@ fn test_primitive_types() {
         assert_eq!(Ok(data.0), s.get::<T>(field_name));
     }
 
-    let runtime = driver.runtime();
-    let runtime_ref = runtime.borrow();
-
-    let mut foo: StructRef = runtime_ref
+    let mut foo: StructRef = driver
+        .runtime
         .invoke(
             "new_primitives",
             (
@@ -711,10 +712,7 @@ fn can_add_external_without_return() {
     )
     .expect("Failed to build test driver");
 
-    let runtime = driver.runtime();
-    let runtime_ref = runtime.borrow();
-
-    let _: () = runtime_ref.invoke("main", ()).unwrap();
+    let _: () = driver.runtime.invoke("main", ()).unwrap();
 }
 
 #[test]
diff --git a/crates/mun_runtime/tests/memory.rs b/crates/mun_runtime/tests/memory.rs
index 8241e22c3..7422afad9 100644
--- a/crates/mun_runtime/tests/memory.rs
+++ b/crates/mun_runtime/tests/memory.rs
@@ -30,19 +30,17 @@ fn gc_trace() {
     )
     .expect("Failed to build test driver");
 
-    let runtime = driver.runtime();
-    let runtime_ref = runtime.borrow();
+    let runtime = &driver.runtime;
+    let value: StructRef = runtime.invoke("new_foo", ()).unwrap();
+    let value = value.root();
 
-    let value: StructRef = runtime_ref.invoke("new_foo", ()).unwrap();
-    let value = value.root(driver.runtime());
-
-    assert_eq!(runtime_ref.gc_collect(), false);
-    assert!(runtime_ref.gc_stats().allocated_memory > 0);
+    assert_eq!(runtime.gc_collect(), false);
+    assert!(runtime.gc_stats().allocated_memory > 0);
 
     drop(value);
 
-    assert_eq!(runtime_ref.gc_collect(), true);
-    assert_eq!(runtime_ref.gc_stats().allocated_memory, 0);
+    assert_eq!(runtime.gc_collect(), true);
+    assert_eq!(runtime.gc_stats().allocated_memory, 0);
 }
 
 #[test]
@@ -62,16 +60,12 @@ fn map_struct_insert_field1() {
     )
     .expect("Failed to build test driver");
 
-    let runtime = driver.runtime();
-    let runtime_ref = runtime.borrow();
-
     let b = 5i64;
     let c = 3.0f64;
-    let foo: StructRef = runtime_ref.invoke("foo_new", (b, c)).unwrap();
-    let foo = foo.root(driver.runtime());
+    let foo: StructRef = driver.runtime.invoke("foo_new", (b, c)).unwrap();
+    let foo = foo.root();
 
     driver.update(
-        runtime_ref,
         "mod.mun",
         r#"
         struct Foo {
@@ -81,9 +75,11 @@ fn map_struct_insert_field1() {
         }
     "#,
     );
-    assert_eq!(foo.by_ref().get::<i64>("a").unwrap(), 0);
-    assert_eq!(foo.by_ref().get::<i64>("b").unwrap(), b);
-    assert_eq!(foo.by_ref().get::<f64>("c").unwrap(), c);
+
+    let runtime = &driver.runtime;
+    assert_eq!(foo.as_ref(runtime).get::<i64>("a").unwrap(), 0);
+    assert_eq!(foo.as_ref(runtime).get::<i64>("b").unwrap(), b);
+    assert_eq!(foo.as_ref(runtime).get::<f64>("c").unwrap(), c);
 }
 
 #[test]
@@ -103,16 +99,12 @@ fn map_struct_insert_field2() {
     )
     .expect("Failed to build test driver");
 
-    let runtime = driver.runtime();
-    let runtime_ref = runtime.borrow();
-
     let a = 5i64;
     let c = 3.0f64;
-    let foo: StructRef = runtime_ref.invoke("foo_new", (a, c)).unwrap();
-    let foo = foo.root(driver.runtime());
+    let foo: StructRef = driver.runtime.invoke("foo_new", (a, c)).unwrap();
+    let foo = foo.root();
 
     driver.update(
-        runtime_ref,
         "mod.mun",
         r#"
         struct Foo {
@@ -122,9 +114,9 @@ fn map_struct_insert_field2() {
         }
     "#,
     );
-    assert_eq!(foo.by_ref().get::<i64>("a").unwrap(), a);
-    assert_eq!(foo.by_ref().get::<f64>("b").unwrap(), 0.0);
-    assert_eq!(foo.by_ref().get::<f64>("c").unwrap(), c);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<i64>("a").unwrap(), a);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<f64>("b").unwrap(), 0.0);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<f64>("c").unwrap(), c);
 }
 
 #[test]
@@ -144,16 +136,12 @@ fn map_struct_insert_field3() {
     )
     .expect("Failed to build test driver");
 
-    let runtime = driver.runtime();
-    let runtime_ref = runtime.borrow();
-
     let a = 5i64;
     let b = 3.0f64;
-    let foo: StructRef = runtime_ref.invoke("foo_new", (a, b)).unwrap();
-    let foo = foo.root(driver.runtime());
+    let foo: StructRef = driver.runtime.invoke("foo_new", (a, b)).unwrap();
+    let foo = foo.root();
 
     driver.update(
-        runtime_ref,
         "mod.mun",
         r#"
         struct Foo {
@@ -163,9 +151,9 @@ fn map_struct_insert_field3() {
         }
     "#,
     );
-    assert_eq!(foo.by_ref().get::<i64>("a").unwrap(), a);
-    assert_eq!(foo.by_ref().get::<f64>("b").unwrap(), b);
-    assert_eq!(foo.by_ref().get::<f64>("c").unwrap(), 0.0);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<i64>("a").unwrap(), a);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<f64>("b").unwrap(), b);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<f64>("c").unwrap(), 0.0);
 }
 
 #[test]
@@ -186,17 +174,13 @@ fn map_struct_remove_field1() {
     )
     .expect("Failed to build test driver");
 
-    let runtime = driver.runtime();
-    let runtime_ref = runtime.borrow();
-
     let a = 1.0f64;
     let b = 3.0f64;
     let c = 5i64;
-    let foo: StructRef = runtime_ref.invoke("foo_new", (a, b, c)).unwrap();
-    let foo = foo.root(driver.runtime());
+    let foo: StructRef = driver.runtime.invoke("foo_new", (a, b, c)).unwrap();
+    let foo = foo.root();
 
     driver.update(
-        runtime_ref,
         "mod.mun",
         r#"
         struct Foo {
@@ -204,7 +188,7 @@ fn map_struct_remove_field1() {
         }
     "#,
     );
-    assert_eq!(foo.by_ref().get::<i64>("c").unwrap(), c);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<i64>("c").unwrap(), c);
 }
 
 #[test]
@@ -225,17 +209,13 @@ fn map_struct_remove_field2() {
     )
     .expect("Failed to build test driver");
 
-    let runtime = driver.runtime();
-    let runtime_ref = runtime.borrow();
-
     let a = 1.0f64;
     let b = 5i64;
     let c = 3.0f64;
-    let foo: StructRef = runtime_ref.invoke("foo_new", (a, b, c)).unwrap();
-    let foo = foo.root(driver.runtime());
+    let foo: StructRef = driver.runtime.invoke("foo_new", (a, b, c)).unwrap();
+    let foo = foo.root();
 
     driver.update(
-        runtime_ref,
         "mod.mun",
         r#"
         struct Foo {
@@ -243,7 +223,7 @@ fn map_struct_remove_field2() {
         }
     "#,
     );
-    assert_eq!(foo.by_ref().get::<i64>("b").unwrap(), b);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<i64>("b").unwrap(), b);
 }
 
 #[test]
@@ -264,17 +244,13 @@ fn map_struct_remove_field3() {
     )
     .expect("Failed to build test driver");
 
-    let runtime = driver.runtime();
-    let runtime_ref = runtime.borrow();
-
     let a = 5i64;
     let b = 1.0f64;
     let c = 3.0f64;
-    let foo: StructRef = runtime_ref.invoke("foo_new", (a, b, c)).unwrap();
-    let foo = foo.root(driver.runtime());
+    let foo: StructRef = driver.runtime.invoke("foo_new", (a, b, c)).unwrap();
+    let foo = foo.root();
 
     driver.update(
-        runtime_ref,
         "mod.mun",
         r#"
         struct Foo {
@@ -282,7 +258,7 @@ fn map_struct_remove_field3() {
         }
     "#,
     );
-    assert_eq!(foo.by_ref().get::<i64>("a").unwrap(), a);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<i64>("a").unwrap(), a);
 }
 
 #[test]
@@ -305,19 +281,15 @@ fn map_struct_cast_fields1() {
     )
     .expect("Failed to build test driver");
 
-    let runtime = driver.runtime();
-    let runtime_ref = runtime.borrow();
-
     let a = 1u8;
     let b = -2i16;
     let c = 3u32;
     let d = -4i64;
     let e = 3.14f32;
-    let foo: StructRef = runtime_ref.invoke("foo_new", (a, b, c, d, e)).unwrap();
-    let foo = foo.root(driver.runtime());
+    let foo: StructRef = driver.runtime.invoke("foo_new", (a, b, c, d, e)).unwrap();
+    let foo = foo.root();
 
     driver.update(
-        runtime_ref,
         "mod.mun",
         r#"
         struct Foo(
@@ -329,11 +301,26 @@ fn map_struct_cast_fields1() {
         )
     "#,
     );
-    assert_eq!(foo.by_ref().get::<u16>("0").unwrap(), a.into());
-    assert_eq!(foo.by_ref().get::<i32>("1").unwrap(), b.into());
-    assert_eq!(foo.by_ref().get::<u64>("2").unwrap(), c.into());
-    assert_eq!(foo.by_ref().get::<i128>("3").unwrap(), d.into());
-    assert_eq!(foo.by_ref().get::<f64>("4").unwrap(), e.into());
+    assert_eq!(
+        foo.as_ref(&driver.runtime).get::<u16>("0").unwrap(),
+        a.into()
+    );
+    assert_eq!(
+        foo.as_ref(&driver.runtime).get::<i32>("1").unwrap(),
+        b.into()
+    );
+    assert_eq!(
+        foo.as_ref(&driver.runtime).get::<u64>("2").unwrap(),
+        c.into()
+    );
+    assert_eq!(
+        foo.as_ref(&driver.runtime).get::<i128>("3").unwrap(),
+        d.into()
+    );
+    assert_eq!(
+        foo.as_ref(&driver.runtime).get::<f64>("4").unwrap(),
+        e.into()
+    );
 }
 
 #[test]
@@ -352,15 +339,11 @@ fn map_struct_cast_fields2() {
     )
     .expect("Failed to build test driver");
 
-    let runtime = driver.runtime();
-    let runtime_ref = runtime.borrow();
-
     let a = -2i16;
-    let foo: StructRef = runtime_ref.invoke("foo_new", (a,)).unwrap();
-    let foo = foo.root(driver.runtime());
+    let foo: StructRef = driver.runtime.invoke("foo_new", (a,)).unwrap();
+    let foo = foo.root();
 
     driver.update(
-        runtime_ref,
         "mod.mun",
         r#"
         struct Foo(
@@ -369,7 +352,7 @@ fn map_struct_cast_fields2() {
     "#,
     );
 
-    assert_eq!(foo.by_ref().get::<u16>("0").unwrap(), 0);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<u16>("0").unwrap(), 0);
 }
 
 #[test]
@@ -390,17 +373,13 @@ fn map_struct_swap_fields1() {
     )
     .expect("Failed to build test driver");
 
-    let runtime = driver.runtime();
-    let runtime_ref = runtime.borrow();
-
     let a = 1.0f64;
     let b = 3i64;
     let c = 5.0f64;
-    let foo: StructRef = runtime_ref.invoke("foo_new", (a, b, c)).unwrap();
-    let foo = foo.root(driver.runtime());
+    let foo: StructRef = driver.runtime.invoke("foo_new", (a, b, c)).unwrap();
+    let foo = foo.root();
 
     driver.update(
-        runtime_ref,
         "mod.mun",
         r#"
         struct Foo {
@@ -410,9 +389,9 @@ fn map_struct_swap_fields1() {
         }
     "#,
     );
-    assert_eq!(foo.by_ref().get::<f64>("a").unwrap(), a);
-    assert_eq!(foo.by_ref().get::<i64>("b").unwrap(), b);
-    assert_eq!(foo.by_ref().get::<f64>("c").unwrap(), c);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<f64>("a").unwrap(), a);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<i64>("b").unwrap(), b);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<f64>("c").unwrap(), c);
 }
 
 #[test]
@@ -434,18 +413,14 @@ fn map_struct_swap_fields2() {
     )
     .expect("Failed to build test driver");
 
-    let runtime = driver.runtime();
-    let runtime_ref = runtime.borrow();
-
     let a = 1.0f64;
     let b = 3i64;
     let c = 5.0f64;
     let d = 7i64;
-    let foo: StructRef = runtime_ref.invoke("foo_new", (a, b, c, d)).unwrap();
-    let foo = foo.root(driver.runtime());
+    let foo: StructRef = driver.runtime.invoke("foo_new", (a, b, c, d)).unwrap();
+    let foo = foo.root();
 
     driver.update(
-        runtime_ref,
         "mod.mun",
         r#"
         struct Foo {
@@ -456,10 +431,10 @@ fn map_struct_swap_fields2() {
         }
     "#,
     );
-    assert_eq!(foo.by_ref().get::<f64>("a").unwrap(), a);
-    assert_eq!(foo.by_ref().get::<i64>("b").unwrap(), b);
-    assert_eq!(foo.by_ref().get::<f64>("c").unwrap(), c);
-    assert_eq!(foo.by_ref().get::<i64>("d").unwrap(), d);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<f64>("a").unwrap(), a);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<i64>("b").unwrap(), b);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<f64>("c").unwrap(), c);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<i64>("d").unwrap(), d);
 }
 
 #[test]
@@ -480,17 +455,13 @@ fn map_struct_rename_field1() {
     )
     .expect("Failed to build test driver");
 
-    let runtime = driver.runtime();
-    let runtime_ref = runtime.borrow();
-
     let a = 5i64;
     let b = 1.0f64;
     let c = 3.0f64;
-    let foo: StructRef = runtime_ref.invoke("foo_new", (a, b, c)).unwrap();
-    let foo = foo.root(driver.runtime());
+    let foo: StructRef = driver.runtime.invoke("foo_new", (a, b, c)).unwrap();
+    let foo = foo.root();
 
     driver.update(
-        runtime_ref,
         "mod.mun",
         r#"
         struct Foo {
@@ -500,9 +471,9 @@ fn map_struct_rename_field1() {
         }
     "#,
     );
-    assert_eq!(foo.by_ref().get::<i64>("a").unwrap(), a);
-    assert_eq!(foo.by_ref().get::<f64>("d").unwrap(), b);
-    assert_eq!(foo.by_ref().get::<f64>("c").unwrap(), c);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<i64>("a").unwrap(), a);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<f64>("d").unwrap(), b);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<f64>("c").unwrap(), c);
 }
 
 #[test]
@@ -523,17 +494,13 @@ fn map_struct_rename_field2() {
     )
     .expect("Failed to build test driver");
 
-    let runtime = driver.runtime();
-    let runtime_ref = runtime.borrow();
-
     let a = 5i64;
     let b = 1.0f64;
     let c = 3.0f64;
-    let foo: StructRef = runtime_ref.invoke("foo_new", (a, b, c)).unwrap();
-    let foo = foo.root(driver.runtime());
+    let foo: StructRef = driver.runtime.invoke("foo_new", (a, b, c)).unwrap();
+    let foo = foo.root();
 
     driver.update(
-        runtime_ref,
         "mod.mun",
         r#"
         struct Foo {
@@ -543,9 +510,9 @@ fn map_struct_rename_field2() {
         }
     "#,
     );
-    assert_eq!(foo.by_ref().get::<i64>("d").unwrap(), a);
-    assert_eq!(foo.by_ref().get::<f64>("e").unwrap(), b);
-    assert_eq!(foo.by_ref().get::<f64>("f").unwrap(), c);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<i64>("d").unwrap(), a);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<f64>("e").unwrap(), b);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<f64>("f").unwrap(), c);
 }
 
 #[test]
@@ -567,18 +534,14 @@ fn map_struct_all() {
     )
     .expect("Failed to build test driver");
 
-    let runtime = driver.runtime();
-    let runtime_ref = runtime.borrow();
-
     let a = 5i32;
     let b = 1.0f64;
     let c = 3.0f64;
     let d = -1i32;
-    let foo: StructRef = runtime_ref.invoke("foo_new", (a, b, c, d)).unwrap();
-    let foo = foo.root(driver.runtime());
+    let foo: StructRef = driver.runtime.invoke("foo_new", (a, b, c, d)).unwrap();
+    let foo = foo.root();
 
     driver.update(
-        runtime_ref,
         "mod.mun",
         r#"
         pub struct Foo {
@@ -590,10 +553,13 @@ fn map_struct_all() {
         }
     "#,
     );
-    assert_eq!(foo.by_ref().get::<f64>("b").unwrap(), b);
-    assert_eq!(foo.by_ref().get::<i64>("d").unwrap(), d.into());
-    assert_eq!(foo.by_ref().get::<i32>("e").unwrap(), a);
-    assert_eq!(foo.by_ref().get::<i32>("f").unwrap(), 0);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<f64>("b").unwrap(), b);
+    assert_eq!(
+        foo.as_ref(&driver.runtime).get::<i64>("d").unwrap(),
+        d.into()
+    );
+    assert_eq!(foo.as_ref(&driver.runtime).get::<i32>("e").unwrap(), a);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<i32>("f").unwrap(), 0);
 }
 
 #[test]
@@ -614,17 +580,13 @@ fn delete_used_struct() {
     )
     .expect("Failed to build test driver");
 
-    let runtime = driver.runtime();
-    let runtime_ref = runtime.borrow();
-
     let a = 5i64;
     let b = 1.0f64;
     let c = 3.0f64;
-    let foo: StructRef = runtime_ref.invoke("foo_new", (a, b, c)).unwrap();
-    let foo = foo.root(driver.runtime());
+    let foo: StructRef = driver.runtime.invoke("foo_new", (a, b, c)).unwrap();
+    let foo = foo.root();
 
     driver.update(
-        runtime_ref,
         "mod.mun",
         r#"
         pub struct Bar(i64);
@@ -635,19 +597,11 @@ fn delete_used_struct() {
     "#,
     );
 
-    assert!(driver
-        .runtime()
-        .borrow()
-        .get_function_definition("foo_new")
-        .is_none());
-    assert!(driver
-        .runtime()
-        .borrow()
-        .get_function_definition("bar_new")
-        .is_some());
-    assert_eq!(foo.by_ref().get::<i64>("a").unwrap(), a);
-    assert_eq!(foo.by_ref().get::<f64>("b").unwrap(), b);
-    assert_eq!(foo.by_ref().get::<f64>("c").unwrap(), c);
+    assert!(driver.runtime.get_function_definition("foo_new").is_none());
+    assert!(driver.runtime.get_function_definition("bar_new").is_some());
+    assert_eq!(foo.as_ref(&driver.runtime).get::<i64>("a").unwrap(), a);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<f64>("b").unwrap(), b);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<f64>("c").unwrap(), c);
 }
 
 #[test]
@@ -680,31 +634,29 @@ fn nested_structs() {
     )
     .expect("Failed to build test driver");
 
-    let runtime = driver.runtime();
-    let runtime_ref = runtime.borrow();
-
     let a = -3.14f32;
     let b = 6.18f32;
-    let gc_struct: StructRef = runtime_ref.invoke("new_gc_struct", (a, b)).unwrap();
-    let value_struct: StructRef = runtime_ref.invoke("new_value_struct", (a, b)).unwrap();
+    let gc_struct: StructRef = driver.runtime.invoke("new_gc_struct", (a, b)).unwrap();
+    let value_struct: StructRef = driver.runtime.invoke("new_value_struct", (a, b)).unwrap();
 
-    let gc_wrapper: StructRef = runtime_ref
+    let gc_wrapper: StructRef = driver
+        .runtime
         .invoke("new_gc_wrapper", (gc_struct.clone(), value_struct.clone()))
         .unwrap();
 
-    let value_wrapper: StructRef = runtime_ref
+    let value_wrapper: StructRef = driver
+        .runtime
         .invoke(
             "new_value_wrapper",
             (gc_struct.clone(), value_struct.clone()),
         )
         .unwrap();
 
-    let gc_wrapper = gc_wrapper.root(driver.runtime());
-    let value_wrapper = value_wrapper.root(driver.runtime());
+    let gc_wrapper = gc_wrapper.root();
+    let value_wrapper = value_wrapper.root();
 
     // Tests mapping of `gc -> gc`, `value -> value`
     driver.update(
-        runtime_ref,
         "mod.mun",
         r#"
     pub struct(gc) GcStruct(f64, f64);
@@ -715,25 +667,36 @@ fn nested_structs() {
     "#,
     );
 
-    let gc_0 = gc_wrapper.by_ref().get::<StructRef>("0").unwrap();
+    let gc_0 = gc_wrapper
+        .as_ref(&driver.runtime)
+        .get::<StructRef>("0")
+        .unwrap();
     assert_eq!(gc_0.get::<f64>("0"), Ok(a.into()));
     assert_eq!(gc_0.get::<f64>("1"), Ok(b.into()));
 
-    let gc_1 = gc_wrapper.by_ref().get::<StructRef>("1").unwrap();
+    let gc_1 = gc_wrapper
+        .as_ref(&driver.runtime)
+        .get::<StructRef>("1")
+        .unwrap();
     assert_eq!(gc_1.get::<f64>("0"), Ok(a.into()));
     assert_eq!(gc_1.get::<f64>("1"), Ok(b.into()));
 
-    let value_0 = value_wrapper.by_ref().get::<StructRef>("0").unwrap();
+    let value_0 = value_wrapper
+        .as_ref(&driver.runtime)
+        .get::<StructRef>("0")
+        .unwrap();
     assert_eq!(value_0.get::<f64>("0"), Ok(a.into()));
     assert_eq!(value_0.get::<f64>("1"), Ok(b.into()));
 
-    let value_1 = value_wrapper.by_ref().get::<StructRef>("1").unwrap();
+    let value_1 = value_wrapper
+        .as_ref(&driver.runtime)
+        .get::<StructRef>("1")
+        .unwrap();
     assert_eq!(value_1.get::<f64>("0"), Ok(a.into()));
     assert_eq!(value_1.get::<f64>("1"), Ok(b.into()));
 
     // Tests an identity mapping
     driver.update(
-        runtime.borrow(),
         "mod.mun",
         r#"
     pub struct(gc) GcStruct(f64, f64);
@@ -744,30 +707,41 @@ fn nested_structs() {
     "#,
     );
 
-    let gc_0 = gc_wrapper.by_ref().get::<StructRef>("0").unwrap();
+    let gc_0 = gc_wrapper
+        .as_ref(&driver.runtime)
+        .get::<StructRef>("0")
+        .unwrap();
     assert_eq!(gc_0.get::<f64>("0"), Ok(a.into()));
     assert_eq!(gc_0.get::<f64>("1"), Ok(b.into()));
 
-    let gc_1 = gc_wrapper.by_ref().get::<StructRef>("1").unwrap();
+    let gc_1 = gc_wrapper
+        .as_ref(&driver.runtime)
+        .get::<StructRef>("1")
+        .unwrap();
     assert_eq!(gc_1.get::<f64>("0"), Ok(a.into()));
     assert_eq!(gc_1.get::<f64>("1"), Ok(b.into()));
 
-    let value_0 = value_wrapper.by_ref().get::<StructRef>("0").unwrap();
+    let value_0 = value_wrapper
+        .as_ref(&driver.runtime)
+        .get::<StructRef>("0")
+        .unwrap();
     assert_eq!(value_0.get::<f64>("0"), Ok(a.into()));
     assert_eq!(value_0.get::<f64>("1"), Ok(b.into()));
 
-    let value_1 = value_wrapper.by_ref().get::<StructRef>("1").unwrap();
+    let value_1 = value_wrapper
+        .as_ref(&driver.runtime)
+        .get::<StructRef>("1")
+        .unwrap();
     assert_eq!(value_1.get::<f64>("0"), Ok(a.into()));
     assert_eq!(value_1.get::<f64>("1"), Ok(b.into()));
 
-    let gc_0 = gc_0.root(driver.runtime());
-    let gc_1 = gc_1.root(driver.runtime());
-    let value_0 = value_0.root(driver.runtime());
-    let value_1 = value_1.root(driver.runtime());
+    let gc_0 = gc_0.root();
+    let gc_1 = gc_1.root();
+    let value_0 = value_0.root();
+    let value_1 = value_1.root();
 
     // Tests mapping of `gc -> value`, `value -> gc`
     driver.update(
-        runtime.borrow(),
         "mod.mun",
         r#"
     struct(value) GcStruct(f64, f64);
@@ -778,22 +752,33 @@ fn nested_structs() {
     "#,
     );
 
-    assert_eq!(gc_0.by_ref().get::<f64>("0"), Ok(a.into()));
-    assert_eq!(gc_0.by_ref().get::<f64>("1"), Ok(b.into()));
+    assert_eq!(gc_0.as_ref(&driver.runtime).get::<f64>("0"), Ok(a.into()));
+    assert_eq!(gc_0.as_ref(&driver.runtime).get::<f64>("1"), Ok(b.into()));
 
-    assert_eq!(gc_1.by_ref().get::<f64>("0"), Ok(a.into()));
-    assert_eq!(gc_1.by_ref().get::<f64>("1"), Ok(b.into()));
+    assert_eq!(gc_1.as_ref(&driver.runtime).get::<f64>("0"), Ok(a.into()));
+    assert_eq!(gc_1.as_ref(&driver.runtime).get::<f64>("1"), Ok(b.into()));
 
-    assert_eq!(value_0.by_ref().get::<f64>("0"), Ok(a.into()));
-    assert_eq!(value_0.by_ref().get::<f64>("1"), Ok(b.into()));
+    assert_eq!(
+        value_0.as_ref(&driver.runtime).get::<f64>("0"),
+        Ok(a.into())
+    );
+    assert_eq!(
+        value_0.as_ref(&driver.runtime).get::<f64>("1"),
+        Ok(b.into())
+    );
 
-    assert_eq!(value_1.by_ref().get::<f64>("0"), Ok(a.into()));
-    assert_eq!(value_1.by_ref().get::<f64>("1"), Ok(b.into()));
+    assert_eq!(
+        value_1.as_ref(&driver.runtime).get::<f64>("0"),
+        Ok(a.into())
+    );
+    assert_eq!(
+        value_1.as_ref(&driver.runtime).get::<f64>("1"),
+        Ok(b.into())
+    );
 
     // Tests mapping of different struct type, when `gc -> value`, `value -> gc`, and
     // retention of an old library (due to removal of `GcStruct` and `ValueStruct`)
     driver.update(
-        runtime.borrow(),
         "mod.mun",
         r#"
     struct(gc) GcStruct2(f64);
@@ -805,43 +790,66 @@ fn nested_structs() {
     );
 
     // Existing, rooted objects should remain untouched
-    assert_eq!(gc_0.by_ref().get::<f64>("0"), Ok(a.into()));
-    assert_eq!(gc_0.by_ref().get::<f64>("1"), Ok(b.into()));
+    assert_eq!(gc_0.as_ref(&driver.runtime).get::<f64>("0"), Ok(a.into()));
+    assert_eq!(gc_0.as_ref(&driver.runtime).get::<f64>("1"), Ok(b.into()));
 
-    assert_eq!(gc_1.by_ref().get::<f64>("0"), Ok(a.into()));
-    assert_eq!(gc_1.by_ref().get::<f64>("1"), Ok(b.into()));
+    assert_eq!(gc_1.as_ref(&driver.runtime).get::<f64>("0"), Ok(a.into()));
+    assert_eq!(gc_1.as_ref(&driver.runtime).get::<f64>("1"), Ok(b.into()));
 
-    assert_eq!(value_0.by_ref().get::<f64>("0"), Ok(a.into()));
-    assert_eq!(value_0.by_ref().get::<f64>("1"), Ok(b.into()));
+    assert_eq!(
+        value_0.as_ref(&driver.runtime).get::<f64>("0"),
+        Ok(a.into())
+    );
+    assert_eq!(
+        value_0.as_ref(&driver.runtime).get::<f64>("1"),
+        Ok(b.into())
+    );
 
-    assert_eq!(value_1.by_ref().get::<f64>("0"), Ok(a.into()));
-    assert_eq!(value_1.by_ref().get::<f64>("1"), Ok(b.into()));
+    assert_eq!(
+        value_1.as_ref(&driver.runtime).get::<f64>("0"),
+        Ok(a.into())
+    );
+    assert_eq!(
+        value_1.as_ref(&driver.runtime).get::<f64>("1"),
+        Ok(b.into())
+    );
 
     // The values in the wrappers should have been updated
-    let mut gc_0 = gc_wrapper.by_ref().get::<StructRef>("0").unwrap();
+    let mut gc_0 = gc_wrapper
+        .as_ref(&driver.runtime)
+        .get::<StructRef>("0")
+        .unwrap();
     assert_eq!(gc_0.get::<f64>("0"), Ok(0.0));
     gc_0.set::<f64>("0", a.into()).unwrap();
 
-    let mut gc_1 = gc_wrapper.by_ref().get::<StructRef>("1").unwrap();
+    let mut gc_1 = gc_wrapper
+        .as_ref(&driver.runtime)
+        .get::<StructRef>("1")
+        .unwrap();
     assert_eq!(gc_1.get::<f64>("0"), Ok(0.0));
     gc_1.set::<f64>("0", a.into()).unwrap();
 
-    let mut value_0 = value_wrapper.by_ref().get::<StructRef>("0").unwrap();
+    let mut value_0 = value_wrapper
+        .as_ref(&driver.runtime)
+        .get::<StructRef>("0")
+        .unwrap();
     assert_eq!(value_0.get::<f64>("0"), Ok(0.0));
     value_0.set::<f64>("0", a.into()).unwrap();
 
-    let mut value_1 = value_wrapper.by_ref().get::<StructRef>("1").unwrap();
+    let mut value_1 = value_wrapper
+        .as_ref(&driver.runtime)
+        .get::<StructRef>("1")
+        .unwrap();
     assert_eq!(value_1.get::<f64>("0"), Ok(0.0));
     value_1.set::<f64>("0", a.into()).unwrap();
 
-    let gc_0 = gc_0.root(driver.runtime());
-    let gc_1 = gc_1.root(driver.runtime());
-    let value_0 = value_0.root(driver.runtime());
-    let value_1 = value_1.root(driver.runtime());
+    let gc_0 = gc_0.root();
+    let gc_1 = gc_1.root();
+    let value_0 = value_0.root();
+    let value_1 = value_1.root();
 
     // Tests mapping of different struct type, when `gc -> gc`, `value -> value`
     driver.update(
-        runtime.borrow(),
         "mod.mun",
         r#"
     struct(gc) GcStruct(f64, f64);
@@ -853,25 +861,43 @@ fn nested_structs() {
     );
 
     // Existing, rooted objects should remain untouched
-    assert_eq!(gc_0.by_ref().get::<f64>("0"), Ok(a.into()));
-    assert_eq!(gc_1.by_ref().get::<f64>("0"), Ok(a.into()));
-    assert_eq!(value_0.by_ref().get::<f64>("0"), Ok(a.into()));
-    assert_eq!(value_1.by_ref().get::<f64>("0"), Ok(a.into()));
+    assert_eq!(gc_0.as_ref(&driver.runtime).get::<f64>("0"), Ok(a.into()));
+    assert_eq!(gc_1.as_ref(&driver.runtime).get::<f64>("0"), Ok(a.into()));
+    assert_eq!(
+        value_0.as_ref(&driver.runtime).get::<f64>("0"),
+        Ok(a.into())
+    );
+    assert_eq!(
+        value_1.as_ref(&driver.runtime).get::<f64>("0"),
+        Ok(a.into())
+    );
 
     // The values in the wrappers should have been updated
-    let gc_0 = gc_wrapper.by_ref().get::<StructRef>("0").unwrap();
+    let gc_0 = gc_wrapper
+        .as_ref(&driver.runtime)
+        .get::<StructRef>("0")
+        .unwrap();
     assert_eq!(gc_0.get::<f64>("0"), Ok(0.0));
     assert_eq!(gc_0.get::<f64>("1"), Ok(0.0));
 
-    let gc_1 = gc_wrapper.by_ref().get::<StructRef>("1").unwrap();
+    let gc_1 = gc_wrapper
+        .as_ref(&driver.runtime)
+        .get::<StructRef>("1")
+        .unwrap();
     assert_eq!(gc_1.get::<f64>("0"), Ok(0.0));
     assert_eq!(gc_1.get::<f64>("1"), Ok(0.0));
 
-    let value_0 = value_wrapper.by_ref().get::<StructRef>("0").unwrap();
+    let value_0 = value_wrapper
+        .as_ref(&driver.runtime)
+        .get::<StructRef>("0")
+        .unwrap();
     assert_eq!(value_0.get::<f64>("0"), Ok(0.0));
     assert_eq!(value_0.get::<f64>("1"), Ok(0.0));
 
-    let value_1 = value_wrapper.by_ref().get::<StructRef>("1").unwrap();
+    let value_1 = value_wrapper
+        .as_ref(&driver.runtime)
+        .get::<StructRef>("1")
+        .unwrap();
     assert_eq!(value_1.get::<f64>("0"), Ok(0.0));
     assert_eq!(value_1.get::<f64>("1"), Ok(0.0));
 }
@@ -893,16 +919,12 @@ fn insert_struct() {
     )
     .expect("Failed to build test driver");
 
-    let runtime = driver.runtime();
-    let runtime_ref = runtime.borrow();
-
     let a = 5i64;
     let c = 3.0f64;
-    let foo: StructRef = runtime_ref.invoke("foo_new", (a, c)).unwrap();
-    let foo = foo.root(driver.runtime());
+    let foo: StructRef = driver.runtime.invoke("foo_new", (a, c)).unwrap();
+    let foo = foo.root();
 
     driver.update(
-        runtime_ref,
         "mod.mun",
         r#"
         struct Bar(i64);
@@ -917,12 +939,12 @@ fn insert_struct() {
     "#,
     );
 
-    assert_eq!(foo.by_ref().get::<i64>("a").unwrap(), a);
-    assert_eq!(foo.by_ref().get::<f64>("c").unwrap(), c);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<i64>("a").unwrap(), a);
+    assert_eq!(foo.as_ref(&driver.runtime).get::<f64>("c").unwrap(), c);
 
-    let b = foo.by_ref().get::<StructRef>("b").unwrap();
+    let b = foo.as_ref(&driver.runtime).get::<StructRef>("b").unwrap();
     assert_eq!(b.get::<i64>("0"), Ok(0));
 
-    let d = foo.by_ref().get::<StructRef>("d").unwrap();
+    let d = foo.as_ref(&driver.runtime).get::<StructRef>("d").unwrap();
     assert_eq!(d.get::<f64>("0"), Ok(0.0));
 }
diff --git a/crates/mun_runtime/tests/runtime.rs b/crates/mun_runtime/tests/runtime.rs
index 57b3a39ca..861caf075 100644
--- a/crates/mun_runtime/tests/runtime.rs
+++ b/crates/mun_runtime/tests/runtime.rs
@@ -14,11 +14,7 @@ fn invoke() {
     )
     .expect("Failed to build test driver");
 
-    let result: i32 = driver
-        .runtime()
-        .borrow()
-        .invoke("sum", (123i32, 456i32))
-        .unwrap();
+    let result: i32 = driver.runtime.invoke("sum", (123i32, 456i32)).unwrap();
     assert_eq!(123 + 456, result);
 }
 
diff --git a/crates/mun_runtime/tests/threading.rs b/crates/mun_runtime/tests/threading.rs
new file mode 100644
index 000000000..4f71fed4e
--- /dev/null
+++ b/crates/mun_runtime/tests/threading.rs
@@ -0,0 +1,5 @@
+use mun_runtime::Runtime;
+
+// Ensures the [`Runtime`] is Send
+trait IsSend: Send {}
+impl IsSend for Runtime {}
diff --git a/crates/mun_runtime/tests/util.rs b/crates/mun_runtime/tests/util.rs
index f5d720df8..13fb0eab1 100644
--- a/crates/mun_runtime/tests/util.rs
+++ b/crates/mun_runtime/tests/util.rs
@@ -3,9 +3,7 @@
 macro_rules! assert_invoke_eq {
     ($ExpectedType:ty, $ExpectedResult:expr, $Driver:expr, $Name:expr, $($Arg:expr),*) => {
         {
-            let runtime = $Driver.runtime();
-            let runtime_ref = runtime.borrow();
-            let result: $ExpectedType = runtime_ref.invoke($Name, ( $($Arg,)*) ).unwrap();
+            let result: $ExpectedType = $Driver.runtime.invoke($Name, ( $($Arg,)*) ).unwrap();
             assert_eq!(
                 result, $ExpectedResult, "{} == {:?}",
                 stringify!(mun_runtime::invoke_fn!(runtime_ref, $($Arg)*).unwrap()),
diff --git a/crates/mun_test/src/driver.rs b/crates/mun_test/src/driver.rs
index 98bac9d19..ed693d2b1 100644
--- a/crates/mun_test/src/driver.rs
+++ b/crates/mun_test/src/driver.rs
@@ -1,9 +1,7 @@
 use compiler::{Config, DisplayColor, Driver, PathOrInline, RelativePathBuf};
 use runtime::{Runtime, RuntimeBuilder};
 use std::{
-    cell::{Ref, RefCell},
     path::{Path, PathBuf},
-    rc::Rc,
     thread::sleep,
     time::{Duration, Instant},
 };
@@ -127,7 +125,9 @@ impl std::fmt::Debug for CompileTestDriver {
 /// This allows testing of Mun constructs that depend on hot-reloading.
 pub struct CompileAndRunTestDriver {
     driver: CompileTestDriver,
-    runtime: Rc<RefCell<Runtime>>,
+
+    /// The runtime created by this instance.
+    pub runtime: Runtime,
 }
 
 impl std::fmt::Debug for CompileAndRunTestDriver {
@@ -146,8 +146,8 @@ impl CompileAndRunTestDriver {
         config_fn: impl FnOnce(RuntimeBuilder) -> RuntimeBuilder,
     ) -> Result<Self, anyhow::Error> {
         let driver = CompileTestDriver::from_fixture(fixture);
-        let builder = RuntimeBuilder::new(driver.lib_path());
-        let runtime = config_fn(builder).spawn()?;
+        let builder = Runtime::builder(driver.lib_path());
+        let runtime = config_fn(builder).finish()?;
 
         Ok(Self { driver, runtime })
     }
@@ -159,8 +159,8 @@ impl CompileAndRunTestDriver {
         config_fn: impl FnOnce(RuntimeBuilder) -> RuntimeBuilder,
     ) -> Result<Self, anyhow::Error> {
         let driver = CompileTestDriver::from_file(text);
-        let builder = RuntimeBuilder::new(driver.lib_path());
-        let runtime = config_fn(builder).spawn()?;
+        let builder = Runtime::builder(driver.lib_path());
+        let runtime = config_fn(builder).finish()?;
 
         Ok(Self { driver, runtime })
     }
@@ -171,17 +171,11 @@ impl CompileAndRunTestDriver {
     /// A reference to the borrowed `runtime` is used as an argument to allow moving of the
     /// existing borrow inside the update function. This obviates the necessity for `update` to use
     /// the `Runtime`.
-    pub fn update(
-        &mut self,
-        runtime: Ref<'_, Runtime>,
-        path: impl AsRef<paths::RelativePath>,
-        text: &str,
-    ) {
+    pub fn update(&mut self, path: impl AsRef<paths::RelativePath>, text: &str) {
         self.driver.update(path, text);
 
         let start_time = Instant::now();
-        drop(runtime);
-        while !self.runtime().borrow_mut().update() {
+        while !self.runtime.update() {
             let now = Instant::now();
             if now - start_time > Duration::from_secs(10) {
                 panic!("runtime did not update after recompilation within 10 seconds");
@@ -190,9 +184,4 @@ impl CompileAndRunTestDriver {
             }
         }
     }
-
-    /// Returns the `Runtime` used by the driver.
-    pub fn runtime(&self) -> Rc<RefCell<Runtime>> {
-        self.runtime.clone()
-    }
 }