Overview of interfaced types.
LibGimbal's object model supports the concept of the C# or Java-style "Interface," which is a polymorphic type used to model abstract behavior on a class or object.
This is fancy object-oriented speak for allowing you to define a set of methods which can be implemented by any class which can then be queried for later. The main advantage of modeling overridable methods with this approach is that it doesn't require a type to inherit or derive from a common subtype. Any class inheriting from any other can implement any number of interfaces and inherit their implementations from parent classes.
LibGimbal interfaces support:
- abstract overridable methods
- overridable methods with a default implementation
- static members
- implementing other interfaces
- inheriting from other interfaces
Declaring
All interfaces in libGimbal derive from the base type, GBL_INTERFACE_TYPE. This is a class-only, abstract type which defines the base class which we will use: GblInterface.
Structures
We can create our own interface class sructure by deriving from GblInterface:
GBL_INTERFACE_DERIVE(ISerializable)
GBL_INTERFACE_END
Mutable string type optimized for building and writing.
- Note
- It is not a hard requirement that interface methods must return a GBL_RESULT type; however, this can be extremely convenient for propagating errors should one occur within the implementation. Since these methods are not what are typically called by a user, it doesn't make your API any uglier to do so (see the next section).
Macro Utilities
As is typical with most libGimbal types, it is often most convenient on the user (and you) to define a set of common macro operators for working with your type.
For our serializable interface, we will use the following:
#define ISERIALIZABLE_TYPE (GBL_TYPEID(ISerializable))
#define ISERIALIZABLE(instance) (GBL_CAST(instance, ISerializable))
#define ISERIALIZABLE_CLASS(klass) (GBL_CLASS_CAST(klass, ISerializable))
#define ISERIALIZABLE_GET_CLASS(instance) (GBL_CLASSOF(instance, ISerializable))
Public Methods
Typically, when working with interface methods, we would rather provide a user-friendly API function wrapping the virtual method and handling any errors, rather than making a user reach into an interface and call a function pointer directly.
We do this for our virtual save method:
GBL_CTX_BEGIN(NULL);
ISerializableClass* pClass = ISERIALIZABLE_GET_CLASS(pSelf);
if(pClass) {
if(pClass->pFnSave) {
GBL_CTX_VERIFY_CALL(pClass->pFnSave(pSelf, pBuffer);
}
}
GBL_CTX_END();
}
As you can see, when we expose our virtual methods via a public API wrapper, the entry-point becomes prettier than calling directly into a function pointer, and we are able to do type checking and error handling. We can check to see whether the given instance was even compatible with our interface or whether the class implementing the interface actually overrode the method or not.
Registering
Once we've defined our structures, created our utility macros, and created a public API around our virtual methods, it's time to register our type. To do this, we implement the ISerializable_type() function declared earlier to register a new meta type if we haven't already:
.classSize = sizeof(ISerializableClass)
},
}
return type;
}
#define GBL_INVALID_TYPE
GblType UUID of the invalid type.
@ GBL_TYPE_FLAG_ABSTRACT
Type cannot be instantiated without being derived.
GblType GblType_register(const char *pName, GblType baseType, const GblTypeInfo *pInfo, GblFlags flags)
Registers a new type with the given information, returning a unique identifier for it.
Provides type information when registering a new GblType.
- Note
- If we wish to provide a default implementation of our virtual methods, we would also set GblTypeInfo::pFnClassInit to a GblClassInitFn function where we would initialize our class structure with some default values.
Implementing
In order to use your interface with a given type, the type must implement the interface and then provide the meta type system with a mapping during type registration.
Structures
If we wish to implement our interface on another type, we embed it within that type's class structure. Here we will use the libGimbal macro DSL which will handle generating our structures for us.
GBL_CLASS_BASE(IntSerializable, ISerializable)
GBL_CLASS_END
GBL_INSTANCE_BASE(IntSerializable)
int integer;
GBL_INSTANCE_END
Overrides
Finally, lets create an implementation of the save and load functions from the ISerializableIFace class, along with a class constructor for initializing IntSerializableClass:
IntSerializable* pInt = (IntSerializable*)pSelf;
pInt->integer = GblStringView_toInt(GblStringBuffer_view(pBuffer));
}
IntSerializable* pInt = (IntSerializable*)pSelf;
return GblStringBuffer_appendInt(pBuffer, pInt->integer);
}
IntSerializableClass* pSelfClass = (IntSerializableClass*)pClass;
pSelfClass->iSerializable.pFnLoad = IntSerializable_load_;
pSelfClass->iSerializable.pFnSave = IntSerializable_save_;
}
Base struct for all type classes.
Top-level context object.
Registration
In order to register a type as having implemented an interface, we have to tell the meta type system how to "map" between the interface and the class. In order to achieve this, we pass an array of interface mappings to GblType_register() via GblTypeInfo.pInterfaceImpls:
GblType IntSerializableType_type(
void) {
.classSize = sizeof(IntSerializableClass),
.pFnClassInit = IntSerializable_initializeClass,
.instanceSize = sizeof(IntSerializable),
.interfaceCount = 1,
{
.interfaceType = ISERIALIZABLE_TYPE,
.classOffset = offsetof(IntSerializableClass, iSerializable)
}
},
}
return type;
}
#define GBL_INSTANCE_TYPE
Type UUID for GblInstance.
@ GBL_TYPE_FLAGS_NONE
Type which adds no additional flags beyond what's inherited.
Provides implementation details of a GblInterface for a type.
As you can see, we provided a single entry into the interface mapping, which we used to associate our interface type with the given class offset. Now the meta type system knows everything it needs to be able to cast to and from your interface!
Querying
Querying for interfaces is extremely simple. Since they're essentially a type of GblClass, you use the same set of functions you would use to cast between class types. Lets create an instance of the IntSerializable type and try to serialize it with our interface using the utility macros defined earlier to handle casting:
GblStringBuffer_construct(&buffer,
GBL_STRV(
"7"));
ISerializable_load(ISERIALIZABLE(pIntInstance), &buffer);
assert(pIntInstance->integer == 7);
pIntInstance->integer = -7;
GblStringBuffer_clear(&buffer);
ISerializable_save(ISERIALIZABLE(pIntInstance), &buffer));
assert(GblStringView_toInt(GblStringBuffer_view(&buffer)) == -7);
GblInstance * GblInstance_create(GblType type, size_t publicSize, GblClass *pClass)
Creates and returns an instance, optionally with an extended size and/or non-default class.
GblRefCount GblInstance_destroy(GblInstance *pSelf)
Destructs and deallocates an instance. It must have been created with GblInstance_create().
#define GBL_STRV(...)
Convenience shorthand macro for creating a GblStringView from an existing string or substring.
Querying for the GblInterface structure without having defined the convenience macros from the previous section is still possible, but it's much uglier and more verbose.
ISerializable* pSerializable = (ISerializable*)GblInstance_cast((
GblInstance*)pIntInstance,
GBL_ISERIALIZABLE_TYPE);
ISerializableIFace* pIFace = (ISerializableIFace*)GblClass_cast(pClass, GBL_ISERIALIZABLE_TYPE);
Base struct for all instantiable meta types.