Zen API

- Home
- Topics
- Types
- Files

All Classes Files Functions Variables Typedefs Friends Macros Modules Pages
Extending Zen

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.

# Assemblies

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:

#if defined (MY_API_EMIT)
# define MyFx(TYPE) kExportFx(TYPE)
# define MyDx(TYPE) kExportDx(TYPE)
#else
# define MyFx(TYPE) kImportFx(TYPE)
# define MyDx(TYPE) kImportDx(TYPE)
#endif

And for static libraries or applications:

#if defined (MY_API_EMIT)
# define MyFx(TYPE) kInFx(TYPE)
# define MyDx(TYPE) kInFx(TYPE)
#else
# define MyFx(TYPE) kInFx(TYPE)
# define MyDx(TYPE) kInFx(TYPE)
#endif

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):

//MyApiLib.x.h
kDeclareAssembly(My, MyApi)

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):

//MyApiLib.c
kBeginAssembly(My, MyApi, MY_API_VERSION)
//Assembly dependencies
kAddDependency(kApi)
//Values
kAddType(MyEnum)
kAddType(MyStruct)
//Classes
kAddType(MyFirstClass)
kAddType(MySecondClass)
kAddType(MyThirdClass)
kEndAssembly()

The first argument to the kBeginAssembly 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 kBeginAssembly and kEndAssembly 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.

# Values

Value types are declared using the following syntax:

/* MyTypes.h */
//C structure declaration
typedef struct MyStruct
{
k32s fieldA;
k32s fieldB;
} MyStruct;
//C enum declaration
typedef k32s MyEnum;
#define MY_ENUM_VALUE_A (123)
#define MY_ENUM_VALUE_B (456)
/* MyTypes.x.h */
//Zen structure declaration
kDeclareValue(My, MyStruct, kValue)
//Zen enum declaration
kDeclareEnum(My, MyEnum, kValue)

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:

/* MyTypes.c */
//Zen structure definition
kBeginValue(My, MyStruct, kValue)
kAddField(MyStruct, k32s, fieldA)
kAddField(MyStruct, k32s, fieldB)
kEndValue()
//Zen enum definition
kBeginEnum(My, MyEnum, kValue)
kAddEnumerator(MyEnum, MY_ENUM_VALUE_A)
kAddEnumerator(MyEnum, MY_ENUM_VALUE_B)
kEndEnum()

The first argument to the kBeginValue/kBeginEnum 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.

# Classes

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:

/* MyType.h */
typedef kObject MyType;
MyFx(kStatus) MyType_Construct(MyType* my, kAlloc allocator);
MyFx(kStatus) MyType_MethodA(MyType my, k32s arg);
MyFx(kStatus) MyType_MethodB(MyType my, k64u arg);
/* MyType.x.h */
//C class declaration
typedef struct MyTypeClass
{
kObjectClass base; //base class
k32s fieldA; //instance field
k64u fieldB; //instance field
} MyTypeClass;
//C static declaration
typedef struct MyTypeStatic
{
kAtomic32s accessCounter; //static field;
} MyTypeStatic;
//C virtual table declaration
typedef struct MyTypeVTable
{
kObjectVTable base; //base virtual table
kStatus (kCall* VMethodA)(MyType my, k32s arg); //virtual method added by MyType
} MyTypeVTable;
//Zen full class declaration
kDeclareFullClass(My, MyType, kObject)
//Private method declarations; used in the implementation of MyType
MyFx(kStatus) MyType_InitStatic();
MyFx(kStatus) MyType_ReleaseStatic();
MyFx(kStatus) MyType_Init(MyType my, kType type, kAlloc alloc);
MyFx(kStatus) MyType_VRelease(MyType my);
//Convenient macros to access object instance fields, virtual table, or static fields.
//In particular, kMyType_Cast_ will generate an assertion (in debug builds) if the type of
//of the object doesn't match expectations.
#define kMyType_Cast_(OBJ) kCastClass_(kMyType, OBJ)
#define kMyType_VTable_(OBJ) kCastVTable_(kMyType, OBJ)

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 kDeclareFullClass macro completes the class declaration. The first argument to kDeclareFullClass 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:

/* MyType.c */
//Zen full class definition
kBeginFullClass(My, MyType, kObject)
kAddVMethod(MyType, kObject, VRelease) //MyType overrides kObject_VRelease
kAddVMethod(MyType, MyType, VMethodA) //MyType provides a new virtual method (VMethodA)
kEndFullClass()

Virtual method overrides are specified using the kAddVMethod macro, which takes 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 implementations of the various methods required by the MyType class are shown below:

/* MyType.c */
//static initialization; called by type system at assembly load time
MyFx(kStatus) MyType_InitStatic()
{
MyTypeStatic* sobj = kStaticOf(MyType);
//static data is always zero-initialized by the type system, so the
//initialization statement below isn't strictly necessary
sobj->accessCounter = 0;
return kOK;
}
//static release; called by type system at assembly unload time
MyFx(kStatus) MyType_ReleaseStatic()
{
MyTypeStatic* sobj = kStaticOf(MyType);
kLogf("Total access count: %d", sobj->accessCounter);
return kOK;
}
//instance constructor; allocates and initializes a new MyType object
MyFx(kStatus) MyType_Construct(MyType* my, kAlloc allocator)
{
kAlloc alloc = kAlloc_Fallback_(allocator); //gets the default allocator if kNULL given
kStatus status;
//allocate memory for a MyType class instance structure (object)
kCheck(kAlloc_GetObject(alloc, kTypeOf(MyType), my));
//call an instance initializer for the MyType object
if (!kSuccess(status = kMy_Init(*my, kTypeOf(MyType), alloc)))
{
//free object memory in the event of failure
kAlloc_FreeRef(alloc, my);
}
return status;
}
//instance initializer; initializes instance variables, allocates any necessary resources
MyFx(kStatus) MyType_Init(MyType my, kType type, kAlloc alloc)
{
MyTypeClass* obj = my; //an "obj" variable acts like a "this" pointer
kStatus exception;
//initialize base as first action
kCheck(kObject_Init(my, type, alloc));
//next, initialize instance fields such that it would be safe to call MyType_VRelease
obj->fieldA = 0;
obj->fieldB = 0;
//next, perform any complex initializations
kTry
{
kTest(kLogf("There's nothing left to initialize for this class\n"));
}
kCatch(&exception)
{
//in the event of failure, perform any necessary clean-up
MyType_VRelease(my);
kEndCatch(exception);
}
return kOK;
}
//virtual method implementation; overrides kObject_VRelease; called by kObject_Destroy/kObject_Dispose
MyFx(kStatus) MyType_VRelease(MyType my)
{
MyTypeClass* obj = kMyType_Cast_(my); //will assert (in debug builds) if type is not MyType
//free any resources held by this object
kCheck(kLogf("There's nothing to free for this class.\n"));
//uninitialize base as final action
kCheck(kObject_VRelease(my));
return kOK;
}
//public interface to MethodA virtual method; looks up method from virtual table and invokes it
MyFx(kStatus) MyType_MethodA(MyType my, k32s arg)
{
MyTypeClass* obj = kMyType_Cast_(my);
return kMyType_VTable_(obj)->VMethodA(my, arg);
}
//MethodA virtual method implementation; can be overridden by classes that extend MyType
MyFx(kStatus) MyType_VMethodA(MyType my, k32s arg)
{
MyTypeClass* obj = kMyType_Cast_(my);
MyTypeStatic* sobj = kStaticOf(MyType);
//just for fun, we'll count the number of times that this method
//has been called, using a static field
kCheck(kAtomic32s_Increment(&sobj->accessCounter));
obj->fieldA = arg;
return kOK;
}
//non-virtual method
MyFx(kStatus) MyType_MethodB(MyType my, k64u arg)
{
MyTypeClass* obj = kMyType_Cast_(my);
obj->fieldB = arg;
return kOK;
}

In the example above, the public MyType_Construct method dynamically allocates a MyTypeClass instance using the supplied memory allocator, and then calls the MyType_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.

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 [TypeName]_InitStatic and [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:

kDeclareClass/kBeginClass/kEndClass
Virtual table and static data declarations (and static methods) can be omitted.

kDeclareVirtualClass/kBeginVirtualClass/kEndVirtualClass
Static data declarations (and static methods) can be omitted.

kDeclareStaticClass/kBeginStaticClass/kEndStaticClass
Class instance and virtual table declarations can be omitted.

The kDeclareClass 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 kDeclareVirtualClass macro is useful when a class must declare new virtual methods for child classes to override, but does not require static data. The kDeclareStaticClass 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).

# Interfaces

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:

/* MyType.h */
typedef kObject MyType;
MyFx(kStatus) MyType_MethodA(MyType my, k32s arg);
MyFx(kStatus) MyType_MethodB(MyType my, k64u arg);
/* MyType.x.h */
//C virtual table declaration
typedef struct MyTypeVTable
{
kStatus (kCall* VMethodA)(MyType my, k32s arg); //virtual method added by MyType
kStatus (kCall* VMethodB)(MyType my, k64u arg); //virtual method added by MyType
} MyTypeVTable;
//Zen interface declaration
kDeclareInterface(My, MyType, kNull)
//convenience macros, to access class instance and virtual table
#define kMyType_Cast_(OBJ) kCastClass_(kMyType, OBJ)
#define kMyType_VTable_(OBJ) kCast(MyTypeVTable*, kType_IVTable_(kObject_Type_(OBJ), kTypeOf(MyType)))

An interface declaration requires only a virtual table (MyTypeVTable); the kDeclareInterface macro completes the interface declaration. The first argument to kDeclareInterface 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:

/* MyType.c */
//Zen interface definition
kBeginInterface(My, MyType, kNull)
kAddVMethod(MyType, MyType, VMethodA) //MyType virtual method (VMethodA)
kAddVMethod(MyType, MyType, VMethodB) //MyType virtual method (VMethodB)
kEndInterface()

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:

/* MyType.c */
//public interface to MethodA virtual method; looks up method from virtual table and invokes it
MyFx(kStatus) MyType_MethodA(MyType my, k32s arg)
{
MyTypeClass* obj = kMyType_Cast_(my);
return kMyType_VTable_(obj)->VMethodA(my, arg);
}
//default implementation of VMethodA; will be overridden by classes that implement this interface
MyFx(kStatus) MyType_VMethodA(MyType my, k32s arg)
{
return kOK;
}
//public interface to MethodB virtual method; looks up method from virtual table and invokes it
MyFx(kStatus) MyType_MethodB(MyType my, k64u arg)
{
MyTypeClass* obj = kMyType_Cast_(my);
return kMyType_VTable_(obj)->VMethodB(my, arg);
}
//default implementation of VMethodB; will be overridden by classes that implement this interface
MyFx(kStatus) MyType_VMethodB(MyType my, k64u arg)
{
return kOK;
}

To implement an interface in a class, use the syntax shown below:

kBeginClass(My, Other, kObject)
//virtual methods
kAddVMethod(Other, kObject, VRelease)
//MyType interface
kAddInterface(Other, MyType)
kAddIVMethod(Other, MyType, VMethodA, MethodA)
kAddIVMethod(Other, MyType, VMethodB, MethodB)
kEndClass()
//...other implementation...
//implementation of MyType_MethodA
MyFx(kStatus) Other_MethodA(Other other, k32s arg)
{
OtherClass* obj = other;
obj->fieldA = arg;
return kOK;
}
//implementation of MyType_MethodB
MyFx(kStatus) Other_MethodB(Other other, 64u arg)
{
OtherClass* obj = other;
obj->fieldB = arg;
return kOK;
}

See also
  kAssembly, kValue, kObject