How Exception Handling Works in mulle-objc Runtime: Try-Catch-Finally Implementation

The mulle-objc runtime implements Objective-C exception handling through a universe-level vector table that dispatches try-catch-finally operations via setjmp/longjmp and function pointers stored in struct _mulle_objc_universe.

The mulle-objc runtime provides a low-level implementation of Objective-C exception handling that diverges from traditional unwinding-based approaches. Instead of using stack unwinding libraries like libunwind, the runtime manages control flow through explicit jump buffers and a configurable vector table stored in each universe. This design allows embedded and specialized environments to customize exception behavior while maintaining full compatibility with Objective-C syntax.

The Universe-Level Exception Vector Table

At the core of exception handling in mulle-objc runtime lies the exceptionvectors structure within struct _mulle_objc_universe. Defined in src/mulle-objc-universe-struct.h, this structure contains function pointers that dispatch the five essential operations required by compiler-generated exception code:

  • try_enter: Pushes a new catch buffer onto the thread-local exception stack
  • throw: Delivers an exception to the topmost catch buffer and performs a longjmp
  • try_exit: Pops the current catch buffer when leaving the @try scope
  • extract: Retrieves the exception object stored in the current catch buffer
  • match: Tests whether a caught exception matches a given class for @catch filtering

These vectors are initialized by _mulle_objc_universe_init_exception in src/mulle-objc-universe-exception.c, allowing the runtime to bootstrap the exception handling mechanism before any Objective-C code executes.

Entering a @try Block

When the compiler encounters a @try statement, it allocates a catch buffer (struct _mulle_objc_exceptionstackentry) on the stack. This structure contains a jmp_buf for saving execution context plus metadata fields. The runtime then calls mulle_objc_exception_tryenter in src/mulle-objc-try-catch-finally.c, which forwards to the universe's try_enter vector:

void mulle_objc_exception_tryenter(void *localExceptionData,
                                   mulle_objc_universeid_t universe)
{
    struct _mulle_objc_universe *universePtr =
        mulle_objc_global_get_universe_inline(universe);
    universePtr->exceptionvectors.try_enter(universePtr, localExceptionData);
}

The concrete implementation _mulle_objc_universe_tryenter in src/mulle-objc-universe-exception.c stores the previous stack top and clears the exception field in the buffer. The compiler pairs this call with setjmp(catchbuf.buf) to save the execution context before entering the protected code region.

Throwing Exceptions via the throw Vector

When @throw executes, mulle_objc_exception_throw resolves the target universe and invokes the throw vector:

void mulle_objc_exception_throw(void *exception,
                                mulle_objc_universeid_t universe)
{
    struct _mulle_objc_universe *universePtr =
        mulle_objc_global_get_universe_inline(universe);
    universePtr->exceptionvectors.throw(universePtr, exception);
}

The implementation _mulle_objc_universe_throw in src/mulle-objc-universe-exception.c retrieves the current catch buffer from thread-local storage, stores the exception object within it, pops the buffer from the stack, and performs a longjmp to transfer control back to the corresponding setjmp site established at the beginning of the @try block.

Catching and Class Matching

After the longjmp returns execution to the setjmp location, the runtime calls mulle_objc_exception_extract to retrieve the thrown object from the catch buffer. The compiler then uses mulle_objc_exception_match to determine if the exception matches any @catch clause:

void *mulle_objc_exception_extract(void *localExceptionData,
                                   mulle_objc_universeid_t universe);

The match operation forwards to the universe's match vector (_mulle_objc_universe_match_exception), which traverses the class hierarchy of the caught object to verify if it is an instance of the requested exception class. This enables polymorphic catch blocks where a @catch (NSException *e) will catch any subclass of NSException.

Exiting Scope and @finally Semantics

When control exits a @try block—whether normally, through a @catch handler, via a control flow statement, or during stack unwinding—the compiler emits a call to mulle_objc_exception_tryexit. This function invokes the try_exit vector to pop the current catch buffer from the thread-local exception stack, ensuring that nested exception handlers maintain correct nesting semantics.

