Implement struct arguments for methods

Summary:
Before invoking TurboModule ObjC methods, we loop through all the arguments and transform them from `jsi::Value` to ObjC data structures. `jsi::Value`s that represent JS Objects get converted to `NSDictionary`s in ObjC. This isn't good enough because `NSDictionary` isn't typed. What we really need is a C/C++ struct that represents the type of the JS Object.

Therefore, for every argument of a TurboModule method that is a JS Object, this diff allows you to specify a method on `RCTCxxConvert` (which you have to declare and implement) that transforms that type of JS Object into something else. Basically, with the changes in this diff, we'll be able to transform JS Objects into C++ struct instances, which gives us type safety for JS Object method argumetns to TurboModule functions.

I modified the codegen to also create a mapping from NativeModule method name => argument num => `RCTCxxModule` conversion selector. This way, the FB codegen that generates the `RCTCxxConversion` function also informs our TurboModule system which conversion function to use before we call the method requiring complex argument. I just had to extend the `ObjCTurboModule` class to accomplish this.

The old system relies on the additional method generated by `RCT_EXPORT_METHOD` macro. It takes the written code, does string processing on it to parse the type of the struct arguments, and then replaces all instances of `::` with `_`. Super hacky. I didn't take this approach because it seemed unnecessarily hacky brittle.

The other approach I considered was to try to use reflection to infer the type of the struct at runtime. We'd have to do a bit of string processing and concat the TurboModule class name with the struct type. The solution would be simpler because it'd only modify the objective C and it wouldn't touch the Codegen system. **Edit:** I implemented it here: D14513078. The one downside of this design is that the name of the conversion function is individually constructed in two different locations in the code (i.e: no source of truth for this name). The other downside is that we have to rely on `RCTBridgeModuleNameForClass`, which we want to eliminate. This also computationally more expensive since it requires string processing/regex parsing and additional reflection. Therefore, we decided to move on with the current solution.

Reviewed By: fkgozali

Differential Revision: D14513429

fbshipit-source-id: 3d1b87e02ee908a19305686ff82b2ed624d8ac67
This commit is contained in:
Ramanpreet Nara
2019-03-21 17:31:40 -07:00
committed by Facebook Github Bot
parent 3e8d2a18d7
commit ef512194a6
2 changed files with 107 additions and 57 deletions

View File

@@ -14,6 +14,8 @@
#import <cxxreact/MessageQueueThread.h>
#import <jsireact/JSCallInvoker.h>
#import <jsireact/TurboModule.h>
#import <unordered_map>
#import <string>
#define RCT_IS_TURBO_MODULE_CLASS(klass) ((RCTTurboModuleEnabled() && [(klass) conformsToProtocol:@protocol(RCTTurboModule)]))
#define RCT_IS_TURBO_MODULE_INSTANCE(module) RCT_IS_TURBO_MODULE_CLASS([(module) class])
@@ -36,6 +38,22 @@ public:
size_t count) override;
id<RCTTurboModule> instance_;
protected:
void setMethodArgConversionSelector(NSString *methodName, int argIndex, NSString *fnName);
private:
NSMutableDictionary<NSString *, NSMutableArray *> *methodArgConversionSelectors_;
NSInvocation *getMethodInvocation(
jsi::Runtime &runtime,
TurboModuleMethodValueKind valueKind,
const id<RCTTurboModule> module,
std::shared_ptr<JSCallInvoker> jsInvoker,
const std::string& methodName,
SEL selector,
const jsi::Value *args,
size_t count,
NSMutableArray *retainedObjectsForInvocation);
BOOL hasMethodArgConversionSelector(NSString *methodName, int argIndex);
SEL getMethodArgConversionSelector(NSString *methodName, int argIndex);
};
} // namespace react

View File

