You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

946 lines
38 KiB

//
// FLEXRuntimeUtility.m
// Flipboard
//
// Created by Ryan Olson on 6/8/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXRuntimeUtility.h"
#import "FLEXObjcInternal.h"
// See https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html#//apple_ref/doc/uid/TP40008048-CH101-SW6
NSString *const kFLEXUtilityAttributeTypeEncoding = @"T";
NSString *const kFLEXUtilityAttributeBackingIvar = @"V";
NSString *const kFLEXUtilityAttributeReadOnly = @"R";
NSString *const kFLEXUtilityAttributeCopy = @"C";
NSString *const kFLEXUtilityAttributeRetain = @"&";
NSString *const kFLEXUtilityAttributeNonAtomic = @"N";
NSString *const kFLEXUtilityAttributeCustomGetter = @"G";
NSString *const kFLEXUtilityAttributeCustomSetter = @"S";
NSString *const kFLEXUtilityAttributeDynamic = @"D";
NSString *const kFLEXUtilityAttributeWeak = @"W";
NSString *const kFLEXUtilityAttributeGarbageCollectable = @"P";
NSString *const kFLEXUtilityAttributeOldStyleTypeEncoding = @"t";
static NSString *const FLEXRuntimeUtilityErrorDomain = @"FLEXRuntimeUtilityErrorDomain";
typedef NS_ENUM(NSInteger, FLEXRuntimeUtilityErrorCode) {
FLEXRuntimeUtilityErrorCodeDoesNotRecognizeSelector = 0,
FLEXRuntimeUtilityErrorCodeInvocationFailed = 1,
FLEXRuntimeUtilityErrorCodeArgumentTypeMismatch = 2
};
// Arguments 0 and 1 are self and _cmd always
const unsigned int kFLEXNumberOfImplicitArgs = 2;
@implementation FLEXRuntimeUtility
#pragma mark - General Helpers (Public)
+ (BOOL)pointerIsValidObjcObject:(const void *)pointer
{
return FLEXPointerIsValidObjcObject(pointer);
}
+ (id)potentiallyUnwrapBoxedPointer:(id)returnedObjectOrNil type:(const FLEXTypeEncoding *)returnType
{
if (!returnedObjectOrNil) {
return nil;
}
NSInteger i = 0;
if (returnType[i] == FLEXTypeEncodingConst) {
i++;
}
BOOL returnsObjectOrClass = returnType[i] == FLEXTypeEncodingObjcObject ||
returnType[i] == FLEXTypeEncodingObjcClass;
BOOL returnsVoidPointer = returnType[i] == FLEXTypeEncodingPointer &&
returnType[i+1] == FLEXTypeEncodingVoid;
BOOL returnsCString = returnType[i] == FLEXTypeEncodingCString;
// If we got back an NSValue and the return type is not an object,
// we check to see if the pointer is of a valid object. If not,
// we just display the NSValue.
if (!returnsObjectOrClass) {
// Skip NSNumber instances
if ([returnedObjectOrNil isKindOfClass:[NSNumber class]]) {
return returnedObjectOrNil;
}
// Can only be NSValue since return type is not an object,
// so we bail if this doesn't add up
if (![returnedObjectOrNil isKindOfClass:[NSValue class]]) {
return returnedObjectOrNil;
}
NSValue *value = (NSValue *)returnedObjectOrNil;
if (returnsCString) {
// Wrap char * in NSString
const char *string = (const char *)value.pointerValue;
returnedObjectOrNil = string ? [NSString stringWithCString:string encoding:NSUTF8StringEncoding] : NULL;
} else if (returnsVoidPointer) {
// Cast valid objects disguised as void * to id
if ([FLEXRuntimeUtility pointerIsValidObjcObject:value.pointerValue]) {
returnedObjectOrNil = (__bridge id)value.pointerValue;
}
}
}
return returnedObjectOrNil;
}
+ (NSUInteger)fieldNameOffsetForTypeEncoding:(const FLEXTypeEncoding *)typeEncoding
{
NSUInteger beginIndex = 0;
while (typeEncoding[beginIndex] == FLEXTypeEncodingQuote) {
NSUInteger endIndex = beginIndex + 1;
while (typeEncoding[endIndex] != FLEXTypeEncodingQuote) {
++endIndex;
}
beginIndex = endIndex + 1;
}
return beginIndex;
}
+ (NSArray<Class> *)classHierarchyOfObject:(id)objectOrClass
{
NSMutableArray<Class> *superClasses = [NSMutableArray new];
id cls = [objectOrClass class];
do {
[superClasses addObject:cls];
} while ((cls = [cls superclass]));
return superClasses;
}
#pragma mark - Property Helpers (Public)
+ (NSString *)prettyNameForProperty:(objc_property_t)property
{
NSString *name = @(property_getName(property));
NSString *encoding = [self typeEncodingForProperty:property];
NSString *readableType = [self readableTypeForEncoding:encoding];
return [self appendName:name toType:readableType];
}
+ (NSString *)typeEncodingForProperty:(objc_property_t)property
{
NSDictionary<NSString *, NSString *> *attributesDictionary = [self attributesDictionaryForProperty:property];
return attributesDictionary[kFLEXUtilityAttributeTypeEncoding];
}
+ (BOOL)isReadonlyProperty:(objc_property_t)property
{
return [[self attributesDictionaryForProperty:property] objectForKey:kFLEXUtilityAttributeReadOnly] != nil;
}
+ (SEL)setterSelectorForProperty:(objc_property_t)property
{
SEL setterSelector = NULL;
NSString *setterSelectorString = [[self attributesDictionaryForProperty:property] objectForKey:kFLEXUtilityAttributeCustomSetter];
if (!setterSelectorString) {
NSString *propertyName = @(property_getName(property));
setterSelectorString = [NSString
stringWithFormat:@"set%@%@:",
[propertyName substringToIndex:1].uppercaseString,
[propertyName substringFromIndex:1]
];
}
if (setterSelectorString) {
setterSelector = NSSelectorFromString(setterSelectorString);
}
return setterSelector;
}
+ (NSString *)fullDescriptionForProperty:(objc_property_t)property
{
NSDictionary<NSString *, NSString *> *attributesDictionary = [self attributesDictionaryForProperty:property];
NSMutableArray<NSString *> *attributesStrings = [NSMutableArray array];
// Atomicity
if (attributesDictionary[kFLEXUtilityAttributeNonAtomic]) {
[attributesStrings addObject:@"nonatomic"];
} else {
[attributesStrings addObject:@"atomic"];
}
// Storage
if (attributesDictionary[kFLEXUtilityAttributeRetain]) {
[attributesStrings addObject:@"strong"];
} else if (attributesDictionary[kFLEXUtilityAttributeCopy]) {
[attributesStrings addObject:@"copy"];
} else if (attributesDictionary[kFLEXUtilityAttributeWeak]) {
[attributesStrings addObject:@"weak"];
} else {
[attributesStrings addObject:@"assign"];
}
// Mutability
if (attributesDictionary[kFLEXUtilityAttributeReadOnly]) {
[attributesStrings addObject:@"readonly"];
} else {
[attributesStrings addObject:@"readwrite"];
}
// Custom getter/setter
NSString *customGetter = attributesDictionary[kFLEXUtilityAttributeCustomGetter];
NSString *customSetter = attributesDictionary[kFLEXUtilityAttributeCustomSetter];
if (customGetter) {
[attributesStrings addObject:[NSString stringWithFormat:@"getter=%@", customGetter]];
}
if (customSetter) {
[attributesStrings addObject:[NSString stringWithFormat:@"setter=%@", customSetter]];
}
NSString *attributesString = [attributesStrings componentsJoinedByString:@", "];
NSString *shortName = [self prettyNameForProperty:property];
return [NSString stringWithFormat:@"@property (%@) %@", attributesString, shortName];
}
+ (id)valueForProperty:(objc_property_t)property onObject:(id)object
{
NSString *customGetterString = nil;
char *customGetterName = property_copyAttributeValue(property, kFLEXUtilityAttributeCustomGetter.UTF8String);
if (customGetterName) {
customGetterString = @(customGetterName);
free(customGetterName);
}
SEL getterSelector;
if (customGetterString.length > 0) {
getterSelector = NSSelectorFromString(customGetterString);
} else {
NSString *propertyName = @(property_getName(property));
getterSelector = NSSelectorFromString(propertyName);
}
return [self performSelector:getterSelector onObject:object withArguments:nil error:NULL];
}
+ (NSString *)descriptionForIvarOrPropertyValue:(id)value
{
NSString *description = nil;
// Special case BOOL for better readability.
if ([value isKindOfClass:[NSValue class]]) {
const char *type = [value objCType];
if (strcmp(type, @encode(BOOL)) == 0) {
BOOL boolValue = NO;
[value getValue:&boolValue];
description = boolValue ? @"YES" : @"NO";
} else if (strcmp(type, @encode(SEL)) == 0) {
SEL selector = NULL;
[value getValue:&selector];
description = NSStringFromSelector(selector);
}
}
@try {
if (!description) {
// Single line display - replace newlines and tabs with spaces.
description = [[value description] stringByReplacingOccurrencesOfString:@"\n" withString:@" "];
description = [description stringByReplacingOccurrencesOfString:@"\t" withString:@" "];
}
} @catch (NSException *e) {
description = [@"Thrown: " stringByAppendingString:e.reason ?: @"(nil exception reason)"];
}
if (!description) {
description = @"nil";
}
return description;
}
+ (void)tryAddPropertyWithName:(const char *)name
attributes:(NSDictionary<NSString *, NSString *> *)attributePairs
toClass:(__unsafe_unretained Class)theClass
{
objc_property_t property = class_getProperty(theClass, name);
if (!property) {
unsigned int totalAttributesCount = (unsigned int)attributePairs.count;
objc_property_attribute_t *attributes = malloc(sizeof(objc_property_attribute_t) * totalAttributesCount);
if (attributes) {
unsigned int attributeIndex = 0;
for (NSString *attributeName in attributePairs.allKeys) {
objc_property_attribute_t attribute;
attribute.name = attributeName.UTF8String;
attribute.value = attributePairs[attributeName].UTF8String;
attributes[attributeIndex++] = attribute;
}
class_addProperty(theClass, name, attributes, totalAttributesCount);
free(attributes);
}
}
}
#pragma mark - Ivar Helpers (Public)
+ (NSString *)prettyNameForIvar:(Ivar)ivar
{
const char *nameCString = ivar_getName(ivar);
NSString *name = nameCString ? @(nameCString) : nil;
const char *encodingCString = ivar_getTypeEncoding(ivar);
NSString *encoding = encodingCString ? @(encodingCString) : nil;
NSString *readableType = [self readableTypeForEncoding:encoding];
return [self appendName:name toType:readableType];
}
+ (id)valueForIvar:(Ivar)ivar onObject:(id)object
{
id value = nil;
const char *type = ivar_getTypeEncoding(ivar);
#ifdef __arm64__
// See http://www.sealiesoftware.com/blog/archive/2013/09/24/objc_explain_Non-pointer_isa.html
const char *name = ivar_getName(ivar);
if (type[0] == FLEXTypeEncodingObjcClass && strcmp(name, "isa") == 0) {
value = object_getClass(object);
} else
#endif
if (type[0] == FLEXTypeEncodingObjcObject || type[0] == FLEXTypeEncodingObjcClass) {
value = object_getIvar(object, ivar);
} else {
ptrdiff_t offset = ivar_getOffset(ivar);
void *pointer = (__bridge void *)object + offset;
value = [self valueForPrimitivePointer:pointer objCType:type];
}
return value;
}
+ (void)setValue:(id)value forIvar:(Ivar)ivar onObject:(id)object
{
const char *typeEncodingCString = ivar_getTypeEncoding(ivar);
if (typeEncodingCString[0] == FLEXTypeEncodingObjcObject) {
object_setIvar(object, ivar, value);
} else if ([value isKindOfClass:[NSValue class]]) {
// Primitive - unbox the NSValue.
NSValue *valueValue = (NSValue *)value;
// Make sure that the box contained the correct type.
NSAssert(
strcmp(valueValue.objCType, typeEncodingCString) == 0,
@"Type encoding mismatch (value: %s; ivar: %s) in setting ivar named: %s on object: %@",
valueValue.objCType, typeEncodingCString, ivar_getName(ivar), object
);
NSUInteger bufferSize = 0;
@try {
// NSGetSizeAndAlignment barfs on type encoding for bitfields.
NSGetSizeAndAlignment(typeEncodingCString, &bufferSize, NULL);
} @catch (NSException *exception) { }
if (bufferSize > 0) {
void *buffer = calloc(bufferSize, 1);
[valueValue getValue:buffer];
ptrdiff_t offset = ivar_getOffset(ivar);
void *pointer = (__bridge void *)object + offset;
memcpy(pointer, buffer, bufferSize);
free(buffer);
}
}
}
#pragma mark - Method Helpers (Public)
+ (NSString *)prettyNameForMethod:(Method)method isClassMethod:(BOOL)isClassMethod
{
NSString *selectorName = NSStringFromSelector(method_getName(method));
NSString *methodTypeString = isClassMethod ? @"+" : @"-";
char *returnType = method_copyReturnType(method);
NSString *readableReturnType = [self readableTypeForEncoding:@(returnType)];
free(returnType);
NSString *prettyName = [NSString stringWithFormat:@"%@ (%@)", methodTypeString, readableReturnType];
NSArray<NSString *> *components = [self prettyArgumentComponentsForMethod:method];
if (components.count > 0) {
prettyName = [prettyName stringByAppendingString:[components componentsJoinedByString:@" "]];
} else {
prettyName = [prettyName stringByAppendingString:selectorName];
}
return prettyName;
}
+ (NSArray<NSString *> *)prettyArgumentComponentsForMethod:(Method)method
{
NSMutableArray<NSString *> *components = [NSMutableArray array];
NSString *selectorName = NSStringFromSelector(method_getName(method));
NSMutableArray<NSString *> *selectorComponents = [[selectorName componentsSeparatedByString:@":"] mutableCopy];
// this is a workaround cause method_getNumberOfArguments() returns wrong number for some methods
if (selectorComponents.count == 1) {
return @[];
}
if ([selectorComponents.lastObject isEqualToString:@""]) {
[selectorComponents removeLastObject];
}
for (unsigned int argIndex = 0; argIndex < selectorComponents.count; argIndex++) {
char *argType = method_copyArgumentType(method, argIndex + kFLEXNumberOfImplicitArgs);
NSString *readableArgType = (argType != NULL) ? [self readableTypeForEncoding:@(argType)] : nil;
free(argType);
NSString *prettyComponent = [NSString
stringWithFormat:@"%@:(%@) ",
selectorComponents[argIndex],
readableArgType
];
[components addObject:prettyComponent];
}
return components;
}
+ (FLEXTypeEncoding *)returnTypeForMethod:(Method)method
{
return (FLEXTypeEncoding *)method_copyReturnType(method);
}
#pragma mark - Method Calling/Field Editing (Public)
+ (id)performSelector:(SEL)selector
onObject:(id)object
withArguments:(NSArray *)arguments
error:(NSError * __autoreleasing *)error
{
// Bail if the object won't respond to this selector.
if (![object respondsToSelector:selector]) {
if (error) {
NSString *msg = [NSString
stringWithFormat:@"%@ does not respond to the selector %@",
object, NSStringFromSelector(selector)
];
NSDictionary<NSString *, id> *userInfo = @{ NSLocalizedDescriptionKey : msg };
*error = [NSError
errorWithDomain:FLEXRuntimeUtilityErrorDomain
code:FLEXRuntimeUtilityErrorCodeDoesNotRecognizeSelector
userInfo:userInfo
];
}
return nil;
}
// Probably an unsupported type encoding, like bitfields
// or inline arrays. In the future, we could calculate
// the return length on our own. For now, we abort.
//
// For future reference, the code here will get the true type encoding.
// NSMethodSignature will convert {?=b8b4b1b1b18[8S]} to {?}
// A solution might involve hooking NSGetSizeAndAlignment.
//
// returnType = method_getTypeEncoding(class_getInstanceMethod([object class], selector));
NSMethodSignature *methodSignature = [object methodSignatureForSelector:selector];
if (!methodSignature.methodReturnLength &&
methodSignature.methodReturnType[0] != FLEXTypeEncodingVoid) {
return nil;
}
// Build the invocation
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
[invocation setSelector:selector];
[invocation setTarget:object];
[invocation retainArguments];
// Always self and _cmd
NSUInteger numberOfArguments = [methodSignature numberOfArguments];
for (NSUInteger argumentIndex = kFLEXNumberOfImplicitArgs; argumentIndex < numberOfArguments; argumentIndex++) {
NSUInteger argumentsArrayIndex = argumentIndex - kFLEXNumberOfImplicitArgs;
id argumentObject = arguments.count > argumentsArrayIndex ? arguments[argumentsArrayIndex] : nil;
// NSNull in the arguments array can be passed as a placeholder to indicate nil.
// We only need to set the argument if it will be non-nil.
if (argumentObject && ![argumentObject isKindOfClass:[NSNull class]]) {
const char *typeEncodingCString = [methodSignature getArgumentTypeAtIndex:argumentIndex];
if (typeEncodingCString[0] == FLEXTypeEncodingObjcObject ||
typeEncodingCString[0] == FLEXTypeEncodingObjcClass ||
[self isTollFreeBridgedValue:argumentObject forCFType:typeEncodingCString]) {
// Object
[invocation setArgument:&argumentObject atIndex:argumentIndex];
} else if (strcmp(typeEncodingCString, @encode(CGColorRef)) == 0 &&
[argumentObject isKindOfClass:[UIColor class]]) {
// Bridging UIColor to CGColorRef
CGColorRef colorRef = [argumentObject CGColor];
[invocation setArgument:&colorRef atIndex:argumentIndex];
} else if ([argumentObject isKindOfClass:[NSValue class]]) {
// Primitive boxed in NSValue
NSValue *argumentValue = (NSValue *)argumentObject;
// Ensure that the type encoding on the NSValue matches the type encoding of the argument in the method signature
if (strcmp([argumentValue objCType], typeEncodingCString) != 0) {
if (error) {
NSString *msg = [NSString
stringWithFormat:@"Type encoding mismatch for argument at index %lu. "
"Value type: %s; Method argument type: %s.",
(unsigned long)argumentsArrayIndex, argumentValue.objCType, typeEncodingCString
];
NSDictionary<NSString *, id> *userInfo = @{ NSLocalizedDescriptionKey : msg };
*error = [NSError
errorWithDomain:FLEXRuntimeUtilityErrorDomain
code:FLEXRuntimeUtilityErrorCodeArgumentTypeMismatch
userInfo:userInfo
];
}
return nil;
}
@try {
NSUInteger bufferSize = 0;
// NSGetSizeAndAlignment barfs on type encoding for bitfields.
NSGetSizeAndAlignment(typeEncodingCString, &bufferSize, NULL);
if (bufferSize > 0) {
void *buffer = alloca(bufferSize);
[argumentValue getValue:buffer];
[invocation setArgument:buffer atIndex:argumentIndex];
}
} @catch (NSException *exception) { }
}
}
}
// Try to invoke the invocation but guard against an exception being thrown.
id returnObject = nil;
@try {
[invocation invoke];
// Retrieve the return value and box if necessary.
const char *returnType = methodSignature.methodReturnType;
if (returnType[0] == FLEXTypeEncodingObjcObject || returnType[0] == FLEXTypeEncodingObjcClass) {
// Return value is an object.
__unsafe_unretained id objectReturnedFromMethod = nil;
[invocation getReturnValue:&objectReturnedFromMethod];
returnObject = objectReturnedFromMethod;
} else if (returnType[0] != FLEXTypeEncodingVoid) {
NSAssert(methodSignature.methodReturnLength, @"Memory corruption lies ahead");
// Will use arbitrary buffer for return value and box it.
void *returnValue = malloc(methodSignature.methodReturnLength);
if (returnValue) {
[invocation getReturnValue:returnValue];
returnObject = [self valueForPrimitivePointer:returnValue objCType:returnType];
free(returnValue);
}
}
} @catch (NSException *exception) {
// Bummer...
if (error) {
// "… on <class>" / "… on instance of <class>"
NSString *class = NSStringFromClass([object class]);
NSString *calledOn = object == [object class] ? class : [@"an instance of " stringByAppendingString:class];
NSString *message = [NSString
stringWithFormat:@"Exception '%@' thrown while performing selector '%@' on %@.\nReason:\n\n%@",
exception.name, NSStringFromSelector(selector), calledOn, exception.reason
];
*error = [NSError errorWithDomain:FLEXRuntimeUtilityErrorDomain
code:FLEXRuntimeUtilityErrorCodeInvocationFailed
userInfo:@{ NSLocalizedDescriptionKey : message }];
}
}
return returnObject;
}
+ (BOOL)isTollFreeBridgedValue:(id)value forCFType:(const char *)typeEncoding
{
// See https://developer.apple.com/library/archive/documentation/General/Conceptual/CocoaEncyclopedia/Toll-FreeBridgin/Toll-FreeBridgin.html
#define CASE(cftype, foundationClass) \
if (strcmp(typeEncoding, @encode(cftype)) == 0) { \
return [value isKindOfClass:[foundationClass class]]; \
}
CASE(CFArrayRef, NSArray);
CASE(CFAttributedStringRef, NSAttributedString);
CASE(CFCalendarRef, NSCalendar);
CASE(CFCharacterSetRef, NSCharacterSet);
CASE(CFDataRef, NSData);
CASE(CFDateRef, NSDate);
CASE(CFDictionaryRef, NSDictionary);
CASE(CFErrorRef, NSError);
CASE(CFLocaleRef, NSLocale);
CASE(CFMutableArrayRef, NSMutableArray);
CASE(CFMutableAttributedStringRef, NSMutableAttributedString);
CASE(CFMutableCharacterSetRef, NSMutableCharacterSet);
CASE(CFMutableDataRef, NSMutableData);
CASE(CFMutableDictionaryRef, NSMutableDictionary);
CASE(CFMutableSetRef, NSMutableSet);
CASE(CFMutableStringRef, NSMutableString);
CASE(CFNumberRef, NSNumber);
CASE(CFReadStreamRef, NSInputStream);
CASE(CFRunLoopTimerRef, NSTimer);
CASE(CFSetRef, NSSet);
CASE(CFStringRef, NSString);
CASE(CFTimeZoneRef, NSTimeZone);
CASE(CFURLRef, NSURL);
CASE(CFWriteStreamRef, NSOutputStream);
#undef CASE
return NO;
}
+ (NSString *)editableJSONStringForObject:(id)object
{
NSString *editableDescription = nil;
if (object) {
// This is a hack to use JSON serialization for our editable objects.
// NSJSONSerialization doesn't allow writing fragments - the top level object must be an array or dictionary.
// We always wrap the object inside an array and then strip the outer square braces off the final string.
NSArray *wrappedObject = @[object];
if ([NSJSONSerialization isValidJSONObject:wrappedObject]) {
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:wrappedObject options:0 error:NULL];
NSString *wrappedDescription = [NSString stringWithUTF8String:jsonData.bytes];
editableDescription = [wrappedDescription substringWithRange:NSMakeRange(1, wrappedDescription.length - 2)];
}
}
return editableDescription;
}
+ (id)objectValueFromEditableJSONString:(NSString *)string
{
id value = nil;
// nil for empty string/whitespace
if ([string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].length) {
value = [NSJSONSerialization
JSONObjectWithData:[string dataUsingEncoding:NSUTF8StringEncoding]
options:NSJSONReadingAllowFragments
error:NULL
];
}
return value;
}
+ (NSValue *)valueForNumberWithObjCType:(const char *)typeEncoding fromInputString:(NSString *)inputString
{
NSNumberFormatter *formatter = [NSNumberFormatter new];
[formatter setNumberStyle:NSNumberFormatterDecimalStyle];
NSNumber *number = [formatter numberFromString:inputString];
// Make sure we box the number with the correct type encoding so it can be properly unboxed later via getValue:
NSValue *value = nil;
if (strcmp(typeEncoding, @encode(char)) == 0) {
char primitiveValue = [number charValue];
value = [NSValue value:&primitiveValue withObjCType:typeEncoding];
} else if (strcmp(typeEncoding, @encode(int)) == 0) {
int primitiveValue = [number intValue];
value = [NSValue value:&primitiveValue withObjCType:typeEncoding];
} else if (strcmp(typeEncoding, @encode(short)) == 0) {
short primitiveValue = [number shortValue];
value = [NSValue value:&primitiveValue withObjCType:typeEncoding];
} else if (strcmp(typeEncoding, @encode(long)) == 0) {
long primitiveValue = [number longValue];
value = [NSValue value:&primitiveValue withObjCType:typeEncoding];
} else if (strcmp(typeEncoding, @encode(long long)) == 0) {
long long primitiveValue = [number longLongValue];
value = [NSValue value:&primitiveValue withObjCType:typeEncoding];
} else if (strcmp(typeEncoding, @encode(unsigned char)) == 0) {
unsigned char primitiveValue = [number unsignedCharValue];
value = [NSValue value:&primitiveValue withObjCType:typeEncoding];
} else if (strcmp(typeEncoding, @encode(unsigned int)) == 0) {
unsigned int primitiveValue = [number unsignedIntValue];
value = [NSValue value:&primitiveValue withObjCType:typeEncoding];
} else if (strcmp(typeEncoding, @encode(unsigned short)) == 0) {
unsigned short primitiveValue = [number unsignedShortValue];
value = [NSValue value:&primitiveValue withObjCType:typeEncoding];
} else if (strcmp(typeEncoding, @encode(unsigned long)) == 0) {
unsigned long primitiveValue = [number unsignedLongValue];
value = [NSValue value:&primitiveValue withObjCType:typeEncoding];
} else if (strcmp(typeEncoding, @encode(unsigned long long)) == 0) {
unsigned long long primitiveValue = [number unsignedLongValue];
value = [NSValue value:&primitiveValue withObjCType:typeEncoding];
} else if (strcmp(typeEncoding, @encode(float)) == 0) {
float primitiveValue = [number floatValue];
value = [NSValue value:&primitiveValue withObjCType:typeEncoding];
} else if (strcmp(typeEncoding, @encode(double)) == 0) {
double primitiveValue = [number doubleValue];
value = [NSValue value:&primitiveValue withObjCType:typeEncoding];
} else if (strcmp(typeEncoding, @encode(long double)) == 0) {
long double primitiveValue = [number doubleValue];
value = [NSValue value:&primitiveValue withObjCType:typeEncoding];
}
return value;
}
+ (void)enumerateTypesInStructEncoding:(const char *)structEncoding
usingBlock:(void (^)(NSString *structName,
const char *fieldTypeEncoding,
NSString *prettyTypeEncoding,
NSUInteger fieldIndex,
NSUInteger fieldOffset))typeBlock
{
if (structEncoding && structEncoding[0] == FLEXTypeEncodingStructBegin) {
const char *equals = strchr(structEncoding, '=');
if (equals) {
const char *nameStart = structEncoding + 1;
NSString *structName = [@(structEncoding)
substringWithRange:NSMakeRange(nameStart - structEncoding, equals - nameStart)
];
NSUInteger fieldAlignment = 0;
NSUInteger structSize = 0;
@try {
// NSGetSizeAndAlignment barfs on type encoding for bitfields.
NSGetSizeAndAlignment(structEncoding, &structSize, &fieldAlignment);
} @catch (NSException *exception) { }
if (structSize > 0) {
NSUInteger runningFieldIndex = 0;
NSUInteger runningFieldOffset = 0;
const char *typeStart = equals + 1;
while (*typeStart != FLEXTypeEncodingStructEnd) {
NSUInteger fieldSize = 0;
// If the struct type encoding was successfully handled by NSGetSizeAndAlignment above, we *should* be ok with the field here.
const char *nextTypeStart = NSGetSizeAndAlignment(typeStart, &fieldSize, NULL);
NSString *typeEncoding = [@(structEncoding)
substringWithRange:NSMakeRange(typeStart - structEncoding, nextTypeStart - typeStart)
];
// Padding to keep proper alignment. __attribute((packed)) structs will break here.
// The type encoding is no different for packed structs, so it's not clear there's anything we can do for those.
const NSUInteger currentSizeSum = runningFieldOffset % fieldAlignment;
if (currentSizeSum != 0 && currentSizeSum + fieldSize > fieldAlignment) {
runningFieldOffset += fieldAlignment - currentSizeSum;
}
typeBlock(
structName,
typeEncoding.UTF8String,
[self readableTypeForEncoding:typeEncoding],
runningFieldIndex,
runningFieldOffset
);
runningFieldOffset += fieldSize;
runningFieldIndex++;
typeStart = nextTypeStart;
}
}
}
}
}
#pragma mark - Internal Helpers
+ (NSDictionary<NSString *, NSString *> *)attributesDictionaryForProperty:(objc_property_t)property
{
NSString *attributes = @(property_getAttributes(property));
// Thanks to MAObjcRuntime for inspiration here.
NSArray<NSString *> *attributePairs = [attributes componentsSeparatedByString:@","];
NSMutableDictionary<NSString *, NSString *> *attributesDictionary = [NSMutableDictionary dictionaryWithCapacity:attributePairs.count];
for (NSString *attributePair in attributePairs) {
[attributesDictionary setObject:[attributePair substringFromIndex:1] forKey:[attributePair substringToIndex:1]];
}
return attributesDictionary;
}
+ (NSString *)appendName:(NSString *)name toType:(NSString *)type
{
if (!type.length) {
type = @"(?)";
}
NSString *combined = nil;
if ([type characterAtIndex:type.length - 1] == FLEXTypeEncodingCString) {
combined = [type stringByAppendingString:name];
} else {
combined = [type stringByAppendingFormat:@" %@", name];
}
return combined;
}
+ (NSString *)readableTypeForEncoding:(NSString *)encodingString
{
if (!encodingString) {
return nil;
}
// See https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
// class-dump has a much nicer and much more complete implementation for this task, but it is distributed under GPLv2 :/
// See https://github.com/nygard/class-dump/blob/master/Source/CDType.m
// Warning: this method uses multiple middle returns and macros to cut down on boilerplate.
// The use of macros here was inspired by https://www.mikeash.com/pyblog/friday-qa-2013-02-08-lets-build-key-value-coding.html
const char *encodingCString = encodingString.UTF8String;
// Some fields have a name, such as {Size=\"width\"d\"height\"d}, we need to extract the name out and recursive
const NSUInteger fieldNameOffset = [FLEXRuntimeUtility fieldNameOffsetForTypeEncoding:encodingCString];
if (fieldNameOffset > 0) {
// According to https://github.com/nygard/class-dump/commit/33fb5ed221810685f57c192e1ce8ab6054949a7c,
// there are some consecutive quoted strings, so use `_` to concatenate the names.
NSString *const fieldNamesString = [encodingString substringWithRange:NSMakeRange(0, fieldNameOffset)];
NSArray<NSString *> *const fieldNames = [fieldNamesString
componentsSeparatedByString:[NSString stringWithFormat:@"%c", FLEXTypeEncodingQuote]
];
NSMutableString *finalFieldNamesString = [NSMutableString string];
for (NSString *const fieldName in fieldNames) {
if (fieldName.length > 0) {
if (finalFieldNamesString.length > 0) {
[finalFieldNamesString appendString:@"_"];
}
[finalFieldNamesString appendString:fieldName];
}
}
NSString *const recursiveType = [self readableTypeForEncoding:[encodingString substringFromIndex:fieldNameOffset]];
return [NSString stringWithFormat:@"%@ %@", recursiveType, finalFieldNamesString];
}
// Objects
if (encodingCString[0] == FLEXTypeEncodingObjcObject) {
NSString *class = [encodingString substringFromIndex:1];
class = [class stringByReplacingOccurrencesOfString:@"\"" withString:@""];
if (class.length == 0 || (class.length == 1 && [class characterAtIndex:0] == FLEXTypeEncodingUnknown)) {
class = @"id";
} else {
class = [class stringByAppendingString:@" *"];
}
return class;
}
// Qualifier Prefixes
// Do this first since some of the direct translations (i.e. Method) contain a prefix.
#define RECURSIVE_TRANSLATE(prefix, formatString) \
if (encodingCString[0] == prefix) { \
NSString *recursiveType = [self readableTypeForEncoding:[encodingString substringFromIndex:1]]; \
return [NSString stringWithFormat:formatString, recursiveType]; \
}
// If there's a qualifier prefix on the encoding, translate it and then
// recursively call this method with the rest of the encoding string.
RECURSIVE_TRANSLATE('^', @"%@ *");
RECURSIVE_TRANSLATE('r', @"const %@");
RECURSIVE_TRANSLATE('n', @"in %@");
RECURSIVE_TRANSLATE('N', @"inout %@");
RECURSIVE_TRANSLATE('o', @"out %@");
RECURSIVE_TRANSLATE('O', @"bycopy %@");
RECURSIVE_TRANSLATE('R', @"byref %@");
RECURSIVE_TRANSLATE('V', @"oneway %@");
RECURSIVE_TRANSLATE('b', @"bitfield(%@)");
#undef RECURSIVE_TRANSLATE
// C Types
#define TRANSLATE(ctype) \
if (strcmp(encodingCString, @encode(ctype)) == 0) { \
return (NSString *)CFSTR(#ctype); \
}
// Order matters here since some of the cocoa types are typedefed to c types.
// We can't recover the exact mapping, but we choose to prefer the cocoa types.
// This is not an exhaustive list, but it covers the most common types
TRANSLATE(CGRect);
TRANSLATE(CGPoint);
TRANSLATE(CGSize);
TRANSLATE(CGVector);
TRANSLATE(UIEdgeInsets);
if (@available(iOS 11.0, *)) {
TRANSLATE(NSDirectionalEdgeInsets);
}
TRANSLATE(UIOffset);
TRANSLATE(NSRange);
TRANSLATE(CGAffineTransform);
TRANSLATE(CATransform3D);
TRANSLATE(CGColorRef);
TRANSLATE(CGPathRef);
TRANSLATE(CGContextRef);
TRANSLATE(NSInteger);
TRANSLATE(NSUInteger);
TRANSLATE(CGFloat);
TRANSLATE(BOOL);
TRANSLATE(int);
TRANSLATE(short);
TRANSLATE(long);
TRANSLATE(long long);
TRANSLATE(unsigned char);
TRANSLATE(unsigned int);
TRANSLATE(unsigned short);
TRANSLATE(unsigned long);
TRANSLATE(unsigned long long);
TRANSLATE(float);
TRANSLATE(double);
TRANSLATE(long double);
TRANSLATE(char *);
TRANSLATE(Class);
TRANSLATE(objc_property_t);
TRANSLATE(Ivar);
TRANSLATE(Method);
TRANSLATE(Category);
TRANSLATE(NSZone *);
TRANSLATE(SEL);
TRANSLATE(void);
#undef TRANSLATE
// For structs, we only use the name of the structs
if (encodingCString[0] == FLEXTypeEncodingStructBegin) {
const char *equals = strchr(encodingCString, '=');
if (equals) {
const char *nameStart = encodingCString + 1;
// For anonymous structs
if (nameStart[0] == FLEXTypeEncodingUnknown) {
return @"anonymous struct";
} else {
NSString *const structName = [encodingString
substringWithRange:NSMakeRange(nameStart - encodingCString, equals - nameStart)
];
return structName;
}
}
}
// If we couldn't translate, just return the original encoding string
return encodingString;
}
+ (NSValue *)valueForPrimitivePointer:(void *)pointer objCType:(const char *)type
{
// Remove the field name if there is any (e.g. \"width\"d -> d)
const NSUInteger fieldNameOffset = [FLEXRuntimeUtility fieldNameOffsetForTypeEncoding:type];
if (fieldNameOffset > 0) {
return [self valueForPrimitivePointer:pointer objCType:type + fieldNameOffset];
}
// CASE macro inspired by https://www.mikeash.com/pyblog/friday-qa-2013-02-08-lets-build-key-value-coding.html
#define CASE(ctype, selectorpart) \
if (strcmp(type, @encode(ctype)) == 0) { \
return [NSNumber numberWith ## selectorpart: *(ctype *)pointer]; \
}
CASE(BOOL, Bool);
CASE(unsigned char, UnsignedChar);
CASE(short, Short);
CASE(unsigned short, UnsignedShort);
CASE(int, Int);
CASE(unsigned int, UnsignedInt);
CASE(long, Long);
CASE(unsigned long, UnsignedLong);
CASE(long long, LongLong);
CASE(unsigned long long, UnsignedLongLong);
CASE(float, Float);
CASE(double, Double);
CASE(long double, Double);
#undef CASE
NSValue *value = nil;
@try {
value = [NSValue valueWithBytes:pointer objCType:type];
} @catch (NSException *exception) {
// Certain type encodings are not supported by valueWithBytes:objCType:. Just fail silently if an exception is thrown.
}
return value;
}
@end