Skip to content

Instantly share code, notes, and snippets.

@vurtun
Last active June 21, 2019 02:36
Show Gist options
  • Save vurtun/2e32089f0f012aab2b4c6385f145b5bd to your computer and use it in GitHub Desktop.
Save vurtun/2e32089f0f012aab2b4c6385f145b5bd to your computer and use it in GitHub Desktop.
/* Lightweight module based templates in standard C
===================================================
This is a proof of concept example of lightweight module based templates in C and
is loosely based on https://gist.github.com/pervognsen/c56d4ddce94fbef3c80e228b39efc028 from Per Vognsen.
While his approach (at least as far as I understood) is based on a python script to generate
the .h/.c files for you is this implementation contained in one single header file.
This is an outline to show how you can use the single header approach
and bend it to its absolute extrems. I tend to write specialized datastructures
for most of my problems but sometimes it happens that I have to use a particular
solution multiple times. While this happens a lot less than template
advocates want me to believe but it does come up. Usually you are forced to
use C++ templates or ugly macros in pure C which are extremely hard to ensure to be
correct and not really debuggable.
This is NOT about the actual queue implementation here which
under no circumstances should be used and is only here as dummy problem.
(I use this approach at work but have no direct problem to showcase it on so I made
something up)
Personally I stay away from C++ as far I am able to and dislike templates. Still all what I
present here can be done with C++ as well, so it is not like this is a magic bullet.
Furthermore while my solution is at least for my taste less ugly then templates
there is a big chance that it is only my personal preference.
This module based template approach allows a massive amount of control.
You can define names, constants, change module behavior by activating or
deactivating parts, overwrite functions without resorting to callbacks (for example
the compare callback of `qsort` which C++ advocates love to point out as a downside
of C) and abstract platform depended functions and types. It basically extends my
library approach I used for nuklear and turns it up a notch for even more control
on the users side.
Biggest downside of my approach shown here is that you have to define the same defines for every
header and implementation pair and that include guards do not work correctly since
you have to include this file multiple times. So far I did not find a solution
to only allow including this file with a given type only once (any idea appreciated).
In general this approach increases the ability to abstract without losing debug ability while
being specialized at compiling time while not losing performance or the ability
to debug often encountered with templates and macros. In the end this is not the
holy grail but definitely at least for me an interesting approach.
USAGE:*/
#if 0
/* 1.) Basic seperated example: */
--------------------------------
/* included in other header: */
#define FIFO_HEADER
#define FIFO_TYPE int
#define FIFO_NAME int_queue
#define FIFO_BUFFER_SIZE 256
#include "queue.h"
/* included in source file: */
#define FIFO_IMPLEMENTATION
#define FIFO_TYPE int
#define FIFO_NAME int_queue
#define FIFO_BUFFER_SIZE 256
#include "queue.h"
2.) Basic unified example: */
--------------------------------
#define FIFO_HEADER
#define FIFO_IMPLEMENTATION
#define FIFO_TYPE char
#define FIFO_NAME char_queue
#define FIFO_BUFFER_SIZE 256
#include "queue.h"
3.) Heavy user modification example: */
--------------------------------
#define FIFO_INCLUDE_FIXED_TYPES
#define FIFO_TYPE float
#define FIFO_BUFFER_SIZE 1024
#define FIFO_NAME float_queue
#define FIFO_ASSERT(e) my_assert(e)
#define FIFO_SEMAPHORE sem_t
#define FIFO_SEMAPHORE_INIT(s) sem_init(&s,0,0)
#define FIFO_SEMAPHORE_DESTROY(s) sem_close(&s)
#define FIFO_SEMAPHORE_RELEASE(s) sem_post(&s)
#define FIFO_SEMAPHORE_WAIT(s) sem_wait(&s)
#define FIFO_ATOMIC uint32_t volatile
#define FIFO_ATOMIC_STORE(a,v) asm volatile("" ::: "memory"); *(a) = (v)
#define FIFO_ATOMIC_LOAD(a,v) asm volatile("" ::: "memory"); *(v) = (a)
#define FIFO_ATOMIC_CAS(a,n,o) __sync_bool_compare_and_swap((a),o,n)
#define FIFO_HEADER
#define FIFO_IMPLEMENTATION
#include <semaphore.h>
#include "queue.h"
int main(int arg, char **argv)
{
{int value = 5;
struct int_queue iq;
int_queue_init(&iq);
int_queue_push(&iq, value);
if (int_queue_pop(&iq, &value))
printf("%d\n",value);
int_queue_destroy(&iq);}
{float value = 5;
struct float_queue fq;
float_queue_init(&fq);
float_queue_push(&fq, value);
if (float_queue_pop(&fq, &value))
printf("%.2f\n",value);
float_queue_destroy(&fq);}
return 0;
}
#endif
/* ============================================================================
*
* CHECK
*
* ===========================================================================*/
/* This sections basically checks if all needed identifier were defined. At first
* glance this seems like a lot of work and it is. You check for misuse which should be done
* by the compiler. Sadly templates or big macro error message produced
* by C/C++ compilers are aweful. So I prefer to provide actually useful infomation .
*/
#if !defined(FIFO_HEADER) && !defined(FIFO_IMPLEMENTATION)
#error "fifo: you forgot to set either header and/or implementation mode"
#endif
#ifndef FIFO_NAME
#error "fifo: you forgot to set a name"
#endif
#ifndef FIFO_BUFFER_SIZE
#error "fifo: you forgot to set a buffer size"
#endif
#ifdef FIFO_ATOMIC
#ifndef FIFO_ATOMIC_STORE
#error "fifo: missing atomic store functionality"
#endif
#ifndef FIFO_ATOMIC_LOAD
#error "fifo: missing atomic load functionality"
#endif
#ifndef FIFO_ATOMIC_CAS
#error "fifo: missing atomic compare exchange functionality"
#endif
#endif
#ifdef FIFO_SEMAPHORE
#ifndef FIFO_SEMAPHORE_INIT
#error "fifo: missing semaphore init functionality"
#endif
#ifndef FIFO_SEMAPHORE_DESTROY
#error "fifo: missing semaphore destroy functionality"
#endif
#ifndef FIFO_SEMAPHORE_RELEASE
#error "fifo: missing semaphore release functionality"
#endif
#ifndef FIFO_SEMAPHORE_WAIT
#error "fifo: missing semaphore wait functionality"
#endif
#endif
/* ============================================================================
*
* CONFIG
*
* ===========================================================================*/
/* This section basically handles different ways to configure this implementation
* to your needs. It symbolizes the biggest advantage of single header libraries:
* configurability. You can overwrite behavior, remove or add library parts and
* react on different platforms, compiler versions, basically all variables outside
* your controll while first writing this library.
*/
/* select correct fixed size types */
#ifdef FIFO_INCLUDE_FIXED_TYPES
#include <stdint.h>
#ifndef FIFO_I32
#define FIFO_I32 int32_t
#endif
#ifndef FIFO_U32
#define FIFO_U32 uint32_t
#endif
#else
#ifndef FIFO_I32
#if defined(_MSC_VER)
#define FIFO_I32 __int32
#else
#define FIFO_I32 signed int
#endif
#endif
#ifndef FIFO_U32
#if defined(_MSC_VER)
#define FIFO_U32 unsigned __int32
#else
#define FIFO_U32 unsigned int
#endif
#endif
#endif
/* abstract atomic type */
#ifndef FIFO_ATOMIC
#ifdef _WIN32
#include "windows.h"
#define FIFO_ATOMIC FIFO_U32 volatile
#define FIFO_ATOMIC_STORE(a,v) _WriteBarrier(); *(a) = (v)
#define FIFO_ATOMIC_LOAD(a,v) _ReadBarrier(); *(v) = (a)
#define FIFO_ATOMIC_CAS(a,n,o) InterlockedCompareExchange((LONG volatile*)a,n,o)
#else
#error "Fifo: Missing atomic type definition"
#endif
#endif
/* abstract semaphore type */
#ifndef FIFO_SEMAPHORE
#ifdef _WIN32
#include "windows.h"
#define FIFO_SEMAPHORE HANDLE
#define FIFO_SEMAPHORE_INIT(s) (s) = CreateSemaphore(0,0,FIFO_BUFFER_SIZE,0)
#define FIFO_SEMAPHORE_DESTROY(s) CloseHandle(s)
#define FIFO_SEMAPHORE_RELEASE(s) ReleaseSemaphore(s,1,0)
#define FIFO_SEMAPHORE_WAIT(s) WaitforSingleObject(s, INFINITE,FALSE)
#else
#error "Fifo: Missing semaphore type definition"
#endif
#endif
/* function visibility */
#ifndef FIFO_PRIVATE
#define FIFO_API static
#else
#define FIFO_API extern
#endif
/* identifier */
#ifndef FIFO_INIT
#define FIFO_INIT FIFO_IDENTIFIER(FIFO_NAME,_init)
#endif
#ifndef FIFO_DESTROY
#define FIFO_DESTROY FIFO_IDENTIFIER(FIFO_NAME,_destroy)
#endif
#ifndef FIFO_PUSH
#define FIFO_PUSH FIFO_IDENTIFIER(FIFO_NAME,_push)
#endif
#ifndef FIFO_TRY_POP
#define FIFO_TRY_POP FIFO_IDENTIFIER(FIFO_NAME,_try_pop)
#endif
/* ============================================================================
*
* FILE
*
* ===========================================================================*/
/* This section contains identifiers or constants which are universal for all
* different invocations of a templated single header library. Basically things
* like enums global constants defines, flags or global helper macros.
*/
#ifndef FIFO_H_INCLUDED
#define FIFO_H_INCLUDED
#ifndef FIFO_JOIN
#define FIFO_JOIN_IM(a,b) a##b
#define FIFO_JOIN_DELAY(a,b) FIFO_JOIN_IM(a,b)
#define FIFO_JOIN(a,b) FIFO_JOIN_DELAY(a,b)
#define FIFO_IDENTIFIER(a,b) FIFO_JOIN(a,b)
#endif
#endif
/* ============================================================================
*
* HEADER
*
* ===========================================================================*/
/* The header section provides types and function declaration depending on the
* current 'template' parameters. So they are created for every new invocation.
* Because C/C++ does not like different types with same name you have to define
* a prefix every time you declare a new type by including this file.
*/
#ifdef FIFO_HEADER
/* first in first out buffer */
struct FIFO_NAME {
FIFO_SEMAPHORE semaphore;
FIFO_ATOMIC next_write;
FIFO_TYPE buffer[FIFO_BUFFER_SIZE];
FIFO_ATOMIC next_read;
};
FIFO_API void FIFO_INIT(struct FIFO_NAME*);
FIFO_API void FIFO_DESTROY(struct FIFO_NAME*);
FIFO_API void FIFO_PUSH(struct FIFO_NAME*, FIFO_TYPE);
FIFO_API int FIFO_TRY_POP(struct FIFO_NAME *q, FIFO_TYPE *var);
#endif
/* ============================================================================
*
* IMPLEMENTATION
*
* =========================================================================== */
/* The implementation section provides is the counter part to the header section
* and actually implements all functionality declared in the header section.
* Important is that both header and implementation are required to have all the
* same 'template' parameters or name conflicts or other unforseen consequences can
* occur.
*/
#ifdef FIFO_IMPLEMENTATION
#ifndef FIFO_ZERO
#include <string.h>
#define FIFO_ZERO(p,s) memset(p,0,s)
#endif
#ifndef FIFO_ASSERT
#include <assert.h>
#define FIFO_ASSERT(e) assert(e)
#endif
FIFO_API void
FIFO_INIT(struct FIFO_NAME *q)
{
FIFO_ASSERT(q);
if (!q) return;
FIFO_ZERO(q, sizeof(*q));
FIFO_SEMAPHORE_INIT(q->semaphore);
}
FIFO_API void
FIFO_DESTROY(struct FIFO_NAME *q)
{
FIFO_ASSERT(q);
if (!q) return;
FIFO_SEMAPHORE_DESTROY(q->semaphore);
FIFO_ZERO(q, sizeof(*q));
}
FIFO_API void
FIFO_PUSH(struct FIFO_NAME *q, FIFO_TYPE var)
{
FIFO_ASSERT(q);
if (!q) return;
FIFO_U32 next_write = (q->next_write+1) % FIFO_BUFFER_SIZE;
FIFO_ASSERT(next_write != q->next_read);
FIFO_TYPE *entry = q->buffer + q->next_write;
*entry = var;
FIFO_ATOMIC_STORE(&q->next_write, next_write);
FIFO_SEMAPHORE_RELEASE(q->semaphore)
}
FIFO_API int
FIFO_TRY_POP(struct FIFO_NAME *q, FIFO_TYPE *var)
{
int success = 1;
FIFO_ASSERT(q);
FIFO_ASSERT(var);
if (!q || !var) return 0;
FIFO_U32 old_read = q->next_read;
FIFO_U32 next_read = (old_read + 1) % FIFO_BUFFER_SIZE;
if (to_read != q->next_to_write) {
FIFO_U32 index = FIFO_ATOMIC_CAS(&q->next_read, next_read, old_read);
if (index == old_read) *var = q->buffer[index];
} else success = 0;
return success;
}
#endif
/* ============================================================================
*
* CLEANUP
*
* =========================================================================== */
/* Finally the last section is cleanup. Basically all 'template' parameter previously
* defined by the user or default configuration has to be undefined at this point
* to make it possible to include this file multiple times.
*/
#undef FIFO_API
#undef FIFO_ASSERT
#undef FIFO_ZERO
#undef FIFO_INCLUDE_FIXED_TYPES
#undef FIFO_NAME
#undef FIFO_TYPE
#undef FIFO_BUFFER_SIZE
#undef FIFO_INIT
#undef FIFO_DESTROY
#undef FIFO_PUSH
#undef FIFO_TRY_POP
#undef FIFO_I32
#undef FIFO_U32
#undef FIFO_ATOMIC
#undef FIFO_ATOMIC_STORE
#undef FIFO_ATOMIC_LOAD
#undef FIFO_ATOMIC_CAS
#undef FIFO_SEMAPHORE
#undef FIFO_SEMAPHORE_CREATE
#undef FIFO_SEMAPHORE_DESTROY
#undef FIFO_SEMAPHORE_RELEASE
#undef FIFO_SEMAPHORE_WAIT
#undef FIFO_HEADER
#undef FIFO_IMPLEMENTATION
@justinmeiners
Copy link

justinmeiners commented Jun 16, 2019

include guards do not work correctly since
you have to include this file multiple times. So far I did not find a solution
to only allow including this file with a given type only once (any idea appreciated).

Can you talk any more about this?
I am looking at Per's project and don't see how it handles header guards.

One idea I thought of was wrapping the various declarations in a single header guard:

somewhere.h

#ifndef BTREE_GUARD
#define BTREE_GUARD

// declare all variants you need
#define BTREE_KEY int
#include "tree.inl"

#define BTREE_KEY const char*
#include "tree.inl"

#endif // guard

@dumblob
Copy link

dumblob commented Jun 18, 2019

There is a quite powerful way how to do universal templating - see dhall. It might give you some inspiration (even though dhall itself is total overkill for "merge of few header files").

@justinmeiners
Copy link

Thanks, I will look.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment