std::shared_ptr and dlopen(), avoiding undefined behavior
Asked Answered
S

3

13

dlopen() is a C function used for dynamically loading shared libraries at runtime. The pattern, in case you're not familiar, is thus:

  • Call dlopen("libpath", flag) to get a void *handle to the library
  • Call dlsym(handle, "object_name") to get a void *object to the thing you want from the library
  • Do what you want with object
  • Call dlclose (handle) to unload the library.

This is, in C++, a perfect use-case for the so-called aliasing constructor of std::shared_ptr. The pattern becomes:

  • Construct a std::shared_ptr<void> handle from dlopen("libpath", flag) that will call dlclose() when its destructor is called
  • Construct a std::shared_ptr<void> object from handle and dlsym(handle, "object_name")
  • Now we can pass object wherever we want, and completely forget about handle; when object's destructor is called, whenever that happens to be, dlclose() will be called automagically

Brilliant pattern, and it works beautifully. One small problem, though. 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.

In C, there is a hack to get around this. From the dlopen man page:

// ...
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");
// ...

which obviously works just fine, in C. But is there an easy way to do this with std::shared_ptr?

Shannashannah answered 16/3, 2016 at 15:57 Comment(6)
why do you need std::shared_ptr to the poointer, returned by dlsym?Shuck
@Slava: to guaranty the lifetime (don't call dlclose when there is that pointer).Flower
The aliasing constructor of std::shared_ptr allows two std::shared_ptrs to share the same "close condition" (my term, not official) without pointing to the same object or even the same type. Using a std::shared_ptr for the value returned by dlsym() gains me this benefit: the lifetime of the library is tied to the lifetime of object.Shannashannah
You ask if you could do the C way to get the pointer (before using it for the shared_ptr) ?Flower
Then just create a structure with pointer to function and std::shared_ptr to library handle and have std::shared_ptr to that structure. You may loose coolness of using "close condition" but that should work.Shuck
While C leaves it undefined, doesn't the writer of dlopen provide any guarantees about what happens? I guess not.Aqua
M
4

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.

Well this is not entirely true, at least in C++ it is just conditionally-supported.

5.2.10.8 says:

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.

So assuming that what dlsym does internally is casting a function pointer to a void*, I believe that you are ok if you just cast it back to a function pointer.

Martens answered 16/3, 2016 at 18:5 Comment(2)
en.cppreference.com/w/cpp/language/reinterpret_cast In particular point 8 covers thisNorvell
And I do think this was the intent open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#195Norvell
A
1

Something like this?

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

I doubt that hack is needed. As @sbabbi noted, the round-trip to void* is conditionally supported. On a system using dlsym to return function pointers, it better be supported.

Aqua answered 16/3, 2016 at 18:18 Comment(2)
You don't inherit from std::enable_shared_from_this. I think that you swap order of shared_ptr arguments (for the aliasing constructor).Flower
@Flower oops, earlier version did. Turned out storing a handle was smarter.Aqua
S
0

You can make a struct to have your pointer to function and handle to library:

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

I did not compile it, but I think it is sufficient to show the idea.

Shuck answered 16/3, 2016 at 16:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.