7.3.12. C Integration: Mock ECS

This tutorial demonstrates a pattern for integrating daslang with a native Entity Component System (ECS). The C side owns component data as flat arrays (SOA layout). daslang scripts define “systems” – small update functions annotated with [es] – and a daslang macro module handles all the registration plumbing.

Three files work together:

File

Role

12_ecs.c

C host – mock ECS data, module, game loop

ecs_macro.das

Macro module – [es] annotation, @required globals

12_ecs.das

User script – struct, globals, ES functions

7.3.12.1. The user script

The user defines a component struct whose field names match the C-side array names, declares host-provided globals with @required, and writes [es] functions with no arguments – the macro injects them:

options gen2

require tutorial_c_12
require ecs_macro

var @required dt : float

struct Movement {
    position : float3
    velocity : float3
}

[es]
def update() {
    position += velocity * dt
}

[es]
def apply_gravity() {
    velocity.y -= 9.8 * dt
}

[export]
def test() {
    print("ECS ready\n")
}

The [es] macro transforms def update() into def update(var position : float3&; var velocity : float3&) and generates an [init] function that calls ecs_register with the function pointer and the struct’s TypeInfo.

7.3.12.2. The macro module

The ecs_macro module provides the [es] function annotation:

  1. Find the component struct in the user’s module (one per module, skips generated/lambda/generator structs).

  2. Add struct fields as arguments using qmacro_variable with a ref type – type<$t(fld._type)&>.

  3. Set [export] so the C host can find the function.

  4. Generate an [init] function that calls ecs_register passing the function pointer (via @@), the name, and a TypeInfo pointer obtained with typeinfo rtti_typeinfo.

  5. Register @required globals – scans for the annotation argument on module globals and generates ecs_register_global calls with addr() of the global variable.

[function_macro(name="es")]
class EsMacro : AstFunctionAnnotation {
    def override apply(var func : FunctionPtr; ...) : bool {
        // ... find struct, add fields as var ref args ...
        for (fld in st.fields) {
            func.arguments |> emplace_new <| qmacro_variable(
                string(fld.name), type<$t(fld._type)&>)
        }
        func.flags |= FunctionFlags.exports
        // Generate [init] registration
        var blk <- setup_call_list("register`es`{funcName}", ...)
        blk.list |> emplace_new <| qmacro(
            ecs_register(
                unsafe(reinterpret<void?> @@$c(funcName)),
                $v(funcName),
                typeinfo rtti_typeinfo(type<$t(stType)>)))
        return true
    }
}

7.3.12.3. The C host

Mock ECS data – hardcoded SOA arrays:

#define NUM_ENTITIES 4
static float comp_position[NUM_ENTITIES][3] = { ... };
static float comp_velocity[NUM_ENTITIES][3] = { ... };

Component registry maps names to data arrays:

typedef struct { const char *name; void *data; int elem_size; } ComponentArray;

register_component("position", comp_position, 3 * sizeof(float));
register_component("velocity", comp_velocity, 3 * sizeof(float));

C module (tutorial_c_12) exposes two interop functions:

  • ecs_register(fn : void?; name : string; ti : TypeInfo const) – stores the function pointer and introspects the struct via TypeInfo

  • ecs_register_global(name : string; ptr : void?; ti : TypeInfo const) – stores a pointer to the daslang global variable

Type mangling uses CH<rtti_core::TypeInfo> for a const handled type parameter. The module group must include the rtti_core module (via das_module_find) so the mangled name parser can resolve it.

Game loop – each tick, the C host writes @required globals and calls each registered ES function per entity. Argument names are matched to component arrays once per ES (not per entity):

// Resolve components for each argument (once per ES)
ComponentArray * arg_comp[16];
for (int a = 0; a < argc; a++)
    arg_comp[a] = find_component(das_func_info_get_arg_name(fi, a));

// Call per entity
for (int ent = 0; ent < NUM_ENTITIES; ent++) {
    vec4f args[16];
    for (int a = 0; a < argc; a++) {
        char * base = (char *)arg_comp[a]->data + ent * arg_comp[a]->elem_size;
        args[a] = das_result_ptr(base);
    }
    das_context_eval_with_catch(ctx, fn, args);
}

7.3.12.4. Build & run

Build:

cmake --build build --config Release --target integration_c_12

Run:

bin/Release/integration_c_12

Expected output:

[C] ecs_register: 'update' (struct 'Movement', 2 fields: position:float3 velocity:float3)
[C] ecs_register_global: 'dt' (float, 4 bytes)
[C] ecs_register: 'apply_gravity' (struct 'Movement', 2 fields: position:float3 velocity:float3)
ECS ready

Initial state:
  [0] pos=(0.00, 10.00, 0.00) vel=(1.00, 0.00, 0.00)
  [1] pos=(5.00, 20.00, 0.00) vel=(0.00, 2.00, -1.00)
  [2] pos=(-3.00, 5.00, 2.00) vel=(0.00, 0.00, 0.00)
  [3] pos=(0.00, 0.00, 0.00) vel=(3.00, 1.00, 0.00)

After 3 ticks (dt=0.0167):
  [0] pos=(0.05, 9.99, 0.00) vel=(1.00, -0.49, 0.00)
  [1] pos=(5.00, 20.09, -0.05) vel=(0.00, 1.51, -1.00)
  [2] pos=(-3.00, 4.99, 2.00) vel=(0.00, -0.49, 0.00)
  [3] pos=(0.15, 0.04, 0.00) vel=(3.00, 0.51, 0.00)

See also

Full source: 12_ecs.c, ecs_macro.das, 12_ecs.das

Previous tutorial: tutorial_integration_c_type_introspection

C API reference: embedding_c_api

daScriptC.h API header: include/daScript/daScriptC.h