2 minute read

Objects in the Lely core libraries are all defined and implemented according to the pattern shown here. This pattern is designed to allow a header-only C++ interface for C objects which is as close as possible to a native C++ interface.

Example of a public C header (lely/lib/obj.h):

#ifndef LELY_LIB_OBJ_H_
#define LELY_LIB_OBJ_H_

// A convenience typedef for an opaque object type.
typedef struct obj obj_t;

#ifdef __cplusplus
extern "C" {
#endif

// In C++, memory allocation and object initialization are separate steps. The
// C implementation of these steps is exposed so they can be used separately by
// the C++ interface, if necessary. Users of the C interface SHOULD NOT invoke
// these functions directly, but use obj_create() and obj_destroy() instead.
void *obj_alloc(void);
void obj_free(void *ptr);
obj_t *obj_init(obj_t *obj, Args... args);
void obj_fini(obj_t *obj);

// Creates a new object instance. Internally, this function invokes obj_alloc()
// followed by obj_init().
obj_t *obj_create(Args... args);

// Destroys an object instance. Internally, this function invokes obj_fini()
// followed by obj_free().
void obj_destroy(obj_t *obj);

// An example of an object method. The first parameter is always a pointer to
// the object (cf. "this" in C++).
int obj_method(obj_t *obj, Args... args);

#ifdef __cplusplus
}
#endif

#endif // !LELY_LIB_OBJ_H_

Example of the C implementation (obj.c):

#include <lely/lib/obj.h>
#include <lely/util/errnum.h>

#include <assert.h>
#include <stdlib.h>

struct obj {
	...
};

void *
obj_alloc(void)
{
	void *ptr = malloc(sizeof(struct obj));
	if (!ptr)
		// On POSIX platforms this is a no-op (errno = errno), but on
		// Windows this converts errno into a system error code and
		// invokes SetLastError().
		set_errc(errno2c(errno));
	return ptr;
}

void
obj_free(void *ptr)
{
	free(ptr);
}

obj_t *
obj_init(obj_t *obj, Args... args)
{
	assert(obj);

	// Initialize all object members. If an error occurs, clean up and
	// return NULL.
	...

	return obj;
}

void
obj_fini(obj_t *obj)
{
	assert(obj);

	// Finalize all object members.
	...
}

obj_t *
obj_create(Args... args)
{
	int errc = 0;

	obj_t *obj = obj_alloc();
	if (!obj) {
		errc = get_errc();
		goto error_alloc;
	}

	obj_t *tmp = obj_init(obj, args...);
	if (!tmp) {
		errc = get_errc();
		goto error_init;
	}
	obj = tmp;

	return obj;

error_init:
	obj_free(obj);
error_alloc:
	set_errc(errc);
	return NULL;
}

void
obj_destroy(obj_t *obj)
{
	// obj_destroy() and obj_free() are the only methods which can be called
	// with `obj == NULL`.
	if (obj) {
		obj_fini(obj);
		obj_free(obj);
	}
}

int
obj_method(obj_t *obj, Args... args)
{
	assert(obj);

	// Do something with the object.
	...
}

Example of a public C++ header (lely/lib/obj.hpp):

#ifndef LELY_LIB_OBJ_HPP_
#define LELY_LIB_OBJ_HPP_

#include <lely/lib/obj.h>
#include <lely/util/error.hpp>

// Needed for std::swap().
#include <utility>

namespace lely {
namespace lib {

// A non-owning reference to an object. This class is basically a wrapper around
// obj_t*.
class ObjectBase {
 public:
  explicit ObjectBase(obj_t* obj_) noexcept : obj(obj_) {}

  operator obj_t*() const noexcept { return obj; }

  int
  method(Args... args) noexcept {
    return obj_method(*this, args...);
  }

  ...

 protected:
  obj_t* obj{nullptr};
};

// An owning reference to an object, a bit like std::unique_ptr<obj_t>. When it
// goes out of scope, the instance is destroyed. Ownership can be transferred
// with std::move().
class Object : public ObjectBase {
 public:
  Object(Args... args) : ObjectBase(obj_create(args...)) {
    if (!obj) util::throw_errc("Object");
  }

  Object(const Object&) = delete;

  Object(Object&& other) noexcept : ObjectBase(other.obj) {
    other.obj = nullptr;
  }

  Object& operator=(const Object&) = delete;

  Object&
  operator=(Object&& other) noexcept {
    using ::std::swap;
    swap(obj, other.obj);
    return *this;
  }

  ~Object() { obj_destroy(*this); }

  ...
};

}  // namespace lib
}  // namespace lely

#endif  // !LELY_LIB_OBJ_HPP_

Updated: