Problems throwing and catching exceptions on OS X with -fno-rtti
c++, exception, macos, rtti, shared-libraries
Solution
Well, even though I have accepted an answer it did not solve all problems. So I'm writing down the solution which did work in the end.
I made a small tool for post-processing the object files and marking the local symbols as `UNDEF`. This forces the linker to use definitions from `libstdc++` and not local ones from the file. The basic approach of the tool is:
- load the Mach-O header
- walk the load commands and find the `LC_SYMTAB` command
- load the list of symbols (`struct nlist`) and the strings
- walk the symbols and look for those that we need (e.g. `__ZTISt9bad_alloc`)
- set the found symbols' type to `N_UNDF|N_EXT`.
- after processing, write the modified symbol table back to the file.
(I also made a similar implementation for ELF)
I post-process any file that's using std exceptions, either for throwing or for catching. To make sure the file list does not go stale, I added a post-link check for unwanted local symbols using `nm`.
This seems to resolve all the problems I've had so far.
Problem
The issue is somewhat similar to this question but the accepted answer does not really propose a solution or workaround. In our project, we have a dylib and the main executalble. The dylib is compiled with `-fno-rtti`, while the executable does use RTTI. The problem happens when an exception (e.g. `std::bad_alloc`) is thrown from the dylib and is caught in the exe. (Before you yell "exceptions need RTTI so you must have it on!", please note that the RTTI necessary for exceptions is always generated regardless of the `-frtti` or `-fno-rtti` setting. This is actually documented in the `-fno-rtti` flag description. The issue on OS X is that it's not generated in the same way) After some investigation, the following was discovered: - In the dylib (`-fno-rtti`), there is a local copy of the exception's RTTI structures; in particular, the `__ZTISt9bad_alloc` symbol (`typeinfo for std::bad_alloc`). - The exe (`-frtti`) imports the typeinfo symbol from `libstdc++.6.dylib` and does not have a local copy Since the exception handling code relies on comparing typeinfo pointers to determine exception match, the matching fails, and only the `catch(...)` succeeds. So far I see the following options: 1) compile everything, or at least the files that throw and catch exceptions, with `-frtti`. This is doable but I don't like the idea of generating RTTI for everything even if we don't use it; and the list of files which work with exceptions is prone to get stale. 2) when linking the dylib, somehow make the linker throw away the weak exception definition from the object file and use the one from `libstdc++.6.dylib`. So far I was not successful. 3) ??? I made a small test illustrating the problem. ``` --- throw.cpp --- #include <iostream> #if defined(__GNUC__) #define EXPORT __attribute__((visibility("default"))) #else #define EXPORT __declspec(dllexport) #endif EXPORT void dothrow () { std::cout << "before throw" << std::endl; throw std::bad_alloc(); } --- main.cpp --- #include <stdio.h> #include <iostream> #if defined(__GNUC__) #define IMPORT extern #else #define IMPORT __declspec(dllimport) #endif IMPORT void dothrow (); int main (void) { try { std::cout << "trying lib->main exception" << std::endl; dothrow (); } catch ( const std::bad_alloc& ) { std::cout << "caught bad_alloc in main - good." << std::endl; } catch (...) { std::cout << "caught ... in main - bad!" << std::endl; } } --- makefile --- # for main exe CFLAGS_RTTI=-m32 -frtti -fvisibility=hidden -fvisibility-inlines-hidden -shared-libgcc -funwind-tables # for dylib CFLAGS_NORTTI=-m32 -fno-rtti -fvisibility=hidden -fvisibility-inlines-hidden -shared-libgcc # for linking; some switches which don't help CLFLAGS=-Wl,-why_live,-warn_commons,-weak_reference_mismatches,error,-commons,error all: test test: libThrow.dylib main.o g++ $(CFLAGS_RTTI) -o test main.o -lthrow -L./ $(CLFLAGS) main.o: main.cpp g++ $(CFLAGS_RTTI) -c -o main.o main.cpp throw.o: throw.cpp g++ $(CFLAGS_NORTTI) -c -o throw.o throw.cpp libThrow.dylib: throw.o g++ $(CFLAGS_NORTTI) -dynamiclib -o libThrow.dylib throw.o clean: rm test main.o throw.o ``` Running: ``` $ ./test trying lib->main exception before throw caught ... in main - bad! ``` Symbols of the files involved: ``` $ nm -m throw.o | grep bad_alloc 000001be (__TEXT,__textcoal_nt) weak private external __ZNSt9bad_allocC1Ev 000001be (__TEXT,__textcoal_nt) weak private external __ZNSt9bad_allocC1Ev 00000300 (__TEXT,__eh_frame) weak private external __ZNSt9bad_allocC1Ev.eh (undefined) external __ZNSt9bad_allocD1Ev 00000290 (__DATA,__const_coal) weak external __ZTISt9bad_alloc 000002a4 (__TEXT,__const_coal) weak external __ZTSSt9bad_alloc (undefined) external __ZTVSt9bad_alloc $ nm -m libThrow.dylib | grep bad_alloc 00000ce6 (__TEXT,__text) non-external __ZNSt9bad_allocC1Ev (undefined) external __ZNSt9bad_allocD1Ev (from libstdc++) 00001050 (__DATA,__const) weak external __ZTISt9bad_alloc 00000e05 (__TEXT,__const) weak external __ZTSSt9bad_alloc (undefined) external __ZTVSt9bad_alloc (from libstdc++) $ nm -m main.o | grep bad_alloc (undefined) external __ZTISt9bad_alloc $ nm -m test | grep bad_alloc (undefined) external __ZTISt9bad_alloc (from libstdc++) ``` Note: similar compilation options on Linux and Windows works fine. I can throw exceptions from a shared object/dll and catch them in the main exe, even if they're compiled with different `-frtti`/`-fno-rtti` options. EDIT: here's how I ended up solving it for the specific case of `bad_alloc`: ``` #if defined(__GLIBCXX__) || defined(_LIBCPP_VERSION) #define throw_nomem std::__throw_bad_alloc #else #define throw_nomem throw std::bad_alloc #endif EXPORT void dothrow () { std::cout << "before throw" << std::endl; throw_nomem(); } ``` The function `__throw_bad_alloc` is imported from `libstdc++.6.dylib` and so always throws a correct type.