C / Embedded Target Guide
Complete guide for configuring the C target across platforms: Arduino, Desktop (Windows/Linux/macOS), and RTOS.
Table of Contents
- Overview
- Quick Start
- Platform Support
- Datamodel Support
- Understanding the Executor System
- ContinuousExecutor
- Invoke Children with --bundle
- Tracing
- Configuration Reference
- Memory Optimization
- MISRA C:2012 Support
- Platform-Specific Details
- Troubleshooting
- Complete Example
- API Reference
Overview
The C target generates high-performance, portable C11 code from SCXML state machines.
Key Features
| Feature | Description |
|---|---|
| Zero Overhead | Direct function calls, O(1) state lookups |
| Zero Allocation | Can run without malloc (static memory only) |
| Embedded Ready | No OS dependencies (bare-metal support) |
| Thread Safe | Optional mutex protection via SCXML_THREAD_SAFE |
| W3C Compliant | 100% ECMAScript datamodel (188/188 tests) |
Supported Platforms
| Platform | Timer System | Polling Required | Use Case |
|---|---|---|---|
| Arduino/Bare-Metal | Software countdown | Yes (tick()) |
Microcontrollers |
| Windows | Timer Queue API | No | Windows applications |
| Linux/macOS | timer_create |
No | Unix applications |
| RTOS | Native OS timers | Configurable | FreeRTOS, Zephyr |
Quick Start
1. Generate Code
# Standard generation
scxml-gen my_machine.scxml -t c -o my_machine.c
# Amalgamated (single-header runtime for easy integration)
scxml-gen my_machine.scxml -t c --amalgamate -o my_machine.c
This generates:
my_machine.c- State machine implementationmy_machine.h- Public API headerscxml_transpiled.h- Runtime (if amalgamated)
2. Basic Usage
#include "my_machine.h"
int main() {
// 1. Allocate (stack or static)
MyMachine sm;
// 2. Initialize
MyMachine_init(&sm);
// 3. Start (enters initial state)
MyMachine_start(&sm);
// 4. Send events (using generated constants)
MyMachine_send(&sm, EVT_START, NULL);
// 5. Check state
if (MyMachine_is_in(&sm, S_RUNNING)) {
printf("Running!\n");
}
// 6. Cleanup
MyMachine_destroy(&sm);
return 0;
}
3. Compile
The build commands depend on which datamodel you're using:
Native-C / Null Datamodel (Simple)
For native-c or null datamodels, compilation is straightforward:
# Linux/macOS
gcc -DSCXML_PLATFORM_POSIX my_machine.c main.c -o app -lpthread -lrt
# Windows (MSVC)
cl /DSCXML_PLATFORM_WINDOWS my_machine.c main.c
# Bare Metal (no OS)
arm-none-eabi-gcc -DSCXML_PLATFORM_BARE_METAL my_machine.c main.c -o app.elf
ECMAScript Datamodel (Requires JerryScript)
For ecmascript datamodel, you must link against JerryScript:
# Linux/macOS with JerryScript
gcc -DSCXML_PLATFORM_POSIX -DSCXML_USE_JERRYSCRIPT \
-I/path/to/jerry/include \
my_machine.c main.c \
scxml/src/datamodel_ecmascript.c \
scxml/src/scxml_xml_dom.c \
-L/path/to/jerry/lib -ljerry-core -ljerry-port \
-o app -lpthread -lrt
# Windows (MSVC) with JerryScript
cl /DSCXML_PLATFORM_WINDOWS /DSCXML_USE_JERRYSCRIPT ^
/I"C:\path\to\jerry\include" ^
my_machine.c main.c ^
scxml\src\datamodel_ecmascript.c ^
scxml\src\scxml_xml_dom.c ^
/link /LIBPATH:"C:\path\to\jerry\lib" jerry-core.lib jerry-port.lib
Important: If SCXML_USE_JERRYSCRIPT is not defined or JerryScript is unavailable, the generated code will silently fall back to the null datamodel. ECMAScript expressions will not be evaluated - only the In() predicate will work.
Using CMake (Recommended for ECMAScript)
The generated CMakeLists.txt can automatically fetch and build JerryScript:
# Generate with CMake support
scxml-gen my_machine.scxml -t c -o my_machine.c
# Build with JerryScript enabled
cd output_directory
mkdir build && cd build
cmake -DSCXML_USE_JERRYSCRIPT=ON ..
cmake --build .
The CMake configuration will:
- Fetch JerryScript v3.0.0 via FetchContent
- Configure appropriate heap size (default 512KB, configurable)
- Link all required libraries automatically
Platform Support
Define the appropriate macro during compilation:
| Macro | Platform | Timer Mechanism | Threading |
|---|---|---|---|
SCXML_PLATFORM_POSIX |
Linux, macOS, BSD | timer_create |
pthreads |
SCXML_PLATFORM_WINDOWS |
Windows | Timer Queue API | Win32 |
SCXML_PLATFORM_BARE_METAL |
Arduino, ARM Cortex | Polling (tick()) |
None |
Datamodel Support
The C target supports three datamodels with different capabilities and requirements:
| Datamodel | Description | Build Requirements | Best For |
|---|---|---|---|
native-c |
Direct C expressions and variables | None (header-only) | Embedded, type-safe |
null |
No data storage, In() only |
None (header-only) | Minimal footprint |
ecmascript |
Full W3C JavaScript expressions | JerryScript library | W3C compliance |
Native-C Datamodel (Recommended for Embedded)
Type-safe C variables with compile-time checking:
<scxml datamodel="native-c" initial="idle">
<datamodel>
<data id="counter" type="int" expr="0"/>
<data id="threshold" type="int" expr="100"/>
</datamodel>
<state id="idle">
<transition cond="counter >= threshold" target="done"/>
</state>
</scxml>
Build: No additional libraries required. Works with simple gcc/cl commands.
Null Datamodel
Minimal overhead - only state-based In() predicate supported:
<scxml datamodel="null" initial="off">
<state id="off">
<transition event="toggle" target="on"/>
</state>
<state id="on">
<transition event="toggle" target="off"/>
<transition cond="In('on')" event="check" target="on"/>
</state>
</scxml>
Build: No additional libraries required.
ECMAScript Datamodel
Full W3C compliance with JavaScript expressions (100% ECMAScript tests pass):
<scxml datamodel="ecmascript" initial="start">
<datamodel>
<data id="count" expr="0"/>
<data id="items" expr="[]"/>
</datamodel>
<state id="start">
<onentry>
<script>count++; items.push({id: count});</script>
</onentry>
<transition cond="count > 5" target="done"/>
</state>
</scxml>
Build: Requires JerryScript library. See Compile section for build commands.
Fallback behavior: If JerryScript is not linked (SCXML_USE_JERRYSCRIPT not defined), ECMAScript datamodel silently falls back to null datamodel behavior. Conditions and scripts will not execute - only In() predicates will work.
Understanding the Executor System
What is an Executor?
The Executor is a platform abstraction layer for timer scheduling. When your SCXML uses <send delay="3s"/>:
- Generated code calls
scxml_executor_schedule(3000ms, callback) - Executor manages the timer using platform-appropriate mechanism
- When timer expires, executor calls the callback
- Callback calls
_send()which processes the event immediately
┌─────────────────────────────────────────────────────────────────┐
│ SCXML: <send delay="3s"/> │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ scxml_executor_schedule() │
└─────────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Bare-Metal │ │ Windows │ │ POSIX/Linux │
│ tick() poll │ │ Timer Queue │ │ timer_create │
│ │ │ │ │ │
│ requires_tick:│ │ requires_tick: │ │ requires_tick: │
│ TRUE │ │ FALSE │ │ FALSE │
└───────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└─────────────────────┴─────────────────────┘
│
▼
┌───────────────────────────────┐
│ Timer callback fires │
│ → _send() called │
│ → Event processed immediately│
└───────────────────────────────┘
Timer Lifecycle
1. State enters with <send delay="3s" id="myTimer"/>
→ scxml_executor_schedule(3000, callback)
→ Timer registered with sendid "myTimer"
2. Time passes...
- Bare-metal: tick() decrements countdown
- Desktop: OS manages timer in background
3a. Timer expires:
→ Callback fires
→ MyMachine_send() called
→ Event processed IMMEDIATELY
3b. State exits before timer fires:
→ <onexit> runs <cancel sendid="myTimer"/>
→ Timer cancelled, resources freed
What is SCXML_MAX_ACTIVE_TIMERS?
This defines the maximum number of simultaneously pending delayed events.
Example: This state needs SCXML_MAX_ACTIVE_TIMERS >= 3:
<state id="complex">
<onentry>
<send id="t1" event="e1" delay="1s"/>
<send id="t2" event="e2" delay="2s"/>
<send id="t3" event="e3" delay="3s"/>
</onentry>
</state>
What is process_delayed() For?
NOT for timer events! Timer callbacks process events immediately.
process_delayed() is only needed for:
<invoke>child events - Child machines queue events to parent's external queue- Manual external queue operations
For simple state machines without <invoke>, you don't need it.
ContinuousExecutor
The ScxmlContinuousExecutor provides a high-level, thread-safe event loop for running state machines. It matches the executor pattern used in Java, C#, Go, and Python targets.
Why Use ContinuousExecutor?
Without it, timer callbacks fire directly on OS timer threads (POSIX/Windows), creating race conditions if you also call _send() from your application thread. The ContinuousExecutor solves this by:
- Serializing all events through a single command queue
- Processing events on one thread (background worker or tick-based)
- Redirecting timer callbacks through the queue instead of calling
_send()directly
Desktop / Server Usage (POSIX, Windows)
Uses a background worker thread. Timer fires and external events are all processed on the worker thread.
#include "my_machine.h"
#include <scxml/scxml_continuous_executor.h>
int main(void) {
MyMachine sm;
MyMachine_init(&sm);
/* Create executor with type-erased machine interface */
ScxmlContinuousExecutor exec;
scxml_cexec_init(&exec, &sm,
(ScxmlCexecSendFn)MyMachine_send,
(ScxmlCexecIsFinishedFn)MyMachine_is_finished);
/* Wire wakeup so delayed <send> events route through executor */
sm._cexec_wakeup_fn = (void(*)(void*,int,void*))scxml_cexec_timer_wakeup;
sm._cexec_wakeup_ctx = &exec;
/* Start machine and background thread */
MyMachine_start(&sm);
scxml_cexec_start(&exec);
/* Send events from any thread (thread-safe) */
scxml_cexec_send(&exec, EVT_START, NULL);
/* Block until machine reaches a final state */
if (scxml_cexec_wait(&exec, 30000)) {
printf("Machine finished.\n");
}
/* Cleanup */
scxml_cexec_stop(&exec);
scxml_cexec_destroy(&exec);
MyMachine_destroy(&sm);
return 0;
}
Bare Metal / Arduino / RTOS Usage
No background thread. Call scxml_cexec_tick() from your main loop or RTOS task.
#include "my_machine.h"
#include <scxml/scxml_continuous_executor.h>
MyMachine sm;
ScxmlContinuousExecutor exec;
void setup(void) {
MyMachine_init(&sm);
scxml_cexec_init(&exec, &sm,
(ScxmlCexecSendFn)MyMachine_send,
(ScxmlCexecIsFinishedFn)MyMachine_is_finished);
sm._cexec_wakeup_fn = (void(*)(void*,int,void*))scxml_cexec_timer_wakeup;
sm._cexec_wakeup_ctx = &exec;
MyMachine_start(&sm);
scxml_cexec_start(&exec);
}
void loop(void) {
/* Process queued events and check timers */
scxml_cexec_tick(&exec);
/* Send events from interrupts or polling */
if (button_pressed()) {
scxml_cexec_send(&exec, EVT_BUTTON, NULL);
}
delay(10);
}
Generated Project Output
When generating a C project (non --code-only mode), the output includes a main.c that demonstrates both desktop and bare-metal ContinuousExecutor patterns:
scxml-gen my_machine.scxml -t c -o my_machine.c
# Output:
# my_machine.h - Public API header
# my_machine.c - State machine implementation
# scxml_runtime.h - Amalgamated runtime (includes ContinuousExecutor)
# main.c - Entry point with desktop + bare-metal examples
ContinuousExecutor API
| Function | Description |
|---|---|
scxml_cexec_init(exec, sm, send_fn, is_finished_fn) |
Initialize executor with machine |
scxml_cexec_start(exec) |
Start background thread (POSIX/Windows) or mark ready (bare metal) |
scxml_cexec_send(exec, event_id, payload) |
Enqueue event (thread-safe) |
scxml_cexec_wake(exec, event_id, payload) |
Timer wakeup (called internally) |
scxml_cexec_tick(exec) |
Process pending events (bare metal only) |
scxml_cexec_wait(exec, timeout_ms) |
Block until finished or timeout |
scxml_cexec_stop(exec) |
Request shutdown and join worker |
scxml_cexec_destroy(exec) |
Free resources |
scxml_cexec_is_running(exec) |
Check if executor is running |
scxml_cexec_is_finished(exec) |
Check if machine reached final state |
scxml_cexec_requires_tick(exec) |
Returns true on bare metal |
scxml_cexec_on_finished(exec, fn, ctx) |
Set completion callback |
scxml_cexec_timer_wakeup |
Static callback for wakeup wiring |
Configuration
| Macro | Default | Description |
|---|---|---|
SCXML_CEXEC_QUEUE_SIZE |
32 | Command ring buffer size (must be power of 2) |
SCXML_CEXEC_DEFAULT_TIMEOUT_MS |
30000 | Default wait timeout |
Wakeup Wiring
The key integration point between the ContinuousExecutor and generated code is the wakeup redirect. When you set sm._cexec_wakeup_fn and sm._cexec_wakeup_ctx, all delayed <send> events route through the executor queue instead of calling _send() directly from timer threads:
Timer fires → scxml_delayed_event_fire()
→ if wakeup_fn set: enqueue to executor (thread-safe)
→ else: call _send() directly (legacy, NOT thread-safe)
Invoke Children with --bundle
When your state machine uses <invoke> to spawn child machines, use the --bundle option to include external SCXML files in the generated code.
Why Bundle?
By default, external src attributes in <invoke> elements require a runtime callback for child creation. The --bundle option pre-compiles all child machines into a static registry for:
- No runtime loading: Child machines are compiled at build time
- Full transpilation: All children run as transpiled C code
- Static memory: Children can be statically allocated (no malloc)
- Embedded friendly: No filesystem or dynamic loading required
Usage
# Automatic discovery (recommended) - follows src attributes recursively
scxml-gen parent.scxml -t c -o parent.c --bundle-auto
# Manual bundling - specify files explicitly
scxml-gen parent.scxml -t c -o parent.c \
--bundle child1.scxml --bundle child2.scxml
# Or comma-separated
scxml-gen parent.scxml -t c -o parent.c \
--bundle child1.scxml,child2.scxml
# Combine both - auto-discover plus additional manual files
scxml-gen parent.scxml -t c -o parent.c \
--bundle-auto --bundle extra.scxml
Automatic Discovery with --bundle-auto
The --bundle-auto option scans the SCXML file for <invoke src="..."> attributes and:
- Resolves relative paths (e.g.,
file:child.scxml) from the parent file's directory - Resolves absolute paths directly
- Recursively scans discovered children for their own invoke references
- Tracks visited files to prevent infinite loops
- Skips dynamic
srcexprattributes (cannot be resolved at compile time)
How It Works
- Each bundled SCXML file is transpiled to C structs and functions
- A static
g_child_registry[]mapssrcpaths to child initializers - When
<invoke src="file:child.scxml">executes, the registry is searched - Child machines are initialized using the pre-compiled code
Example
Parent SCXML:
<scxml name="Parent" initial="running" datamodel="native-c">
<state id="running">
<invoke src="file:child.scxml" id="child1"/>
<transition event="done.invoke.child1" target="done"/>
</state>
<final id="done"/>
</scxml>
Generate:
scxml-gen parent.scxml -t c -o parent.c --bundle child.scxml
Generated Code Structure:
// child.h - Bundled child machine
typedef struct Child {
ScxmlStateMachine base;
// Child state data...
} Child;
void Child_init(Child* sm);
void Child_start(Child* sm);
// parent.h - Parent machine
typedef struct Parent {
ScxmlStateMachine base;
// Parent state data...
} Parent;
// Static registry for invoke src lookups
typedef struct {
const char* src;
size_t child_size;
void (*init_func)(void*);
void (*start_func)(void*);
} ScxmlChildEntry;
static const ScxmlChildEntry g_child_registry[] = {
{"file:child.scxml", sizeof(Child), (void(*)(void*))Child_init, (void(*)(void*))Child_start},
{NULL, 0, NULL, NULL} // Sentinel
};
Memory Considerations
| Allocation Mode | Description | Use Case |
|---|---|---|
| Static | Pre-allocate child slots at compile time | Embedded, deterministic |
| Dynamic | Allocate children via malloc | Desktop, variable workloads |
With --bundle, you can pre-allocate all child machine slots statically:
// Static allocation for embedded systems
static Child child_instances[SCXML_MAX_INVOKES];
static int child_index = 0;
void* create_child(const char* src) {
// Look up in registry
for (int i = 0; g_child_registry[i].src; i++) {
if (strcmp(g_child_registry[i].src, src) == 0) {
if (child_index >= SCXML_MAX_INVOKES) return NULL;
Child* child = &child_instances[child_index++];
g_child_registry[i].init_func(child);
return child;
}
}
return NULL;
}
Tracing
The C target supports tracing for debugging and state machine monitoring.
JsonlTraceWriter
#include "scxml_trace.h"
ScxmlTraceListener listener;
ScxmlJsonlWriter* writer = scxml_trace_jsonl_writer_create("trace.jsonl", 1, &listener);
MyStateMachine sm;
MyStateMachine_init(&sm);
MyStateMachine_set_trace_listener(&sm, &listener);
MyStateMachine_start(&sm);
MyStateMachine_send(&sm, EVT_START, NULL);
// Cleanup
scxml_trace_jsonl_writer_destroy(writer);
Custom Trace Callbacks
#include "scxml_trace.h"
// Implement custom trace callbacks
void my_on_state_enter(const char* state_id, uint64_t timestamp_us, void* user_data) {
printf("Entering: %s\n", state_id);
}
void my_on_state_exit(const char* state_id, uint64_t timestamp_us, void* user_data) {
printf("Exiting: %s\n", state_id);
}
void my_on_transition(const char* source, const char* target,
const char* event, uint64_t timestamp_us, void* user_data) {
printf("Transition: %s -> %s on %s\n", source, target, event ? event : "(null)");
}
// Set up listener
ScxmlTraceListener listener;
scxml_trace_listener_init(&listener);
listener.on_state_enter = my_on_state_enter;
listener.on_state_exit = my_on_state_exit;
listener.on_transition_fired = my_on_transition;
MyStateMachine sm;
MyStateMachine_init(&sm);
MyStateMachine_set_trace_listener(&sm, &listener);
Trace API
| Function | Description |
|---|---|
scxml_trace_jsonl_writer_create(path, auto_flush, out_listener) |
Create JSONL trace writer |
scxml_trace_jsonl_writer_destroy(writer) |
Destroy and close trace writer |
scxml_trace_get_timestamp_micros() |
Get microsecond timestamp |
scxml_trace_listener_init(listener) |
Initialize all callbacks to NULL |
scxml_trace_jsonl_writer_set_invoke_id(writer, id) |
Set invoke context for output |
Configuration Reference
All configuration is done via #define macros in <gen:includes>.
Event Queue Configuration
| Macro | Default | Description |
|---|---|---|
SCXML_INTERNAL_QUEUE_SIZE |
32 | Max events in internal queue |
SCXML_SENDID_SIZE |
64 | Max sendid string length |
Timer Configuration
| Macro | Default | Description |
|---|---|---|
SCXML_MAX_ACTIVE_TIMERS |
16 | Max concurrent delayed events |
SCXML_TIMER_SENDID_SIZE |
64 | Max timer sendid length |
Invoke Configuration
| Macro | Default | Description |
|---|---|---|
SCXML_MAX_INVOKES |
4 | Max concurrent child machines |
SCXML_INVOKEID_SIZE |
64 | Max invoke ID length |
If you don't use <invoke>: Set SCXML_MAX_INVOKES 0 to save ~1.5KB.
Logging Configuration
| Macro | Default | Description |
|---|---|---|
SCXML_LOG_ENABLED |
1 | Enable/disable <log> output |
SCXML_LOG(label, fmt, ...) |
printf | Custom log implementation |
Arduino Serial example:
#define SCXML_LOG(label, fmt, ...) \
do { Serial.print("["); Serial.print(label); Serial.print("] "); \
Serial.println(fmt); } while(0)
Thread Safety
| Macro | Effect |
|---|---|
SCXML_THREAD_SAFE |
Enable mutex protection for multi-threaded access |
Memory Optimization
Memory Usage by Configuration
| Configuration | RAM Usage | Use Case |
|---|---|---|
| Default | ~7-8 KB | Desktop, full features |
| Moderate | ~2-3 KB | RTOS, larger MCUs |
| Minimal | ~300-500 bytes | Arduino, ATmega328P |
Minimal Configuration (Arduino)
#define SCXML_INTERNAL_QUEUE_SIZE 4
#define SCXML_SENDID_SIZE 16
#define SCXML_MAX_ACTIVE_TIMERS 2
#define SCXML_TIMER_SENDID_SIZE 16
#define SCXML_MAX_INVOKES 0
#define SCXML_INVOKEID_SIZE 1
#define SCXML_LOG_ENABLED 0
Result: ~400 bytes RAM, fits on ATmega328P (2KB RAM).
Moderate Configuration (ESP32, STM32)
#define SCXML_INTERNAL_QUEUE_SIZE 8
#define SCXML_SENDID_SIZE 32
#define SCXML_MAX_ACTIVE_TIMERS 4
#define SCXML_TIMER_SENDID_SIZE 32
#define SCXML_MAX_INVOKES 2
#define SCXML_INVOKEID_SIZE 32
#define SCXML_LOG_ENABLED 1
Feature-Based Savings
| If you don't use... | Set this | Savings |
|---|---|---|
<invoke> |
SCXML_MAX_INVOKES 0 |
~1.5 KB |
| Long sendids | SCXML_SENDID_SIZE 16 |
~100 bytes |
| Many timers | SCXML_MAX_ACTIVE_TIMERS 2 |
~1 KB |
<log> |
SCXML_LOG_ENABLED 0 |
Flash space |
MISRA C:2012 Support (MISRA-Aware Mode)
The C target provides a MISRA-aware mode designed to support MISRA C:2012 compliance for safety-critical embedded systems.
Important Disclaimer
This is NOT certified MISRA C:2012 compliant code.
VSCXML's MISRA mode is designed to support compliance, but:
- No official MISRA verification tool has been run against the generated code
- No third-party certification has been obtained
- Users must perform their own MISRA compliance verification as part of their safety certification process
What MISRA mode provides: Zero dynamic allocation, static memory pools, and architectural patterns compatible with MISRA C:2012 requirements.
What users must do: Run their own MISRA analysis tools (e.g., PC-lint, Polyspace, QA-C) on generated code, document any deviations per their safety process, and obtain appropriate certifications.
Suitability Matrix
| Datamodel | Platform | MISRA-Aware | Notes |
|---|---|---|---|
| native-c | Bare-metal | ✅ Designed for | Zero dynamic allocation possible |
| null | Bare-metal | ✅ Designed for | Minimal runtime, no expressions |
| ecmascript | Any | ❌ Not suitable | JerryScript requires heap allocation |
| native-c/null | POSIX/Windows | ⚠️ Limited | OS timer APIs require additional deviations |
Enabling MISRA Mode
Define SCXML_MISRA_MODE to enable MISRA-compliant code paths:
#define SCXML_MISRA_MODE 1
#define SCXML_PLATFORM_BARE_METAL
#include "my_machine.h"
MISRA mode enforces:
- Static memory allocation only (no
malloc/free) - Static executor initialization
- Static datamodel context
- Disabled trace/JSONL writer (uses dynamic allocation)
Static Memory Allocation
With MISRA mode, all allocations are static:
// Static state machine
static MyMachine sm;
// Static executor storage
static BareMetalExecutor executor_storage;
// Initialize with static storage
MyMachine_init(&sm);
sm.executor = scxml_executor_init_bare_metal_static(&executor_storage);
MyMachine_start(&sm);
Memory Configuration for MISRA
All memory pools are compile-time sized:
| Pool | Macro | Default | Description |
|---|---|---|---|
| Timer slots | SCXML_MAX_ACTIVE_TIMERS |
16 | Static timer array |
| Delayed events | SCXML_MAX_DELAYED_EVENTS |
8 | Static event context pool |
| Event queue | SCXML_INTERNAL_QUEUE_SIZE |
32 | Static queue arrays |
| Invoke sessions | SCXML_MAX_INVOKES |
4 | Static invoke registry |
Known Deviations Requiring User Documentation
The following MISRA C:2012 rules will likely trigger violations that users must document per their safety process:
| Rule | Category | Issue | Recommended Justification |
|---|---|---|---|
| 11.5 | Advisory | void* cast from void pointer |
Platform-agnostic callback interfaces. Type safety enforced by API contracts and single-type usage patterns. |
| 11.1 | Required | Function pointer casts | Executor vtable pattern requires casts. Safe due to matching ABI-compatible signatures. |
| 8.13 | Advisory | Missing const qualification |
Internal APIs where modification is implementation-defined. |
| 10.3 | Required | Implicit enum conversions | Event and state ID comparisons. Values are within valid range by construction. |
These deviations are common in embedded systems code. Users must evaluate each deviation against their specific safety requirements and document appropriately.
MISRA Compliance Checklist
For full MISRA compliance, ensure:
- Use
SCXML_MISRA_MODEdefine - Use
SCXML_PLATFORM_BARE_METAL - Use
native-cornulldatamodel (NOTecmascript) - Use
scxml_executor_init_bare_metal_static()instead of_create() - Statically allocate state machine struct
- Set
SCXML_LOG_ENABLED 0(stdio functions not MISRA compliant) - Document deviations per your safety process
Example: MISRA-Compliant Configuration
<scxml xmlns="http://www.w3.org/2005/07/scxml"
datamodel="native-c" initial="idle" name="SafetyController">
<gen:generator lang="c">
<gen:includes><![CDATA[
/* MISRA C:2012 Compliant Configuration */
#define SCXML_MISRA_MODE 1
#define SCXML_PLATFORM_BARE_METAL
/* Static pool sizes */
#define SCXML_INTERNAL_QUEUE_SIZE 8
#define SCXML_MAX_ACTIVE_TIMERS 4
#define SCXML_MAX_DELAYED_EVENTS 4
#define SCXML_MAX_INVOKES 0
/* Disable non-compliant features */
#define SCXML_LOG_ENABLED 0
]]></gen:includes>
</gen:generator>
<!-- State machine definition -->
</scxml>
Usage in Safety-Critical Code
#include "safety_controller.h"
/* All storage is static */
static SafetyController sm;
static BareMetalExecutor executor_storage;
void safety_init(void) {
SafetyController_init(&sm);
sm.executor = scxml_executor_init_bare_metal_static(&executor_storage);
SafetyController_start(&sm);
}
void safety_tick(uint32_t elapsed_ms) {
/* Poll timers */
scxml_executor_tick(sm.executor, elapsed_ms);
}
void safety_event(int event_id) {
SafetyController_send(&sm, event_id, NULL);
}
Limitations and Caveats
Fundamental limitations:
- ECMAScript datamodel: Not usable in MISRA mode (JerryScript requires heap allocation)
- POSIX/Windows platforms: OS timer APIs (pthreads, Windows API) introduce additional MISRA deviations beyond those documented above
- No formal verification: This code has not been analyzed with MISRA verification tools (PC-lint, Polyspace, QA-C, etc.) - users must run their own analysis
Disabled features in MISRA mode:
- Trace/JSONL output: Uses stdio and dynamic allocation
- Dynamic invoke src: Requires
--bundleflag to statically compile all child machines - Runtime machine creation: All machines must be statically allocated
Compile-time constraints:
- All pool sizes (timers, events, invoke slots) are fixed at compile time
- Exceeding pool limits causes silent failures (returns NULL) rather than runtime errors
- Pool exhaustion can cause state machine malfunction - size conservatively
Testing status:
- MISRA mode is tested via automated test suite (
tests/bare_metal/test_misra_mode.c) - Tests verify: static allocation, pool lifecycle, executor initialization, datamodel contexts
- Tests do NOT verify MISRA rule compliance - only functional correctness
Platform-Specific Details
Arduino / Bare-Metal
Required: Call tick() in your main loop to fire expired timers.
uint32_t last_tick = 0;
void loop() {
// 1. Poll executor - fires expired timers
uint32_t now = millis();
scxml_executor_tick(sm.executor, now - last_tick);
last_tick = now;
// 2. Handle inputs
if (digitalRead(BUTTON_PIN) == LOW) {
MyMachine_send(&sm, EVT_BUTTON, NULL);
}
// NOTE: process_delayed() only needed if using <invoke>
}
Timer precision = tick() frequency:
| Frequency | Precision | CPU Overhead |
|---|---|---|
| Every 1ms | ±1ms | Higher |
| Every 10ms | ±10ms | Low |
| Every 100ms | ±100ms | Minimal |
Windows / Linux / macOS
No tick() required - timers fire automatically in background threads.
int main() {
MyMachine sm;
MyMachine_init(&sm);
MyMachine_start(&sm);
while (MyMachine_is_running(&sm)) {
// Handle user input
if (user_pressed_button()) {
MyMachine_send(&sm, EVT_BUTTON, NULL);
}
// Timer events fire automatically!
sleep_ms(10);
}
MyMachine_destroy(&sm);
}
Thread safety: Timer callbacks run in OS threads. Define SCXML_THREAD_SAFE if you also call _send() from other threads.
RTOS (FreeRTOS, Zephyr)
Option 1: Use bare-metal executor with tick() from a task:
void scxml_task(void* param) {
MyMachine* sm = (MyMachine*)param;
TickType_t last_wake = xTaskGetTickCount();
while (MyMachine_is_running(sm)) {
scxml_executor_tick(sm->executor, 10);
vTaskDelayUntil(&last_wake, pdMS_TO_TICKS(10));
}
}
Option 2: Implement custom executor wrapping native RTOS timers for better precision.
Troubleshooting
Timer not firing
Bare-metal:
- Are you calling
scxml_executor_tick()in your loop? - Is elapsed time calculated correctly?
- Is
SCXML_MAX_ACTIVE_TIMERSlarge enough?
Desktop:
- Is
SCXML_MAX_ACTIVE_TIMERSlarge enough? - Timer callbacks fire automatically - no polling needed.
Out of memory / Timer registration failed
- Increase
SCXML_MAX_ACTIVE_TIMERS - Ensure timers are cancelled properly with
<cancel>in<onexit>
Child events not reaching parent
If using <invoke>, call process_delayed():
MyMachine_process_delayed(&sm);
Note: process_delayed() is NOT needed for timer events.
Poor timer precision (bare-metal)
Increase tick() frequency:
// Better precision
while (1) {
scxml_executor_tick(sm.executor, 1);
delay(1);
}
Complete Example: Light Switch with Auto-Off
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml"
xmlns:gen="http://vscxml.com/generator"
version="1.0" datamodel="native-c" initial="off" name="LightSwitch">
<gen:generator lang="c">
<gen:includes><![CDATA[
/* Minimal embedded configuration */
#define SCXML_INTERNAL_QUEUE_SIZE 4
#define SCXML_SENDID_SIZE 16
#define SCXML_MAX_ACTIVE_TIMERS 2
#define SCXML_MAX_INVOKES 0
#ifdef ARDUINO
#include <Arduino.h>
#define LED_PIN 13
#endif
]]></gen:includes>
</gen:generator>
<state id="off">
<onentry>
<script>digitalWrite(LED_PIN, LOW);</script>
</onentry>
<transition event="button" target="on"/>
</state>
<state id="on">
<onentry>
<script>digitalWrite(LED_PIN, HIGH);</script>
<send id="auto_off" event="timeout" delay="3s"/>
</onentry>
<onexit>
<cancel sendid="auto_off"/>
</onexit>
<transition event="button" target="off"/>
<transition event="timeout" target="off"/>
</state>
</scxml>
Memory usage with this config: ~400 bytes RAM
API Reference
Generated Functions
| Function | Description |
|---|---|
void MyMachine_init(MyMachine* sm) |
Initialize state machine |
void MyMachine_start(MyMachine* sm) |
Enter initial state |
bool MyMachine_send(MyMachine* sm, int eventId, void* payload) |
Send event by integer ID (fast path, use EVT_* constants) |
bool MyMachine_send_name(MyMachine* sm, const char* eventName, void* payload) |
Send event by name string (convenience, slightly slower) |
bool MyMachine_is_in(MyMachine* sm, int state) |
Check if state is active (use S_* constants) |
bool MyMachine_is_running(MyMachine* sm) |
Check if not in a final state |
bool MyMachine_is_finished(MyMachine* sm) |
Check if machine has reached a final state |
void MyMachine_destroy(MyMachine* sm) |
Release resources (cancel timers, free children) |
void MyMachine_set_trace_listener(MyMachine* sm, ScxmlTraceListener* listener) |
Set trace listener for state/transition callbacks |
void MyMachine_process_delayed(MyMachine* sm) |
Drain the external event queue — only needed when using <invoke> |
Generated Constants
// Events (EVT_*)
#define EVT_BUTTON 1
#define EVT_TIMEOUT 2
// States (S_*)
#define S_OFF 0
#define S_ON 1
Event Introspection
const char* events[64];
int count = MyMachine_get_all_events(events, 64);
int enabled = MyMachine_get_enabled_events(&sm, events, 64);
int forState = MyMachine_get_events_for_state(&sm, "s1", events, 64);
int enabledFor = MyMachine_get_enabled_events_for_state(&sm, "s1", events, 64);
See Also
Target Guides
- Java Target - JVM integration, executors, GraalVM
- JavaScript Target - Node.js, Browser, React/Vue
Reference
- Datamodels Guide - Detailed datamodel comparison
- Feature Matrix - Supported SCXML elements
- W3C Compliance - Test results
Tutorials
- User Guide - Complete toolchain guide
- Tutorial - Step-by-step introduction
- Arduino Light Switch - Embedded example
- Desktop Light Switch - Desktop example