如何使用动态加载函数(dlopen)操作/返回数据?

4
我花了几天的时间阅读并反复阅读有关主题的所有教程,并在SO上浏览相关问题数小时(甚至数天),但我仍然不能让以下内容正常工作。如有重复,请谅解:很可能我已经看到和重读了许多次重复的问题,但无法理解答案与我的问题的相关性。有了这个前提...
我正在尝试为我的应用程序实现一个插件架构。插件被编译并安装为库。运行时,应用程序然后使用dlopen() / dlsym()来加载和链接到插件的函数。
想法是插件(库)将实现一组函数,以返回数据到主应用程序,或操纵从应用程序传递的数据。
为了测试这个想法,我尝试实现一个函数(在插件内部),该函数将返回插件本身的(可读)名称(作为std :: string)。我认为那是一个简单的开始.... :-/
以下是我目前得到的内容:
// Plugin.cpp
extern "C" void plugin_name(std::string *name) {
        name = new std::string("Example plugin name");
}

// Application.cpp
void* handle = dlopen("libplugin.so", RTLD_LAZY);
typedef void (*plugin_t)(std::string*);
dlerror(); // Reset errors.
plugin_t call_plugin_name = (plugin_t) dlsym(handle, "plugin_name");
// ... Some error handling code.
std::string my_plugin_name;
call_plugin_name(&my_plugin_name);
dlclose(handle);
// More code that displays my_plugin_name.

我尝试了许多不同的组合,包括一种看起来更直接(但效果并不更好)的方法,其中返回插件名称:

// Plugin.cpp
extern "C" std::string plugin_name(void) {
        return std::string("Example plugin name");
}

我知道我已经接近成功了:代码编译通过,应用程序也停止崩溃了;) 但是,我期望看到实际的插件名称却出现了空白。
到目前为止,我阅读的所有教程都非常快速地介绍了数据双向传递的机制:插件<=>应用程序。对于我想要使用“简单”的std::string实现的功能,我希望以后可以使用更复杂的对象(即插件函数将通过引用获取对象并更改其属性)。这些教程或多或少都在使用dlsym()创建指针的时候就停止了,并没有给出如何使用该指针的示例。
那么,如何做到这一点呢?
另一个相关的问题:我是否使用通用头文件,既可以与应用程序一起使用,也可以与插件一起使用,并在其中定义函数调用签名?我该如何做到这一点,这样做有什么好处?

你能否使用常规的 char* 让它正常工作? - genpfault
@genpfault:让我试试,然后我会回来的;我想知道是否使用 std::string(一个 C++ 类)会引起问题... - augustin
我在我的答案中添加了一些注释。 - ssmir
@ssmir:谢谢。你的回答让我找到了正确的方向。我目前正在仔细阅读你指出的LinuxJournal文章。 - augustin
3个回答

5
函数的签名是从其名称和参数类型生成的(返回值类型并不重要)。当您使用extern“C”声明函数时,会使用C符号命名方案,显然无法处理像std::string这样的C ++类型。这就是为什么将std :: string作为参数传递不起作用的原因。
我无法解释为什么返回std :: string不起作用。也许使用了不同的调用约定。
无论如何,从共享库导入C ++代码的正确方法是从入口点返回C ++类型的指针。而这些入口点必须具有在C中可用的类型的参数。(入口点是从共享库导出的已记录函数) 这里是一篇关于从共享库加载C ++类的基本方面的好文章。该文章将全面回答您的问题。
请注意,在从共享库抛出异常并将其用于主应用程序时存在陷阱。以及在库内创建的对象的dynamic_cast。我提到这些主题,以便您在面对这些问题时能够做好准备。 [编辑] 为了使我的回答更清晰,我将添加一些示例。
要获取插件名称,可以使用:
extern "C" const char * plugin_name() {
    return "Example plugin name";
}

// main.cc:
void* handle = dlopen("libplugin.so", RTLD_LAZY);
// ...
typedef const char * (*plugin_t)();
plugin_t call_plugin_name = (plugin_t) dlsym(handle, "plugin_name");
// ...
std::string my_plugin_name(call_plugin_name());
// use it

为了真正使用插件功能,您应该在头文件中声明一个基类:

要真正使用插件功能,您应该在头文件中声明一个基类:

// plugin.h
class Plugin {
    public:
        virtual void doStuff() = 0;
        virtual ~Plugin() = 0;
};

// plugin.cc
Plugin::~Plugin() {
}

// myplugin.cc
class MyPlugin : public Plugin {
    virtual void doStuff() {
        std::cout << "Hello from plugin" << std::endl;
    }
};

extern "C" Plugin *createMyPluginInstance() {
    return new MyPlugin;
}

2

尝试:

 extern "C" void plugin_name(std::string **name) {
     *name = new std::string("Example plugin name");
 }

 ...

 std::string *my_plugin_name;
 call_plugin_name(&my_plugin_name);

由于您分配了作为参数传递的指针的副本,而不是您打算分配的指针,因此出现了这种情况。 编辑:这里是: 文件main.cpp
#include <iostream>
#include <dlfcn.h>
#include <string>

// Application.cpp
int main() {
    void* handle = dlopen("libplugin.so", RTLD_LAZY);
    typedef void (*plugin_t)(std::string**);
    dlerror(); // Reset errors.
    plugin_t call_plugin_name = (plugin_t) dlsym(handle, "plugin_name");
    // ... Some error handling code.
    std::string *my_plugin_name;
    call_plugin_name(&my_plugin_name);
    dlclose(handle);
    // More code that displays my_plugin_name.
    std::cout << "Plugin name is " << *my_plugin_name << std::endl;
    delete my_plugin_name;
    return 0;
}

File plugin.cpp

#include <string>

extern "C" void plugin_name(std::string **name) {
    *name = new std::string("example plugin name");
}

提醒一句警告。尽管这段代码可以编译和运行,但在dll边界传递C++类型是有风险的。上述代码仅仅是为了使你的代码能够编译和运行而进行了修复,它并不安全,并且需要非常明确的内存处理。你可能需要用不同的方式来解决这个问题。


错误:在参数传递中无法将 'std::string **' 转换为 'std::string *' - augustin
理论上,我理解你的观点。但是当我尝试你建议的方法时,我遇到了上面评论中提到的编译错误。当我尝试其他类似的方法时,在运行时我得到了一个段错误... :-/ - augustin

1
请阅读这个问题及其答案。在C++中,共享库边界存在许多不兼容性的机会。

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