Yan's Personal Site

ryan@suchocki.co.uk PGP Key MØRSU CV GitLab GitHub LinkedIn Flickr

Context Managers in C
New
Monday 1 January 2024

Python's with statement syntax (and the associated "Context Manager" protocol) is a significant development in programming language design.

Like a try...finally block, the with syntax provides "automatic" invocation of clean-up actions in a way that respects the lexical structure of the program and is ambivalent to the "reason for leaving" each lexical scope. i.e.: in Python, once a with block has been entered, the corresponding context manager's __exit__() function will be called when control leaves that block regardless of whether a return/break/continue/ statement has been reached, an exception has been raised, or whether the end of the block has been reached "naturally".

Unlike a try...finally block, the with statement pattern allows the clean-up routine to be defined once (alongside the definitions of other types and functions relating to the resource) and then used repeatedly. By making the context manager's __enter__() function the only way of acquiring the resource, this absolves the "caller" of the responsibility for arranging the clean-up actions properly.

Equivalent behaviours and a similar level of source code modularity can be achieved in C++ using the RAII pattern (i.e. using destructors and objects with automatic storage duration appearing at the appropriate scope). The unique benefits of the with statement are that the presence of clean-up logic of some sort is declared explicitly at the call site, the point at which the clean-up will happen in is explicitly demarcated using the mandatory lexical block and the order in which nested contexts will be unwound is also explicitly declared by way of the lexical structure of the source code. In other words, it is the "structured programming" approach to resource clean-up.

Moreover, since the context manager pattern is realised by way of a special language construct and a convention for entry and exit function signatures (a "protocol" in Python terminology) it is possible to imagine a congruent C extension, even in the absence of any syntax-level object orientation.

What follows is a "toy" implementation of the context manager pattern in (non-standard) C. This is based around GCC's __cleanup__ attribute which, in combination with a plain scope block, provides the essential semantics. A simple variadic macro provides the with syntax (just about) and the rest is in the eye of the beholder.

#define with(type, name, args, ...) \
{ \
    type name __attribute__ ((__cleanup__(type ## _exit))) = type ## _enter args; \
    __VA_ARGS__ \
}

General Usage

Note that the "underlying resource" type may be a handle or a pointer.

typedef <underlying resource type> foo_context_t;

foo_context_t foo_context_t_enter(<entry params>)
{
    // Do some init

    return <underlying handle>;
}

void foo_context_t_exit(foo_context_t *self)
{
    // Do some clean-up
}

int main()
{
    with(foo_context_t, foo, (<entry args>),
    {
        // Make use of 'foo'
    })
}

Wrapping a resource with distinct open/close functions

typedef FILE *file_context_t;

static file_context_t file_context_t_enter(const char *fname)
{
    printf("Opened file\n");
    return fopen(fname, "r");
}

static void file_context_t_exit(file_context_t *self)
{
    fclose(*self);
    printf("Closed file\n");
}

Wrapping a resource with dynamically allocated memory

typedef char *str_context_t;

static str_context_t str_context_t_enter(const char *contents)
{
    str_context_t result = strdup(contents);
    printf("Allocated %p\n", result);
    return result;
}

static void str_context_t_exit(str_context_t *self)
{
    printf("Freed %p\n", *self);
    free(*self);
}

Bringing it all together

int main()
{
    with(str_context_t, hello_ctx, ("Hello"),
    {
        with(file_context_t, f, ("/dev/urandom"),
        {
            with(str_context_t, world_ctx, ("World"),
            {
                printf("%s %s!\n", hello_ctx, world_ctx);

                char n = getc(f);
                printf("A random number is: %d\n", n);

                if (n % 2)
                {
                    printf("Early return\n");
                    return 1;
                }
            })
        })
    })

    return 0;
}

Here is the output from on occasion when the early return was triggered and from an occasion when it was skipped:

Allocated 0x564853b8a2a0
Opened file
Allocated 0x564853b8a8b0
Hello World!
A random number is: 21
Early return
Freed 0x564853b8a8b0
Closed file
Freed 0x564853b8a2a0
Allocated 0x5642d9dd82a0
Opened file
Allocated 0x5642d9dd88b0
Hello World!
A random number is: 112
Freed 0x5642d9dd88b0
Closed file
Freed 0x5642d9dd82a0