8.4.19. C++ Integration: Class Adapters
This tutorial shows how to let daslang classes derive from a C++ abstract base class and have C++ call their virtual methods seamlessly. This is the key pattern for game object systems where scripts define behavior but C++ owns the update loop.
Topics covered:
The 3-layer adapter pattern (base → generated adapter → bridge)
Pre-generated class adapter via
daslib/cpp_bindcompileBuiltinModulewith XDD-embedded.das.incfilesRTTI (
StructInfo *) for adapter constructiongetDasClassMethod/das_invoke_functionfor virtual dispatchVirtual dispatch across the C++/daslang boundary
8.4.19.1. Architecture
The pattern uses three layers:
C++ base class (
BaseClass) — pure virtual interface that C++ code programs against.Generated adapter (
TutorialBaseClass) — generated bydaslib/cpp_bind’slog_cpp_class_adapter. For each virtual method, it providesget_<method>(checks if the daslang class overrides the method) andinvoke_<method>(calls it viadas_invoke_function).Dual-inheritance bridge (
BaseClassAdapter) — inherits both the real C++ class and the generated adapter. When C++ callsobj->update(dt)through the vtable, the bridge checks for a daslang override and invokes it.
8.4.19.2. Prerequisites
Tutorial 08 (C++ Integration: Binding Methods) — binding methods.
Tutorial 10 (C++ Integration: Custom Modules) — custom modules.
Understanding of
ManagedStructureAnnotationanddas_invoke_function.
8.4.19.3. Layer 1: C++ base class
A simple abstract interface that C++ code iterates over:
class BaseClass {
public:
virtual ~BaseClass() = default;
virtual void update(float dt) = 0;
virtual float3 getPosition() = 0;
};
C++ maintains a list of shared_ptr<BaseClass> objects and calls
update / getPosition polymorphically.
8.4.19.4. Layer 2: Generated adapter
The adapter is generated by daslib/cpp_bind (see log_cpp_class_adapter).
It lives in a .inc file that you #include in your C++ source:
#include "19_class_adapters_gen.inc"
The generated code provides:
TutorialBaseClass— storesStructInfoand method lookup dataget_update(classPtr)— returns theFuncif the daslang class overridesupdate, ornullptrif it does notinvoke_update(ctx, fn, classPtr, dt)— calls the override viadas_invoke_function<void>::invoke
8.4.19.5. Layer 3: Bridge class
The bridge inherits both BaseClass (for the vtable) and
TutorialBaseClass (for daslang method lookup):
class BaseClassAdapter : public BaseClass,
public TutorialBaseClass {
public:
BaseClassAdapter(char * pClass,
const StructInfo * info,
Context * ctx)
: TutorialBaseClass(info),
classPtr(pClass), context(ctx) {}
void update(float dt) override {
if (auto fn = get_update(classPtr)) {
invoke_update(context, fn, classPtr, dt);
}
}
float3 getPosition() override {
if (auto fn = get_get_position(classPtr)) {
return invoke_get_position(context, fn, classPtr);
}
return float3(0.0f);
}
protected:
void * classPtr;
Context * context;
};
8.4.19.6. Module with XDD-embedded daslang
The abstract base class definition lives in a .das file that is
embedded into C++ using XDD (a CMake macro that converts files to
byte arrays). The module loads it with compileBuiltinModule:
#include "class_adapters_module.das.inc"
class Module_Tutorial19 : public Module {
public:
Module_Tutorial19() : Module("tutorial_19") {
ModuleLibrary lib(this);
lib.addBuiltInModule();
addBuiltinDependency(lib, Module::require("rtti_core"));
addExtern<DAS_BIND_FUN(addObject)>(*this, lib, "add_object",
SideEffects::modifyExternal, "addObject");
compileBuiltinModule(this, "class_adapters_module.das",
class_adapters_module_das,
sizeof(class_adapters_module_das));
}
};
REGISTER_MODULE(Module_Tutorial19);
8.4.19.7. The daslang side
Scripts derive from the exposed abstract class and use def override
to provide implementations:
options gen2
require tutorial_19
require daslib/rtti
class ExampleObject : TutorialBaseClass {
position : float3
velocity : float3
gravity : float = 9.8f
name : string
def override update(dt : float) : void {
print(" [das] {name}.update({dt})\n")
position += velocity * dt
velocity.y -= gravity * dt
}
def override get_position : float3 {
print(" [das] {name}.get_position => {position}\n")
return position
}
}
// Helper: registers the object with C++ via the Context* injected argument.
// The Context* parameter is injected automatically — do not pass it explicitly.
def add_new_object(classPtr) {
add_object(classPtr, class_info(*classPtr))
}
[export]
def test {
print("tick(0.0) = {tick(0.0)}\n\n") // no objects yet
add_new_object(new ExampleObject(name = "A"))
for (t in range(3)) {
print("tick(0.1) = {tick(0.1)}\n\n")
}
add_new_object(new ExampleObject(name = "B", velocity = float3(1.0, 0.0, 0.0)))
for (t in range(3)) {
print("tick(0.1) = {tick(0.1)}\n\n")
}
}
8.4.19.8. Build & run
cmake --build build --config Release --target integration_cpp_19
bin/Release/integration_cpp_19
C++ drives each object’s overridden update / get_position through the
adapter every tick, so the run is interleaved with per-object traces
(abridged):
tick(0.0) = 0,0,0
[das] A.update(0.1)
[das] A.get_position => 0,0,0
tick(0.1) = 0,0,0
...
[das] A.update(0.1)
[das] A.get_position => 0,-0.58800006,0
[das] B.update(0.1)
[das] B.get_position => 0.1,0,0
tick(0.1) = 0.05,-0.29400003,0
See also
Full source:
19_class_adapters.cpp,
19_class_adapters.das,
class_adapters_module.das,
19_class_adapters_gen.inc
Previous tutorial: C++ Integration: Dynamic Scripts
Next tutorial: C++ Integration: Standalone Contexts
Related: C++ Integration: Binding Methods, C++ Integration: Custom Modules