Skip to content

Commit

Permalink
Add book sections on multithreading and hot reloading
Browse files Browse the repository at this point in the history
  • Loading branch information
udoprog committed Apr 14, 2024
1 parent 4aa9e1f commit bda5ca0
Show file tree
Hide file tree
Showing 8 changed files with 377 additions and 3 deletions.
2 changes: 2 additions & 0 deletions book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
- [Closures](./closures.md)
- [Asynchronous programming](./async.md)
- [Streams](./streams.md)
- [Multithreading](./multithreading.md)
- [Hot reloading](./hot_reloading.md)
- [Macros](./macros.md)
- [Advanced](./advanced.md)
- [Safety](./safety.md)
Expand Down
22 changes: 22 additions & 0 deletions book/src/hot_reloading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Hot reloading

Compiling a [`Unit`] and a [`RuntimeContext`] are expensive operations compared
to the cost of calling a function. So you should try to do this as little as
possible. It is appropriate to recompile a script when the source of the script
changes. This section provides you with details for how this can be done when
loading scripts from the filesystem.

A typical way to accomplish this is to watch a scripts directory using the
[`notify` crate]. This allow the application to generate events whenever changes
to the directory are detected. See the [`hot_reloading` example] and in
particular the [`PathReloader`] type.

```
{{#include ../../examples/examples/hot_reloading.rs}}
```

[`notify` crate]: https://docs.rs/notify
[`Unit`]: https://docs.rs/rune/latest/rune/runtime/unit/struct.Unit.html
[`hot_reloading` example]: https://github.com/rune-rs/rune/blob/main/examples/examples/hot_reloading.rs
[`PathReloader`]: https://github.com/rune-rs/rune/blob/main/examples/examples/hot_reloading/path_reloader.rs

61 changes: 61 additions & 0 deletions book/src/multithreading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Multithreading

Rune is thread safe, but the [`Vm`] does not implement `Sync` so cannot directly
be shared across threads. This section details instead how you are intended to
use Rune in a multithreaded environment.

Compiling a [`Unit`] and a [`RuntimeContext`] are expensive operations compared
to the cost of calling a function. So you should try to do this as little as
possible. It is appropriate to recompile a script when the source of the script
changes. See the [Hot reloading] section for more information on this.

Once you have a `Unit` and a `RuntimeContext` they are thread safe and can be
used by multiple threads simultaneously through `Arc<Unit>` and
`Arc<RuntimeContext>`. Constructing a `Vm` with these through `Vm::new` is a
very cheap operation.

> `Vm`'s do allocate a stack, to avoid this you'd have to employ even more
> advanced techniques, such as storing the virtual machine in a thread local and
> using it by swapping out the `Unit` and `RuntimeContext` associated with it
> through [`Vm::unit_mut`] and [`Vm::context_mut`] respectively.
```rust
let unit: Arc<Unit> = /* todo */;
let context: Arc<RuntimeContext> = /* todo */;

std::thread::spawn(move || {
let mut vm = Vm::new(unit, context);
let value = vm.call(["function"], (42,))?;
Ok(())
});
```

Using [`Vm::send_execute`] is a way to assert that a given execution is thread
safe. And allows you to use Rune in asynchronous multithreaded environments,
such as Tokio. This is achieved by ensuring that all captured arguments are
[`ConstValue`]'s, which in contrast to [`Value`]'s are guaranteed to be
thread-safe:

```
{{#include ../../examples/examples/tokio_spawn.rs}}
```

