Skip to main content

Plugins

Everything above the core is a plugin: physics, rendering, audio, networking. One Plugin trait covers them all, and one source builds either way.

StaticDynamic
Ships asPart of the engine binary, behind a cargo featureA shared library in the project's extensions/ directory
Runs onEvery platform, the web includedmacOS, Linux and Windows
Written inRustRust, or C, Zig, Odin, C++
NeedsAn engine built from sourceA desktop release, which has the extensions feature
Startercrates/balaur_physicsexamples/extension_greeter (Rust), examples/extension_c_counter (C)

A dynamic plugin in Rust

balaur_plugin is the one dependency.

Cargo.toml
[package]
name = "greeter"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]

[dependencies]
balaur_plugin = { git = "https://github.com/balaurengine/balaur", tag = "v0.2.0" }
src/lib.rs
use balaur_plugin::prelude::*;

pub struct Greeter {
manifest: Manifest,
}

impl Default for Greeter {
fn default() -> Self {
Self {
manifest: Manifest::new("greeter", env!("CARGO_PKG_VERSION")),
}
}
}

impl Plugin for Greeter {
fn manifest(&self) -> &Manifest {
&self.manifest
}

fn declare(&mut self, reg: &mut Registry<'_>) -> Result<()> {
let mut m = reg.script_module("greeter")?;
m.function("greet", |_: &Engine, name: String| Ok(format!("hello, {name}")));
m.constant("VERSION", Value::Str(env!("CARGO_PKG_VERSION").into()));
Ok(())
}
}

balaur_plugin::export_plugin!(Greeter);
cargo build --release
cp target/release/libgreeter.dylib my-game/extensions/ # .so on Linux, greeter.dll on Windows

Scripts call greeter::greet("world") like any built-in module.

  • Match the engine's compiler. Copy the engine's rust-toolchain.toml into the plugin and pin tag to the version balaur --version prints. The host refuses any other build and names the difference.
  • Registry also takes resources, systems, components, presets, asset types and replay sources. balaur_plugin re-exports balaur_core, balaur_script, anyhow and toml.
  • A Rust plugin reads and writes its own resources, not the engine's. The two builds compute different type ids for the same type.

A dynamic plugin in C

Any compiler with a C ABI works. No toolchain match is needed.

  • The header is include/balaur_extension.h in every desktop download, and crates/balaur_plugin/include in the repository.
  • Export balaur_extension_abi, balaur_extension_name, balaur_extension_version and balaur_extension_declare.
  • declare receives a table of host functions, opens a script module and adds functions and constants to it.
  • Values are borrowed for one call, and the host copies what is returned. Static storage keeps a returned string alive.
int32_t balaur_extension_declare(const BalaurApi *api, BalaurRegistry *registry) {
BalaurModule *m = api->module_open(registry, balaur_str("counter"));
api->module_function(m, balaur_str("add"), add, NULL);
api->module_close(m);
return 0;
}
cc -shared -fPIC -I path/to/include -o libcounter.so counter.c # .dylib on macOS

A C plugin covers script functions and constants. Components, systems and calls back into scripts are on the roadmap.

A static plugin

A static plugin is part of the engine build and runs on the web and mobile.

  • Add the crate to the modules! table in crates/balaur/src/lib.rs, behind a cargo feature.
  • Build the editor and the runtime templates with that feature (A custom engine). The editor needs the feature too: balaur export compiles scripts against the modules its own binary has.

Turning plugins on and off

[plugins] in project.toml decides what a project loads.

  • audio = false turns a plugin off. Turning off one every build has, such as physics, is refused.
  • A table turns a plugin on and hands it the table, read with reg.config().
  • Asking for a plugin nothing registered is an error naming the feature to build with.
  • Manifest::new(..).requiring(&["physics"]) declares a dependency. Plugins load by name and requirement, in the same order on every machine.
  • balaur export ships the libraries for its target with a desktop game: extensions/ beside a flat executable, Contents/PlugIns inside a .app, signed with the game's identity. Keep a .so, a .dylib and a .dll side by side to export for all three.
  • A web, iOS or Android export warns and ships without them. A plugin those platforms need is a static one.

The design is in ARCHITECTURE.md.