ObjC [1.5] NSException (EN)
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
callarguments:(foreign-funcall "coca_lisp_call_wrapper" :pointer (callback invoke-error))
the
NSExceptionis 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 implementcompile-objc-method-callingto generateffi_cif - objc: the
safe_objc_msgSendshould 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.