3
\$\begingroup\$

This is my attempt to load shared library on linux (and may be mac - did not test yet)

I am interested if I am implementing everything correctly and if I can really use my function in a loop like that.

I deliberately use iostream, because it is part of the C++ library and it wont work unless library is linked (-lstdc++)

Thanks in advance.

base.h

#ifndef BASE_H__
#define BASE_H__

struct Base{
    virtual int do_something() = 0;

    virtual ~Base(){
    }
};

#endif

main.cpp

#include "base.h"

#include <dlfcn.h>

#include <iostream>
#include <memory>
#include <optional>

template<typename T, typename... Args>
std::optional<T> dl_exec(const char *filename, const char *symbol, Args... args){
    constexpr bool show_errors = true;

    auto err = [show_errors](const char *msg){
        if constexpr(show_errors){
            std::cerr << "Error Message: " << msg << '\n';
            std::cerr << "Error System: " << dlerror() << '\n';
        }
        return std::optional<T>{};
    };

    void *handle = dlopen(filename, RTLD_LAZY);

    if (!handle)
        return err("Bad filename");

    void *fp =  dlsym(handle, symbol);

    if (!fp)
        return err("Bad symbol name");

    auto f =  ( T (*)(Args...) ) fp;

    return f(args...);
}

int main(int argc, char **argv){
    for(int i = 0; i < 10; ++i){
        auto o = dl_exec<Base *, int>(argv[1], "create_class", i);

        if (!o){
            std::cerr << "Error detected" << '\n';
            return 1;
        }

        std::unique_ptr<Base> b{ *o };

        std::cout << "result " << b->do_something() << '\n';
    }
}

dynamic1.cpp

#include "base.h"

#include <iostream>

struct Dynamic : public Base{
    int do_something() final{
        std::cout << "It works!!!" << '\n';

        return 42;
    }
};

extern "C" {
    Base *create_class(int){
        return new Dynamic();
    }
}

// g++ -fPIC -shared dynamic1.cc -o dynamic1.so

dynamic2.cpp

#include "base.h"

#include <iostream>

struct Dynamic : public Base{
    Dynamic(int a) : a(a){
    }

    int do_something() final{
        std::cout << "It works 100%!!!" << '\n';

        return 100 + a;
    }

    int a;
};

extern "C" {
    Base *create_class(int a){
        return new Dynamic(a);
    }
}

// g++ -fPIC -shared dynamic2.cc -o dynamic2.so

Example usage

[nmmm@zenbook DL]$ gcc main.cpp -lstdc++
[nmmm@zenbook DL]$ g++ -fPIC -shared dynamic1.cpp -o dynamic1.so
[nmmm@zenbook DL]$ g++ -fPIC -shared dynamic2.cpp -o dynamic2.so
[nmmm@zenbook DL]$ ./a.out ./dynamic1.so 
result It works!!!
42
result It works!!!
42
result It works!!!
42
result It works!!!
42
result It works!!!
42
result It works!!!
42
result It works!!!
42
result It works!!!
42
result It works!!!
42
result It works!!!
42
[nmmm@zenbook DL]$ ./a.out ./dynamic2.so 
result It works 100%!!!
100
result It works 100%!!!
101
result It works 100%!!!
102
result It works 100%!!!
103
result It works 100%!!!
104
result It works 100%!!!
105
result It works 100%!!!
106
result It works 100%!!!
107
result It works 100%!!!
108
result It works 100%!!!
109
\$\endgroup\$
1
  • \$\begingroup\$ Have you tested this using a C test file, not sure it will work correctly. \$\endgroup\$
    – pacmaninbw
    Jun 23 at 21:37

1 Answer 1

1
\$\begingroup\$

Prefer = default over {}

Prefer using = default instead of using an empty function body for the special member functions. While it doesn't really matter for the code you wrote, there is a slight difference between = default and {}.

In particular, defining a function with an empty body {} causes it to be treated as a user-provided function, which means the class would no longer be trivial, which could impact performance and/or the ability to freely copy it around in memory. However, since your class is already virtual, it can't be trivial to begin with.

Another potential issue with virtual classes is that if the base class has its destructor deleted, then you cannot create a destructor in the derived class. But as explained in this post, if you use = default in the derived class, it would be legal and it would cause the destructor in the derived class to also be deleted. Basically, it would work without you having to know anything about the destructor of the base class.

So in general, write = default instead of {} for the special member functions, as it will almost always be better.

Resource leak

You call dlopen() but you never call dlclose(). While this is not as bad as forgetting to close a file or freeing memory, as it won't actually load in the shared object twice, this is bad practice. Always make sure resources are freed after you have used them. However, calling dlclose() inside dl_exec() is actually very inefficient, which brings me to:

Create a class that wraps a shared object handle

Calling dlopen() and dlclose() every time you want to execute a function is inefficient, as it then might unload and reload the whole shared object each time. It would be much better to load it once and reuse the handle you got back from dlopen() multiple times. The same actually goes for the function you want to execute: calling dlsym() every time can be slow. You could create a class that wraps the handle returned by dlopen(), and instead of a function that executes a given function in the shared object, just add a function that returns the function pointer.

class SharedObject {
    void *handle;

public:
    SharedObject(const char *filename) {
        handle = dlopen(filename, RTLD_LAZY);
        if (!handle)
            throw std::runtime_error("Could not load shared object");
    }

    ~SharedObject() {
         dlclose(handle);
    }

    template<typename T>
    T* get_function(const char *symbol) {
        return reinterpret_cast<T*>(dlsym(handle, symbol));
    }
};

Then you can use it like so:

int main(int argc, char *argv[]) {
    SharedObject so(argv[1]);
    auto create_class = so.get_function<Base *(int)>("create_class");

    if (!create_class) {
        std::cerr << "Function not found!\n";
        return EXIT_FAILURE;
    }

    for (int i = 0; i < 10; ++i) {
         std::unique_ptr<Base> b{create_class(i)};
         std::cout << "result " << b->do_something() << '\n';
    }
}

Simplify template parameters

Instead of passing the types of the return value and all the parameters to dl_exec(), consider just passing the type of the function you want it to return. That way there is only one template parameter to deal with. I've shown an example of that in the code above.

Avoid C-style casts

Always prefer to use a C++-style cast, as they are typically a bit safer than C-style casts. In this case, you need to use reinterpret_cast<>() to cast the handle you got from dlsym() to the function pointer type you want.

Consider having create_class() return a std::unique_ptr<Base *>

Instead of the caller having to create a std::unique_ptr, consider doing that already inside create_class. The earlier you do this, the less chance of any code forgetting this and causing a potential memory leak.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ In the default constructor section you provide 2 very good links, but it isn't obvious there are 2 links, and more explanation might be nice. \$\endgroup\$
    – pacmaninbw
    Jun 23 at 21:29
  • \$\begingroup\$ thanks a lot. did not know I need to do dlclose(). I thought is OK. this is why I did in a loop. can you elaborate about {} and default? is this for d-tor? Is virtual, so using {} makes whoever check the code, seeing there is something special about it. wrapping in a class and str::unique_ptr - yes I know this, but wanted to see if delete will work. \$\endgroup\$
    – Nick
    Jun 24 at 9:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.