Skip to content

Tutorial 5: Code Generation & Build Integration

In this tutorial, you will learn how to turn your verified state machine models into production-ready software:

  • How fsmc uses the Generation Gap Pattern to ensure safe, non-destructive builds.
  • Generating standalone and modular C++ code (C++17 and C++20).
  • Integrating automated compilation into CMake (fsmc_target_sources).
  • Deploying with partitioned memory domains (InPorts, OutPorts, Registers, Services).

1. The Generation Gap Pattern: Safe, Non-Destructive Builds

A common pitfall with code generation tools is that modifying a model might overwrite manually written business logic.

fsmc prevents this by enforcing a strict separation between Generated Artifacts and User Code:

my_project/
├── models/
│   └── connection.sysml        <-- Source model (Source of Truth)
├── src/
│   ├── main.cpp                <-- User Application Code (Untouched)
│   └── connection_services.hpp <-- User Service Implementations (Untouched)
└── build/generated/
    └── connection_fsm.hpp      <-- Generated Artifact (Regenerated automatically)
  1. The generated file (connection_fsm.hpp) is never edited manually. It lives in your build/ directory.
  2. Your custom side-effects live in your services implementation (connection_services.hpp).
  3. Whenever you update connection.sysml, fsmc regenerates the header in milliseconds. Your application code simply links against the updated transition table with zero lost work.

2. Generating Code from the Command Line

# Option A: Generate a standalone self-contained C++20 header with embedded zero-alloc runtime
fsmc -i connection.sysml -o connection_fsm.hpp --target cpp --std 20 --standalone

# Option B: Export the runtime once and generate modular lightweight headers (ideal for multi-FSM projects)
# 1. Export standalone runtime library into your project include path (creates fsm.hpp):
fsmc --export-runtime ./include/ --std 20

# 2. Generate lightweight FSM definition including external "fsm.hpp":
fsmc -i connection.sysml -o connection_fsm.hpp --target cpp --std 20 --modular

# Option C: Use custom runtime header include path
fsmc -i connection.sysml -o connection_fsm.hpp --modular="<fsm/fsm.hpp>"

[!NOTE] Roadmap Preview: Rust code generation is a planned multi-target release feature. C++ is the current production runtime.

# Generate idiomatic `#![no_std]` Rust module
fsmc -i connection.sysml -o connection_fsm.rs --target rust --namespace conn

[!NOTE] Roadmap Preview: ISO C99 embedded C code generation is a planned multi-target feature. C++ is the current production runtime.

# Generate embedded C header and implementation
fsmc -i connection.sysml -o connection_fsm.h --target c --prefix conn_

3. Understanding the Generated Header

The generated connection_fsm.hpp exposes three policy-based type aliases — one for each concurrency model:

// Synchronous FSM (single-threaded, zero-allocation)
using ConnectionManager = ::fsm::make_fsm<
    ConnectionManagerTable,
    ::fsm::with_ports<ConnectionManagerInPorts, ConnectionManagerOutPorts>,
    ::fsm::with_registers<ConnectionManagerRegisters>,
    ::fsm::with_services<ConnectionManagerServices>
>;

// Lock-free SPSC FSM (ISR producer → consumer thread)
using SpscConnectionManager = ::fsm::make_spsc_fsm<
    ConnectionManagerTable,
    ::fsm::with_ports<ConnectionManagerInPorts, ConnectionManagerOutPorts>,
    ::fsm::with_registers<ConnectionManagerRegisters>,
    ::fsm::with_services<ConnectionManagerServices>,
    ::fsm::with_queue_capacity<64>
>;

// Mutex-protected thread-safe FSM (multi-consumer safe)
using ThreadSafeConnectionManager = ::fsm::make_thread_safe_fsm<
    ConnectionManagerTable,
    ::fsm::with_ports<ConnectionManagerInPorts, ConnectionManagerOutPorts>,
    ::fsm::with_registers<ConnectionManagerRegisters>,
    ::fsm::with_services<ConnectionManagerServices>
