5.3.21. C++ Integration: Threading
This tutorial demonstrates how to use daslang contexts and compilation pipelines across multiple threads in a C++ host application.
Topics covered:
Part A — Running a compiled context on a worker thread
Part B — Compiling a script from scratch on a worker thread
daScriptEnvironment::getBound()/setBound()— thread-local environment bindingReuseCacheGuard— thread-local free-list cache managementContextCategory::thread_clone— context clone category for threadsshared_ptr<Context>withsharedPtrContext = trueCodeOfPolicies::threadlock_context— context mutex for threadsPer-thread
Module::Initialize()/Module::Shutdown()
5.3.21.1. Prerequisites
Tutorial 1 completed (tutorial_integration_cpp_hello_world) — basic compile → simulate → eval cycle.
Understanding of
ContextanddaScriptEnvironmentfrom tutorial_integration_cpp_standalone_contexts.
5.3.21.2. Why threading matters
daslang uses thread-local storage (TLS) for its environment object
(daScriptEnvironment). This object holds the module registry,
compilation state, and various global flags. Every thread that touches
daslang API must have a valid environment bound.
There are two common patterns:
Share the environment — the worker thread binds the same environment object that the main thread uses. This works when the worker only executes (not compiles) and the main thread is idle.
Independent environment — the worker thread creates its own module registry and compilation pipeline from scratch. This is fully isolated and safe for concurrent compilation.
5.3.21.3. The daslang script
A simple script that computes the sum 0 + 1 + … + 99 = 4950:
options gen2
[export]
def compute() : int {
var total = 0
for (i in range(100)) {
total += i
}
return total
}
5.3.21.4. Part A — Run on a worker thread
Compile and simulate on the main thread, then clone the context and
run it on a std::thread.
// 1. Compile & simulate on main thread (standard boilerplate).
CodeOfPolicies policies;
policies.threadlock_context = true; // context will run on another thread
auto program = compileDaScript(getDasRoot() + SCRIPT_NAME,
fAccess, tout, dummyLibGroup, policies);
Context ctx(program->getContextStackSize());
program->simulate(ctx, tout);
auto fnCompute = ctx.findFunction("compute");
// 2. Clone the context for the worker thread.
shared_ptr<Context> threadCtx;
threadCtx.reset(new Context(ctx, uint32_t(ContextCategory::thread_clone)));
threadCtx->sharedPtrContext = true;
auto fnComputeClone = threadCtx->findFunction("compute");
// 3. Capture the current environment.
auto bound = daScriptEnvironment::getBound();
// 4. Launch the worker thread.
int32_t result = 0;
std::thread worker([&result, threadCtx, fnComputeClone, bound]() mutable {
daScriptEnvironment::setBound(bound);
vec4f res = threadCtx->evalWithCatch(fnComputeClone, nullptr);
result = cast<int32_t>::to(res);
});
worker.join();
Key points:
Concept |
Explanation |
|---|---|
|
Returns a pointer to the current thread’s environment (TLS). Must be captured before launching the worker. |
|
Binds the environment on the worker thread. Without this, any daslang API call will crash. |
|
Marks the context as a thread-owned clone. The runtime can use this flag for diagnostics and thread-safety checks. |
|
Ensures the clone’s lifetime extends until the worker is done.
Set |
|
Compile-time policy that gives the context a mutex. Required when the context (or its clone) will be accessed from a non-main thread. |
|
Each context has its own function table. Always look up functions on the context that will execute them. |
5.3.21.5. Part B — Compile on a worker thread
Create a fully independent daslang environment on a new thread.
This is the pattern used in test_threads.cpp for concurrent
compilation benchmarks.
std::thread worker([&result]() {
// 1. Thread-local free-list cache.
ReuseCacheGuard guard;
// 2. Register module factories.
NEED_ALL_DEFAULT_MODULES;
// 3. Initialize modules — creates a fresh environment in TLS.
Module::Initialize();
// 4. Standard compile → simulate → eval cycle.
TextPrinter tout;
ModuleGroup dummyLibGroup;
CodeOfPolicies policies;
policies.threadlock_context = true; // context mutex for thread safety
auto fAccess = make_smart<FsFileAccess>();
auto program = compileDaScript(getDasRoot() + SCRIPT_NAME,
fAccess, tout, dummyLibGroup, policies);
Context ctx(program->getContextStackSize());
program->simulate(ctx, tout);
auto fn = ctx.findFunction("compute");
vec4f res = ctx.evalWithCatch(fn, nullptr);
result = cast<int32_t>::to(res);
program.reset();
// 5. Shut down this thread's modules.
Module::Shutdown();
});
worker.join();
Key points:
Concept |
Explanation |
|---|---|
|
Initializes and tears down per-thread free-list caches. Must be created first on any thread that uses daslang. |
|
Registers module factory functions (not instances). This
is a macro that must run before |
|
Creates a new |
|
Destroys this thread’s modules and environment. Must be called before the thread exits. |
|
Same policy as Part A — ensures the context has a mutex even when compiled on the worker thread. |
|
Release the program before |
5.3.21.6. Choosing the right pattern
Criteria |
Part A (clone + run) |
Part B (compile on thread) |
|---|---|---|
Compilation cost |
Zero on worker (pre-compiled) |
Full compilation on worker |
Isolation |
Shares environment with main |
Fully independent |
Concurrent compilation |
Not safe (shared env) |
Safe (separate env per thread) |
Use case |
Game threads running pre-compiled AI/logic |
Build servers, parallel test runners |
5.3.21.7. Build & run
cmake --build build --config Release --target integration_cpp_21
bin/Release/integration_cpp_21
Expected output:
=== Part A: Run on a worker thread ===
compute() on worker thread returned: 4950
PASS
=== Part B: Compile on a worker thread ===
compute() compiled + run on worker thread returned: 4950
PASS
See also
Full source:
21_threading.cpp,
21_threading.das
Previous tutorial: tutorial_integration_cpp_standalone_contexts
Related: tutorial_integration_cpp_hello_world (basic compile/simulate/eval)