CwlPreconditionTesting
A Mach exception handler, written in Swift and Objective-C, that allows EXC_BAD_INSTRUCTION
(as raised by Swift's assertionFailure
/preconditionFailure
/fatalError
) to be caught and tested.
For an extended discussion of this code, please see the Cocoa with Love article:
Partial functions in Swift, Part 2: Catching precondition failures
Usage
The short version is:
git clone https://github.com/mattgallagher/CwlPreconditionTesting.git
- drag the "CwlPreconditionTesting.xcodeproj" file into your project's file tree in Xcode
- go to your testing target's Build Phase settings and under "Target Dependencies" press the "+" button and select the relevant "CwlPreconditionTesting" target ("_iOS" or "_OSX", depending on your testing target's SDK)
- write
import CwlPreconditionTesting
at the top of any test file where you want to usecatchBadInstruction
(Swift should handle the linkage automatically when you do this) - use the
catchBadInstruction
function as shown in the CwlCatchBadInstructionTests.swift tests file
Project details
The "CwlPreconditionTesting.xcodeproj" contains two targets:
- CwlPreconditionTesting_OSX
- CwlPreconditionTesting_iOS
both build a framework named "CwlPreconditionTesting.framework". If you're linking manually, be certain to select the "CwlPreconditionTesting.framework" from the appropriate target.
Remember: the iOS build is useful only in the simulator. All Mach exception handling code will be conditionally excluded in any device build.
Static inclusion
Due to the complications associated with needing to call into and out of Objective-C, static inclusion in other projects is not a single file nor a quick drag and drop. There's at least 7 files and you'll need to add some project settings.
All of the following files:
- CwlCatchBadInstruction.swift
- CwlCatchBadInstruction.h
- CwlCatchBadInstruction.m
- CwlCatchException.swift
- CwlCatchException.h
- CwlCatchException.m
and either:
- $(SDKROOT)/usr/include/mach/mach_exc.defs
- mach_excServer.c
need to be added to the testing target for OS X projects or iOS projects, respectively.
Your target will also need to have the following macros defined in the "Apple LLVM - Preprocessing" → "Preprocessor Macros" build setting:
PRODUCT_NAME=$(PRODUCT_NAME)
This lets the Objective-C file generate the include directive for the autogenerated Swift header so it can call back into Swift during the Mach exception handler callbacks. This macro should stay in sync if you change the target name but if you do anything else in your project that changes the name of the autogenerated Swift header independent of the target name (or you want to add spaces or other command-line complications to the target name), you'll want to update "CwlCatchBadInstruction.m" directly with the correct include directive.
Additionally, you'll need a standard Objective-C "Bridging header" for your testing target and it will need to include the following import statements:
#if defined(__x86_64__)
#import <CwlPreconditionTesting/CwlCatchBadInstruction.h>
#endif
#import <CwlPreconditionTesting/CwlCatchException.h>
Using POSIX signals and setjmp/longjmp
For comparison or for anyone running this code on a platform without Mach exceptions or the Objective-C runtime, I've added a proof-of-concept implementation of catchBadInstruction
that uses a POSIX SIGILL sigaction
and setjmp
/longjmp
to perform the throw.
In Xcode, you can simply select the CwlPreconditionTesting_POSIX target (instead of the OSX or iOS targets). If you're building without Xcode: all you need is the CwlCatchBadInstructionPOSIX.swift file (compared to the Mach exception handler, the code is tiny doesn't have any weird Objective-C/MiG file dependencies).
Warning No. 1: on OS X, this approach can't be used when lldb is attached since lldb's Mach exception handler blocks the SIGILL from ever occurring (I've disabled the "Debug Executable" setting for the tests in Xcode - re-enable it to witness the problem).
Warning No. 2: if you're switching between the CwlPreconditionTesting_OSX and CwlPreconditionTesting_POSIX targets, Xcode (as of Xcode 7.2.1) will not detect the change and will not remove the old framework correctly so you'll need to clean your project otherwise the old framework will hang around.
Additional problems in decreasing severity include:
- the signal handler is whole process (rather than correctly scoped to the thread where the "catch" occurs)
- the signal handler doesn't deal with re-entrancy whereas the mach exception handler remains deterministic in the face of multiple fatal errors
- the signal handler overwrites the "red zone" which is technically frowned upon in signal handlers (although unlikely to cause problems here)