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 borrows from aProgram.
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.
let (program, line_tables) = brink_runtime::link(&story_data)?;
let mut story = Story::new(&program, line_tables);
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
Line. The loop, theLinevariants, 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.
A minimal driver looks like this — see the execution-model page for what each arm means:
loop {
match story.continue_single()? {
Line::Text { text, .. } | Line::Done { text, .. } => print!("{text}"),
Line::Choices { text, choices, .. } => {
print!("{text}");
story.choose(/* player's pick */ choices[0].index)?;
}
Line::End { text, .. } => { print!("{text}"); break; }
}
}