Ogre(1.9)的插件原理
研究目的
本人研究过部分cocos2dx游戏引擎等源码,它们都是揉在一团的,要修改引擎里面的一些代码,需要重新编译整套源码。这样,引擎的可扩展性就大大降低了。
Ogre这个引擎让我最吃惊的就是这个插件化的系统,渲染系统、场景管理系统 居然可以作为插件直接嵌入,还有如果要加入新的功能,也可以不需要重新编译整套源码,通过动态链接库的方式很方便地添加。
更让我惊喜的是,Ogre引入了SampleBrowser的概念,可以把一个一个游戏的场景作为一个一个的Sample,然后编译成相应的动态链接库,这样一来,每个Sample的加载和运行也以Plugin插件技术为基础了,即可以动态添加游戏场景 和 游戏逻辑,同时也是 热插拔的,即可以随时加载和卸载。这个做法,既具有脚本语言的强大灵活性,C++本身高效的运行效率也可以继续保留。
研究问题
- 如何实现热插拔的插件模块?
- 如何实现跨平台?
- 如何和插件模块进行交换?
- 插件工作的整个流程是?
解决问题
以 动态链接 的方式实现热插拔插件
插件加载的源码在,OgreDynLib.h 和 OgreDynLib.cpp,先从这里下手。
/** Resource holding data about a dynamic library.
@remarks
This class holds the data required to get symbols from
libraries loaded at run-time (i.e. from DLL's for so's)
@author
Adrian Cearn„u ([email protected])
@since
27 January 2002
*/
class _OgreExport DynLib : public DynLibAlloc
{
protected:
String mName;
/// Gets the last loading error
String dynlibError(void);
public:
/** Default constructor - used by DynLibManager.
@warning
Do not call directly
*/
DynLib( const String& name );
/** Default destructor.
*/
~DynLib();
/** Load the library
*/
void load();
/** Unload the library
*/
void unload();
/// Get the name of the library
const String& getName(void) const { return mName; }
/**
Returns the address of the given symbol from the loaded library.
@param
strName The name of the symbol to search for
@return
If the function succeeds, the returned value is a handle to
the symbol.
@par
If the function fails, the returned value is <b>NULL</b>.
*/
void* getSymbol( const String& strName ) const throw();
protected:
/// Handle to the loaded library.
DYNLIB_HANDLE mInst;
};
看注释,我们可以清楚认识到,Ogre的插件库是使用动态链接库的方法做到的,而不是脚本。
构造函数,传入 Plugin的名字,load函数加载这个Plugin对应名字的动态链接库,unload函数卸载它,getSymbol通过符号获取动态链接库的函数。
用 宏 来解决跨平台问题
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
# define DYNLIB_HANDLE hInstance
# define DYNLIB_LOAD( a ) LoadLibraryEx( a, NULL, 0 ) // we can not use LOAD_WITH_ALTERED_SEARCH_PATH with relative paths
# define DYNLIB_GETSYM( a, b ) GetProcAddress( a, b )
# define DYNLIB_UNLOAD( a ) !FreeLibrary( a )
struct HINSTANCE__;
typedef struct HINSTANCE__* hInstance;
#elif OGRE_PLATFORM == OGRE_PLATFORM_WINRT
# define DYNLIB_HANDLE hInstance
# define DYNLIB_LOAD( a ) LoadPackagedLibrary( UTFString(a).asWStr_c_str(), 0 )
# define DYNLIB_GETSYM( a, b ) GetProcAddress( a, b )
# define DYNLIB_UNLOAD( a ) !FreeLibrary( a )
struct HINSTANCE__;
typedef struct HINSTANCE__* hInstance;
#elif OGRE_PLATFORM == OGRE_PLATFORM_LINUX || OGRE_PLATFORM == OGRE_PLATFORM_ANDROID || OGRE_PLATFORM == OGRE_PLATFORM_NACL || OGRE_PLATFORM == OGRE_PLATFORM_FLASHCC
# define DYNLIB_HANDLE void*
# define DYNLIB_LOAD( a ) dlopen( a, RTLD_LAZY | RTLD_GLOBAL)
# define DYNLIB_GETSYM( a, b ) dlsym( a, b )
# define DYNLIB_UNLOAD( a ) dlclose( a )
#elif OGRE_PLATFORM == OGRE_PLATFORM_APPLE || OGRE_PLATFORM == OGRE_PLATFORM_APPLE_IOS
# define DYNLIB_HANDLE void*
# define DYNLIB_LOAD( a ) mac_loadDylib( a )
# define FRAMEWORK_LOAD( a ) mac_loadFramework( a )
# define DYNLIB_GETSYM( a, b ) dlsym( a, b )
# define DYNLIB_UNLOAD( a ) dlclose( a )
#endif
由于各大平台的动态链接库操作接口都是类似的,可以用 DYNLIB_HANDLE、 DYNLIB_LOAD、 DYNLIB_GETSYM 、 DYNLIB_UNLOAD 这四个宏来统一各平台的动态链接库的系统API。
使用跨动态链接库的单例Root,实现交互
加载插件后,如何与插件的交互 是 一个永恒的话题。Ogre的解决方法真是十分机智。
SamplePlugin* sp;
Sample* s;
extern "C" _OgreSampleExport void dllStartPlugin()
{
s = new Sample_DeferredShading;
sp = OGRE_NEW SamplePlugin(s->getInfo()["Title"] + " Sample");
sp->addSample(s);
Root::getSingleton().installPlugin(sp);
}
extern "C" _OgreSampleExport void dllStopPlugin()
{
Root::getSingleton().uninstallPlugin(sp);
OGRE_DELETE sp;
delete s;
}
上面是,一个Sample的小例子,dllStartPlugin()有个 Root::getSingleton().installPlugin,使用了Root单例把整个动态链接库里面的东西都交给了 主程序的 Root。
这里有个很神奇的问题,动态链接库里的单例Root 是如何做到 和主程序是同一个的?
奥秘就是,Root类前面加了 _OgreExport
# if defined( OGRE_NONCLIENT_BUILD )
# define _OgreExport __declspec( dllexport )
# else
# if defined( __MINGW32__ )
# define _OgreExport
# else
# define _OgreExport __declspec( dllimport )
# endif
# endif
_OgreExport使用了宏实现不同编译器的动态符号导出,当Root单例以动态符号导出后,就可以实现 动态链接库 和 主程序 之间的共享这个单例Root了。然后,再由这个单例Root把这个插件里面的 代码和数据 绑定到主程序,这样主程序就可以随意调用动态链接库里面的代码了。
整体流程
- Root通过读取插件配置文件,把需要的加载模块的名字传给 loadPlugin 函数。
void Root::loadPlugin(const String& pluginName)
{
#if OGRE_PLATFORM != OGRE_PLATFORM_NACL
// Load plugin library
DynLib* lib = DynLibManager::getSingleton().load( pluginName );
// Store for later unload
// Check for existence, because if called 2+ times DynLibManager returns existing entry
if (std::find(mPluginLibs.begin(), mPluginLibs.end(), lib) == mPluginLibs.end())
{
mPluginLibs.push_back(lib);
// Call startup function
#ifdef __GNUC__
__extension__
#endif
DLL_START_PLUGIN pFunc = (DLL_START_PLUGIN)lib->getSymbol("dllStartPlugin");
if (!pFunc)
OGRE_EXCEPT(Exception::ERR_ITEM_NOT_FOUND, "Cannot find symbol dllStartPlugin in library " + pluginName,
"Root::loadPlugin");
// This must call installPlugin
pFunc();
}
#endif
}
- loadPlugin函数通过DynLibManager构建里面DynLib,并调用load函数加载动态链接库。
// OgreDynLibManager.cpp
DynLib* DynLibManager::load( const String& filename)
{
DynLibList::iterator i = mLibList.find(filename);
if (i != mLibList.end())
{
return i->second;
}
else
{
DynLib* pLib = OGRE_NEW DynLib(filename);
pLib->load();
mLibList[filename] = pLib;
return pLib;
}
}
加载完动态链接库后,整个返回Root的loadPlugin 函数中,接着获取dllStartPlugin符号的函数,然后调用。
进入插件中的dllStartPlugin函数中,里面初始化一个继承Plugin类的扩展类,然后用单例Root的installPlugin函数把这个插件类添加进去。
完成整个插件的加载。
总结
Ogre这个插件的机制既有类似脚本语言的灵活性,也有原生C++的高效。在以后的一些开发的过程中,我可能会大量使用这种灵活的插件开发方法,不过还是值得考量的,毕竟使用脚本来扩展还是很大程度提高开发效率的。
至于缺陷,我倒是发现一个,就是 插件的在不同版本Ogre中(或者是同版本同操作系统的Ogre在不同编译器下)的兼容性就很难保证。比如,修改了一下里面的类的成员,类编译后的二进制结构就不一样了,这样的话,再调用动态链接库里面的东西,就有可能会出错了。
参考
http://blog.sina.com.cn/s/blog_9151e7300101fdfh.html
http://www.cnblogs.com/yzwalkman/archive/2012/12/20/2826687.html
http://www.cnblogs.com/bourneli/archive/2011/12/28/2305280.html