Automatic Code Generation from UML Models - Building translation templates

. Stephen J. Mellor is vice president of Project Technology, Inc., the company he and Sally Shlaer co-founded in 1985, now located in Tucson, AZ. He can be contacted at [email protected].


In the first article of this series,1 we laid out a six-step process for the work involved in translative code generation and described the first three steps in detail. Namely:

  • The developer builds a set of application models using an executable UML.
  • The developer enters these models into a tool that "understands" the semantics of the metamodel of that executable UML.
  • The tool stores the models in the repository that has a structure as defined by the metamodel of the executable UML.
The result of executing these steps is a populated repository. So if we model an example application such as the AutoSampler,* the repository will be populated with the semantics of that AutoSampler application model.

The remaining steps are:

  • Separately, the developer defines an application-independent software architecture, which comprises a set of design decisions to apply to an arbitrary application model.
  • The developer buys or builds a set of translation templates for each construct required by the application-independent software architecture. These translation templates drive the code-generation process.
  • Finally, the code-generator tool interprets the translation templates, replacing placeholders in the translation templates with the values of instances from the metamodel in the repository.
The previous issue described the concept of an application-independent software architecture, but it did not provide an example. It now falls to us here to provide that example, build a set of translation templates for that application-independent software architecture, and generate some code.

AN EXAMPLE APPLICATION-INDEPENDENT SOFTWARE ARCHITECTURE An application-independent software architecture defines a set of design decisions expressed as a set of rules to apply to an application to produce the implementation of a system. Conceptually, the simplest architecture of all is a one-to-one mapping from the elements of the executable UML to the code.+ So for a one-to-one mapping of a model to C++, we might say that each (conceptual) class in the application model becomes a class in the code; each (conceptual) attribute becomes a private data member; each (conceptual) accessor becomes a public member function of the encapsulating class; and so on. Each rule maps a conceptual element of the executable UML to the corresponding element in C++. We could also write an equivalent one-to-one mapping to Java, and say instead that each (conceptual) class in the application model becomes a class in the code; each (conceptual) attribute becomes a private data field; each (conceptual) accessor becomes a public method of the encapsulating class; and so on. And equally we could build a similar set of mappings for C, Basic, or even COBOL—though as the gap between the UML metamodel and the available concepts in the target language becomes wider, there is more work to do to fill it in.

Figure 1. The structure of the metamodel repository.

So that we may focus our attention on translation technology, and not on the intricacies of some complicated mapping, we shall define an architecture that has a structure similar to the UML metamodel. Specifically, the class structure will be direct, where each conceptual class becomes a C++ class with its private data members, and the execution engine is based on the state charts used to model the application. Once this direct mapping is clear, we shall demonstrate other less direct mappings.

To provide the execution engine, we note that the metamodel of executable UML associates a state chart with each class that has interesting dynamics. To support these dynamics directly, we build a StateChart class that holds a representation of the state chart so that we can find the next state for an object instance, given the current state and a signal event¥ directed to that object instance.

We then instantiate one StateChart object instance for each application class that has a state chart. In the AutoSampler example, there will be four instances of the StateChart class, one for each application state chart—Row, Probe, Probe Assignment, and Carousel—because each of those classes has a state chart to define its behavior.

Separately, we create an abstract base class ActiveInstance. This class captures the data and behavior common to each object instance of those classes that have state charts. Hence, if there are two rows, one probe, one probe assignment, and one carousel, there will be five instances that inherit from ActiveInstance. Each application-class object instance is an ActiveInstance, and it has an attribute that captures its current state.

Each application class derived from ActiveInstance has a static data member myStateChart, which refers to the instance of StateChart that models the specification of that application class' behavior. Thus, the Row class has a reference to the instance of StateChart that captures the behavior of each row instance, and the Carousel class has a reference to its StateChart instance, and so on.

The StateChart and ActiveInstance classes capture related but different aspects of the state behavior. The StateChart class captures the specification of the state behavior, while the ActiveInstance captures the current state of each object instance. Hence, there will be as many StateChart instances as there are classes that have state charts; and there will be as many instances of ActiveInstance as there are object instances of classes that have state charts.

When an object instance in the application model sends a signal event, the sending class queues it by creating an instance of a SignalEvent class. A separate signal event dispatcher dequeues the signal event and invokes a member function TakeEvent in the destination class. This function picks up the handle for the associated state chart, held in the static data member myStateChart, and invokes a member function of ActiveInstance, which uses the StateChart class to determine the new state. Armed with the new state, the receiving class invokes the private member function that corresponds to the appropriate action sequence by jumping through an array of function pointers.§

This scheme for signal events is asynchronous in nature. When an action sequence generates a signal event, it is placed on a queue. If the action sequence generates multiple signal events, the dispatcher takes a signal event off the queue (one at a time), and dispatches it to another active instance, which may generate further events for the queue. Eventually, there are no further events to process. The task then waits for a message from outside the task, typically from the user or hardware interface.

With the exception of the initialization of the StateChart instances, this is all just code. There is nothing in here that is specific to the application at hand. We now show how a typical application class uses these mechanisms. Listing 1 is the partial code for the Row class. This class has elements in it that depend directly on the application semantics, so we also need a way to incorporate the application semantics into these architectural design decisions.

These architectural design decisions and the application (as stored in the metamodel) are combined using a translation template written in a special-purpose language.

WRITING TRANSLATION TEMPLATES The purpose of the language is to implement the design decisions and link those design decisions to the application model semantics, which are available in the repository. For example, the rule, "each (conceptual) class in the application model becomes a class in the code," can be expressed by writing a translation template for the class, expressed largely in C++ syntax, with a placeholder for the name of the application class. The translation template language interpreter replaces the placeholder with the name of each application class taken from the repository. We apply the same process for each code element lexically included in the class. The final output is compilable C++.

At a more abstract level, the translation template language is a tool for traversing an arbitrary repository to find strings that it embeds into output text. As a consequence, the translation template language is completely independent of the target programming language, and it may be used to produce Java, C, Basic, or even COBOL.

The translation template language manipulates several different realms of information. Specifically, it manipulates: (1) the repository, which comprises a set of tables that corresponds to the structure of the UML metamodel; (2) the instances in the repository—the set of tables—that refer to elements of the application; and (3) the target language, in this case C++. Because the purpose of the translation template language is to link these realms together, often via one-to-one mappings, the same word can often appear in several realms at once. For example, the application contains classes; the UML metamodel has a class whose name is class, and the eventual output of this is a C++ class. The word class is overloaded—way overloaded. The same word refers to different but strongly related concepts. Consequently, it is very easy to lose track of which word belongs in which realm, and to keep track of which realm is which. For example, a meaningful (though less-than-intuitively obvious) sentence is: "The instance reference class refers to a instance of the class class." Sure.

An instance reference is a kind of variable type that refers to an instance of a class as held in a table in the repository. We may think of it as a handle to an arbitrary object instance. Then in the sentence above, "the instance reference class" is introducing a variable, CLASS, of type instance reference. This variable, CLASS, refers to an instance of a class in the repository. The name of that class happens to be Class. Therefore, we may restate our example sentence above using typographical conventions as: "The instance reference CLASS refers to an instance of the class Class." This is not exactly a model of clarity, but is still a lot easier to understand than the original. (Table 1 presents the typographical conventions used to distinguish realms.)

Table 1. Typographical conventions used to distinguish realms.
Realm/Concept Typography Example
C++ code monospaced font static StateChart
Element in the repository underlined text Class, Name, StateID
Translation template commands code font .select any
Instance reference SMALL CAPS CLASS, STATE
Instance references sets END CAPITALIZATION CLASSES, STATES

A TRANSLATION TEMPLATE LANGUAGE The translation template language comprises three parts:

  • literal text, in this case C++
  • commands, which are lines that begin with a period (.)
  • substitutions, shown as ${ placeholder }
The literal text in the examples is C++, with which readers of C++ Report are familiar.

A command is a way to insert the translation template language within the target language. The primary purpose of the translation template language is to traverse the repository and thus act as a data access language. So we begin by describing data access commands.

First, to select an instance of a table in the repository, write:

.select any CLASS from instances of Class

