Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Statics

A static item is a named piece of mutable state that lives for as long as the program using it, rather than for the duration of a single call.

static COUNTER = 0;

fn bump() {
    COUNTER += 1;
    COUNTER
}

println!("{}", bump());
println!("{}", bump());
println!("{}", bump());
$> cargo run -- run scripts/book/statics/counter.rn
1
2
3

This is the main thing that distinguishes a static from a const. A constant is inlined at every place it is used, so each use produces a fresh copy and there is nothing to assign to. A static instead occupies a numbered slot, and every use reads or writes that one slot.

Statics are declared at the item level, so they can appear inside modules and be addressed by path like any other item:

pub mod config {
    pub static WIDTH = 800;
}

println!("{}", config::WIDTH);

Initializers

The expression a static is initialized with must be a constant expression, the same kind that a const accepts. It is evaluated lazily, the first time something reads the static:

static LIMIT = 4 * 8;

The initializer is optional. A static declared without one starts out uninitialized, and reading it before anything has assigned it is an error:

static CONFIG;

println!("{CONFIG}");
$> cargo run -- run scripts/book/statics/uninitialized.rn
error: Reading uninitialized static `CONFIG` (slot 0)
  ┌─ scripts/book/statics/uninitialized.rn:3:10
  │
3 │ println!("{CONFIG}");
  │          ^^^^^^^^^^ Reading uninitialized static `CONFIG` (slot 0)

Note that the static is named in the diagnostic. This relies on debug information being available. Without it the message falls back to reporting the slot number alone.

Since a static isn’t known until runtime, it cannot be used anywhere a value is required at compile time. Referring to one from a const, from another static’s initializer, or from a pattern is a compile error.

Statics hold values, not copies

As with every other value in Rune, what a static holds is a reference counted value. Reading a static therefore hands you a handle to the same instance, so mutating through that handle mutates what the static holds without any assignment taking place:

static SEEN = [];

fn record(value) {
    SEEN.push(value);
}

record(1);
record(2);

println!("{SEEN:?}");
$> cargo run -- run scripts/book/statics/shared_value.rn
[1, 2]

Storage is configured on the virtual machine

The values of statics are not stored in the Unit. A unit only records how many statics there are, which slot each one was assigned, and what each one’s initializer is. The values live in a separate Globals storage which is handed to the virtual machine when it is constructed:

#![allow(unused)]
fn main() {
let globals = Globals::new(unit.clone())?;
let mut vm = Vm::new(runtime, unit).with_globals(globals);
}

This separation means one compiled unit can back any number of independent states - one per tenant, per session, per test - simply by constructing more than one Globals for it. Two machines built from the same unit with different storage do not observe each other’s statics.

Globals is a cheaply cloned handle, so the caller can hold on to a clone and read or write statics while the machine that uses them is running. Statics are addressed by item, the same way functions are:

#![allow(unused)]
fn main() {
let globals = Globals::new(unit.clone())?;

// Assign before anything runs.
globals.set(["COUNTER"], rune::to_value(41i64)?)?;

let mut vm = Vm::new(runtime, unit).with_globals(globals.clone());
vm.call(["main"], ())?;

// Read back what the script left behind.
let value = globals.get(["COUNTER"])?;
}

Writing a static from the outside takes precedence over its initializer, since the initializer only runs for a slot which is still uninitialized.

If you construct a Vm without calling Vm::with_globals, it has no storage and reading a static errors. Build::build_vm provisions storage for you, so the shorthand path needs no extra setup:

#![allow(unused)]
fn main() {
let mut vm = rune::prepare(&mut sources)
    .with_context(&context)
    .build_vm()?;
}

Providing a global registry

The most useful thing this enables is handing scripts access to something the host owns - a registry, a service handle, a configuration object - without threading it through the signature of every function that might need it.

Declare a static with no initializer and let the host fill it in:

static REGISTRY;

fn describe(key) {
    match REGISTRY.get(key) {
        Some(value) => format!("{key} is {value}"),
        None => format!("{key} is unset"),
    }
}

describe takes no registry argument, and neither does anything that calls it. The host writes the slot once, before the first call:

//! Injecting a host-owned registry into scripts through a bare `static`.
//!
//! The registry is never passed as an argument. The script declares
//! `static REGISTRY;` with no initializer, the host writes it into the
//! [`Globals`] storage once, and every function in the script can reach it.

use std::collections::HashMap;

use rune::runtime::Globals;
use rune::sync::Arc;
use rune::termcolor::{ColorChoice, StandardStream};
use rune::{Any, ContextError, Diagnostics, Module, Vm};

/// A registry owned by the host and shared with every script that runs.
#[derive(Debug, Any)]
struct Registry {
    settings: HashMap<String, i64>,
}

impl Registry {
    /// Look up a setting, returning `None` if it isn't present.
    #[rune::function]
    fn get(&self, key: &str) -> Option<i64> {
        self.settings.get(key).copied()
    }
}

fn module() -> Result<Module, ContextError> {
    let mut m = Module::new();
    m.ty::<Registry>()?;
    m.function_meta(Registry::get)?;
    Ok(m)
}

