Header-only C++20 bridge between the Element ECS and the Oak scripting runtime.
The bridge lets Oak scripts declare and run ECS systems over native elm components. It is optional: pure ECS users only need Element. Reach for the bridge when you want gameplay or logic authored in Oak to drive elm::Registry data.
The single public header is include/elm/oak_bridge_core.hpp. All bridge symbols live in the oak_bridge namespace.
- A C++20 compiler.
- Element, consumed as the
elementMeson dependency. - An Oak checkout providing the Oak headers and a runtime library (for example
libacorn).
This project is header-only, so there is nothing to compile on its own. It exposes the element-oak-bridge Meson dependency, which adds the bridge include directory plus the Oak include paths that elm/oak_bridge_core.hpp needs. It does not build Oak itself.
oak_root is required and must point at your Oak source root:
meson setup build path/to/element-oak-bridge -Doak_root=/path/to/oakConsume it from another Meson project:
element_oak_bridge_dep = dependency('element-oak-bridge', required: true)
oak_runtime_dep = cpp.find_library('acorn', dirs: '/path/to/oak/build')
executable('my_oak_target',
'main.cpp',
dependencies: [element_oak_bridge_dep, oak_runtime_dep],
)The dependency adds Oak include paths for headers under:
${OAK_ROOT}/include${OAK_ROOT}/src/runtime${OAK_ROOT}/src/common${OAK_ROOT}/src/compiler/internal
You are still responsible for providing and linking the Oak runtime/library your application uses. In a typical setup that runtime is imported from ${OAK_BUILD}/libacorn.so.
A typical Oak integration does this at startup:
- Include
elm/oak_bridge_core.hpp. - Create an object that implements
oak_bridge::BridgeInterfacefor your application. - Store the bridge's
oak_bridge::BridgeInterface*in each Oak VM'suser_data. - Initialize Oak through either the
oak.hppRAII wrappers or the raw C API. - Register your C++ component types with
oak_bridge::ComponentBuilder<T>. - Call
oak_bridge::register_common_native_functions(...)andoak_bridge::register_attributes(...). - Load an Oak entry file with
oak_bridge::load_program(...). - Convert
@ElementSystemfunctions intoelm::Systemvalues withoak_bridge::build_systems(...). - Rebuild view caches before each tick with
oak_bridge::rebuild_view_caches(...). - Run the generated systems through the same scheduler you use for C++ systems.
Minimal shape:
#include <elm/oak_bridge_core.hpp>
#include <vector>
struct Position {
float x = 0.0f;
float y = 0.0f;
};
struct Velocity {
float x = 0.0f;
float y = 0.0f;
};
class MyBridge final : public oak_bridge::BridgeInterface {
// Store an elm::Registry, Oak allocator/VM state, component slots,
// view caches, and implement the BridgeInterface virtual functions.
};
int main()
{
MyBridge bridge;
std::vector<elm::System> systems;
// During setup, after creating Oak compile options and the Oak Entity type:
// oak_bridge::ComponentBuilder<Position>(compile_opts, entity_type, "Position")
// .field<&Position::x>("x")
// .field<&Position::y>("y")
// .register_component(register_component);
// oak_bridge::ComponentBuilder<Velocity>(compile_opts, entity_type, "Velocity")
// .field<&Velocity::x>("x")
// .field<&Velocity::y>("y")
// .register_component(register_component);
// oak_bridge::register_attributes(compile_opts, components, pending_systems, &bridge);
// auto program = oak_bridge::load_program("scripts/main.oak", compile_opts, allocator, vm, module_registry);
// oak_bridge::build_systems(program.chunk, allocator, entity_type, module_registry,
// components, pending_systems, slot_view_types,
// slots, &delta_seconds, systems);
// Each tick:
// oak_bridge::rebuild_view_caches(registry, view_caches, slots, slot_count);
// for (auto& system : systems) {
// system.run(registry);
// }
}The bridge includes Oak's C++20 facade and accepts its RAII wrappers directly. This gives bridge integrations ownership-safe values, allocators, VMs, compile options, module registries, typed bindings, callable bindings, and diagnostics while preserving the raw C API overloads used by existing integrations.
oak::Allocator allocator;
oak::CompileOptions options(allocator);
oak::VM vm(allocator);
oak::ModuleRegistry modules(allocator);
options.with_stdlib();
vm.set_user_data(static_cast<oak_bridge::BridgeInterface*>(&bridge));
auto entity = options.bind_type<elm::Entity>("Entity", OAK_BIND_TYPE_VALUE);
oak_bridge::register_common_native_functions(options, entity.raw());
oak_bridge::register_attributes(options, entity.raw(), components,
view_slot_indices, pending_systems,
slot_view_types, attribute_state);
auto program = oak_bridge::load_program("scripts/main.oak", options, vm, modules);Use oak::CompileOptions::bind_fn, bind_type, bind_enum, and bind_attr
alongside the bridge registrations to expose application-specific Oak APIs.
Oak-generated systems are plain elm::System values, so they run through the same scheduler as your C++ systems. See the Element README for scheduler and conflict-detection details.