Skip to content

An robust high-level Defer, RAII implementation for C89, automatic memory safety, smartly!

License

Notifications You must be signed in to change notification settings

zelang-dev/c-raii

Repository files navigation

c-raii

windows & linux & macos centos various cpu's

An robust high-level Defer, RAII implementation for C89, automatic memory safety, smarty!

This library has been decoupled from c-coroutine to be independently developed.

In the effort to find uniform naming of terms, various other packages was discovered Except, and exceptions-and-raii-in-c. Choose to settle in on A defer mechanism for C, an upcoming C standard compiler feature. It's exactly this library's working design and concepts addressed in c-coroutine.

This library uses an custom version of rpmalloc for malloc/heap allocation, not C11 compiler thread local dependant, nor C++ focus, removed. The customization is merged with required cthread for C11 thread like emulation.

As a side benefit, just including a single #include "raii.h" will make your Linux only application Windows compatible, see work-steal.c in examples folder, it's from Complementary Concurrency Programs for course "Linux Kernel Internals", 2 minor changes, using macro make_atomic.

The planned implementation from defer reference implementation for C:

RAII C89 Planned C11
// includes "cthread.h" emulated C11 threads
#include "raii.h"

int main(int argc, char **argv)
guard {
    // Panic if p == NULL
    // Automatically _defer(free, p)
    void *p = _malloc(25);
    void *q = _calloc(1, 25);

    if (mtx_lock(&mut)==thrd_error) break;
    _defer(mtx_unlock, &mut);

    // all resources acquired
} unguarded(0);
guard {
  void * const p = malloc(25);
  if (!p) break;
  defer free(p);

  void * const q = malloc(25);
  if (!q) break;
  defer free(q);

  if (mtx_lock(&mut)==thrd_error) break;
  defer mtx_unlock(&mut);

  // all resources acquired
}

There C Standard implementation states: The important interfaces of this tool are:

  • guard prefixes a guarded block
  • defer prefixes a defer clause
  • break ends a guarded block and executes all its defer clauses
  • return unwinds all guarded blocks of the current function and returns to the caller
  • exit unwinds all defer clauses of all active function calls of the thread and exits normally
  • panic starts global unwinding of all guarded blocks
  • recover inside a defer clause stops a panic and provides an error code

There example from source - gitlab, outlined in C Standard WG14 meeting - pdf

RAII C89 Planned C11
#include "raii.h"

char number[20];
void g_print(void *ptr) {
    int i = raii_value(ptr)->integer;
    printf("Defer in g = %d.\n", i);
}

void g(int i) {
    if (i > 3) {
        puts("Panicking!");
        snprintf(number, 20, "%ld", i);
        _panic(number);
    }
    guard {
      _defer(g_print, &i);
      printf("Printing in g = %d.\n", i);
      g(i + 1);
    } guarded;
}

void f_print(void *na) {
    puts("In defer in f");
    fflush(stdout);
    if (_recover(_get_message())) {
        printf("Recovered in f = %s\n", _get_message());
        fflush(stdout);
    }
}

void f()
guard {
    _defer(f_print, NULL);
    puts("Calling g.");
    g(0);
    puts("Returned normally from g.");
} guarded;

int main(int argc, char **argv) {
    f();
    puts("Returned normally from f.");
    return EXIT_SUCCESS;
}
#include <stdio.h>
#include <stddef.h>
#include <threads.h>
#include "stddefer.h"

void g(int i) {
  if (i > 3) {
    puts("Panicking!");
    panic(i);
  }
  guard {
    defer {
      printf("Defer in g = %d.\n", i);
    }
    printf("Printing in g = %d.\n", i);
    g(i+1);
  }
}

void f() {
  guard {
    defer {
        puts("In defer in f");
        fflush(stdout);
      int err = recover();
      if (err != 0) {
        printf("Recovered in f = %d\n", err);
        fflush(stdout);
      }
    }
    puts("Calling g.");
    g(0);
    puts("Returned normally from g.");
  }
}

int main(int argc, char* argv[static argc+1]) {
  f();
  puts("Returned normally from f.");
  return EXIT_SUCCESS;
}

Function ​f​ containing a ​defer​ statement which contains a call to the ​recover function. Function ​f​ invokes function ​g​ which recursively descends before panicking when the value of ​i > 3​. Execution of ​f​ produces the following output:

