std :: shared_ptr和dlopen(),避免未定义的行为

时间:2016-03-16 15:57:50

标签: c++ c++11 shared-libraries shared-ptr undefined-behavior

dlopen()是一个C函数,用于在运行时动态加载共享库。因此,如果您不熟悉,该模式是:

  • 致电dlopen("libpath", flag)以获取void *handle到图书馆
  • 致电dlsym(handle, "object_name")以获取void *object您想要的图书馆
  • 使用object
  • 执行您想要的操作
  • 致电dlclose (handle)以卸载图书馆。

在C ++中,这是std::shared_ptr的所谓别名构造函数的完美用例。模式变为:

  • std::shared_ptr<void> handle构建dlopen("libpath", flag),在调用其析构函数时调用dlclose()
  • std::shared_ptr<void> objecthandle
  • 构建dlsym(handle, "object_name")
  • 现在我们可以在任何我们想要的地方通过object,并完全忘记handle;当调用object的析构函数时,无论何时发生,dlclose()都将被自动调用

精美的图案,效果很好。但是有一个小问题。上述模式需要从void*转换为whatever_type_object_is*。如果"object_name"引用了一个函数(考虑到用例,它大部分时间都是这样),这是未定义的行为。

在C中,有一个黑客来解决这个问题。从dlopen手册页:

// ...
void *handle;    
double (*cosine)(double);
// ...
handle = dlopen("libm.so", RTLD_LAZY);
// ...

/* Writing: cosine = double (*)(double)) dlsym(handle, "cos");
   would seem more natural, but the C99 standard leaves
   casting from "void *" to a function pointer undefined.
   The assignment used below is the POSIX.1-2003 (Technical
   Corrigendum 1) workaround; see the Rationale for the
   POSIX specification of dlsym(). */

*(void **) (&cosine) = dlsym(handle, "cos");
// ...

显然在C语言中运行得很好。但使用std::shared_ptr是否有一种简单的方法可以做到这一点?

3 个答案:

答案 0 :(得分:4)

  

上述模式需要从void *转换为whatever_type_object_is *。如果“object_name”引用一个函数(考虑到用例,它大部分时间都是这样),这是未定义的行为。

这不完全正确,至少在C ++中它只是有条件支持。

5.2.10.8 说:

  

有条件地支持将函数指针转换为对象指针类型,反之亦然。意思   这种转换是实现定义的,除非实现支持转换   两个方向,将一种类型的prvalue转换为另一种类型并返回,可能具有不同的资格,   将产生原始指针值。

因此,假设dlsym内部正在向void*投射函数指针,我相信如果你只是将其强制转换为函数指针就可以了。

答案 1 :(得分:1)

这样的东西?

struct dlib
{
public:
  template<class T>
  std::shared_ptr<T> sym(const char* name) const {
    if (!handle) return {};
    void* sym = dlsym(handle->get(), name);
    if (!sym) return {};
    return {reinterpret_cast<T*>(sym), handle};
  }
  // returns a smart pointer pointing at a function for name:
  template<class Sig>
  std::shared_ptr<Sig*> pfunc(const char* name) const {
    if (!handle) return {};
    void* sym = dlsym(handle->get(), name);
    if (!sym) return {};
    Sig* ret = 0;
    // apparently approved hack to convert void* to function pointer
    // in some silly compilers:
    *reinterpret_cast<void**>(&ret) = sym;
    return {ret, handle};
  }
  // returns a std::function<Sig> for a name:
  template<class Sig>
  std::function<Sig> function(const char* name) const {
    // shared pointer to a function pointer:
    auto pf = pfunc(name);
    if (!pf) return {};
    return [pf=std::move(pf)](auto&&...args)->decltype(auto){
      return (*pf)(decltype(args)(args)...);
    };
  }
  dlib() = default;
  dlib(dlib const&)=default;
  dlib(dlib &&)=default;
  dlib& operator=(dlib const&)=default;
  dlib& operator=(dlib &&)=default;

  dlib(const char* name, int flag) {
    void* h = dlopen(name, flag);
    if (h)
    {
      // set handle to cleanup the dlopen:
      handle=std::shared_ptr<void>(
        h,
        [](void* handle){
          int r = dlclose(handle);
          ASSERT(r==0);
        }
      );
    }
  }
  explicit operator bool() const { return (bool)handle; }
private:
  std::shared_ptr<void> handle;
};

我怀疑是否需要黑客攻击。正如@sbabbi所指出的那样,void*的往返是有条件支持的。在使用dlsym返回函数指针的系统上,最好支持它。

答案 2 :(得分:0)

你可以创建一个结构来指向函数和处理库:

template<typename T>
struct dlsymbol {
   dlsymbol( const std::string &name, std::shared_ptr<void> handle ) :
      m_handle( std::move( handle ) )
   {
       *(void **)(&m_func) = dlsym( handle.get(), name.c_str );
   }

   std::shared_ptr<void> m_handle;
   T *m_func;
};

auto cosine = std::make_shared<dlsymbol<double(double)>>( "cos", handle );
auto d = cosine->m_func( 1.0 );

我没有编译它,但我认为这足以表明这个想法。