hide all comments

DSM

Combining hand-written code with generated code

September 20, 2018 13:05:06 +0300 (EEST)

There are many ways to integrate hand-written code with generated code. The easiest end of the scale is also the most common: generated code that calls, references, subclasses or otherwise uses hand-written code. There, the two kinds of code can lead separate lives, just like any reusable component need not be aware of, nor should be tightly integrated with, code that uses it. The hand-written code isn't specific to a particular model. At the other end of the scale are cases where the models can't capture everything that might be needed, and we need to add hand-written code at specific points of specific models. Although there are often ways to avoid this, let's take it as the hardest case, and see how good tooling can cope palatably even there.

Diagram watch

Here's a diagram of a Stopwatch app. The watch can be in various states, and you can press buttons to cause actions and move to a new state. Actions are things like setting time variables or turning on an icon on the display. In short, the language lets you specify things from the point of view of an end-user of the watch, with the code generation and a little state machine framework taking care of the innards.

You can grab MetaEdit+ (and its Eclipse plugin if that's your IDE) to follow along. See the Watch Example for more details.

Models to Code

Let's take a look at the code, then. Select the top-level WatchModels graph from the Graph Browser pane in Eclipse, and from its pop-up menu choose to edit the Graph's Properties... and set the Generation target platform to 'Java: Linux'. Select and open the pop-up menu again and choose Run Autobuild. (If you're not on Eclipse, select WatchModels in the top MetaEdit+ window's Graph Browser tab, from its pop-up menu those pop-up menu choices are Properties... then Generate... | Autobuild.) The code will be generated (and either added to an Eclipse project if using the plugin or opened in a Generated Files window if not), compiled and run.

Generated Files

Java WatchModels Java Stopwatch

We can start the Stopwatch by choosing the Sporty WatchModel, then pressing Mode. As we can see from our model, pressing Up starts it running, and Up again stops it. Pressing Down will then reset it.

Freedom through code

If we look at the model for the Down button's action at the top, we can see it's a bit ugly: resetting startTime to zero by setting it to stopTime - stopTime. The 'official' Watch Example tutorial uses that as an excuse to improve the modeling language by adding constants, but here let's use it as an excuse to integrate some 'real' code. Let's have the model just specify a function to call, and we can then implement it in code to do what we want.

Diagram reset

Extending the modeling language

To follow these instructions, you should be familiar with MetaEdit+, e.g. by doing the 'Family tree' evaluation tutorial or the Watch tutorial.

Open a Graph Tool on Watch Application and Add New for the selected types — further details for the first two are below.

Graph Tool

The first will be our Function object type, just calling a single named function with no parameters.

Property Tool

Let's make sure the Function Name is a legal Java method name by giving it a Value Regex, [a-z]\w* :

Property Tool

The Run relationship will connect the Action to the Function. Let's give it a bold red 20px exclamation mark as its symbol, to show the Action executes the Function. Place a Point Connectable (last on the toolbar) at the bottom left: that's where the lines will run through.

Symbol Editor

On the Graph Tool's Bindings tab, Add a binding for our Run relationship, adding an ActionBody role connecting to an Action, and our Function role connecting to our Function object.

Graph Tool Bindings

Save in the Graph Tool and re-open the diagram, and now we can click Function on the toolbar and click in the diagram to add our 'reset' Function. Then click Run's ! icon on the toolbar and drag from the Action to 'reset' to connect them.

Diagram reset

Automating integration of generated and hand-written code

We want each Function to generate a Java method with an empty body. The Functions will come after the Variable getters and setters:

Generator Variables

newline
foreach .Function
{	subreport '_function' run
}

The _function subreport will output the current Function as a Java method. Use Generator | New... to create the _function generator. Since we want to be able to edit the generated code to add the actual body of the method, we use an MD5 block to produce a leading comment naming this block after the Function name, and a trailing comment giving the MD5 sum of the generated content (initially empty, but where we will add our manual code. And yes, this is a bit of an anti-pattern: ideally we'd keep that code in a separate file. That would actually make life easier, but let's prove a point by taking the hardest case first.).

Generator function

Report '_function'
'	public void ' id '() {
		' md5id id md5block
'		' md5sum
'	}
'
endreport

With our Function generation in place, we need to make sure each place a Function is used in the model calls that function. As each use occurs when a Run relationship is attached to the Function, we use Generator | New... to create a generator called _Run. It navigates along the Function role line to the Function object itself, to output the function's id (i.e. name), then parentheses and semicolon.

Generator Run

Report '_Run'
do ~Function.() {id} '();'
endreport

Finally, for MetaEdit+ to know to merge the model changes with the existing hand-edited code, we change the MERL commands for creating the Java file from write to merge. (It's probably easiest to find the _JavaFile generator if you switch above the list to the Alphabetical view.)

Generator _JavaFile

Trying out the manual code integration

Now we can generate the code for our model, build it and run it. Select the top-level graph in the Graph Browser pane in Eclipse and press the Run Autobuild button. Choose Sporty, press Mode to get to the Stopwatch application, and press Up a couple of times to start and stop the stopwatch. You'll then be back in the Stopped state, seeing the time you measured. For now, pressing Down in the Stopped state will not change the displayed time: it calls the reset method, but that has no body yet, so does nothing.

In Stopwatch.java, edit the reset method to add a line between the leading and trailing MD5 comments, to set stopTime to zero. (Note that we're editing a generated file: normally this is a no-no, but here the MD5 blocks will take care of things, maintaining our manual code but regenerating the rest from the model.)

		setstopTime(new METime());

Eclipse reset

Close the running Sporty and WatchModels. Rebuild the application with Autobuild, and try Sporty again with the Mode, Up, Up, Down steps, and see that Down now does reset the stopwatch time to zero.

Add some new functionality to the model itself, e.g. a Laptime state, Autobuild again and see that the hand-coded line in reset() is maintained.

Summary

So, we took an existing modeling language and its generators, and added a few things:

  1. Extended the modeling language with a new object, relationship and role type, their symbols and rules.
  2. Added the code generation for the new parts of the modeling language.
  3. Allowed the generated code to be manually edited in specific protected regions, which are maintained when the model changes and code is re-generated.