Using the Model

To use ObjAsm a few rules must be followed. In the following lines, they are explained in detail.

System setup

All ObjAsm applications start including the Model.inc file and calling the SysSetup macro. Here is where the framework is customized to support a user interface or not, the application bitness, the debugging support and so on.

The SysInit macro should be the first code sequence to execute. It initializes all framework runtime variables. Similarly, the last executed code sequence should be the SysDone macro just before the ExitProcess API is called. This macro releases all resources assigned by the framework.

Object definition

To declare an object, the Object macro is used, as shown in the following example from Demo01.inc file:

21	Object Shape, ShapeID, OA:Primer
22	    VirtualAbstract      GetArea
23	    RedefineMethod   Init, DWORD, DWORD
24	
25	    DefineVariable   dBase,    DWORD, 0
26	    DefineVariable   dHeight, DWORD, 0
27	ObjectEnd

The first line (21) starts an object definition called Shape. An ID (DWORD) defines its type, which is in this case ShapeID (i.e. 2). Finally, after the type ID, the ancestor object from which this object is derived is declared. In this particular case, OA:Primer defines a destructor called Done, that does not perform any operation, but ensures that the first position in the virtual method table (VMT) of all Primer inherited objects is always filled with this method. To implement some action, Done must be overridden.

OA:Primer, OA is the declaration of the object namespace used to identify the Primer object.

The next line (22) defines a virtual abstract method called GetArea. An abstract method is a placeholder that has to be overwritten in descendant objects to be called. Line 23 defines a virtual method, called Init, which will be used as a constructor to initialize the object data. It has two parameters of DWORD type.

Line 25 and 26 define two object variables called dBase and dHeight, both of DWORD type and initialized to zero.

Object instantiation

ObjAsm can create and setup object instances in different ways. There are two types of instances, dynamic and static.

Dynamic instances

The most common case is the dynamic allocation of the object instance. It is started with the New and ended with the Destroy macro. New allocates memory from the process heap using the MemAlloc macro. This memory is immediately initialized copying the data from a template stored in the data segment. The information contained in this template is generated at compile time using the object definition. This ensures that all instances are created with consistent data that the constructor can work with. After the object has completed his purpose, the destructor must be called to release the resources allocated by the constructor. Finally the instance memory must be freed again. These last steps are done by the Destroy macro, which automatically calls the default destructor (Done) and releases the instance memory using the MemFree macro.

New Shape
.if eax != NULL
    mov esi, eax
    OCall esi::Shape.Init, 10, 15
    .if eax == OBJ_OK

    Destroy esi
.endif

New returns a pointer to the object instance. Depending on the target bitness setting, this vale is returned in the rax or eax register.

An object instance can also be placed on the memory stack like a local variable. In this case, the New macro can also be used, but it skips the allocation process and continues with the memory initialization from the object template. When the method or procedure is about to return, the epilogue code frees the stack where the object instance resides. This means that the Destroy macro should never be used in these situations. On the other hand, the destructor must be called explicitly to free allocated resources.

Method …
    local AboutDlg:$Obj(DialogAbout)

    New AboutDlg::DialogAbout
    OCall AboutDlg::DialogAbout.Init, xsi, [xsi].hWnd, NULL, offset szAboutText
    OCall AboutDlg::DialogAbout.Show
    OCall AboutDlg::DialogAbout.Done

Static instances

A static instance is memory reserved in the .data or .data? segment, like a global variable.

When the object instance is placed on the .data segment, the $ObjInst macro is used to create a copy of the object template at compile time.

.data
MyShape $ObjInst(Shape)

When the object instance is placed on the .data? segment, only memory reservation is required and the New macro cares at runtime for the initialization.

.data?
MyShape $Obj(Shape)  {}

.code
New MyShape::Shape

In these last two cases, as with instances on the stack, only the destructor has to be called when the object is no longer needed.

The object template, from which objects instances get their initialization data, can also be considered as a static instance. This is especially interesting if we have only a single object instance in the application. In this case, we can use the object template as a static instance. Usually this is the case when the SdiApp, MdiApp, DlgApp or one of its descendant objects are used as the main application.

The object template can be accessed using the $ObjTmpl() macro. To call a method of the object template, the following code can be used:

OCall $ObjTmpl(Shape)::Shape.Init

Some objects destroy themselves when the OS destroy the window object associated with them. This behaviour works only on dynamic instances. For this reason, static instances such as SdiApp, MdiApp, DlgApp or their descendants must be explicitly destroyed.

Namespaces

Namespaces were introduced to avoid name clashes between API definitions or third party modules. Namespaces can be used for object definitions. Interfaces and the VMT of the DMT are not affected.

Object members names are automatically mangled to avoid such conflicts. The same applies to interface members.

The regular syntax to reference object is

Instance::Namespace:Object.Method Parameters

In some cases, parts of the reference can be omitted and default values are used. I.e. if Instance is omitted inside a method, the pSelf value is used. The same way, if the object name is omitted, the name is inferred from the method definition where the reference is used. See simplified syntax.

If not specified, the namespace is the default, called “OA”. To use another namespace use the ObjNamespace macro.

Adding objects to the application

There are three ways to add a new object to the application.

  1. Using the MakeObjects macro

  2. Using the LoadObjects macro

  3. Using the include statement

Using the MakeObjects macro, the whole file that defined the object is included into the project. The object definition is created and all the methods are compiled.

Using the LoadObjects macro, the object must be precompiled beforehand into a static library. A tool like OATools can be used for that purpose. This technique has the advantage of improving compilation speed.

Using the include statement, the assembler adds the whole file to the application. It has the same effect as using the MakeObjects macro, but compilation switches can be set manually. This is useful for object compilation files. See the .asm files in the “Code\Objects” directory.

The MakeObjects and LoadObjects macros accept multiple arguments for processing many objects in the same call. This way it is possible to write all objects of an inheritance chain in one single line.

Object static library

To improve the application compilation speed, it is possible to precompile object methods into a static library (.lib file). The OATools application can be used for that purpose.

When these precompiled object methods are loaded, we have to avoid a redefinition with their original code. Since the object definition is always required, a conditional compilation switch between the object definition and the object's methods is added using the IMPLEMENT symbol.

Example:

         Object MyObject,, Primer
           VirtualMethod  MyMethod,  dword
         ObjectEnd
         
-->	if IMPLEMENT
	
	Method MyObject.MyMethod, uses esi, MyParam:DWORD
	  SetObject esi
	  ...
	MethodEnd
	
-->	endif

An .asm file has to be created to compile each object, indicating to the assembler the complete code dependency, all ancestor objects and switches.

Object variables

Object members used to store values are called object variables. These variables can be of any type known by MASM® like DWORD, WORD or BYTE or any custom type like structures. The DefineVariable macro is used to declare a variable, its type and initial value.

Example:

Object MyObject,, Primer
  DefineVariabbe  MyVar,  dword,  100
ObjectEnd

or if you don’t care about the initial value

Object MyObject,, Primer
    DefineVariabbe  MyVar,  DWORD,  ?
ObjectEnd

You can also use structures as variables. Example:

Data struc
    var1    DWORD    ?
    var2    DWORD    ?
    var3    DWORD    ?
Data ends

Object MyObject,, Primer
    DefineVariable  MyData,  Data,  {0,100,20}
ObjectEnd

or

Object MyObject,, Primer
    DefineVariable  MyData,  Data,  {}
ObjectEnd

In this case, the structure variables remain uninitialized.

It is allowed to use the MASM® DUP operator using the following syntax:

Object MyObject,, Primer
    DefineVariable  MyVar,  dword,  20 DUP(0)
ObjectEnd

or

Object MyObject,, Primer
    DefineVariable  MyVar,  dword,  20 DUP(?)
ObjectEnd

Only the initial variable value can be changed in a descendant object using the RedefineVariable macro. Example:

//Object MyDescendant,, MyObject
    RedefineVariable  MyVar,  250
ObjectEnd

The variable type cannot the modified!

Method definitions

A method definition follows the same rules for MASM® procedures. They begin with the Method macro invocation followed by the method name, the procedure modifiers like the calling convention and the uses clauses and finally the argument list. This has to correspond exactly with the arguments declared in the object declaration.

The method code is finalized by the MethodEnd macro, which contains an implicit return instruction.

The Method macro hides the instance pointer called pSelf, which is always passed as first argument of the call. To assign this pointer to a register, the SetObject macro has to be used. To finally unassign it, the ReleaseObject macro has to be used. If it was not done before, the MethodEnd macro automatically calls ReleaseObject.

The MethodEnd macro also contains a label called @@EOM (End Of Method), which simplifies the method exit call. A handy macro called ExitMethod was defined to jump to this label. A conditional jump is possible too, adding the jump condition to the macro invocation, i.e.:

ExitMethod .if eax == 0

This macro is a convenience that is neither optimized for code size nor speed.

Method scope

Within a method definition, all object members can be called without specifying the object type. The method's object type is assumed by default.

Private methods are only allowed to be called from within method definitions of the same object type or a descendant. Any attempt to call a private method from a different scope will end in a compilation error.

Method calls

The complete syntax to call a method is as follows:

OCall InstancePointer::NameSpace:ObjectName.MethodName, Arguments

