I have a proposal to integrate C++ and proton C memory management, I
need a sanity check.

Attached is an executable C++ sketch and test (pn_ptr.cpp) and a script
(test.sh) that runs the combinations of g++/clang++ and c++11/c++03, as
well as some tests to verify that we get compile errors to prevent
mistakes.

The idea is that every pn_foo_t type has a corresponding C++ foo class
with member functions to make it easy to call pn_foo_* functions in C++
(converting std::string/char* etc.)

The first trick: the foo class is empty and never instantiated. A foo*
actually points to the same memory location as the pn_foo_t*. You can
reinterpret_cast between the two, and deleting the foo* will actually
call pn_foo_free(pn_foo_t*).

The next trick: proton::event accessor functions return a pn_ptr<foo>,
which is an internal class that users cannot instantiate. What they can
do is convert it by assignment or construction to any of: foo*,
std::auto_ptr<foo>, std::unique_ptr<foo> or std::shared_ptr<foo>. In
the shared_ptr case the conversion automatically does a pn_incref().

The upshot of this is that you can use plain foo* or any of the
std::smart pointers to point to a foo and it Just Works. If you don't
use shared_ptr you need to understand the proton C API lifecycle rules,
but with shared_ptr it is all fully automatic.

Moreover if you don't use shared_ptr there is almost no overhead over
using pn_foo_t* directly in the C API, as the compiler should optimise
away all the inline template magic.

This works with c++11 (everything works) or c++03 (just foo* and
auto_ptr). It will be trivial to add support for boost::shared_ptr so
nice memory management will work with c++03 and boost.
/*
I have a proposal to integrate C++ and proton C memory management, I need a sanity check.

Attached is an executable C++ sketch and test (pn_ptr.cpp) and a script (test.sh) that runs the combinations of g++/clang++ and c++11/c++03, as well as some tests to verify that we get compile errors to prevent mistakes.

The idea is that every pn_foo_t type has a corresponding C++ foo class with member functions to make it easy to call pn_foo_* functions in C++ (converting std::string/char* etc.)

The first trick: the foo class is empty and never instantiated. A foo* actually points to the same memory location as the pn_foo_t*. You can reinterpret_cast between the two, and deleting the foo* will actually call pn_foo_free(pn_foo_t*).

The next trick: proton::event accessor functions return a pn_ptr<foo>, which is an internal class that users cannot instantiate. What they can do is convert it by assignment or construction to any of: foo*, std::auto_ptr<foo>, std::unique_ptr<foo> or std::shared_ptr<foo>. In the shared_ptr case the conversion automatically does a pn_incref().

The upshot of this is that you can use plain foo* or any of the std::smart pointers to point to a foo and it Just Works. If you don't use shared_ptr you need to understand the proton C API lifecycle rules, but with shared_ptr it is all fully automatic.

Moreover if you don't use shared_ptr there is almost no overhead over using pn_foo_t* directly in the C API, as the compiler should optimise away all the inline template magic.

This works with c++11 (everything works) or c++03 (just foo* and auto_ptr). It will be trivial to add support for boost::shared_ptr so nice memory management will work with c++03 and boost.
*/

#if (not defined USE_CPP11 && (defined(__cplusplus) && __cplusplus >= 201100) || (defined(_MSC_FULL_VER) && _MSC_FULL_VER >= 150030729))
#define USE_CPP11 1
#endif

#include <iostream>
#include <memory>
using namespace std;

// ==== Simulate private proton .h files.

// Fake pn object.
struct pn_foo_t {
    string s_;    // Dummy state
    int refs_; // Simulate the external refcount


    // The "external" refcount is the number of c++ shared_ptr *groups* referencing the object.
    // This is in addition to any internal library refs - for this test we don't care about those
    // so we start refs_ at 0, meaning no external references.
    //
    // It is OK to have multiple shared_ptr groups pointing at the same pn_foo_t
    // since the end of a shared_ptr group only *decrefs* the pn_foo, it doesn't
    // delete it.
    //
    pn_foo_t(string s) : s_(s), refs_(0) { cout << s_ << " created" << endl; }
    ~pn_foo_t() { cout << s_ << " freed" << endl; }
};

// ==== Simulate public proton C API and proton internal refcounts.

pn_foo_t* pn_foo_create(string s) { return new pn_foo_t(s); } // 0 external refs
string pn_foo_get_s(pn_foo_t* p) { return p->s_; }
void pn_foo_set_s(pn_foo_t* p, string s) { p->s_ = s; }
void pn_foo_free(pn_foo_t* p) { delete p; }


// Simulate pn_object_incref/decref
void pn_incref(void* v) {
    pn_foo_t* p = reinterpret_cast<pn_foo_t*>(v);
    ++p->refs_;
}

void pn_decref(void* v) {
    pn_foo_t* p = reinterpret_cast<pn_foo_t*>(v);
    --p->refs_;
    if (p->refs_ == 0) delete p;
}

// ==== Simulate C++ binding

