【问题标题】:Automatically Creating Wrappers for Classes Loaded with dlopen()为使用 dlopen() 加载的类自动创建包装器
【发布时间】:2019-07-08 10:15:24
【问题描述】:

我正在编写一个代理类,它使用dlopen() 加载一个共享库,并将其成员函数转发给后台加载的共享对象内的代理类实例的适当成员。

例如,共享对象有一个Person 类:

class Person
{
    ...
    void setName(std::string name);
};

我添加了一个包含 person.h 标头并定义以下符号的包装文件:

extern "C" {
    void Person_setName(void* instancePointer, std::string name);
}

这个调用只是转发到作为第一个参数的 person 对象,extern "C" 以避免整个名称修改问题。在客户端,我编写了一个 Person 类,该类具有相同的成员,该类包含指向包装类的指针并转发所有调用。

现在出现了一些问题:

  1. 是否有更好的方法来实例化和使用加载的共享对象中的类?我发现了一些其他解决方案可以在没有包装器的情况下生存,但它在 Usenet 上不受欢迎,并且高度依赖于 GCC 及其版本以及未定义的行为,因此我决定反对这条路线。
  2. 有没有办法自动创建这些包装器?他们都只是添加第一个参数,该参数是指向每个方法的实例的指针并转发给真实的东西。必须有一些模板魔法,不是吗?目前我正在使用一些宏来完成这项工作,但我对模板解决方案感兴趣。
  3. 也许有一些工具可以自动完成这样的事情,比如 SWIG?据我所知,SWIG 只是用于将 C++ 接口导出到高级语言。

【问题讨论】:

  • 我倾向于直接从共享库中返回对象。 Are clang++ and g++ ABI compatible?
  • 这就是我在undefined behavior 下提到的解决方案。我已经解决了损坏的符号并使用了 void (*setName)(Person*, std::string) 的强制转换,但这似乎不鼓励。如果您有使用此路线的代码链接,我仍然会感兴趣。
  • 如果你直接从共享库返回一个Person 的实例,你就不需要 C 包装器和强制转换。
  • 如果使用dlopen()打开共享对象的客户端程序在这样一个返回的实例上调用setName,它将如何找到正确的符号?
  • 我做了一个例子来说明如何。

标签: c++ templates shared-libraries dlopen


【解决方案1】:

有没有更好的方法来实例化和使用加载的共享对象中的类?

如果您想要安全并支持任何共享对象(即由任何编译器/标志等编译),那么不行:您必须通过 C ABI。

请记住,您也不应该在接口中使用 C++ 对象,例如就像 std::string 你传入的 Person_setName

有没有办法自动创建这些包装器?必须有一些模板魔法,不是吗?目前我正在使用一些宏来完成这项工作,但我会对模板解决方案感兴趣。

不,您不能即时创建成员函数(我们还没有反射、元类和类似的编译时特性)。

它们都只是添加第一个参数,该参数是指向每个方法的实例的指针并转发给真实的东西。

当然,您可以创建一个可变参数模板,将参数转发给给定的 extern "C" 函数,但与简单地调用 C 函数相比,您并没有真正从中获得任何有用的东西。换句话说,不要这样做:要么创建一个适当的 C++ 类,要么让用户调用 C 函数。

也许有一些工具可以自动完成这样的事情,比如 SWIG?据我所知,SWIG 只是用于将 C++ 接口导出到高级语言。

我过去曾使用Clang's tooling support 来执行与预构建步骤类似的任务,因此确实可以!

【讨论】:

  • 你能提供一些关于你是如何使用 Clang 的指针吗?我已经看到 SWIG 导出 XML,以便可用于生成包装器的自定义工具。问题是可能有几个相当大的类需要包装,但会不断被其他开发人员更改,因此保持包装最新将意味着很多麻烦。
  • 完成,我在答案中添加了一个链接 :-) 是的,我实际上在一个项目中遇到了同样的情况,外部开发人员随时更改代码。我们对所有这些都强制执行了预构建步骤,从而解决了问题。
【解决方案2】:

原来的问题已经回答了,但是cmet里面有这个问题我觉得也需要回答一下。

使用dlopen()的客户端程序将如何打开共享 如果对象在这样的返回上调用setName,则找到正确的符号 实例?

您创建方法virtual。这是一个创建libfoobar.so 的示例。它提供了一个工厂函数(make_Foo)来创建一个Foo。可以从 Foo 实例创建 Bar。所有方法都可以通过实例指针使用。我混合使用原始指针和unique_ptrs 来显示一些选项。

首先,foobar.hppfoobar.cpp 将被放入 libfoobar.so

foobar.hpp

#pragma once

#include <string>
#include <memory>

class Bar;

class Foo {
public:
    Foo();
    virtual ~Foo();

    virtual void set_name(const std::string& name);
    virtual std::string const& get_name() const;

    virtual Bar* get_Bar() const;

private:
    std::string m_name;
};

class Bar {
public:
    Bar(const Foo&);
    virtual ~Bar();

    virtual std::string const& get_value() const;
private:
    std::string m_value;
};

// a Foo factory
extern "C" {
std::unique_ptr<Foo> make_Foo();
}

foobar.cpp

#include "foobar.hpp"

// You can also use the library constructor and destructor
// void __attribute__((constructor)) init(void) {}
// void __attribute__((destructor)) finalize(void) {}

// Foo - impl

Foo::Foo() : m_name{} {}

Foo::~Foo() {}

void Foo::set_name(const std::string& name) {
    m_name = name;
}

std::string const& Foo::get_name() const {
    return m_name;
}

Bar* Foo::get_Bar() const {
    return new Bar(*this);
}

// Bar - impl

