Zen API
|
The Zen API can be extended by defining new type assemblies. This allows new values, classes, or interfaces to be defined using Zen's type system. Because Zen supports object-oriented and reflective programming techniques that are not directly supported in the C language, these features are provided through library support. In order to simplify the process of defining new types, Zen relies heavily on macros as syntactic aids.
Defining a new type assembly is fairly straightforward. Let's consider a new assembly called MyApi that contains a few value types and classes.
First, type assemblies are usually associated with a specific library (dll/so or lib). Due to DLL import/export rules in Visual C++, it is necessary to create import/export macros for each new library and to use these macros when declaring functions and data. For dynamic libraries, this can be accomplished with the following code:
And for static libraries or applications:
Next, to declare the MyApi assembly so that other code can construct and use the assembly, add the following line to a header file (e.g. MyApiLib.x.h):
The first argument to the kDeclareAssembly macro specifies the prefix for the MyApi function/data declaration macros (as defined above). The second argument specifies the name of the assembly.
Finally, the assembly is defined in a source file (e.g. MyApiLib.c):
The first argument to the kBeginAssemblyEx macro is the function/data declaration prefix for MyApi. The second argument specifies the name of the assembly and the third argument specifies the assembly version.
Assembly definition takes place between the kBeginAssemblyEx and kEndAssemblyEx macros. In the example above, the first line specifies an assembly dependency. Assembly dependencies inform the type system about inter-assembly relationships. In this case, the MyApi assembly depends on the kApi assembly. Specifying assembly dependencies ensures that assemblies are initialized in the correct order.
Next, the various values and types that are part of the MyApi assembly are added. There are no requirements about the order in which types are added during assembly definition. For example, it is valid to add a derived type before its base.
If a type is omitted from the assembly definition block, the type system will be unaware of its existence and the corresponding type value will be null. For example, if we forget to add MyThirdClass and then try to access its type information using kTypeOf(MyThirdClass), the kTypeOf macro will yield null.
Value types are declared using the following syntax:
Each value declaration consists of two parts; a declaration for the C compiler, and a corresponding declaration for the Zen type system (macro). By convention, Zen type declarations are placed in private headers (.x.h).
The macros to declare structures and enumerations have the same arguments. First, the function/data declaration prefix for the assembly. Second, the name of the type being declared, and third, the base from which the type descends. In most cases, value types descend from kValue, which is the root of all Zen value types.
Value types are defined using the following syntax:
The first argument to the kBeginValueEx/kBeginEnumEx macros is the function/data declaration prefix for MyApi. The second argument specifies the name of the type and the third argument specifies the type base.
Type definition takes place between the begin/end macros. For structures, the fields can be defined, and for enumerations, the enumerator values can be defined. For simple types that have limited use, field and enumerator definitions can be omitted; these optional definitions enable the type system to provide better services via reflection, but aren't strictly required.
Class declarations come in multiple forms. A full class declaration requires all elements of the class declaration to be provided by the programmer, while other kinds of class declarations allow some elements to be omitted. In most cases, simpler declaration alternatives (discussed at the end of this section) are appropriate, but in order to understand the details, we will examine full class declarations first.
Full class declarations use the following syntax:
A full class declaration requires C structure definitions for the class instance (MyTypeClass), static data (MyTypeStatic), and virtual table (MyTypeVTable).
A class instance structure represents data fields that will be part of each MyType instance. The first field of any Zen class should always be the class structure for its base type (and the field name must be base); this arrangement is used to support data inheritance.
A virtual table structure is used to store pointers for inherited and/or overridden methods. The first field of any Zen virtual table should always be the virtual table structure for its base type (and the field name must be base); this arrangement is used to support method inheritance.
In addition to class instances, the Zen type system also supports static data for each class. Static fields represent variables that are shared between all instances of a class.
The kDeclareFullClassEx macro completes the class declaration. The first argument to kDeclareFullClassEx specifies the function/data declaration prefix for the assembly (My), the second argument specifies the name of the type being declared (MyType), and the third argument specifies the base from which the type descends (kObject).
Full class definitions require the following syntax:
Virtual method overrides are specified using the kAddVMethod or kAddPrivateVMethod macros, which take three arguments: the overriding type, the overridden type, and the virtual method name. In the example above, the MyType class overrides the VRelease method from the kObject class, and provides one virtual method of its own (VMethodA). The only difference between kAddVMethod or kAddPrivateVMethod is that kAddPrivateVMethod expects the function name to begin with an 'x' – an informal convention for indicating that a method name is private.
The implementations of the various methods required by the MyType class are shown below:
In the example above, the public MyType_Construct method dynamically allocates a MyTypeClass instance using the supplied memory allocator, and then calls the xMyType_Init method to initialize the instance. After allocation and initialization, a handle(opaque pointer) to the MyTypeClass instance is returned to the caller. This handle is of type MyType, which evaluates to void*, preventing users from directly accessing the private fields defined in MyTypeClass. When a MyType handle is passed to any method in the MyType class, the handle is cast to a MyTypeClass pointer, so that internal instance fields can be accessed. By convention, the instance pointer (or this pointer) used in each method is named obj. Helper macros are used to declare this local variable.
Every class that has static data must declare a pair of methods to initialize and release the static data. Zen macros assume that these methods will be called x[TypeName]_InitStatic and x[TypeName]_ReleaseStatic. Static initialization methods are called at assembly load time, while static release methods are called at assembly unload time. The static data for any class can be accessed using the kStaticOf macro; by convention static data pointers are named sobj.
While full class declarations provide control over all declaration elements, they are overkill for most classes. Accordingly, simpler macros are available:
kDeclareClassEx/kBeginClassEx/kEndClassEx
Virtual table and static data declarations (and static methods) can be omitted.
kDeclareVirtualClassEx/kBeginVirtualClassEx/kEndVirtualClassEx
Static data declarations (and static methods) can be omitted.
kDeclareStaticClassEx/kBeginStaticClassEx/kEndStaticClassEx
Class instance and virtual table declarations can be omitted.
The kDeclareClassEx macro is the most common way to declare a class. Using this approach, the virtual table is assumed to be identical to that of the base class, and static data is not available. Virtual methods defined in base classes can still be overridden using the kAddVMethod macro. The kDeclareVirtualClassEx macro is useful when a class must declare new virtual methods for child classes to override, but does not require static data. The kDeclareStaticClassEx macro is useful when a class contains only static data (e.g., a class that represents a collection of static methods, where variables are shared between the methods).
Zen interfaces are similar to abstract base classes containing no data fields; essentially, an interface contributes a virtual table to classes that implement the interface. In this way, interfaces can be used to provide a limited form of multiple inheritance.
Interface declarations use the following syntax:
An interface declaration requires only a virtual table (MyTypeVTable); the kDeclareInterfaceEx macro completes the interface declaration. The first argument to kDeclareInterfaceEx specifies the function/data declaration prefix for the assembly (My), the second argument specifies the name of the type being declared (MyType), and the third argument specifies the base from which the type descends (in this case, a special symbol called kNull, because the interface does not extend another interface).
As with classes, an interface's virtual table structure is used to store pointers for inherited and/or overridden methods. If an interface extends another interface (atypical), then the first field of the virtual table should be the virtual table of the base type (the field name should be base).
Interface definitions require the following syntax:
As with classes, virtual methods are specified using the kAddVMethod macro.
Example implementations of the various methods required by the MyType interface are shown below:
To implement an interface in a class, use the syntax shown below: