BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles RGen: Ruby Modelling and Code Generation Framework

RGen: Ruby Modelling and Code Generation Framework

This item in japanese

Bookmarks

Introduction

This article introduces the RGen framework [1] which supports a “Ruby way” of dealing with modelling and code generation. I use the term “modelling” in the sense of MDA/MDD [2] (but apart from that do not follow this approach very closely): Models are instances of metamodels which in turn are (closely related to) Domain Specific Languages (DSL). Model transformations are used to convert models into instances of different metamodels and code generation is a special kind of transformation that converts a model to textual output.

RGen has been strongly inspired by openArchitectureWare (oAW) [3] which is a Java framework with a very similar scope of application. The key idea behind RGen is to use Ruby not only for the implementation of application logic within the framework but also for the definition of metamodels, model transformations and code generation. RGen facilitates this by providing internal DSLs for each of the mentioned aspects. As other projects demonstrate, Ruby is very well suited for this approach. A popular example is Ruby on Rails [4] which includes several internal Ruby DSLs. In contrast to that, oAW uses external DSLs for the definition of model transformations and code generation.

Experience shows that the RGen approach is extremely lightweight and flexible and thus allows for efficient development and simple deployment. I found this especially helpful in a consulting project where it turned out that we were lacking tool support but tool development was not planned. Using Ruby and RGen we started the required tool with minimal effort to get things going and then convinced people that they would greatly benefit from developing this tool.

Typical applications for frameworks like RGen and oAW are code generators (e.g. for embedded devices) and tools for building and manipulating models, often represented as XML or an own textual or graphical language. In this article I will use a “UML State Chart to C++ code generator” as an example. In the real world we are still using RGen in the project mentioned above to build models and generate code for automotive embedded Electronic Control Units (ECU).

Model and Metamodel

The most basic aspect of a modelling framework is the ability to represent models and metamodels. A metamodel describes what models for a specific purpose may look like, thus defining the abstract syntax of the domain specific language. Typically, an application of a modelling framework involves the use of several metamodels and model transformations between them.

RGen employs a straight forward representation of models and metamodels in Ruby as an object oriented programming language: objects are used to represent model elements and classes are used for metamodel elements. The relationship between models, metamodels and their Ruby representation is shown in Figure 1.

In order to support domain specific languages, a modelling framework must allow for custom metamodels. RGen facilitates this by providing a metamodel definition language, which can be seen as a domain specific language by itself (for the domain of modelling). Just as for any other DSL, the abstract syntax of the metamodel definition language is defined by its metamodel, in this case referred to as meta-metamodel.

Figure 1: Model, Metamodel and its Ruby representation

Unlike the metamodels, the meta-metamodel is fixed in RGen. The framework uses ECore, the meta-metamodel of the Eclipse Modelling Framework (EMF) [5]. Figure 2 shows a simplified view of the ECore metamodel: In ECore, metamodels basically consist of classes which are organized in hierarchical packages and have attributes and references to other classes. A class may be derived from multiple superclasses. References are unidirectional but may be linked with an opposite reference, to make them bidirectional. The target of a reference is its type, which must be a class; the role of the target is the reference’s name. Attributes are instances of (non-class) data types, which may be primitive types or enumerations.

Figure 2: Simplified view of the ECore metamodel

In contrast to other frameworks like oAW, the concrete syntax of RGen’s metamodel definition language is plain Ruby, making it an internal DSL. Listing 1 shows a simple statemachine metamodel: The code uses regular Ruby keywords to define a module1 and several classes representing a metamodel package and metamodel classes respectively. In order to distinguish these elements from regular Ruby classes and modules some additional code is required: The module is extended by a special RGen module extension (1) and the classes are derived from the RGen metamodel base class MMBase (2).

The superclass relationship of metamodel classes is represented by Ruby class inheritance (3). Note that Ruby does not support multiple-inheritance natively, but due to its flexibility, this feature can be provided by a special RGen command. In this case the respective class must be derived from MMMultiple(,,…), which is a global method building an intermediate superclass on the fly.

# Listing 1: Statemachine metamodel example

module StatemachineMetamodel
  extend RGen::MetamodelBuilder::ModuleExtension         # (1)

  
  class ModelElement < RGen::MetamodelBuilder::MMBase    # (2)
    has_attr 'name', String

  end
  
  class Statemachine < ModelElement; end                 # (3)

  class State < ModelElement; end

  class SimpleState < State; end

  class CompositeState < State; end

  
  class Transition < ModelElement
    has_attr 'trigger', String                           # (4)

    has_attr 'action', String
  end
  
  Statemachine.contains_one_uni 'topState', State        # (5)

  Statemachine.contains_many_uni 'transitions', Transition

  CompositeState.contains_many 'subStates', State, 'container'

  CompositeState.has_one 'initState', State

  State.one_to_many 'outgoingTransitions', Transition, 'sourceState'

  State.one_to_many 'incomingTransitions', Transition, 'targetState'
end

Metamodel attributes and references are specified using special class methods provided by MMBase. This is possible since Ruby class definitions are interpreted when the corresponding code is evaluated. Within the scope of a class definition, the current object is the class object and so methods on the class object can be called directly2.

The has_attr method is used to define attributes. It takes the attribute’s name and a primitive Ruby data type3 (4). RGen internally maps the Ruby types to ECore primitive types, in this case to EString. For the specification of references, several methods are available. contains_one_uni and contains_many_uni define unidirectional containment relationships to one or many target classes respectively. contains_one and contains_many are the bidirectional counterparts. has_one and has_many are used for regular unidirectional references whereas one_to_one, one_to_many and many_to_many are their bidirectional counterparts. These methods are called on the source class object, with the target role as first argument, followed by the target class and the source role in case of bidirectional references (5). Note that the target class needs to be defined before to be able to reference it. For this reason in most RGen metamodels the references are defined outside of and after the class definitions.

When a model is actually created, the metamodel Ruby classes are instantiated. Attribute values and reference targets are stored in instance variables of the respective objects. As Ruby does not allow direct access to instance variables, accessor methods are used. In idiomatic Ruby, getter methods are named like the corresponding variables and setter methods have the same name with a trailing “=”. Such setter methods can be used on the left hand side of an assignment.

The class methods described above build the required accessor methods dynamically by means of meta-programming. Besides the mere access to the instance variable, these methods check the type of their arguments and throw an exception in case of a violation. This way runtime type checking is built on top of Ruby, which does not do this kind of checks natively4. In case of to-many references, the getter returns an array and there are setter methods to add and remove referenced objects. In case of bidirectional references, the accessor methods automatically add and remove the opposite references on the other side of the relationship.

Listing 2 shows an example: The regular Ruby class instantiation mechanism (1) can be used to create an object as well as a special constructor for conveniently setting attributes and references (4). Note that the package name is required to qualify the class names. By including the package module into the current namespace, repetition can be avoided (3). The name attribute of state s1 is set and the result of the getter method is checked (2). An outgoing transition is added to s1 (to-many) and the automatic creation of the backward-reference (to-one) is checked (5). The transition’s target state is set to s2 by explicitly assigning a value (to-one) and the array result is asserted to contain one element t1 (to-many) (6). A second target state is created and connected to the source state using another transition. Finally the target states of all outgoing transitions of s1 are asserted to be s2 and s3 (7). Note that the method targetState is called on the result of outgoingTransitions which is an array. This concise notation is possible since RGen extends the Ruby array by the ability to relay calls to unknown methods to the contained elements and collect the outputs into a single set.

# Listing 2: Example instantiation of the statemachine metamodel

s1 = StatemachineMetamodel::State.new                    # (1)
s1.name = 'SourceState'                                  # (2)

assert_equal 'SourceState', s1.name

include StatemachineMetamodel                            # (3)

s2 = State.new(:name => 'TargetState1')                  # (4)

t1 = Transition.new
s1.addOutgoingTransitions(t1)                            # (5)
assert_equal s1, t1.sourceState

t1.targetState = s2                                      # (6)
assert_equal [t1], s2.incomingTransitions

s3 = State.new(:name => 'TargetState2')
t2 = Transition.new(:sourceState => s1, :targetState => s3) # (7)

assert_equal [s2,s3], s1.outgoingTransitions.targetState

As show above, the RGen metamodel definition language creates the modules, classes and methods required to represent the metamodel in Ruby. Besides this however, the metamodel itself is available as a regular RGen model. This is possible since RGen includes a version of the ECore metamodel expressed using its own metamodel definition language. The metamodel RGen model can be accessed using the ecore method on a Ruby class or module representing a metamodel element.

Listing 3 shows an example: Calling the ecore method on the StatemachineMetamodel module which represents a package results in an instance of EPackage (1), calling it on the State class results in an instance of EClass (2). Note that both, the EPackage and the EClass belong to the same model, in fact the EClass named “State” is one of the classifiers within the EPackage named “StatemachineMetamodel” (3). The metamodel RGen model can be navigated just as any RGen model. The example code asserts that the superclass of class “State” has an attributed named “name” (4).

# Listing 3: Accessing the ECore metamodel

smPackage = StatemachineMetamodel.ecore
assert smPackage.is_a?(ECore::EPackage)                  # (1)
assert_equal 'StatemachineMetamodel', smPackage.name 

