How is catching Rust FFI panic possible?
Published:
How is it possible for C++ to catch panic from Rust function? Is it an intended behavior? Let me show it and prove it with details of source implementation!
Catching foreign exception
Exceptions are one of familiar control flow mechanisms for C++ developers. The implementation of exception handling is defined based on runtime ABI, which can be separated to two levels including base ABI and C++ ABI. Base ABI is language-independent, while C++ ABI can interact with C++ to get language-dependent information. In this part, I would like to introduce how C++ ABI define to throw exception and catch exception.
struct __cxa_exception {
std::type_info * exceptionType;
// ...
// set_terminate and set_unexpected
__cxa_exception * nextException;
int handlerCount;
// handler data during unwinding phase
// ...
_Unwind_Exception unwindHeader;
};
Take a brief view on C++ exception object above. C++ program would maintain an exception stack for each thread, and the each exception object in the stack is linked with the pointer nextException
. When C++ program starts throwing exception, the program would allocate memory for the thrown exception object first. Next, it will call __cxa_throw
to indirectly start unwinding process with implementation defined in base ABI _Unwind_RaiseException
. During the unwinding process, program would try to find the landing pad which is a part of user code to catch and handle the thrown exception. If matched landing pad for the exception object exists, control flow would be transferred to the landing pad, and ended with calling __cxa_end_catch
to delete exception object.
C++ developers can specify the type of exception the program wants to catch, but also can use catch-all block (e.g., catch(...)
to catch all types of exception if developers want to handle multiple types in a consistent way. However, when catch-all block is exposed to the foreign function interface, developers should also take more care of the user’s code in catch-all block because it is able to catch foreign exception. From the perspective of C++, foreign exception represents the exception object thrown by other programming languages, while native exception represents the exception thrown by C++ itself. It is also convenient to identify whether the exception is foreign or not, the language information will be stored in the exception_class
field of _Unwind_Exception
when exception is thrown. Take C++ exception object for example, the value of exception_class
could be GNUCC++
. Based on the implementation of __cxa_begin_catch
and __cxa_end_catch
, it is clear that foreign exception is intended to be caught by the catch-all block. For example, C++ will call _Unwind_DeleteException
to immediately delete foreign exception and return at the end of landing pad, while it will process and update the exception object when caught exception is native. The reason is that C++ can’t maintain the foreign exception and the native exception at the same time. Even though the foreign exception and native exception share the same data such as _Unwind_Exception
, the language dependent data could be inconsistent with foreign exception. When C++ tries to delete native exception object, it needs to find out next exception object by nextException
which can’t be found in foreign exception object. Therefore, when the program finds that foreign exception and native exception exist at the same time, the program will call abort right away. There are only two ways to handle foreign exception in catch-all block: The first way is to let C++ runtime help rethrow the foreign exception object, and this is the best way to continue finding the matched handler and stack unwinding. The second way is to stop the exception propagation by deleting the exception object; however, it is not recommended because it might mess up the exception count of the programming language who throws it. In the case that rust panic is caught by the C++ catch-all block, abort would be triggered by calling rust’s cleanup function __rust_drop_panic
when C++ doesn’t rethrow rust’s panic.
Sample code of Rust/C++
The code works on macOS.
After we build the dynamic-linked library of Rust, we can try to search
// given the command `nm liboob.dylib| grep ' T '`
...
000000000001bb7c T __ZN93_$LT$std..panicking..begin_panic_handler..StrPanicPayload$u20$as$u20$core..panic..BoxMeUp$GT$3get17he4a456fc7d67afa4E
000000000001bb38 T __ZN93_$LT$std..panicking..begin_panic_handler..StrPanicPayload$u20$as$u20$core..panic..BoxMeUp$GT$8take_box17h77ac41f158bcd267E
0000000000019a48 T __ZN95_$LT$std..path..Components$u20$as$u20$core..iter..traits..double_ended..DoubleEndedIterator$GT$9next_back17h02d204dfbd934912E
...
0000000000021d5c T ___rust_start_panic
0000000000002348 T _oob
000000000001b93c T _rust_begin_unwind
000000000001d87c T _rust_eh_personality
000000000001af40 T _rust_oom
000000000001bf10 T _rust_panic
Reference
Thanks for the guide of Using Unsafe for Fun and Profit