gmNIEventHandlers
- Mark Juras
The Gemini Runtime Event Handlers
Gemini enables one to write native methods to handle situations when a migration cannot be written entirely in gmSL (Great Migrations Scripting Language), e.g., when the standard capabilities of the translation tool do not support some operation needed by the compiler, analyser, author, or auditor. Code to be made available to a translation tool is linked into a dynamic-link-library that is then executed by the tool when certain events occur. These events and the details of their handlers are discussed in this subsection.
Explicit examples are provided to both document the actual events and show the situations in which these events can be used:
The migration of Flp32a30.ocx To Telerik.WinControls.GridView
The migration of Flp32a30.ocx To Telerik.WinControls.GridView is performed using the standard refactoring and editing facilities available with the tool; however, there are a few issues that cannot be. In this discussion three such events will be described:
- The author has is ready to write the declarations of externals references for a project;
- The analyser is ready to start analysing the compiled code;
- The analyser is performing a final scan of the code and has encountered a component that needs to be transformed.
To take advantage of these events external refactoring libraries register runtime DLLs with the tool that are to be loaded when that specification is read. For example, the specification
<Refactor id="[Flp32a30.ocx]" dllName="LpLibMigration.dll" event="LpLib" >
tells the tool that when it encounters a request to load the named OCX, typically in a VBP file, then it should also load the named DLL for exports that satisfy given named events. Thus, the external references event is handled by an export named "AuthorReference". The startup of the analyser is handled by an export named "StartAnalyser" and the transformation event is handled by "Transform". The front-end of a DLL that handles these events would look as follows.
DLL_EXPORT int StartAnalyser(int migNumber) { ... } DLL_EXPORT int Transform(int subRoot,int target,int iRefer,int* qeStack) { ... } DLL_EXPORT int AuthorReference(int projRoot) { ... }
Note that the actual content of these handlers is highly patterned. The migration specialist actually supplies a scripting tool with a simple specification of what events and what components he wishes to work with. That scripting tool then produces the code framework needed with stubs to be filled where needed.
ASP Global Includes
The problem addressed here is that ASP include files are code fragments that reference symbols that are not defined within the file itself. Instead, they are:
- defined in the file that is including them
- defined in another file that has been included
- are not defined anywhere
- simply a local undeclared symbol.
Within the default behavior of the tool, the determination of the status of the external symbols is made on the basis of the environment in which the included is first loaded. This is the standard rule for these types of situations where user components are used multiple times. In this instance the rule does not work in some situations. The environments surrounding includes vary wildly. A stronger approach is needed to ensure that all translations for a given include are at least consistent. First all pageslices that are eventually to be combined into a site build are compiled and translated individually to the point where they are well-formed and ideally build.
The introduction of GlobalIncludes has also necessitated a change in the way in which the tool processes included pages. Here are notable changes:
- Much like ASPX files, the code that isn’t in a function or subroutine needs to go in the ASCX file instead of the Author_Markup function
- The UserControl needs to be registered for use by the page. This can be done using a Register declarative on the page itself or it can be done in the web.config file (whichever is easiest). The current approach does it in the page. The control is entered into the page as a markup tag instead of calling the author_markup function. Each instance of an include needs to be specified uniquely on the page.
- If an include references undefined variables or functions, An interface needs to be created specifying all of the undefined members, and the host page needs to implement the interface. In the Page_Load event handler of the UserControl, the UserControls “Cls_” variable should be set to the Page cast to the interface type. Instead of referencing another UserControl (default behavior) the local “Cls_” variable should be referenced, and the page should return the value that was previously referenced directly. If a member isn’t found for the page to use, a default implementation should be used (local variable for properties, default return value for functions, no implementation for subroutines)
- The project file needs updated to reference the all of the new files.
The StartPass2 Event Handler
gmNI:DLL_EXPORT int StartPass2(int target,int migNumber)
The event handler StartPass2 is called immediately before the compiler starts its second pass while compiling a project code or a pageslice. The first pass of the compiler has been completed; therefore, the global symbols table is present. No code has been generated and no local variables have been stored in the symbol table. The parameter target is the offset of the project or the main page to be compiled. The parameter migNumber is the sequence number, relative to one, of the load order of the dynamic link library containing the handler. During any given run, this number is unique and can be used to mark components as needed. The number will vary from run to run. The handler is expected to return a one if it performs any actions based on the target component, and a zero it does not. In the case of this particular event all handlers are called regardless of this return value.
The purpose of this handler is to make any changes in the types and other characteristics of any symbols to ensure a clean pass2 compile. In the case of the global includes this involves looking in the registry for special information about the pages. That logic is implemented in a private method GlobalIncludes_StartPass2 in the library, which will not be shown here. The method below shows the actual handler.
DLL_EXPORT int StartPass2(int target,int migNumber) { codptr = (UBYTE*)(Opcode_GetCode()); selectFlags = (tVb7Flags*)(Select_GetFlags()); GlobalIncludes_StartPass2(target,migNumber); return 1; }
It gets the handles to the global code block and select information from the tool in case they are needed and then it executes the local method and returns a one.
The CodeEvent Event Handler
gmNI:
AnaUser.AnaUserMigration() CodeReview AnaUser.AnaUserPass1() Initialize
The AuthorClass Event Handler
gmNI:
hor.LocAuthorClass()
The AutReference Event Handler
gmNI:
thor.AuthorSupportReference() The first issue involves using the AuthorReference Event. This event occurs when the author is writing the project file immediately before writing the list of external references needed by project. These typically look as follows
<Reference Include="ADODB"> <Name>ADODB</Name> <HintPath>\deploy\externs\Interop.ADODB.dll</HintPath> </Reference> <Reference Include="Scripting"> <Name>Scripting</Name> <HintPath>\deploy\externs\Interop.Scripting.dll</HintPath> </Reference>
The information for this list comes from the name and location attributes of the library commands in the referenced IDFs. The purpose of the call is to give user the opportunity to change how the reference is to be written. Since the event is called only once, using it typically requires two things -- first, declare the location of the reference as "DoNotDeclare" and second, author the desired reference within the AuthorReference code in the DLL.
In the present example the reference to the OCX is being displayed as follows
<Reference Include="AxLpLib"> <Name>AxLpLib</Name> <HintPath>C:\gmproj\ELC2\deploy\externs\AxInterop.LpLib.dll</HintPath> </Reference> <Reference Include="LpLib"> <Name>LpLib</Name> <HintPath>C:\gmproj\ELC2\deploy\externs\Interop.LpLib.dll</HintPath> </Reference>
Instead a series of references like the following is needed
<Reference Include="TelerikCommon"> <HintPath>..\runtime\TelerikCommon.dll</HintPath> </Reference> <Reference Include="Telerik.WinControls"> <HintPath>..\runtime\Telerik.WinControls.dll</HintPath> </Reference> <Reference Include="Telerik.WinControls.UI"> <HintPath>..\runtime\Telerik.WinControls.UI.dll</HintPath> </Reference> <Reference Include="Telerik.WinControls.GridView"> <HintPath>..\runtime\Telerik.WinControls.GridView.dll</HintPath> </Reference>
To achieve this the location of the library is migrated to DoNotDeclare and the following event handler code is added to the LpLibMigration dll.
#ifdef FPROTOTYPE int LpLibAuthorReference(int projRoot) #else int LpLibAuthorReference(projRoot) int projRoot; #endif { projRoot; Write_Text("<Reference Include=\"TelerikCommon\">"); Write_Record(); Write_Text(" <HintPath>..\\runtime\\TelerikCommon.dll</HintPath>"); Write_Record(); Write_Text("</Reference>"); Write_Record(); Write_Text("<Reference Include=\"Telerik.WinControls\">"); Write_Record(); Write_Text(" <HintPath>..\\runtime\\Telerik.WinControls.dll</HintPath>"); Write_Record(); Write_Text("</Reference>"); Write_Record(); Write_Text("<Reference Include=\"Telerik.WinControls.UI\">"); Write_Record(); Write_Text(" <HintPath>..\\runtime\\Telerik.WinControls.UI.dll</HintPath>"); Write_Record(); Write_Text("</Reference>"); Write_Record(); Write_Text("<Reference Include=\"Telerik.WinControls.GridView\">"); Write_Record(); Write_Text(" <HintPath>..\\runtime\\Telerik.WinControls.GridView.dll</HintPath>"); Write_Record(); Write_Text("</Reference>"); Write_Record(); return 1;
The BeginProp Event Handler
gmNI:
Vb6Form.CmpBeginProperty()
The StartAnalyser Event Handler
gmNI:
The remainder of the issues associated with this method use the StartAnalyser event and then a series of Transform events. The StartAnalyser event occurs when the analyser first starts up before it performs any other operations. Its purpose is to allow the user to do any initializations that might be needed later. This event handler must always be present when the transform event is to be used. The transformation event is triggered by markers in the library components information vectors that are placed there by this handler. Note that this actual handler is normally written for the user by a script based on a simple list of the names of the components being transformed or referenced. The components themselves are identified as "nameSpace.className.compName". This nameSpace is normally the library name attribute value. The ClassName is the identifier of the actual IDF class that contains the component. It is NOT the coClass identifier that is typically used in SourceCode. Finally, compName is the actual name of the component in the named class in the named namespace.
The Transform Event Handler
gmNI:
The transform events occur during the final scan of the code by the analyser. Each reference to each component marked by the StartAnalyser event triggers a Transform event. Transformation is controlled entirely by the event code. It does not expect or require any entries in the IDF entries for the library components being migrated. Most transformations, however, end up referencing operations that are not part of the original external reference. That's really the whole point behind this operation -- introducing new operations into the code to accommodate the new implementation. These additional operations are added to the refactor specification via a migClass command. These are discussed below.
Specifying the Number or Columns
In the unmigrated code the number of columns is set to a value.
cmbName.Columns = 4;
In the migrated call the equivalent operation is to clear out the columns and then to add as many as are indicated. This would look as follows:
cmbName.MultiColumnComboBoxElement.Columns.Clear(); for (int x = 0;x < 4;x++)cmbName.MultiColumnComboBoxElement.Columns.Add("");
- The wrinkle is that there are also statements like this
cmbName.Columns = (short)(cmbName.Columns + 1); // Add an additional column to dropbox
- At this point the best way to migrate these has not been determined so we merely want to detect
- them and exclude them from this particular migration.
Using transform usually ends up replacing calls in the unmigrated code with calls to migrated components that did not exist in the original reference. In this case we want to end up performing a Clear operation followed by an operation that adds columns. We define these operations using a migclass which is added to the RefactorLibrary specification.
<migClass id="LpLibComboBox" Creatable="off" > <method id="AddColumns" type="void" nPram="2" migPattern='for(int x = 0;x < %2d;x++) %1d.MultiColumnComboBoxElement.Columns.Add("")\c' > <argument id="rightSide" type="Integer" status="ByVal"/> </method> <method id="Clear" type="void" nPram="1" migPattern="%1d.cmbName.MultiColumnComboBoxElement.Columns.Clear()\c" /> </migClass>
Note that these specifications include how they are to be authored in the target. The %1d in the above patterns refer to the "hostObject" of the call which is "cmbName" in this case. The %2d in the AddColumns pattern refers to the first argument of the call. This argument is named "rightSide" because it will receive the "rightSide" specification from the property setter.
We now have everything we need to write the transformation code.
#ifdef FPROTOTYPE static int Columns(int subRoot,int target,int iRefer,int* qeStack) #else static int Columns(subRoot,target,iRefer,qeStack) int subRoot; int target; int iRefer; int* qeStack; #endif { auto tCodeGroup indexInfo; auto int iRet; auto int nColumn; auto int iStart; indexInfo.target = target; 1) iRet = isPropertySetter(subRoot,target,&indexInfo,iRefer,qeStack); if(iRet == 0) return 0; 2) nColumn = intValue("RightSide",&indexInfo); if(nColumn == NO_VALUE_FOUND) return 0; 3) iStart = callDerivedMethod(compAddr[COMBO_CLEAR],&indexInfo); callDerivedMethod(compAddr[COMBO_ADDCOLUMNS],&indexInfo); 4) replaceCodeGroup(iStart,&indexInfo); return 1; }
First of all note that there are many scripting facilities available to set up these codes. These utilities will end up creating Stubs for the components. The migration expert will have an "Columns" stub already laid out for him into which he can enter the code he wants.
The first step is to ensure that we are looking at a reference that is setting a Property equal to something. These are called "PropertySetters". If this is not a Property Setter the method returns a zero indicating that no change was made. If it was a PropertSetter, then the indexInfo tCodeGroup structure will have the code associated with the operation broken into 3 labeled arguments:
argument-Name | Description |
target | This is the codeblock that actually references the target symbol that triggered the call to this method. The actual reference is usually followed by a MEM.Child operation which ties the target symbol to the host object. When present this operation is included in the codeblock. Typically this codeblock will be deleted when the targetcode is transformed. |
rightSide | This codeblock specifies the value that is to be assigned to the property. |
hostObject | This codeblock specifies the user object instance whose property value is being set. |
For example in "employee.pay = dailyRate * daysInPayPeriod" the "target" argument would be the code that specifies the "pay" property; the "rightSide" argument would be the code that specifies the pay calulation; and the "hostObject" argument would be the code that specifies the "employee" object. Remember we are talking about code not about character strings.
The second step asks if the "rightSide" value is a simple integer value. If a NO_VALUE_FOUND is returned than we have a complicated code reference of the sort described in the wrinkle and we abort the migration.
The third step now creates calls to the two derived methods that were defined in the migClass. Note that the names of the arguments are keyed to the names assigned to the arguments of the PropertySetter.
The fourth step completes the migration and replaces the original code with the newly formed migration code. A comparison of the new version of the code shows the result
***** unmigrated cmbName.Columns = 4; ***** migrated cmbName.cmbName.MultiColumnComboBoxElement.Columns.Clear(); for(int x = 0;x < 4;x++) cmbName.MultiColumnComboBoxElement.Columns.Add(""); ***** unmigrate cmbName.Columns = 3; ***** migrated cmbName.cmbName.MultiColumnComboBoxElement.Columns.Clear(); for(int x = 0;x < 3;x++) cmbName.MultiColumnComboBoxElement.Columns.Add("");
Setting Individual Column Property Values
In the unmigrated code a "Col" property is set and then subsequent column property setting are applied to that column. Thus a code sequence like
cmbName.Col = 0; cmbName.ColWidth = 25; cmbName.ColName = "colValue";
establishes a width and a name for the first column. Within the migrated code this column number has to be associated with the column-specific property setting. Thus the migrated code needs to be something like the following:
cmbName_colIndex = 0; cmbName.MultiColumnComboBoxElement.Columns[cmbName_colIndex].Width = 25; cmbName.MultiColumnComboBoxElement.Columns[cmbName_colIndex].Name = "colValue";
- The wrinkle here is that a variable "cmbName_colIndex" must be declared once and only once
- for each different object within the code that is using this shorthand notation to define
- column properties. The process is much like the one above.
Setting the Col Property
As in the above we first add the new operation to the LpLibComboBox migclass.
<method id="SetCol" type="void" nPram="2" migPattern="%1d_colIndex = %2d\c" > <argument id="rightSide" type="Integer" status="ByVal"/> </method>
The hostObject identifier will be used to form the name of the particular colIndex and rightSide value will again appear on the rightSide. The transformation code is as follows:
#ifdef FPROTOTYPE static int Col(int subRoot,int target,int iRefer,int* qeStack) #else static int Col(subRoot,target,iRefer,qeStack) int subRoot; int target; int iRefer; int* qeStack; #endif { auto tCodeGroup indexInfo; auto int iRet; auto int iStart; auto int comboBox[5]; auto int nChild; indexInfo.target = target; 1) iRet = isPropertySetter(subRoot,target,&indexInfo,iRefer,qeStack); if(iRet == 0) return 0; nChild = getLvalue("hostObject",&indexInfo,comboBox) - 1; if(nChild == 0) return 0; 3) iStart = callDerivedMethod(compAddr[COMBO_SETCOL],&indexInfo); replaceCodeGroup(iStart,&indexInfo); 4) iRefer += defineVariable(colIndex,&indexInfo,comboBox,nChild,TYP_INTEGER,1); return iRefer; }
First we make certain that this is a set for the Col property. Second, we need the root offset of the hostObject on the leftSide of the Set, because this offset is needed to form the name of the colIndex. Note that the actual lValue (left Value) includes the target offset as well, so we decrement it by one. If we do not get at least one offset -- meaning a simple local object variable -- we abort the migration. Third we need the code for the new Combo.setCol method to replace the setter. Fourth, we add a declaration of a variable to the front of the method code if that variable is not yet declared. The variable colIndex is a string variable
static char* colIndex = "%1d_colIndex";
which defines the template for the name of the variable. If a new declaration is added then the method returns the length in bytes of that declaration. This value is needed and returned to the calling code scanner so that it can reposition itself. If no declaration is added, because one already exists, then defineVariable return zero. Implementing this handler introduces the variable declaration and makes the expected change in the setter,
int cmbName_colIndex = 0; ***** unmigrated cmbName.Col = 0; ***** migrated cmbName_colIndex = 0;
Changing the Column Property Sets
As in the previous examples the new methods are first added to the migClass.
<method id="SetColWidth" type="void" nPram="2" migPattern="%1d.MultiColumnComboBoxElement.Columns[%1d_colIndex].Width = %2d\c" > <argument id="rightSide" type="Integer" status="ByVal"/> </method> <method id="SetColName" type="void" nPram="2" migPattern="%1d.MultiColumnComboBoxElement.Columns[%1d_colIndex].Name = %2d\c" > <argument id="rightSide" type="String" status="ByVal"/> </method>
The actual transformation code is straightforward. Only the code for Width is shown, the others are the same.
#ifdef FPROTOTYPE static int ColWidth(int subRoot,int target,int iRefer,int* qeStack) #else static int ColWidth(subRoot,target,iRefer,qeStack) int subRoot; int target; int iRefer; int* qeStack; #endif { auto tCodeGroup indexInfo; auto int iRet; auto int iStart; indexInfo.target = target; 1) iRet = isPropertySetter(subRoot,target,&indexInfo,iRefer,qeStack); if(iRet == 0) return 0; 2) iStart = callDerivedMethod(compAddr[COMBO_SETCOLWIDTH],&indexInfo); replaceCodeGroup(iStart,&indexInfo); return 1; }
First make certain its a property setter and then do the replacement.
The AuthorProjectFile Event Handler
gmNI:
hor.Vb6AuthorProject()
The AuthorClassFile Event Handler
gmNI:
AspAuthor.AspAuthor() AspAuthor.LocAuthorPageTop() Author.Vb6AuthorProject()
The CallLateBinding Event Handler
gmNI:
engbasic.EngBasic()
The ExecuteCommand Event Handler
gmNI:
Whenever the translation tool encounters an XML command in a translation script that it does not recognize it executes the ExecuteCommand() event handler in each loaded runtime DLL that has this handler defined in the order that the DLLs were loaded. If one of these handlers returns a non-zero value, then the tool assumes that the XML command was processed by that DLL and ends its search. If no DLL indicates that it has processed the command, then the tool issues a command not found error.
For this example assume a command "FindDimAsNew" is to be implemented. This actual command is discussed in the section on the DimAsNew Runtime Dll. This discussion is only concerned with the command key. The actual implementation of the command would have the following prototype:
void ExecuteCommand_FindDimAsNew(char* command,int nCommand);
which is called by the actual exported ExecuteCommand() handler with the following prototype:
DLL_EXPORT int ExecuteCommand(char* command,int nCommand);
The command character buffer contains the actual command as entered in the translation script in null-terminated form. All leading and trailing blanks have been removed. In this example it might contain
<FindDimAsNew site="C:\fkgtest\ClientCode\local" />
The nCommand integer specifies the overall size of the command buffer. By convention in the script processing logic all methods use this same command buffer for storing the commands read from the script. Even though the actual memory for the command buffer is allocated in tool memory, it can be freely used in the handler so long as its size is not exceeded.
The implementation of the handler is highly patterned and should always have a form similar to the following:
DLL_EXPORT int ExecuteCommand(char* command,int nCommand) { auto int iRet; (a) selectFlags = (tVb7Flags*)(Select_GetFlags()); (b) if(String_Compare(command,"<FindDimAsNew",13) == 0) { iRet = 1; ExecuteCommand_FindDimAsNew(command,nCommand); } else iRet = 0; return iRet; }
This handler could be added to any Runtime DLL currently being loaded or could be placed in the main control logic of a separate DLL.
The first step (a) retrieves the setting of the Selection properties. Almost all implementations need to get values of some of these properties, so it is always a good idea to include this operation in all event handlers.
The second step (b) checks for the specified Xml-style command keyword with the '<' character immediately before the command word. If there is already an ExecuteCommand() handler present, the above logic could be added with an else if. Again, the actual acceptance and execution of the command is triggered entirely by its initial command word. If it is matched, the handler return value is set to one and the implementation method is executed. If it is not matched, the return value is set to zero. The zero setting tells the tool to continue its search.
The FinishAnalyser Event Handler
gmNI:
The EditAspSource Event Handler
gmNI:
The GetSpecialName Event Handler
gmNI:
The AuthorDeclaration Event Handler
gmNI:
Defn.AuthorDefinition()
The AuthorLibraryStub Event Handler
gmNI:
Wrapper.AuthorLibraryStubs()
The EditProgId Event Handler
gmNI:
The CompileScript Event Handler
gmNI:
The EditStatement Event Handler
gmNI: