84

Is it possible to implement RAII in pure C?

I assume it isn't possible in any sane way, but perhaps is it possible using some kind of dirty trick. Overloading the standard free function comes to mind or perhaps overwriting the return address on the stack so that when the function returns, it calls some other function that somehow releases resources? Or maybe with some setjmp/longjmp trick?

This is of a purely academic interest and I have no intention of actually writing such unportable and crazy code but I'm wondering if that is at all possible.

unwind
  • 391,730
  • 64
  • 469
  • 606
elifiner
  • 7,347
  • 8
  • 39
  • 48
  • You can't simply overwrite the return address on the stack; you have to preserve the value on entry and then overwrite it with an alternative. Ugly, but possibly effective. Consider using arena-based memory allocation for memory. Otherwise, just be very careful (and worry about interrupts!). – Jonathan Leffler Dec 15 '08 at 15:37
  • Is RAII that useful in the absence of exceptions? (just asking) – Josh Petitt Jun 07 '13 at 21:49
  • 3
    @JoshPetitt sure, early return, and just not having to remember to free every single thing = fewer bugs. – Oktalist Jun 07 '13 at 22:14
  • @JoshPetitt you at least have to write one less statement. eg fopen without corresponding fclose – Justin Meiners Jul 11 '13 at 02:13
  • I am surprised that no one suggested you use a C++ compiler, and write in that arcane C dialect that is compilable by C++ (just using RAII features when you want them). I am also surprised you have not accepted Johannes's answer, unless you are holding out for a "more general" solution. – jxh Feb 17 '17 at 18:13
  • Why stick to C and do weird things? Just start porting code to C++ if you can do it ;) Convince your bosses... – Raúl Salinas-Monteagudo Mar 10 '20 at 08:05

10 Answers10

111

This is inherent implementation dependent, since the Standard doesn't include such a possibility. For GCC, the cleanup attribute runs a function when a variable goes out of scope:

#include <stdio.h>

void scoped(int * pvariable) {
    printf("variable (%d) goes out of scope\n", *pvariable);
}

int main(void) {
    printf("before scope\n");
    {
        int watched __attribute__((cleanup (scoped)));
        watched = 42;
    }
    printf("after scope\n");
}

Prints:

before scope
variable (42) goes out of scope
after scope

See here

Johannes Schaub - litb
  • 496,577
  • 130
  • 894
  • 1,212
15

One solution to bring RAII to C (when you don't have cleanup()) is to wrap your function call with code that will perform a cleanup. This can also be packaged in a tidy macro (shown at the end).

/* Publicly known method */
void SomeFunction() {
  /* Create raii object, which holds records of object pointers and a
     destruction method for that object (or null if not needed). */
  Raii raii;
  RaiiCreate(&raii);

  /* Call function implementation */
  SomeFunctionImpl(&raii);

  /* This method calls the destruction code for each object. */
  RaiiDestroyAll(&raii);
}

/* Hidden method that carries out implementation. */
void SomeFunctionImpl(Raii *raii) {
  MyStruct *object;
  MyStruct *eventually_destroyed_object;
  int *pretend_value;

  /* Create a MyStruct object, passing the destruction method for
     MyStruct objects. */
  object = RaiiAdd(raii, MyStructCreate(), MyStructDestroy);

  /* Create a MyStruct object (adding it to raii), which will later
     be removed before returning. */
  eventually_destroyed_object = RaiiAdd(raii,
      MyStructCreate(), MyStructDestroy);

  /* Create an int, passing a null destruction method. */
  pretend_value = RaiiAdd(raii, malloc(sizeof(int)), 0);

  /* ... implementation ... */

  /* Destroy object (calling destruction method). */
  RaiiDestroy(raii, eventually_destroyed_object);

  /* or ... */
  RaiiForgetAbout(raii, eventually_destroyed_object);
}

You can express all of the boiler plate code in SomeFunction with macros since it will be the same for every call.

For example:

/* Declares Matrix * MatrixMultiply(Matrix * first, Matrix * second, Network * network) */
RTN_RAII(Matrix *, MatrixMultiply, Matrix *, first, Matrix *, second, Network *, network, {
  Processor *processor = RaiiAdd(raii, ProcessorCreate(), ProcessorDestroy);
  Matrix *result = MatrixCreate();
  processor->multiply(result, first, second);
  return processor;
});

void SomeOtherCode(...) {
  /* ... */
  Matrix * result = MatrixMultiply(first, second, network);
  /* ... */
}

Note: you'd want to make use of an advanced macro framework such as P99 to make something like the above possible.

Keldon Alleyne
  • 2,103
  • 16
  • 23
  • 3
    Having to explicitly call a method (`RaiiDestroyAll`) kind of goes against the very idea of raii. – Mooing Duck Oct 24 '18 at 17:14
  • This is the mechanism through the language. If you want you can hide the explicit call with macro such as, `RTN_RAII(int, func_name, int, arg0, int, arg1, {/* code */})` (you can use P99 to do heavy lifting of macro). – Keldon Alleyne Oct 31 '18 at 14:33
  • 2
    https://en.cppreference.com/w/cpp/language/raii "Another name for this technique is Scope-Bound Resource Management (SBRM), after the basic use case where the lifetime of an RAII object ends due to scope exit." Bjarne Stroustrup said "RAII is a bad name for the concept... A better name is probably: Constructor Acquires, Destructor Releases" The point is that release is automatic, no matter what. The point is that you shouldn't make a cleanup call. That's the definition of RAII. I've heard that some C compilers offer something similar as an extension, but C itself can't do it. – Mooing Duck Oct 31 '18 at 17:06
  • 3
    Obviously C can't do automatic scoped based cleanup. From the question: "I assume it isn't possible in any sane way, but perhaps is it possible using some kind of dirty trick". What's I've provided is a mechanism as a workaround whereby you can get *managed* cleanup in C with the aid of a simple macro where you will not have to manually call destroy or the cleanup (cleanup will be done in the macro), but you will need to register the pointers. – Keldon Alleyne Oct 31 '18 at 19:09
  • 1
    Even C++ requires you to mark the end of the scope somehow, usually with a close brace. It seems to me that RaiiDestroyAll() is merely serving the same purpose, except that it is uncoupled from the closing brace. One could re-couple them with a macro, but I don't think that would make things nicer: it is something of the essence of C++ and C to respectively hide and not hide implementation details, so this seems to me to be a fitting approach (and not a dirty trick) to RAII in C. And separate brace and RAII scope lets you do things like e.g. using one RAII scope over several brace ones. – michaeljt Nov 23 '18 at 16:36
  • Follow-up to my last comment. A real-world example of what can be done when RAII-like memeory management is separated from the local stack-frame scope is the Linux kernel devm_kcalloc() and companion functions[1]. Might is make sense to mention this in the answer above? [1] https://www.mjmwired.net/kernel/Documentation/driver-model/devres.txt – michaeljt Nov 25 '18 at 14:05
9

If your compiler supports C99 (or even a substantial part of it) you can use a variable length array (VLA), such as:

int f(int x) { 
    int vla[x];

    // ...
}

If memory serves, gcc had/supported this feature well before it was added to C99. This is (roughly) equivalent to the simple case of:

int f(int x) { 
    int *vla=malloc(sizeof(int) *x);
    /* ... */
    free vla;
}

It does not, however, let you do any of the other things a dtor can do such as closing files, database connections, etc.

Jerry Coffin
  • 476,176
  • 80
  • 629
  • 1,111
  • 3
    Beware that 1) the stack is usually much more limited than the heap; 2) You basically cannot recover from a stack overflow (you will get a SIGSEGV, that you cannot handle). A failed malloc will return nullptr, while a failed new will throw std::bad_alloc. – Raúl Salinas-Monteagudo Feb 28 '18 at 12:13
  • @RaúlSalinas-Monteagudo what do you mean by "much more limited" ? – j b Mar 04 '20 at 09:04
  • 1
    @jb: Depending on the OS, the size of the stack is often limited to only a few megabytes or so. But he's only sort of half right. Just for example, on Linux a failed malloc (or new) will frequently result in the OOMKILLER running, which may simply kill program in question (or may kill other programs to free up enough memory to let the allocation succeed). As such, although the heap is often larger, trying to use more than it has available can also be unrecoverable. – Jerry Coffin Mar 05 '20 at 00:40
  • 1
    @jb: If I remember correctly, the stack size is (by default) 8 MB for a normal process and 2 MB for a thread. If you start creating arrays in the stack, you might well end up overflowing it. But that depends of course on the nature of your date and how deep you nest invocations. I would not go into such dangers for a reliable program. – Raúl Salinas-Monteagudo Mar 10 '20 at 08:07
  • 1
    @RaúlSalinas-Monteagudo good points. 8 MB (or even 2 MB) is actually quite a lot of memory for most applications and I must admit I've never run into any trouble. However, I just wrote a simple test program and was able to crash it instantly by allocating a 10 MB array on the stack. Will definitely bear this in mind for future. Thanks! – j b Mar 10 '20 at 08:41
  • It's fine as long as you don't work with files, but once you do, 8 MB is most of the time too little. – Luxalpa Feb 20 '22 at 10:58
4