The first parameter is the object instance pointer, the second, separated by a double colon, is the object namespace followed by a single colon and the object name. Finally, the method name, separated by a dot followed by the method arguments.

Depending on the context, some parts of this syntax can be omitted. If the invocation is done within a method implementation, if the instance pointer is omitted, the hidden pSelf POINTER is taken. In addition, if the object namespace is omitted, the current is assumed. Equally for the object name.

This flexible syntax save typing time and enhances the readability of the code.

In some cases, when you have overwritten a dynamic method, you would call the original method. This can be achieved using a call to the object's template where the original method address is still stored:

TCall esi::Triangle.GetArea

On static methods, a TCall has no effect, since an OCall executes a TCall for this type of methods.

In other situations, you will need to call the method of the direct ancestor. This is frequently happens in constructor and destructor methods. You can accomplish this easily by using the ACall macro:

ACall esi::Triangle.GetArea

In this example, this line calls the Shape's GetArea method.

Internally ACall performs a TCall on the ancestor's template. In this way, if the ancestor's method is a dynamic method that was overridden, ACall uses the original method address stored in the template!

The last possible way to call a method is directly, circumventing the information stored in its virtual method table. It is the fastest way to invoke a method, but you lose the OOP polymorphism ability. A direct call follows the same syntax as the previous method calls but using the DCall macro.

All method calls in x86 mode use the STDCALL calling convention with automatic prologue and epilogue generation. In x64 mode, the FASTCALL calling convention is used.

ObjAsm automatically adjust the stack for variable parameter count (vararg).

Like other ObjAsm macros, the method calls can be preceded by "$" to return the rax or eax register, used to return a value. The register width depends on the target bitness.

Static or local objects can be called using their symbol in place of the pointer to them. ObjAsm computes internally the pointer in the most efficient way to perform the method call.

local Object1:$Obj(MyObject)
 
New Object1
OCall Object1::MyObject.Init, 120

Method redefinition

All methods types previously defined can be redefined. A method redefinition means that an object descendant modifies the behaviour of a method by rewriting it. Even more, it is possible to change the method arguments. This is especially useful for constructors, but you must be aware, that methods that have different number of arguments cannot be called polymorphically.

A special case is the invalidation of a method. This is done using the ObsoleMethod macro. It assigns the value 0FFFFFFFFh to the method address placeholder (VMT / IMT / Instance). If the method has an associated Event that triggers the method, this event was also removed from the Event Table.

Abstract methods

Sometimes it is necessary to reserve a method at an ancestor level that are implemented later in its descendants. This strategy makes it possible to call this method for all descendant in the same way (polymorphism), independently of other changes in the structure of the derived objects.

The same applies to common variables. If you plan to use polymorphism, try to place all common variables in the common ancestor object.

The address of an abstract method is set to 0FFFFFFFFh. Any try to call it will end in an exception.

Syntax simplification

In some cases, depending on the current scope, it is possible to omit some parameters of the method call.

If the method call is done within a method implementation and the instance pointer is omitted, the pSelf pointer is assumed.

In this same context, if the object name (second parameter) is omitted, the current object is assumed.

Method ObjectX.Get, uses esi, dIndex:dword
    SetObject esi
    OCall CheckIndex, dIndex

In this example, the OCall is interpreted as:

OCall pSelf::ObjectX.CheckIndex, dIndex

A little faster alternative syntax, but with the same result would be:

OCall xsi.CheckIndex, dIndex

Method prologue and epilogue

In 32-bit mode, methods usually use the standard prologue and epilogue supplied by MASM®, but when the TRACE option is activated using the SysSetup macro, the prologue and epilogue are modified to perform method tracing and performance monitoring. Be aware of this when you supply your own prologue or epilogue code.

Methods in 32-bit mode also support an option calledNOFRAME that disables the prologue and epilogue generation. Normally this is equivalent to prologue:none and epilogue:none MASM® directives, but if the TRACE option is activated, only the tracing code is inserted. It is the coder responsibility to adjust the stack and to access the method parameters the right way!

Path customization

The “Code” folder contains a file called OA_SetupXX.inc that defines all standard paths used by the ObjAsm programming model.

Conventions

To maintain certain degree of consistency over the whole model, some conventions has been taken:

  • Objects were written in .inc files.

  • Compilation files were written in .asm files.

  • Object IDs are located together in the ObjIDs.inc file. Custom IDs should be maintained in a separate file like MyObjIDs.inc.

  • Objects error codes are located together in the ObjErrs.inc. Custom code should be maintained in a separate file.

All objects that support steaming capabilities (derived from Streamable) MUST have an ID.

Last updated