| // |
| // 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 |