5.3.10. C++ Integration: Custom Modules
This tutorial shows how to organize C++ bindings into multiple modules with dependencies between them. Topics covered:
Splitting types and functions across separate modules
Module::require()— looking up modules by nameaddBuiltinDependency— declaring module dependenciesaddConstant— exporting compile-time constantsinitDependencies()— deferred initialization for robust orderingNEED_MODULEordering in the host program
5.3.10.1. Prerequisites
Tutorial 09 completed (tutorial_integration_cpp_operators_and_properties).
Familiarity with
ManagedStructureAnnotationandaddExtern.
5.3.10.2. Why multiple modules?
So far, every tutorial has used a single module. In real projects,
types and functions often belong to separate logical groups. Splitting
them into modules gives scripts fine-grained require control and
keeps each module focused.
In this tutorial we create two modules:
math_types — defines the
Vec2type and mathematical constantsmath_utils — utility functions that depend on
Vec2
5.3.10.3. Module A: types and constants
The first module registers a type and several constants:
class Module_MathTypes : public Module {
public:
Module_MathTypes() : Module("math_types") {
ModuleLibrary lib(this);
lib.addBuiltInModule();
addAnnotation(make_smart<Vec2Annotation>(lib));
addExtern<DAS_BIND_FUN(make_vec2),
SimNode_ExtFuncCallAndCopyOrMove>(
*this, lib, "make_vec2",
SideEffects::none, "make_vec2")
->args({"x", "y"});
// Constants — appear as `let` in daslang
addConstant(*this, "PI", (float)M_PI);
addConstant(*this, "TWO_PI", (float)(2.0 * M_PI));
addConstant(*this, "HALF_PI", (float)(M_PI / 2.0));
addConstant(*this, "ORIGIN", "origin_marker"); // string
}
};
REGISTER_MODULE(Module_MathTypes);
addConstant creates a module-level let constant. It supports
numeric types (int, float, double, uint, etc.) and
strings.
5.3.10.4. Module B: depends on Module A — initDependencies()
The second module uses Vec2 from math_types in its function
signatures. Instead of registering everything in the constructor, it
uses the ``initDependencies()`` pattern — the production standard
for modules with cross-module dependencies.
5.3.10.4.1. Why not the constructor?
Modules are constructed during static initialization (triggered by
REGISTER_MODULE). At that point, the dependency module may not
exist yet — its constructor may not have run. Module::require()
could return nullptr, and the dependency’s types would not be
registered.
initDependencies() is called later by the engine, after all modules
are constructed. This guarantees that Module::require() can find
every loaded module.
5.3.10.4.2. The pattern
class Module_MathUtils : public Module {
bool initialized = false; // guard against double-init
public:
Module_MathUtils() : Module("math_utils") {
// Empty — all work deferred to initDependencies().
}
bool initDependencies() override {
if (initialized) return true; // already done
// 1. Look up the dependency by name
auto mod = Module::require("math_types");
if (!mod) return false; // not loaded
// 2. Ensure it is fully initialized
if (!mod->initDependencies()) return false;
// 3. Mark ourselves as initialized (before registration
// to prevent re-entry from circular dependencies)
initialized = true;
// 4. Set up library with dependency
ModuleLibrary lib(this);
lib.addBuiltInModule();
addBuiltinDependency(lib, mod);
// 5. Register functions — Vec2 is now known
addExtern<DAS_BIND_FUN(vec2_length)>(
*this, lib, "length", ...);
addExtern<DAS_BIND_FUN(vec2_lerp), ...>(
*this, lib, "lerp", ...);
// ...
return true;
}
};
REGISTER_MODULE(Module_MathUtils);
Key elements:
|
Prevents double-initialization |
|
Looks up module by registered name |
|
Ensures dependency is fully registered |
|
Makes dependency’s types visible + records the relationship for the compiler |
|
Signals success or failure to the engine |
5.3.10.4.3. Real-world examples
This pattern appears throughout the daslang ecosystem:
dasAudio (requires rtti):
class Module_Audio : public Module {
bool initialized = false;
public:
Module_Audio() : Module("audio") {}
bool initDependencies() override {
if (initialized) return true;
if (!Module::require("rtti")) return false;
initialized = true;
ModuleLibrary lib(this);
lib.addBuiltInModule();
addBuiltinDependency(lib, Module::require("rtti"));
// ... register types, functions ...
return true;
}
};
dasIMGUI_NODE_EDITOR (requires imgui):
bool Module_dasIMGUI_NODE_EDITOR::initDependencies() {
if (initialized) return true;
auto mod_imgui = Module::require("imgui");
if (!mod_imgui) return false;
if (!mod_imgui->initDependencies()) return false;
initialized = true;
lib.addModule(this);
lib.addBuiltInModule();
lib.addModule(mod_imgui);
// ... register types, functions ...
return true;
}
The mod->initDependencies() call is especially important — it
chains initialization so that modules initialize in the correct order
regardless of how NEED_MODULE lines are arranged.
5.3.10.4.4. Constructor vs initDependencies() — when to use which
Constructor |
Leaf modules with no custom-module deps
(like |
``initDependencies()`` |
Any module that depends on another custom module — use this by default |
5.3.10.5. Host program — NEED_MODULE
In main(), both modules must be listed. With initDependencies(),
explicit ordering is no longer critical — the chained
mod->initDependencies() calls handle it automatically:
int main(int, char * []) {
NEED_ALL_DEFAULT_MODULES;
NEED_MODULE(Module_MathTypes);
NEED_MODULE(Module_MathUtils);
Module::Initialize();
// ... compile & run ...
Module::Shutdown();
return 0;
}
NEED_MODULE forces the linker to pull in the module’s translation
unit. Module::require("name") finds a module by its registered
name string, returning nullptr if not found.
5.3.10.6. Using both modules from daslang
Scripts require each module independently:
options gen2
require math_types
require math_utils
[export]
def test() {
// Constants from math_types
print("PI = {PI}\n")
// Type from math_types
let a = make_vec2(3.0, 4.0)
// Functions from math_utils (operate on Vec2)
print("length(a) = {length(a)}\n")
let mid = lerp(a, make_vec2(1.0, 0.0), 0.5)
print("mid = ({mid.x}, {mid.y})\n")
5.3.10.7. Building and running
cmake --build build --config Release --target integration_cpp_10
bin\Release\integration_cpp_10.exe
Expected output:
=== Constants ===
PI = 3.1415927
TWO_PI = 6.2831855
HALF_PI = 1.5707964
ORIGIN = origin_marker
=== Vec2 ===
a = (3, 4)
b = (1, 0)
=== Utility functions ===
length(a) = 5
dot(a, b) = 3
normalize(a) = (0.6, 0.8)
=== Operators ===
a + b = (4, 4)
a * 2 = (6, 8)
=== Lerp ===
lerp(a, b, 0.5) = (2, 2)
lerp(a, b, 0.0) = (3, 4)
lerp(a, b, 1.0) = (1, 0)
See also
Full source:
10_custom_modules.cpp,
10_custom_modules.das
Previous tutorial: tutorial_integration_cpp_operators_and_properties
Next tutorial: tutorial_integration_cpp_context_variables