This version is in HTML, a PDF version can be found in Chapter 7 of
Volume 2: Software Architecture (Local PDF) or
.(Ptolemy Web Site PDF)
Authors: Man-Kit Leung
Gang Zhou
Contributor: Christopher Brooks
So the code template file contains code blocks written in the target language. The target code blocks are hand-coded so users have flexibility in choosing their design styles and algorithms. Hand-coded templates also retain readability in the generated code. The codegen kernel uses the java class of the helper to harvest code blocks from the code template file. The java class of the helper determines which code blocks to harvest based on the actor instance-specific information (e.g., port type, port width, and parameter values). The code template file contains codegen macros that are processed by the codegen kernel. These macros allow the kernel to generate customized code based on the actor instance-specific information.
A C code template file has a .c file extension but it is not C-compilable due to its unique structure. Only the CodeStream object understands how to parse and use these files. Figure 7.2
shows the C code template file for the CountTrues helper, located in $PTII/ptolemy/codegen/c/domains/sdf/lib.
A C code template file consists of one or more C code blocks. Each code block has a header and a footer. The helper uses a CodeStream object to parse the code blocks. Please refer to Appendix C for a detailed documentation of the CodeStream object. The header and footer tags are code block separators that help the CodeStream in parsing. The footer is simply the tag "/**/". The header starts with the begin tag "/***" and ends with the end tag "***/". The header also contains a code block name and optionally a parameter list. The parameter list is enclosed by a pair of parentheses "()" and multiple parameters in the list are separated by commas ",". A code block may have arbitrary number of parameters. Each parameter is prefixed by the dollar sign "$" symbol (i.e. $value, $width, etc.), which allows the CodeStream object to do a straight text substitution with the string value of the parameter. Formally, the signature of a code block is defined as the pair (N, p) where N is the code block name and p is the number of parameters. A code block (N, p) may be overloaded by another code block (N, p')1. Furthermore, different helpers in a class hierarchy may contain code blocks with the same (N, p). So a unique reference to a code block signature is the tuple (H, N, p) where H is the corresponding helper. Defining the uniqueness of a code block prevents unambiguity in referencing a code block.
A code block can also be overridden. A code block (H, N, p) is overridden by a code block (H', N, p) given that H' is a child class of H. This gives rise to code block inheritance. Since Ptolemy II actors are defined within a well-defined class hierarchy, many actors inherits fields and methods from parent actors. The codegen helpers mirror the same class hierarchy. Since code blocks represent functions of actors, the code blocks should be inherited for helpers just as functions are inherited for actors. Given a request for fetching a code block, the CodeStream object searches through all code template files of the helper and its ancestors, starting from the bottom of the class hierarchy. This mirrors the same behavior of invoking an (inherited) function for an actor.
Figure 7.3
is the ElementsToArray helper's implementation of the generateFireCode() method, found in $PTII/ptolemy/codegen/c/actor/lib. This method uses the channel number of the actor input port as the parameter and fetches the "fillArray" code block from the ElementsToArray.c code template file inside the for-loop. It generates multiple copies of the "fillAarray" code block, each customized with a different channel number. It then fetches and appends a different code block with the name "sendOutput" (with no parameters). Finally, it invokes the processCode() function to process the embedded macros in the code string. Note that a helper java class needs to understand the semantics of its corresponding actor in order to implement these generate methods.
Returns a unique reference to a parameter or a port in the global scope. For a multiport, use $ref(name#i) where i is the channel number. During macro expansion, the name is replaced by the full name resulting from the model hierarchy.
Returns a unique reference to an element in an array parameter or a port with an offset in the global scope. The offset must not be negative. $ref(name, 0) is equivalent to $ref(name).
Returns the current value of the parameter associated with an actor in the simulation model. The advantage of not using $ref macro in place of $val is that no additional memory needs to be allocated. $val macro is usually used when the parameter is constant during the execution.
Returns a unique reference of a user-defined variable in the global scope. This macro is used to define additional variables, for example, to hold internal states of actors between firings. The uniqueness only requires that the name argument be unique within the scope of each actor. The helper writer is responsible for declaring these variables.
If the given name represents an ArrayType parameter, it returns the size of the array. If the given name represents a port of an actor, it returns the width of that port.
Returns the numeric constant that represents the type of the given port or parameter. The code generator uses the mapping between the token type and the numeric constant to look up the function table.
Returns the corresponding target language type of the given typed parameter or port.
Returns the name string of the codegen type corresponding to a given typed parameter or port.
Returns a new Token object of the given type. This macro takes at least one argument: the codegen type name of the Token with the value that are needed by the constructor function of the specific Token type. The code generator keeps track of the different types used through this macro. For example, "$new(Int(2))" creates an Int Token variable in the macro language. It allocates space for the Token; however, the user is responsible for calling the specific delete() function on the Token to deallocate space.
Returns a function call associated with the given token. The function call is translated to a function pointer in a two-dimensional function table. The first index of the table is the different token types, and the second index is the different functions for each type. The type of the given token is used to find the first index, and the name of the function is used to find the second index. The first argument of the function is always the given token, which acts as 'this' in an object-oriented environment. The result is always another Token. For example, the following code illustrates how a user adds two Int tokens together:
$tokenFunc($new(Int(2)::add($new(Int(3)))))
Returns a function call associated with the given token type. Instead of using an associated token, type function uses an associated type class which acts similar to a static class function. The following illustrates how a user converts an Int token to a String token:
$typeFunc(TYPE_String::convert($new(Int(2))))
Figure 7.4
shows a model in the synchronous dataflow (SDF) domain that counts the true values produced from the data source, which in this case is the Pulse actor. The CountTrues actor has its blockSize parameter set to 2, which means it reads 2 tokens from its input port for each firing. The Pulse actor' parameters are set to the values shown in the figure. When the model is simulated in the Ptolemy II framework, the produced result is also shown in the figure (the model is fired 4 times because the SDFDirector's "iterations" parameter is set to 4).
Let's look at the C code template files of the actors in the model. Macros are used extensively in the code blocks, and we will see how they are processed and changed in the generated code. Figure 7.5
shows the C code template files for the Pulse and CountTrues helpers.
Double clicking on the StaticSchedulingCodeGenerator icon brings up the code generator window. Clicking the "Generate" button in the code generator window starts the code generation for this model. It generates a stand-alone C application program that executes and produces the same result as the simulation model. Figure 7.6
shows the main function of the generated C program.
The generated code is essentially the result of combining the helpers' code blocks. The $ref() and $actorSymbol() macro are replaced with unique labels to represent different variable references. The $val() macro in the CountTrues' "fireBlock" code block is replaced by the parameter value of the CountTrue instance in the model. When the generated C program is compiled and executed, the same result is produced as from the Ptolemy II simulation:
Display: 1 Display: 1 Display: 1 Display: 1
The ptolemy.codegen.kernel.CodeGeneratorHelper class is the base class implementing these interfaces and provides common features for all actor helpers. It gives a skeleton implementation for writing a helper class. Each actor has a corresponding helper class for generating functionally equivalent code for this actor in a target language. Actors and their helpers have the same names so that the Java reflection mechanism can be used to load the helper for the corresponding actor during the code generation process. For example, there is a Ramp actor in the package ptolemy.actor.lib. Correspondingly, there is a Ramp helper in the package ptolemy.codegen.c.actor.lib. Here c represents the fact that all the helpers under ptolemy.codegen.c generate C code. Assume we would like to generate code for another target language X, the helpers for generating X code could be implemented under ptolemy.codegen.x. This would result in extendable code generation framework. Developers could not only contribute their own actors and helpers, but also add functionality to generate code for a new target language.
In the generated code, the ports of actors become memory resources in the target language, e.g., global variables in C code. A code template file can also define new variables to specify the need for global resources. A helper, however, does not have the full knowledge of the global resources such as their full names since that would be resolved only during the code generation process. Therefore a set of macros to access the global resources, as defined in the previous section, can be used. The macros are resolved and expanded according to the context in a specific model.The code generation framework has been under active development and we can generate code for a lot of models. But still we cannot generate code for all SDF models, mostly due to the lack of helpers for the actors contained in these models, but sometimes due to other reasons such as the lack of codegen support for the data types used in the models. We are continuously adding more helpers and more functionalities.
Several interesting demos for the SDF code generation are presented next. Figure 7.8
shows the Butterfly demo in $PTII/ptolemy/domains/sdf/demo/Butterfly. During code generation, the helper for the Expression actor uses a PtParser to parse the Ptolemy expression specified in the actor and then uses a CParseTreeCodeGenerator to generate the corresponding C code. The C code generated by the helper for the XYPlotter actor invokes JVM (Java Virtual Machine) through JNI (Java Native Interface) and then calls the methods of the classes in the plot package for two-dimensional graphical display. Notice the generated C code does not need the Ptolemy framework to run. It merely uses the plot utilities (which happen to be written in Java) for displaying data. One could write a different helper for the XYPlotter actor to generate the customized code for displaying data in a specific target system.
The Case demo in $PTII/ptolemy/actor/lib/hoc/demo/Case shows a model with a switch/case-type structure. The interesting part about this model is the use of a Case actor controlled by a CaseDirector. Correspondingly, there is a helper for the Case actor and a helper for the CaseDirector for code generation.
Correspondingly, the helper for the FSMActor is designed to generate appropriate target code according to its context. When the FSMActor is used as a standalone actor, the helper generates the code for all outgoing transitions for each state; when it is used as a ModalController in a ModalModel, the helper generates the code for preemptive transitions and the code for non-preemptive transitions separately. Regardless of the context, for each state and each outgoing transition from that state, the generated code follows exactly the same execution sequence as in the original model--first, the guard expression is translated into the target code using a ParseTreeCodeGenerator; then all the choice actions contained by the transition are transformed into the target code; if there is any refinement associated with the transition (represented by a TransitionRefinement actor which is different from a Refinement actor associated with a state), the corresponding code for the refinement is generated; next, all the commit actions contained by the transition are transformed into the target code; the code for updating new current state follows; finally the code for (re)initializing the refinement associated with the new state is generated when the reset parameter of the transition is true.
The internal execution of a ModalModel is controlled by a local director, which can be an FSMDirector, a MultirateFSMDirector, or an HDFFSMDirector. A user can choose a director for a ModalModel. Usually only one specific director makes sense for the behavior the user wants for the model. For example, an FSMDirector is used when the rate for all the ports of a ModalModel is 1 and the ModalModel tries a transition from the current state during every firing of the ModalModel. A MultirateFSMDirector is inherited from FSMDirector to model multirate behavior. To guarantee static schedulability under SDF, all state refinements must present the same rate to the outside for all the ports mirrored to the same port in the ModalModel. In addition, a MultirateFSMDirector makes only non-preemptive transitions so that the refinement for the current state gets fired before trying a transition and tokens are consumed from input ports and sent to output ports according to the rate of each port. If the refinement associated with different state presents a different rate, an HDFFSMDirector must be used. It is further inherited from MultirateFSMDirector and only tries a transition after the whole model finishes the execution of one complete schedule. Together with HDFDirector, it implements the HDF domain (see section 7.4.3). No matter which FSM director the user chooses, the corresponding helper generates the target code which preserves the original model behavior.The VariableScope inner class in CodeGeneratorHelper handles code generation for expressions contained by parameters. To support code generation for modal models, the derived PortScope inner class in the helper for the FSMActor handles code generation for guard expressions with multi-channel and multi-rate syntax. Both inner classes would expand any expression recursively until any variable in the expression is either a constant (in this case the constant is substituted for the variable) or a modifiable variable, e.g., modified in a mode transition or in a SetVariable actor (in this case a variable which is defined at the beginning of the generated code is used).
Several interesting demos for the FSM code generation are presented next. The Blending demo in $PTII/ptolemy/domains/fsm/demo/Blending models a controller with two major control modes and two transition modes. Each major mode has one Refinement. Each transition mode has three Refinements. Some Refinements are shared by different modes. After the controller is designed and simulated to meet the given specification in the Ptolemy environment, the generated C code can then run on some embedded platform.
The Modal Binary Symmetric Channel demo in $PTII/ptolemy/domains/fsm/demo/ModalBSC models a channel with two states, each with different probabilities of error. The model not only has Refinement associated with state, but also has TransitionRefinement associated with transition. These modeling constructs are automatically realized in the generated code.
Since it's expensive to compute the schedule during the run time, all possible schedules are precomputed during the compilation time (i.e., code generation time). The structure of the generated code is hard-coded in such a way that it reflects all possible execution paths for different schedules.
Since an HDF model can have arbitrary levels of hierarchy, there must be a systematic way to find out the number of schedules and enumerate all schedules for any HDF model. The approach taken here uses the following definition.
For each opaque actor (i.e., atomic actor or opaque composite actor), let
be the number of configurations of this actor. For each configuration of the actor, it has a corresponding local SDF schedule. (An atomic actor has a degenerate form of local SDF schedule: fire itself once.)
Let
be the rate signature of this actor in the
th configuration.
During code generation,
and
,
for each opaque actor are derived in a recursive bottom-up fashion:
/*** CodeBlockName [($parameter1, $parameter2, ...)] ***/ CodeBlockBody /**/Parameterized code blocks can contain parameters which the user can specify. Parameter substitution syntax is straight-forward string pattern substitution, so the user is responsible for declaring unique parameter names. For example, a code block is declared to be the following:
/*** initBlock ($arg) ***/ if ($ref(input) != $arg) { $ref(output) = $arg; } /**/If the helper invokes the appendCodeBlock() method with a single parameter, which is the integer 3,
ArrayList args = new ArrayList(); args.add(Integer.toString(3)); appendCodeBlock("initBlock", args);then after parameter substitution, the code block would become:
if ($ref(input) != 3) { $ref(output) = 3; }
Ptolemy II supports a variety of types that are different from the target language. It sometimes dynamically converts tokens in the execution of the model. Thus, the code generator has to deal with some type conversion issues. First, it has to generate code that represents the different PTII token types and functionality of the tokens in the target language. Second, it has to be able to convert one type to another that is higher in the type lattice. Thirdly, the code generator has to know where type conversions need to occur in order to generate compilable code and code that produces the correct result.
For each Ptolemy token type, there is an associated codegen Token type. In the $PTII/ptolemy/codegen/kernel/type directory, there is a target-language specific file which contains functionality code for each Ptolemy type. The code generator uses the code stream mechanism to harvest the code blocks in these files. E.g. The Int.c file contains code to operate on an integer token.
/***negateBlock***/ Token Int_negate(Token this, ...) { this.payload.Int = -this.payload.Int; return this; } /**/
The code generator handles three kinds of type conversion. The first case is converting between primitive (non-Token) types. The target languages often support some of the Ptolemy types as primitive types. The code generator can take advantage of the language constructs to avoid storage and processing time overhead by using these primitive types. For example, the c code generator uses int, double and char array (char*) to represent the Ptolemy int, double and string types. The c code generator generates functions to convert int and double to char* type (because string is higher than int and double in the type lattice).
char* InttoString (int i) { char* string = (char*) malloc(sizeof(char) * 12); sprintf((char*) string, "%d", i); return string; }
The second case is to upgrade a primitive type to a Token type. This happens often in a multiport connection where the sink port (multiport) is resolved to type 'general' and the source ports are resolved to concrete types, like int, string, etc. The code generator takes care of this by using the $new() macro to create a new Token for the given primitive value (there is an associated codegen Token type for each Ptolemy type including the primitive type).
The third case is to convert between different Token types. The functionality code for each codegen type has a convert function that converts the given token. For example, in $PTII/ptolemy/codegen/kernel/type/String.c, the convertBlock looks like2:
/***convertBlock***/ Token String_convert(Token token, ...) { char* stringPointer; switch (token.type) { #ifdef TYPE_Boolean case TYPE_Boolean: stringPointer = BooleantoString(token.payload.Boolean); break; #endif #ifdef TYPE_Int case TYPE_Int: stringPointer = InttoString(token.payload.Int); break; #endif #ifdef TYPE_Double case TYPE_Double: stringPointer = DoubletoString(token.payload.Double); break; #endif default: // FIXME: not finished fprintf(stderr, "String_convert(): Conversion from an unsupported type. (%d)\n", token.type); break; } token.payload.String = stringPointer; token.type = TYPE_String; return token; } /**/
The following is the code generation fire method in the AddSubtract helper (AddSubtract.java):
public String generateFireCode() throws IllegalActionException { super.generateFireCode(); ptolemy.actor.lib.AddSubtract actor = (ptolemy.actor.lib.AddSubtract) getComponent(); Type type = actor.output.getType(); boolean minusOnly = actor.plus.getWidth() == 0; ArrayList args = new ArrayList(); args.add(new Integer(0)); if (type == BaseType.STRING) { _codeStream.appendCodeBlock("StringPreFireBlock"); for (int i = 0; i < actor.plus.getWidth(); i++) { args.set(0, new Integer(i)); _codeStream.appendCodeBlock("StringLengthBlock", args); } _codeStream.appendCodeBlock("StringAllocBlock"); } else { String blockType = isPrimitive(type) ? "" : "Token"; String blockPort = (minusOnly) ? "Minus" : ""; _codeStream.appendCodeBlock(blockType + blockPort + "PreFireBlock"); } String blockType = isPrimitive(type) ? codeGenType(type) : "Token"; for (int i = 1; i < actor.plus.getWidth(); i++) { args.set(0, new Integer(i)); _codeStream.appendCodeBlock(blockType + "AddBlock", args); } for (int i = minusOnly ? 1 : 0; i < actor.minus.getWidth(); i++) { args.set(0, new Integer(i)); _codeStream.appendCodeBlock(blockType + "MinusBlock", args); } _codeStream.appendCodeBlock("PostFireBlock"); return processCode(_codeStream.toString()); }These are the corresponding code blocks in the code template (AddSubtract.c):
/***PreFireBlock***/ $actorSymbol(result) = $ref(plus#0); /**/ /***MinusPreFireBlock***/ $actorSymbol(result) = -$ref(minus#0); /**/ /***TokenPreFireBlock***/ $actorSymbol(result) = $ref(plus#0); /**/ /***TokenMinusPreFireBlock***/ $actorSymbol(result) = $tokenFunc($ref(minus#0)::negate()); /**/ /***IntAddBlock($channel)***/ $actorSymbol(result) += $ref(plus#$channel); /**/ /***IntMinusBlock($channel)***/ $actorSymbol(result) -= $ref(minus#$channel); /**/ /***DoubleAddBlock($channel)***/ $actorSymbol(result) += $ref(plus#$channel); /**/ /***DoubleMinusBlock($channel)***/ $actorSymbol(result) -= $ref(minus#$channel); /**/ /***BooleanAddBlock($channel)***/ $actorSymbol(result) |= $ref(plus#$channel); /**/ /***StringPreFireBlock***/ $actorSymbol(length) = 1; // null terminator. /**/ /***StringLengthBlock($channel)***/ $actorSymbol(length) += strlen($ref(plus#$channel)); /**/ /***StringAllocBlock***/ $actorSymbol(result) = (char*) realloc($ref(output), $actorSymbol(length)); strcpy($actorSymbol(result), $ref(plus#0)); /**/ /***StringAddBlock($channel)***/ strcat($actorSymbol(result), $ref(plus#$channel)); /**/ /***TokenAddBlock($channel)***/ $actorSymbol(result) = $tokenFunc($actorSymbol(result)::add($ref(plus#$channel))); /**/ /***TokenMinusBlock($channel)***/ $actorSymbol(result) = $tokenFunc($actorSymbol(result)::substract($ref(minus#$channel))); /**/ /***PostFireBlock***/ $ref(output) = $actorSymbol(result); /**/
// Uniform.java public class Uniform extends RandomSource { public Uniform(ptolemy.actor.lib.Uniform actor) { super(actor); } protected String _generateRandomNumber() throws IllegalActionException { return _generateBlockCode("randomBlock"); } }This is the Uniform helper's code template file (Uniform.c):
/*** randomBlock ***/ $ref(output) = (RandomSource_nextDouble(&$actorSymbol(seed)) * ($val(upperBound) - $val(lowerBound))) + $val(lowerBound); /**/It references the function RandomSource_nextDouble from its parent's code template. The following is the code template for the RandomSource helper (RandomSource.c):
/*** sharedBlock ***/ int RandomSource_next(int bits, double* seed) { *seed = (((long long) *seed * 0x5DEECE66DLL) + 0xBLL) & ((1LL << 48) - 1); return (int)((signed long long) *seed >> (48 - bits)); } double RandomSource_nextDouble(double* seed) { return (((long long)RandomSource_next(26, seed) << 27) + RandomSource_next(27, seed)) / (double)(1LL << 53); } /**/ /*** gaussianBlock ***/ double RandomSource_nextGaussian(double* seed, boolean* haveNextNextGaussian, double* nextNextGaussian) { double multiplier; double v1; double v2; double s; if (*haveNextNextGaussian) { *haveNextNextGaussian = false; return *nextNextGaussian; } else { do { v1 = 2 * RandomSource_nextDouble(seed) - 1; // between -1.0 and 1.0 v2 = 2 * RandomSource_nextDouble(seed) - 1; // between -1.0 and 1.0 s = v1 * v1 + v2 * v2; } while (s >= 1 || s == 0); multiplier = sqrt(-2 * log(s)/s); *nextNextGaussian = v2 * multiplier; *haveNextNextGaussian = true; return v1 * multiplier; } } /**/ /*** setSeedBlock0($hashCode) ***/ $actorSymbol(seed) = $actorSymbol(seed) = time (NULL) + $hashCode; /**/ /*** setSeedBlock1 ***/ /* see documentation from http://java.sun.com/j2se/1.4.2/docs/api/java/util/Random.htm#setSeed(long) */ //this.seed = (seed ^ 0x5DEECE66DL) & ((1L << 48) - 1); $actorSymbol(seed) = ((long long) $val(seed) ^ 0x5DEECE66DLL) & ((1LL << 48) - 1); /**/ /*** preinitBlock ***/ double $actorSymbol(seed); /**/Since the Uniform helper does not override any of code generation methods, the parent's methods are called instead. The sharedBlock, setSeedBlock and preinitBlock code blocks are harvested from the RandomSource code template. The child helper may override its parent's code blocks while using its parent's code generation methods. A child helper can also override the code generation method. Neither way of overriding the super class's code harvesting increases run-time overhead in the generated code. The code generator handles method /code block overrides during compile time and add no extra logic in the final code.