Finally [`Function::into_sync`] exists to coerce a function into a
[`SyncFunction], which is a thread-safe variant of a regular [`Function`]. This
is a fallible operation since all values which are captured in the function-type
in case its a closure has to be coerced to [`ConstValue`]. If this is not the
case, the conversion will fail.

[`ConstValue`]: https://docs.rs/rune/latest/rune/runtime/enum.ConstValue.html
[`Function::into_sync`]: https://docs.rs/rune/latest/rune/runtime/struct.Function.html#method.into_sync
[`Function`]: https://docs.rs/rune/latest/rune/runtime/struct.Function.html
[`notify`]: https://docs.rs/notify
[`RuntimeContext`]: https://docs.rs/rune/latest/rune/runtime/struct.RuntimeContext.html
[`SyncFunction`]: https://docs.rs/rune/latest/rune/runtime/struct.SyncFunction.html
[`Unit`]: https://docs.rs/rune/latest/rune/runtime/struct.Unit.html
[`Value`]: https://docs.rs/rune/latest/rune/runtime/enum.Value.html
[`Vm::context_mut`]: https://docs.rs/rune/latest/rune/runtime/struct.Vm.html#method.context_mut
[`Vm::send_execute`]: https://docs.rs/rune/latest/rune/runtime/struct.Vm.html#method.send_execute
[`Vm::unit_mut`]: https://docs.rs/rune/latest/rune/runtime/struct.Vm.html#method.unit_mut
[`Vm`]: https://docs.rs/rune/latest/rune/runtime/struct.Vm.html
[Hot reloading]: ./hot_reloading.md
33 changes: 32 additions & 1 deletion crates/rune/src/runtime/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ pub struct Vm {

impl Vm {
/// Construct a new virtual machine.
///
/// Constructing a virtual machine is a cheap constant-time operation.
///
/// See [`unit_mut`] and [`context_mut`] documentation for information on
/// how to re-use existing [`Vm`]'s.
///
/// [`unit_mut`]: Vm::unit_mut
/// [`context_mut`]: Vm::context_mut
pub const fn new(context: Arc<RuntimeContext>, unit: Arc<Unit>) -> Self {
Self::with_stack(context, unit, Stack::new())
}
Expand Down Expand Up @@ -169,6 +177,19 @@ impl Vm {
}

/// Access the context related to the virtual machine mutably.
///
/// Note that this can be used to swap out the [`RuntimeContext`] associated
/// with the running vm. Note that this is only necessary if the underlying
/// [`Context`] is different or has been modified. In contrast to
/// constructing a [`new`] vm, this allows for amortised re-use of any
/// allocations.
///
/// After doing this, it's important to call [`clear`] to clean up any
/// residual state.
///
/// [`clear`]: Vm::clear
/// [`Context`]: crate::Context
/// [`new`]: Vm::new
#[inline]
pub fn context_mut(&mut self) -> &mut Arc<RuntimeContext> {
&mut self.context
Expand All @@ -180,7 +201,17 @@ impl Vm {
&self.context
}

/// Access the underlying unit of the virtual machine mutablys.
/// Access the underlying unit of the virtual machine mutably.
///
/// Note that this can be used to swap out the [`Unit`] of execution in the
/// running vm. In contrast to constructing a [`new`] vm, this allows for
/// amortised re-use of any allocations.
///
/// After doing this, it's important to call [`clear`] to clean up any
/// residual state.
///
/// [`clear`]: Vm::clear
/// [`new`]: Vm::new
#[inline]
pub fn unit_mut(&mut self) -> &mut Arc<Unit> {
&mut self.unit
Expand Down
7 changes: 5 additions & 2 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ full = ["rune-modules/full"]
default = ["full"]

[dependencies]
tokio = { version = "1.28.1", features = ["macros"] }

rune = { path = "../crates/rune" }
rune-modules = { path = "../crates/rune-modules" }

tokio = { version = "1.28.1", features = ["macros"] }
notify = "6.1.1"
anyhow = "1.0.82"
pin-project = "1.1.5"
59 changes: 59 additions & 0 deletions examples/examples/hot_reloading.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#[path = "hot_reloading/path_reloader.rs"]
mod path_reloader;

use std::path::PathBuf;
use std::pin::pin;
use std::sync::Arc;

use anyhow::{Context as _, Result};
use rune::{Context, Vm};

#[tokio::main]
async fn main() -> Result<()> {
let root =
PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").context("missing CARGO_MANIFEST_DIR")?);

let context = Context::with_default_modules()?;

let mut exit = pin!(tokio::signal::ctrl_c());
let mut reloader = pin!(path_reloader::PathReloader::new(
root.join("scripts"),
&context
)?);

let context = Arc::new(context.runtime()?);

let mut events = Vec::new();

loop {
tokio::select! {
_ = exit.as_mut() => {
break;
}
result = reloader.as_mut().watch(&mut events) => {
result?;
}
}

for event in events.drain(..) {
match event {
path_reloader::PathEvent::Added(path, unit) => {
let mut vm = Vm::new(context.clone(), unit);

if let Err(error) = vm.call(["hello"], ()) {
println!("Error: {}", error);
}
}
path_reloader::PathEvent::Removed(path, unit) => {
let mut vm = Vm::new(context.clone(), unit);

if let Err(error) = vm.call(["goodbye"], ()) {
println!("Error: {}", error);
}
}
}
}
}

Ok(())
}
Loading

0 comments on commit bda5ca0

Please sign in to comment.