Bar::Bar(const Foo& f) :  m_value(f.get_name()) {}
Bar::~Bar() {}

std::string const& Bar::get_value() const { return m_value; }

// a factory function that can be loaded with dlsym()
extern "C" {
std::unique_ptr<Foo> make_Foo() {
    return std::make_unique<Foo>();
}
}

然后是一个通用的动态库助手:

dynlib.hpp

#pragma once

#include <dlfcn.h> // dlload, dlsym, dlclose
#include <stdexcept>

using dynlib_error = std::runtime_error;

class dynlib {
public:
    dynlib(const char* filename);
    dynlib(const dynlib&) = delete;
    dynlib(dynlib&&);
    dynlib& operator=(const dynlib&) = delete;
    dynlib& operator=(dynlib&&);
    virtual ~dynlib();

protected:
    template<typename T>
    T load(const char* symbol) const {
        static_cast<void>(dlerror()); // clear errors
        return reinterpret_cast<T>(dlsym(handle, symbol));
    }

private:
    void* handle;
};

dynlib.cpp

#include "dynlib.hpp"
#include <utility>

dynlib::dynlib(const char* filename) : handle(dlopen(filename, RTLD_NOW | RTLD_LOCAL)) {
    if(handle == nullptr) throw dynlib_error(std::string(dlerror()));
}

dynlib::dynlib(dynlib&& o) : handle(std::exchange(o.handle, nullptr)) {}

dynlib& dynlib::operator=(dynlib&& o) {
    if(handle) dlclose(handle);
    handle = std::exchange(o.handle, nullptr);
    return *this;
}

dynlib::~dynlib() {
    if(handle) dlclose(handle);
}

还有一个dynlib 后代加载libfoobar.so

foobarloader.hpp

#pragma once

#include "foobar.hpp"
#include "dynlib.hpp"

#include <memory>

class foobarloader : public dynlib {
public:
    using foo_t = std::unique_ptr<Foo> (*)();
    const foo_t make_Foo; // a factory function to load
                          // add more if needed

    foobarloader();
};

foobarloader.cpp

#include "foobarloader.hpp"

foobarloader::foobarloader() :
    dynlib("./libfoobar.so"),
    make_Foo(load<foo_t>("make_Foo")) // load function
{
    if(make_Foo == NULL) throw dynlib_error(std::string(dlerror()));
}

最后是一个不以任何方式与libfoobar.so 链接的应用程序:

test_app.cpp

#include "foobarloader.hpp"

#include <iostream>
#include <stdexcept>
#include <utility>
#include <memory>

int main() {
    try {
        foobarloader fbl;

        auto f = fbl.make_Foo(); // std::unique_ptr<Foo> example
        f->set_name("Howdy");
        std::cout << "Foo name: " << f->get_name() << "\n";

        Bar* b = f->get_Bar();   // raw Bar* example
        std::cout << "Bar value: " << b->get_value() << "\n";
        delete b;

    } catch(const std::exception& ex) {
        std::clog << "Exception: " << ex.what() << "\n";
        return 1;
    }
}

建筑

如果您使用clang++ 添加-Wno-return-type-c-linkage 来禁止有关工厂方法的警告。我使用了-DNDEBUG -std=c++14 -O3 -Wall -Wextra -Wshadow -Weffc++ -pedantic -pedantic-errors,但为简洁起见将其排除在下面。

g++ -fPIC -c -o foobar.o foobar.cpp
g++ -shared -o libfoobar.so foobar.o

g++ -c -o dynlib.o dynlib.cpp
g++ -c -o foobarloader.o foobarloader.cpp
g++ -c -o test_app.o test_app.cpp

g++ -rdynamic -o test_app test_app.o foobarloader.o dynlib.o -ldl

没有libfoobar.so链接:

% ldd test_app
    linux-vdso.so.1 (0x00007ffcee58c000)
    libdl.so.2 => /lib64/libdl.so.2 (0x00007fb264cb3000)
    libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007fb264aba000)
    libm.so.6 => /lib64/libm.so.6 (0x00007fb264974000)
    libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007fb26495a000)
    libc.so.6 => /lib64/libc.so.6 (0x00007fb264794000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fb264cfd000)

但是类成员函数按预期工作:

% ./test_app
Foo name: Howdy
Bar value: Howdy

【讨论】:

  • 这是一个很酷的解决方案,可惜我只能为你所投入的努力投票一次......You make the methods virtual. 你能解释为什么virtual 方法有助于在这种情况下加载吗?
  • @hochl 谢谢!好吧,我不是该主题的专家,但使用virtual 表示late binding 使链接器“高兴”,并承诺稍后“将”绑定。我不确定我自己的解释实际上是否正确,但这就是我在 Linux 和 Windows 中使用它的方式:-)
  • 我已经相应地改变了我的实现,它似乎工作。现在我会接受这两个答案,但不幸的是这是不可能的;-)
  • @hochl 很好 :-) 有一些事情需要考虑:它很脆弱。在我的示例中,.so 正在执行new - 但它是应用程序端执行delete。从.so 返回的unique_ptr 也必须与编译应用程序时使用的匹配。在 Windows 中构建 DLL 时(我不确定哪个工具链会使用它),我为接口 (#ifndef BUILDING_LIB) 中的类重载了 operator delete,这将 delete 推迟到编译成的 Destroy 成员函数.so 以确保使用正确的 delete
  • 析构函数将被动态找到,但不是用于释放内存的实际free。如果.so 使用的那个和应用程序端使用的有区别,它可能会变得丑陋。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-01-15
  • 2017-01-26
  • 2016-06-27
  • 1970-01-01
相关资源
最近更新 更多