> Delegating to an external vtable (mandatory to avoid overhead) means that you have to forward-declare all of the types you'll ever use a vtable with.
We went down the rabbit hole of writing a compiler for this as part of a project I used to work on (Apache Clownfish[1], a subproject of the retired Apache Lucy project). We started off parsing .h files, but eventually it made sense to create our own small header language (.cfh "Clownfish Header" files).
Here's some generated code for invoking the CharBuf version of the "Clone" method defined in parent class "Obj":
typedef cfish_CharBuf*
(*CFISH_CharBuf_Clone_t)(cfish_CharBuf* self);
extern uint32_t CFISH_CharBuf_Clone_OFFSET;
static inline cfish_CharBuf*
CFISH_CharBuf_Clone(cfish_CharBuf* self) {
const CFISH_CharBuf_Clone_t method
= (CFISH_CharBuf_Clone_t)cfish_obj_method(
self,
CFISH_CharBuf_Clone_OFFSET
);
return method(self);
}
Usage: cfish_CharBuf *charbuf = cfish_CharBuf_new();
cfish_CharBuf *clone = CFISH_CharBuf_Clone(charbuf);
We had our reasons for going to these extremes: the point of Clownfish was to provide a least-common-denominator object model for bindings to multiple dynamic languages (similar problem domain to SWIG), and the .cfh files also were used to derive types for the binding languages. But there was truly an absurd amount of boilerplate being generated to get around the issue you identify.This is why almost everybody just uses casts to void* for the invocant, skipping type safety.
i am firmly of the opinion that compiling to c is a better route than doing clever c tricks to sort of get what you want. the compiler can be pretty minimal and as you note it pays for itself.