Linux上C++类的动态加载

Linux上C++类的动态加载

摘要:本文是翻译文章,主要介绍了运行时重载C++类的技术,包括了Linux上的动态加载接口、C++类的动态加载技术点及实现、自动加载技术等。最后给出了两个应用案例及相关的源代码。

  • 关键字:动态加载,C++类,Linux

    原文链接:http://porky.linuxjournal.com:8080/LJ/073/3687.html

    推荐:

    (原文)http://www.tldp.org/HOWTO/text/C++-dlopen

    (翻译)http://hi.baidu.com/clivestudio/item/fec21be454fd93aac00d75bf

     

    1. 前言

    一种可以让开发者进行更灵活设计的技术。

    Linux操作系统下的开发平台提供了很好的环境:自带丰富的测试工具,健壮的操作环境。令Linux引以为豪的是,它能适应各种编程语言。下面的说法,相信并不为过:对Linux开发者来,在所有的编译语言中,选择C作为开发语言是最多的。由此,像C++这样的语言似乎经常不在Linux开发者的讨论范围内。

    类的动态加载技术可以让开发者的设计更灵活。类的动态加载可以使实现更具扩展性,而不牺牲鲁棒性。

    本文将设计一个简单的应用,这个应用只有一个类:用于绘图包的形状类。我们将会看到,类的动态加载技术可以让我们平滑地扩展功能——增加新的形状类而不修改原有代码。

    2. 多态

    动态加载类的基本思想就是类的多态性。任何熟悉C++的人对此都不会陌生,这里就只简洁地说明一下。总的来说,多态,就是子类对象可以像父类对象的那样表现。这就是OOP(object-oriented programming,面向对象)中众所周知的“is a”关系。比如,下面的代码段中,类circle“is a”基类shape的子类(见列表1),因而对象my_circle可以像shape对象一样操作shape的成员函数draw

    列表1 基类shape的头文件

     

    01. class shape {
    02. public:
    03. void draw();
    04. };
    05. class circle : public shape { };
    06.  
    07. int main(int argc, char **argv){
    08. circle my_circle;
    09. my_circle.draw();
    10. }

    这包括所有的常规优点(如代码复用)以外,多态性的真正优势在于draw声明为虚函数或者纯虚函数的时候,如下:

     

    1. class shape{
    2. public:
    3. virtual void draw()=0;
    4. };
    5. class circle : public shape {
    6. public:
    7. void draw();
    8. }

     

    这里circle定义了自己的draw函数,以确保circle对象有正确的行为。同样,我们可以定义一些新的shape子类,并重写draw函数。这样,全部的子类都实现了shape的接口,进而可以创建一系列表现不同的对象,这些对象都调用同一个方法(调用draw成员函数)。下面是例子:

     

    1. shape *shape_list[3];   // the array that will
    2. // pointer to our shape objects
    3. shape[0] = new circle;  // three types of shapes
    4. shape[1] = new square;  // we have defined
    5. shape[2] = new triangle;
    6. for(int i = 0; i < 3; i++){
    7. shape_list[i].draw();
    8. }

     

    调用draw函数时,并不需要知道列表中对象的其他信息;C++会正确地调用对应的draw函数。这种强大的技术可以让我们的设计更灵活。现在我们可以通过继承shape来实现我们期望的行为。这里的关键是我们可以把接口(shape的原型)和实现分开。

    虽然此技术相当强大,但如果我们想添加新的继承函数的话,我们仍不得不重新编译代码。如果能够在运行时加载新类,那将会非常方便。而且,使用我们代码库的人完全可以提供新的形状类(重写draw函数)而不必需要我们的原始代码。好消息是,这是可能的,这也是本文讨论的主题。

    3. dlopen接口和类的动态加载

    虽然Linux下还没有直接的机制可以在运行时加载C++类,但是有一个直接在运行时加载C库的机制:dl函数dlopendlsymdlerrordlclose。这组函数提供了访问动态链接器ld的方法。完整的说明可以参考这些函数的man手册,这里仅简要说明。

    函数原型如下:

     

    1. void *dlopen(const char * filename, int flag);
    2. void *dlsym(void *handle, char*symbol);
    3. const char *dlerror();
    4. int dlclose(void *handle);

     

    dlopen函数通过文件名filename打开so文件,文件中的符号通过dlsym函数读取。参数flag可以取下面的值:RTLD_LAZYRTLD_NOW。如果flag设置为RTLD_LAZY,那么dlopen会不解析任何符号就返回。如果flag设置为RTLD_NOW,那么dlopen会尝试解析文件中所有未定义的符号。如果出现解析错误,函数调用失败,返回NULL。dlerror解析失败的原因。dlsym函数用于读取库中函数(或其他符号)的指针。handle是指向被引用项的指针,symbol是被引用项在实际保存文件中的字符串名字。

    假如可以通过这些函数访问C库里的函数,要样才能利用它们访问C++库?要达到这个目的,有几个问题需要解决。一个是必须能够定位到目标库函数的符号。由于C和C++文件中保存的符号是不一样的,导致解决这个问题会比看起来麻烦一些。另一个问题是要如何创建目标类的对象?最后,访问这些对象的便捷方法又是什么?下面将反过来回答这三个问题。

    因为并不知道动态加载的类的类型,在代码中应该如何访问?答案的诀窍在于前面提到的多态。可以通过基类提供的公用接口来使用新类的功能。延续上面的例子,新的shape类会重写draw函数,并在实际的对象中正确调用自己的draw函数。

    好,现在可以通过基类的指针访问子类的对象了。怎样在开始的地方创建这些对象?除了知道它们可以调用shape的接口外,并不知道目标类的其他相关信息。举个例子,假如动态地加载了一个库,这个库中有个类hexapod,如果不能提前知道类名,不能这样写:

     

    1. shape *my_shape = new hexapod;

     

    解决办法是,主程序不负责创建对象,至少不是直接创建。库中提供的shape子类必须提供一个创建对象的方法。这个可以用工厂类来实现,就像工厂模式那样,或者直接调用一个函数创建。为了简化说明,这里直接用一个函数来创建。所有形状类的函数返回值都一样:

     

    1. shape *maker();

     

    maker函数没有参数,返回构建成功的对象指针。针对前面的hexapod类,maker函数如下:

     

    1. shape *maker(){
    2. return new hexapod;
    3. }

     

    使用new来创建新的对象是绝对合法的,因为maker函数和hexapod定义是在同一个文件中。

    现在,先用dlopen加载一个库,然后用dlsym得到对应类的maker函数指针。使用这个指针创建对应类的对象。例,假如想要动态链接库libnewshapes.so,并应用其中的hexapod类。可以像这样子进行:

     

    1. void *hndl =dlopen("libnewshapes.so", RTLD_NOW);
    2. if(hndl == NULL){
    3. cerr << dlerror() << endl;
    4. exit(-1);
    5. }
    6. void *mkr = dlsym(hndl,"maker");

     

    指向maker的指针必须是void*类型的,因为dlsym返回的就是这个类型。现在,可以通过调用mkr创建hexapod类的对象。

     

    1. shape *my_shape = static_cast<shape*()>(mkr)();

     

    调用时要记得将mkr的返回强制转换成shape*类型。

    读到这里,可能有读者发现代码有个问题:调用dlsym可能失败了,无法解析“maker”。这个问题根源在于C++为了实现重载,把函数名改了,因而库中的maker函数可能变了。可以通过解析改名的规则来找到改过名的符号,不过有一个更简单的方法。只需要用extern "C"标识来告诉编译器使用C风格的链接包,如下所示:

    列表 2. 类circle的头文件和源文件

     

    01. #ifndef __CIRCLE_H
    02. #define __CIRCLE_H
    03. #include "shape.hh"
    04. class circle : public shape {
    05. public:
    06. void draw();
    07. };
    08. #endif // __CIRCLE_H
    09.  
    10.  
    11. #include <iostream> #include"circle.hh"
    12. void circle::draw(){
    13. // simple ascii circle
    14. cout << "
    15. ";
    16. cout << "     ****
    17. ";
    18. cout << "    *      *
    19. ";
    20. cout << "   *        *
    21. ";
    22. cout << "   *        *
    23. ";
    24. cout << "   *        *
    25. ";
    26. cout << "    *      *
    27. ";
    28. cout << "     ****
    29. ";
    30. cout << "
    31. ";
    32. }
    33. extern "C" {
    34. shape *maker(){
    35. return new circle;
    36. }
    37. class proxy {
    38. public:
    39. proxy(){
    40. // register the maker with the factory
    41. factory["circle"] = maker;
    42. }
    43. };
    44. // our one instance of the proxy
    45. proxy p;
    46. }

     

    4. 自动注册

    可以将maker函数载入到一个专门存放maker函数的数组中。不过,用更灵活的关联数组来保存maker函数在某些情况下或许会更有用。可以用标准模板库(Standard Template Library , STL)中的map类,通过键关联到maker函数,就能根据键值来访问。例如,给各个类命名,并通过名字来调用合适的maker函数。本例中,建立如下映射:

     

    1. typedef shape *maker_ptr();
    2. map <string, maker_ptr> factory;

     

    现在,要创建一个指定的形状时,可以通过形状的名称调用合适的maker函数:

     

    1. shape *my_shape = factory[

     

    还可以把这种技术扩展得更灵活。载入类的maker函数时,与其显式地赋值,何不让类的设计者来做这个事?稍微费点心思,让工厂方法自动注册maker函数,这样能让类设计者自由确定名字。(这里有两个忠告:所有的键类型必须相同,绝对不能出现重复的键。)

    完成这个工作的第一步是,每个形状库必须包含maker函数,并且每次打开时,都调用这个函数。(通过dlopen的man手册可以知道,如果库定义导出了一个_init函数,那么库每次打开的时候都会调用它。这看起来是一个注册maker函数的好地方,不幸的是目前在linux系统上不能正常工作。问题在于一个标准的链接对象文件crt.o导出了一个__init函数,这就导致了冲突。)至此,我们延续了这个函数名,这个机制仍正常工作。我个人赞成摒弃那种方法,更希望采用一打开库就能注册maker函数的方法。这种方法就是Jim Beveridge引入的著名的“自注册对象”(参考文献)。

    可以创建一个代理类单例用于注册maker函数。注册过程会在类的构建函数中进行,因而只需要创建一个代理类实例来处理这件事。这个类的原型如下:

     

    1. class proxy {
    2. public:
    3. proxy(){
    4. factory["shape name"] = maker;
    5. }
    6. };

     

    这里假设工厂实例是主程序里定义导出的全局map实例。使用gcc/egcs,可以设置rdynamic选项来强制主程序导出能够用dlopen函数载入的符号表。

    下一步,只定义一个代理实例:

     

    1. proxy p;

     

    现在,打开库时,传参数RTLD_NOW给dlopen函数,p就会被实例化,并注册目标的maker函数。如果想创建一个circle,可以这样调用circle的maker函数:

     

    1. shape *my_circle =factory["circle"];

     

    自动注册处理是非常强大的,利用这个功能设计出的主程序也能支持信息缺乏的类。例如,主程序动态载入了某个形状库之后,通过工厂实例里注册的键,就能够创建一个形状选择菜单。用户从菜单列表中选择"circle",程序就立刻能调用正确的maker函数。这里,circle类已经支持shape的API,并且maker函数也正确定义了,所以主程序根本不需要其它的信息。

    列表 3. 类Square的头文件和源文件

     

    01. #ifndef __SQUARE_H
    02. #define __SQUARE_H
    03. #include "shape.hh"
    04. class square : public shape {
    05. public:
    06. void draw();
    07. };
    08. #endif // __SQUARE_H
    09.  
    10.  
    11. #include <iostream>
    12. #include "square.hh"
    13. void square::draw(){
    14. // simple ascii square
    15. cout << "
    16. ";
    17. cout << "   *********
    18. ";
    19. cout << "    *       *
    20. ";
    21. cout << "    *       *
    22. ";
    23. cout << "    *       *
    24. ";
    25. cout << "    *       *
    26. ";
    27. cout << "   *********
    28. ";
    29. cout << "
    30. ";
    31. }
    32. extern "C" {
    33. shape *maker(){
    34. return new square;
    35. }
    36. class proxy {
    37. public:
    38. proxy(){
    39. // register the maker with the factory
    40. factory["square"] = maker;
    41. }
    42. };
    43. // our one instance of the proxy
    44. proxy p;
    45. }

     

     

    到这里,把列表1到列表5放到一起,相关的概念已经清晰了。列表1的shape类是所有形状类的基类。列表2和列表3分别是circle类和square类的源代码,它们支持库的动态加载。

    列表 4. 主程序代码,调用可以动态载入的类circle和类square

     

    001. #include <iostream>
    002. #include <map>
    003. #include <list>
    004. #include <vector>
    005. #include <string>
    006. #include <dlfcn.h>
    007. #include <stdio.h>
    008. #include <unistd.h>
    009. #include "shape.hh"
    010. // size of buffer for reading indirectory entries
    011. static unsigned int BUF_SIZE = 1024;
    012. // our global factory for making shapes
    013. map<string, maker_t *,less<string> > factory;
    014. int main(int argc, char **argv){
    015. FILE *dl;   // handle to readdirectory
    016. char *command_str = "ls *.so"// command
    017. // string to get dynamic libnames
    018. char in_buf[BUF_SIZE]; // input buffer for lib
    019. // names
    020. list<void *> dl_list; // list to hold handles
    021. // for dynamiclibs
    022. list<void *>::iterator itr;
    023. vector<string> shape_names;  // vector of shape
    024. // types used to build menu
    025. list<shape *> shape_list; // list of shape
    026. // objects we create
    027. list<shape *>::iterator sitr;
    028. map<string, maker_t *,less<string> >::iterator fitr;
    029. // get the names of all the dynamic libs (.so
    030. // files) in the current dir
    031. dl = popen(command_str, "r");
    032. if(!dl){
    033. perror("popen");
    034. exit(-1);
    035. }
    036. void *dlib;
    037. char name[1024];
    038. while(fgets(in_buf, BUF_SIZE, dl)){
    039. // trim off the whitespace
    040. char *ws = strpbrk(in_buf, "  
    041. ");
    042. if(ws) *ws = '';
    043. // append ./ to the front of the lib name
    044. sprintf(name, "./%s", in_buf);
    045. dlib = dlopen(name, RTLD_NOW);
    046. if(dlib == NULL){
    047. cerr << dlerror() << endl;
    048. exit(-1);
    049. }
    050. // add the handle to our list
    051. dl_list.insert(dl_list.end(), dlib);
    052. }
    053. int i = 0;
    054. // create an array of the shape names
    055. for(fitr=factory.begin(); fitr!=factory.end();
    056. fitr++){
    057. shape_names.insert(shape_names.end(),
    058. fitr->first);
    059. i++;
    060. }
    061. int choice;
    062. // create a menu of possible shapes to create and let the user make some
    063. while(1){
    064. i = 1;
    065. for(fitr=factory.begin();
    066. fitr!=factory.end(); fitr++){
    067. cout << i << " - Create " << fitr->first
    068. << endl;
    069. i++;
    070. }
    071. cout << i << " - Draw created shapes
    072. ";
    073. i++; i
    074. cout << i << " - Exit
    075. ";
    076. cout << "> ";
    077. cin >> choice;
    078. if(choice == i){
    079. // destroy any shapes we created
    080. for(sitr=shape_list.begin();
    081. sitr!=shape_list.end();sitr++){
    082. delete *sitr;
    083. }
    084. // close all the dynamic libs we opened
    085. for(itr=dl_list.begin(); itr!=dl_list.end(); itr++){
    086. dlclose(*itr);
    087. }
    088. exit(1);
    089. }
    090. if(choice == i - 1){
    091. // draw the shapes
    092. for(sitr=shape_list.begin();
    093. sitr!=shape_list.end();sitr++){
    094. (*sitr)->draw();
    095. }
    096. }
    097. if(choice > 0 && choice < i - 1){
    098. // add the appropriate shape to the shape list
    099. shape_list.insert(shape_list.end(),
    100. factory[shape_names[choice-1]]());
    101. }
    102. }
    103. }

     

     

    列表4是可扩展的动态加载库主程序。程序扫描当前目录下的所有so文件(库文件)并打开。这些库会用主程序提供的全局工厂对象注册自身的maker函数。然后,主程序根据库注册过的名字动态地创建形状菜单给用户。通过菜单,用户可以创建各种形状,画形状,或直接退出程序。列表5是用于编译项目的Makefile。

    列表 5.Makefile

     

    01. CC = g++
    02. LIBS = -ldl
    03. .cc .o:
    04. $(CC) -ggdb -c $<
    05. default:
    06. make testdcl
    07. OBJS = testdcl.o
    08. testdcl: testdcl.o
    09. $(CC) -rdynamic -o testdcl testdcl.o $(LIBS)
    10. libcircle.so:  circle.o
    11. g++ -shared -Wl,-soname,libcircle.so -o libcircle.so circle.o
    12. libsquare.so:  square.o
    13. g++ -shared -Wl,-soname,libsquare.so -o libsquare.so square.o
    14. all: testdcl libcircle.so libsquare.so
    15. clean:
    16. rm -f *.so *.o testdcl

     

     

    5. 应用案例

    最近,本人有用到了这个技术的两个案例。第一个案例,开发移动物体的模拟器。需要在不能访问主要源代码的情况下,让用户添加移动物体的新类型。要完成这个功能,定义了一个entity基础类,这个类提供了模拟移动物体的所有接口。一个简化版的entity定义如下:

     

    1. class entity {
    2. private:
    3. float xyz[3];  // position of theobject
    4. public:
    5. activate(float)=0// tell the object to move
    6. render()=0;  // tell the object todraw itself
    7. };

     

    所有的entity都至少有三维坐标,都可以画出自身。大部分entity除了位置之外还有其他的状态变量,同时也不仅仅只有activate函数和render函数,但是这些并不能通过entity接口访问。

    可以根据用户期望的动作来定义新的entity类型。运行时,程序载入子目录Entity下的所有库,使它们在模拟的过程能被调用。

    第二个案例是最近的项目,创建一个能加载/保存多种图片格式的库。这个库要可扩展的,因此我们新建了一个image_handler基类用于加载和保存图片。

     

    1. class image_handler{
    2. public:
    3. virtual Image loadImage(char *)=0;
    4. virtual int saveImage(char *, Image &)=0;
    5. };

     

    image_handler有两个公有函数,分别用于加载和保存图片。Image类是库中图片的基本类型,可以访问图片的数据成员和一些基本的图片操作函数。

    在这个案例中,不关心不同类型的image_handler类的多个对象,只要求image_handler类对象可以加载和保存对应类型的图片。因而只需在库中创建这个handler的单例,而不是给每个handler注册一个maker函数,创建的这个handler指针会注册到一个全局映射里。这个全局映射也不是工厂对象,更像是一个普通的图片加载/保存操作器。这里把文件后缀(tiff, jpg等)作为键。一种图片格式可能会有多种文件名后缀(如tiff,TIFF),因此每个handler可能会在全局映射中注册多次,一个后缀一次。

    解析出文件后缀后,这个库能够让主程序简单地通过来调用正确的处理函数进行图片的载入和保存。

    使用这个库,主程序可以根据文件后缀调用适当的处理函数载入或保存图片。

     

    1. map <string, handler,less<string>> handler_map;
    2. char *filename ="flower.tiff";
    3. char ext[MAX_EXT_LEN];
    4. howEverYouWantToParseTheExtensions(filename,ext);
    5. // after parsing"flower.tiff" ext = "tiff"
    6. Image img1 =handler_map[ext]->loadImage(filename);
    7. // process data here
    8. handler_map[ext]->saveImage(filename,img1);

     

    6. 结论

    利用类的动态加载技术可以实现更具扩展性更强健的代码。只要设计出考虑周全的类,运用动态加载类的技术,就能够把扩展代码的实用方法提供给用户。

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
文中是linuxC++动态库 实现接口提供导出的一个例子 注意其中使用函数返回基指针的用法,因为Linux动态链接库不能像MFC中那样直接导出 一、介绍 如何使用dlopen API动态加载C++函数和,是Unix C++程序员经常碰到的问题。 事实上,情况偶尔有些复杂,需要一些解释。这正是写这篇mini HOWTO的缘由。 理解这篇文档的前提是对C/C++语言中dlopen API有基本的了解。 这篇HOWTO的维护链接是: http://www.isotton.com/howtos/C++-dlopen-mini-HOWTO/ 二、问题所在 有时你想在运行时加载一个库(并使用其中的函数),这在你为你的程序写一些插件或模块架构的时候经常发生。 在C语言中,加载一个库轻而易举(调用dlopen、dlsym和dlclose就够了),但对C++来说,情况稍微复杂。 动态加载一个C++库的困难一部分是因为C++的name mangling (译者注:也有人把它翻译为“名字毁坏”,我觉得还是不翻译好), 另一部分是因为dlopen API是用C语言实现的,因而没有提供一个合适的方式来装载。 在解释如何装载C++库之前,最好再详细了解一下name mangling。 我推荐您了解一下它,即使您对它不感兴趣。因为这有助于您理解问题是如何产生的,如何才能解决它们。 1. Name Mangling 在每个C++程序(或库、目标文件)中, 所有非静态(non-static)函数在二进制文件中都是以“符号(symbol)”形式出现的。 这些符号都是唯一的字符串,从而把各个函数在程序、库、目标文件中区分开来。 在C中,符号名正是函数名:strcpy函数的符号名就是“strcpy”,等等。 这可能是因为两个非静态函数的名字一定各不相同的缘故。 而C++允许重载(不同的函数有相同的名字但不同的参数), 并且有很多C所没有的特性──比如、成员函数、异常说明──几乎不可能直接用函数名作符号名。 为了解决这个问题,C++采用了所谓的name mangling。它把函数名和一些信息(如参数数量和大小)杂糅在一起, 改造成奇形怪状,只有编译器才懂的符号名。 例如,被mangle后的foo可能看起来像foo@4%6^,或者,符号名里头甚至不包括“foo”。 其中一个问题是,C++标准(目前是[ISO14882])并没有定义名字必须如何被mangle, 所以每个编译器都按自己的方式来进行name mangling。 有些编译器甚至在不同版本间更换mangling算法(尤其是g++ 2.x和3.x)。 即使您搞清楚了您的编译器到底怎么进行mangling的,从而可以用dlsym调用函数了, 但可能仅仅限于您手头的这个编译器而已,而无法在下一版编译器下工作。 三、 使用dlopen API的另一个问题是,它只支持加载函数。 但在C++中,您可能要用到库中的一个,而这需要创建该的一个实例,这不容易做到。 四、解决方案 1. extern "C" C++有个特定的关键字用来声明采用C binding的函数: extern "C" 。 用 extern "C"声明的函数将使用函数名作符号名,就像C函数一样。 因此,只有非成员函数才能被声明为extern "C",并且不能被重载。 尽管限制多多,extern "C"函数还是非常有用,因为它们可以象C函数一样被dlopen动态加载。 冠以extern "C"限定符后,并不意味着函数中无法使用C++代码了, 相反,它仍然是一个完全的C++函数,可以使用任何C++特性和各种型的参数。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值