The result of this command is to create an instance reference CLASS that refers to an arbitrary instance taken from the table Class. The keyword any indicates that any instance of the many available is acceptable.

The placeholder in a substitution is generally an instance reference and the name of an attribute:

${ Class.Name }
Note the typographical conventions here. The expression to the left of the period is an instance reference, while the expression to the right refers to an attribute of some table in the repository. Hence, this statement returns a string—the value of the Name attribute of an instance of the table (presumably) named "class." In the AutoSampler example, the selected class may be Row.

To traverse an association, write:

.select one STATECHART related to instances of CLASS -> StateChart

This command creates an instance reference, STATECHART, that refers to the instance in the StateChart table to which CLASS is related. For example, if the instance reference CLASS refers to the Row, the result is the instance reference to the state chart for the Row. The keyword one indicates that only one associated instance is acceptable, because the association is 0..1.

To select an arbitrary instance when traversing a to-many (..*) association, use the keyword any:

.select any STATE related to instances of STATECHART->State

This command returns an arbitrary STATE from the state chart referred to by STATECHART.

To qualify the selection, write:

.select any STATE related to instances of STATECHART->State
    where (selected.isFinal == False)
The where clause is implicitly a loop. The current instance under examination is referred to as selected.

The traversals and qualifications may be arbitrarily complex:

.select any STATE related to instances of CLASS->StateChart->State
    where (selected.isFinal == False) &&
              (selected.Number > 1)
To select multiple instances from the repository, write:
.select many STATES related to instances of STATECHART->State
Note that the instance reference has a plural-style name. By convention, it is written in uppercase to increase its profile. The type of the variable STATES is an instance-reference set, which is to say, a set of instance references.

To iterate over the set, write:

.select many STATES related to instances of STATECHART->State
.for each STATE in STATES
    ${STATE.Name}
.end for
To invoke a translation template function, use ".invoke". Hence, we may write:
.select many CLASSES from instances of Class
.for each CLASS in CLASSES
    class ${CLASS.Name} : public ActiveInstance {
            private:
                   .invoke addAttrDecls( CLASS )
                   .// ....
    };
To define the invoked translation template function, write:
.function addAttrDecls( CLASS )
	.for each ATTR related to instances of CLASS->Attribute
                   ${ATTR.Type} ${ATTR.Name};
	.end for
.end function
A COMPLETE EXAMPLE FRAGMENT We now have a sufficient understanding of the basic elements of the translation template language to be able to generate some meaningful code. Listing 2 shows the translation templates necessary to generate the code shown in Listing 1.

These examples demonstrate the basic technique. We can continue to add translation templates for the remaining elements of the class, task headers, initialization structures, state charts, data structures, and so on. The same technique applies for any portion of the application that has been modeled in the metamodel and stored in the repository.

A VARIATION So far, the code that we've generated has been a fairly direct mapping from the metamodel. However, it is possible to construct translation templates for different architectures.

To support the one-to-one mapping of the executable UML into code, we have based the architecture on state charts. A review of the AutoSampler model reveals that, as it happens, we can determine the behavior of the system if we know the signal event, and we do not need to know the current state of the receiver of the signal event.#

When we can determine the behavior from the signal event, we can dispense with the state chart mechanisms completely, including the StateChart class, the static data member myStateChart, and the array of function pointers. We can declare the action sequence functions public instead of private, and name them with the name of terminating state. Instead of the sending class queuing a signal event, the sending class invokes the member function corresponding to the action sequence directly. This is a synchronous architecture, in contrast to the asynchronous architecture described first.

Listing 3 shows the translation template required to generate a signal event in the variation, and the action sequence member function declaration is shown in Listing 4.

To preserve the run-to-completion semantics of the UML metamodel definition, all signal events must be generated last in the action sequence. This implies that when there are multiple signal events in an action sequence, the thread of control follows the generation of a signal event to its completion before starting another.&

COMMENTARY ON THE VARIATION If implemented as stated, this scheme can only work if all signal events always uniquely determine the terminating state. A completely innocent modification of the application that violates this rule would cause the translation process to hairball. Such catastrophic failure would cause the application analyst to distort the model to meet the needs of the architecture or select a different one in disgust.

A better alternative is to modify the architecture to work in all cases. One approach is to use this variation only when it works, and to use the original scheme, with the StateChart class, when a signal event in a single state chart has multiple termination states. Unfortunately, this requires two sets of translation templates, one for the asynchronous original and one for the synchronous variation.

Using two approaches also requires some up-front integration effort. Because there are two ways to generate and receive a signal event, the architects must be certain than the system will work when we put the two schemes together. To verify this, we must examine the architecture both statically and dynamically.

Statically, in this generated code, the sender and receiver can use either scheme for signal event generation and receipt interchangeably because the code-generation process for both schemes operates on the same boundary. That is, the logical content of signal event generation and receipt is exactly the same, even though we use different schemes.

Dynamically, at runtime, we must guarantee that we preserve the order of signal events between sender and receiver object instance pairs. This we know to be true because the sender always uses the same approach to generate a signal event to a given receiver.

Whenever there are several ways of doing the same thing, we can verify that the system will integrate these various approaches at the same time that we build the architecture. This ensures that the selected schemes will work everywhere, not just in one place. The result is that integration and testing time drops dramatically. Further, any failure will be generated repeatedly, which often eases error detection. When we detect an error, however, one fix in the translation template fixes the problem in all locations.

A VARIATION ON THE VARIATION An alternative approach to generalizing the variation is to add state variables in each state, and use the state variables to determine which action sequence to execute. In this approach, we build a public member function for each event, which acts as a shell that selects which action sequence to invoke.

The shell comprises a switch against the complete set of states. In each branch of the switch, there may be only one (or zero) action sequence function. If there is an action sequence function, we update the current state first, then invoke the function. When two events with the same name terminate in the same state, the same action sequence member function will appear in two branches of the switch. Clearly, both these situations can be optimized in the translation template. Alternatively, we can leave it to the compiler to generate efficient code for these circumstances. The translation template appears as Listing 5.

Extending this variation one step further, we can replace the switch with a set of if statements. When an event terminates in just one state, there is no need to generate any test. If there is a sequence of states that do not need a test, there is also no need to update the state variable. However, when a test is needed, there is a need for a state variable, or perhaps we should call it a flag. For that is what a flag is—a way of determining system state to direct the flow of logic.

THE MODEL IS NOT THE IMPLEMENTATION The state chart formalism defines required sequencing and allows us to model a system in a clear, unambiguous, and object-oriented manner. But the state chart formalism doesn't need to be reflected directly in the code. All the implementation has to do is preserve the semantics of the problem, as defined by the state chart in particular and the UML model in general.

We have shown only a very limited set of variations. There are many, many variations possible: multitasking schemes, periodic tasks, multiprocessor schemes, threaded schemes, shared memory, and so on. In addition, there are many possible variations of the data organization. We could produce lists out of compositions, join one-to-one associations into a single class, produce redundant data to reduce access time, and so on. Then we can optimize data access: determine the navigability from the actions, and only generate links when necessary; build hash tables for multiple instances, depending on the access patterns; and so on.

Every choice of which variation to use depends on performance of the software in the face of a particular pattern of external events and the patterns of instantiations and deletions of instances. These choices do not depend on the application semantics.

GENERATING THE CODE FOR ACTION SEQUENCES The next article takes up the issue of generating code for the action sequences.

Reference
1. Mellor, S. J. "Automatic Code Generation from UML Models," C++ Report, 11(4):28-33, April 1999.


Footnotes
* The AutoSampler is a small example model that we used in the previous article. We will now generate code for that problem, so it would be helpful if you had those models available.

+ A one-to-one mapping is not required. If we did require a one-to-one mapping, we would have to enrich the executable UML to allow for all possible implementations, which would yield a very large modeling language and a correspondingly large metamodel.

¥ A signal event is a subtype of the more general class event in the metamodel of UML. The distinguishing features of a signal event are that it is asynchronous and immediate (i.e., not deferred).

§ The example ignores the problem of handling signal events that cause creation of an instance.

# We can determine what action to execute if each event always causes a transition to a single state.

& Warning: Synchronous architectures contain several traps, not all of which are described here!