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
You might also like...
Lightweight single-file utilities for C99. Portable & zero dependency

plainlibs Lightweight single-file utilities for C99. Key Features Portable across Unix & Windows (including MSVC) Zero dependencies (besides C stdlib)

An HTML5 parsing library in pure C99

Gumbo - A pure-C HTML5 parser. Gumbo is an implementation of the HTML5 parsing algorithm implemented as a pure C99 library with no outside dependencie

Easy to use, header only, macro generated, generic and type-safe Data Structures in C
Easy to use, header only, macro generated, generic and type-safe Data Structures in C

C Macro Collections Easy to use, header only, macro generated, generic and type-safe Data Structures in C. Table of Contents Installation Contributing

Easy to use, header only, macro generated, generic and type-safe Data Structures in C
Easy to use, header only, macro generated, generic and type-safe Data Structures in C

C Macro Collections Easy to use, header only, macro generated, generic and type-safe Data Structures in C. Table of Contents Installation Contributing

a header-only crossplatform type-safe dynamic compiler generator based on C++ 17.
a header-only crossplatform type-safe dynamic compiler generator based on C++ 17.

Mu Compiler Generator MuCompilerGenerator(MuCplGen) a Header-Only dynamic compiler generator based on C++ 17. Why MuCplGen? header-only cross-platform

A lightweight, portable pure C99 onnx inference engine for embedded devices with hardware acceleration support.
A lightweight, portable pure C99 onnx inference engine for embedded devices with hardware acceleration support.

Libonnx A lightweight, portable pure C99 onnx inference engine for embedded devices with hardware acceleration support. Getting Started The library's

MMCTX (Memory Management ConTeXualizer), is a tiny ( 300 lines), single header C99 library that allows for easier memory management by implementing contexts that remember allocations for you and provide freeall()-like functionality.

MMCTX (Memory Management ConTeXualizer), is a tiny ( 300 lines), single header C99 library that allows for easier memory management by implementing contexts that remember allocations for you and provide freeall()-like functionality.

oZKS (Ordered Zero-Knowledge Set) is a library that provides an implementation of an Ordered (and Append Only) Zero-Knowledge Set.

Ordered Zero-Knowledge Set - oZKS Introduction oZKS is a library that provides an implementation of an Ordered (and Append Only) Zero Knowledge Set. A

Single header asymmetric stackful cross-platform coroutine library in pure C.
Single header asymmetric stackful cross-platform coroutine library in pure C.

minicoro Minicoro is single-file library for using asymmetric coroutines in C. The API is inspired by Lua coroutines but with C use in mind. The proje

Single header lib for JPEG encoding. Public domain. C99. stb style.

tiny_jpeg.h A header-only public domain implementation of Baseline JPEG compression. Features: stb-style header only library. Does not do dynamic allo

single-header json parser for c99 and c++

ghh_json.h a single-header ISO-C99 (and C++ compatible) json loader. why? obviously this isn't the first json library written for C, so why would I wr

Single header lib for JPEG encoding. Public domain. C99. stb style.

tiny_jpeg.h A header-only public domain implementation of Baseline JPEG compression. Features: stb-style header only library. Does not do dynamic allo

Pure C ONNX runtime with zero dependancies for embedded devices

🤖 cONNXr C ONNX Runtime A onnx runtime written in pure C99 with zero dependencies focused on embedded devices. Run inference on your machine learning

PikaScript is an ultra-lightweight Python engine with zero dependencies and zero-configuration, that can run with 4KB of RAM (such as STM32G030C8 and STM32F103C8), and is very easy to deploy and expand.
PikaScript is an ultra-lightweight Python engine with zero dependencies and zero-configuration, that can run with 4KB of RAM (such as STM32G030C8 and STM32F103C8), and is very easy to deploy and expand.

PikaScript 中文页| Star please~ 1. Abstract PikaScript is an ultra-lightweight Python engine with zero dependencies and zero-configuration, that can run

"SaferCPlusPlus" is essentially a collection of safe data types intended to facilitate memory and data race safe C++ programming

A collection of safe data types that are compatible with, and can substitute for, common unsafe native c++ types.

Minimal, type safe printf replacement library for C++

tinyformat.h A minimal type safe printf() replacement tinyformat.h is a type safe printf replacement library in a single C++ header file. If you've ev

Owner
null
Library of generic and type safe containers in pure C language (C99 or C11) for a wide collection of container (comparable to the C++ STL).

M*LIB: Generic type-safe Container Library for C language Overview M*LIB (M star lib) is a C library enabling to use generic and type safe container i

PpHd 553 Nov 21, 2022
A demonstration of implementing, and using, a "type safe", extensible, and lazy iterator interface in pure C99.

c-iterators A demonstration of implementing, and using, a "type safe", extensible, and lazy iterator interface in pure C99. The iterable is generic on

Chase 68 Nov 11, 2022
Single-header header-only C++11 / C++14 / C++17 library for easily managing set of auto-generated type-safe flags.

Single-header header-only C++11 / C++14 / C++17 library for easily managing set of auto-generated type-safe flags. Quick start #include <bitflags/bitf

Marin Peko 76 Nov 22, 2022
Embed read-only filesystems into any C++11 program w. a single header, zero dependencies and zero modifications to your code

c-embed Embed read-only filesystems into any C++11 program w. a single header, zero dependencies and zero modifications to your code. Usage c-embed al

Nick McDonald 7 Oct 3, 2022
variant lite - A C++17-like variant, a type-safe union for C++98, C++11 and later in a single-file header-only library

variant lite: A single-file header-only version of a C++17-like variant, a type-safe union for C++98, C++11 and later Contents Example usage In a nuts

Martin Moene 221 Nov 26, 2022
Type safe - Zero overhead utilities for preventing bugs at compile time

type_safe type_safe provides zero overhead abstractions that use the C++ type system to prevent bugs. Zero overhead abstractions here and in following

Jonathan Müller 1.2k Nov 27, 2022
Cross-platform STL-styled and STL-compatible library with implementing containers, ranges, iterators, type traits and other tools; actors system; type-safe config interface.

Yato A small repository where I'm gatherting useful snippets and abstractions for C++ development. Yato includes 3 main modules: multidimensional cont

Alexey 9 Jul 22, 2022