Calling g.
Printing in g = 0.
Printing in g = 1.
Printing in g = 2.
Printing in g = 3.
Panicking!
Defer in g = 3.
Defer in g = 2.
Defer in g = 1.
Defer in g = 0.
In defer in f
Recovered in f = 4
Returned normally from f.

Synopsis

C++ has a concept called unique_ptr) where "a smart pointer that owns and manages another object through a pointer and disposes of that object when the unique_ptr goes out of scope. "

Here too the same process is in effect through an new typedef unique_t aka memory_t structure.

There is 1 way to create an smart memory pointer.

/* Creates smart memory pointer, this object binds any additional requests to it's lifetime.
for use with `malloc_*` `calloc_*` wrapper functions to request/return raw memory. */
C_API unique_t *unique_init(void);

This system use macros RAII_MALLOC, RAII_FREE, RAII_REALLOC, and RAII_CALLOC. If not defined, the default malloc/calloc/realloc/free are used when expanded. The expansion thereto points to custom replacement allocation routines rpmalloc.

The following malloc/calloc wrapper functions are used to get an raw memory pointer.

/* Request/return raw memory of given `size`, using smart memory pointer's lifetime scope handle.
DO NOT `free`, will be `RAII_FREE` when scope smart pointer panics/returns/exits. */
C_API void *malloc_by(memory_t *scope, size_t size);

/* Request/return raw memory of given `size`, using smart memory pointer's lifetime scope handle.
DO NOT `free`, will be `RAII_FREE` when scope smart pointer panics/returns/exits. */
C_API void *calloc_by(memory_t *scope, int count, size_t size);

/* Request/return raw memory of given `size`, using smart memory pointer's lifetime scope handle.
DO NOT `free`, will be freed with given `func`, when scope smart pointer panics/returns/exits. */
C_API void *malloc_full(memory_t *scope, size_t size, func_t func);

/* Request/return raw memory of given `size`, using smart memory pointer's lifetime scope handle.
DO NOT `free`, will be freed with given `func`, when scope smart pointer panics/returns/exits. */
C_API void *calloc_full(memory_t *scope, int count, size_t size, func_t func);

Note the above functions will panic/throw if request fails, is NULL, and begin unwinding, executing deferred statements.

Thereafter, an smart pointer can be use with these raii_* functions.

/* Defer execution `LIFO` of given function with argument,
to the given `scoped smart pointer` lifetime/destruction. */
C_API size_t raii_deferred(memory_t *, func_t, void *);

/* Same as `raii_deferred` but allows recover from an Error condition throw/panic,
you must call `raii_is_caught` inside function to mark Error condition handled. */
C_API void raii_recover_by(memory_t *, func_t, void *);

/* Compare `err` to scoped error condition, will mark exception handled, if `true`. */
C_API bool raii_is_caught(memory_t *scope, const char *err);

/* Get scoped error condition string. */
C_API const char *raii_message_by(memory_t *scope);

/* Begin `unwinding`, executing given scope smart pointer `raii_deferred` statements. */
C_API void raii_deferred_free(memory_t *);

/* Same as `raii_deferred_free`, but also destroy smart pointer. */
C_API void raii_delete(memory_t *ptr);

Using thread local storage for an default smart pointer, the following functions always available.

/* Request/return raw memory of given `size`,
uses current `thread` smart pointer,
DO NOT `free`, will be `RAII_FREE`
when `raii_deferred_clean` is called. */
C_API void *malloc_default(size_t size);

/* Request/return raw memory of given `size`,
uses current `thread` smart pointer,
DO NOT `free`, will be `RAII_FREE`
when `raii_deferred_clean` is called. */
C_API void *calloc_default(int count, size_t size);

/* Defer execution `LIFO` of given function with argument,
to current `thread` scope lifetime/destruction. */
C_API size_t raii_defer(func_t, void *);

/* Same as `raii_defer` but allows recover from an Error condition throw/panic,
you must call `raii_caught` inside function to mark Error condition handled. */
C_API void raii_recover(func_t, void *);

/* Compare `err` to current error condition, will mark exception handled, if `true`. */
C_API bool raii_caught(const char *err);

/* Get current error condition string. */
C_API const char *raii_message(void);

/* Begin `unwinding`, executing current `thread` scope `raii_defer` statements. */
C_API void raii_deferred_clean(void);

Fully automatic memory safety, using guard/unguarded/guarded macro.

The full potently of RAII is encapsulated in the guard macro. Using try/catch/catch_any/catch_if/finally/end_try exception system macros separately will be unnecessary, however see examples folder for usage.

try {
    throw(division_by_zero);
    printf("never reached\n");
} catch_if {
    if (caught(bad_alloc))
        printf("catch: exception %s (%s:%d) caught\n", err, err_file, err_line);
    else if (caught(division_by_zero))
        printf("catch: exception %s (%s:%d) caught\n", err, err_file, err_line);

    ex_backtrace(err_backtrace);
} finally {
    if (err)
        printf("finally: try failed -> %s (%s:%d)\n", err, err_file, err_line);
    else
        printf("finally: try succeeded\n");
} end_try;

The Planned C11 implementation details still holds here, but defer not confined to guard block, actual function call.

/* Creates an scoped guard section, it replaces `{`.
Usage of: `_defer`, `_malloc`, `_calloc`, `_assign_ptr` macros
are only valid between these sections.
    - Use `_return(x);` macro, or `break;` to exit early.
    - Use `_assign_ptr(var)` macro, to make assignment of block scoped smart pointer. */
#define guard

/* This ends an scoped guard section, it replaces `}`.
On exit will begin executing deferred functions,
return given `result` when done, use `NONE` for no return. */
#define unguarded(result)

/* This ends an scoped guard section, it replaces `}`.
On exit will begin executing deferred functions. */
#define guarded

/* Returns protected raw memory pointer,
DO NOT FREE, will `throw/panic` if memory request fails. */
#define _malloc(size)

/* Returns protected raw memory pointer,
DO NOT FREE, will `throw/panic` if memory request fails. */
#define _calloc(count, size)

/* Defer execution `LIFO` of given function with argument,
execution begins when current `guard` scope exits or panic/throw. */
#define _defer(func, ptr)

/* Compare `err` to scoped error condition, will mark exception handled, if `true`. */
#define _recover(err)

/* Compare `err` to scoped error condition,
will mark exception handled, if `true`.
DO NOT PUT `err` in quote's like "err". */
#define _is_caught(err)

/* Get scoped error condition string. */
#define _get_message()

/* Stops the ordinary flow of control and begins panicking,
throws an exception of given message. */
#define _panic(err)

/* Makes a reference assignment of current scoped smart pointer. */
#define _assign_ptr(scope)

/* Exit `guarded` section, begin executing deferred functions,
return given `value` when done, use `NONE` for no return. */
#define _return(value)

The idea way of using this library, is to make a new field for unique_t into your current typedef object, mainly one held throughout application, and setup your wrapper functions to above raii_ functions.

There are also 2 global callback functions that need to be setup for complete integration.

// Currently an wrapper function that set ctx->data, scoped error, and protection state, working on removing need
typedef void (*ex_setup_func)(ex_context_t *ctx, const char *ex, const char *panic);
// Your wrapper to raii_deferred_free(ctx->data)
typedef void (*ex_unwind_func)(void *);

ex_setup_func exception_setup_func;
ex_unwind_func exception_unwind_func;

Installation

The build system uses cmake, by default produces static library stored under built, and the complete include folder is needed.

As cmake project dependency

if(UNIX)
    set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g -D USE_DEBUG")
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 -fomit-frame-pointer -Wno-return-type")
endif()

if(WIN32)
    set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} /D USE_DEBUG")
    add_definitions(-D_CRT_SECURE_NO_DEPRECATE)
    add_definitions("/wd4244 /wd4267 /wd4033 /wd4715")
endif()

add_subdirectory(deps/raii)
target_link_libraries(project PUBLIC raii)

Linux

mkdir build
cd build
cmake .. -DCMAKE_BUILD_TYPE=Debug/Release -D BUILD_TESTS=OFF -D BUILD_EXAMPLES=OFF # use to not build tests and examples
cmake --build .

Windows

mkdir build
cd build
cmake .. -D BUILD_TESTS=OFF -D BUILD_EXAMPLES=OFF # use to not build tests and examples
cmake --build . --config Debug/Release

Contributing

Contributions are encouraged and welcome; I am always happy to get feedback or pull requests on Github :) Create Github Issues for bugs and new features and comment on the ones you are interested in.

License

The MIT License (MIT). Please see License File for more information.