stateClass = StatemachineMetamodel::State.ecore
assert stateClass.is_a?(ECore::EClass)                   # (2)

assert_equal 'State', stateClass.name

assert smPackage.eClassifiers.include?(stateClass)       # (3)

assert stateClass.eSuperClasses.first.eAttributes.name.include?('name') # (4)

Being a regular model, the metamodel model can be serialized and instantiated by any available serializer/instantiator. RGen includes a XMI serializer and a XMI instantiator which allow to exchange metamodels with EMF. Similarly, the metamodel model can be source or target of an RGen model transformation, e.g. to or from a UML class model. Model transformation is covered in the next section. Finally, the metamodel model can be turned back into its RGen metamodel DSL representation using the RGen metamodel generator. Figure 3 summarizes the different metamodel representations and their relationships.

Figure 3: RGen metamodel representation summary

The RGen metamodel model provides reflection capabilities on metamodels similar to EMF. Having reflection at hand is useful for programmers in numerous situations, e.g. when implementing a custom model serializer or instantiator. The fact that the meta-metamodel is ECore ensures exchangeability with a number of existing modelling frameworks: using the RGen metamodel generator any ECore metamodel can directly be used within RGen.

Listing 4 shows the serialization of the metamodel model to XMI (1) as well as the usage of the metamodel generator to regenerate the RGen DSL representation (2). Note that in both cases, the metamodel is referred to by the root EPackage element returned by the ecore method of StatemachineMetamodel. The generated DSL representation of the metamodel is read from file and evaluated (3). In order to avoid name clashes between the original classes and modules and the reloaded ones, the evaluation is done within the scope of another module Regenerated which acts as a namespace. Apart from the values of the “instanceClassName” attribute which include the additional namespace in the reloaded version, both models are equivalent (4).

# Listing 4: Serializing the metamodel

File.open("StatemachineMetamodel.ecore","w") do |f|
  ser = RGen::Serializer::XMI20Serializer.new
  ser.serialize(StatemachineMetamodel.ecore)             # (1)

  f.write(ser.result)
end

include MMGen::MetamodelGenerator

outfile = "StatemachineModel_regenerated.rb"
generateMetamodel(StatemachineMetamodel.ecore, outfile)  # (2)

module Regenerated
  Inside = binding
end
      
File.open(outfile) do |f|
  eval(f.read, Regenerated::Inside)                      # (3)

end

include RGen::ModelComparator
    
assert modelEqual?( StatemachineMetamodel.ecore,         # (4)
  Regenerated::StatemachineMetamodel.ecore, ["instanceClassName"])

At present, RGen provides only limited support for dynamic changes of the metamodel at runtime. In particular, changes to the ECore metamodel model do not affect the Ruby metamodel classes and modules in memory. However, there is ongoing work on a dynamic version of the Ruby metamodel representation. This dynamic metamodel consists of dynamic classes and modules which are tied to EClass and EPackage ECore elements. When the ECore elements are modified the dynamic classes and modules instantly change their behaviour even if instances already exist. This allows for advanced techniques in model transformation as shown in the next section.

One of the big advantages of the RGen approach is the flexibility gained by using internal DSLs and the tight coupling to the Ruby language. This allows for example to programmatically create the metamodel classes and modules and call the class methods to create attributes and references. An application making use of this is a generic XML instantiator which creates the target metamodel on the fly depending on the XML tags and attributes encountered and a set of rules mapping these to metamodel elements. The RGen distribution includes a prototypical version of such an instantiator.

Another interesting possibility enabled by the internal DSL approach is to embed metamodels into regular code. This can be very helpful when the code needs to deal with complex, inter-connected data structures. The developer may think of the structures in terms of (metamodel) classes, attributes and references and program them within the scope of the affected host class or module. By using the metamodel approach, the developer also decides to have attribute and reference types automatically checked at runtime5.

Model Transformation

Many real world modelling applications can benefit from using several metamodels. As an example, an application may have an internal metamodel as well as several input/output specific metamodels. Model transformation is used to convert an instance of one metamodel into an instance of another metamodel as depicted in Figure 4.

Figure 4: Model transformation

In case of the Statemachine example introduced above, input of UML 1.3 Statechart models can be added by means of a model transformation. RGen includes a version of the UML 1.3 metamodel expressed in RGen’s metamodel DSL. It also includes an XMI instantiator which allows to create an instance of the UML metamodel directly from an XML file stored by a compatible UML tool. Figure 5 shows an example input statechart.taken from [6].

Figure 5: Example UML statechart

