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

std::shared_ptr and dlopen(), avoiding undefined behavior

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

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

精美的图案,效果很好。不过有一个小问题。上面的模式需要从 void*whatever_type_object_is* 的转换。如果 "object_name" 指的是一个函数(考虑到用例,大多数情况下它确实如此),这是未定义的行为。

在 C 中,有一个 hack 可以解决这个问题。来自 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 来做到这一点?

您可以创建一个结构,使您的指针指向函数并指向库:

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 );

我没有编译,但我觉得足以说明思路了

The pattern above requires a cast from void* to whatever_type_object_is*. If "object_name" refers to a function (which most of the time it does, considering the use-case), this is undefined behavior.

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

5.2.10.8 说:

Converting a function pointer to an object pointer type or vice versa is conditionally-supported. The meaning of such a conversion is implementation-defined, except that if an implementation supports conversions in both directions, converting a prvalue of one type to the other type and back, possibly with different cvqualification, shall yield the original pointer value.

所以假设 dlsym 内部所做的是将函数指针转换为 void*,我相信如果你将它转换回函数指针就没问题。

是这样的吗?

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;
};

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