Plugins and extensions
· 2 min read
One Plugin trait. Linked in behind a cargo feature, it is a module; built
as a shared library and dropped into a project's extensions/, it is an
extension. The boundary takes C too.
What landed
- One trait. A plugin has a manifest and a
declare: resources, systems, components, script modules.Registryis deliberately narrow and free of generics, because the same operations have to cross a C ABI. - Modules.
balaur_audiowas the first. Built without it, the binary is 19.2 MB against 21.5 MB, and rodio, cpal and the platform audio stack are not compiled at all. - Extensions.
examples/extension_greeterlives outside the engine crates, depends only onbalaur_plugin, and a game script callsgreeter::greet("balaur")on code the engine never linked against. - A C API.
examples/extension_c_counteris the same thing in C: a committed header, a table of host function pointers, no allocation across the boundary. Anything with a C ABI can write one.
impl Plugin for Greeter {
fn manifest(&self) -> &Manifest {
&self.manifest
}
fn declare(&mut self, reg: &mut Registry<'_>) -> anyhow::Result<()> {
let mut m = reg.script_module("greeter")?;
m.function("greet", |_: &Engine, name: String| Ok(format!("hello, {name}")));
Ok(())
}
}
Refuse before crashing
Rust has no stable ABI, so a library built by a different compiler is
undefined behaviour, not a version error. The first symbol an extension
exports is a repr(C) tag of fixed-size fields — rustc, engine version,
registry ABI — and only when all three match does anything Rust-shaped
cross. A mismatch names which one differs.
Extensions load sorted by name and by declared requirements, never by directory order: load order is registration order, and registration order is the simulation. Modules and extensions has the boundary table.