Apart from creating a new target model, a model transformation may alternatively modify the source model. This case of in-place model transformation requires that the metamodel elements can be changed while the transformation takes place. This is currently not possible with RGen, but as already mentioned above, there is ongoing work to support such dynamic metamodels.

As an example, in-place model transformation could be used in tools which need to be able to read older versions of their input models for backward compatibility. Every change of the input metamodel in a new version of the tool could be accompanied with a built-in in-place model transformation. Each such transformation would typically comprise only a few changes to the metamodel and the model, but it may need to be applied to large amounts of data. Using a series of in-place transformations all operating on the same source model, the input model migration can be very efficient6.

Similar to the metamodel definition DSL explained above, RGen provides an internal DSL for defining model transformations. RGen model transformation specifications consist of transformation rules for single metamodel classes of the source metamodel. The rules specify the target metamodel class as well as the assignment of target attributes and references, where the latter typically incorporates the results of applying transformation rules.

Figure 6 shows the definition and application of transformation rules at an example7: The rule for source metamodel class A specifies the target metamodel class A’ and defines the assignment of the multiple reference b’ and the single reference c’. The target value of b’ is defined to be the result of the transformation of the elements referred to by the source reference b. This means that the corresponding rule for metamodel class B (see below) is applied (b’:=trans(b)). In RGen, the transformation result of an array of elements is an array of the results of each element’s transformation. Similarly, the value of c’ is assigned to be the transformation result of the element referred to by c (c’:=trans(c)). The transformation rule for metamodel class C in turn specifies the values of  references b1’ and b2’ to be the transformation results of the elements referred to by the source references b1 and b2 (b1’:=trans(b1), b2’:=trans(b2)). For the transformation of metamodel class B, no further assignments are necessary in this example.

Figure 6: Definition and application of transformation rules

As an internal DSL the model transformation language employs plain Ruby as concrete syntax. Each model transformation is defined within a Ruby class derived from the RGen Transformer class by means of special class methods. Most important, the transform class method defines a transformation rule, taking the source and target metamodel class objects as arguments. The assignment of attributes and references is specified by a Ruby Hash object mapping the names of attributes and references to the actual target objects. The Hash object is created by a code block associated with the transform  method call which evaluates in the context of the source model element.

Note that transformation rules can recursively use other rules. The RGen transformer mechanism takes care that the overall evaluation stops by caching the results of individual transformations. The execution of a rule’s code block is completely finished before the code blocks of any recursively used rules are executed. This deterministic behaviour is especially important when custom code is added to the code blocks.

Listing 5 shows a model transformation from the UML 1.3 metamodel to the example statechart metamodel introduced above: A new class UmlToStatemachine is derived from the RGen Transformer class and the target metamodel module is included in the current namespace in order to keep target class names short (1). A regular Ruby instance method (in this example named transform) acts as transformation entry point. It calls the trans transformer method, triggering the transformation of all statemachine elements in the input model8 (2). The trans method looks up transformation rules defined using the transform class method starting with the class of the source object and walking up the inheritance hierarchy if no rule is found. There is a transformation rule for UML13::StateMachine, which specifies that such elements are to be transformed into instances of Statemachine (3). Note that both, the source and the target metamodel classes are regular Ruby class objects and the Ruby namespace mechanisms must be used. The code block attached to this call of transform creates a Hash object assigning values to the attribute “name” and the references “transitions” and “topState”. The values are calculated by calling the accessor methods on the source model element which automatically is the context of the code block. For the reference target values the trans method is called recursively.

# Listing 5: Statemachine model transformation example

class UmlToStatemachine < RGen::Transformer              # (1)
  include StatemachineMetamodel

    
  def transform
    trans(:class => UML13::StateMachine)                 # (2)

  end
  
  transform UML13::StateMachine, :to => Statemachine do

    { :name => name, :transitions => trans(transitions), # (3)
      :topState => trans(top) }
  end

  
  transform UML13::Transition, :to => Transition do
    { :sourceState => trans(source), :targetState => trans(target),
      :trigger => trigger && trigger.name,
      :action => effect && effect.script.body }
  end

  
  transform UML13::CompositeState, :to => CompositeState do
    { :name => name,
      :subStates => trans(subvertex),
      :initState => trans(subvertex.find { |s|
        s.incoming.any?{ |t| t.source.is_a?(UML13::Pseudostate) && # (4)

        t.source.kind == :initial }})}
  end
  
  transform UML13::StateVertex, :to => :stateClass, :if => :transState do # (5)

    { :name => name, :outgoingTransitions => trans(outgoing),
      :incomingTransitions => trans(incoming) }
  end

  
  method :stateClass do
    (@current_object.is_a?(UML13::Pseudostate) &&        # (6)

      kind == :shallowHistory)?  HistoryState : SimpleState
  end
  
  method :transState do

    !(@current_object.is_a?(UML13::Pseudostate) && kind == :initial)
  end
end

Since almost any Ruby code can be used within the code block creating the Hash object, very powerful assignments are possible: Composite states in the example target metamodel have an explicit reference to the initial state whereas the initial state in the source metamodel is marked by having an incoming transition from an “initial” pseudo state. This transformation is realized using Ruby’s built-in Array methods by first finding a substate which has such an incoming transition and then transforming it using the trans method (4).

Instead of a target class object, the transform method can optionally take a method, calculating the target class object. In the example, an UML13::StateVertex should either be transformed into a SimpleState or into a HistoryState, depending on the result of the method stateClass (5). With a further optional method argument the rule can be made conditional. In the example, the rule is not used for the initial pseudo state and nil will be the result since no other rule applies. Besides regular Ruby methods, the Transformer class provides the method class method which allows to define a kind of method whose body evaluates in the context of the current transformation source object (6). In case of ambiguity, the current transformation source object can also be accessed using the @current_object instance variable.

Since a call of the transform class method is regular code, it can also be called in more sophisticated ways, allowing to “script” the definition of transformations. A good example is the implementation of the copy class method of the Transformer class itself shown in Listing 6: The method takes a source and optionally a target metamodel class assuming that they are either the same or have the same attributes and references. It then calls the transform class method with a code block which automatically creates the right assignment hash for every given source object by looking up its attributes and references via metamodel reflection9.

# Listing 6: Implementation of the transformer copy command

def self.copy(from, to=nil)
  transform(from, :to => to || from) do
    Hash[*@current_object.class.ecore.eAllStructuralFeatures.inject([]) {|l,a|
      l + [a.name.to_sym, trans(@current_object.send(a.name))]
    }]
  end

end

The copy class method can also be applied for every class of a metamodel. This is a generic way to create a copy transformer for a given metamodel which can be used to make a deep copy (clone) of an instance of that metamodel. Listing 7 shows an example copy transformer for the UML 1.3 metamodel.

# Listing 7: Example copy transformer for the UML 1.3 metamodel

class UML13CopyTransformer < RGen::Transformer

  include UML13

  def transform
    trans(:class => UML13::Package)
  end

  UML13.ecore.eClassifiers.each do |c|
    copy c.instanceClass 
  end
end

Another interesting application of the transformer mechanism within the RGen framework is the implementation of metamodel reflection described above. When the ecore method is called, the receiving class or module is fed into the built-in ECore transformer, which applies rules for the transformation of attributes and references. This is possible since the mechanism is flexible enough to not only use metamodel classes but also plain Ruby classes as the “input metamodel”.

Code Generation

Besides the transformation and modification of models, code generation is another important application of the RGen framework. Code generation can be regarded as a special kind of transformation turning a model into textual output.

The RGen framework includes a template based generator mechanism which is very similar to the one that comes with oAW. Both, the RGen and oAW solution differ from many other template based approaches in the relationship of templates, template files and output files: A template file may contain several templates, a template may create several output files and the content of an output file may be produced by several templates.

Figure 7 shows an example: Within file “fileA.tpl” two templates “tplA1” and “tplA2” are defined, within file “fileC.tpl” template “tplC1” is defined (keyword define). Template “tplA1” creates an output file “out.txt” (keyword file) and generates a line of text to it. It further expands the contents of template “tplA2” and “tplC1” into the same output file (keyword expand). Since template “tplC1” resides in a different file, its name must be prepended with the relative path of the template file.

Figure 7: RGen generator templates

When RGen templates are expanded, their content is evaluated within the context of a context model element. Every template is associated with a metamodel class at definition time using the :for attribute and can only be expanded for context elements of that type. By default, the expand command expands templates in the current context, but different context elements can be specified using the :for or :foreach attribute. In the latter case an array is expected and the template is expanded for every element of the array. Templates may also be overloaded by specification of a different context type and expand automatically chooses the right template for a given context element.

The RGen template mechanism is built upon ERB (Embedded Ruby), which provides basic template support for Ruby. ERB is part of the standard Ruby distribution and allows to embed Ruby code within arbitrary text using the tags <%, <%= and %>. The RGen template language consists of the ERB syntax plus additional keywords implemented as regular Ruby methods. This way, the template language makes up another internal DSL within RGen. Building upon the standard ERB mechanism, the implementation is very lightweight.

One major issue with code generation is the formatting of the output: Since the template itself should be readable to the developer, additional whitespace is added which later disturbs the readability of the output. Some approaches address this problem by feeding the output through a pretty printer. This however takes more time and requires an additional tool which might not be available for certain kinds of output.

The RGen template language provides a simple means to format the output without any additional tool: By default, whitespace in the beginning of lines and empty lines are removed. The developer may then control the indentation and the creation of empty lines by explicit RGen commands: iinc and idec are used to set the current indentation level and nl is used to insert a blank line. Experience shows that the effort required to add to the formatting commands is tolerable. In particular when special kinds of output need to be formatted, this approach is very useful.

Listing 8 shows a complete template from the statechart example. It is used to generate the header file of an abstract C++ class created for every composite state. Following the State Pattern and [6], a state class will be derived from this class for every substate.

# Listing 8: Statemachine generator template example

<% define 'Header', :for => CompositeState do %>         # (1)
  <% file abstractSubstateClassName+".h" do %>

    <% expand '/Util::IfdefHeader', abstractSubstateClassName %> # (2)
    class <%= stateClassName %>;
    <%nl%>

    class <%= abstractSubstateClassName %>               # (3)
    {
    public:<%iinc%>                                      # (4)
      <%=abstractSubstateClassName%>(<%=stateClassName%> &cont, char* name);
      virtual ~<%= abstractSubstateClassName %>() {};
      <%nl%>

      <%= stateClassName %> &getContext() {<%iinc%>
        return fContext;<%idec%>

      }
      <%nl%>
      char *getName() { return fName; };
      <%nl%>
      virtual void entryAction() {};
      virtual void exitAction() {};
      <%nl%>

      <% for t in (outgoingTransitions + allSubstateTransitions).trigger %> # (5)
        virtual void <%= t %>() {};
      <% end %>

      <%nl%><%idec%>
    private:<%iinc%>
      char* fName;  
      <%= stateClassName %> &fContext;<%idec%>

    };
    <% expand '/Util::IfdefFooter', abstractSubstateClassName %>
  <% end %>

<% end %>

The template starts by defining the name and the context metamodel class and opening a new output file (1). All C/C++ header files should start with a guard protecting against duplicate inclusion, which normally contains the filename in all uppercase letters. The template “IfdefHeader” produces this guard for a given filename (2). In the next lines, the class definition is started (3) and the indentation level is incremented after the “public” keyword (4). Besides several infrastructure methods, a virtual method for every outgoing transition of every substate need to be declared. This is realized by a simple Ruby for-loop iterating over all relevant transitions (5). Within the body of the for-loop the method declaration is created. In order to write the value of the trigger attribute to the output <%= is used instead of <%. At the end of the template the footer part of the duplicate inclusion guard is added.

In general, all template output that is not copied verbatim is created from the information represented by the model. Attribute accessor methods of the context model element can be read directly, other model elements can be navigated via the reference accessor methods. Again, the possibility to call methods on arrays of model elements is very useful.

In many cases however, the information from the model needs to be processed before it can be used to generate output. If a calculation is more complex or should be used in different locations, it is often advisable to implement it as a separate method. In the above example, stateClassName, abstractSubstateClassName and allSubstateTransitions are  derived attributes / derived references available as methods of the metamodel class CompositeState.

Such derived attributes and references could be implemented as regular Ruby methods of the metamodel class. However, as RGen supports multiple inheritance of metamodel classes, special care must be taken. User defined methods must only be added to a special “class module” which is part of every RGen metamodel class and can be accessed by means of the constant ClassModule.

# Listing 9: Example statemachine metamodel extension

require 'rgen/name_helper'

module StatemachineMetamodel

  include RGen::NameHelper                               # (1)

  module CompositeState::ClassModule                     # (2)
    def stateClassName

      container ? firstToUpper(name)+"State" : firstToUpper(name) # (3)
    end
    
    def abstractSubstateClassName

      "Abstract"+firstToUpper(name)+"Substate"
    end
    
    def realSubStates

      subStates.reject{|s| s.is_a?(HistoryState)} 
    end
    
    def allSubstateTransitions
      realSubStates.outgoingTransitions +                # (4)
        realSubStates.allSubstateTransitions
    end

  end
end

Listing 9 shows the implementation of the derived attributes and reference of the example: First the StatemachineMetamodel package module is opened and a helper module is mixed-in (1). Within the package module, the class module of the CompositeState class is opened (2). The method implementation takes place in the class module making use of the regular accessor methods, other derived attributes and references and possibly methods from mixed-in modules (3). The method implementation of allSubstateTransitions is recursive and strongly benefits from the RGen feature that allows to call element methods on arrays (4).

Note that the methods in Listing 9 are not defined in the same file as the original metamodel classes. Instead, Ruby’s “open classes” feature is used to contribute the methods from within a different file. Although it is possible to put the methods in the same file, it is often advisable to use one or more metamodel extension files. This way the metamodel is not “littered” with helper methods which often are useful for a very specific purpose only.

In the example the extension file is named “statemachine_metamodel_ext.rb” and the extension methods are used for code generation. Depending on the project size in a real life case, it may be useful to have a “statemachine_metamodel_ext.rb” for general extensions and a “statemachine_metamodel_gen_ext.rb” for generator specific extensions. By keeping the second file with the template files, the generator logic can be cleanly separated.

In order to start code generation, the template files must be loaded and the root template needs to be expanded. Listing 10 shows how this is done in the statechart example: First an instance of DirectoryTemplateContainer is created (1). The container needs to know the output directory, i.e. the directory the output files (keyword file) are created in. It also needs to know the metamodel(s) serving as namespace for references to metamodel classes within the templates. Next the templates are loaded by specifying the template directory (2). In the example the model elements used for code generation were filled into an RGen environment by the preceding model transformation. The root context elements (i.e. instances of the metamodel class Statemachine) are retrieved from the environment (3) and code generation is started by expanding the root template for each of them (4).

# Listing 10: Starting the generator

outdir = File.dirname(__FILE__)+"/../src"
templatedir = File.dirname(__FILE__)+"/templates"

tc = RGen::TemplateLanguage::DirectoryTemplateContainer.new( # (1)
   StatemachineMetamodel, outdir)

tc.load(templatedir)                                     # (2)

stateMachine = envSM.find(:class => StatemachineMetamodel::Statemachine) # (3)

tc.expand('Root::Root', :foreach => stateMachine)        # (4)

Listing 11 shows a part of the final generator output: The file “AbstractOperatingSubstate.h” is generated by the template show in Listing 8 for the “Operating” state of the example model. Note that no additional post processing is necessary to achieve this result.

// Listing 11: Example C++ output file "AbstractOperatingSubstate.h"

#ifndef ABSTRACTOPERATINGSUBSTATE_H_
#define ABSTRACTOPERATINGSUBSTATE_H_

class OperatingState;

class AbstractOperatingSubstate
{
public:
   AbstractOperatingSubstate(OperatingState &context, char* name);
   virtual ~AbstractOperatingSubstate() {};

   OperatingState &getContext() {
      return fContext;
   }

   char *getName() { return fName; };

   virtual void entryAction() {};
   virtual void exitAction() {};

   virtual void powerBut() {};
   virtual void modeBut() {};


private:
   char* fName;
   OperatingState &fContext;
};

#endif /* ABSTRACTOPERATINGSUBSTATE_H_ */

 

Application Notes

Ruby is a language which lets the programmer express her ideas in a simple and concise way, thus allowing for efficient development of well maintainable software. RGen adds modelling and code generation support to Ruby, enabling the developer to deal with models and code generation in a similarly simple, Ruby-like way.

Following the principle of simplicity, the RGen framework is lightweight in terms of size, dependencies and rules opposed on the developer. This leads to a great amount of flexibility allowing to use the framework for everyday scripting just as well as for large applications.

As a dynamic, interpreted language Ruby brings some inherent differences in development. One of the most prominent is the missing compiler support regarding type checking. Another one is that editor support like auto completion is typically available only on a minimal level. This applies to RGen just as well, since it fully relies on the Ruby language features. In particular, frameworks like oAW using external DSLs can provide better editor support.

Ruby developers just as those of other dynamically typed languages like Python and Smalltalk have arranged with these disadvantages very well: Missing compiler checks are typically compensated by more intensive (unit) testing, which is a good strategy anyway. Missing editor support can partly be outweighed by making use of those dynamic language features which make good editor support so hard to build in the first place.

Especially in larger projects though, the power of those features needs to be harnessed. This has to be done by programmers by adding (runtime) checks where required. However, the language itself supports this task as it allows to define project specific checking DSLs.

The RGen metamodel definition language may be regarded as such a DSL: It can be used to define attribute and reference types which are checked at runtime. This means that an RGen metamodel can serve as the skeleton of a large Ruby application. The metamodel also supports the common understanding of the developers, especially when visualized using ECore or UML compliant or even graph visualization tools. The type checks added by RGen are one step to make sure that the core data of the program, the model, is in a consistent state.

RGen started out a couple of years ago as an experiment of bringing together the modelling domain and the Ruby language. As mentioned in the introduction, it has been successfully used for a prototype tool during a consulting project within automotive industry. By now this prototype tool has matured and is currently being used for the regular development of automotive electronic control units (ECU).

