Interfaces can be quite handy if you have a more complex multi-modular process that wants to share API between each module, so that there aren't independent copies of the API inside each module. However, how to implement this in practice? Well, one way is through using interfaces.
Definition of an C++ interface
An interface is essentially an abstract class with a virtual destructor that has a separate class that implements it. A pure virtual class that cannot be instantiated, hence the class that implements it.
In GoldSrc, the topmost class that every interface must derive from is called IBaseInterface. It only contains the already mentioned virtual destructor. Using this, we can then get to the interface that derives from it, as we'll see later on.
An example implementation of an interface would look like this:
// interface.h
class IBaseInterface
{
public:
virtual ~IBaseInterface() {}
};
//...
// IMyInterface.h (includes interface.h)
class IMyInterface : public IBaseInterface
{
public:
virtual void routine1() = 0;
virtual void routine2() = 0;
};
// finally, MyInterface.cpp (in some module)
// a class that implements the IMyInterface interface.
class CMyInterface : public IMyInterface
{
public:
virtual void routine1()
{
// some code
}
virtual void routine2()
{
// some code
}
private:
// can have private members, code, etcetera. (unlike the interface)
// These members can be then accessed by the member functions without any problem.
};
So, a brief explanation of what's going on here. The IBaseInterface class is needed in order to access this interface from other modules, we'll discuss this later. The IMyInterface itself is an abstract class (without an implementation), someone must implement it, hence the CMyInterface class.
Ok, so now we have an implemented interface but we said earlier that the main purpose of an interface is to be shared between modules, but how? Let's talk about that.
Registering implemented interfaces
If we talk about Valve games (GoldSrc, to be more exact), the key is to implement sort of a "interface manager" that will take care of the exposure of each interface, when defined. Let me explain.
Valve uses such class called InterfaceReg in order to register an interface, when defined. The declaration of it is rather simple. It contains a pointer to the base of the registered interface, its identifier (or a version), and a linked list of itself.
// type of a function that is passed into the constructor of InterfaceReg when registering an interface.
typedef IBaseInterface* (*InstantiateInterfaceFn)();
class InterfaceReg
{
public:
// This gets called when a new interface implementation is exposed.
InterfaceReg(InstantiateInterfaceFn fn, const char *pName)
{
m_pName = pName;
m_CreateFn = fn;
m_pNext = s_pInterfaceRegs;
s_pInterfaceRegs = this;
}
public:
InstantiateInterfaceFn m_CreateFn; // pointer to the base of the interface
const char *m_pName; // name, id, version...
InterfaceReg *m_pNext; // for the global list
static InterfaceReg *s_pInterfaceRegs;
};
Now, this registration class is located inside every module that exposes some interface, without the class instance per module, it simply would not work. It exists in order to keep track of which interfaces belong to current module, e.g. which are implemented inside current module.
Finally, if we go back to our CMyInterface implementation, there's one thing missing, and that is the actual exposure of the interface to the local registration manager. (InterfaceReg)
// right after the CMyInterface class...
EXPOSE_SINGLE_INTERFACE(CMyInterface, IMyInterface, "IMyInterface001");
Here's how's the macro defined.
// Use this to expose a singleton interface with a global variable you've created.
#define EXPOSE_SINGLE_INTERFACE_GLOBALVAR(className, interfaceName, versionName, globalVarName) \
static IBaseInterface* __Create##className##interfaceName##_interface() {return (IBaseInterface *)&globalVarName;}\
static InterfaceReg __g_Create##className##interfaceName##_reg(__Create##className##interfaceName##_interface, versionName);
// Use this to expose a singleton interface. This creates the global variable for you automatically.
#define EXPOSE_SINGLE_INTERFACE(className, interfaceName, versionName) \
static className __g_##className##_singleton;\
EXPOSE_SINGLE_INTERFACE_GLOBALVAR(className, interfaceName, versionName, __g_##className##_singleton)
Now, I know what you are probably thinking. It's a big mess, but let me explain, it's actually simpler than it looks. So the main key to all of this is to expose the interface and especially somehow tell the InterfaceReg to register this interface. This is what this macro does, actually.
First, it needs to declare a global variable for the interface implementation instance, let's say g_MyInterface (by the way, this will call the class constructor even before DllMain gets called, after the dll is loaded, of course). Then it creates a function of return type IBaseInterface* that simply returns the global variable defined earlier. Finally, it creates an "instance" of the InterfaceReg object, with first parameter being the function, and second the interface version. It will eventually result into something like this:
static CMyInterface g_MyInterface;
static IBaseInterface* CMyInterfaceBase() { return (IBaseInterface*)&g_MyInterface; }
static InterfaceReg g_RegisterInterface(CMyInterfaceBase, "IMyInterface001");
What does this piece of code do? Well, not only that it creates an instance of the interface implementation, but it also calls the constructor of the InterfaceReg class and adds the interface base as well as its version (or an id) string to the interface list.
Note that this is all happening right after the dlls is loaded. There's no need to have any thirdparty code involved. Because, as you probably know, static instantiated objects gets their constructors called even before DllMain is executed. This is done by the Windows loader. Anyways...
So now the interface exists inside the DLL's InterfaceReg's linked list. The next step is to enabling other modules to actually access this list and lookup the interface they want, depending on the version string (or an id, again).
Exposing registered interfaces to other modules
There's no other way really, than to export a function per dll, that would be then imported by other dlls. In this case, it's called CreateInterface (which is, by the way, kind of a silly name, because in reality, it just locates the interface, it doesn't "create" it). This function is implemented as follows:
EXPORT_FUNCTION IBaseInterface *CreateInterface(const char *pName, int *pReturnCode)
{
InterfaceReg *pCur;
// walk the registered list of interfaces by this module, and compare it with the one
// that the callee requested. (callee being a distant dll that called this function)
for (pCur = InterfaceReg::s_pInterfaceRegs; pCur; pCur = pCur->m_pNext)
{
if (strcmp(pCur->m_pName, pName) == 0)
{
if (pReturnCode) // don't mind the return code, it isn't important
{
*pReturnCode = IFACE_OK;
}
// return the base address of the interface. In our case, &g_MyInterface.
return pCur->m_CreateFn();
}
}
if (pReturnCode) // don't mind
{
*pReturnCode = IFACE_FAILED;
}
return NULL;
}
As stated earlier, and as can be seen from the code, this function is exported by every dll that implements some interface. Client dlls then import this function and uses it to load interfaces that are normally inaccessible from their perspective for themselves.
So that's it. The magic of this is, that as stated earlier, that this avoids of having multiple copies of one class inside a process. When the process shares memory with client dlls, why not just use an interface instead? If dll1 would need to use CMyInterface, it would simple include the MyInterface.cpp. But if another dll would want to use it too, it would have to do the same, resulting in a copied instance of the same class. Plus, in some situations, it's necessary to to have a shared class between modules. Anyway... that's it, Thanks for reading :^)