Lesson 3
Building Interpreters


Introduction to Lesson 3
3.1 The Builder Object Network (BON)
3.2 Creating a new component
3.3 Writing the interpreter code


Introduction to Lesson 3

For some GME applications, the only motivation for a modeling project is the desire to describe a system in a structured way and to use the description as a form of interpersonal communication. Usually, however, we also want the computer to be able to process data from the model automatically. Typical processing tasks range from the simple to the sophisticated:

A common theme for all these applications is that they require programmatic access to the GME model information. To meet this requirement, GME provides several ways to create programs that access its data. The most popular technique is writing a GME interpreter. Interpreters are not standalone progams; they are components (usually DLLs) that are loaded and executed by GME upon a user's request. Other types of programs that can access model data are standalone applications (which can be executed without the GME GUI) and other integrated components that serve different purposes (Plugins and AddOns).

There are also different technologies available to access GME data:

This lesson discusses the process of building a Builder Object Network-based interpreter. The application domain and paradigm are those used in the previous lesson. Our interpreter will complete a simple task: it will print out a list of all routers, along with their ports and IP addresses.




3.1 The Builder Object Network (BON)

As mentioned above, the job of the Builder Object Network is to instantiate C++ objects for each of the objects in the GME model tree. Access to the objects, and to the relationships between them, are available through methods that act on these objects. Class names and method names are often self-explanatory: CBuilderAtom, CBuilderConnection, GetAttribute( ), SetAttribute( ), GetChildren( ), GetParent( ), CreateNewAtom( ), etc. The main BON header file, Builder.h, is the authentic repository of the BON interfaces.



Fig 3.1 Classes in the Builder Object Network


The BON class hierarchy is shown in Fig 3.1. Many of the classes are organized into a hierarchy with the common baseclass CBuilderObject. This class represents the common functionality available for all FCOs, while the specialized versions provide object-type specific methods. CBuilderAtom, CBuilderModel, and CBuilderConnection represent atoms, models, and connections, all well-known from the previous lessons. CBuilderReference (along with the specialized versions, CBuilderAtomReference and CBuilderModelReference) and CBuilderSet represent references and sets, which are discussed in Lesson 4. CBuilderReferencePort is a pseudo-object that is only present in the BON.

Two other classes are "outsiders" to the CBuilderObject hierarchy. CBuilder represents the modeling project as a whole. It provides access to the root folder and to other folders, as well as to project-wide settings (such as GetProjectName( )). CBuilderFolder represents folders and is capable of navigating the folder hierarchy and accessing root FCOs.

This tutorial does not attempt to explain the entire BON interface. Most methods listed in Builder.h are self-explanatory. However, the GME User's Manual does contain a detailed description of the Builder Object Network.

Looking at the Builder.h file, it is obvious that this framework relies quite heavily on certain data types and templates not listed in Fig 3.1, such as CString, CTypedPtrList<>, CTypedPtrMap<>, etc. These are not defined by the framework, but by MFC, a popular class library provided with Microsoft Visual Studio. Please consult the MS Visual Studio / MFC documentation for help with these data types. Also, you will notice that a typical instruction pattern is used to navigate the collection attributes:

POSITION pos = list.GetHeadPosition(); while(pos) { process(list.GetNext(pos)); }
The Builder Object Network also provides an optional user-defined subclassing mechanism based on the factory design pattern [ref]. BON classes allow subclassing, and user-defined subclasses can be registered into the BON class factory. When this happens, the object network is not built from the predefined classes in Fig 3.1, but from the user's own subclasses, which include additional functionality.

The Builder Object Network is provided as part of a convenient framework, which includes a set of tools (or "wizards") that help the user to set up a component development project. Thanks to this framework, the process of creating an interpreter is both easy and quick. Even casual component developers can build meaningful interpreters within 30-60 minutes.

Let's see if this is true...




3.2 Creating a new component


3.2.1 Generate a workspace

The easiest way to create components is to use CreateNewComponent.exe, a program that generates and initializes a ready-to compile Visual Studio workspace (Fig 3.1). You can start it from the \bin directory of the GME folder (e.g. Program Files\GME\bin\CreateNewComponent.exe)



Fig 3.2 CreateNewComponent.exe


This program unpacks the necessary files for the selected component technology. Specify Builder Object Network and a suitable directory, and click OK. A message box reports the number of files extracted, and a second program, ConfigureComponent.exe, is initialized just before CreateNewComponent terminates.


3.2.2 Specify information

ConfigureComponent.exe (shown in Fig 3.3) is actually one of the files that was extracted in the previous step. It specifies the identification and registration information for the new component. While CreateNewComponent is normally used just once during this process, ConfigureComponent can be run again whenever something needs to be changed.



Fig 3.3 ConfigureComponent.exe


Many of the fields in ConfigureComponent are automatically generated from the component name, so we will only need to specify four things:

Click the "Generate" button. The program creates ComponentConfig.h, the last file missing from the set of component source files.


3.2.3 Execute the component

Open the Visual Studio workspace, "Component.dsw".

A source file, Component.cpp, is opened along with the workspace. Most of the other files in the project are not meant to be modified (and are protected with a read-only attribute). But this file and its header, Component.h, are just "skeletons", code modules that we will modify in order to implement the specific component logic.

Let us test the component as it currently exists. Execute "Build" (F7). The project should compile smoothly. (If a problem occurs, the most likely cause is that the GME interface directory is not specified correctly.)

Since the component is not a standalone program, it cannot be executed directly. Start GME and open a project based on the "networking" paradigm. If "networking" was contained in the "Paradigms" field in ConfigureComponent, the interpreter should be immediately available. There are several ways to start it:

  1. From the File/Interpret submenu
  2. Through the generic interpreter button ("i") in the GME toolbar
  3. Through the "Interpret" item in the context menus in the GME diagram windows or tree browser
  4. Through the component-specific toolbar button (if this option was enabled during ConfigureComponent).
(Note: When using methods #2 or #3, you may need to handle an additional "Select Interpreter" dialog if more than one interpreter is activated for the current paradigm.)

Upon execution, the interpreter displays a message box containing the name of the root folder, exactly as dictated in the CComponent::InvokeEx method in the Component.cpp file. We will implement the interpreter by changing this method and possibly extending CComponent.




3.3 Writing the interpreter code

The framework has been set up, and the interpreter is running correctly. Our final task consists of writing the crucial portion of the interpreter code. Like any other program, the difficulty of this task depends on the complexity of the functionality. Most interpreters implement at least two typical functions:

The operation of this particular interpreter should not depend on the focus object or on selected objects (which may be NULL in certain situations). Therefore, we must traverse the object tree through the root of the object hierarchy. The paradigm does not allow subfolders, so it is relatively easy to travel through all the root models in the diagram.
#include <fstream.h>
static ofstream outf;

void CComponent::InvokeEx(CBuilder &builder,CBuilderObject *focus, CBuilderObjectList &selected, long param) {
    outf.open("netlist.lst");
    outf << "Router list for network" << builder.GetRootFolder()->GetName() << endl;
    const CBuilderModelList *diags = builder.GetRootFolder()->GetRootModels();
    POSITION pos = diags->GetHeadPosition();
    while(pos) {
        CBuilderModel *diagram = diags->GetNext(pos);
        ProcessDiagram(diagram);
    }
    outf.close();
}
InvokeEx demonstrates the basic tricks of traversing the BON network: accessing child objects and iterating through collections. The only difference between it and CComponent::ProcessDiagram is that the latter is recursive and processes different children in a different way.
void CComponent::ProcessDiagram(CBuilderModel *d) {
    ASSERT(d->GetKindName() == "NetDiagram");
    const CBuilderModelList *diags = d->GetModels("NetDiagram");
    POSITION pos = diags->GetHeadPosition();
    while(pos) ProcessDiagram(diags->GetNext(pos)); // recursion

    const CBuilderModelList *routers = d->GetModels("Router");
    pos = routers->GetHeadPosition();
    while(pos) ProcessRouter(routers->GetNext(pos));
}
ProcessDiagram is fairly complex, even though we managed to squeeze the iteration loops into a single line. Together, the two methods above provide a nearly complete implementation of the "traversal" function of the interpreter, the part that locates routers in the hierarchy. The ProcessRouter method takes care of the rest; it implements the "node processing" function, as well as some minimal traversal into the ports of a router.
void CComponent::ProcessRouter(CBuilderModel *r) {
    ASSERT(r->GetKindName() == "Router");
    CString fam;
    r->GetAttribute("Family", fam);
    outf << "\tRouter " << r->GetName() << " (" << fam << ")" << endl;

    const CBuilderAtomList *ports = r->GetAtoms("Port");
    POSITION pos = ports->GetHeadPosition();
    while(pos) {
            CBuilderAtom *port = ports->GetNext(pos);
            CString iftype, ipaddr;
            int ifspeed;
            port->GetAttribute("IFType", iftype);
            port->GetAttribute("IFSpeed", ifspeed);
            port->GetAttribute("IPAddress", ipaddr);
            outf << "\t\tPort " << port->GetName();
            outf << "(" << iftype << "; "<< ifspeed << "Kbps), Addr: " << ipaddr << endl;
    }
}
Do not forget to add ProcessDiagram and ProcessRouter to the CComponent declaration in Component.h. It is not particularly useful to define these functions as methods; normal C-style functions will suffice. The same is true for our static file variable outf, which could just as well have been declared as a CComponent (static or non-static) member.

The compilation should run smoothly unless there are typos in your code. If you encounter assertions when running the interpreter for the first time, make sure that you used the exact same names (kindnames, rolenames and attribute names) in your paradigm that you are using in your interpreter code. For reference, here are my copies of Component.cpp and Component.h.

After running the interpreter and verifying that it works, check the time. In less than 30 minutes, we have built our first interpreter!



<< Previous Lesson Complete List Next Lesson >>