8.4.4. C++ Integration: Binding Types
This tutorial shows how to expose C++ structs to daslang so that scripts can create instances, access fields, and pass them to/from C++ functions. Topics covered:
MAKE_TYPE_FACTORY— registering a C++ type with the daslang type systemManagedStructureAnnotation— describing struct fieldsaddAnnotation— plugging type metadata into a moduleSimNode_ExtFuncCallAndCopyOrMove— returning bound types by valueFactory functions — creating instances without
unsafe
8.4.4.1. Prerequisites
Tutorial 03 completed (C++ Integration: Binding Functions).
Familiarity with
addExternandSideEffects.
8.4.4.2. Defining the C++ types
We define three simple structs. They are intentionally POD (no default member initializers, no virtual functions) so that the daslang type system sees them as plain data:
struct Vec2 {
float x;
float y;
};
struct Color {
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t a;
};
struct Rect {
Vec2 pos;
Vec2 size;
};
8.4.4.3. MAKE_TYPE_FACTORY
Before daslang can work with a C++ type, you must declare a
type factory at file scope. MAKE_TYPE_FACTORY creates two things:
typeFactory<CppType> (so addExtern resolves the type in function
signatures) and typeName<CppType> (the type’s display name):
MAKE_TYPE_FACTORY(Vec2, Vec2);
MAKE_TYPE_FACTORY(Color, Color);
MAKE_TYPE_FACTORY(Rect, Rect);
The first argument is the daslang-visible name; the second is the C++
type. They can differ when the C++ name lives in a namespace — e.g.
MAKE_TYPE_FACTORY(Vec2, math::Vec2).
8.4.4.4. ManagedStructureAnnotation
An annotation describes a type’s layout to daslang. For structs,
derive from ManagedStructureAnnotation<T> and call addField for
each member:
struct Vec2Annotation : ManagedStructureAnnotation<Vec2, false> {
Vec2Annotation(ModuleLibrary & ml)
: ManagedStructureAnnotation("Vec2", ml)
{
addField<DAS_BIND_MANAGED_FIELD(x)>("x", "x");
addField<DAS_BIND_MANAGED_FIELD(y)>("y", "y");
}
};
The template parameters are <CppType, canNew, canDelete>. Passing
false for canNew prevents scripts from calling new Vec2()
directly (we provide factory functions instead).
DAS_BIND_MANAGED_FIELD(member) resolves the offset and type of a
struct member at compile time. The two string arguments are the daslang
field name and the C++ field name (used for AOT).
Nested bound types work naturally — Rect has Vec2 fields:
struct RectAnnotation : ManagedStructureAnnotation<Rect, false> {
RectAnnotation(ModuleLibrary & ml)
: ManagedStructureAnnotation("Rect", ml)
{
addField<DAS_BIND_MANAGED_FIELD(pos)>("pos", "pos");
addField<DAS_BIND_MANAGED_FIELD(size)>("size", "size");
}
};
8.4.4.5. Registering annotations in the module
Call addAnnotation in the module constructor. Order matters —
if type B contains type A as a field, register A first:
addAnnotation(new Vec2Annotation(lib)); // Vec2 first
addAnnotation(new ColorAnnotation(lib));
addAnnotation(new RectAnnotation(lib)); // Rect uses Vec2
8.4.4.6. Returning bound types by value
When a C++ function returns a bound struct by value, addExtern needs
the SimNode_ExtFuncCallAndCopyOrMove sim-node so that the return
value is properly copied into daslang’s stack:
Vec2 vec2_add(const Vec2 & a, const Vec2 & b) {
return { a.x + b.x, a.y + b.y };
}
addExtern<DAS_BIND_FUN(vec2_add), SimNode_ExtFuncCallAndCopyOrMove>(
*this, lib, "vec2_add",
SideEffects::none, "vec2_add")
->args({"a", "b"});
Functions that return scalars (float, bool, etc.) or take bound
types by const & do not need this — the default sim-node works:
float vec2_length(const Vec2 & v) {
return sqrtf(v.x * v.x + v.y * v.y);
}
addExtern<DAS_BIND_FUN(vec2_length)>(*this, lib, "vec2_length",
SideEffects::none, "vec2_length")
->args({"v"});
8.4.4.7. Factory functions
Types bound via ManagedStructureAnnotation are handled (reference)
types. Creating a mutable local variable of such a type requires an
unsafe block. To give scripts a safe and ergonomic API, provide
factory functions that return the type by value:
Vec2 make_vec2(float x, float y) {
Vec2 v; v.x = x; v.y = y;
return v;
}
addExtern<DAS_BIND_FUN(make_vec2), SimNode_ExtFuncCallAndCopyOrMove>(
*this, lib, "make_vec2",
SideEffects::none, "make_vec2")
->args({"x", "y"});
8.4.4.8. Using bound types in daslang
require tutorial_04_cpp
[export]
def test() {
// Immutable locals — no `unsafe` needed
let a = make_vec2(3.0, 4.0)
print("length(a) = {vec2_length(a)}\n")
let c = vec2_add(a, make_vec2(1.0, 2.0))
print("a + b = ({c.x}, {c.y})\n")
// Mutable local requires `unsafe`
unsafe {
var d = make_vec2(3.0, 4.0)
vec2_normalize(d)
print("normalize = ({d.x}, {d.y})\n")
}
// Nested types and field access
let r = make_rect(10.0, 20.0, 100.0, 50.0)
print("rect area = {rect_area(r)}\n")
Immutable locals created via let from factory functions work without
unsafe. Use unsafe { var ... } only when the variable must be
mutated (e.g. passed to a modifyArgument function).
8.4.4.9. Building and running
cmake --build build --config Release --target integration_cpp_04
bin\Release\integration_cpp_04.exe
Expected output:
a = (3, 4)
length(a) = 5
a + b = (4, 6)
a * 2 = (6, 8)
dot(a, b) = 11
normalize(3,4) = (0.6, 0.8)
color = (255, 128, 0, 255)
rect area = 5000
contains(50,30) = true
contains(200,200) = false
See also
Full source:
04_binding_types.cpp,
04_binding_types.das
Previous tutorial: C++ Integration: Binding Functions
Next tutorial: C++ Integration: Binding Enumerations