The core principle driving the design of the binding generator is to have a tool that consumes a machine readable description of the JitBuilder API and produces appropriate bindings for a given language.
The basic principle followed is to have implemented functionality on one side and the language-specific interface or client API on the other side.
On the implementation side, an object contains all the functionality. On the client API side, a one-to-one corresponding object forwards API calls to the implementation class.
A C client API is used as base for bindings to other languages. Since the JitBuilder
implementation relies heavily on virtual functions and overriding, a special
mechanism is needed to allow overriding to be done in C. Because a user can
override the implementation of certain JitBuilder services (e.g. IlBuilder::buildIL()
and VirtualMachineState::Commit()
), the implementation side cannot rely on calling
those services on its side to get the correct behaviour. Instead it must call
the service on the client side and ensure user overrides are invoked if present.
The C overriding mechanism makes use of function pointers to set callbacks. When a client-side object is constructor or initialized, it sets a callback for each virtual function on the implementation side that can be overriden by a user.
For the mechanism to work, every function that can have a user overridden must be
paired with a second, non-virtual function that handles dispatch. While the virtual
function just contains the function's implementation, the non-virtual dispatch function
invokes the callback corresponding to the function that is set by the client-side
object. By convention, the name of the virtual function is capitalized
(e.g. virtual Foo()
) since it provides the functionality of the client API.
The name of the dispatch function is not capitalized (e.g. foo()
) as it's only
intended for internal use in the implementation.
On the implementation side, instead of calling a virtual function Foo()
, the
corresponding dispatch function foo()
is called to ensure that, if set, the
client-side override of Foo()
is executed. As a result, the callback should,
by default, just invoke the default implementation provided on the implementation
side, assuming one exists.
Client API : Implementation
:
: +---------------+
: | Base |
+---------------+ : +---------------+
+ FooCallback() <---------+ foo() |
+-----------+---+ : | |
`-------------> virtual Foo() |
: +---------------+
:
To override the default implementation, a different callback with the custom implementation should be set.
In client APIs built on top of the C API, the callback may instead forward its
invocation to a function (or method) implemented in the target language, forwarding
the responsibility of calling the default implementation. For example, in the C++
client API, a call to the callback would invoke a client-side virtual function
Foo()
that would take care calling back into the implementation side when no
user overrides are exist.
Client API : Implementation
:
+---------------+ : +---------------+
| Base | : | Base |
+---------------+ +---------------+ : +---------------+
| virtual Foo() <---+ FooCallback() <-----------+ foo() |
+------------+--+ +---------------+ : | |
| : ,----> virtual Foo() |
`-----------------------------' +---------------+
:
If an implementation-side derived class overrides Foo()
, then a matching
client-side derived class also defines an override that will just invoke
the implementation-side override. The client-side class also sets a different
callback that will invoke the a client-side override of Foo()
.
Client API : Implementation
:
+---------------+ : +---------------+
| Base | : | Base |
+---------------+ +---------------+ : +---------------+
| virtual Foo() | | +---------------+ : ,----+ foo() |
+---.-----------+ +-| FooCallback() <----' | |
/_\ +--+------------+ : | virtual Foo() |
| | : +--------.------+
| | : /_\
| | : |
+---+-----------+ | : +--------+------+
| Derived | | : | Derived |
+---------------+ | : +---------------+
| virtual Foo() <--------' : ,----> virtual Foo() |
+------------+--+ : | +---------------+
`-----------------------------'
:
If a user overrides Foo()
, then virtual dispatch will ensure that the
user's override is invoked when foo()
is called on the implementation side.
Client API : Implementation
:
+---------------+ : +---------------+
| Base | : | Base |
+---------------+ +---------------+ : +---------------+
| virtual Foo() | | +---------------+ : ,----+ foo() |
+---.-----------+ +-| FooCallback() <----' | |
/_\ +--+------------+ : | virtual Foo() |
| | : +--------.------+
| | : /_\
| | : |
+---+-----------+ | : +--------+------+
| Derived | | : | Derived |
+---------------+ | : +---------------+
| virtual Foo() |<-------' : | virtual Foo() |
+---.-----------+ \ : +---------------+
/_\ | :
| | [virtual dispatch] :
+---+-----------+ | :
| UserDerived | | :
+---------------+ | :
| virtual Foo() <--' :
+---------------+ :
:
A minimal implementation (base-)class declaration will look like:
namespace TR {
extern "C" {
typedef void * (*ImplGetter)(void *client);
typedef void * (*ClientAllocator)(void *implObject);
}
class Base {
public:
vitual void * client();
void setClient(void * c);
static void setGetImpl(ImplGetter getter);
static void setClientAllocator(ClientAllocator allocator);
protected:
void * _client;
static ImplGetter * _getImpl;
private:
static ClientAllocator * _clientAllocator;
};
}
Let's break it down.
The _client
field is an opaque pointer to the client-side object corresponding
to the current object. It is the only means by which an implementation can
communicated with a client object. The member function client()
is a (special)
getter for the _client
. It must be virtual to allow subclasses to override it,
as will be shown later. As it's name implies setClient()
is just an old
fashion setter for the field.
The static field _getImpl
is a pointer to a function that, when called with an
instance of a client object, will return the corresponding implementation
object. ImplGetter
is a typedef
for the corresponding function pointer type.
It is the responsibility of client APIs to set this field by a call to the
static member function setGetImpl()
.
The static field _clientAllocator
is a pointer to a function that returns a
pointer to a newly allocated client object. When called, the corresponding
implementation object is passed as argument. It's type, ClientAllocator
is
just a typedef
for the function pointer type. Once again, it is the client
API's responsibility to set this field with a call the static member function
setClientAllocator()
.
These fields and member functions are the bare minimum all implementation-side classes must have in order to fully support code produced by the binding generator.
Regular, non-virtual functions are implemented as normal member functions. When a call is made on a client-side object, it will simply be forwarded to the corresponding implementation-side function (more on this later).
// declaration
namespace TR {
class Base {
public:
// ...
void Foo();
// ...
};
}
// implementation
void TR::Base::Foo() {
// implementation
}
Like regular class functions, class fields are also straight forward. The only requirement is that fields must be public in order to be visible to the client.
However, fields present a risk because they must never be modified by either the client API or a consuming project. Using private fields with public getters and setters should be preferred.
This section needs work. There are clearly some weaknesses in the design that will have to be resolved. Hopefully, these will be mostly safe changes. There should still be a description of the short-commings and ways to improve.
Virtual functions, normally used to implement callbacks, require a bit more consideration. First, the function implementation itself looks the same as normal virtual functions:
// declaration
namespace TR {
class Base {
public:
// ...
virtual void Bar(); // contains implementation
// ...
};
}
// implementation
void TR::Base::Bar() {
// implementation
}
Next, there needs to be a way of allow both classes on the client-side and the implementation-side to override the function. Let's consider the first scenario. In C, a virtual function can be simply thought of as a function pointer. When the function is "overriden", the function pointer is simply assigned to a new function. So, we can treat a client-side override as assigning some function pointer that we keep internally. A setter function is also required to allow the clients to set these overrides.
// declaration
extern "C" {
// ...
typedef void * (*BarCallback)();
// ...
}
namespace TR {
class Base {
public:
// ...
virtual void Bar(); // contains implementation
void setClientCallback_Bar(void * callback) {
_clientCallbackBar = (BarCallback *) callback;
}
// ...
protected:
BarCallback * _clientCallbackBar; // pointer to client override
};
}
// implementation
void TR::Base::Bar() {
// implementation
}
Finally, a special function is used to decide whether to call the client side override or an implementation override. By convention such a function is given the same name as the implementation function, but with the first letter lower case. In addition, this is the function that must be called from other parts of the implementation, not the virtual implementation function.
// declaration
extern "C" {
// ...
typedef void * (*BarCallback)();
// ...
}
namespace TR {
class Base {
public:
// ...
virtual void Bar(); // contains implementation
void bar() {
// call the callback, if it is set
if (_clientCallbackBar != NULL)
_clientCallbackBar(client());
}
void setClientCallback_Bar(void * callback) {
_clientCallbackBar = (BarCallback *) callback;
}
// ...
protected:
BarCallback * _clientCallbackBar; // pointer to client override
};
}
// implementation
void TR::Base::Bar() {
// implementation
}
This section describes how the client API is implemented, not how to use it.
For every class on the implementation side, there is a corresponding client API class.
namespace OMR {
namespace JitBuilder {
class Base {
public:
void Foo();
virtual void Bar();
void * _impl;
};
}
}
For every member function of the implementation class, there is also a corresponding client API function.
Fields are mapped in a similar fashion.
There is an addition _impl
field, which is an opaque pointer to the
corresponding implementation object. This pointer must always point to the base
type of the object.
In all cases, the implementation of client-side functions simply forward the call to the implementation side.
void OMR::JitBuilder::Base::Foo() {
static_cast<TR::Base *>(_impl)->Foo();
}
void OMR::JitBuilder::Base::Bar() {
static_cast<TR::Base *>(_impl)->Bar();
}
The _impl
field must be cast to the class type corresponding to the current
client API class. Because _impl
always points to the base type of an object,
casting directly to the current type would result in undefined behaviour, if the
current type is not the base type. So, two casts must be done. The first cast
goes from void *
to the base type and the second goes from the base type to
the derived type.
void OMR::JitBuilder::Derived::Bar() {
// double cast to avoid undefined behaviour
static_cast<TR::Derived *>(static_cast<TR::Base *>(_impl))->Bar();
}
Returning values from a function is the first instance of a complication in the client API. Two cases need to be considered:
- the function returns a primitive type
- the function returns an API/implementation defined type (a class)
The first case is straight forward, as the value returned from the implementation-side function can be simply forwarded.
uint32_t OMR::JitBuilder::Base::Foo() {
return static_cast<TR::Base *>(_impl)->Foo();
}
In the second case, given the value returned by the implementation-side function
is a pointer to an implementation-side object, the corresponding client-side
object must be retrieved and returned, taking care to properly handle possible
null pointers. For convenience, the GET_CLIENT_OBJECT()
macro can be used.
OMR::JitBuilder::Other * OMR::JitBuilder::Base::Foo() {
TR::Other * implRet = static_cast<TR::Base *>(_impl)->Foo();
GET_CLIENT_OBJECT(clientObj, Other, implRet);
return clientObject;
}
GET_CLIENT_OBJECT(clientObj, Other, implRet)
will get the client object
corresponding to implRet
by calling the client()
method on the object. The
client object is then assigned to a new variable named clientObj
of type
Other
(the namespace qualifier must not be specified). If thimplRet
is null,
the clientObj
will also be null.
Argument marshalling further complicates things as arguments of the following types must be handled:
- primitive types (including arrays of primitive types and in/out parameters of primitive types)
- API/implementation types (a class)
- in-out API/implementation types
- array of API/implementation types
- vararg (mapped to an array of API/implementation type)
Once again, primitive types represent the simple case as the arguments can be simply forwarded to the implementation-side function.
void OMR::JitBuilder::Base::Foo(int32_t a, double * b) {
static_cast<TR::Base *>(_impl)->Foo(a, b);
}
For class types, the implementation-side object corresponding to each object must be retrieved, taking care to properly handle null pointers.
void OMR::JitBuilder::Base::Foo(OMR::JitBuilder::A * a) {
static_cast<TR::Base *>(_impl)->Foo(static_cast<TR::A *>(a != NULL ? a->_impl : NULL));
}
Note that _impl
always points the the type of the object itself, not the base type!