5.6. Advanced Topics

This page covers advanced embedding techniques: ahead-of-time compilation, class adapters, coroutines, dynamic script generation, and standalone contexts.

5.6.1. AOT compilation

daslang can compile scripts ahead-of-time into C++ source files that, when built alongside the host, replace the interpreter with direct native calls. This provides significant performance improvements for hot paths.

5.6.1.1. AOT pipeline overview

  1. Compile the .das file as normal.

  2. Run the AOT tool: daslang -aot script.das output.cpp

  3. Build the generated .cpp alongside your application and link libDaScript and libDaScript_runtime (the full hand-wired link set: C API Reference, section Linking).

  4. At runtime, simulate() detects AOT-compiled functions and uses them instead of the interpreter.

See C++ Integration: AOT Compilation for a step-by-step walkthrough.

5.6.1.2. AOT annotations

[no_aot]

Prevents AOT compilation for a specific function.

[hybrid]

AOT function uses full ABI — slower calls, but no semantic hash dependency. Required when the module may be compiled separately from the script.

5.6.1.3. AOT template functions

By default, AOT-generated functions expect blocks to be passed as the C++ TBlock class (see Block). If the block is small enough, AOT can replace it with a direct call. setAotTemplate enables this:

addExtern<DAS_BIND_FUN(my_func)>(*this, lib, "my_func",
    SideEffects::invoke, "my_func")
        ->setAotTemplate();

This generates inlined block evaluation instead of das_invoke.

5.6.1.4. AOT type customization

Handled types can customize their AOT generation through TypeAnnotation virtual methods:

aotPreVisitGetField(writer, fieldName)

Emits code before field access (e.g., dereference operator).

aotVisitGetField(writer, fieldName)

Emits the field access itself.

aotPreVisitGetFieldPtr(writer, fieldName)

Same as above, for pointer field access.

aotVisitGetFieldPtr(writer, fieldName)

Emits pointer field access.

Index operations are covered by das_index<T, IDX> and das_iterator<T> C++ templates. Override these templates to provide AOT-compatible indexing and iteration for custom types.

das_index<T, IDX>

Used for ExprAt (array[index]) and ExprSafeAt (array?[index]). Must provide at(value, index, context) and at(value, index, count, context) methods.

das_iterator<T>

Used for for loop iteration. Must provide main_loop and associated helper methods.

5.6.2. Class adapters

Class adapters bridge C++ virtual method dispatch to daslang class method implementations. This allows daslang scripts to define classes that a C++ host can polymorphically call through base class pointers.

5.6.2.1. Pattern overview

  1. Define a C++ abstract base class with virtual methods.

  2. Generate a “tutorial adapter” class that maps each virtual method to a das_invoke_function call.

  3. Create a dual-inheritance adapter: struct Adapter : BaseClass, TutorialAdapter.

  4. In the daslang script, derive a class from the adapter type and override methods.

  5. The C++ host calls virtual methods on base class pointers — dispatch reaches daslang automatically.

Key C++ APIs:

  • das_invoke_function<Ret>::invoke(ctx, at, fn, args...) — calls a daslang function pointer from C++

  • getDasClassMethod(classPtr, offset) — retrieves the daslang function pointer for a virtual method

  • compileBuiltinModule — compiles an embedded .das module as a built-in

  • class_info(*classPtr) — gets StructInfo for RTTI bridging

The adapter class is typically generated by log_cpp_class_adapter from daslib/cpp_bind, which produces the boilerplate get_<method> / invoke_<method> static helpers.

See C++ Integration: Class Adapters for a complete, compilable example.

5.6.3. Coroutines (generators)

daslang generators (generator<T>) can be consumed from C++ through the Sequence type. This enables coroutine-style patterns where the host drives execution step by step.

5.6.3.1. Consuming a generator

  1. Call the generator-returning function with evalWithCatch, passing a Sequence * as the third argument:

    Sequence it;
    ctx.evalWithCatch(fnTest, nullptr, &it);
    
  2. Step through values with builtin_iterator_iterate:

    int32_t value = 0;
    while (builtin_iterator_iterate(it, &value, &ctx)) {
        // process value
    }
    
  3. Clean up with builtin_iterator_close:

    builtin_iterator_close(it, &value, &ctx);
    

Even if the generator has finished, always call close to release resources. If you break early, close is essential.

See C++ Integration: Coroutines.

5.6.4. Dynamic script generation

Scripts can be constructed as strings and compiled without writing to disk, using virtual files:

TextWriter ss;
ss << "options gen2\n";
ss << "var x = 0.0f\n";
ss << "var y = 0.0f\n";
ss << "[export] def eval() : float { return x*x + y*y; }\n";

auto fAccess = make_smart<FsFileAccess>();
auto fileInfo = make_unique<TextFileInfo>(
    ss.str().c_str(), uint32_t(ss.str().length()), false);
fAccess->setFileInfo("virtual.das", das::move(fileInfo));

auto program = compileDaScript("virtual.das", fAccess, tout,
                                dummyLibGroup);

After simulation, global variables can be located and written to directly through context memory:

int idx = ctx->findVariable("x");
float * xPtr = (float *)ctx->getVariable(idx);
*xPtr = 3.0f;   // set x = 3.0

This pattern is useful for expression evaluators, configuration systems, and dynamic code generation.

See C++ Integration: Dynamic Scripts.

5.6.4.1. Context variables

ctx.findVariable(name)

Returns the index of a global variable, or -1 if not found.