@@ -7,6 +7,7 @@
#import "RCTTurboModule.h"
#import <objc/message.h>
#import <objc/runtime.h>
#import <sstream>
#import <vector>
@@ -17,6 +18,8 @@
#import <jsireact/LongLivedObject.h>
#import <jsireact/TurboModule.h>
#import <jsireact/TurboModuleUtils.h>
#import <React/RCTCxxConvert.h>
#import <React/RCTManagedPointer.h>
using namespace facebook;
@@ -290,51 +293,31 @@ SEL resolveMethodSelector(
// Notes:
// - This may be expensive lookup. The codegen output should specify the exact selector name.
// - Some classes may have strictly typed arg that isn't compatible with plain NSDictionary/NSArray. For those, allow a helper method
// with "__turbo__" prefix (hand-written) that can do the translation from plain NSDictionary/NSArray to the stricter type.
// This is only for migration purpose.
if (adjustedCount == 0) {
SEL turboSelector = NSSelectorFromString([NSString stringWithFormat:@"__turbo__%s", methodName.c_str()]);
if ([module respondsToSelector:turboSelector]) {
selector = turboSelector;
} else {
selector = NSSelectorFromString([NSString stringWithUTF8String:methodName.c_str()]);
if (![module respondsToSelector:selector]) {
throw std::runtime_error("Unable to find method: " + methodName + " for module: " + moduleName + ". Make sure the module is installed correctly.");
}
selector = NSSelectorFromString([NSString stringWithUTF8String:methodName.c_str()]);
if (![module respondsToSelector:selector]) {
throw std::runtime_error("Unable to find method: " + methodName + " for module: " + moduleName + ". Make sure the module is installed correctly.");
}
} else if (adjustedCount == 1) {
SEL turboSelector = NSSelectorFromString([NSString stringWithFormat:@"__turbo__%s:", methodName.c_str()]);
if ([module respondsToSelector:turboSelector]) {
selector = turboSelector;
} else {
selector = NSSelectorFromString([NSString stringWithFormat:@"%s:", methodName.c_str()]);
if (![module respondsToSelector:selector]) {
throw std::runtime_error("Unable to find method: " + methodName + " for module: " + moduleName + ". Make sure the module is installed correctly.");
}
selector = NSSelectorFromString([NSString stringWithFormat:@"%s:", methodName.c_str()]);
if (![module respondsToSelector:selector]) {
throw std::runtime_error("Unable to find method: " + methodName + " for module: " + moduleName + ". Make sure the module is installed correctly.");
}
} else {
SEL turboSelector = nil;
unsigned int numberOfMethods;
Method *methods = class_copyMethodList([module class], &numberOfMethods);
if (methods) {
NSString *methodPrefix = [NSString stringWithFormat:@"%s:", methodName.c_str()];
NSString *turboMethodPrefix = [NSString stringWithFormat:@"__turbo__%s:", methodName.c_str()];
for (unsigned int i = 0; i < numberOfMethods; i++) {
SEL s = method_getName(methods[i]);
NSString *objcMethodName = NSStringFromSelector(s);
if ([objcMethodName hasPrefix:methodPrefix]) {
selector = s;
} else if ([objcMethodName hasPrefix:turboMethodPrefix]) {
turboSelector = s;
break;
}
}
free(methods);
}
if (turboSelector) {
selector = turboSelector;
}
if (!selector) {
throw std::runtime_error("Unable to find method: " + methodName + " for module: " + moduleName + ". Make sure the module is installed correctly.");
}
@@ -343,33 +326,6 @@ SEL resolveMethodSelector(
return selector;
}
NSInvocation *getMethodInvocation(
jsi::Runtime &runtime,
TurboModuleMethodValueKind valueKind,
const id<RCTTurboModule> module,
std::shared_ptr<JSCallInvoker> jsInvoker,
SEL selector,
const jsi::Value *args,
size_t count) {
NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[[module class] instanceMethodSignatureForSelector:selector]];
[inv setSelector:selector];
for (size_t i = 0; i < count; i++) {
const jsi::Value *arg = &args[i];
if (arg->isBool()) {
bool v = arg->getBool();
[inv setArgument:(void *)&v atIndex:i + 2];
} else if (arg->isNumber()) {
double v = arg->getNumber();
[inv setArgument:(void *)&v atIndex:i + 2];
} else {
id v = convertJSIValueToObjCObject(runtime, *arg, jsInvoker);
[inv setArgument:(void *)&v atIndex:i + 2];
}
}
[inv retainArguments];
return inv;
}
/**
* Perform method invocation on a specific queue as configured by the module class.
* This serves as a backward-compatible support for RCTBridgeModule's methodQueue API.
@@ -384,12 +340,14 @@ jsi::Value performMethodInvocation(
NSInvocation *inv,
TurboModuleMethodValueKind valueKind,
const id<RCTTurboModule> module,
std::shared_ptr<JSCallInvoker> jsInvoker) {
std::shared_ptr<JSCallInvoker> jsInvoker,
NSMutableArray *retainedObjectsForInvocation) {
__block void *rawResult = NULL;
jsi::Runtime *rt = &runtime;
void (^block)() = ^{
[inv invokeWithTarget:module];
[retainedObjectsForInvocation removeAllObjects];
if (valueKind == VoidKind) {
return;
@@ -448,6 +406,49 @@ jsi::Value performMethodInvocation(
} // namespace
NSInvocation *ObjCTurboModule::getMethodInvocation(
jsi::Runtime &runtime,
TurboModuleMethodValueKind valueKind,
const id<RCTTurboModule> module,
std::shared_ptr<JSCallInvoker> jsInvoker,
const std::string& methodName,
SEL selector,
const jsi::Value *args,
size_t count,
NSMutableArray *retainedObjectsForInvocation) {
NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[[module class] instanceMethodSignatureForSelector:selector]];
[inv setSelector:selector];
for (size_t i = 0; i < count; i++) {
const jsi::Value *arg = &args[i];
if (arg->isBool()) {
bool v = arg->getBool();
[inv setArgument:(void *)&v atIndex:i + 2];
} else if (arg->isNumber()) {
double v = arg->getNumber();
[inv setArgument:(void *)&v atIndex:i + 2];
} else {
id v = convertJSIValueToObjCObject(runtime, *arg, jsInvoker);
NSString *methodNameObjc = @(methodName.c_str());
if ([v isKindOfClass:[NSDictionary class]] && hasMethodArgConversionSelector(methodNameObjc, i)) {
SEL methodArgConversionSelector = getMethodArgConversionSelector(methodNameObjc, i);
// Message dispatch logic from old infra (link: https://git.io/fjf3U)
RCTManagedPointer *(*convert)(id, SEL, id) = (__typeof__(convert))objc_msgSend;
RCTManagedPointer *box = convert([RCTCxxConvert class], methodArgConversionSelector, v);
void *pointer = box.voidPointer;
[inv setArgument:&pointer atIndex:i + 2];
[retainedObjectsForInvocation addObject:box];
} else {
[inv setArgument:(void *)&v atIndex:i + 2];
}
}
}
[inv retainArguments];
return inv;
}
ObjCTurboModule::ObjCTurboModule(
const std::string &name,
id<RCTTurboModule> instance,
@@ -462,7 +463,8 @@ jsi::Value ObjCTurboModule::invokeMethod(
const jsi::Value *args,
size_t count) {
SEL selector = resolveMethodSelector(valueKind, instance_, name_, methodName, count);
NSInvocation *inv = getMethodInvocation(runtime, valueKind, instance_, jsInvoker_, selector, args, count);
NSMutableArray *retainedObjectsForInvocation = [NSMutableArray new];
NSInvocation *inv = getMethodInvocation(runtime, valueKind, instance_, jsInvoker_, methodName, selector, args, count, retainedObjectsForInvocation);
if (valueKind == PromiseKind) {
// Promise return type is special cased today, i.e. it needs extra 2 function args for resolve() and reject(), to
@@ -476,11 +478,41 @@ jsi::Value ObjCTurboModule::invokeMethod(
[inv setArgument:(void *)&resolveBlock atIndex:count + 2];
[inv setArgument:(void *)&rejectBlock atIndex:count + 3];
// The return type becomes void in the ObjC side.
performMethodInvocation(rt, inv, VoidKind, instance_, jsInvoker_);
performMethodInvocation(rt, inv, VoidKind, instance_, jsInvoker_, retainedObjectsForInvocation);
});
}
return performMethodInvocation(runtime, inv, valueKind, instance_, jsInvoker_);
return performMethodInvocation(runtime, inv, valueKind, instance_, jsInvoker_, retainedObjectsForInvocation);
}
BOOL ObjCTurboModule::hasMethodArgConversionSelector(NSString *methodName, int argIndex) {
return methodArgConversionSelectors_ && methodArgConversionSelectors_[methodName] && ![methodArgConversionSelectors_[methodName][argIndex] isEqual:[NSNull null]];
}
SEL ObjCTurboModule::getMethodArgConversionSelector(NSString *methodName, int argIndex) {
assert(hasMethodArgConversionSelector(methodName, argIndex));
return (SEL)((NSValue *)methodArgConversionSelectors_[methodName][argIndex]).pointerValue;
}
void ObjCTurboModule::setMethodArgConversionSelector(NSString *methodName, int argIndex, NSString *fnName) {
if (!methodArgConversionSelectors_) {
methodArgConversionSelectors_ = [NSMutableDictionary new];
}
if (!methodArgConversionSelectors_[methodName]) {
auto metaData = methodMap_.at([methodName UTF8String]);
auto argCount = metaData.argCount;
methodArgConversionSelectors_[methodName] = [NSMutableArray arrayWithCapacity:argCount];
for (int i = 0; i < argCount; i += 1) {
[methodArgConversionSelectors_[methodName] addObject:[NSNull null]];
}
}
SEL selector = NSSelectorFromString(fnName);
NSValue *selectorValue = [NSValue valueWithPointer:selector];
methodArgConversionSelectors_[methodName][argIndex] = selectorValue;
}
} // namespace react