fn main() -> rune::support::Result<()> {
    let mut context = rune_modules::default_context()?;
    context.install(module()?)?;
    let runtime = Arc::try_new(context.runtime()?)?;

    // Note that `REGISTRY` is not a parameter of either function.
    let mut sources = rune::sources!(
        entry => {
            static REGISTRY;

            fn describe(key) {
                match REGISTRY.get(key) {
                    Some(value) => format!("{key} is {value}"),
                    None => format!("{key} is unset"),
                }
            }

            pub fn main() {
                [describe("width"), describe("height"), describe("depth")]
            }
        }
    );

    let mut diagnostics = Diagnostics::new();

    let result = rune::prepare(&mut sources)
        .with_context(&context)
        .with_diagnostics(&mut diagnostics)
        .build();

    if !diagnostics.is_empty() {
        let mut writer = StandardStream::stderr(ColorChoice::Always);
        diagnostics.emit(&mut writer, &sources)?;
    }

    let unit = Arc::try_new(result?)?;

    let registry = Registry {
        settings: HashMap::from([("width".to_owned(), 800), ("height".to_owned(), 600)]),
    };

    // Wire the registry into the slot the script declared, before it runs.
    let globals = Globals::new(unit.clone())?;
    globals.set(["REGISTRY"], rune::to_value(registry)?)?;

    let mut vm = Vm::new(runtime, unit).with_globals(globals);

    let output: Vec<String> = rune::from_value(vm.call(["main"], ())?)?;

    for line in output {
        println!("{line}");
    }

    Ok(())
}
$> cargo run --example global_registry
width is 800
height is 600
depth is unset

Because the storage is per-machine rather than per-unit, this stays sound when the same script is used for many independent things at once - each caller constructs its own Globals and injects its own registry.

Declaring statics with the build

A host which owns the state doesn’t necessarily want the script to be responsible for declaring it. Statics can therefore also be declared with the build through Statics, in which case the source doesn’t mention them at all:

#![allow(unused)]
fn main() {
let mut statics = Statics::new();
statics.insert(["REGISTRY"])?;

let unit = rune::prepare(&mut sources)
    .with_context(&context)
    .with_statics(&statics)
    .build()?;
}

The script above then works unchanged with its static REGISTRY; removed, in the same way that a native module makes a function available without the script declaring it. Every source in the unit sees the static, and the name is an item, so ["config", "REGISTRY"] declares it inside of the config module.

There is no source to evaluate, so a static declared this way has no initializer. It starts out uninitialized just like static REGISTRY; does, and the caller is expected to assign it before anything reads it:

//! Demonstrates statics which are declared with the build rather than by the
//! script.
//!
//! A host which owns a piece of state usually doesn't want the script to be
//! responsible for declaring it. Declaring it with the build makes it available
//! to every source in the unit without them saying anything about it, in the
//! same way that a native module makes a function available.

use rune::runtime::Globals;
use rune::sync::Arc;
use rune::termcolor::{ColorChoice, StandardStream};
use rune::{Diagnostics, Statics, Vm};

fn main() -> rune::support::Result<()> {
    let context = rune_modules::default_context()?;
    let runtime = Arc::try_new(context.runtime()?)?;

    // Note that the script uses `GREETING` and `CALLS` without declaring them.
    let mut sources = rune::sources!(
        entry => {
            pub fn main(name) {
                CALLS += 1;
                format!("{GREETING}, {name}! (call {CALLS})")
            }
        }
    );

    let mut statics = Statics::new();
    statics.insert(["GREETING"])?;
    statics.insert(["CALLS"])?;

    let mut diagnostics = Diagnostics::new();

    let result = rune::prepare(&mut sources)
        .with_context(&context)
        .with_diagnostics(&mut diagnostics)
        .with_statics(&statics)
        .build();

    if !diagnostics.is_empty() {
        let mut writer = StandardStream::stderr(ColorChoice::Always);
        diagnostics.emit(&mut writer, &sources)?;
    }

    let unit = Arc::try_new(result?)?;

    // A declared static has no initializer, so both of these have to be
    // assigned before the script reads them.
    let globals = Globals::new(unit.clone())?;
    globals.set(["GREETING"], rune::to_value("Hello")?)?;
    globals.set(["CALLS"], rune::to_value(0i64)?)?;

    let mut vm = Vm::new(runtime, unit).with_globals(globals.clone());

    for name in ["Jane", "John"] {
        let output = vm.call(["main"], (name,))?;
        let output: String = rune::from_value(output)?;
        println!("{output}");
    }

    let calls = globals.get(["CALLS"])?.expect("CALLS to be initialized");
    println!("calls: {}", rune::from_value::<i64>(calls)?);
    Ok(())
}
$> cargo run --example declared_statics
Hello, Jane! (call 1)
Hello, John! (call 2)
calls: 2

Since a declared static occupies the item it is named with, a source which declares an item of the same name is a compile error rather than a silent override of either one.

Statics and threads

Static storage holds Value’s, so like the Vm itself it cannot be shared across threads. Each thread builds its own Globals out of the shared Unit; see Multithreading for the general picture.

That does not stop statics from being shared state across threads, it just moves the responsibility. Assign a host value whose interior is synchronized, such as an Arc<Mutex<..>>, an atomic, or a channel sender, and every thread’s slot points at the same interior, with that value deciding how concurrent access is handled. This pairs naturally with a pool of machines: a pooled machine keeps the storage it was built with, so a worker that takes one out of the pool already has its statics wired up. See Statics across threads for a worked example.

One consequence is worth calling out: Function::into_sync produces a SyncFunction, which cannot carry the storage. Calling a static through a SyncFunction reports that no storage has been configured.