The @finally block is handled entirely by the compiler front-end (clang/LLVM), not by the runtime vectors. The compiler generates code that executes the @finally block both after successful @try completion and after @catch handling, placing this logic between the catch handler and the mulle_objc_exception_tryexit call. The runtime guarantees that try_exit only executes after any finally-style cleanup completes.

Practical Example: Manual Try-Catch-Finally

The following example demonstrates the low-level API that the compiler uses to implement @try/@catch/@finally syntax:

#include "mulle-objc-try-catch-finally.h"
#include "mulle-objc-universe.h"
#include <setjmp.h>
#include <stdio.h>

int main(void)
{
    mulle_objc_universeid_t uni = mulle_objc_global_universeid();
    struct _mulle_objc_exceptionstackentry catchbuf;

    /* Enter try region */
    mulle_objc_exception_tryenter(&catchbuf, uni);
    if (setjmp(catchbuf.buf) == 0)
    {
        printf("Inside try block – about to throw\n");
        mulle_objc_exception_throw((void *)0xDEADBEEF, uni);
        printf("Never reached\n");
    }
    else
    {
        void *ex = mulle_objc_exception_extract(&catchbuf, uni);
        printf("Caught exception %p\n", ex);
    }

    /* Exit try region (where @finally would run) */
    mulle_objc_exception_tryexit(&catchbuf, uni);
    printf("After try/catch – cleanup done\n");
    return 0;
}

For class-specific catching, the runtime provides mulle_objc_exception_match to filter exception types against the class hierarchy:

#define MY_ERROR_CLASSID   (mulle_objc_global_classid("MyError"))
#define NS_EXCEPTION_CLASSID (MULLE_OBJC_CLASSID(0xa41284db))

void handle_exception(void *exception, mulle_objc_universeid_t uni)
{
    if (mulle_objc_exception_match(exception, uni, MY_ERROR_CLASSID))
        printf("Handled MyError\n");
    else if (mulle_objc_exception_match(exception, uni, NS_EXCEPTION_CLASSID))
        printf("Handled NSException\n");
    else
        printf("Unhandled exception %p\n", exception);
}

Summary

  • Exception handling in mulle-objc runtime operates through a configurable vector table (exceptionvectors) stored in each universe structure in src/mulle-objc-universe-struct.h
  • The try_enter and try_exit vectors manage a thread-local stack of jmp_buf catch buffers, with throw using longjmp for control transfer
  • Exception objects are stored in struct _mulle_objc_exceptionstackentry buffers and retrieved via mulle_objc_exception_extract
  • Class matching for @catch clauses uses the match vector in src/mulle-objc-universe-exception.c to walk the inheritance hierarchy
  • @finally semantics are implemented by the compiler, not the runtime, ensuring cleanup code executes before mulle_objc_exception_tryexit pops the exception frame

Frequently Asked Questions

How does mulle-objc runtime store exception state during a try block?

The runtime stores exception state in a struct _mulle_objc_exceptionstackentry allocated on the stack by the compiler. This structure contains a jmp_buf for saving the execution context and a field for the thrown exception object. When mulle_objc_exception_tryenter is called, the runtime links this buffer to the thread-local exception stack via the try_enter vector implemented in src/mulle-objc-universe-exception.c.

What mechanism does mulle-objc use instead of stack unwinding?

Rather than using libunwind or similar stack unwinding libraries, mulle-objc uses setjmp/longjmp for exception handling. When an exception is thrown, the throw vector (implemented in src/mulle-objc-universe-exception.c) performs a longjmp back to the setjmp site established at the beginning of the @try block. This approach provides deterministic control flow suitable for embedded systems and specialized environments.

How does the runtime determine if a caught exception matches a catch clause?

The runtime uses mulle_objc_exception_match to test whether the caught exception is an instance of the class specified in a @catch clause. This function forwards to the universe's match vector (_mulle_objc_universe_match_exception), which traverses the class hierarchy of the thrown object to check for inheritance relationships, enabling polymorphic exception catching.

Is @finally handled by the runtime exception vectors?

No, the @finally block is not handled by the runtime exception vectors. According to the mulle-objc implementation, the compiler (clang/LLVM) generates code to execute the @finally block both after normal @try completion and after @catch handling. The runtime only manages the try_exit vector to pop the exception frame after all finally-style cleanup has occurred.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →