-
Notifications
You must be signed in to change notification settings - Fork 6
03 A Simple Math Library
Let's look at something a bit closer to our ultimate goal to start exploring some more advanced features of the bindings. To do this, we'll switch to binding a simple 3d vector type inspired by Imath
// file: 03_math/include/math.hpp
#include <math.h>
// Define the internal versioned namespace
#define MYMATH_INTERNAL_NAMESPACE Mymath_1_1
// Declare everything in the versioned namespace
namespace MYMATH_INTERNAL_NAMESPACE {
// A simple 3d vector type
struct Vec3 {
float x;
float y;
float z;
// Default 0-initialize
Vec3() : x(0), y(0), z(0) {}
// Initialize all members
Vec3(float x, float y, float z) : x(x), y(y), z(z) {}
// Initialize all members with a single value
Vec3(float v) : x(v), y(v), z(v) {}
// Copy constructor
Vec3(const Vec3& v) : x(v.x), y(v.y), z(v.z) {}
float length() const { return sqrt(x * x + y * y + z * z); }
};
} // namespace MYMATH_INTERNAL_NAMESPACE
// Pull all symbols into public namespace
namespace Mymath {
using namespace MYMATH_INTERNAL_NAMESPACE;
}
The type itself is a straightforward vector with overloaded constructors and a method to get its length. but there's some shenanigans going on with the namespacing. What's happening is that everything's being declared in the namespace Mymath_1_1
then re-exported in the public Mymath
namespace. So users of the code use Mymath::Vec3
but the symbols actually resolve to Mymath_1_1::Vec3
. This namespace versioning allows client code to avoid symbol-resolution hell if multiple versions of the library end up linked into the same program (often through complex dependency trees), and is a common trick in VFX libraries.
This means that a straightforward binding would look something like this, using the CPPMM_RENAME
macro we learnt in the last part to give our constructor overloads sensible names:
#include <math.hpp>
#include <cppmm_bind.hpp>
namespace cppmm_bind {
namespace Mymath_1_1 {
struct Vec3 {
using BoundType = ::Mymath_1_1::Vec3;
Vec3() CPPMM_RENAME(default);
Vec3(float x, float y, float z) CPPMM_RENAME(new);
Vec3(float v) CPPMM_RENAME(from_f32);
Vec3(const ::Mymath_1_1::Vec3& v) CPPMM_RENAME(copy);
float length() const;
};
} // namespace Mymath_1_1
} // namespace cppmm_bind
Remember that to bind the copy constructor we specify the target type not the binding type. So we need to specify the absolute namespace to the library type there. Generating a C library from this gives us:
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
typedef struct Mymath_1_1__Vec3_t_s {
char data[12];
} __attribute__((aligned(4))) Mymath_1_1__Vec3_t;
typedef Mymath_1_1__Vec3_t Mymath_1_1_Vec3_t;
void Mymath_1_1__Vec3_default(
Mymath_1_1_Vec3_t * this_);
#define Mymath_1_1_Vec3_default Mymath_1_1__Vec3_default
void Mymath_1_1__Vec3_new(
Mymath_1_1_Vec3_t * this_
, float x
, float y
, float z);
#define Mymath_1_1_Vec3_new Mymath_1_1__Vec3_new
void Mymath_1_1__Vec3_from_f32(
Mymath_1_1_Vec3_t * this_
, float v);
#define Mymath_1_1_Vec3_from_f32 Mymath_1_1__Vec3_from_f32
void Mymath_1_1__Vec3_copy(
Mymath_1_1_Vec3_t * this_
, Mymath_1_1_Vec3_t const * v);
#define Mymath_1_1_Vec3_copy Mymath_1_1__Vec3_copy
float Mymath_1_1__Vec3_length(
Mymath_1_1_Vec3_t const * this_);
#define Mymath_1_1_Vec3_length Mymath_1_1__Vec3_length
#ifdef __cplusplus
}
#endif
There's a couple of things worth thinking about in here. The first is the struct definition:
typedef struct Mymath_1_1_Vec3_s {
char data[12];
} __attribute__((aligned(4))) Mymath_1_1_Vec3;
Our individual members from the C++ definition aren't represented here, instead it's an opaque bag of bytes, or in cppmm's parlance, an opaquebytes
struct. This is the default way of representing a C++ type. We cannot expose members that depend on C++ features to C, so we hide them but still allow the resulting bag of bytes to be passed around in memory at the user's discretion.
The other options are opaqueptr
and valuetype
. opaqueptr
is mainly used for abstract types or types with no public constructors, that have to be created by the target library and can only be represented by a pointer in client code. valuetype
on the other hand is for types that can be directly represented in C (and Rust via repr(C)
), which sounds like exactly what we want for our Vec3 type. We can tell astgen
that's what we want by adding the appropriate attribute to our binding Vec3 declaration:
struct Vec3 {
using BoundType = ::Mymath_1_1::Vec3;
Vec3() CPPMM_RENAME(default);
Vec3(float x, float y, float z) CPPMM_RENAME(new);
Vec3(float v) CPPMM_RENAME(from_f32);
Vec3(const ::Mymath_1_1::Vec3& v) CPPMM_RENAME(copy);
float length() const;
} CPPMM_VALUETYPE;
and then binding again gives us:
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
typedef struct Mymath_1_1__Vec3_t_s {
float x;
float y;
float z;
} __attribute__((aligned(4))) Mymath_1_1__Vec3_t;
typedef Mymath_1_1__Vec3_t Mymath_1_1_Vec3_t;
void Mymath_1_1__Vec3_default(Mymath_1_1_Vec3_t* this_);
#define Mymath_1_1_Vec3_default Mymath_1_1__Vec3_default
void Mymath_1_1__Vec3_new(Mymath_1_1_Vec3_t* this_, float x, float y, float z);
#define Mymath_1_1_Vec3_new Mymath_1_1__Vec3_new
void Mymath_1_1__Vec3_from_f32(Mymath_1_1_Vec3_t* this_, float v);
#define Mymath_1_1_Vec3_from_f32 Mymath_1_1__Vec3_from_f32
void Mymath_1_1__Vec3_copy(Mymath_1_1_Vec3_t* this_,
Mymath_1_1_Vec3_t const* v);
#define Mymath_1_1_Vec3_copy Mymath_1_1__Vec3_copy
float Mymath_1_1__Vec3_length(Mymath_1_1_Vec3_t const* this_);
#ifdef __cplusplus
}
#endif
Now we can see (and poke at) the individual fields as we'd expect.
The second issue is that we've hardcoded the full versioned namespace into the binding. If we want to use the same binding files for new versions of the target library we'd have to update all the instances of Mymath_1_1
to Mymath_1_2
or whatever, potentially in many places. We can work around this by using MYMATH_INTERNAL_NAMESPACE
instead of Mymath_1_1
and declaring a namespace alias in the cppmm_bind::Mymath
namespace like so:
#include <math.hpp>
#include <cppmm_bind.hpp>
namespace cppmm_bind {
namespace MYMATH_INTERNAL_NAMESPACE {
namespace Mymath = ::MYMATH_INTERNAL_NAMESPACE;
struct Vec3 {
using BoundType = Mymath::Vec3;
Vec3();
Vec3(float x, float y, float z);
Vec3(float v);
Vec3(const Mymath::Vec3& v);
float length() const;
} CPPMM_VALUETYPE;
} // namespace MYMATH_INTERNAL_NAMESPACE
} // namespace cppmm_bind
Which give us:
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
typedef struct Mymath_1_1__Vec3_t_s {
float x;
float y;
float z;
} __attribute__((aligned(4))) Mymath_1_1__Vec3_t;
typedef Mymath_1_1__Vec3_t Mymath_Vec3_t;
void Mymath_1_1__Vec3_Vec3(
Mymath_Vec3_t * this_);
#define Mymath_Vec3_Vec3 Mymath_1_1__Vec3_Vec3
void Mymath_1_1__Vec3_Vec3_1(
Mymath_Vec3_t * this_
, float x
, float y
, float z);
#define Mymath_Vec3_Vec3_1 Mymath_1_1__Vec3_Vec3_1
void Mymath_1_1__Vec3_Vec3_2(
Mymath_Vec3_t * this_
, float v);
#define Mymath_Vec3_Vec3_2 Mymath_1_1__Vec3_Vec3_2
void Mymath_1_1__Vec3_Vec3_3(
Mymath_Vec3_t * this_
, Mymath_Vec3_t const * v);
#define Mymath_Vec3_Vec3_3 Mymath_1_1__Vec3_Vec3_3
float Mymath_1_1__Vec3_length(
Mymath_Vec3_t const * this_);
#define Mymath_Vec3_length Mymath_1_1__Vec3_length
#ifdef __cplusplus
}
#endif
If you were wondering what those typedef
s and #define
s were doing there before, now it should be obvious - they give us a nice, version-agnostic way of calling the API. It's C's version of namespaces, essentially.
In Rust all the symbols are just re-exported from their submodules with into the crate root with their nice names:
pub mod c_math;
pub use c_math::Mymath_1_1__Vec3_t as Mymath_Vec3_t;
pub use c_math::Mymath_1_1__Vec3_Vec3 as Mymath_Vec3_Vec3;
pub use c_math::Mymath_1_1__Vec3_Vec3_1 as Mymath_Vec3_Vec3_1;
pub use c_math::Mymath_1_1__Vec3_Vec3_2 as Mymath_Vec3_Vec3_2;
pub use c_math::Mymath_1_1__Vec3_Vec3_3 as Mymath_Vec3_Vec3_3;
pub use c_math::Mymath_1_1__Vec3_length as Mymath_Vec3_length;
In this way, users of the generated C and Rust APIs doesn't have to update when changing library version, while still being protected from potential symbol clashing.