5.3.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_bind

  • compileBuiltinModule with XDD-embedded .das.inc files

  • RTTI (StructInfo *) for adapter construction

  • getDasClassMethod / das_invoke_function for virtual dispatch

  • Virtual dispatch across the C++/daslang boundary

5.3.19.1. Architecture

The pattern uses three layers:

  1. C++ base class (BaseClass) — pure virtual interface that C++ code programs against.

  2. Generated adapter (TutorialBaseClass) — generated by daslib/cpp_bind’s log_cpp_class_adapter. For each virtual method, it provides get_<method> (checks if the daslang class overrides the method) and invoke_<method> (calls it via das_invoke_function).

  3. Dual-inheritance bridge (BaseClassAdapter) — inherits both the real C++ class and the generated adapter. When C++ calls obj->update(dt) through the vtable, the bridge checks for a daslang override and invokes it.

5.3.19.2. Prerequisites

  • Tutorial 08 (tutorial_integration_cpp_methods) — binding methods.

  • Tutorial 10 (tutorial_integration_cpp_custom_modules) — custom modules.

  • Understanding of ManagedStructureAnnotation and das_invoke_function.

5.3.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.

5.3.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 — stores StructInfo and method lookup data

  • get_update(classPtr) — returns the Func if the daslang class overrides update, or nullptr if it does not

  • invoke_update(ctx, fn, classPtr, dt) — calls the override via das_invoke_function<void>::invoke

5.3.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;
};

5.3.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"));

        addExtern<DAS_BIND_FUN(addObject)>(*this, lib, "add_object",
            SideEffects::modifyExternal, "addObject");

        compileBuiltinModule("class_adapters_module.das",
            class_adapters_module_das,
            sizeof(class_adapters_module_das));
    }
};

REGISTER_MODULE(Module_Tutorial19);

5.3.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 rtti

class ExampleObject : TutorialBaseClass {
    position : float3
    speed : float

    def override update(dt : float) : void {
        position.x += speed * dt
    }

    def override get_position() : float3 {
        return position
    }
}

[export]
def test {
    var obj = new ExampleObject()
    obj.position = float3(0.0, 0.0, 0.0)
    obj.speed = 10.0

    unsafe {
        add_object(addr(*obj), class_info(*obj), this_context())
    }

    let avg = tick(0.5)
    print("After tick(0.5): avg position = ({avg.x}, {avg.y}, {avg.z})\n")
}

5.3.19.8. Build & run

cmake --build build --config Release --target integration_cpp_19
bin/Release/integration_cpp_19

Expected output:

After tick(0.5): avg position = (5, 0, 0)

See also

Full source: 19_class_adapters.cpp, 19_class_adapters.das, class_adapters_module.das, 19_class_adapters_gen.inc

Previous tutorial: tutorial_integration_cpp_dynamic_scripts

Next tutorial: tutorial_integration_cpp_standalone_contexts

Related: tutorial_integration_cpp_methods, tutorial_integration_cpp_custom_modules