C / Embedded Target Guide

C / Embedded Target Guide

Complete guide for configuring the C target across platforms: Arduino, Desktop (Windows/Linux/macOS), and RTOS.

Table of Contents

  1. Overview
  2. Quick Start
  3. Platform Support
  4. Datamodel Support
  5. Understanding the Executor System
  6. ContinuousExecutor
  7. Invoke Children with --bundle
  8. Tracing
  9. Configuration Reference
  10. Memory Optimization
  11. MISRA C:2012 Support
  12. Platform-Specific Details
  13. Troubleshooting
  14. Complete Example
  15. 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

bash
# 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 implementation
  • my_machine.h - Public API header
  • scxml_transpiled.h - Runtime (if amalgamated)

2. Basic Usage

c
#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:

bash
# 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:

bash
# 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.

The generated CMakeLists.txt can automatically fetch and build JerryScript:

bash
# 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

Type-safe C variables with compile-time checking:

xml
<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:

xml
<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):

xml
<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"/>:

  1. Generated code calls scxml_executor_schedule(3000ms, callback)
  2. Executor manages the timer using platform-appropriate mechanism
  3. When timer expires, executor calls the callback
  4. 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:

xml
<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:

  1. Serializing all events through a single command queue
  2. Processing events on one thread (background worker or tick-based)
  3. 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.

c
#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.

c
#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:

bash
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

bash
# 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:

  1. Resolves relative paths (e.g., file:child.scxml) from the parent file's directory
  2. Resolves absolute paths directly
  3. Recursively scans discovered children for their own invoke references
  4. Tracks visited files to prevent infinite loops
  5. Skips dynamic srcexpr attributes (cannot be resolved at compile time)

How It Works

  1. Each bundled SCXML file is transpiled to C structs and functions
  2. A static g_child_registry[] maps src paths to child initializers
  3. When <invoke src="file:child.scxml"> executes, the registry is searched
  4. Child machines are initialized using the pre-compiled code

Example

Parent SCXML:

xml
<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:

bash
scxml-gen parent.scxml -t c -o parent.c --bundle child.scxml

Generated Code Structure:

c
// 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:

c
// 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

c
#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

c
#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:

c
#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)

c
#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)

c
#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:

c
#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:

c
// 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.
note

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_MODE define
  • Use SCXML_PLATFORM_BARE_METAL
  • Use native-c or null datamodel (NOT ecmascript)
  • 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

xml
<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

c
#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 --bundle flag 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.

c
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.

c
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:

c
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_TIMERS large enough?

Desktop:

  • Is SCXML_MAX_ACTIVE_TIMERS large 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():

c
MyMachine_process_delayed(&sm);

Note: process_delayed() is NOT needed for timer events.

Poor timer precision (bare-metal)

Increase tick() frequency:

c
// Better precision
while (1) {
    scxml_executor_tick(sm.executor, 1);
    delay(1);
}

Complete Example: Light Switch with Auto-Off

xml
<?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

c
// Events (EVT_*)
#define EVT_BUTTON 1
#define EVT_TIMEOUT 2

// States (S_*)
#define S_OFF 0
#define S_ON 1

Event Introspection

c
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

Reference

Tutorials