blob: 6600e14256957b15e7108297b94ff9f7a0a12365 [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/EDOObject.h"
#import <Foundation/Foundation.h>
#include <objc/runtime.h>
#import "Service/Sources/EDOClientService+Private.h"
#import "Service/Sources/EDOClientService.h"
#import "Service/Sources/EDOObject+Private.h"
#import "Service/Sources/EDOObjectReleaseMessage.h"
#import "Service/Sources/EDOParameter.h"
#import "Service/Sources/EDOServiceError.h"
#import "Service/Sources/EDOServiceException.h"
#import "Service/Sources/EDOServicePort.h"
#import "Service/Sources/EDOValueObject.h"
static NSString *const kEDOObjectCoderPortKey = @"edoServicePort";
static NSString *const kEDOObjectCoderRemoteAddressKey = @"edoRemoteAddress";
static NSString *const kEDOObjectCoderRemoteClassKey = @"edoRemoteClass";
static NSString *const kEDOObjectCoderClassNameKey = @"edoClassName";
static NSString *const kEDOObjectCoderProcessUUIDKey = @"edoProcessUUID";
/** Returns the boolean idicating if the two objects are from the same process. */
static BOOL IsFromSameProcess(id object1, id object2);
@interface EDOObject ()
/** The port to connect to the local socket. */
@property(nonatomic, readonly) EDOServicePort *servicePort;
/** The proxied object's address in the remote. */
@property(nonatomic, readonly, assign) EDOPointerType remoteAddress;
/** The proxied object's class object in the remote. */
@property(nonatomic, readonly, assign) EDOPointerType remoteClass;
/** The proxied object's class name in the remote. */
@property(nonatomic, readonly) NSString *className;
/** The process unique identifier for the remote object. */
@property(nonatomic, readonly) NSString *processUUID;
@end
@implementation EDOObject
+ (BOOL)supportsSecureCoding {
return YES;
}
// Initialize a remote object on the host side that is going to be sent back to the client side.
+ (instancetype)objectWithTarget:(id)target port:(EDOServicePort *)port {
return [[self alloc] edo_initWithLocalObject:target port:port];
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
NSAssert(sizeof(EDOPointerType) >= sizeof(void *), @"The pointer size is not big enough.");
_servicePort = [aDecoder decodeObjectOfClass:[EDOServicePort class]
forKey:kEDOObjectCoderPortKey];
_remoteAddress = [aDecoder decodeInt64ForKey:kEDOObjectCoderRemoteAddressKey];
_remoteClass = [aDecoder decodeInt64ForKey:kEDOObjectCoderRemoteClassKey];
_className = [aDecoder decodeObjectOfClass:[NSString class] forKey:kEDOObjectCoderClassNameKey];
_local = NO;
_processUUID = [aDecoder decodeObjectOfClass:[NSString class]
forKey:kEDOObjectCoderProcessUUIDKey];
return self;
}
- (BOOL)isLocalEdo {
return [self.processUUID isEqualToString:[EDOObject edo_processUUID]];
}
- (BOOL)weaklyReferenced {
return [self.className isEqualToString:@"EDOWeakObject"];
}
- (id)returnByValue {
return [[EDOValueObject alloc] initWithRemoteObject:self];
}
- (id)remoteWeak {
[[NSException exceptionWithName:EDOWeakObjectRemoteWeakMisuseException
reason:@"Calling remoteWeak on a remote object."
userInfo:nil] raise];
return self;
}
- (void)doesNotRecognizeSelector:(SEL)sel {
[[NSException
exceptionWithName:EDOObjectCalledUnrecognizedSelectorException
reason:[NSString
stringWithFormat:
@"eDO failed to proxy a method invocation because the proxied "
@"class %@ doesn't have the %s method",
[self className], sel_getName(sel)]
userInfo:nil] raise];
}
// No-op when called on a remote object.
- (id)passByValue {
return self;
}
- (instancetype)edo_initWithLocalObject:(id)target port:(EDOServicePort *)port {
_servicePort = port;
_remoteAddress = (EDOPointerType)target;
_remoteClass = (EDOPointerType)(__bridge void *)object_getClass(target);
_className = NSStringFromClass(object_getClass(target));
_processUUID = [EDOObject edo_processUUID];
_local = YES;
return self;
}
#pragma mark - Class method proxy
- (instancetype)alloc {
return [self edo_forwardInvocationForSelector:_cmd];
}
- (instancetype)allocWithZone:(NSZone *)zone {
// +[allocWithZone:] is deprecated, forwarding to +[alloc]
return [self alloc];
}
#pragma mark - Instance method proxy
// Forward the invocation to -[copy].
- (instancetype)copyWithZone:(NSZone *)zone {
return [self copy];
}
// Forward the invocation to -[mutableCopy].
- (instancetype)mutableCopyWithZone:(NSZone *)zone {
return [self mutableCopy];
}
- (instancetype)copy {
return [self edo_forwardInvocationForSelector:_cmd];
}
- (instancetype)mutableCopy {
return [self edo_forwardInvocationForSelector:_cmd];
}
- (void)dealloc {
// Send a release message only when the object is not from the same process.
if (![self isLocal] && ![self isLocalEdo]) {
// Release the local edo manually to make sure the entry is removed from the cache.
[EDOClientService removeDistantObjectReference:self.remoteAddress];
@try {
EDOObjectReleaseRequest *request =
[EDOObjectReleaseRequest requestWithRemoteAddress:_remoteAddress];
[EDOClientService sendSynchronousRequest:request onPort:_servicePort.hostPort];
} @catch (NSException *e) {
// There's an error with the service or most likely it's dead.
// TODO(haowoo): Convert the exception to NSError and handle it accordingly.
}
}
}
- (id)forwardingTargetForSelector:(SEL)sel {
// TODO(haowoo): Use this to forward to the local object.
return nil;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
NSAssert(sizeof(int64_t) >= sizeof(void *), @"The pointer size is not big enough.");
[aCoder encodeObject:self.servicePort forKey:kEDOObjectCoderPortKey];
[aCoder encodeInt64:self.remoteAddress forKey:kEDOObjectCoderRemoteAddressKey];
[aCoder encodeInt64:self.remoteClass forKey:kEDOObjectCoderRemoteClassKey];
[aCoder encodeObject:self.className forKey:kEDOObjectCoderClassNameKey];
[aCoder encodeObject:self.processUUID forKey:kEDOObjectCoderProcessUUIDKey];
}
- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
}
if (IsFromSameProcess(self, object)) {
BOOL returnValue = NO;
NSInvocation *invocation = [self edo_invocationForSelector:_cmd];
[invocation setArgument:&object atIndex:2];
[self forwardInvocation:invocation];
[invocation getReturnValue:&returnValue];
return returnValue;
}
return NO;
}
- (BOOL)isKindOfClass:(Class)aClass {
if (!IsFromSameProcess(self, aClass)) {
NSString *warningMessage = [NSString
stringWithFormat:
@"EDO WARNING: %@'s class is being compared via isKindOfClass: with a class that "
@"doesn't belong to the process of the callee object. It will always return NO. To "
@"expect isKindOfClass: to return YES, please use a class object that is fetched by "
@"-[EDOClientService classObjectWithName:hostPort:].",
self.className];
// Starting with Xcode 16.3, errors thrown by Swift functions are captured by Swift testing and
// go through a backtracing step, where the @c EDOObject is force cast to a @c Swift.Error,
// resulting in a crash. To get around this, the check is forwarded to the actual object.
// Note that only checks for @c NSError are forwarded to address this specific case. Attempts to
// forward other checks, most notably for @c NSArray, have resulted in exceptions and require
// further investigation.
if (aClass != NSError.class) {
NSLog(@"%@", warningMessage);
return NO;
}
NSString *className = NSStringFromClass(aClass);
aClass = [EDOClientService classObjectWithName:className hostPort:self.servicePort.hostPort];
if (!aClass) {
NSLog(@"%@", warningMessage);
return NO;
}
}
NSInvocation *invocation = [self edo_invocationForSelector:_cmd];
[invocation setArgument:&aClass atIndex:2];
[self forwardInvocation:invocation];
BOOL returnValue = NO;
[invocation getReturnValue:&returnValue];
return returnValue;
}
- (id)arrayWithObjects:(id)object1, ... {
NSLog(@"EDO WARNING: You are making remote invocation to variadic method [%@ arrayWithObjects:]. "
@"Only the first argument will be passed to the remote execution, the rest of the "
@"arguments will be dropped. Use +arrayWithArray: or NSMutableArray instead.",
self.className);
NSInvocation *invocation = [self edo_invocationForSelector:@selector(arrayWithObject:)];
[invocation setArgument:&object1 atIndex:2];
[self forwardInvocation:invocation];
id __unsafe_unretained returnValue = nil;
[invocation getReturnValue:&returnValue];
return returnValue;
}
- (id)objectAtIndex:(NSUInteger)index {
NSMethodSignature *methodSignature = [self methodSignatureForSelector:_cmd];
if (EDO_IS_OBJPOINTER(methodSignature.methodReturnType)) {
// Objective-C bridged Swift array returns Unmanaged<AnyObject>. Unmanaged<AnyObject> is
// interpreted as pointer by method signature and fails the eDO type check. To bypass the
// validation check, eDO redirects invocations to objectAtIndex: to objectsAtIndexes:.
Class arrayClass = [EDOClientService classObjectWithName:@"NSArray"
hostPort:self.servicePort.hostPort];
if ([self isKindOfClass:arrayClass]) {
NSIndexSet *indexSet = [NSIndexSet indexSetWithIndex:index];
return [((NSArray *)self) objectsAtIndexes:indexSet].firstObject;
} else {
// The redirection only works for array type and its subclasses. Users custom type which
// has objectAtIndex: and returns Unmanaged<AnyObject> will eventually enter this branch. This
// case is currently not supported, please file a bug to eDO if you see this message.
NSAssert(NO,
@"The non array remote class %@ declares objectAtIndex: to return object pointer, "
@"which is not supported by eDO.",
self.className);
return nil;
}
} else {
// If the return type is not object pointer, eDO simply forwards the invocation to the original
// method.
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
invocation.target = self;
invocation.selector = _cmd;
[invocation setArgument:&index atIndex:2];
[self forwardInvocation:invocation];
id __unsafe_unretained returnValue;
[invocation getReturnValue:&returnValue];
return returnValue;
}
}
- (id)objectAtIndexedSubscript:(NSUInteger)index {
return [self objectAtIndex:index];
}
/** Overrides NSDictionary enumeration to bypass the restriction of the C pointer. */
- (void)enumerateKeysAndObjectsUsingBlock:(void (^)(id key, id obj, BOOL *stop))block {
NSMethodSignature *methodSignature = [self methodSignatureForSelector:_cmd];
if (methodSignature) {
__block BOOL stopped = NO;
id modifiedBlock = ^(id key, id obj, NSUInteger unusedStop) {
if (!stopped) {
block(key, obj, &stopped);
}
};
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
invocation.target = self;
invocation.selector = _cmd;
[invocation setArgument:&modifiedBlock atIndex:2];
[self forwardInvocation:invocation];
}
}
- (NSUInteger)hash {
NSUInteger remoteHash = 0;
NSInvocation *invocation = [self edo_invocationForSelector:_cmd];
[self forwardInvocation:invocation];
[invocation getReturnValue:&remoteHash];
return remoteHash;
}
- (NSString *)description {
return [self edo_forwardInvocationForSelector:_cmd];
}
/**
* The override for __SwiftNativeNSError Swift-only method.
*
* This method is being called when a Swift error is propagated beyond the Swift XCTestCase. The
* proxied error doesn't reveal useful information as the local Swift error, so eDO rethrows the
* error message through its client error handler.
*/
- (BOOL)xct_shouldBeRecordedAsTestFailure {
NSDictionary<NSErrorUserInfoKey, id> *userInfo = @{
EDOErrorSwiftErrorDescription : [(id)self localizedDescription],
};
NSError *error = [NSError errorWithDomain:EDOServiceErrorDomain
code:EDOServiceErrorSwiftErrorThrow
userInfo:userInfo];
EDOExportEDOClientError(error);
return NO;
}
#pragma mark - NSFastEnumeration
/**
* Implement the fast enumeration protocol that works for the remote container like NSArray, NSSet
* and NSDictionary who implements "slow" enumeration's NSEnumerator. This doesn't provide the
* performance gain usually given by the fast enumeration but a syntax benefit such that the
* existing code will also work.
*
* @param state Context information that is used in the enumeration to, in addition to other
* possibilities, ensure that the collection has not been mutated.
* @param buffer A C array of objects over which the sender is to iterate.
* @param len The maximum number of objects to return in stackbuf.
*
* @return The number of objects returned in stackbuf, or 0 when the iteration is finished.
*/
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(id __unsafe_unretained _Nullable[_Nonnull])buffer
count:(NSUInteger)len {
// state 0: it is entering the enumeration, setting up the internal state.
if (state->state == 0) {
// Detecting concurrent mutations remotely isn't supported. We only make sure the remote address
// doesn't change during the enumeration. The fast enumeration algorithm checks the value its
// mutationsPtr points to.
state->mutationsPtr = (unsigned long *)&_remoteAddress;
// We use keyEnumerator or objectEnumerator to enumerate the remote container.
// extra[0] to point to the enumerator and hold a strong reference of it.
if ([self methodSignatureForSelector:@selector(keyEnumerator)]) {
*((CFTypeRef *)(state->extra)) = CFBridgingRetain([(id)self keyEnumerator]);
} else if ([self methodSignatureForSelector:@selector(objectEnumerator)]) {
*((CFTypeRef *)(state->extra)) = CFBridgingRetain([(id)self objectEnumerator]);
} else {
BOOL implementsFastEnumeration =
![self methodSignatureForSelector:@selector(countByEnumeratingWithState:objects:count:)];
NSString *reason = implementsFastEnumeration
? @"Fast enumeration is not supported on custom types."
: @"Fast enumeration is not supported on the current object.";
[[NSException exceptionWithName:NSInternalInconsistencyException reason:reason
userInfo:nil] raise];
return 0;
}
// The enumeration has started.
state->state = 1;
}
NSEnumerator *enumerator = (__bridge NSEnumerator *)(void *)state->extra[0];
id nextObject = [enumerator nextObject];
if (!nextObject) {
objc_setAssociatedObject(enumerator, &_cmd, nil, OBJC_ASSOCIATION_RETAIN);
CFRelease((void *)state->extra[0]);
return 0;
} else {
objc_setAssociatedObject(enumerator, &_cmd, nextObject, OBJC_ASSOCIATION_RETAIN);
}
// We only return one object for each call.
buffer[0] = nextObject;
state->itemsPtr = &buffer[0];
return 1;
}
#pragma mark - NSCoder overrides
// Overrides the NSCoder/NSKeyedArchiver so those won't get proxied.
// TODO(haowoo): Replace with other NSCoders.
- (id)replacementObjectForCoder:(NSCoder *)aCoder {
return self;
}
- (id)replacementObjectForKeyedArchiver:(NSKeyedArchiver *)archiver {
return self;
}
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
return self;
}
- (Class)classForCoder {
return nil;
}
- (Class)classForKeyedArchiver {
return nil;
}
+ (Class)classForKeyedUnarchiver {
return [self class];
}
+ (NSArray<NSString *> *)classFallbacksForKeyedArchiver {
return nil;
}
#pragma mark - Private
+ (NSString *)edo_processUUID {
static NSString *gProcessUUID;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
gProcessUUID = NSProcessInfo.processInfo.globallyUniqueString;
});
return gProcessUUID;
}
+ (EDOObject *)edo_remoteProxyFromUnderlyingObject:(id)underlyingObject
withPort:(EDOServicePort *)port {
return [[self alloc] edo_initWithLocalObject:underlyingObject port:port];
}
/**
* Forwards the @c selector to the remote underlying object.
*
* @param selector The selector to forward to the underlying remote object.
* @return The object returned by the remote invocation.
*/
- (id)edo_forwardInvocationForSelector:(SEL)selector {
NSInvocation *invocation = [self edo_invocationForSelector:selector];
[self forwardInvocation:invocation];
id __unsafe_unretained returnObject;
[invocation getReturnValue:&returnObject];
return returnObject;
}
/**
* Creates NSInvocation with the @c selector by fetching the method signature from the underlying
* remote object.
*
* @param selector The selector to forward to the underlying remote object.
* @return The NSInvocation instance that can be passed to [self -forwardInvocation:].
*/
- (NSInvocation *)edo_invocationForSelector:(SEL)selector {
NSMethodSignature *methodSignature = [self methodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
invocation.target = self;
invocation.selector = selector;
return invocation;
}
// This dummy method is to resolve an issue that this may be invoked during encoding, which
// leads to an unexpected message forwarding.
// TODO(ynzhang): this is a workaround and will be removed after root cause is fixed.
- (id)_accessibilityAttributedLocalizedString {
return nil;
}
@end
static BOOL IsFromSameProcess(id object1, id object2) {
Class edoClass = [EDOObject class];
return ([object1 class] != edoClass && [object2 class] != edoClass) ||
([object1 class] == edoClass && [object2 class] == edoClass &&
[((EDOObject *)object1).processUUID isEqual:((EDOObject *)object2).processUUID]);
}