Skip to content

Commit

Permalink
Merge pull request #472 from Quick/update-CwlPreconditionTesting
Browse files Browse the repository at this point in the history
Update CwlPreconditionTesting
  • Loading branch information
ikesyo committed Nov 18, 2017
2 parents 1b135a5 + 50458a4 commit 72916d2
Show file tree
Hide file tree
Showing 29 changed files with 385 additions and 311 deletions.
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@ Nimble.framework.zip
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts

Carthage/Checkouts/**/*.*
Carthage/Checkouts/**/Cart*
Carthage/Checkouts/**/Tests
!Carthage/Checkouts/**/LICENSE.*
!Carthage/Checkouts/**/*.md
!Carthage/Checkouts/**/*.swift
!Carthage/Checkouts/**/*.h
!Carthage/Checkouts/**/*.m
!Carthage/Checkouts/**/*.c
Carthage/Build

# Swift Package Manager
Expand Down
2 changes: 1 addition & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ included:
- Tests

excluded:
- Sources/Lib
- Carthage/Checkouts

trailing_comma:
mandatory_comma: true
Expand Down
1 change: 1 addition & 0 deletions Cartfile.private
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
github "mattgallagher/CwlPreconditionTesting" "1.1.0-beta.12"
2 changes: 2 additions & 0 deletions Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github "mattgallagher/CwlCatchException" "b14c111e9b33cd142bd4bc75c482cfd5c3490923"
github "mattgallagher/CwlPreconditionTesting" "1.1.0-beta.12"
13 changes: 13 additions & 0 deletions Carthage/Checkouts/CwlCatchException/LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Copyright © 2017 Matt Gallagher ( http://cocoawithlove.com ). All rights reserved.

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
9 changes: 9 additions & 0 deletions Carthage/Checkouts/CwlCatchException/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import PackageDescription

let package = Package(
name: "CwlCatchException",
targets: [
Target(name: "CwlCatchException", dependencies: ["CwlCatchExceptionSupport"]),
Target(name: "CwlCatchExceptionSupport")
]
)
46 changes: 46 additions & 0 deletions Carthage/Checkouts/CwlCatchException/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# CwlCatchException
A simple Swift wrapper around an Objective-C `@try`/`@catch` statement that selectively catches Objective-C exceptions by `NSException` subtype, rethrowing if any caught exception is not the expected subtype.

Look at [CwlCatchExceptionTests.swift](https://github.com/mattgallagher/CwlCatchException/blob/master/CwlCatchExceptionTests/CwlCatchExceptionTests.swift?ts=4) for syntax.

## Adding to your project

This project can be used by direct inclusion in your projects or through any of the Swift Package Manager, CocoaPods or Carthage.

Minimum requirements are iOS 8 or macOS 10.9.

### Manual inclusion

1. In a subdirectory of your project's directory, run `git clone https://github.com/mattgallagher/CwlCatchException.git`
2. Drag the "CwlCatchException.xcodeproj" file from the Finder into your own project's file tree in Xcode
3. Add the "CwlCatchException.framework" from the "Products" folder of the CwlCatchException project's file tree to the "Copy Files (Frameworks)" build phases of any target that you want to include this module.

That third step is a little tricky if you're unfamiliar with Xcode but it involves:

a. click on your project in the file tree
b. click on the target to whih you want to add this module
c. select the "Build Phases" tab
d. if you don't already have a "Copy File" build phase with a "Destination: Frameworks", add one using the "+" button in the top left of the tab
e. click the "+" within the "Copy File (Frameworks)" phase and from the list that appears, select the "CwlCatchException.framework" (if there are multiple frameworks with the same name, look for the one that appears *above* the corresponding macOS or iOS CwlCatchException testing target).

### Swift Package Manager

Add the following to the `dependencies` array in your "Package.swift" file:

.Package(url: "https://github.com/mattgallagher/CwlCatchException.git", majorVersion: 1),

Or, if you're using the `swift-tools-version:4.0` package manager, add the following to the `dependencies` array in your "Package.swift" file:

.package(url: "https://github.com/mattgallagher/CwlCatchException.git", majorVersion: 1)

### CocoaPods

Add the following to your target in your "Podfile":

pod 'CwlCatchException', :git => 'https://github.com/mattgallagher/CwlCatchException.git'

### Carthage

Add the following line to your Cartfile:

git "https://github.com/mattgallagher/CwlCatchException.git" "master"
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ import Foundation
import CwlCatchExceptionSupport
#endif

private func catchReturnTypeConverter<T: NSException>(_ instance: T, block: () -> Void) -> T? {
private func catchReturnTypeConverter<T: NSException>(_ type: T.Type, block: () -> Void) -> T? {
// Get the type from an *instance*, instead of a receiving the type directly
return catchExceptionOfKind(T.self, block) as? T
return catchExceptionOfKind(type, block) as? T
}

extension NSException {
public static func catchException(in block: () -> Void) -> Self? {
// Use a dummy instance of Self to provide the type
return catchReturnTypeConverter(self.init(), block: block)
return catchReturnTypeConverter(self, block: block)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,3 @@ FOUNDATION_EXPORT const unsigned char CwlCatchExceptionVersionString[];
__attribute__((visibility("hidden")))
#endif
NSException* __nullable catchExceptionOfKind(Class __nonnull type, __attribute__((noescape)) void (^ __nonnull inBlock)(void));

13 changes: 13 additions & 0 deletions Carthage/Checkouts/CwlPreconditionTesting/LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Copyright © 2017 Matt Gallagher ( http://cocoawithlove.com ). All rights reserved.

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19 changes: 19 additions & 0 deletions Carthage/Checkouts/CwlPreconditionTesting/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import PackageDescription

let package = Package(
name: "CwlPreconditionTesting",
targets: [
Target(name: "CwlPreconditionTesting", dependencies: [
"CwlMachBadInstructionHandler"
]),
Target(name: "CwlMachBadInstructionHandler")
],
dependencies: [
.Package(url: "https://github.com/mattgallagher/CwlCatchException.git", Version(1, 0, 2, prereleaseIdentifiers: ["beta", "3"])),
],
exclude: [
"Sources/CwlPreconditionTesting/Mach/CwlPreconditionTesting.h",
"Sources/CwlPreconditionTesting/Posix/CwlPreconditionTesting.h",
"Sources/CwlPreconditionTesting/CwlCatchBadInstructionPosix.swift",
]
)
73 changes: 73 additions & 0 deletions Carthage/Checkouts/CwlPreconditionTesting/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# 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.

NOTE: the iOS code runs in the simulator *only*. It is for logic testing and cannot be deployed to the device due to the Mach exception API being private on iOS.

For an extended discussion of this code, please see the Cocoa with Love article:

[Partial functions in Swift, Part 2: Catching precondition failures](http://cocoawithlove.com/blog/2016/02/02/partial-functions-part-two-catching-precondition-failures.html)

## Adding to your project

This project can be used by manual inclusion in your projects or through any of the Swift Package Manager, CocoaPods or Carthage.

Minimum requirements are iOS 8 (simulator-only) or macOS 10.9. The project includes tvOS 9 and POSIX targets but these aren't regularly tested.

### Manual inclusion

1. In a subdirectory of your project's directory, run `git clone https://github.com/mattgallagher/CwlPreconditionTesting.git`
2. Drag the "CwlPreconditionTesting.xcodeproj" file from the Finder into your own project's file tree in Xcode
3. Add the "CwlPreconditionTesting.framework" from the "Products" folder of the CwlPreconditionTesting project's file tree to the "Copy Files (Frameworks)" build phases of any target that you want to include this module.
4. Drag the "CwlCatchException.framework" from the "Dependencies" group (within the CwlPreconditionTesting project's file tree) onto the same "Copy Files (Frameworks)" build phase (this item may be red but that shouldn't be a problem).

That third step is a little tricky if you're unfamiliar with Xcode but it involves:

a. click on your project in the file tree
b. click on the target to whih you want to add this module
c. select the "Build Phases" tab
d. if you don't already have a "Copy File" build phase with a "Destination: Frameworks", add one using the "+" button in the top left of the tab
e. click the "+" within the "Copy File (Frameworks)" phase and from the list that appears, select the "CwlPreconditionTesting.framework" (if there are multiple frameworks with the same name, look for the one that appears *above* the corresponding macOS or iOS CwlPreconditionTesting testing target).

When building using this approach, the "FetchDependencies" target will use the Swift Package Manager to download the "CwlCatchException" project from github. The download is stored in the "Build intermediates" directory for your project. Normally, you can ignore its existence but if you get any errors from the "FetchDependencies" target, you might need to clean the build folder (Hold "Option" key while selecting "Product" &rarr; "Clean Build Folder..." from the Xcode menubar).

You can use the "Package.swift" to manage the behavior of the Swift Package Manager or if you want to download dependencies manually (instead of using this behind-the-scenes use of the Swift package manager), you should delete the "FetchDependencies" target and replace the "CwlCatchException" targets with alternatives that build the dependencies in accordance with your manual download.

### Swift Package Manager

Add the following to the `dependencies` array in your "Package.swift" file:

.Package(url: "https://github.com/mattgallagher/CwlPreconditionTesting.git", majorVersion: 1),

Or, if you're using the `swift-tools-version:4.0` package manager, add the following to the `dependencies` array in your "Package.swift" file:

.package(url: "https://github.com/mattgallagher/CwlPreconditionTesting.git", majorVersion: 1)

### CocoaPods

Add the following lines to your target in your "Podfile":

pod 'CwlPreconditionTesting', :git => 'https://github.com/mattgallagher/CwlPreconditionTesting.git'
pod 'CwlCatchException', :git => 'https://github.com/mattgallagher/CwlCatchException.git'

### Carthage

Add the following line to your Cartfile:

git "https://github.com/mattgallagher/CwlPreconditionTesting.git" "master"

## 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](https://en.wikipedia.org/wiki/Red_zone_(computing))" which is technically frowned upon in signal handlers (although unlikely to cause problems here)
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// CwlMachBadExceptionHandler.h
// CwlMachBadInstructionHandler.h
// CwlPreconditionTesting
//
// Created by Matt Gallagher on 2016/01/10.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import Foundation

#if SWIFT_PACKAGE
import CwlCatchException
import CwlMachBadInstructionHandler
#endif

Expand All @@ -38,16 +37,6 @@ import Foundation
}
}

extension execTypesCountTuple {
mutating func pointer<R>(in block: (UnsafeMutablePointer<T>) -> R) -> R {
return withUnsafeMutablePointer(to: &self) { p -> R in
return p.withMemoryRebound(to: T.self, capacity: EXC_TYPES_COUNT) { ptr -> R in
return block(ptr)
}
}
}
}

extension request_mach_exception_raise_t {
mutating func withMsgHeaderPointer<R>(in block: (UnsafeMutablePointer<mach_msg_header_t>) -> R) -> R {
return withUnsafeMutablePointer(to: &self) { p -> R in
Expand Down Expand Up @@ -78,17 +67,23 @@ import Foundation
var currentExceptionPort: mach_port_t = 0
var handlerThread: pthread_t? = nil

mutating func withUnsafeMutablePointers<R>(in block: (UnsafeMutablePointer<exception_mask_t>, UnsafeMutablePointer<mach_port_t>, UnsafeMutablePointer<exception_behavior_t>, UnsafeMutablePointer<thread_state_flavor_t>) -> R) -> R {
return masks.pointer { masksPtr in
return ports.pointer { portsPtr in
return behaviors.pointer { behaviorsPtr in
return flavors.pointer { flavorsPtr in
return block(masksPtr, portsPtr, behaviorsPtr, flavorsPtr)
static func internalMutablePointers<R>(_ m: UnsafeMutablePointer<execTypesCountTuple<exception_mask_t>>, _ c: UnsafeMutablePointer<mach_msg_type_number_t>, _ p: UnsafeMutablePointer<execTypesCountTuple<mach_port_t>>, _ b: UnsafeMutablePointer<execTypesCountTuple<exception_behavior_t>>, _ f: UnsafeMutablePointer<execTypesCountTuple<thread_state_flavor_t>>, _ block: (UnsafeMutablePointer<exception_mask_t>, UnsafeMutablePointer<mach_msg_type_number_t>, UnsafeMutablePointer<mach_port_t>, UnsafeMutablePointer<exception_behavior_t>, UnsafeMutablePointer<thread_state_flavor_t>) -> R) -> R {
return m.withMemoryRebound(to: exception_mask_t.self, capacity: 1) { masksPtr in
return c.withMemoryRebound(to: mach_msg_type_number_t.self, capacity: 1) { countPtr in
return p.withMemoryRebound(to: mach_port_t.self, capacity: 1) { portsPtr in
return b.withMemoryRebound(to: exception_behavior_t.self, capacity: 1) { behaviorsPtr in
return f.withMemoryRebound(to: thread_state_flavor_t.self, capacity: 1) { flavorsPtr in
return block(masksPtr, countPtr, portsPtr, behaviorsPtr, flavorsPtr)
}
}
}
}
}
}

mutating func withUnsafeMutablePointers<R>(in block: @escaping (UnsafeMutablePointer<exception_mask_t>, UnsafeMutablePointer<mach_msg_type_number_t>, UnsafeMutablePointer<mach_port_t>, UnsafeMutablePointer<exception_behavior_t>, UnsafeMutablePointer<thread_state_flavor_t>) -> R) -> R {
return MachContext.internalMutablePointers(&masks, &count, &ports, &behaviors, &flavors, block)
}
}

/// A function for receiving mach messages and parsing the first with mach_exc_server (and if any others are received, throwing them away).
Expand Down Expand Up @@ -121,7 +116,7 @@ import Foundation

handledfirstException = true
} else {
// If multiple fatal errors occur, don't handle subsequent errors (let the program crash)
// If multiple fatal errors occur, don't handle subsquent errors (let the program crash)
reply.RetCode = KERN_FAILURE
}

Expand Down Expand Up @@ -170,14 +165,15 @@ import Foundation
mach_port_insert_right(mach_task_self_, context.currentExceptionPort, context.currentExceptionPort, MACH_MSG_TYPE_MAKE_SEND)
}

try kernCheck { context.withUnsafeMutablePointers { masksPtr, portsPtr, behaviorsPtr, flavorsPtr in
let currentExceptionPtr = context.currentExceptionPort
try kernCheck { context.withUnsafeMutablePointers { masksPtr, countPtr, portsPtr, behaviorsPtr, flavorsPtr in
// 3. Apply the mach port as the handler for this thread
thread_swap_exception_ports(mach_thread_self(), EXC_MASK_BAD_INSTRUCTION, context.currentExceptionPort, Int32(bitPattern: UInt32(EXCEPTION_STATE) | MACH_EXCEPTION_CODES), x86_THREAD_STATE64, masksPtr, &context.count, portsPtr, behaviorsPtr, flavorsPtr)
thread_swap_exception_ports(mach_thread_self(), EXC_MASK_BAD_INSTRUCTION, currentExceptionPtr, Int32(bitPattern: UInt32(EXCEPTION_STATE) | MACH_EXCEPTION_CODES), x86_THREAD_STATE64, masksPtr, countPtr, portsPtr, behaviorsPtr, flavorsPtr)
} }

defer { context.withUnsafeMutablePointers { masksPtr, portsPtr, behaviorsPtr, flavorsPtr in
defer { context.withUnsafeMutablePointers { masksPtr, countPtr, portsPtr, behaviorsPtr, flavorsPtr in
// 6. Unapply the mach port
_ = thread_swap_exception_ports(mach_thread_self(), EXC_MASK_BAD_INSTRUCTION, 0, EXCEPTION_DEFAULT, THREAD_STATE_NONE, masksPtr, &context.count, portsPtr, behaviorsPtr, flavorsPtr)
_ = thread_swap_exception_ports(mach_thread_self(), EXC_MASK_BAD_INSTRUCTION, 0, EXCEPTION_DEFAULT, THREAD_STATE_NONE, masksPtr, countPtr, portsPtr, behaviorsPtr, flavorsPtr)
} }

try withUnsafeMutablePointer(to: &context) { c throws in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
#import <Foundation/Foundation.h>

//! Project version number for CwlUtils.
FOUNDATION_EXPORT double CwlPreconditionTestingVersionNumber;
FOUNDATION_EXPORT double CwlPreconditionTesting_POSIXVersionNumber;

//! Project version string for CwlUtils.
FOUNDATION_EXPORT const unsigned char CwlAssertingTestingVersionString[];
FOUNDATION_EXPORT const unsigned char CwlAssertingTesting_POSIXVersionString[];
Loading

0 comments on commit 72916d2

Please sign in to comment.