Probably the easiest way is to use goto to jump to a label at the end of a function but that's probably too manual for the sort of thing you're looking at.

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
Jasper Bekkers
  • 6,711
  • 32
  • 46
1

To complement this part of Johannes's answer:

the cleanup attribute runs a function when a variable goes out of scope

There is a limitation on cleanup attribute (http://gcc.gnu.org/onlinedocs/gcc-4.0.4/gcc/Variable-Attributes.html): This attribute can only be applied to auto function scope variables.

So if there is a static variable in a file it is possible to implement RAII for a static variable in this way:

#include <stdio.h>
#include <stdlib.h>

static char* watched2;

__attribute__((constructor))
static void init_static_vars()
{
  printf("variable (%p) is initialazed, initial value (%p)\n", &watched2, watched2);
  watched2=malloc(1024);
}


__attribute__((destructor))
static void destroy_static_vars()
{
  printf("variable (%p), value( %p) goes out of scope\n", &watched2, watched2);
  free(watched2);
}

int main(void)
{
  printf("exit from main, variable (%p) value(%p) is static\n", &watched2, watched2);
  return 0;
}

This is a test:

>./example
variable (0x600aa0) is initialazed, initial value ((nil))
exit from main, variable (0x600aa0) value(0x16df010) is static
variable (0x600aa0), value( 0x16df010) goes out of scope
1

I'd opt for overwriting the return address on the stack. It'd work out as the most transparent. Replacing free will only work with heap-allocated "objects".

Roger Lipscombe
  • 89,048
  • 55
  • 235
  • 380
1

Have you looked at alloca()? It will free when an var leaves scope. But to use it effecticly the caller must always do the alloca before sending it to things... If you were implementing strdup, well, you can't use alloca.

nelix
  • 29
  • 1
  • 3
    `alloca()` isn't really pure C. It's not in any C standard and as such is not available everywhere C is available. For instance it is not available in Microsoft's C compilers for Windows. See the [C FAQ](http://c-faq.com/malloc/alloca.html). – hippietrail Mar 19 '13 at 02:17
  • 2
    It doesn't free when a variable leaves its scope. It frees when the function exits, which makes it very dangerous. – Bauss Jul 16 '17 at 22:53
0

Check https://github.com/psevon/exceptions-and-raii-in-c for a C implementation of unique and shared smartpointers and exceptions. This implementation relies on macro brackets BEGIN ... END replacing braces and detecting smartpointers going out of scope, as well as macro replacement for return.

0

I didn't know about attribute cleanup before. Certainly a neat solution where it's applicable, but it doesn't seem to behave well with setjmp/longjmp based exception implementations; the cleanup function is not called for any intermediate scopes/functions between the scope that threw the exception and the scope that catches it. Alloca doesn't have this problem, but with alloca you cannot transfer ownership of the memory chunk to an outer scope from the function that called it since the memory is allocated from the stack frame. It's possible to implement smartpointers somewhat akin to C++ unique_ptr and shared_ptr, thought it requires using macro brackets instead of {} and return to be able to associate extra logic to scope entry/exit. See autocleanup.c in https://github.com/psevon/exceptions-and-raii-in-c for an implementation.

0
my implementation of raii for c in pure c and minimal asm
@ https://github.com/smartmaster/sml_clang_raii

**RAII for C language in pure C and ASM**

**featurs : **

-easy and graceful to use
- no need seperate free cleanup functions
- able to cleanup any resources or call any function on scope exits


**User guide : **

-add source files in src folder to your project
-include sml_raii_clang.h in.c file
-annote resource and its cleanup functions

/* sample code */

void sml_raii_clang_test()
{
    //start a scope, the scope name can be any string
    SML_RAII_BLOCK_START(0);


    SML_RAII_VOLATILE(WCHAR*) resA000 = calloc(128, sizeof(WCHAR)); //allocate memory resource
    SML_RAII_START(0, resA000); //indicate starting a cleanup code fragment, here 'resA000' can be any string you want
    if (resA000) //cleanup code fragment
    {
        free(resA000);
        resA000 = NULL;
    }
    SML_RAII_END(0, resA000); //indicate end of a cleanup code fragment


    //another resource
    //////////////////////////////////////////////////////////////////////////
    SML_RAII_VOLATILE(WCHAR*) res8000 = calloc(128, sizeof(WCHAR));
    SML_RAII_START(0, D000);
    if (res8000)
    {
        free(res8000);
        res8000 = NULL;
    }
    SML_RAII_END(0, D000);


    //scope ended, will call all annoated cleanups
    SML_RAII_BLOCK_END(0);
    SML_RAII_LABEL(0, resA000); //if code is optimized, we have to put labels after SML_RAII_BLOCK_END
    SML_RAII_LABEL(0, D000);
}