A transition condition and action in an itemis CREATE statechart can look like this:
[sensor.temperature > TEMP_LIMIT_HIGH] / hal_pump_set(PUMP_SPEED_FULL)
Nothing unusual at first glance. It gets interesting once you ask where those four identifiers come from. From sensors.h come the typedef struct SensorReading that sensor is declared with, and the threshold constant TEMP_LIMIT_HIGH. From hal_pump.h come the driver function hal_pump_set() and the speed constant PUMP_SPEED_FULL.
All four come from code that has been sitting in the project for years. Nobody transferred them into the model, rebuilt them as model types, or passed them through an interface. The statechart editor read the headers, offers content assist on them, and validates the field access sensor.temperature against the real struct definition.
So what’s notable about this expression is less what’s there than what’s missing: the layer in between. And that layer is exactly why modeling so often stalls in existing projects.
The diagram on the wall and the code that counts
Since 2006 I’ve been working with teams at itemis who apply model-based methods or want to adopt them. The starting point often looks much the same.
The system being built is reactive by nature: it responds to sensors, buttons, timers or bus messages, holds state while doing so, and triggers actions under defined conditions. That’s very often the case in embedded systems, and in plenty of other event-driven application systems too. Reading that logic as plain code is tedious, because which states exist and when a given transition fires is spread across flags, switch blocks and conditions.
So people draw. A state machine on the whiteboard, in the specification, in the architecture document. That works well. The picture settles reviews, gets new team members up to speed faster, and makes discussions with the domain side and with test possible in the first place. State machines here aren’t a development tool but a means of understanding, and at that they’re strong.
Then the diagram gets implemented by hand, as an enum with switch, as a state table, or as a state pattern. We walked through those three in a series of their own. They all work, and they all have the same catch.
From that moment on there are two artefacts: the picture that explains, and the code that counts. Both describe the same behaviour, but only one of them runs. Every change would have to hit both; under deadline pressure it hits the code, because the code has to work. The diagram gets updated when there’s time.
After a few iterations nobody updates it any more. It’s still roughly right, and with a state machine “roughly” means wrong: a missing guard or a forgotten transition out of the error state sits precisely where the interesting bugs sit too. The diagram then explains a system that no longer exists in that form. As a means of understanding it has become harmful rather than merely useless.
Why the obvious way out creates a second break
If the diagram is meant to describe the truth anyway, then the obvious conclusion is to generate the code straight from it. The diagram becomes the one leading artefact, and there’s no synchronisation question left.
The conclusion is right. In the adoptions I’ve supported, though, that step is where things regularly get stuck. The modeling tool brings its own world, with a type system that doesn’t know the project’s types, no knowledge of the driver API, the constants, the error codes. Whatever the model needs has to be defined a second time inside the model; whatever it’s supposed to call gets exposed through an interface and wired up on the other side.
That wiring code is the real price. It’s dull, there’s a lot of it, and it’s the second place where the same information lives. Change a struct field or a constant and the model side has to follow. The team now has two breaks instead of one, and the new one weighs more, because it leaves the documentation alone and hits the build instead.
That’s what modeling adoptions fail on: not the notation and not the tool, but the realisation after three weeks that a parallel world is taking shape and will want to be maintained from here on.
The model as an extension of the code
The second break is avoidable if the model speaks the language of the project.
That means more than the target language of generation. The expression language inside the model uses the same type system as the existing code: same types, same constants, same signatures. The model imports the existing headers or classes and works on them directly.
This shifts the division of roles away from the pure doctrine of model-driven software development. The model isn’t out to replace the code; it extends it, and it owns exactly the part that reads badly as code.
The practical payoff follows from that. When the model takes over one slice and leaves the rest untouched, adopting it becomes a local decision: one statechart for one component, in the existing project, in the existing toolchain. And because the code that actually runs comes out of that statechart, the first break disappears as well. The diagram no longer explains the implementation, it is the implementation.
“We’d have to rebuild our definitions inside the model”
The most common objection, and it holds as long as the tool has no concept for it.
In itemis CREATE an import handles this. On the C/C++ side, this is enough inside the statechart:
import: "sensors.h"
import: "hal_pump.h"
That makes the following available in the model: functions (including variadic parameters), typedefs, structs, unions and enums, function pointers inside structs, #define macro values, variables, statically allocated arrays, and pointers. C++ adds smart pointers, classes with their fields and methods, namespaces (which show up as packages in the statechart), template classes and functions, and functions with optional parameters.
Variables can be declared by value or as pointers, var n: int32_t alongside var pInt: pointer<int32_t>, and var spInt: shared_ptr<int32_t> for C++ smart pointers, which the generator can resolve back into a raw pointer if you want it to. Dereferencing goes through the value extension function, available on every pointer-typed variable; with several layers of indirection it stacks accordingly, as in n = ppInt.value.value.
Java follows the same principle with Java means. You import classes, interfaces and enums; on a variable of a Java type all public members are reachable, methods as well as fields, static ones included. Inner classes and enumerators can be used, and constructors appear as static new(...) methods, so objects can be created inside the model. Everything on the project’s classpath is available, Java’s own standard types and classes included; content assist lists them at the import. The transition from above then reads like this:
[reading.getTemperature() > Limits.TEMP_HIGH] / pump.setSpeed(PumpSpeed.FULL)
The editor isn’t merely tolerant towards these identifiers, it knows them: content assist lists the reachable members, and validation checks field accesses and calls against the actual declaration. A typo in a field name shows up in the editor rather than in the build.

“The model doesn’t know our hardware and our APIs”
That it can know them comes down to the domain concept in itemis CREATE. When you create a statechart you pick a domain, either in the wizard or later in the statechart properties. Available are the default domain for models without language integration, a C, a C++ and a Java domain, plus an SCXML domain. The domain determines which type system the expression language in the model uses.
For embedded work that’s more than convenience. The C domain knows int8_t through int64_t, uint8_t through uint64_t, plus bool, float, double, string and void: the widths the target hardware actually computes in, rather than a generic numeric representation the generator has to map somehow later on.
Prerequisites on the C/C++ side: a CDT project, a statechart in the C or C++ domain, and headers inside the workspace or on the CDT include paths. Recognised extensions are .h, .hh, .hpp, .hxx and .inc. The details are in the documentation on deep C/C++ integration.
“The generated code won’t fit our code”
Here the import pays off a second time. Because the expressions in the model are already based on the real declarations, the generated state machine calls the existing functions directly and uses their data types unchanged. There’s no adapter layer to generate, maintain and understand. The glue code doesn’t disappear through discipline, it disappears because there’s nothing left to glue.
The existing build treats the generated code like any other: same compiler, same flags, same static analyses. Whether all headers pulled in through deep integration end up in the generated header, or only the ones actually used, is down to a configuration parameter. The generation step can be hooked into the automated build, so every run reproducibly yields the same result from the model.
When not to bind the model to the language
Deep integration is a trade. It takes the wiring code away and pays with binding: from then on the model knows one concrete language and one concrete codebase. In four situations that trade is a bad one, and the default domain with declared operations is the better choice.
Not every behaviour needs more than what the statechart brings along. When the logic gets by with bool, integers and a handful of events, and the outside world is reached through declared operations, an import buys nothing, because there’s nothing to import that the language core doesn’t already cover. Some teams keep the indirection even when they could drop it: the declared operation is the seam where an implementation can be swapped out, for a test double, for a different HAL, for a simulator.
If code for several target languages is meant to come out of the same statechart, deep integration rules that out. itemis CREATE generates C, C++, C#, Java and Python; deep domains exist for C, C++ and Java. A statechart in the C domain therefore can’t sensibly serve Python code, because there is no Python domain in which the imported declarations would have a counterpart. The same holds with a time lag, when a codebase is due to migrate to another language later on.
The third case concerns projects that model consistently. When the data types and interfaces a statechart uses come from models themselves rather than from C, C++ or Java sources, language integration finds nothing to attach to. The model is then meant to fit other models, not code.
And finally the case where no code exists yet. Modeling behaviour before the driver or the supplier interface exists leaves no header to import. Declared operations aren’t a compromise here, they’re the only possible order: the statechart is the specification the implementation lines up with later.
Which language constructs the integration covers in detail, and where it has gaps today, is in the documentation. For deciding whether to use it at all, the four cases are the better yardstick.
A principle, not a feature
Deep C/C++ integration and deep Java integration look like two functions of the same tool. They’re two instances of one idea: a model doesn’t have to be a closed world, it can adapt to the language the project already thinks in. That turns it from an alternative to the code into an extension of it.
The principle carries beyond state machines. A domain-specific language pays off precisely when it docks onto the existing terms and artefacts instead of building a second description of the system alongside them. How to design such languages is the subject of our Custom Tools work.
To get started the small variant is enough, and that’s the actual point: one statechart, for one component, in the existing project. The headers are already there. So is the sketch; it’s on the wall and hasn’t been right since the last sprint.
For an overview of the editor, the simulation and the generators, see the itemis CREATE product page.
Model-driven software development at itemis: Model and simulate state machines, and generate code from them: Model-Driven Software Development →