Linux上的C++动态共享库

187
这是使用g++编译动态共享库的后续。
我正在尝试在Linux上创建一个C++的共享类库。 我能够编译库,并且可以使用我在这里这里找到的教程调用一些(非类)函数。 当我尝试使用在库中定义的类时,问题就出现了。 我链接的第二个教程展示了如何加载符号以创建在库中定义的类的对象,但没有进一步说明如何使用这些对象来完成任何工作。
有没有人知道一个更完整的教程,用于创建共享的C++类库,同时还展示了如何在单独的可执行文件中使用这些类? 一个非常简单的教程,展示对象的创建、使用(简单的getter和setter也可以),以及删除,将会很棒。 提供一个演示使用共享类库的开源代码的链接或参考也同样好。
尽管codelogicnimrodm的答案是有效的,但我想补充一下,在提出这个问题后,我买了Beginning Linux Programming的一本书,它的第一章有示例C代码和创建和使用静态和共享库的良好解释。这些示例可以在该书的早期版本中通过Google图书搜索获得。

我不确定你所说的“使用”是什么意思,一旦返回对象的指针,你可以像使用任何其他对象的指针一样使用它。 - codelogic
我链接的文章展示了如何使用dlsym创建指向对象工厂函数的函数指针。但它并没有展示从库中创建和使用对象的语法。 - Bill the Lizard
1
您将需要头文件来描述该类。为什么您认为您必须使用“dlsym”而不是让操作系统在加载时找到并链接库呢?如果您需要一个简单的示例,请告诉我。 - nimrodm
3
@nimrodm: 使用"dlsym"的替代方案是什么?我正在(应该)编写3个C++程序,它们都将使用共享库中定义的类。我还有一个Perl脚本要使用它,但那是下周的问题。 - Bill the Lizard
4个回答

181

myclass.h

#ifndef __MYCLASS_H__
#define __MYCLASS_H__

class MyClass
{
public:
  MyClass();

  /* use virtual otherwise linker will try to perform static linkage */
  virtual void DoSomething();

private:
  int x;
};

#endif

myclass.cc

#include "myclass.h"
#include <iostream>

using namespace std;

extern "C" MyClass* create_object()
{
  return new MyClass;
}

extern "C" void destroy_object( MyClass* object )
{
  delete object;
}

MyClass::MyClass()
{
  x = 20;
}

void MyClass::DoSomething()
{
  cout<<x<<endl;
}

class_user.cc

#include <dlfcn.h>
#include <iostream>
#include "myclass.h"

using namespace std;

int main(int argc, char **argv)
{
  /* on Linux, use "./myclass.so" */
  void* handle = dlopen("myclass.so", RTLD_LAZY);

  MyClass* (*create)();
  void (*destroy)(MyClass*);

  create = (MyClass* (*)())dlsym(handle, "create_object");
  destroy = (void (*)(MyClass*))dlsym(handle, "destroy_object");

  MyClass* myClass = (MyClass*)create();
  myClass->DoSomething();
  destroy( myClass );
}
在Mac OS X上,使用以下命令进行编译:

编译命令:

g++ -dynamiclib -flat_namespace myclass.cc -o myclass.so
g++ class_user.cc -o class_user

在Linux上,使用以下命令进行编译:

g++ -fPIC -shared myclass.cc -o myclass.so
g++ class_user.cc -ldl -o class_user

如果这是针对插件系统的,你将使用MyClass作为基类,并定义所有必需的函数为虚函数。插件作者将从MyClass派生,并覆盖虚函数并实现create_objectdestroy_object。你的主应用程序不需要以任何方式进行更改。


7
我正在尝试这个过程,但只有一个问题。是严格必要使用void,还是create_object函数可以返回MyClass?我不是要求你为我更改此内容,我只想知道使用其中一个而不是另一个是否有原因。 - Bill the Lizard
1
你为什么要使用extern "C"来声明这些变量?因为这是使用g++编译器编译的。为什么要使用c命名约定?C无法调用C++。只有使用C++编写的包装接口才能从C中调用它。 - ant2009
7
@ant2009,你需要使用extern "C",因为dlsym函数是一个C函数。而且为了动态加载create_object函数,它将使用C风格的链接方式。如果不使用extern "C",由于C++编译器中的名称重整,将无法知道.so文件中create_object函数的名称。 - kokx
1
不错的方法,它与某人在Microsoft编译器上的做法非常相似。通过一些#if #else的工作,您可以得到一个很好的平台无关系统。 - user365268
1
我知道这已经很老了(我差点说“三年”,但后来意识到实际上快接近5年了),但是调用dlclose关闭句柄非常重要。请添加该操作。 - stefan
显示剩余9条评论

61
以下是一个共享类库 shared.[h,cpp] 的示例和使用该库的 main.cpp 模块。这只是一个非常简单的示例,Makefile 可以做得更好,但它有效并且可能对你有所帮助。
shared.h 定义了该类:
class myclass {
   int myx;

  public:

    myclass() { myx=0; }
    void setx(int newx);
    int  getx();
};

shared.cpp 定义了 getx/setx 函数:

#include "shared.h"

void myclass::setx(int newx) { myx = newx; }
int  myclass::getx() { return myx; }

main.cpp使用了这个类,

#include <iostream>
#include "shared.h"

using namespace std;

int main(int argc, char *argv[])
{
  myclass m;

  cout << m.getx() << endl;
  m.setx(10);
  cout << m.getx() << endl;
}

这是生成libshared.so并将main链接到共享库的Makefile:

main: libshared.so main.o
    $(CXX) -o main  main.o -L. -lshared

libshared.so: shared.cpp
    $(CXX) -fPIC -c shared.cpp -o shared.o
    $(CXX) -shared  -Wl,-soname,libshared.so -o libshared.so shared.o

clean:
    $rm *.o *.so

如果要实际运行'main'并链接到libshared.so,您可能需要指定加载路径(或将其放在/usr/local/lib或类似位置)。

以下是将当前目录指定为库的搜索路径并运行main的命令(bash语法):

export LD_LIBRARY_PATH=.
./main

要查看程序是否链接了libshared.so库,您可以尝试使用ldd命令:

LD_LIBRARY_PATH=. ldd main

在我的机器上打印:

  ~/prj/test/shared$ LD_LIBRARY_PATH=. ldd main
    linux-gate.so.1 =>  (0xb7f88000)
    libshared.so => ./libshared.so (0xb7f85000)
    libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0xb7e74000)
    libm.so.6 => /lib/libm.so.6 (0xb7e4e000)
    libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0xb7e41000)
    libc.so.6 => /lib/libc.so.6 (0xb7cfa000)
    /lib/ld-linux.so.2 (0xb7f89000)

1
这似乎(在我非常不熟练的眼中)是将libshared.so静态链接到您的可执行文件中,而不是在运行时使用动态链接。我正确吗? - Bill the Lizard
11
不,这是标准的Unix(Linux)动态链接。动态库的扩展名为“.so”(共享对象),在加载主程序时与可执行文件(本例中的main)进行链接 - 每次加载main时都会进行链接。静态链接发生在链接时间,并使用扩展名为“.a”(存档)的库。 - nimrodm
10
这是在构建时动态链接的。换句话说,您需要预先了解您正在链接的库(例如,链接'dl'用于dlopen)。这与基于用户指定的文件名动态加载库不同,后者不需要预先了解。 - codelogic
11
我试图(不太成功地)解释的是,在这种情况下,你需要在编译时知道库的名称(需要将-lshared传递给gcc)。通常,当这个信息不可用时,即在运行时才发现库的名称(例如:插件枚举),使用dlopen()。 - codelogic
3
在链接时使用-L. -lshared -Wl,-rpath=$$(ORIGIN),并删除LD_LIBRARY_PATH=. - Maxim Egorushkin
显示剩余2条评论

15
除了之前的答案外,我想提醒大家,为了安全地销毁处理程序,应该使用RAII(资源获取即初始化)习惯用法

这里有一个完整的工作示例:

接口声明:Interface.hpp

class Base {
public:
    virtual ~Base() {}
    virtual void foo() const = 0;
};

using Base_creator_t = Base *(*)();

共享库内容:

#include "Interface.hpp"

class Derived: public Base {
public:
    void foo() const override {}
};

extern "C" {
Base * create() {
    return new Derived;
}
}

动态共享库处理程序:Derived_factory.hpp

#include "Interface.hpp"
#include <dlfcn.h>

class Derived_factory {
public:
    Derived_factory() {
        handler = dlopen("libderived.so", RTLD_NOW);
        if (! handler) {
            throw std::runtime_error(dlerror());
        }
        Reset_dlerror();
        creator = reinterpret_cast<Base_creator_t>(dlsym(handler, "create"));
        Check_dlerror();
    }

    std::unique_ptr<Base> create() const {
        return std::unique_ptr<Base>(creator());
    }

    ~Derived_factory() {
        if (handler) {
            dlclose(handler);
        }
    }

private:
    void * handler = nullptr;
    Base_creator_t creator = nullptr;

    static void Reset_dlerror() {
        dlerror();
    }

    static void Check_dlerror() {
        const char * dlsym_error = dlerror();
        if (dlsym_error) {
            throw std::runtime_error(dlsym_error);
        }
    }
};

客户端代码:

#include "Derived_factory.hpp"

{
    Derived_factory factory;
    std::unique_ptr<Base> base = factory.create();
    base->foo();
}

注意:
  • 为了简洁起见,我把所有内容都放在头文件中。在实际工作中,你当然应该将代码分成.hpp.cpp文件。
  • 为了简化,我忽略了你想处理new/delete重载的情况。

获取更多详细信息的两篇文章:


这是一个很好的例子。RAII 绝对是正确的方法。 - David Steinhauer
为什么不使用 std::unique_ptr<void, void(*)(void *)> handle(或者显式的 dlcloser 函数对象)呢? - Caleth

9
基本上,您应该在要在共享库中使用该类的代码中包含类的头文件。然后,在链接时,使用“-l”标志将您的代码与共享库链接在一起。当然,这需要.os文件在操作系统可以找到它的地方。请参见3.5.安装和使用共享库 如果您在编译时不知道要使用哪个库,则可以使用dlsym。但这似乎不是这里的情况。也许混淆的原因是Windows无论您在编译时还是运行时(使用类似的方法)调用动态加载的库?如果是这样,那么您可以将dlsym视为LoadLibrary的等效物。
如果您确实需要动态加载库(即它们是插件),则此FAQ应该会有所帮助 。

1
我需要一个动态共享库的原因是我还将从Perl代码中调用它。我可能完全误解了自己,认为我还需要从我正在开发的其他C++程序中动态调用它。 - Bill the Lizard
我从未尝试过集成 Perl 和 C++,但是我认为你需要使用 XS:http://www.johnkeiser.com/perl-xs-c++.html - Matt Lewis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接