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 stackthrow: Delivers an exception to the topmost catch buffer and performs alongjmptry_exit: Pops the current catch buffer when leaving the@tryscopeextract: Retrieves the exception object stored in the current catch buffermatch: Tests whether a caught exception matches a given class for@catchfiltering
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 insrc/mulle-objc-universe-struct.h - The
try_enterandtry_exitvectors manage a thread-local stack ofjmp_bufcatch buffers, withthrowusinglongjmpfor control transfer - Exception objects are stored in
struct _mulle_objc_exceptionstackentrybuffers and retrieved viamulle_objc_exception_extract - Class matching for
@catchclauses uses thematchvector insrc/mulle-objc-universe-exception.cto walk the inheritance hierarchy @finallysemantics are implemented by the compiler, not the runtime, ensuring cleanup code executes beforemulle_objc_exception_tryexitpops 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →