-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Chapter 2: Plugins and Modules
When I was learning the Ogre3D engine, the first concept I learned was the plugin. Everything is a plugin in Ogre's world. The author of Ogre abstracted the important interfaces such as file management, rendering engine, and scene manager into plugins. Users can simply re-implement that interface as a new plugin to replace the default plugin. For instance, you can replace the D3D rendering plugin with an OpenGL plugin to replace the renderer Ogre uses, and hot-swap it without recompiling or restarting the application.
NF's plug-in management system is slightly more complicated than OGRE's, mainly because of how each plugin contains modules, which are each registered to the PluginManager and managed by it.
NFServer is the programs executable entry point. It will automatically find the Plugin.xml file located in the startup directory, and then load each plugin listed in this XML config file.
Let's look at an example Plugin.xml file:
<XML>
<GameServer>
<Plugin Name="NFKernelPlugin" />
<Plugin Name="NFConfigPlugin" />
<Plugin Name="NFGameServerPlugin" />
<Plugin Name="NFGameServerNet_ServerPlugin" />
<Plugin Name="NFGameServerNet_ClientPlugin" />
<Plugin Name="NFLogPlugin" />
</GameServer>
<LoginServer>
<Plugin Name="NFKernelPlugin" />
<Plugin Name="NFConfigPlugin" />
<Plugin Name="NFLoginLogicPlugin" />
<Plugin Name="NFLoginNet_ServerPlugin" />
<Plugin Name="NFLoginNet_ClientPlugin" />
<Plugin Name="NFLogPlugin" />
</LoginServer>
<MasterServer>
<Plugin Name="NFKernelPlugin" />
<Plugin Name="NFConfigPlugin" />
<Plugin Name="NFMasterServerPlugin" />
<Plugin Name="NFMasterNet_ServerPlugin" />
<Plugin Name="NFMasterNet_HttpServerPlugin" />
<Plugin Name="NFLogPlugin" />
</MasterServer>
<ProxyServer>
<Plugin Name="NFKernelPlugin" />
<Plugin Name="NFConfigPlugin" />
<Plugin Name="NFProxyLogicPlugin" />
<Plugin Name="NFProxyServerNet_ClientPlugin" />
<Plugin Name="NFProxyServerNet_ServerPlugin" />
<Plugin Name="NFLogPlugin" />
</ProxyServer>
<WorldServer>
<Plugin Name="NFKernelPlugin" />
<Plugin Name="NFConfigPlugin" />
<Plugin Name="NFWorldNet_ClientPlugin" />
<Plugin Name="NFWorldNet_ServerPlugin" />
<Plugin Name="NFLogPlugin" />
</WorldServer>
</XML>
Plugin.xml declares each plugin name and the path to the configuration files (NFDataCfg folder) for each server to load when it started. Because a script is usually used to start the server in batches, an app ID can be passed to the script, for example:
./NFServer -d Server=MasterServer ID=3
./NFServer -d Plugin=PluginX.xml Server=MasterServer ID=3
The NFPluginManager singleton class handles all plugin loading. When the application started, the NFCPluginManager singleton will be created and starts loading all plugins dynamic libraries (.so on Linux/Mac, .dll on Windows) listed in the "Plugin.xml" file.
Below is the code:
void MidWareLoader(NFIPluginManager* pPluginManager)
{
#if NF_PLATFORM == NF_PLATFORM_WIN
//TUTORIAL
CREATE_PLUGIN(pPluginManager, Tutorial1)
CREATE_PLUGIN(pPluginManager, Tutorial2)
CREATE_PLUGIN(pPluginManager, Tutorial3Plugin)
CREATE_PLUGIN(pPluginManager, Tutorial4Plugin)
CREATE_PLUGIN(pPluginManager, Tutorial5)
CREATE_PLUGIN(pPluginManager, Tutorial6)
CREATE_PLUGIN(pPluginManager, Tutorial7)
#endif
}
////////////////////////////////////////////////////////
int main(int argc, char* argv[])
{
std::cout << "__cplusplus:" << __cplusplus << std::endl;
std::vector<NF_SHARE_PTR<NFPluginServer>> serverList;
std::string strArgvList;
for (int i = 0; i < argc; i++)
{
strArgvList += " ";
strArgvList += argv[i];
}
if (argc == 1)
{
//IDE
serverList.push_back(NF_SHARE_PTR<NFPluginServer>(NF_NEW NFPluginServer(strArgvList + " Server=MasterServer ID=3 Plugin=Plugin.xml")));
serverList.push_back(NF_SHARE_PTR<NFPluginServer>(NF_NEW NFPluginServer(strArgvList + " Server=WorldServer ID=7 Plugin=Plugin.xml")));
serverList.push_back(NF_SHARE_PTR<NFPluginServer>(NF_NEW NFPluginServer(strArgvList + " Server=LoginServer ID=4 Plugin=Plugin.xml")));
serverList.push_back(NF_SHARE_PTR<NFPluginServer>(NF_NEW NFPluginServer(strArgvList + " Server=DBServer ID=8 Plugin=Plugin.xml")));
serverList.push_back(NF_SHARE_PTR<NFPluginServer>(NF_NEW NFPluginServer(strArgvList + " Server=ProxyServer ID=5 Plugin=Plugin.xml")));
serverList.push_back(NF_SHARE_PTR<NFPluginServer>(NF_NEW NFPluginServer(strArgvList + " Server=GameServer ID=6 Plugin=Plugin.xml")));
}
else
{
serverList.push_back(NF_SHARE_PTR<NFPluginServer>(NF_NEW NFPluginServer(strArgvList)));
}
for (auto item : serverList)
{
item->SeMidWareLoader(MidWareLoader);
item->Init();
}
////////////////
uint64_t nIndex = 0;
while (true)
{
nIndex++;
std::this_thread::sleep_for(std::chrono::milliseconds(1));
for (auto item : serverList)
{
item->Execute();
}
}
////////////////
for (auto item : serverList)
{
item->Final();
}
serverList.clear();
return 0;
}
All plugins must inherit the NFIPlugin class, and manage some modules that inherit the NFIModule class. All modules must implement the following functions (Awake, Init, AfterInit, CheckConfig, BeforeShut, Shut, ReadyExecute, and Execute):
class NFIModule
{
public:
virtual bool Awake()
{
return true;
}
virtual bool Init()
{
return true;
}
virtual bool AfterInit()
{
return true;
}
virtual bool CheckConfig()
{
return true;
}
virtual bool BeforeShut()
{
return true;
}
virtual bool Shut()
{
return true;
}
virtual bool ReadyExecute()
{
return true;
}
virtual bool Execute()
{
return true;
}
};
The order in which these functions are called is the same as the order shown in the previous NFCPluginManager code snippet. Each class that inherits NFIModule will have these functions called in that order.
All modules inheriting from the NFIModule class are dynamic libraries, so in order to allow loading, NF first abstracts the dynamic library into a class called NFCDynLib (thanks again to Ogre for this concept). The NFCPluginManager class stores pointers to all loaded NFCDynLib objects and manages them. NFCDynLib also requires platform-specific code for loading libraries on each operating system, summary code is shown below:
class NFDynLib
{
public:
NFDynLib(const std::string& strName)
{
mbMain = false;
mstrName = strName;
#ifdef NF_DEBUG_MODE
mstrName.append("_d");
#endif
#if NF_PLATFORM == NF_PLATFORM_WIN
mstrName.append(".dll");
#else
mstrName.append(".so");
#endif
printf("LoadPlugin:%s\n", mstrName.c_str());
}
~NFDynLib()
{
}
bool Load()
{
std::string strLibPath = "./";
strLibPath += mstrName;
mInst = (DYNLIB_HANDLE)DYNLIB_LOAD(strLibPath.c_str());
return mInst! = NULL;
}
bool UnLoad()
{
DYNLIB_UNLOAD(mInst);
return true;
}
const std::string& GetName(void) const
{
return mstrName;
}
const bool GetMain(void) const
{
return mbMain;
}
void* GetSymbol(const char* szProcName)
{
return (DYNLIB_HANDLE)DYNLIB_GETSYM(mInst, szProcName);
}
protected:
std::string mstrName;
bool mbMain;
DYNLIB_HANDLE mInst;
};
The NFPluginManager::Awake()
function calls LoadPluginConfig()
first to load the plugin list from the Plugin.xml config file:
bool NFPluginManager::LoadPluginConfig()
{
std::string strContent;
GetFileContent(mstrConfigName, strContent);
rapidxml::xml_document<> xDoc;
xDoc.parse<0>((char*)strContent.c_str());
rapidxml::xml_node<>* pRoot = xDoc.first_node();
rapidxml::xml_node<>* pAppNameNode = pRoot->first_node(mstrAppName.c_str());
if (!pAppNameNode)
{
NFASSERT(0, "There are no App ID", __FILE__, __FUNCTION__);
return false;
}
for (rapidxml::xml_node<>* pPluginNode = pAppNameNode->first_node("Plugin"); pPluginNode; pPluginNode = pPluginNode->next_sibling("Plugin"))
{
const char* strPluginName = pPluginNode->first_attribute("Name")->value();
mPluginNameMap.insert(PluginNameMap::value_type(strPluginName, true));
}
rapidxml::xml_node<>* pPluginConfigPathNode = pAppNameNode->first_node("ConfigPath");
if (!pPluginConfigPathNode)
{
NFASSERT(0, "There are no ConfigPath", __FILE__, __FUNCTION__);
return false;
}
if (NULL == pPluginConfigPathNode->first_attribute("Name"))
{
NFASSERT(0, "There are no ConfigPath.Name", __FILE__, __FUNCTION__);
return false;
}
mstrConfigPath = pPluginConfigPathNode->first_attribute("Name")->value();
return true;
}
Next, NFPluginManager's initialization functions Init and AfterInit will be called. These functions will call the corresponding functions of the same name on each NFIPlugin, and each NFIPlugin will call these functions on all of it its modules:
// First NFCPluginManager Init function is called and iterates over all NFIPlugins, calling their Init functions
virtual bool NFPluginManager::Init()
{
PluginInstanceMap::iterator itInstance = mPluginInstanceMap.begin();
for (itInstance; itInstance != mPluginInstanceMap.end(); itInstance++)
{
itInstance->second->Init();
}
return true;
}
// Now each NFIPlugin also iterates over all its NFIModules, calling all their Init functions
virtual bool NFIPlugin::Init()
{
NFIModule* pModule = First();
while (pModule)
{
bool bRet = pModule->Init();
if (!bRet)
{
assert(0);
}
pModule = Next();
}
}
After successful initialization, the main loop is started and the network library, heartbeat library, etc, each has the opportunity to handle their own logic (such as receiving messages and calling the modules within them to handle different parts of the logic.
// Look familiar?
virtual bool NFPluginManager::Execute()
{
NFIModule* pModule = First();
while (pModule)
{
pModule->Execute();
pModule = Next();
}
return true;
}
virtual bool NFIPlugin::Execute()
{
NFIModule* pModule = First();
while (pModule)
{
pModule->Execute();
pModule = Next();
}
return true;
}
In NF's plugin architecture, modules contain most of the logic and do most of the work. Plug-ins act only as carriers for modules. When a module is instantiated, it has almost nothing to do with plug-ins. Modules can use other modules across plugins without restrictions. Because NF uses interface-oriented programming, any module only needs to depend on the interfaces of other modules. So, how can you obtain one module from inside another? When each module is initialized, it will call Awake, Init, AfterInit, and ReadyExecute functions. These functions are generally used as follows: NFIModule::Awake
is used to initialize modules own resources, for example for what is needed to provide services to other modules, such as providing random number pools, pre-generated objects, etc.; NFIModule::Init
is used mainly to initialize interfaces of other modules it needs, by using the pPluginManager->GetModule<NFIModule>()
function to obtain them. Replace NFIModule with your own module. Take note that in NF's world, only core modules should start with NFI or NFC, and if you prefix your own module with these instead of NF, it may fail to find your module. NFIModule::AfterInit
: After obtaining the interfaces of other modules, your module may depend on the resources of some of these other modules to do some more of its own initialization. NFIModule::ReadyExecute
is mainly used in the network message processor's snoop handling function.