1

I am writing a C++ wrapper around a C library. Here is an example of my strategy.

    // header file
    class LibrdfUri { // wrapper around librdf.h librdf_uri* 

        /*
         * If the deleter of std::unique_ptr is an empty
         * class then it can do some optimizations and
         * not actually store the deleter object.
         * Otherwise it has to accommodate extra space for
         * the deleter, which is unnecessary
         *  https://stackoverflow.com/questions/61969200/what-is-the-purpose-of-wrapping-this-private-deleter-function-in-a-struct/61969274#61969274
         */
        struct deleter {
            // turns deleter into a functor. For passing on to unique_ptr
            void operator()(librdf_uri *ptr);
        };
        // automate management of librdf_uri* lifetime
        std::unique_ptr<librdf_uri, deleter> librdf_uri_;

    public:
        LibrdfUri() = default;

        explicit LibrdfUri(const std::string& uri); // construct from string

        librdf_uri *get(); // returns the underlying raw pointer

    };
    // implementation
    void LibrdfUri::deleter::operator()(librdf_uri *ptr) {
        librdf_free_uri(ptr); // this is the C library function for destruction of librdf_uri
    }

    LibrdfUri::LibrdfUri(const std::string &uri) {
        // create pointer to underlying C library 'object'
        librdf_uri_ = std::unique_ptr<librdf_uri, deleter>(
                librdf_new_uri(World::getWorld(), (const unsigned char *) uri.c_str()) // World::getWorld is static. Returns a pointer required by librdf_new_uri
        );
    }

    librdf_uri *LibrdfUri::get() {
        return librdf_uri_.get();
    }

// and is used like so:
LibrdfUri uri("http://uri.com");
librdf_uri* curi = uri.get(); // when needed

This works for the single type librdf_uri* which is a part of the underlying library however I have lots of these. My question is double barrelled. The first part concerns the best general strategy for generalizing this wrapper to other classes while the second is concerns the implementation of that strategy.

Regarding the first part, here are my thoughts: 1. I could implement each class manually like I've done here. This is probably the simplest and least elegant. Yet it still might be my best option. However there is a small amount of code duplication involved, since each CWrapper I write will essentially have the same structure. Not to mention if I need to change something then I'll have to do each class individually. 2. Use an base class (abstract?) 3. Use a template

The second part of my question is basically: if I implement either option 2 or 3 (which I think might even be just a single option) how would I do it?

Here is a (vastly broken) version of what I'm thinking:

template<class LibrdfType>
class CWrapper {

    struct deleter { ; //?
        void operator()(LibrdfType *ptr) {
            // ??
        };
    }

    std::unique_ptr<LibrdfType, deleter> ptr;

public:
    CWrapper() = default;

    LibrdfType *get() {
        ptr.get();
    };

};

Then, LibrdfUri and any other C class I need to wrap, would just subclass CWrapper

CiaranWelsh
  • 7,014
  • 10
  • 53
  • 106
  • Your thin wrapper is just the pointer-to-implementation pattern. It won't be very amenable to being turned into a template, unless you're not going to add all the thunk methods to call into the underlying C library methods. But then it will be awkward to use, unless you add free function thunks to the underlying methods. – Eljay May 23 '20 at 13:12
  • So you think option 1 is my best bet. That's good to know. – CiaranWelsh May 23 '20 at 13:18
  • 1
    Your "general wrapper" can be reduced to `template using SafeLibPtr = std::unique_ptr` and then you would use it like `auto deleter = [](auto ptr) { lib_free_function(ptr); }; SafeLibPtr lib_ptr(lib_function_that_returns_pointer());` – NathanOliver May 23 '20 at 13:21
  • Do you have to wrap the entire library's public API, or just what you need? Are you trying to completely isolate the library from the rest of the program (an abstraction "firewall") or does the rest of the program use the `get` function and the C library's types and functions directly (so no need for thunks)? – Eljay May 23 '20 at 13:25
  • @NathanOliver very interesting, but as a new C++ developer (python background) I don't fully understand your suggestion. I'd be greatful if you would turn it into an answer with a bit of explaination. – CiaranWelsh May 23 '20 at 13:26
  • @Eljay Only what I need, but I need a large chunk of the (`Redland`) library. The main goal of the wrapper is to use smart pointers for managing the memory. (and sorry, I'm not sure what you mean by 'thunk') – CiaranWelsh May 23 '20 at 13:31
  • A thunk is a small function used to basically call another function and pass along the parameters as arguments to the actual implementation function. It's a glue function. The approach you're taking will help with object ownership and life-cycle management and allow for RAII (so the destructor can do the clean up), which I like. I've wrapped C libraries before, it starts out a bother, but gets easier as you go. – Eljay May 23 '20 at 13:33
  • I think you've summed up what I'm doing better than I could have. So the answer is yes, there will be thunks. I'll be including methods in the C++ library which are basically calls to an OOP styled C library. But I won't do the whole library, only what I need for another layer on top. – CiaranWelsh May 23 '20 at 13:36

1 Answers1

1

This is a better deleter:

template<auto f>
using deleter=std::integral_constant< std::decay_t<decltype(f)>, f >;

use:

deleter<librdf_free_uri>

is a stateless deleter that calls librdf_free_uri.

But we don't need that I think. Here is what I might do:

There are 3 pieces of information you need.

  • How to construct
  • How to destroy
  • What type to store

One way is to define ADL baser helpers with famous names that you override to delete/construct.

template<class T>struct tag_t{};
template<class T>constexpr tag_t<T> tag{};

template<class T>
void delete_wrapptr(T*)=delete;
struct cleanup_wrapptr{
  template<class T>
  void operator()(T* t)const{ delete_wrapptr(t); }
};
template<class T>
using wrapptr=std::unique_ptr<T, cleanup_wrapptr>;
template<class T>
wrapptr<T> make_wrapptr( tag_t<T>, ... )=delete;

now you just have to write overloads for make and delete.

void delete_wrapptr(librdf_uri* ptr){
    librdf_free_uri(ptr); // this is the C library function for destruction of librdf_uri
}

librdr_uri* make_wrapptr(tag_t<librdf_uri>, const std::string &uri) {
    return librdf_new_uri(World::getWorld(), (const unsigned char *) uri.c_str()); // World::getWorld is static. Returns a pointer required by librdf_new_uri
 }

and you can;

wrapptr<librdf_uri> ptr = make_wrapptr(tag<librdf_uri>, uri);

the implementation becomes just overriding those two functions.

make_wrapptr and delete_wrapptr overloads you write need to be visible at creating point, and in the namespace of T, tag_t or cleanup_wrapptr. The implementations can be hidden in a cpp file, but the declaration of the overload cannot.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524