Interface99
Type-safe zero-boilerplate interfaces for pure C99, implemented as a single-header library.
[ examples/state.c
]
#include <interface99.h>
#include <stdio.h>
#define State_INTERFACE \
iFn(int, get, void *self); \
iFn(void, set, void *self, int x);
interface(State);
typedef struct {
int x;
} Num;
int Num_State_get(void *self) {
return ((Num *)self)->x;
}
void Num_State_set(void *self, int x) {
((Num *)self)->x = x;
}
impl(State, Num);
void test(State st) {
printf("x = %d\n", st.vptr->get(st.self));
st.vptr->set(st.self, 5);
printf("x = %d\n", st.vptr->get(st.self));
}
int main(void) {
Num n = {0};
State st = dyn(Num, State, &n);
test(st);
}
Output
x = 0
x = 5
Highlights
-
Zero-boilerplate. Forget about constructing virtual tables manually -- Interface99 will do it for you!
-
Portable. Everything you need is a standard-conforming C99 preprocessor.
-
Predictable. Interface99 comes with formal code generation semantics, meaning that the generated data layout is guaranteed to always be the same.
-
Comprehensible errors. Despite that Interface99 is built upon macros, compilation errors are usually comprehensible.
Features
Feature | Status | Description |
---|---|---|
Multiple interface inheritance |
|
A type can inherit multiple interfaces at the same time. |
Superinterfaces |
|
One interface can require a set of other interfaces to be implemented as well. |
Marker interfaces |
|
An interface with no functions. |
Single/Dynamic dispatch |
|
Determine a function to be called at runtime based on self . |
Multiple dispatch |
|
Determine a function to be called at runtime based on multiple arguments. Likely to never going to be implemented. |
Dynamic objects of multiple interfaces |
|
Given interfaces Foo and Bar , you can pass an object of both interfaces to a function, FooBar obj . |
Default functions |
|
Some interface functions may be given default implementations. |
Installation
- Download Interface99 and Metalang99 (minimum supported version -- 1.2.0).
- Add
interface99
andmetalang99/include
to your include paths. #include
beforehand.
Some handy advices:
-
PLEASE, use Interface99 only with
-ftrack-macro-expansion=0
(GCC),-fmacro-backtrace-limit=1
(Clang), or something similar, otherwise it will throw your compiler to the moon. -
Precompile headers that use Interface99 so that they will not be compiled each time they are included. It is helpful to reduce compilation times, but they are not mandatory.
Usage
Interface99 aims to provide a minimalistic, yet useable set of features found in most programming languages, while staying natural to C. Therefore, if you have experience with other general-purpose PLs, you already know how to use Interface99. Go and look through the examples to see how it performs in the wild.
In this section we are to clarify some details that are specific to Interface99. First of all, there are three major things:
- interface definition,
- interface implementation declaration,
- interface implementation definition.
The terms "declaration" & "definition" have the same semantics as in the C programming language: normally you put declarations inside headers, whereas definitions reside in *.c
files (except an interface definition, which can be located in a header file since it defines nothing but a couple of structures). If your interface must appear only in a single TU, feel free to omit the declarations and place the definitions at the top of the file. In this case, I recommend you to prepend interface implementations with static
: static impl(...);
.
What do the macros generate? interface
generates a virtual table and a so-called dynamic interface object type. In the case of examples/state.c
:
typedef struct StateVTable {
int (*get)(void *self);
void (*set)(void *self, int x);
} StateVTable;
typedef struct State {
void *self;
const StateVTable *vptr;
} State;
impl
generates a constant variable of type StateVTable
:
const StateVTable Num_State_impl = {
.get = Num_State_get,
.set = Num_State_set,
};
This is the implementation of State
for Num
. Normally you will not use it directly but through State.vptr
. State
, in its turn, is instantiated by dyn
:
Num n = {0};
State st = dyn(Num, State, &n);
Since State
is polymorphic over its implementations, you can accept it as a function parameter and manipulate it through .self
& .vptr
:
void test(State st) {
printf("x = %d\n", st.vptr->get(st.self));
st.vptr->set(st.self, 5);
printf("x = %d\n", st.vptr->get(st.self));
}
The last thing is superinterfaces, or interface requirements. examples/airplane.c
demonstrates how to extend interfaces with new functionality:
#define Vehicle_INTERFACE \
iFn(void, move_forward, void *self, int distance); \
iFn(void, move_back, void *self, int distance);
interface(Vehicle);
#define Airplane_INTERFACE \
iFn(void, move_up, void *self, int distance); \
iFn(void, move_down, void *self, int distance);
#define Airplane_EXTENDS (Vehicle)
interface(Airplane);
(Note that #define Airplane_EXTENDS
must appear prior to interface(Airplane);
.)
Here, Airplane
extends Vehicle
with the new functions move_up
and move_down
. Everywhere you have Airplane
, you also have a pointer to VehicleVTable
accessible as Airplane.vptr->Vehicle
:
Airplane my_airplane = dyn(MyAirplane, Airplane, &(MyAirplane){0, 0});
my_airplane.vptr->Vehicle->move_forward(my_airplane.self, 10);
my_airplane.vptr->Vehicle->move_back(my_airplane.self, 3);
Thus, Interface99 embeds superinterfaces into subinterfaces's virtual tables, thereby forming a virtual table hierarchy. Of course, you can specify an arbitrary amount of interfaces along with (Vehicle)
, like Repairable
or Armoured
, and they all will be included in AirplaneVTable
like so:
typedef struct AirplaneVTable {
void (*move_up)(void *self, int distance);
void (*move_down)(void *self, int distance);
const VehicleVTable *Vehicle;
const RepairableVTable *Repairable;
const ArmouredVTable *Armoured;
} AirplaneVTable;
Happy hacking!
Syntax and semantics
Having a well-defined semantics of the macros, you can write an FFI which is quite common in C.
EBNF syntax
::= "interface(" <iface> ")" ; ::= "iFn(" <fn-ret-ty> "," <fn-name> "," <fn-params> ");" ; ::= <type> ; ::= <ident> ; ::= <parameter-type-list> ; ::= "impl(" <iface> "," <implementor> ")" ; ::= "implPrimary(" <iface> "," <implementor> ")" ; ::= "declImpl(" <iface> "," <implementor> ")" ; ::= "dyn(" <implementor> "," <iface> "," <ptr> ")" ; ::= "VTABLE(" <implementor> "," <iface> ")" ; ::= <ident> ; ::= <ident> ; ::= <iface> ;
Notes:
refers to a user-defined macro
which must expand to_INTERFACE {
. It must be defined for every interface.}* - For any interface, a macro
can be defined. It must expand to_EXTENDS "("
.{ "," }* ")"
Semantics
(It might be helpful to look at the generated data layout of examples/state.c
.)
interface
Expands to
typedef struct VTable VTable;
typedef struct ;
struct VTable {
// Only if is a marker interface without superinterfaces:
char dummy;
0 (*0)(0);
...
N (*N)(N);
const 0VTable *;
...
const NVTable *;
};
struct {
void *self;
const VTable *vptr;
}
(char dummy;
is needed for an empty
because a structure must have at least one member, according to C99.)
I.e., this macro defines a virtual table structure for
, as well as the structure
polymorphic over
implementors. This is generated in two steps:
- Function pointers. For each
specified in the macroI
, the corresponding function pointer is generated._INTERFACE - Requirements obligation. If the macro
is defined, then the listed requirements are generated to obligate_EXTENDS
implementors to satisfy them.
impl
Expands to
const VTable VTABLE(, ) = {
// Only if is a marker interface without superinterfaces:
.dummy = '\0',
0 = __0,
...
N = __N,
0 = &VTABLE(0),
...
N = &VTABLE(N),
}
I.e., this macro defines a virtual table instance of type
for
. It is generated in two steps:
- Function implementations. Each
refers to a function belonging to_ _ I
which implements the corresponding function of
. - Requirements satisfaction. If the macro
is defined, then the listed requirements are generated to satisfy_EXTENDS
.
implPrimary
Like impl
but captures the
functions instead of
.
declImpl
Expands to const
, i.e., it declares a virtual table instance of
of type
.
dyn
Expands to an expression of type
, with .self
initialised to
and .vptr
initialised to &VTABLE(
.
VTABLE
Expands to
, i.e., a virtual table instance of
of type
.
Miscellaneous
-
The macros
IFACE99_MAJOR
,IFACE99_MINOR
, andIFACE99_PATCH
stand for the corresponding components of a version of Interface99. -
If you do not want the shortened versions to appear (e.g.,
interface
without the prefix99
), defineIFACE99_NO_ALIASES
prior to#include
. -
For each macro using
ML99_EVAL
, Interface99 provides its Metalang99-compliant counterpart which can be used inside derivers and other Metalang99-compliant macros:
Macro | Metalang99-compliant counterpart |
---|---|
interface |
IFACE99_interface |
impl |
IFACE99_impl |
implPrimary |
IFACE99_implPrimary |
(An arity specifier and desugaring macro are provided for each of the above macros.)
Guidelines
- Prepend
impl
s withstatic
if they must appear only in a single TU:static impl(...);
. - Use
implPrimary
to avoid boilerplate if you implement an interface considered primary for some concrete type (seeexamples/media_stream.c
).
Pitfalls
No pitfalls discovered yet.
Credits
Thanks to Rust and Golang for their implementations of traits/interfaces.
FAQ
Q: Why use C instead of Rust/Zig/whatever else?
A: See Datatype99's README >>.
Q: Why not third-party code generators?
A: See Metalang99's README >>.
Q: How does it work?
A: Interface99 is implemented upon Metalang99, a preprocessor metaprogramming library.
Q: Does it work on C++?
A: Yes, C++11 and onwards is supported.
Q: How Interface99 differs from similar projects?
A:
-
Less boilerplate. In particular, Interface99 deduces function implementations from the context, thus improving code maintenance. To my knowledge, no other alternative can do this.
-
Small. Interface99 only features the software interface concept, no less and no more -- it does not bring all the other fancy OOP stuff, unlike GObject or COS.
-
Depends on Metalang99. Interface99 is built upon Metalang99, the underlying metaprogramming framework. With Metalang99, you can also use Datatype99.
Other worth-mentioning projects:
- typeclass-interface-pattern, though it is rather a general idea than a ready-to-use implementation.
- OOC -- a book about OO programming in ANSI C.
Q: What about compile-time errors?
Error: missing interface implementation
#define Foo_INTERFACE iFn(void, foo, int x, int y);
interface(Foo);
typedef struct {
char dummy;
} MyFoo;
// Missing `void MyFoo_Foo_foo(int x, int y)`.
impl(Foo, MyFoo);
playground.c:12:1: error: ‘MyFoo_Foo_foo’ undeclared here (not in a function); did you mean ‘MyFoo_Foo_impl’?
12 | impl(Foo, MyFoo);
| ^~~~
| MyFoo_Foo_impl
Error: improperly typed interface implementation
#define Foo_INTERFACE iFn(void, foo, int x, int y);
interface(Foo);
typedef struct {
char dummy;
} MyFoo;
void MyFoo_Foo_foo(const char *str) {}
impl(Foo, MyFoo);
playground.c:12:1: warning: initialization of ‘void (*)(int, int)’ from incompatible pointer type ‘void (*)(const char *)’ [-Wincompatible-pointer-types]
12 | impl(Foo, MyFoo);
| ^~~~
Error: unsatisfied interface requirement
#define Foo_INTERFACE iFn(void, foo, int x, int y);
interface(Foo);
#define Bar_INTERFACE iFn(void, bar, void);
#define Bar_EXTENDS (Foo)
interface(Bar);
typedef struct {
char dummy;
} MyBar;
void MyBar_Bar_bar(void) {}
// Missing `impl(Foo, MyBar)`.
impl(Bar, MyBar);
playground.c:17:1: error: ‘MyBar_Foo_impl’ undeclared here (not in a function); did you mean ‘MyBar_Bar_impl’?
17 | impl(Bar, MyBar);
| ^~~~
| MyBar_Bar_impl
playground.c:17:1: warning: missing initializer for field ‘Foo’ of ‘BarVTable’ [-Wmissing-field-initializers]
playground.c:9:1: note: ‘Foo’ declared here
9 | interface(Bar);
| ^~~~~~~~~
dyn
Error: typo in #define Foo_INTERFACE iFn(void, foo, void);
interface(Foo);
typedef struct {
char dummy;
} MyFoo;
void MyFoo_Foo_foo(void) {}
impl(Foo, MyFoo);
int main(void) {
Foo foo = dyn(Foo, /* MyFoo */ MyBar, &(MyFoo){0});
}
playground.c: In function ‘main’:
playground.c:15:15: error: ‘MyBar’ undeclared (first use in this function)
15 | Foo foo = dyn(Foo, /* MyFoo */ MyBar, &(MyFoo){0});
| ^~~
playground.c:15:15: note: each undeclared identifier is reported only once for each function it appears in
playground.c:15:18: error: expected ‘)’ before ‘{’ token
15 | Foo foo = dyn(Foo, /* MyFoo */ MyBar, &(MyFoo){0});
| ~~~^
| )
VTABLE
Error: typo in #define Foo_INTERFACE iFn(void, foo, void);
interface(Foo);
typedef struct {
char dummy;
} MyFoo;
void MyFoo_Foo_foo(void) {}
impl(Foo, MyFoo);
int main(void) {
FooVTable foo = VTABLE(/* MyFoo */ MyBar, Foo);
}
playground.c: In function ‘main’:
playground.c:15:21: error: ‘MyBar_Foo_impl’ undeclared (first use in this function); did you mean ‘MyFoo_Foo_impl’?
15 | FooVTable foo = VTABLE(/* MyFoo */ MyBar, Foo);
| ^~~~~~
| MyFoo_Foo_impl
From my experience, nearly 95% of errors make sense.
If an error is not comprehensible at all, try to look at generated code (-E
). Hopefully, the code generation semantics is formally defined so normally you will not see something unexpected.
Q: What about IDE support?
A: VS Code automatically enables suggestions of generated types but, of course, it does not support macro syntax highlightment.
void *self
instead of T *self
in implementations?
Q: Why use A: This trick technically results in UB; Interface99 is agnostic to function parameters (including self
) though as it claims strict C99 conformance, all the examples are using void *self
.
Q: What compilers are tested?
A: Interface99 is known to work on these compilers:
- GCC
- Clang
- MSVC
- TCC