>;

The with_* policy modifiers can be composed in any order — the compiler resolves each domain independently via template specialization.


4. Using the Generated State Machine

In your main.cpp:

#include <cassert>
#include <iostream>
#include "connection_fsm.hpp"

// Implement the abstract service interface generated by fsmc
struct ConcreteConnectionServices : public conn::ConnectionManagerServices {
    void log(const std::string& msg) override {
        std::cout << "[SERVICE LOG] " << msg << "\n";
    }
};

int main() {
    using namespace conn;

    // Policy-based instantiation: all domains bound at compile time
    ConnectionManagerRegisters reg{};
    ConcreteConnectionServices srv;
    ConnectionManager fsm(reg, srv);

    ConnectionManagerInPorts in;
    in.latency_ms = 45.0;
    in.is_authenticated = true;
    ConnectionManagerOutPorts out;

    // Validate port range contracts (generated by fsmc from SysML v2 constraints)
    assert(in.validate_contracts());
    std::cout << "Initial state: " << fsm.current_state_name() << "\n";

    // 1. Dispatch typed events
    fsm.dispatch(ConnectCmd{}, in, out);
    std::cout << "State after ConnectCmd: " << fsm.current_state_name() << "\n";

    fsm.dispatch(HandshakeOk{}, in, out);
    std::cout << "State after HandshakeOk: " << fsm.current_state_name() << "\n";

    // 2. Sampled continuous step (periodic IEC 61131-3 / SCADE style)
    fsm.step(in, out);

    return 0;
}

[!NOTE] Roadmap Preview: Preview of upcoming Rust #![no_std] application code.

In your main.rs:

use conn::connection_fsm::*;

fn main() {
    let mut fsm = ConnectionManagerFsm::new(ConnectionRegisters::default());
    let mut in_ports = ConnectionInPorts { latency_ms: 45.0, is_authenticated: true };
    let mut out_ports = ConnectionOutPorts::default();

    // 1. Dispatch typed events
    fsm.dispatch(&Event::ConnectCmd, &in_ports, &mut out_ports);
    fsm.dispatch(&Event::HandshakeOk, &in_ports, &mut out_ports);

    // 2. Sampled continuous step
    fsm.step(&in_ports, &mut out_ports);
}

[!NOTE] Roadmap Preview: Preview of upcoming ISO C99 application code.

In your main.c:

#include "connection_fsm.h"

int main(void) {
    conn_fsm_t fsm;
    conn_registers_t reg = {0};
    conn_in_ports_t in = {.latency_ms = 45.0f, .is_authenticated = true};
    conn_out_ports_t out = {0};

    conn_fsm_init(&fsm, &reg);

    /* 1. Dispatch typed events */
    conn_fsm_dispatch(&fsm, CONN_EV_CONNECT, &in, &out);
    conn_fsm_dispatch(&fsm, CONN_EV_HANDSHAKE_OK, &in, &out);

    /* 2. Sampled continuous step */
    conn_fsm_step(&fsm, &in, &out);

    return 0;
}


5. Seamless CMake Integration (fsmc_target_sources)

To automate state machine compilation in your C++ build pipeline, use the CMake helper function provided by fsmc:

cmake_minimum_required(VERSION 3.16)
project(my_embedded_app CXX)

find_package(fsmc REQUIRED)

add_executable(my_app
    src/main.cpp
)

target_link_libraries(my_app PRIVATE fsmc::runtime)

# Automatically compile models/connection.sysml into ${CMAKE_CURRENT_BINARY_DIR}/generated_fsm/
fsmc_target_sources(my_app
    DIAGRAMS models/connection.sysml
    NAME ConnectionManagerFSM
    STANDARD 20
    STANDALONE
    NAMESPACE conn
)

Next Steps

Now that you have mastered the complete pipeline from modeling to code generation, proceed to the capstone chapter:

Tutorial 6: Complete Real-World Case Study (Autonomous UAV Controller).