Embedding the Runtime
brink-runtime is the bytecode VM. Embed it to drive ink stories from a Rust
program — a game, a tool, a custom engine. It depends only on brink-format, so
pulling it in doesn’t drag the compiler along.
This section is the hands-on path. For the mental model behind it, read The Execution Model; for the exhaustive API surface, see Reference › Runtime API.
The two-object model
The runtime keeps compiled data and execution state in separate objects — this is the one structural idea to internalize:
Program— the immutable bytecode, variable defaults, and metadata. Built once vialink(), shareable across threads.Story— all the mutable state: operand stack, call stack, globals, visit counts, output buffer, and the line tables it renders with. It holds anArc<Program>.
Because Program is immutable, many Story instances can run concurrently
against one Program — parallel playthroughs, or replaying with different
choices, share the compiled data for free.
#![allow(unused)]
fn main() {
extern crate brink_format;
extern crate brink_runtime;
use brink_format::StoryData;
use brink_runtime::{RuntimeError, Story};
fn demo(story_data: StoryData) -> Result<(), RuntimeError> {
use std::sync::Arc;
let (program, line_tables) = brink_runtime::link(&story_data)?;
let mut story: Story = Story::new(Arc::new(program), line_tables);
let _ = &mut story;
Ok(())
}
}
Story owns a refcount, not a borrow, so it carries no lifetime — it can be
moved into a thread, stored in a struct, or held in an ECS component without
threading a 'p parameter through your types. To fan out playthroughs, clone
the Arc (cheap) and give each Story its own line tables:
#![allow(unused)]
fn main() {
extern crate brink_format;
extern crate brink_runtime;
use std::sync::Arc;
use brink_format::LineEntry;
use brink_runtime::{Program, Story};
fn demo(program: Program, line_tables: Vec<Vec<LineEntry>>) {
let program = Arc::new(program);
let mut a: Story = Story::new(Arc::clone(&program), line_tables.clone());
let mut b: Story = Story::new(Arc::clone(&program), line_tables);
let _ = (&mut a, &mut b);
}
}
The shape of embedding
- Loading & Linking — produce
StoryData(compile.inkor read.inkb) andlink()it into aProgram+ line tables. - Drive it — step the story and react to each
Step. The loop, theStepvariants, and choice handling all live in The Execution Model. - External Functions — let the story call back
into your code (
EXTERNALfunctions), synchronously or deferred. - Named Flows — run parallel execution contexts within one story.
- Sessions & Replay — journal a playthrough for a save file, deterministic replay, and state snapshots/diffs.
- Speculation — run the story forward from its current state without committing to it, then discard the run.
A minimal driver looks like this — see the execution-model page for what each arm means:
#![allow(unused)]
fn main() {
extern crate brink_runtime;
use brink_runtime::{Step, RuntimeError, Story};
fn demo(story: &mut Story) -> Result<(), RuntimeError> {
loop {
match story.continue_single()? {
Step::Line(line) => print!("{}", line.text),
Step::Done => {}
Step::Choices(choices) => {
story.choose(/* player's pick */ choices[0].index)?;
}
Step::End => break,
// Reserved for flow suspension; not yet emitted.
Step::Suspended => break,
}
}
Ok(())
}
}