About

So this stuff has bother me for days… How to catch NSException when invoke ObjC methods?

In Lisp, we could do error (condition) capture like this:

(handler-case (error your-error-condition)
  (some-other-error ()
    ...)
  (your-error-condition ()
    (format t "Haha")))

which, is like you do the following in ObjC:

@try {
  @throw YourErrorCondition
}
@catch (YourErrorCondition *e) {
  NSLog(@"Haha")
}

however, it's not trivial to implement such thing in coca… I gave up… Well, just kidding, if I just give up, you won't see this post. :p

Note: I was natively speaking Chinese. This blog post is written in English to share to other Lisp programmers on reddit. If you think my English is poor, use LLM to refine or translate it. :p

How I Fails: The First Try

Callback wrapped by @try and @catch

I posted my struggling experience in reddit: CFFI callback function in try-catch black is not working.

And here's its trivial ideas:

  • we want to capture the NSException, so we could just write a wrapper code:
    void coca_lisp_call_wrapper (void (*call)(void)) {
      @try {
        call();
      }
      @catch (NSException *e) {
        coca_lisp_exception_callback(e);
      }
    }
    
  • but if we pass CFFI callback as call arguments:
    (foreign-funcall "coca_lisp_call_wrapper" :pointer (callback invoke-error))
    

    the NSException is just not captured!

    And here's a minimum reproducable example if you are willing to try:

    Folded, Click me to expand.
    #import <Foundation/Foundation.h>
    
    void coca_lisp_call_wrapper (void (*call)(void)) {
      @try {
        call();
      }
      @catch (NSException *e) {
        NSLog(@"Safe... ");
      }
    }
    
    void coca_lisp_error_function () {
      NSArray *arr = @[@1, @2];
      arr[10];
    }
    
    int main (int argc, char** argv) {
      // this will capture NSException
      coca_lisp_call_wrapper(&coca_lisp_error_function);
    }
    

    if compiled and run the above code, it would return:

    clang -fobjc-arc -framework Foundation -o test test.m && ./test
    test.m:14:3: warning: container access result unused - container access should not be used for side effects [-Wunused-value]
       14 |   arr[10];
          |   ^~~~~~~
    1 warning generated.
    2025-12-27 21:20:02.399 test[96331:13873785] Safe...
        

    if compiled as a library and linked into lisp, you may write a callback function like this:

    (defcallback invoke-error :void ()
      (foreign-funcall "coca_lisp_error_function" :void))
    
    (foreign-funcall "coca_lisp_call_wrapper" :pointer (callback invoke-error))
    

    the NSException would not be catched…

    How to link automatically

    You could use the cffi-grovel, note that the online documentation is a little out-dated. And to work properly, you may need my CFFI-grovel patch.

Why this

According to silly LLM, they said that when you switched between Lisp, C, ObjC calling frame, the error capture boundary would be broken and so the program does not how to throw or capture the exception.

This is really a sad story.

A trick that solving the problem

Invoke IMP directly

So invoking CFFI Lisp callback is impossible, what if we directly pass the C function pointer?

(foreign-funcall "coca_lisp_call_wrapper"
                 :pointer (foreign-symbol-pointer "coca_lisp_error_function"))

this would work fine. The exception would be captured and so the ObjC runtime won't just panic and throw SIGABORT.

So want we want is a safe_objc_msgSend that does:

void safe_objc_msgSend (IMP imp, id self, SEL sel, ...) {
  @try {
    return imp(self, sel, ...);
  }
  @catch (NSException *e) {
    coca_throw_exception_to_lisp(e);
  }
}

va_list is not a trivial thing and __ASM__ is not portable

To implement ... in ObjC/C is difficult and using __ASM__ to shift registers to prepare for function calling is also not so trivial.

NOTE: i was not a professional C programmer (neither a professional Lisp programmer). So can't be sure the above is true.

register shifting

You might try this:

_coca_dispatch_imp:
  mov x9, x0    // imp
  mov x0, x1    // self
  mov x1, x2    // sel
  mov x2, x3    // arg1
  mov x3, x4    // arg2
  mov x4, x5    // arg3
  mov x5, x6    // arg4
  mov x6, x7    // arg5

  br x9         // imp(self, sel, ...)

with the new safe_objc_msgSend:

void safe_objc_msgSend (IMP imp, id self, SEL sel, ...) {
  @try {
    return _coca_dispatch_imp(imp, self, sel, ...);
  }
  @catch (NSException *e) {
    coca_throw_exception_to_lisp(e);
  }
}

I haven't try this so cannot ensure it's working properly.

LibFFI

So I guess I'll just use libffi to do this:

  • lisp: when in coca.objc, we could implement compile-objc-method-calling to generate ffi_cif
  • objc: the safe_objc_msgSend should be implemented like:
    void safe_objc_msgSend (ffi_cif *cif, IMP imp, void* retval, void** arg_values) {
      @try {
        return ffi_call(cif, FFI_FN(imp), retval, arg_values);
      }
      @catch (NSException *e) {
        coca_throw_exception(e);
      }
    }
    

So in theory, we could copy the code in cffi/libffi/funcall.lisp. The safe_objc_msgSend just works like ffi_call.

A minimum compilable test
#import  <Foundation/Foundation.h>
#include <ffi.h>

void coca_lisp_test_fail_function (unsigned int arg1, float arg2) {
  NSArray *arr = @[@1];
  arr[10];
}

void coca_objc_msgSend (ffi_cif *cif, IMP imp, void* retval, void** args) {
  @try {
    return ffi_call(cif, FFI_FN(imp), retval, args);
  }
  @catch (NSException *e) {
    NSLog(@"Safe");
  }
}

int main (int argc, char** argv) {
  ffi_cif cif;
  if (ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 0, &ffi_type_void, NULL) != FFI_OK) {
    NSLog(@"Failed to call ffi_pref_cif");
  }

  coca_objc_msgSend(&cif, FFI_FN(coca_lisp_test_fail_function), NULL, NULL);

  return 0;
}

the compilation result is like below:

clang -fobjc-arc -framework Foundation -lffi -I/Library/Developer/CommandLineTools/SDKs/MacOSX26.sdk/usr/include/ffi -o test test.m && ./test
test.m:14:3: warning: container access result unused - container access should not be used for side effects [-Wunused-value]
   14 |   arr[10];
      |   ^~~~~~~
1 warning generated.
2025-12-27 23:48:27.165 test[99720:14024008] Safe

Compilation finished at Sat Dec 27 23:48:27, duration 0.39 s

A word about performance

Yes, performace… Although I don't like to care about it (I write shit codes, really really shit codes), I think we could make a hole on the previous compile-objc-method-calling rules: if we got a objc_msgSend with no additional arguments other than id self and SEL sel, we could just call the simple wrapper function:

void safe_objc_msgSend_0 (IMP imp, id _self, SEL sel) {
  @try {
    return imp(_self, sel);
  }
  @catch (NSException *e) {
    coca_throw_exception(e);
  }
}

but this is just left here as it is. I don't think I'd like to implement it. Maybe you could generate a wrapper like CFFI-grovel, generate the ObjC wrapper code for every method of likely same type encoding, and doing JIT like things (compile and load the dylib into lisp image). I'd like to see such PR if you are willing to contribute.