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 |
|---|---|
|
C host – mock ECS data, module, game loop |
|
Macro module – |
|
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:
Find the component struct in the user’s module (one per module, skips generated/lambda/generator structs).
Add struct fields as arguments using
qmacro_variablewith a ref type –type<$t(fld._type)&>.Set
[export]so the C host can find the function.Generate an
[init]function that callsecs_registerpassing the function pointer (via@@), the name, and aTypeInfopointer obtained withtypeinfo rtti_typeinfo.Register
@requiredglobals – scans for the annotation argument on module globals and generatesecs_register_globalcalls withaddr()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 viaTypeInfoecs_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