blob: d273f0efd0546dc4c31d8b8aac00e3dd4f78f652 [file] [log] [blame]
//
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#import "Service/Sources/EDOInvocationMessage.h"
#include <objc/runtime.h>
#import "Channel/Sources/EDOHostPort.h"
#import "Service/Sources/EDOBlockObject.h"
#import "Service/Sources/EDOClientService+Private.h"
#import "Service/Sources/EDOClientService.h"
#import "Service/Sources/EDOHostService+Private.h"
#import "Service/Sources/EDOHostService.h"
#import "Service/Sources/EDOMessage.h"
#import "Service/Sources/EDOObject+Private.h"
#import "Service/Sources/EDOObject.h"
#import "Service/Sources/EDOParameter.h"
#import "Service/Sources/EDORemoteException.h"
#import "Service/Sources/EDORuntimeUtils.h"
#import "Service/Sources/EDOServiceException.h"
#import "Service/Sources/EDOServicePort.h"
#import "Service/Sources/EDOServiceRequest.h"
#import "Service/Sources/NSObject+EDOParameter.h"
#import "Service/Sources/NSObject+EDOValue.h"
// Box the value type directly into NSValue, the other types into a EDOObject, and the nil value.
#define BOX_VALUE(__value, __target, __service, __hostPort) \
([(__value) edo_parameterForTarget:(__target) service:(__service)hostPort:(__hostPort)] \
?: [EDOBoxedValueType parameterForNilValue])
#define CHECK_PREFIX(__string, __prefix) (strncmp(__string, __prefix, sizeof(__prefix) - 1) == 0)
static NSString *const kEDOInvocationCoderTargetKey = @"target";
static NSString *const kEDOInvocationCoderSelectorNameKey = @"selName";
static NSString *const kEDOInvocationCoderArgumentsKey = @"arguments";
static NSString *const kEDOInvocationCoderHostPortKey = @"hostPort";
static NSString *const kEDOInvocationReturnByValueKey = @"returnByValue";
static NSString *const kEDOInvocationCoderReturnRetainedKey = @"returnRetained";
static NSString *const kEDOInvocationCoderReturnValueKey = @"returnValue";
static NSString *const kEDOInvocationCoderOutValuesKey = @"outValues";
static NSString *const kEDOInvocationCoderExceptionKey = @"exception";
/** The list of method families that should retain the returned object. */
typedef NS_ENUM(NSUInteger, EDOMethodFamily) {
EDOMethodFamilyNone,
EDOMethodFamilyAlloc,
EDOMethodFamilyCopy,
EDOMethodFamilyNew,
EDOMethodFamilyMutableCopy,
};
/** A struct representing an Objective-C method family. */
typedef struct MethodFamily {
/** The method family type. */
EDOMethodFamily family;
/** The prefix identifying the method family. */
const char *prefix;
/** The length of the prefix of the method family. */
size_t length;
} MethodFamily;
/** The helper macro to define a @c MethodFamily above. */
#define METHOD_FAMILY(__family, __str) \
((MethodFamily){.family = (__family), .prefix = (__str), .length = sizeof(__str) - 1})
/** The methods family that should retain the returned object. */
const MethodFamily kRetainReturnsMethodsFamily[] = {
METHOD_FAMILY(EDOMethodFamilyAlloc, "alloc"),
METHOD_FAMILY(EDOMethodFamilyCopy, "copy"),
METHOD_FAMILY(EDOMethodFamilyNew, "new"),
METHOD_FAMILY(EDOMethodFamilyMutableCopy, "mutableCopy"),
};
/**
* Gets the family type of the method belonging to the ns_returns_retained family.
*
* More info here:
* https://clang.llvm.org/docs/AutomaticReferenceCounting.html#retained-return-values.
*
* @param methodName The method name.
* @return The method family type.
*/
static EDOMethodFamily MethodTypeOfRetainsReturn(const char *methodName, Class targetClass) {
if (!methodName ||
[targetClass isSubclassOfClass:NSClassFromString(@"ComGoogleProtobufGeneratedMessage")]) {
return EDOMethodFamilyNone;
}
/**
* To find out if a selector is in a certain method family:
*
* A selector is in a certain selector family if, ignoring any leading underscores, the first
* component of the selector either consists entirely of the name of the method family or it
* begins with that name followed by a character other than a lowercase letter.
* http://clang.llvm.org/docs/AutomaticReferenceCounting.html#method-families
*/
// Skip the leading underscore as it is considered to be the same method family.
while (*methodName == '_') {
++methodName;
}
// Skip the first component if it begins with a method family that is implicitly annotated with
// the ns_returns_retained attribute.
BOOL matchesMethodFamily = NO;
int familySize = sizeof(kRetainReturnsMethodsFamily) / sizeof(MethodFamily);
int methodIdx = 0;
for (; methodIdx < familySize; methodIdx++) {
MethodFamily family = kRetainReturnsMethodsFamily[methodIdx];
if (strncmp(methodName, family.prefix, family.length) == 0) {
methodName += family.length;
matchesMethodFamily = YES;
break;
}
}
if (!matchesMethodFamily) {
return EDOMethodFamilyNone;
}
// It should end or be followed by a character other than a lowercase letter.
if (*methodName == '\0' || !islower(*methodName)) {
return kRetainReturnsMethodsFamily[methodIdx].family;
} else {
return EDOMethodFamilyNone;
}
}
static EDORemoteException *CreateRemoteException(id localException) {
if (!localException) {
return nil;
}
NSArray<NSString *> *exceptionStackTrace = [localException callStackSymbols];
NSArray<NSString *> *currentStackTrace = [NSThread callStackSymbols];
NSArray<NSString *> *majorStackTrace = [exceptionStackTrace
subarrayWithRange:NSMakeRange(0, exceptionStackTrace.count - currentStackTrace.count + 1)];
return [[EDORemoteException alloc] initWithName:[localException name]
reason:[localException reason]
callStackSymbols:majorStackTrace];
}
#pragma mark - EDOInvocationRequest extension
@interface EDOInvocationRequest ()
/** The remote target. */
@property(nonatomic, readonly) EDOPointerType target;
/** The selector name. */
@property(nonatomic, readonly) NSString *selectorName;
/** The boxed arguments. */
@property(nonatomic, readonly) NSArray<EDOBoxedValueType *> *arguments;
/** The flag indicationg return-by-value. */
@property(nonatomic, readonly, assign) BOOL returnByValue;
/** The host port. */
@property(nonatomic, readonly) EDOHostPort *hostPort;
@end
#pragma mark -
@implementation EDOInvocationResponse
+ (BOOL)supportsSecureCoding {
return YES;
}
+ (instancetype)responseWithReturnValue:(EDOBoxedValueType *)value
exception:(EDORemoteException *)exception
outValues:(NSArray<EDOBoxedValueType *> *)outValues
forRequest:(EDOInvocationRequest *)request
targetClass:(Class)targetClass {
return [[self alloc] initWithReturnValue:value
exception:exception
outValues:outValues
forRequest:request
targetClass:targetClass];
}
- (instancetype)initWithReturnValue:(EDOBoxedValueType *)value
exception:(EDORemoteException *)exception
outValues:(NSArray<EDOBoxedValueType *> *)outValues
forRequest:(EDOInvocationRequest *)request
targetClass:(Class)targetClass {
self = [super initWithMessageID:request.messageID];
if (self) {
_returnValue = value;
_exception = exception;
_outValues = outValues;
_returnRetained = MethodTypeOfRetainsReturn(request.selectorName.UTF8String, targetClass) !=
EDOMethodFamilyNone;
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
_returnRetained = [aDecoder decodeBoolForKey:kEDOInvocationCoderReturnRetainedKey];
_returnValue = [aDecoder decodeObjectOfClass:[EDOParameter class]
forKey:kEDOInvocationCoderReturnValueKey];
_exception = [aDecoder decodeObjectOfClass:[EDORemoteException class]
forKey:kEDOInvocationCoderExceptionKey];
NSSet *anyClasses =
[NSSet setWithObjects:[EDOBlockObject class], [NSObject class], [EDOObject class], nil];
_outValues = [aDecoder decodeObjectOfClasses:anyClasses forKey:kEDOInvocationCoderOutValuesKey];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[super encodeWithCoder:aCoder];
[aCoder encodeBool:self.returnRetained forKey:kEDOInvocationCoderReturnRetainedKey];
[aCoder encodeObject:self.returnValue forKey:kEDOInvocationCoderReturnValueKey];
[aCoder encodeObject:self.exception forKey:kEDOInvocationCoderExceptionKey];
[aCoder encodeObject:self.outValues forKey:kEDOInvocationCoderOutValuesKey];
}
- (NSString *)description {
return [NSString stringWithFormat:@"Invocation response (%@)", self.messageID];
}
@end
#pragma mark -
@implementation EDOInvocationRequest
+ (BOOL)supportsSecureCoding {
return YES;
}
+ (instancetype)requestWithTarget:(EDOPointerType)target
selector:(SEL)selector
arguments:(NSArray *)arguments
hostPort:(EDOHostPort *)hostPort
returnByValue:(BOOL)returnByValue {
return [[self alloc] initWithTarget:target
selector:selector
arguments:arguments
hostPort:hostPort
returnByValue:returnByValue];
}
+ (instancetype)requestWithInvocation:(NSInvocation *)invocation
target:(EDOObject *)target
selector:(SEL _Nullable)selector
returnByValue:(BOOL)returnByValue
service:(EDOHostService *)service {
NSMethodSignature *signature = invocation.methodSignature;
char const *returnType = signature.methodReturnType;
if (EDO_IS_POINTER(returnType)) {
NSString *errorMessage =
[NSString stringWithFormat:
@"Failed to make remote invocation to [%@ %@]: the return type is a pointer!",
target.className, selector ? NSStringFromSelector(selector) : @"(block)"];
[[NSException exceptionWithName:EDOTypeEncodingException reason:errorMessage
userInfo:nil] raise];
}
NSUInteger numOfArgs = signature.numberOfArguments;
// If the target is a block, the first argument starts at index 1, whereas for a regular object
// invocation, the first argument starts at index 2, with the selector being the second argument.
NSUInteger firstArgumentIndex = selector ? 2 : 1;
NSMutableArray<id> *arguments =
[[NSMutableArray alloc] initWithCapacity:(numOfArgs - firstArgumentIndex)];
for (NSUInteger i = firstArgumentIndex; i < numOfArgs; ++i) {
char const *ctype = [signature getArgumentTypeAtIndex:i];
EDOBoxedValueType *value = nil;
if (EDO_IS_OBJECT_OR_CLASS(ctype)) {
id __unsafe_unretained obj;
[invocation getArgument:&obj atIndex:i];
value = BOX_VALUE(obj, target, service, nil);
} else if (EDO_IS_OBJPOINTER(ctype)) {
id __unsafe_unretained *objRef;
[invocation getArgument:&objRef atIndex:i];
// Convert and pass the value as an object and decode it on remote side.
value = objRef ? BOX_VALUE(*objRef, target, service, nil)
: [EDOBoxedValueType parameterForDoublePointerNullValue];
} else if (EDO_IS_POINTER(ctype)) {
void *objRef;
[invocation getArgument:&objRef atIndex:i];
// Don't assert if the pointer is NULL. The purpose for disallowing non-Objective-C pointer
// parameters is because there's no way to know how big a C pointer's underlying data is. But
// if the pointer is NULL, then there's nothing to pass, so just pass NULL and don't throw an
// exception. This opens up partial support for key-value observing and other APIs that take
// optional context pointers (so long as the caller doesn't provide one).
if (objRef != NULL) {
NSString *errorMessage =
[NSString stringWithFormat:
@"Failed to make remote invocation to [%@ %@]: the %@%@ parameter is a "
@"non-nil C pointer!",
target.className, selector ? NSStringFromSelector(selector) : @"(block)",
@(i - firstArgumentIndex + 1),
i == firstArgumentIndex ? @"st"
: i == firstArgumentIndex + 1 ? @"nd"
: i == firstArgumentIndex + 2 ? @"rd"
: @"th"];
[[NSException exceptionWithName:EDOTypeEncodingException reason:errorMessage
userInfo:nil] raise];
}
value = [EDOBoxedValueType parameterForNilValue];
} else {
NSUInteger typeSize = 0L;
NSGetSizeAndAlignment(ctype, &typeSize, NULL);
void *argBuffer = alloca(typeSize);
[invocation getArgument:argBuffer atIndex:i];
// save struct or other POD to NSValue
value = [EDOBoxedValueType parameterWithBytes:argBuffer objCType:ctype];
}
[arguments addObject:value];
}
return [self requestWithTarget:target.remoteAddress
selector:selector
arguments:arguments
hostPort:target.servicePort.hostPort
returnByValue:returnByValue];
}
+ (EDORequestHandler)requestHandler {
return ^(EDOServiceRequest *originalRequest, EDOHostService *service) {
EDOInvocationRequest *request = (EDOInvocationRequest *)originalRequest;
NSAssert([request isKindOfClass:[EDOInvocationRequest class]],
@"EDOInvocationRequest is expected.");
EDOHostPort *hostPort = request.hostPort;
id target = (__bridge id)(void *)request.target;
SEL sel = NSSelectorFromString(request.selectorName);
EDOBoxedValueType *returnValue;
NSException *invocationException;
NSMutableArray<EDOBoxedValueType *> *outValues = [[NSMutableArray alloc] init];
@try {
// TODO(haowoo): Throw non-existing method exception.
NSMethodSignature *methodSignature = EDOGetMethodSignature(target, sel);
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
invocation.target = target;
NSUInteger numOfArgs = methodSignature.numberOfArguments;
NSUInteger firstArgumentIndex = sel ? 2 : 1;
if (sel) {
invocation.selector = sel;
}
NSArray<EDOBoxedValueType *> *arguments = request.arguments;
// Allocate enough memory to save the out parameters if any.
size_t outObjectsSize = sizeof(id) * numOfArgs;
id __unsafe_unretained *outObjects = (id __unsafe_unretained *)alloca(outObjectsSize);
memset(outObjects, 0, outObjectsSize);
// TODO(haowoo): Throw a proper exception.
NSAssert(arguments.count == numOfArgs - firstArgumentIndex,
@"The expected number of arguments is not matched.");
for (NSUInteger curArgIdx = firstArgumentIndex; curArgIdx < numOfArgs; ++curArgIdx) {
EDOBoxedValueType *argument = arguments[curArgIdx - firstArgumentIndex];
// TODO(haowoo): Handle errors if the primitive type isn't matched with the remote argument.
char const *ctype = [methodSignature getArgumentTypeAtIndex:curArgIdx];
NSAssert(EDO_IS_OBJPOINTER(ctype) ||
(EDO_IS_OBJECT(ctype) && EDO_IS_OBJECT(argument.objCType)) ||
(EDO_IS_CLASS(ctype) && EDO_IS_OBJECT(argument.objCType)) ||
strcmp(ctype, argument.objCType) == 0 ||
(EDO_IS_POINTER(ctype) && argument.value == NULL),
@"The argument type is not matched (%s : %s).", ctype, argument.objCType);
if (EDO_IS_OBJPOINTER(ctype)) {
NSAssert(EDO_IS_OBJECT(argument.objCType),
@"The argument should be id type for object pointer but (%s) instead.",
argument.objCType);
// The local buffer to save the pointer to the object. We need to inspect if the object
// from the remote process can be unwrapped into a local object and then use the local
// buffer for the outvar (i.e. NSError **) argument.
id __unsafe_unretained *objRef = NULL;
if (![argument isDoublePointerNullValue]) {
[argument getValue:&outObjects[curArgIdx]];
objRef = &outObjects[curArgIdx];
}
if (objRef && *objRef) {
*objRef = [EDOClientService unwrappedObjectFromObject:*objRef];
*objRef = [EDOClientService cachedEDOFromObjectUpdateIfNeeded:*objRef];
}
[invocation setArgument:&objRef atIndex:curArgIdx];
} else if (EDO_IS_OBJECT_OR_CLASS(ctype)) {
id __unsafe_unretained obj;
[argument getValue:&obj];
obj = [EDOClientService unwrappedObjectFromObject:obj];
obj = [EDOClientService cachedEDOFromObjectUpdateIfNeeded:obj];
// Add weakly referenced object to the host dictionary.
if ([[obj class] isEqual:[EDOObject class]] && ((EDOObject *)obj).weaklyReferenced) {
[service addWeakObject:obj];
}
[invocation setArgument:&obj atIndex:curArgIdx];
} else {
NSUInteger valueSize = 0;
NSGetSizeAndAlignment(argument.objCType, &valueSize, NULL);
void *argBuffer = alloca(valueSize);
[argument getValue:argBuffer];
[invocation setArgument:argBuffer atIndex:curArgIdx];
}
}
[invocation invoke];
NSUInteger length = methodSignature.methodReturnLength;
if (length > 0) {
char const *returnType = methodSignature.methodReturnType;
if (EDO_IS_OBJECT_OR_CLASS(returnType)) {
id __unsafe_unretained obj;
[invocation getReturnValue:&obj];
EDOMethodFamily family =
MethodTypeOfRetainsReturn(request.selectorName.UTF8String, [target class]);
if (family == EDOMethodFamilyAlloc &&
(request.returnByValue || [obj edo_isEDOValueType])) {
// We cannot serialize and deserialize the result from +alloc as it is not properly
// initialized yet.
NSString *reason =
[NSString stringWithFormat:@"Attempting to pass the result from +alloc method "
"family (%@) by value for the target (%@).",
request.selectorName, target];
invocationException = [NSException exceptionWithName:EDOServiceAllocValueTypeException
reason:reason
userInfo:nil];
returnValue = nil;
} else {
returnValue = request.returnByValue ? [EDOParameter parameterWithObject:obj]
: BOX_VALUE(obj, nil, service, hostPort);
}
if (family != EDOMethodFamilyNone) {
// We need to do an extra release here because the method return is not autoreleased,
// and because the invocation is dynamically created, ARC won't insert an extra release
// for us.
if (invocationException) {
// We send this to autorelease pool so it can live for another cycle before the actual
// exception is propagated to the client. For example, if the result from +alloc will
// get dealloc, and it can crash because -init is not invoked yet, the crash in
// -dealloc may override our EDO crash, to display a partial information to the
// client.
CFAutorelease((__bridge void *)obj);
} else {
CFBridgingRelease((__bridge void *)obj);
}
}
} else if (EDO_IS_POINTER(returnType)) {
// We don't/can't support the plain memory access.
NSAssert(NO, @"Doesn't support pointer returns and it should be caught at client.");
} else {
void *returnBuf = alloca(length);
[invocation getReturnValue:returnBuf];
// Save any c-struct/POD into the NSValue.
returnValue = [EDOBoxedValueType parameterWithBytes:returnBuf
objCType:methodSignature.methodReturnType];
}
}
for (NSUInteger curArgIdx = firstArgumentIndex; curArgIdx < numOfArgs; ++curArgIdx) {
char const *ctype = [methodSignature getArgumentTypeAtIndex:curArgIdx];
if (!EDO_IS_OBJPOINTER(ctype)) {
continue;
}
// TODO(ynzhang): add device serial info.
[outValues addObject:BOX_VALUE(outObjects[curArgIdx], nil, service, hostPort)];
}
} @catch (NSException *e) {
// TODO(haowoo): Add more error info for non-user exception errors.
invocationException = e;
}
return [EDOInvocationResponse responseWithReturnValue:returnValue
exception:CreateRemoteException(invocationException)
outValues:(outValues.count > 0 ? outValues : nil)
forRequest:request
targetClass:[target class]];
};
}
- (instancetype)initWithTarget:(EDOPointerType)target
selector:(SEL)selector
arguments:(NSArray *)arguments
hostPort:(EDOHostPort *)hostPort
returnByValue:(BOOL)returnByValue {
self = [super init];
if (self) {
_target = target;
_selectorName = selector ? NSStringFromSelector(selector) : nil;
_arguments = [arguments copy];
_hostPort = hostPort;
_returnByValue = returnByValue;
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
NSSet *anyClasses =
[NSSet setWithObjects:[EDOBlockObject class], [EDOObject class], [NSObject class], nil];
_target = [aDecoder decodeInt64ForKey:kEDOInvocationCoderTargetKey];
_selectorName = [aDecoder decodeObjectOfClass:[NSString class]
forKey:kEDOInvocationCoderSelectorNameKey];
_arguments = [aDecoder decodeObjectOfClasses:anyClasses forKey:kEDOInvocationCoderArgumentsKey];
_hostPort = [aDecoder decodeObjectOfClass:[EDOHostPort class]
forKey:kEDOInvocationCoderHostPortKey];
_returnByValue = [aDecoder decodeBoolForKey:kEDOInvocationReturnByValueKey];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[super encodeWithCoder:aCoder];
[aCoder encodeInt64:self.target forKey:kEDOInvocationCoderTargetKey];
[aCoder encodeObject:self.selectorName forKey:kEDOInvocationCoderSelectorNameKey];
[aCoder encodeObject:self.arguments forKey:kEDOInvocationCoderArgumentsKey];
[aCoder encodeObject:self.hostPort forKey:kEDOInvocationCoderHostPortKey];
[aCoder encodeBool:self.returnByValue forKey:kEDOInvocationReturnByValueKey];
}
- (NSString *)description {
return [NSString stringWithFormat:@"Invocation request (%@) on target (%llx) with selector (%@)",
self.messageID, self.target, self.selectorName];
}
@end