Type-safe zero-boilerplate interfaces for pure C99, implemented as a single-header library.

Overview

Interface99

CI

Type-safe zero-boilerplate interfaces for pure C99, implemented as a single-header library.

[ examples/state.c ]

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); } ">
#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

  1. Download Interface99 and Metalang99 (minimum supported version -- 1.2.0).
  2. Add interface99 and metalang99/include to your include paths.
  3. #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:

get(st.self)); st.vptr->set(st.self, 5); printf("x = %d\n", st.vptr->get(st.self)); } ">
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

")" ; ::= "iFn(" "," "," ");" ; ::= ; ::= ; ::= ; ::= "impl(" "," ")" ; ::= "implPrimary(" "," ")" ; ::= "declImpl(" "," ")" ; ::= "dyn(" "," "," ")" ; ::= "VTABLE(" "," ")" ; ::= ; ::= ; ::= ; ">
    ::= "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 _INTERFACE which must expand to { }*. It must be defined for every interface.
  • For any interface, a macro _EXTENDS can be defined. It must expand to "(" { "," }* ")".

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 VTable 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 I specified in the macro _INTERFACE, the corresponding function pointer is generated.
  • Requirements obligation. If the macro _EXTENDS is defined, then the listed requirements are generated to obligate 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 VTable for . It is generated in two steps:

  • Function implementations. Each __I refers to a function belonging to which implements the corresponding function of .
  • Requirements satisfaction. If the macro _EXTENDS is defined, then the listed requirements are generated to satisfy .

implPrimary

Like impl but captures the _ functions instead of __.

declImpl

Expands to const VTable VTABLE(, ), i.e., it declares a virtual table instance of of type VTable.

dyn

Expands to an expression of type , with .self initialised to and .vptr initialised to &VTABLE(, ).

VTABLE

Expands to __impl, i.e., a virtual table instance of of type VTable.

Miscellaneous

  • The macros IFACE99_MAJOR, IFACE99_MINOR, and IFACE99_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 prefix 99), define IFACE99_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 impls with static 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 (see examples/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:

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);
      | ^~~~~~~~~

Error: typo in dyn

#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});
      |               ~~~^
      |                  )

Error: typo in VTABLE

#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?

Suggestion

A: VS Code automatically enables suggestions of generated types but, of course, it does not support macro syntax highlightment.

Q: Why use void *self instead of T *self in implementations?

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
Releases(v0.5.0)
A template for modern C++ projects using CMake, Clang-Format, CI, unit testing and more, with support for downstream inclusion.

Modern C++ Template A quick C++ template for modern CMake projects, aimed to be an easy to use starting point. This is my personal take on such a type

Filip Dutescu 826 Sep 12, 2021
Pitchfork is a Set of C++ Project Conventions

Pitchfork Pitchfork is a set of conventions for native C and C++ projects. The most prominent being the project layout conventions. The layout specifi

null 550 Sep 12, 2021