Skip to content

CAndCPlusPlusAPI

Martin Maly edited this page May 8, 2015 · 5 revisions

upb's main interfaces are defined in .h files (like upb/def.h). These header files are coded in such a way that they provide first-class APIs in both C and C++ that are idiomatic and impose no overhead.

This scheme is implemented with macros. An example of these macros is given below:

// This defines a type called upb::Foo in C++ or upb_foo in C.  In both cases
// there is a typedef for upb_foo, which is important since this is how the
// C functions are defined (which are exposed to both C and C++).

#ifdef __cplusplus
namespace upb { class Foo; }
#endif

UPB_DECLARE_TYPE(upb::Foo, upb_foo);

// Here is the actual definition of the class/struct.  In C++ we get a class
// called upb::Foo and in C we get a struct called "struct upb_foo", but both
// have the same members and the C++ version is "standard-layout" according
// to C++11.  This means that the two should be compatible.
//
// In addition to being completely accessible from C, it also provides C++
// niceities like methods (instead of bare functions).  We also get
// encapsulation in C++, even though this is impossible to provide in C.  We
// provide all method documentation in the C++ class, since the class/method
// syntax is nicer to read than the bare functions of C.

UPB_DEFINE_CLASS(upb::Foo,
 public:
  // Method documentation for DoBar().
  void DoBar(int32_t x);

  // Method documentation for IsSpicy().
  bool IsSpicy() const;

 private:
,
UPB_DEFINE_STRUCT(upb_foo,
  // In C++, these members will be part of the C++ class, but private.
  // In C, these will be members of the struct.
  int32_t private_member;
));

UPB_BEGIN_EXPORT_C

// Next follows the C API, which is how the functionality is actually
// implemented.  We omit documentation here because everything was documented
// in the C++ class, and it's easy to match the functions 1:1 to the C++
// methods.
void upb_foo_dobar(upb_foo *f, int32_t x);
bool upb_foo_isspicy(const upb_foo *f);

// Finally we include inline definitions of the C++ methods, which are nothing
// but this wrappers around the C functions.  Since these are inline, the C++
// API imposes no overhead.

UPB_END_EXPORT_C

#ifdef __cplusplus
namespace upb {
inline void Foo::DoBar(int32_t x) { upb_foo_dobar(this, x); }
inline bool Foo::IsSpicy() const { return upb_foo_isspicy(this); }
}
#endif

Now here is some code that uses upb::Foo:

// C++ API
void use_foo() {
  upb::Foo foo;
  if (foo.IsSpicy()) {
    foo.DoBar(5);
  }
}

// C API
void use_foo_c() {
  upb_foo foo;
  upb_foo_init(&foo);
  if (upb_foo_isspicy(&foo)) {
    upb_foo_dobar(&foo, 5)
  }
}

This scheme works pretty nicely. The macros do abstract away the actual class definition, so it takes a bit of getting used to, but the scheme gives nice, zero-overhead APIs to both C and C++ without having to duplicate the API documentation.

We can also support single-inheritance in a way that is compatible with, and accessible from, C. We can't use C++ inheritance directly, because C++ makes no guarantees about the layout of members once there is inheritance (ie. classes with inheritance are no longer "standard layout" if both base and derived have members). So instead the macros implement inheritance manually:

// This defines a type called upb::Bar in C++ or upb_bar in C, which derives
// from upb::Foo.

#include "foo.h"

#ifdef __cplusplus
namespace upb { class Bar; }
#endif

UPB_DECLARE_TYPE(upb::Bar, upb_bar);

UPB_DEFINE_CLASS(upb::Bar, upb::Foo,
 public:
  // Methods from upb::Foo.  We have to duplicate these explicitly.
  void DoBar(int32_t x);
  bool IsSpicy() const;

  // New methods for upb::Bar.
  void Run();
,
UPB_DEFINE_STRUCT(upb_bar, upb_foo,
  // For C and C++, a member "upb_foo base" will automatically be inserted.
  int32_t bar_data;
)};

UPB_BEGIN_EXPORT_C

// C API for the derived class, starting with the functionality from upb_foo.
void upb_bar_dobar(upb_bar *b, int32_t x);
bool upb_bar_isspicy(const upb_bar *b);
void upb_bar_run(upb_bar *b);

UPB_END_EXPORT_C

// Finally we include inline definitions of the C++ methods, which are nothing
// but this wrappers around the C functions.  Since these are inline, the C++
// API imposes no overhead.

#ifdef __cplusplus
namespace upb {
inline void Bar::DoBar(int32_t x) { upb_bar_dobar(this, x); }
inline bool Bar::IsSpicy() const { return upb_bar_isspicy(this); }
inline void Bar::Run() { return upb_bar_run(this); }
}
#endif

Now, in addition to being able to use upb::Bar, you can upcast it to a Foo using upb::upcast():

void use_foo() {
  vector<upb::Foo*> foos;
  upb::Bar bar;
  foos.push_back(upb::upcast(&bar));
}

The biggest bummer is that there isn't any good way to use C++ inheritance even for types which are trying to express inheritance in C. C++ just doesn't give any guarantees about how it will arrange data members in base classes, so we can't use C++ inheritance while interoperating with C layouts. The biggest effect of this is that we can't get C++'s nice implicit upcasts; all upcasts need to explicitly use upb::upcast, which is a minor pain.