ctx.getVariable(index)

Returns a raw pointer into the context’s global data segment.

ctx.getTotalVariables()

Returns the number of global variables.

ctx.getVariableName(index)

Returns the variable’s name.

ctx.getVariableSize(index)

Returns the variable’s size in bytes.

See C++ Integration: Context Variables.

5.6.5. Standalone contexts

A standalone context pre-compiles a daslang program into C++ source files (.das.h and .das.cpp) that embed the entire program state — function tables, variable layouts, and AOT implementations — without requiring runtime compilation.

Benefits:

  • Zero compilation overhead at runtime — no parsing, no type inference, no simulation.

  • Direct native calls — exported functions become C++ methods on a Standalone class.

  • Deterministic startup — all code is linked at build time.

Pipeline:

  1. Compile-time: daslang utils/aot/main.das -- -ctx script.das output_dir/

  2. This generates script.das.h and script.das.cpp.

  3. Build both alongside your host application.

  4. At runtime, create the Standalone context and call functions directly.

#include "standalone_ctx_generated/script.das.h"

das::ctx_standalone::Standalone ctx;
ctx.test();  // direct call, no findFunction needed

The same generated files also carry a C API: one extern "C" entry point per export, wrapping the C++ method beside it, plus an opaque handle and <prefix>_create / _destroy / _last_error. So one generated pair serves a C++ host and a C host at once, and the two surfaces are not identical — C++ reaches every export in native types, while C carries the subset C can express (the emitter names the ones it skips). The .das.cpp embeds the header’s text rather than including it, so moving or editing your copy of the header cannot break the library’s own translation unit.

See C++ Integration: Standalone Contexts for a complete example.

5.6.6. C libraries

daslang -lib compiles a daslang program to a native library with a C API, so a host that only calls one script needs no daslang headers, no Module, and no compiler. The LLVM backend emits the library; a standalone context (above) emits C++ source your build compiles instead.

Pipeline:

  1. daslang -lib script.das -output build/script

  2. This writes build/script.so (.dylib / .dll) and build/script.h. Add -- --jit-lib-static for a .a / .lib archive instead; the header records what a host then links.

  3. Include the header, link the library, call it.

Which functions cross the boundary is your choice: [export_c] marks them one at a time, and -lib-export-all offers every public function of the entry module whose signature C can spell, naming the ones it skips.

Every library gets the same four entry points, prefixed with the output name — one instance owns one daslang context, with its own globals and heap:

#include "script.h"

script_ctx * ctx = script_create();
if ( !ctx ) {
    printf("%s\n", script_last_error(NULL));
    return 1;
}
script_Vec3 v = { 1.0f, 2.0f, 3.0f };
script_Vec3 out;
script_scale(ctx, &v, 2.0f, &out);   /* a struct result uses a trailing out pointer */
script_destroy(ctx);

A daslang panic never unwinds into C: the call returns zero and leaves out untouched, and script_last_error(ctx) reports the text until the next call on that instance clears it. A const char * a function returns lives in that instance’s string heap, so copy it if you need it past the next call.

The generated header asserts the layout of every structure it declares (_Static_assert in C11, static_assert in C++), so a host built for a different target fails to compile rather than misreading memory.

A daslang host needs no C: -- --jit-lib-bindings build/script_c.das writes the daslang twin of the header beside it, one [extern(cdecl, late, ...)] per entry point plus a das struct per structure that crosses, and the host requires that file. The externs bind late, so the bindings compile before the library exists and a build system can generate them as an ordinary output.

Several such libraries coexist in one process, and so does a library inside a host that registered the daslang modules itself: whoever gets there first registers the runtime, and the rest bind to it. Several instances of one library are fine, on any thread.

5.6.7. Serialization

Compiled programs can be serialized to a binary blob, skipping compilation entirely on subsequent loads:

// Save
AstSerializer ser;
program->serialize(ser);
// ser.buffer contains the binary data

// Load
AstSerializer deser;
deser.buffer = saved_data;
auto restored = Program::deserialize(deser);
// simulate and run as normal

This is faster than recompilation (skips parsing and type inference) but slower than standalone contexts (still requires simulation).

See C++ Integration: Serialization and C Integration: Serialization.

5.6.8. Sandboxing

CodeOfPolicies controls what scripts are allowed to do:

  • no_unsafe = true — forbid unsafe blocks

  • no_global_variables = true — forbid module-level variables

  • no_global_heap = true — forbid heap allocations for globals

  • no_init = true — forbid [init] functions

  • threadlock_context = true — enable context mutex

FsFileAccess can be locked to restrict file access:

  1. Pre-introduce allowed files with das_fileaccess_introduce_file.

  2. Lock with das_fileaccess_lock to prevent filesystem reads.

See C++ Integration: Sandbox and C Integration: Sandbox.

5.6.9. File access customization

Override FsFileAccess methods for custom file resolution:

getNewFileInfo(fname)

Resolves file paths and returns file content.

getModuleInfo(req, from)

Resolves require statements to file paths.

setFileInfo(name, info)

Registers a virtual file from a string — used for dynamic script generation and embedded modules.

For project-level file resolution, implement module_get as a function macro (.das-side):

options gen2
require daslib/ast_boost public

[function_macro(name="module_get")]
class ModuleGetMacro : AstModuleGetMacro {
    def override getModule(req, from : string) : ModuleInfo {
        // resolve require paths here
    }
}

See C++ Integration: Custom Modules.