Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 7 additions & 13 deletions Sources/ITKSuperBuilder/include/ITKSuperBuilder.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#if __APPLE__
#import <Foundation/Foundation.h>
#if !(defined(__APPLE__) && (defined(__arm64__) || defined(__x86_64__)))
#error "[InterposeKit] Supported only on Apple platforms with arm64 or x86_64 architecture."
#endif

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

/**
Expand Down Expand Up @@ -44,7 +46,7 @@ There are a few important details:

@see https://steipete.com/posts/calling-super-at-runtime/
*/
@interface ITKSuperBuilder : NSObject
@interface ITKSuperBuilder: NSObject

/// Adds an empty super implementation instance method to originalClass.
/// If a method already exists, this will return NO and a descriptive error message.
Expand All @@ -53,22 +55,14 @@ There are a few important details:
error:(NSError **)error;

/// Check if the instance method in `originalClass` is a super trampoline.
+ (BOOL)isSuperTrampolineForClass:(Class)originalClass selector:(SEL)selector;

/// x86-64 and ARM64 are currently supported.
@property(class, readonly) BOOL isSupportedArchitecture;

#if (defined (__arm64__) || defined (__x86_64__)) && __APPLE__
/// Helper that does not exist if architecture is not supported.
+ (BOOL)isCompileTimeSupportedArchitecture;
#endif
+ (BOOL)isSuperTrampolineForClass:(Class)originalClass
selector:(SEL)selector;

@end

NSString *const ITKSuperBuilderErrorDomain;

typedef NS_ERROR_ENUM(ITKSuperBuilderErrorDomain, ITKSuperBuilderErrorCode) {
SuperBuilderErrorCodeArchitectureNotSupported,
SuperBuilderErrorCodeNoSuperClass,
SuperBuilderErrorCodeNoDynamicallyDispatchedMethodAvailable,
SuperBuilderErrorCodeFailedToAddMethod
Expand Down
172 changes: 88 additions & 84 deletions Sources/ITKSuperBuilder/src/ITKSuperBuilder.m
Original file line number Diff line number Diff line change
@@ -1,97 +1,74 @@
#if __APPLE__
#import "ITKSuperBuilder.h"

@import ObjectiveC.message;
@import ObjectiveC.runtime;

NS_ASSUME_NONNULL_BEGIN

NSString *const ITKSuperBuilderErrorDomain = @"com.steipete.InterposeKit";

void msgSendSuperTrampoline(void);
void msgSendSuperStretTrampoline(void);

#define let const __auto_type
#define var __auto_type

static IMP ITKGetTrampolineForTypeEncoding(__unused const char *typeEncoding) {
BOOL requiresStructDispatch = NO;
#if defined (__arm64__)
// ARM64 doesn't use stret dispatch. Yay!
#elif defined (__x86_64__)
// On x86-64, stret dispatch is ~used whenever return type doesn't fit into two registers
//
// http://www.sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html
// x86_64 is more complicated, including rules for returning floating-point struct fields in FPU registers, and ppc64's rules and exceptions will make your head spin. The gory details are documented in the Mac OS X ABI Guide, though as usual if the documentation and the compiler disagree then the documentation is wrong.
NSUInteger returnTypeActualSize = 0;
NSGetSizeAndAlignment(typeEncoding, &returnTypeActualSize, NULL);
requiresStructDispatch = returnTypeActualSize > (sizeof(void *) * 2);
#else
// Unknown architecture
// https://devblogs.microsoft.com/xamarin/apple-new-processor-architecture/
// watchOS uses arm64_32 since series 4, before armv7k. watch Simulator uses i386.
// See ILP32: http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dai0490a/ar01s01.html
#endif
NS_ASSUME_NONNULL_BEGIN

return requiresStructDispatch ? (IMP)msgSendSuperStretTrampoline : (IMP)msgSendSuperTrampoline;
}
NSString *const ITKSuperBuilderErrorDomain = @"com.steipete.InterposeKit";

#define ERROR_AND_RETURN(CODE, STRING)\
if (error) { *error = [NSError errorWithDomain:ITKSuperBuilderErrorDomain code:CODE userInfo:@{NSLocalizedDescriptionKey: STRING}];} return NO;
static IMP ITKGetTrampolineForTypeEncoding(__unused const char *typeEncoding);
static BOOL ITKMethodIsSuperTrampoline(Method method);

@implementation ITKSuperBuilder

+ (BOOL)isSupportedArchitecture {
#if defined (__arm64__) || defined (__x86_64__)
return YES;
#else
return NO;
#endif
}

#if defined (__arm64__) || defined (__x86_64__)
+ (BOOL)isCompileTimeSupportedArchitecture {
return [self isSupportedArchitecture];
}
#endif

+ (BOOL)isSuperTrampolineForClass:(Class)originalClass selector:(SEL)selector {
// No architecture check needed - will just be NO.
let method = class_getInstanceMethod(originalClass, selector);
return ITKMethodIsSuperTrampoline(method);
}

+ (BOOL)addSuperInstanceMethodToClass:(Class)originalClass selector:(SEL)selector error:(NSError **)error {
if (!self.isSupportedArchitecture) {
let msg = @"Unsupported Architecture. (Support includes ARM64 and x86-64 )";
ERROR_AND_RETURN(SuperBuilderErrorCodeArchitectureNotSupported, msg)
}

+ (BOOL)addSuperInstanceMethodToClass:(Class)originalClass
selector:(SEL)selector
error:(NSError **)error
{
// Check that class has a superclass
let superClass = class_getSuperclass(originalClass);
if (superClass == nil) {
let msg = [NSString stringWithFormat:@"Unable to find superclass for %@", NSStringFromClass(originalClass)];
ERROR_AND_RETURN(SuperBuilderErrorCodeNoSuperClass, msg)
let superclass = class_getSuperclass(originalClass);

if (superclass == nil) {
if (error) {
let message = [NSString stringWithFormat:@"Unable to find superclass for %@", NSStringFromClass(originalClass)];
*error = [NSError errorWithDomain:ITKSuperBuilderErrorDomain
code:SuperBuilderErrorCodeNoSuperClass
userInfo:@{NSLocalizedDescriptionKey: message}];
return NO;
}
}

// Fetch method called with super
let method = class_getInstanceMethod(superClass, selector);
let method = class_getInstanceMethod(superclass, selector);
if (method == NULL) {
let msg = [NSString stringWithFormat:@"No dynamically dispatched method with selector %@ is available on any of the superclasses of %@", NSStringFromSelector(selector), NSStringFromClass(originalClass)];
ERROR_AND_RETURN(SuperBuilderErrorCodeNoDynamicallyDispatchedMethodAvailable, msg)
if (error) {
let message = [NSString stringWithFormat:@"No dynamically dispatched method with selector %@ is available on any of the superclasses of %@", NSStringFromSelector(selector), NSStringFromClass(originalClass)];
*error = [NSError errorWithDomain:ITKSuperBuilderErrorDomain
code:SuperBuilderErrorCodeNoDynamicallyDispatchedMethodAvailable
userInfo:@{NSLocalizedDescriptionKey: message}];
return NO;
}
}

// Add trampoline
let typeEncoding = method_getTypeEncoding(method);
let trampoline = ITKGetTrampolineForTypeEncoding(typeEncoding);
let methodAdded = class_addMethod(originalClass, selector, trampoline, typeEncoding);
if (!methodAdded) {
let msg = [NSString stringWithFormat:@"Failed to add method for selector %@ to class %@", NSStringFromSelector(selector), NSStringFromClass(originalClass)];
ERROR_AND_RETURN(SuperBuilderErrorCodeFailedToAddMethod, msg)
if (error) {
let message = [NSString stringWithFormat:@"Failed to add method for selector %@ to class %@", NSStringFromSelector(selector), NSStringFromClass(originalClass)];
*error = [NSError errorWithDomain:ITKSuperBuilderErrorDomain
code:SuperBuilderErrorCodeFailedToAddMethod
userInfo:@{NSLocalizedDescriptionKey: message}];
return NO;
}
}
return methodAdded;
}

+ (BOOL)isSuperTrampolineForClass:(Class)originalClass
selector:(SEL)selector
{
let method = class_getInstanceMethod(originalClass, selector);
return ITKMethodIsSuperTrampoline(method);
}

@end

// Control if the trampoline should also push/pop the floating point registers.
// This is slightly slower and not needed for our simple implementation
// However, even if you just use memcpy, you will want to enable this.
Expand All @@ -101,12 +78,50 @@ + (BOOL)addSuperInstanceMethodToClass:(Class)originalClass selector:(SEL)selecto
// One thread local per thread should be enough
_Thread_local struct objc_super _threadSuperStorage;

void msgSendSuperTrampoline(void);

#if defined (__x86_64__)
void msgSendSuperStretTrampoline(void);
#endif

static IMP ITKGetTrampolineForTypeEncoding(__unused const char *typeEncoding) {
#if defined (__arm64__)
// arm64 doesn't use stret dispatch. Yay!
return (IMP)msgSendSuperTrampoline;
#elif defined (__x86_64__)
// On x86_64, stret dispatch is ~used whenever return type doesn't fit into two registers
//
// http://www.sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html
// x86_64 is more complicated, including rules for returning floating-point struct fields in FPU registers, and ppc64's rules and exceptions will make your head spin. The gory details are documented in the Mac OS X ABI Guide, though as usual if the documentation and the compiler disagree then the documentation is wrong.
NSUInteger returnTypeActualSize = 0;
NSGetSizeAndAlignment(typeEncoding, &returnTypeActualSize, NULL);
BOOL requiresStructDispatch = returnTypeActualSize > (sizeof(void *) * 2);
return requiresStructDispatch ? (IMP)msgSendSuperStretTrampoline : (IMP)msgSendSuperTrampoline;
#else
// Unknown architecture
// https://devblogs.microsoft.com/xamarin/apple-new-processor-architecture/
// watchOS uses arm64_32 since series 4, before armv7k. watch Simulator uses i386.
// See ILP32: http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dai0490a/ar01s01.html
return NO;
#endif
}

static BOOL ITKMethodIsSuperTrampoline(Method method) {
let methodIMP = method_getImplementation(method);
return methodIMP == (IMP)msgSendSuperTrampoline || methodIMP == (IMP)msgSendSuperStretTrampoline;

if (methodIMP == (IMP)msgSendSuperTrampoline) {
return YES;
}

#if defined (__x86_64__)
if (methodIMP == (IMP)msgSendSuperStretTrampoline) {
return YES;
}
#endif

return NO;
}

struct objc_super *ITKReturnThreadSuper(__unsafe_unretained id obj, SEL _cmd);
struct objc_super *ITKReturnThreadSuper(__unsafe_unretained id obj, SEL _cmd) {
/**
Assume you have a class hierarchy made of four classes `Level1` <- `Level2` <- `Level3` <- `Level4`,
Expand All @@ -123,15 +138,15 @@ static BOOL ITKMethodIsSuperTrampoline(Method method) {
Looking at the method implementation we can also skip subsequent super calls.
*/
Class clazz = object_getClass(obj);
Class superclazz = class_getSuperclass(clazz);
Class superclass = class_getSuperclass(clazz);
do {
let superclassMethod = class_getInstanceMethod(superclazz, _cmd);
let superclassMethod = class_getInstanceMethod(superclass, _cmd);
let sameMethods = class_getInstanceMethod(clazz, _cmd) == superclassMethod;
if (!sameMethods && !ITKMethodIsSuperTrampoline(superclassMethod)) {
break;
}
clazz = superclazz;
superclazz = class_getSuperclass(clazz);
clazz = superclass;
superclass = class_getSuperclass(clazz);
} while (1);

struct objc_super *_super = &_threadSuperStorage;
Expand All @@ -140,8 +155,6 @@ static BOOL ITKMethodIsSuperTrampoline(Method method) {
return _super;
}

@end

/**
Inline assembly is used to perfectly forward all parameters to objc_msgSendSuper,
while also looking up the target on-the-fly.
Expand Down Expand Up @@ -213,9 +226,6 @@ asm volatile (
: : : "x0", "x1");
}

// arm64 doesn't use _stret variants.
void msgSendSuperStretTrampoline(void) {}

#elif defined(__x86_64__)

__attribute__((__naked__))
Expand Down Expand Up @@ -285,7 +295,6 @@ asm volatile (
: : : "rsi", "rdi");
}


__attribute__((__naked__))
void msgSendSuperStretTrampoline(void) {
asm volatile (
Expand Down Expand Up @@ -332,11 +341,6 @@ asm volatile (
: : : "rsi", "rdi");
}

#else
// Unknown architecture - time to write some assembly :)
void msgSendSuperTrampoline(void) {}
void msgSendSuperStretTrampoline(void) {}
#endif

NS_ASSUME_NONNULL_END
#endif
Loading