Two variants: Without helper https://steipete.wtf/posts/mac-catalyst-crash-hunt/ and then with InterposeKit https://github.com/steipete/InterposeKit
// | |
// Copyright © 2020 Peter Steinberger. MIT Licensed. | |
// | |
import Foundation | |
/// Text input can crash in Mac Catalyst due to a property not being atomic. | |
/// We swizzle the `documentState` property of `RTIInputSystemSession` to make it thread safe. | |
func installMacCatalystAppKitTextCrashFix() { | |
// RTIInputSystemSession is loaded later, so we will check a few times. | |
_dyld_register_func_for_add_image { _, _ in | |
DispatchQueue.main.async { | |
if fixMacCatalystInputSystemSessionRace() { | |
print("Successfully installed Mac Catalyst Text Input Race Fix.") | |
} | |
} | |
} | |
} | |
private var didInstallCrashFix = false | |
/// https://github.com/w0lfschild/macOS_headers/blob/master/macOS/PrivateFrameworks/RemoteTextInput/2399/RTIInputSystemSession.h | |
/// Sample crashers: | |
/// https://gist.github.com/steipete/504e79558d861211a3a9ff794e09c817 | |
/// https://gist.github.com/steipete/7a125b20cce461bf9a072dfacd805507 | |
private func fixMacCatalystInputSystemSessionRace() -> Bool { | |
guard let klass = NSClassFromString("RTIInputSystemSession") else { return false } | |
guard didInstallCrashFix == false else { return false } | |
var lockStore = os_unfair_lock() | |
let sel = NSSelectorFromString("documentState") | |
var origIMP : IMP? = nil | |
let newHandler: @convention(block) (AnyObject) -> AnyObject = { blockSelf in | |
typealias ClosureType = @convention(c) (AnyObject, Selector) -> AnyObject | |
let callableIMP = unsafeBitCast(origIMP, to: ClosureType.self) | |
os_unfair_lock_lock(&lockStore) | |
defer { | |
os_unfair_lock_unlock(&lockStore) | |
} | |
return callableIMP(blockSelf, sel) | |
} | |
guard let method = class_getInstanceMethod(klass, sel) else { return false } | |
origIMP = class_replaceMethod(klass, sel, imp_implementationWithBlock(newHandler), method_getTypeEncoding(method)) | |
let setSel = NSSelectorFromString("setDocumentState:") | |
var setOrigIMP : IMP? = nil | |
let newSetHandler: @convention(block) (AnyObject, AnyObject) -> Void = { blockSelf, newValue in | |
typealias ClosureType = @convention(c) (AnyObject, Selector, AnyObject) -> Void | |
let callableIMP = unsafeBitCast(setOrigIMP, to: ClosureType.self) | |
os_unfair_lock_lock(&lockStore) | |
callableIMP(blockSelf, sel, newValue) | |
os_unfair_lock_unlock(&lockStore) | |
} | |
guard let setMethod = class_getInstanceMethod(klass, setSel) else { return false } | |
setOrigIMP = class_replaceMethod(klass, setSel, imp_implementationWithBlock(newSetHandler), method_getTypeEncoding(setMethod)) | |
didInstallCrashFix = true | |
return origIMP != nil && setOrigIMP != nil | |
} |
// Version using InterposeKit: https://github.com/steipete/InterposeKit | |
private func fixMacCatalystInputSystemSessionRace() { | |
do { | |
// Better to separate this string for static analysis | |
try Interpose.whenAvailable(["RTIInput", "SystemSession"]) { | |
let lock = DispatchQueue(label: "com.steipete.document-state-hack") | |
try $0.hook("documentState", { store in { `self` in | |
lock.sync { | |
store((@convention(c) (AnyObject, Selector) -> AnyObject).self)(`self`, store.selector) | |
}} as @convention(block) (AnyObject) -> AnyObject}) | |
try $0.hook("setDocumentState:", { store in { `self`, newValue in | |
lock.sync { | |
store((@convention(c) (AnyObject, Selector, AnyObject) -> Void).self)(`self`, store.selector, newValue) | |
}} as @convention(block) (AnyObject, AnyObject) -> Void}) | |
} | |
} catch { | |
print("Failed to fix input system: \(error).") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment