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
The short version is:
- In a subdirectory of your project's directory, run
git clone https://github.com/mattgallagher/CwlPreconditionTesting.git
- Drag the "CwlPreconditionTesting.xcodeproj" file from the Finder into your project's file tree in Xcode
- Click on your project in the file tree to access project settings and click on the target to which you want to add CwlUtils.
- Click on the "Build Phases" tab and if you don't already have a "Copy Files" build phase with a "Destination: Frameworks", add one using the "+" in the top left of the tab.
- Still on the "Build Phases" tab, add "CwlPreconditionTesting.framework" to the "Copy Files, Destination: Frameworks" step. NOTE: there may be multiple "CwlPreconditionTesting.framework" files in the list, including one for macOS and one for iOS. You should select the "CwlPreconditionTesting.framework" that appears above the corresponding CwlPreconditionTesting macOS or iOS testing target.
- Optional step: Adding the "CwlPreconditionTesting.xcodeproj" file to your project's file tree will also add all of its schemes to your scheme list in Xcode. You can hide these from your scheme list from the menubar by selecting "Product" -> "Scheme" -> "Manage Schemes" (or typing Command-Shift-,) and unselecting the checkboxes in the "Show" column next to the CwlPreconditionTesting scheme names.
- In Swift files where you want to use CwlPreconditionTesting code, write
import CwlPreconditionTesting
at the top.
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.
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>
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)