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
}
test.sh
Description: application/shellscript
