Covers Engine 2.0.0
Native C++ Execution
Compile DCode on the engine, verify fixed bindings, and understand backend differences.
Native C++ compiles a .dcode file into a shared library that the engine loads and calls. You still write DCode. The parser, channel authority rules, and CAESAR task scheduling remain part of the engine; native callbacks replace Lua execution for the supported language subset.
Native C++ is one part of a predictable repeated path, not a timing certificate. Deterministic Automation is the canonical statement of what DARTWIC measures and what a deployment must still qualify.
Choose Native C++ for repeated numeric calculations and control tasks with known channel inputs and outputs. Choose Lua when you need dynamic names, tables, module imports, plugin DCode function calls, prompts, or managed waits. Both backends use the same language reference. Backend restrictions appear on the existing entries rather than in a second catalog.
Why combine native DCode and fixed channels?
Use Native C++ DCode with RAPID fixed channels for DARTWIC’s strongest supported repeated-release path. Fixed storage exists first to remove recurring allocation and storage-growth work from a declared control cycle; native compilation then removes Lua interpretation and dynamic script dispatch from supported callback logic. Repeated name lookup is reduced too, but it is not the primary reason fixed channels were designed. A plugin can use the same prepared core path by resolving fixed handles and reusing its buffers during configuration. See what this qualified path proves—and does not.
Fixed channels reserve numeric records and resolve bindings before execution. The engine prepares reusable snapshot, write, and normal publication resources during activation. A periodic release can therefore do a declared amount of framework work: read its fixed inputs, execute its callback, stage declared outputs, and commit them. Native compilation runs supported DCode logic as machine code while preserving RAPID validation, command authority, snapshots, and commit semantics.
Fixed snapshots add data consistency: a task reads its captured inputs and publishes staged outputs together. Keep intermediate calculations in local or state, configure channels and buffers before starting, and keep callback loops and inline calculations short. The driver guide shows this workflow with a plugin consuming commands from native DCode.
Prepared numeric fixed writes hand notifications to a bounded asynchronous delivery path. Slow consumers no longer hold task publication buffers, which prevents consumer delay from retaining those resources. This logical queue capacity is not itself proof that every implementation is allocation-free. Inline calculations still run synchronously and are charged to the writer’s callback; command authority keeps its existing behavior. Queue overflow drops old asynchronous notifications and reports the loss; it does not undo live values or skip inline calculations.
The current core still has blocking locks and allocation outside prepared workloads. The ordinary publication path can allocate when its prepared pool is busy; buffers can grow when writes or origin strings exceed preparation; and deeply nested or unsupported publications can use fallback storage. A strict prepared profile must treat those as explicit resource-exhaustion conditions rather than timing guarantees. Native sleep blocks the callback, and unbounded loops remain unbounded. There is currently no stronger timing policy certifying the whole path. Measure release lateness, complete release-to-completion time, missed releases, inline-dependency time, resource exhaustion, and device feedback for the application you deploy.
Before connecting control hardware, review current failure behavior. Native compilation does not add acquisition freshness checks, downstream inhibition after task failure, or hardware safety interlocks.
Install tools on the engine machine
Compilation happens where the engine process runs, including when you activate a script from a remote Interface. Installing tools only on your Interface computer will not help a remote engine.
| Engine platform | Required for a new native artifact |
|---|---|
| Windows x64 | CMake 3.23 or newer on the engine’s PATH; Visual Studio 2022 or its Build Tools with C++ build tools, a Windows SDK, and C++ Clang tools for Windows / ClangCL. The backend explicitly selects Visual Studio 17 2022, x64, and ClangCL. MSVC alone is insufficient. |
| Linux | CMake 3.23 or newer, Ninja, and a CMake-discoverable C++20 compiler with its standard library and linker. The backend selects Ninja and a Release build. |
The engine also needs a writable project runtime directory and permission to load the resulting library. The generated project uses the C++ standard library and an embedded host ABI; it does not require a separate DARTWIC SDK checkout or vcpkg installation. Building the engine itself is a separate workflow.
Check cmake --version from the environment that launches the engine. On Linux, also check ninja --version and your compiler. Restart the engine after changing its environment. A successful native activation is the definitive check that CMake can find and use the whole toolchain. The current loader expects a Windows DLL or Linux .so; it does not define a macOS artifact path.
Build a small example
Create native_demo.dcode in the active project’s Scripts resource:
task_periodic native_demo 10 normal:
start:
state.ticks = 0
task elapsed_seconds:
state.ticks = state.ticks + 1
|demo_native_ticks| = state.ticks
|demo_native_twice| = state.ticks * 2- Select DCode · Native C++ in the editor’s backend selector.
- Run the script to save, build, and activate it. Changing the selector alone does not replace the loaded runtime.
- Open DARTWIC Builder in the sidebar. Wait for the activation session to succeed and its
VERIFY / READYevent. - Start
native_demofrom Tasks. Its channels should advance at about ten updates per second, withdemo_native_twicetwice the tick count. Stop the task when finished.
Activation registers tasks; starting a task executes it. The editor should show 2 fixed · 0 dynamic for this example’s two static output bindings. These counts describe script references, not every task diagnostic channel in the engine.
The selector persists per file in the project’s scripts/scripts_config.json. For a project managed as files, merge this entry into that file:
{
"native_demo.dcode": {
"execution_mode": "native_cpp",
"run_on_startup": false
}
}An absent execution mode defaults to lua. Enable Run on startup after verifying the script if the engine should activate it during startup; task start configuration remains separate. Follow DARTWIC Builder for startup, reload, and failure diagnostics.
What the native subset supports
| Construct | Native behavior |
|---|---|
| Periodic tasks, state machines, sequences, loops | Compiled callbacks run through CAESAR. Sequence bodies must use supported statements; Lua wait statements are unavailable. Native loop blocks are registered as task runtimes. |
| Channel calculations | Compiled numeric callbacks keep the existing inline calculation path. |
| Events | The engine parses metadata and generates calculation bodies for triggers and handlers. Those bodies must satisfy native restrictions. Static text metadata is allowed. |
| Task groups | The engine configures the group’s ordering; members use their selected execution backends. |
| Timelines | task_timeline is rejected in native files; use Lua. |
| Expressions | Numbers, booleans, static value pipes, comparisons, numeric state, and supported math functions. |
| Control flow | if/elif/else, numeric for, while, do, when, once, transitions, and explicit event triggers. Keep loops bounded. |
| Imports and helpers | Lua modules, function definitions, arbitrary function calls, and callDcodeFunction are unavailable. Templates expand before native generation; the expanded code must be supported. |
Use |pressure| or |pressure|:value to read a static name. Native expressions cannot read metadata such as :timestamp or :units, and native writes cannot assign metadata. Configure those fields through the channel UI or API. A static name and fixed storage are different concepts: see Fixed and dynamic channels.
For math, the math entry lists the implemented native bindings. Use math.pow(a, b) instead of a ^ b, and math.floor(a / b) instead of a // b. math.fmod(a, b) is a floating remainder; it is not a drop-in replacement for Lua % with negative operands.
Write explicit numeric and boolean logic
Native code is not a full Lua interpreter. In particular, and and or become C++ boolean operators rather than returning one of their operands. not uses C++ conversion, where numeric zero is false. Simple numeric conditions in DCode if/while retain truthy-zero behavior through a wrapper. Avoid relying on these mixed implicit conversions:
if |demo_enable| == 1 and |demo_pressure| > 5:
|demo_alarm| = 1
else:
|demo_alarm| = 0Native state fields and persistent runtime-root locals are doubles initialized to zero and reset on task start. Use state.ready == 1, and initialize state explicitly in start:. Runtime strings, tables, indexing, concatenation, and Lua nil semantics are unavailable. An uninitialized local becomes numeric zero; prefer an explicit initializer.
Local expression types follow C++ inference: 5 / 2 produces 2, while 5.0 / 2.0 produces 2.5. Use decimal literals when floating division is intended. Channel reads and persistent state are numeric doubles, but an expression made entirely from integer literals can still use integer arithmetic. In calculations, return a number on every path; return nil becomes zero in native code.
print accepts a single string literal or numeric expression. Native sleep directly blocks the callback thread; it does not use Lua’s transaction-releasing managed wait. It can therefore delay task shutdown and Builder configuration as well as the next iteration. Use Lua for workflows that need waits or operator input, and keep inline calculation/event callbacks free of sleeps and other unbounded work.
Verify the loaded runtime
Check three things separately:
| Check | Evidence |
|---|---|
| Compiled and activated | Successful activation job and Builder VERIFY / READY; the loaded script has execution_mode: "native_cpp" and matches the saved source. |
| Bound as intended | Editor fixed/dynamic chip and Builder binding details. A successful build may still contain dynamic inputs. |
| Running as intended | Start the task, check output values, actual frequency, execution time, and missed-cycle diagnostics. Test input-to-output behavior under realistic load. |
The engine caches artifacts under engine/dcode/<project>/native_dcode/<artifact-key>/. Each entry contains generated source, a CMake build directory, and build.log. Windows loads build/Release/dartwic_native_dcode.dll; Linux loads build/libdartwic_native_dcode.so. The key includes the source/dependency hash, native backend cache version, and host build stamp. This cache stays outside the Git-tracked project. A matching binary can be reused without invoking the compiler; a cache hit alone does not verify that the machine can compile a new script.
For API access, use get-script-build-report with {"filePath":"native_demo.dcode"}. Its payload contains the report directly: status, cache_hit, artifact_path, log_path, and log. status: "loaded" means the library loaded; check the completed activation and loaded-script metadata too, because binding and activation happen afterward.
Native compilation removes Lua interpretation from these callbacks. Fixed bindings reduce channel lookup work and make task inputs coherent. Neither establishes a hardware deadline: operating-system scheduling, locks, driver behavior, and external I/O still matter. Execution and Timing explains those boundaries.
Native and Lua inline calculations are both valid. When a native task reaches either one through a committed channel write, its mutex wait and execution are part of that task’s completion interval. Inspect channel_calculations in dartwic/get-registered-tasks for the calculation mode, active state, invocation count, and recent/max duration; do not assume compilation made a transitive dependency chain bounded.
Read the execution-path review
After a Native C++ script is active, the DCode editor and DARTWIC Builder show the same execution-path review. It follows each fixed native output through active dependent channel calculations and reports:
- a native binding that is currently dynamic;
- a fixed output that synchronously reaches a Lua calculation, including transitive calculation hops; and
- a reachable calculation containing a
for,while, orrepeatconstruct.
These are review signals, not prohibitions or a deadline certificate. A bounded loop and a Lua calculation can be the correct implementation. Calculation timing remains available as the largest observed invocation so far and includes the calculation mutex wait, but DARTWIC does not choose a cost threshold for the application. Compare it with the actual calling task period and test under representative load. The review only sees active DCode calculations and static native bindings, so drivers, direct native allocation, opaque plugin work, and runtime-dependent branches still require their own evidence.