Within this tool, several metamodels are used with the largest containing around 600 metamodel classes. As an example, one of our models consist of about 70000 elements which are loaded, transformed and finally turned into about 90000 lines of output code within approximately one minute. Experience shows that in this specific domain, the RGen based approach can easily compete with other Java or C# based approaches, both in terms of runtime and memory usage. As an additional benefit the tool can be deployed as a 2.5MB executable which includes the Ruby interpreter and runs on the host system without installation10.

Although RGen itself is based on internal Ruby DSLs, it does not yet support the programmer in creating the concrete syntax of new internal Ruby DSLs. It also does not provide support for the concrete syntax of external DSLs like generating parsers or grammars. Currently, instances of custom metamodels (the abstract syntax) need to be created either programmatically as shown in this article or by means of existing or custom instantiators. This topic is certainly subject to future improvements.

The complete source code of the example application used within this article is available on the RGen project page [1].

Summary

The Ruby based RGen framework provides support for dealing with models and metamodels, for defining model transformations and for generating textual output. It is tightly coupled with the Ruby language as it uses internal DSLs. Following the Ruby design principles, it is lightweight and flexible and supports efficient development by providing means to write concise, maintainable code.

RGen is successfully being used in an automotive industry project as the basis for a modelling and code generation tool which evolved from a simple prototype. Experience shows the efficiency of the approach despite of the disadvantages often mentioned for languages like Ruby as missing compiler checks and editor support. It also shows that good performance in terms of runtime and memory usage can be achieved although Ruby is an interpreted language.

Besides the productive use of RGen, the framework is still used for experiments. One extension currently under development is support for dynamic metamodels which can be changed at runtime while instances already exist.

References

[1]       RGen, http://ruby-gen.org

[2]       Model Driven Architecture, http://www.omg.org/mda

[3]       openArchitectureWare, http://www.openarchitectureware.org

[4]       Ruby on Rails, http://www.rubyonrails.org

[5]       Eclipse Modelling Framework, http://www.eclipse.org/modeling/emf

[6]       Iftikhar Azim Niaz and Jiro Tanaka , "Code Generation From Uml Statecharts", Institute of Information Sciences and Electronics, University of Tsukuba, 2003

[7]       rubyscript2exe, http://www.erikveen.dds.nl/rubyscript2exe



1 Ruby modules are commonly used as namespaces and for grouping methods, allowing mix-ins into classes.

2 This is one of several ways to implement DSLs in Ruby which is also used by Ruby on Rails. In Rails applications, subclasses of ActiveRecord::Base are used to define metamodels in a way similar to RGen. In contrast to ActiveRecord, RGen metamodels are not tied to a database and use ECore as meta-metamodel.

3 In Ruby there is no distinction between classes and data types as almost everything is an object. Classes like String, Integer or Float serve as primitive types.

4 Ruby allows for duck typing, which means that not an object’s type but its methods make it the right or wrong object for a specific purpose.

5 Also in a dynamically typed language like Ruby type checks are important. However in contrast to a statically typed language the developer can decide which checks are necessary and which are not.

6 In fact, database migrations in Ruby on Rails do pretty much the same thing: Each migration may modify the database content as well as the schema. A series of migration steps brings the database content to a specific schema (or metamodel) version

7 In order to simplify the example, the structure of the source and target model/metamodel is identical, only the class and reference role names differ. In a typical application this is not necessarily the case.

8 When an instance of a transformer is created, the constructor takes references to the source and target model represented by RGen Environment objects which are basically arrays of all elements of the respective model.

9 The inject array method is called on an array with all attributes and references (ECore „features“). It passes each element to the block together with the block result of the last call. This way, an array containing feature names and values is built and finally passed to the hash [] class method which creates a new hash object.

10 rubyscript2exe [7] is used to „compile“ the program, the interpreter and the required libraries into a Windows executable which automatically extracts and runs its content on startup.

Rate this Article

Adoption
Style

Hello stranger!

You need to Register an InfoQ account or or login to post comments. But there's so much more behind being registered.

Get the most out of the InfoQ experience.

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Community comments

  • Article is really worth reading

    by Christian Tschenett,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    Thanks. This article is really worth reading. I am looking forward to hear more from RGen in the future (maybe an included UML 2.x metamodel). I'll definitey give RGen a try.

  • Interesting article on a tool which connects ruby - MDA/MDD

    by Martin Karlsch,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    Nice article - especially interesting because there aren't any good libraries / tools for ruby which connect it to the MDA/MDD world. I'll give it a try as soon as possible.

    What is your solution for inter model referencing?

    I created something similar for the Python world two years ago but not that advanced. It was a part of my master thesis ( called Frodo) and had a slightly different focus. It supported the loading of ecore models and the definition of a concret syntax. If RGen was release by that time, it would have been a great starting point for further experiments/contributions :-).

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

BT