Foundation中的SharedLibrary实现跨平台的dll动态加载。具体使用方法和简介可见:ShareLibrary官方文档
通过SharedLibrary可以实现函数导出和类导出,函数导出是最简单的,Dll提供方除了函数需要使用extern "C"声明之外,和普通C++编写的dll并无区别,在官方文档的例子中,dll使用方代码也很简单:
// LibraryLoaderTest.cpp
#include "Poco/SharedLibrary.h"
using Poco::SharedLibrary;
typedef void (*HelloFunc)(); // function pointer type
int main(int argc, char** argv)
{
std::string path("TestLibrary");
path.append(SharedLibrary::suffix()); // adds ".dll" or ".so"
SharedLibrary library(path); // will also load the library
HelloFunc func = (HelloFunc) library.getSymbol("hello");
func();
library.unload();
return 0;
}
其中,SharedLibray::suffix()根据不同的平台和编译模式为dll选择不同的后缀,比如Windows Release版本,则直接添加".dll"后缀,而Windows Debug版本则添加"d.dll"后缀。ShareLibrary在构造时自动加载该动态链接库(在Windows下调用LoadLibrary),然后通过getSymbol(Windows下的GetProcAddress)获取函数地址,这也是之前说要在dll中在函数前用extern "C"声明的原因。
SharedLibrary是如何实现跨平台的呢?在Foundation/Inlcude/poco/SharedLibrary.h中,可以看到SharedLibrary从一个叫SharedLibraryImpl的类私有继承,而在SharedLibrary中,几乎所有函数都调用对应的Impl函数:
源代码文件位置:Foundation/src/SharedLibrary.cpp
SharedLibrary::SharedLibrary()
{
}
SharedLibrary::SharedLibrary(const std::string& path)
{
loadImpl(path, 0);
}
SharedLibrary::SharedLibrary(const std::string& path, int flags)
{
loadImpl(path, flags);
}
SharedLibrary::~SharedLibrary()
{
}
void SharedLibrary::load(const std::string& path)
{
loadImpl(path, 0);
}
void SharedLibrary::load(const std::string& path, int flags)
{
loadImpl(path, flags);
}
void SharedLibrary::unload()
{
unloadImpl();
}
bool SharedLibrary::isLoaded() const
{
return isLoadedImpl();
}
bool SharedLibrary::hasSymbol(const std::string& name)
{
return findSymbolImpl(name) != 0;
}
void* SharedLibrary::getSymbol(const std::string& name)
{
void* result = findSymbolImpl(name);
if (result)
return result;
else
throw NotFoundException(name);
}
const std::string& SharedLibrary::getPath() const
{
return getPathImpl();
}
std::string SharedLibrary::suffix()
{
return suffixImpl();
}
而这些函数,恰好在其父类SharedLibraryImpl中实现,而在SharedLibrary.h中,还有这一段:
#if defined(hpux) || defined(_hpux)
#include "SharedLibrary_HPUX.cpp"
#elif defined(POCO_VXWORKS)
#include "SharedLibrary_VX.cpp"
#elif defined(POCO_OS_FAMILY_UNIX)
#include "SharedLibrary_UNIX.cpp"
#elif defined(POCO_OS_FAMILY_WINDOWS) && defined(POCO_WIN32_UTF8)
#include "SharedLibrary_WIN32U.cpp"
#elif defined(POCO_OS_FAMILY_WINDOWS)
#include "SharedLibrary_WIN32.cpp"
#elif defined(POCO_OS_FAMILY_VMS)
#include "SharedLibrary_VMS.cpp"
#endif
通过判断其平台,包含对应的ShareLibrary_*头文件,而在这些文件中,提供了各自的SharedLibraryImpl实现,也就是说,在子类SharedLibrary中调用的load,findSymbol,unload等函数,都最终调用其父类的函数。SharedLibrary_*实现了各个平台下的动态加载功能,而SharedLibrary则通过继承于它统一了这种接口,通过load,getSymbol等函数提供给使用方。
打开Foundation/src/SharedLibrary_Win32.cpp文件,可以找到SharedLibrary在Windows下的实现,发现下面几个关键函数:
void SharedLibraryImpl::loadImpl(const std::string& path, int /*flags*/)
{
FastMutex::ScopedLock lock(_mutex);
if (_handle) throw LibraryAlreadyLoadedException(_path);
DWORD flags(0);
Path p(path);
if (p.isAbsolute()) flags |= LOAD_WITH_ALTERED_SEARCH_PATH;
_handle = LoadLibraryExA(path.c_str(), 0, flags);
if (!_handle) throw LibraryLoadException(path);
_path = path;
}
void* SharedLibraryImpl::findSymbolImpl(const std::string& name)
{
FastMutex::ScopedLock lock(_mutex);
if (_handle)
{
return (void*) GetProcAddress((HMODULE) _handle, name.c_str());
}
else return 0;
}
std::string SharedLibraryImpl::suffixImpl()
{
#if defined(_DEBUG)
return "d.dll";
#else
return ".dll";
#endif
}
SharedLibrary通过宏和泛型为我们提供了导出类的接口,文档中一个导出类的例子如下:
dll方头文件
// AbstractPlugin.h
//
// This is used both by the class library and by the application.
#ifndef AbstractPlugin_INCLUDED
#define AbstractPlugin_INCLUDED
class AbstractPlugin
{
public:
AbstractPlugin();
virtual ~AbstractPlugin();
virtual std::string name() const = 0;
};
#endif // AbstractPlugin.h
dll方cpp文件,包含一个基类实现文件AbstractPlugin.cpp和派生类实现文件PluginLibrary.cpp
// AbstractPlugin.cpp
//
// This is used both by the class library and by the application.
#include "AbstractPlugin.h"
AbstractPlugin::AbstractPlugin()
{}
AbstractPlugin::~AbstractPlugin()
{}
// PluginLibrary.cpp
#include "AbstractPlugin.h"
#include "Poco/ClassLibrary.h"
#include <iostream>
class PluginA: public AbstractPlugin
{
public:
std::string name() const
{
return "PluginA";
}
};
class PluginB: public AbstractPlugin
{
public:
std::string name() const
{
return "PluginB";
}
};
//用POCO提供的宏来生成类清单
//这个宏展开实际是个函数声明,该函数由POCO在加载dll时自动调用,完成清单的加载
POCO_BEGIN_MANIFEST(AbstractPlugin)
POCO_EXPORT_CLASS(PluginA)
POCO_EXPORT_CLASS(PluginB)
POCO_END_MANIFEST
// optional set up and clean up functions
void pocoInitializeLibrary()
{
std::cout << "PluginLibrary initializing" << std::endl;
}
void pocoUninitializeLibrary()
{
std::cout << "PluginLibrary uninitializing" << std::endl;
}
dll使用方:
// main.cpp
#include "Poco/ClassLoader.h"
#include "Poco/Manifest.h"
#include "AbstractPlugin.h"
#include <iostream>
typedef Poco::ClassLoader<AbstractPlugin> PluginLoader;
typedef Poco::Manifest<AbstractPlugin> PluginManifest;
int main(int argc, char** argv)
{
PluginLoader loader;
std::string libName("PluginLibrary");
libName += Poco::SharedLibrary::suffix(); // append .dll or .so
loader.loadLibrary(libName);
PluginLoader::Iterator it(loader.begin());
PluginLoader::Iterator end(loader.end());
for (; it != end; ++it)//遍历该loader加载的所有dll 这里只有一个
{
std::cout << "lib path: " << it->first << std::endl;//输出该dll路径
PluginManifest::Iterator itMan(it->second->begin());
PluginManifest::Iterator endMan(it->second->end());
for (; itMan != endMan; ++itMan)//遍历该dll的类清单
std::cout << itMan->name() << std::endl;//输出类名
}
AbstractPlugin* pPluginA = loader.create("PluginA");//创建PluginA类对象
AbstractPlugin* pPluginB = loader.create("PluginB");
std::cout << pPluginA->name() << std::endl;//输出PluginA类名
std::cout << pPluginB->name() << std::endl;
loader.classFor("PluginA").autoDelete(pPluginA);//将pPluginA所指对象所有权交给loader 当loader析构时会自动释放
delete pPluginB;
loader.unloadLibrary(libName);
return 0;
}
导出类明显要复杂得多,同时也可以看出SharedLibrary导出类的一些限制:只能导出dll中的具有公共接口类,但是一个dll中可以支持导出多个接口类。
我们从上往下看。
首先是ClassLoader:
typedef Poco::ClassLoader<AbstractPlugin> PluginLoader;
它实现将dll导入,读取dll中提供的导出类清单,并且可以根据类名创建其对象。它的核心就是一个LibraryInfo的集合:
typedef std::map<std::string, LibraryInfo> LibraryMap;
而LibraryInfo包含了一个dll被加载的基本信息:
typedef Manifest<Base> Manif;
struct LibraryInfo
{
SharedLibrary* pLibrary; //负责该dll的加载和卸载
const Manif* pManifest;//该dll导出类的清单
int refCount; //该dll被加载的次数
};
这里面最重要的是Manifest<Base>,它包含了该dll所有导出类的信息。它负责维护该dll中导出类的类名和类信息的映射:
typedef AbstractMetaObject<B> Meta;
typedef std::map<std::string, const Meta*> MetaMap;
Meta是一个抽象类,一个Meta负责维护一个导出类,它提供最重要的接口,包括获取类名,创建该类对象,维护该类指针(即autoDelete,当Meta对象析构时自动释放该指针)等。它有两个派生类:
//负责维护普通类
template <class C, class B>
class MetaObject: public AbstractMetaObject<B>
//负责维护单例模式类
template <class C, class B>
class MetaSingleton: public AbstractMetaObject<B>
注意,这两个派生类的模板参数不再是一个,而是两个,class B是dll中的公共接口类,class C就是真正维护的类。
看一下AbstractMetaObject的源码:
源代码文件位置:Foundation/Include/poco/MetaObject.h
template <class B>
class AbstractMetaObject
{
public:
AbstractMetaObject(const char* name): _name(name)
{
}
//在析构时,释放被委托(通过autoDelete)的该类指针
virtual ~AbstractMetaObject()
{
for (typename ObjectSet::iterator it = _deleteSet.begin(); it != _deleteSet.end(); ++it)
{
delete *it;
}
}
//获取该类名字,_name从构造函数中传入
const char* name() const
{
return _name;
}
virtual B* create() const = 0;//创建接口 由普通类实现
virtual B& instance() const = 0;//实例接口 由单例类实现
virtual bool canCreate() const = 0;//是否是普通类
virtual void destroy(B* pObject) const//释放指针(前提是该对象有该指针的所有权)
{
typename ObjectSet::iterator it = _deleteSet.find(pObject);//在托管指针集合中寻找该指针
if (it != _deleteSet.end())//如果找到 则说明该对象有该指针所有权 才释放
{
_deleteSet.erase(pObject);
delete pObject;
}
}
B* autoDelete(B* pObject) const//被托管的指针都放在_deleteSet集合中
{
if (this->canCreate()) // guard against singleton
{
poco_check_ptr (pObject);
_deleteSet.insert(pObject);
}
else throw InvalidAccessException("Cannot take ownership of", this->name());
return pObject;
}
virtual bool isAutoDelete(B* pObject) const
{
return _deleteSet.find(pObject) != _deleteSet.end();
}
private:
AbstractMetaObject();
AbstractMetaObject(const AbstractMetaObject&);
AbstractMetaObject& operator = (const AbstractMetaObject&);
typedef std::set<B*> ObjectSet;
const char* _name; //维护的类名
mutable ObjectSet _deleteSet;//托管的指针集合
};
可以看出,该类完成了除了创建对象(对于单例类,则是获取实例)之外的所有功能,包括获取所维护的类的名字,托管该类指针和释放该类指针等。这里需要先记住,类名是通过构造函数传入的。
对于MetaObject类就比较简单了,基本只负责了类的创建:
源代码文件位置:Foundation/Include/poco/MetaObject.h
template <class C, class B>
class MetaObject: public AbstractMetaObject<B>
{
public:
MetaObject(const char* name): AbstractMetaObject<B>(name)
{
}
~MetaObject()
{
}
B* create() const
{
return new C;
}
B& instance() const
{
throw InvalidAccessException("Not a singleton. Use create() to create instances of", this->name());
}
bool canCreate() const
{
return true;
}
};
同样,这里还需记住,类的创建是通过MetaObject的模板参数实现的。
那么,到现在,整个层次结构已经比较清楚了,并且明白了类是如何被维护的,类名获取和类对象创建是在哪里完成的。现在还剩下的问题是,类名和类模板参数是怎么被放进去的,并且它们是怎么被联系在一起的(通过类名创建对象)。
ClassLoader流程分析
在清楚了结构之后,在按照调用顺序跟踪一边,看这些结构都是如何被建立和组织的。
在我们前面的例子中,这些结构的建立和组织都发生在一个函数:loadLibrary中:
源代码文件位置:Foundation/Include/poco/ClassLoader.h
void loadLibrary(const std::string& path, const std::string& manifest)
{
FastMutex::ScopedLock lock(_mutex);
typename LibraryMap::iterator it = _map.find(path);//在维护的map<std::string, LibraryInfo>中寻找该dll
if (it == _map.end())//如果该dll当前没有被该ClassLoader加载
{
LibraryInfo li;
li.pLibrary = new SharedLibrary(path);//在SharedLibrary类的构造函数中,完成dll加载
li.pManifest = new Manif();//typedef Manifest<Base> Manif; 构建一个新的空清单
li.refCount = 1;
try
{
std::string pocoBuildManifestSymbol("pocoBuildManifest");
pocoBuildManifestSymbol.append(manifest);//对于我们前面但参数的调用,manifest为""
if (li.pLibrary->hasSymbol("pocoInitializeLibrary"))//如果有自定义的初始化函数 执行
{
InitializeLibraryFunc initializeLibrary = (InitializeLibraryFunc) li.pLibrary->getSymbol("pocoInitializeLibrary");
initializeLibrary();
}
if (li.pLibrary->hasSymbol(pocoBuildManifestSymbol))//如果找到构造清单函数 执行
{
BuildManifestFunc buildManifest = (BuildManifestFunc) li.pLibrary->getSymbol(pocoBuildManifestSymbol);
if (buildManifest(const_cast<Manif*>(li.pManifest)))//将空清单的指针传入函数,pocoBuildManifest函数,这一步就是构造函数导出清单的关键
_map[path] = li;//如果构建清单成功 则加入到map<string, LibraryInfo>
else
throw LibraryLoadException(std::string("Manifest class mismatch in ") + path, manifest);
}
else throw LibraryLoadException(std::string("No manifest in ") + path, manifest);
}
catch (...)
{
delete li.pLibrary;
delete li.pManifest;
throw;
}
}
else//如果该dll当前已经被该ClassLoader加载 则添加引用计数
{
++it->second.refCount;
}
}
在我们前面的例子中,我们自定义了pocoInitializeLibrary函数,因此它会在该dll刚被加载后就执行,而后面看到的pocoBuildManifest函数好像我们并没有定义,但是前面说过供ClassLoader使用的每个dll都必须有一个导出类清单,实际上,为了最大程度简化使用者的负担,POCO将pocoBuildManifest函数封装成宏了,这就是我们前面在dll提供方cpp文件中看到的:
POCO_BEGIN_MANIFEST(AbstractPlugin)
POCO_EXPORT_CLASS(PluginA)
POCO_EXPORT_CLASS(PluginB)
POCO_END_MANIFEST
在Foundation/Include/poco/ClassLibrary.h中找到该宏的定义:
#define POCO_BEGIN_MANIFEST_IMPL(fnName, base) \
bool fnName(Poco::ManifestBase* pManifest_) \
{ \
typedef base _Base; \
typedef Poco::Manifest<_Base> _Manifest; \
std::string requiredType(typeid(_Manifest).name()); \
std::string actualType(pManifest_->className()); \
if (requiredType == actualType) \
{ \
Poco::Manifest<_Base>* pManifest = static_cast<_Manifest*>(pManifest_);
#define POCO_BEGIN_MANIFEST(base) \
POCO_BEGIN_MANIFEST_IMPL(pocoBuildManifest, base)
#define POCO_BEGIN_NAMED_MANIFEST(name, base) \
POCO_DECLARE_NAMED_MANIFEST(name) \
POCO_BEGIN_MANIFEST_IMPL(POCO_JOIN(pocoBuildManifest, name), base)
#define POCO_END_MANIFEST \
return true; \
} \
else return false; \
}
#define POCO_EXPORT_CLASS(cls) \
pManifest->insert(new Poco::MetaObject<cls, _Base>(#cls));
#define POCO_EXPORT_SINGLETON(cls) \
pManifest->insert(new Poco::MetaSingleton<cls, _Base>(#cls));
现在一切都明了了,这个宏展开后,就变成了pocoBuildManifest函数,并且在POCO_EXPORT_CLASS中传入了派生类和接口类作为模板参数,传入#cls也就是类名给MetaObject构造函数参数,MetaObject在构造时会将类名传给其基类AbstractMetaObject,这样其基类就可以实现name()接口了。(#可以使其后面的cls替换为字面量,比如#define TOSTRING(a) #a 那么TOSTRING(PluginA)将被替换为"PluginA")。
顺便提一下,上面的POCO_BEGIN_NAMED_MANIFEST(name,base)宏实现自命名的清单导出函数名,这样我们就可以在dll中定义多份导出清单,然后在ClassLoader的loadLibrary函数调用中,将第二参数指定为我们想要只用的那份清单名(当第二参数未提供时,默认清单名即为pocoBuildManifest),这样就实现了一个dll的多个接口类导出。
为了查看方便,我将宏展开:bool pocoBuildManifest(Poco::ManifestBase* pManifest_)
{
typedef base _Base;
typedef Poco::Manifest<_Base> _Manifest;
//提供方的类清单Manifest类名 由宏POCO_BEGIN_MANIFEST提供的接口类作为模板参数 在这里是requiredType="class Manifest<AbstractPlugin>"
std::string requiredType(typeid(_Manifest).name());
//使用方的类清单Manifest类名 这是由使用方初始化ClassLoader提供模板参数时确认的 在例子中我们使用的ClassLoader<AbstractPlugin> 这个模板参数提供给Manifest后 得到的actualType="class Manifest<AbstractPlugin>"
std::string actualType(pManifest_->className());
if (requiredType == actualType) //如果匹配 则将导出类加到类清单
{
Poco::Manifest<_Base>* pManifest = static_cast<_Manifest*>(pManifest_);
pManifest->insert(new Poco::MetaObject<PluginA, AbstractPlugin>("PluginA"));
pManifest->insert(new Poco::MetaObject<PluginB, AbstractPlugin>("PluginB"));
return true;
}
else return false;
}
现在整个结构和组织都理清楚了,现在再来看看ClassLoader的create就比较简单了,什么信息都有了,它只是组织一下其组件,通过类名在类清单Manifest中找到其对应的MetaObject类,然后再调用MetaObject类的create(),Over。
源代码文件位置:Foundation/Include/poco/ClassLoader.h
const Meta* findClass(const std::string& className) const
{
FastMutex::ScopedLock lock(_mutex);
for (typename LibraryMap::const_iterator it = _map.begin(); it != _map.end(); ++it)
{
const Manif* pManif = it->second.pManifest;
typename Manif::Iterator itm = pManif->find(className);
if (itm != pManif->end())
return *itm;
}
return 0;
}
const Meta& classFor(const std::string& className) const
{
const Meta* pMeta = findClass(className);
if (pMeta)
return *pMeta;
else
throw NotFoundException(className);
}
Base* create(const std::string& className) const
{
return classFor(className).create();
}
注:一个ClassLoader只能加载dll的一份类清单
最后梳理一下整个过程:
dll提供方:通过使用POCO_BEGIN_MANIFEST,POCO_EXPORT_CLASS等一系列宏来建立dll的pocoBuildManifest函数(可自定义命名),该函数以一个Manifest指针为参数,完成对导出类清单Manifest的填充。
dll加载方:ClassLoader在loadLibrary函数中找到并调用pocoBuildManifest函数,传入new Manifest返回的空清单指针指针,该函数完成:
1.为导出的各个类都建立一个对于的MetaObject(MetaSingleton)对象
2.将类名和对应类的MetaObject(MetaSingleton)对象放入类导出清单的map<string, Meta*>MetaMap中
整个交互都建立在一个公共接口类的前提下,通过该公共接口类Base,建立了AbstractMetaObject<Base>模板类,该类实现对各个导出的派生类的统一,并且将所有导出类都放在map<string, AbstractMetaObject<Base>> MetaMap这个容器中(由Manifest类管理),这个容器将类名和类相关操作关联起来,最后通过指定类名来找到该类对应的MetaObect(或单例MetaSingleton)类,完成该类的管理,再由ClassLoader提供出来。
整个过程就用户看来:
在提供dll时,使用两三个宏(并包含头文件ClassLibrary.h),指出要导出的类(需要有公共接口类)。
在使用dll时,使用ClassLoader类就可完成所有操作(加载库,创建类对象等)。
ClassLoader将泛型和宏结合起来,并且通过容器实现了"拟多态"(通过公共接口统一导出,然后再用类名来找到具体派生类)。
还有一点值得一提的就是ClassLoader Manifest都通过迭代器模式来使我们在遍历和使用整个结构的时候更方便,比如ClassLoader<B>::Iterator it; 对it->second返回的并不是LibraryInfo,而是Manifest*。而对Manifest::Iterator it进行解引用,得到的不是Pair<string, Meta*> 而直接是Meta*。从上面的例子的遍历过程也可以看出,这屏蔽了底层复杂的结构,极大的方便了使用。
使用ClassLoader注意事项:
1.ClassLoader只能导出dll中具有公共接口类的类
2.dll使用方只能使用dll中公共接口类提供的接口
3.公共接口类一般为抽象类,如果不为抽象类,那么其所有函数也应该是虚函数。如果接口类实现了一个普通函数,那么使用方通过ClassLoader将无法调用该方法,会出现链接错误(本人测试出来的,但是还不清楚为什么)。比如,如果在AbstractPlugin中加入一个函数:void SayGoodBye()
{
std::cout<<"GoodBye!"<<std::end;
}那么通过ClassLoader create("PluginA")返回的AbstractPlugin指针调用SayGoodBye() VS2008将会报错:
error LNK2001: 无法解析的外部符号 "public: void __thiscall AbstractPlugin::SayGoodBye(void)" (?SayGoodBye@AbstractPlugin@@QAEXXZ)
但是将该函数声明为virtual就正常了。
4.一个ClassLoader只能从一个dll中导出一份清单
5.一个dll可以有多个导出类清单(通过POCO_BEGIN_NAMED_MANIFEST重命名pocoBuildManifest函数)6.还有一点我也比较奇怪的是,ClassLoader维护的map<string, LibraryInfo> LibraryMap是一个映射,并且提供了迭代器来遍历,应该是可以加载多个dll的。但是ClassLoader又需要一个接口类作为模板参数实例化,难道它只能加载多个具有共同接口类的dll吗?