A Module is a dynamic library which contains a single class loaded at runtime. It means that you must compile every module independently. For that you must implement your modules deriving from oZ::IModule interface. Last, you will need an instantiation function:
extern "C" IModule *CreateModule(void) { return MyModule(); }
// you may want to use the OPEN_ZIA_MAKE_ENTRY_POINT macro:
OPEN_ZIA_MAKE_ENTRY_POINT(MyModule);The API already does the dynamic library loading for you so you can focus more on creating modules.
In this section we will take a closer look at the interface class oZ::IModule, and all its components.
// Module interface
class IModule;
// Module polymorphic instance
using ModulePtr = std::shared_ptr<IModule>We use shared pointers to store instantiated modules. This allows holding a reference to a module while asserting its existence.
Each module can register multiple callback functions at any state of the pipeline. The enumeration of each callback is oZ::State.
Use the different states and the oZ::Priority enumeration to sort modules callback. Each state of the pipeline are triggered in this order:
BeforeParse -> Parse -> AfterParse -> BeforeInterpret -> Interpret -> AfterInterpret -> Completed
Each callback must take a oZ::Context in parameter and return a boolean. If you return false from the callback, the pipeline will not trigger other modules' callbacks from the current oZ::State and go straight for the next one. At any time, you if an error is set using oZ::Context::setErrorState, the pipeline will trigger the special oZ::State::Error sate callback.
bool MyModule::myCallback(oZ::Context &context)
{
if (hasError(context)) {
context.setErrorState();
return false; // If you return true here, the pipeline will finish to trigger every callback of the current state and not immediatly call Error callbacks
}
return true;
}Each module have a set of virtual function to be overriden. Actually, only 2 of them are pure virtual :
// Get the raw string name of module instance
virtual const char *getName(void) const = 0;
// Register module's callbacks to the pipeline
virtual void onRegisterCallbacks(Pipeline &pipeline) = 0;Let's see a simple example with an independent HTTP Module:
class HTTPModule : public oZ::IModule
{
public:
// Get the raw string name of module instance
virtual const char *getName(void) const { return "HTTPModule"; }
// Register module's callbacks to the pipeline
virtual void onRegisterCallbacks(oZ::Pipeline &pipeline) {
pipeline.registerCallback(
oZ::State::Parse, // Callback triggered when parsing the HTTP request
oZ::Priority::ASAP, // As soon as possible
this, &HTTPModule::onParseHeader // Actual function callback
);
}
bool onParseHeader(oZ::Context &context) {
const auto &byteArray = context.getByteArray();
auto &request = context.getRequest();
auto &header = request.getHeader();
// Parse 'byteArray' to fill 'header' and 'request' data
return true; // Tell the pipeline we continue to process this state
}
};And that's it ! You don't have to implement anything else to create an independent module.
However, there are more virtual functions for more complex needs. These functions are default-implemented to let you the choice of using them or not.
// Callback triggered when a client is connected
virtual void onConnected(Context &context);
// Callback triggered when a client is disconnected
virtual void onDisconnected(Context &context);
// Callback triggered when a client sent a message
virtual MessageState onMessageAvaible(Context &context);
// Get the list of dependencies (as a vector of raw string, see function getName above)
virtual Dependencies getDependencies(void) const noexcept;
// Given a pipeline reference, find each dependent module to store them internally
virtual void onRetreiveDependencies(Pipeline &pipeline);
// Given a directory path (where all configs are), load module's configuration file
virtual void onLoadConfigurationFile(const std::string &directory);If you implement networking as a standalone module, you need to use Pipeline::onConnection, Pipeline::onDisconnection, IModule::onConnection, IModule::onDisconnection, IModule::onMessageAvaible to implement it as a standalone module.
// Networking module
class NetModule : public oZ::IModule
{
public:
virtual void onConnection(oZ::Context &context) {
// Do your connection stuff
}
virtual void onDisconnection(oZ::Context &context) {
// Do your disconnection stuff
}
virtual MessageState onMessageAvaible(oZ::Context &context) {
// Fill 'context.getPacket().getByteArray()' by reading is socket
return MessageState::Done; // Returns Ready to tell pipeline that you filled the context
}
// ...
};
// Client structure
struct Client
{
oZ::Context context {}; // Client's reusable context
};
void MyServer::onClientConnected(Client &client)
{
_pipeline.onConnection(client.context);
}
void MyServer::onClientDisconnected(Client &client)
{
_pipeline.onDisconnection(client.context);
}
void MyServer::onClientReadable(Client &client)
{
client.context.clear();
switch (_pipeline.onMessageAvaible(client.context)) {
case MessageState::Readable:
// The message has not been handled by any module
break;
case MessageState::Done:
// The message has been processed by the pipeline
break;
case MessageState::Disconnection:
// The message was a TCP disconnection
break;
}
}Let's first see how dependencies are handled given a short example:
class ChildModule : public oZ::IModule
{
public:
virtual const char *getName(void) const { return "Child"; }
virtual void onRegisterCallbacks(oZ::Pipeline &pipeline) { ... }
};
class RootModule : public oZ::IModule
{
public:
virtual const char *getName(void) const { return "Root"; }
virtual void onRegisterCallbacks(oZ::Pipeline &pipeline) { ... }
// Return the list of dependencies
virtual Dependencies getDependencies(void) const noexcept { return { "Child" }; }
// Find dependencies and store them before any process for performance reasons
virtual void onRetreiveDependencies(oZ::Pipeline &pipeline) {
_child = pipeline.findModule<ChildModule>();
}
private:
std::shared_ptr<ChildModule> _child;
};Let's see how simple it is to add a configuration file to setup a module. Please note that the configuration file language is totally up to you, in this example it is named MyCustomConfigLoader.
// Import your own config loader
#include "MyCustomConfigLoader.hpp"
class MyModule : public oZ::IModule
{
public:
static const auto *FileName = "MyModule.conf";
virtual const char *getName(void) const { return "My" };
virtual void onRegisterCallbacks(oZ::Pipeline &pipeline) { ... }
// Open configuration file and load some properties using MyCustomConfigLoader
virtual void onLoadConfigurationFile(const std::string &directory) {
auto config = MyCustomConfigLoader::Parse(directory + FileName);
_x = config["x"].toInt();
_y = config["y"].toInt();
}
private:
int _x = 0, _y = 0;
};