/**
   For proton object pn_foo_t functions in the C++ binding return
   pn_ptr<foo>. Users do not use pn_ptr<foo> directly, instead they assign or
   convert the result to `foo*` or `shared_ptr<foo>` to get raw or safe access
   respectively

   This does two things:
   - completely automates refcounts if the user always uses shared_ptr<foo>
   - imposes 0 overhead if the user uses foo* and leave the user responsible.

   The brave and foolish can mix the two, and can reinterpret_cast between foo*
   and pn_foo_t* to mix use of C++ and C APIs. That is NOT recommended. I would
   recommend one of the following 2 options:

   1. Most users: Just use shared_ptr<foo>. Don't mess with the C API. Sleep at
   night.

   2. Insane performance freaks and wild-eyed mixed C/C++ developers: use foo*
   and treat the C++ API as a C++ friendly version of the C API. In this mode
   (like a C programmer) you should probably never use proton refcounts: just
   learn the lifecycle rules and call pn_*_free() at the right times. See the C
   API for the rules.

   NOTE: endpoints (connection, session, link) have both _close and _free.
   shared_ptr automates calling _free and freeing memory. If you want a clean
   AMQP close you need to call close also. There will be a close_ptr<foo> to
   automate this, not illustrated below but you can imagine it. The C++
   container will close things automatically for you if you don't close them
   yourself.
*/
class foo;

///@internal traits to deduce cpp type from pn_type
template <class P> struct cpp_type{};
template <> struct cpp_type<pn_foo_t> { typedef foo type; };
template <> struct cpp_type<const pn_foo_t> { typedef const foo type; };
//... etc.

template <class T> class pn_ptr;
template <class P> pn_ptr<typename cpp_type<P>::type> make_pn_ptr(P* p);

/** pn_ptr is returned by functions in the C++ API.

    You should NOT use this type in your code. Most users should assign or
    convert pn_ptr<foo> to a shared_ptr<foo> for automatic memory management.

    If you understand the proton C memory management rules and need to avoid
    shared_ptr, you can convert pn_ptr<foo> to foo*. In that case you are
    responsible for deleting the pointer at the right time. You can use delete
    std::unique_ptr, std::auto_ptr etc. to do this.

    If you understand the C API in detail you can reinterpret_cast between foo*
    to pn_foo_t* and mix the C and C++ APIs but this is not recommended.
*/
template <class T> class pn_ptr {
  public:
    operator T*() { return reinterpret_cast<T*>(ptr_); }
    T* operator->() { return ptr_; }

#if USE_CPP11
    operator shared_ptr<T>() {
        pn_incref(ptr_);        // Add a reference for the shared_ptr family
        return shared_ptr<T>(reinterpret_cast<T*>(ptr_), pn_decref);
    }

    operator unique_ptr<T>() { return unique_ptr<T>(reinterpret_cast<T*>(ptr_)); }
#endif

  private:
    typedef typename T::pn_type P;
    pn_ptr(P* ptr) : ptr_(ptr) {}
    pn_ptr(const pn_ptr& x) : ptr_(x.ptr_) {}
    P* ptr_;

  friend pn_ptr<T> make_pn_ptr<>(P* p);
  friend class foo;
  friend class event;
};

template <class P>
pn_ptr<typename cpp_type<P>::type> make_pn_ptr(P* p) { return pn_ptr<typename cpp_type<P>::type>(p); }

/// The public foo wrapper class.
class foo {
  public:
    typedef pn_foo_t pn_type;

    string s() const { return pn_foo_get_s(reinterpret_cast<pn_foo_t*>(const_cast<foo*>(this))); }
    void s(string s_) { pn_foo_set_s(reinterpret_cast<pn_foo_t*>(this), s_); }

    // Public C++ API for user to create a foo.
    static pn_ptr<foo> create(string s) { return make_pn_ptr(pn_foo_create(s)); }

    void operator delete(void* p) { pn_foo_free(reinterpret_cast<pn_foo_t*>(p)); }
};

// Simulated event handler.

class event {
  public:
    event(string name) : name_(name), pn_foo_(pn_foo_create(name)) {}
    pn_ptr<class foo> foo() { return make_pn_ptr(pn_foo_); }
  private:
    string name_;
    pn_foo_t* pn_foo_;
};

class handler {
  public:
    virtual void on_foo(event&) = 0;
};

void run(string name, handler& h) {
    event e(name);
    h.on_foo(e);
}

// ==== User code

struct raw_handler : public handler {
    foo* p;
    void on_foo(event& e) { p = e.foo(); }
};

#if USE_CPP11
struct shared_handler : public handler {
    shared_ptr<foo> p;
    void on_foo(event& e) { p = e.foo(); }
};

struct unique_handler : public handler {
    unique_ptr<foo> p;
    void on_foo(event& e) { p = e.foo(); }
};
#endif

int main(int, char**) {
    foo* p = foo::create("raw");     // raw user create
    delete p;                        // manual delete

    raw_handler h;
    run("raw-handler", h);                            // raw internal create.
    cout << "after raw-handler: " << h.p->s() << endl; // still exists
    delete h.p;                                       // manual delete

    foo* foo_leak = foo::create("leak"); // raw no delete, should leak.

#if !USE_CPP11
    {
        auto_ptr<foo> p(foo::create("auto_ptr")); // user create
    }
#endif

#if USE_CPP11
    {
        shared_ptr<foo> p = foo::create("shared"); // user create
    }
    {
        shared_handler h;
        run("shared-handler", h);                            // shared internal create.
        cout << "after shared-handler: " << h.p->s() << endl; // still exists
    }
    {
        unique_handler h;
        run("unique-handler", h);                            // unique internal create.
        cout << "after unique-handler: " << h.p->s() << endl; // still exists
    }
    {
        unique_ptr<foo> p = foo::create("unique_ptr"); // user create
    }
#ifdef BROKEN
    // Compile error, you must specify a type.
    auto p = foo::create("auto");
#endif
#endif
}

Attachment: test.sh
Description: application/shellscript

Reply via email to