Introduce ObjCInteropTurboModule (#37367)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/37367

## Changes
This diff introduces ObjCInteropTurboModule.

ObjCInteropTurboModule implements method dispatch for legacy native modules (i.e: modules that aren't schematized).

**Note:** This method dispatch relies on the RCT_EXPORT_METHOD macros, not the TurboModule system's C++ codegen.

Changelog: [Internal]

Reviewed By: mdvacca

Differential Revision: D44807334

fbshipit-source-id: 8b6162181504ca9fbf8bf13a6951061936e0ff26
This commit is contained in:
Ramanpreet Nara
2023-05-10 15:58:13 -07:00
committed by Facebook GitHub Bot
parent f8929f8d54
commit f06632c83d
5 changed files with 794 additions and 13 deletions
@@ -0,0 +1,95 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#import <string>
#import <vector>
#import <ReactCommon/TurboModule.h>
#import <jsi/jsi.h>
#import "RCTTurboModule.h"
namespace facebook {
namespace react {
class JSI_EXPORT ObjCInteropTurboModule : public ObjCTurboModule {
public:
struct MethodDescriptor {
std::string methodName;
SEL selector;
int jsArgCount;
TurboModuleMethodValueKind jsReturnKind;
};
ObjCInteropTurboModule(const ObjCTurboModule::InitParams &params);
std::vector<facebook::jsi::PropNameID> getPropertyNames(facebook::jsi::Runtime &runtime) override;
protected:
jsi::Value create(jsi::Runtime &runtime, const jsi::PropNameID &propName) override;
/**
* Why is this overriden?
*
* Purpose: Converts native module method returns from Objective C values to JavaScript values.
*
* ObjCTurboModule converts returns by returnType. But, Legacy native modules convert returns by the Objective C type:
* React Native cannot infer a method's returnType from the RCT_EXPORT_METHOD annotations.
*/
jsi::Value convertReturnIdToJSIValue(
jsi::Runtime &runtime,
const char *methodName,
TurboModuleMethodValueKind returnType,
id result) override;
/**
* Why is this overriden?
*
* Purpose: Get a native module method's argument's type, given the method name, and argument index.
*
* This override is meant to serve as a performance optimization.
*
* ObjCTurboModule computes the method argument types from the RCT_EXPORT_METHOD macros lazily.
* ObjCInteropTurboModule computes all the method argument types eagerly on module init.
*
* ObjCInteropTurboModule overrides getArgumentTypeName, so ObjCTurboModule doesn't end up re-computing the argument
* type names again.
*/
NSString *getArgumentTypeName(jsi::Runtime &runtime, NSString *methodName, int argIndex) override;
/**
* Why is this overriden?
*
* Purpose: Convert arguments from JavaScript values to Objective C values. Assign the Objective C argument to the
* method invocation.
*
* ObjCTurboModule tries to minimize reliance on RCTConvert for argument conversion. Why: RCTConvert relies on the
* RCT_EXPORT_METHOD macros, which we want to remove long term. But, Legacy native modules rely heavily on RCTConvert
* for argument conversion.
*/
void setInvocationArg(
jsi::Runtime &runtime,
const char *methodName,
const std::string &objCArgType,
const jsi::Value &arg,
size_t i,
NSInvocation *inv,
NSMutableArray *retainedObjectsForInvocation) override;
private:
std::vector<MethodDescriptor> methodDescriptors_;
NSDictionary<NSString *, NSArray<NSString *> *> *methodArgumentTypeNames_;
jsi::Value constantsCache_;
const jsi::Value &getConstants(jsi::Runtime &runtime);
bool exportsConstants();
};
} // namespace react
} // namespace facebook
@@ -0,0 +1,654 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "RCTInteropTurboModule.h"
#import <objc/message.h>
#import <objc/runtime.h>
#import <React/RCTAssert.h>
#import <React/RCTConvert.h>
#import <React/RCTCxxConvert.h>
#import <React/RCTManagedPointer.h>
#import <React/RCTModuleMethod.h>
#import <React/RCTParserUtils.h>
namespace facebook {
namespace react {
namespace {
// This is used for generating short exception strings.
std::string getType(jsi::Runtime &rt, const jsi::Value &v)
{
if (v.isUndefined()) {
return "undefined";
} else if (v.isNull()) {
return "null";
} else if (v.isBool()) {
return v.getBool() ? "true" : "false";
} else if (v.isNumber()) {
return "number";
} else if (v.isString()) {
return "string";
} else if (v.isSymbol()) {
return "symbol";
} else if (v.isBigInt()) {
return "bigint";
} else if (v.isObject()) {
jsi::Object vObj = v.getObject(rt);
return vObj.isFunction(rt) ? "function" : vObj.isArray(rt) ? "array" : "object";
} else {
return "unknown";
}
}
std::vector<const RCTMethodInfo *> getMethodInfos(Class moduleClass)
{
std::vector<const RCTMethodInfo *> methodInfos;
Class cls = moduleClass;
while (cls && cls != [NSObject class] && cls != [NSProxy class]) {
unsigned int methodCount;
Method *methods = class_copyMethodList(object_getClass(cls), &methodCount);
for (unsigned int i = 0; i < methodCount; i++) {
Method method = methods[i];
SEL selector = method_getName(method);
if ([NSStringFromSelector(selector) hasPrefix:@"__rct_export__"]) {
IMP imp = method_getImplementation(method);
const RCTMethodInfo *methodInfo = ((const RCTMethodInfo *(*)(id, SEL))imp)(moduleClass, selector);
methodInfos.push_back(methodInfo);
}
}
free(methods);
cls = class_getSuperclass(cls);
}
return methodInfos;
}
NSString *getJSMethodName(const RCTMethodInfo *methodInfo)
{
std::string jsName = methodInfo->jsName;
if (jsName != "") {
return @(jsName.c_str());
}
NSString *methodName = @(methodInfo->objcName);
NSRange colonRange = [methodName rangeOfString:@":"];
if (colonRange.location != NSNotFound) {
methodName = [methodName substringToIndex:colonRange.location];
}
methodName = [methodName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
RCTAssert(
methodName.length,
@"%s is not a valid JS function name, please"
" supply an alternative using RCT_REMAP_METHOD()",
methodInfo->objcName);
return methodName;
}
class ObjCInteropTurboModuleParseException : public std::runtime_error {
public:
ObjCInteropTurboModuleParseException(std::string moduleName, std::string methodName, std::string message)
: std::runtime_error(
"Failed to create module \"" + moduleName + "\": Error while parsing method " + moduleName + "." +
methodName + ": " + message)
{
}
};
struct ExportedMethod {
NSString *methodName;
NSArray<NSString *> *argumentTypes;
std::string returnType;
SEL selector;
};
std::vector<ExportedMethod> parseExportedMethods(std::string moduleName, Class moduleClass)
{
std::vector<const RCTMethodInfo *> methodInfos = getMethodInfos(moduleClass);
std::vector<ExportedMethod> methods;
methods.reserve(methodInfos.size());
for (const RCTMethodInfo *methodInfo : methodInfos) {
NSString *jsMethodName = getJSMethodName(methodInfo);
NSArray<RCTMethodArgument *> *arguments;
SEL objCMethodSelector = NSSelectorFromString(RCTParseMethodSignature(methodInfo->objcName, &arguments));
NSMethodSignature *objCMethodSignature = [moduleClass instanceMethodSignatureForSelector:objCMethodSelector];
std::string objCMethodReturnType = [objCMethodSignature methodReturnType];
if (objCMethodSignature.numberOfArguments - 2 != [arguments count]) {
std::string message = "Parsed argument count (i.e: " + std::to_string([arguments count]) +
") != Objective C method signature argument count (i.e: " +
std::to_string(objCMethodSignature.numberOfArguments - 2) + ").";
throw ObjCInteropTurboModuleParseException(moduleName, [jsMethodName UTF8String], message);
}
NSMutableArray<NSString *> *argumentTypes = [NSMutableArray new];
for (NSUInteger i = 0; i < [arguments count]; i += 1) {
[argumentTypes addObject:arguments[i].type];
}
if ([argumentTypes count] == 1) {
std::string lastArgType = [argumentTypes[[argumentTypes count] - 1] UTF8String];
if (lastArgType == "RCTPromiseResolveBlock" || lastArgType == "RCTPromiseRejectBlock") {
std::string message =
"Methods that return promises must accept a RCTPromiseResolveBlock followed by a RCTPromiseRejectBlock. This method just accepts a " +
lastArgType + ".";
throw ObjCInteropTurboModuleParseException(moduleName, [jsMethodName UTF8String], message);
}
} else if ([argumentTypes count] > 1) {
std::string lastArgType = [argumentTypes[[argumentTypes count] - 1] UTF8String];
std::string secondLastArgType = [argumentTypes[[argumentTypes count] - 2] UTF8String];
if ((secondLastArgType == "RCTPromiseResolveBlock" && lastArgType != "RCTPromiseRejectBlock") ||
(secondLastArgType != "RCTPromiseResolveBlock" && lastArgType == "RCTPromiseRejectBlock")) {
std::string message =
"Methods that return promises must accept a RCTPromiseResolveBlock followed by a RCTPromiseRejectBlock. This method accepts a " +
secondLastArgType + " followed by a " + lastArgType + ".";
throw ObjCInteropTurboModuleParseException(moduleName, [jsMethodName UTF8String], message);
}
}
methods.push_back(
{.methodName = jsMethodName,
.argumentTypes = argumentTypes,
.returnType = objCMethodReturnType,
.selector = objCMethodSelector});
}
return methods;
}
SEL selectorForType(NSString *type)
{
const char *input = type.UTF8String;
return NSSelectorFromString([RCTParseType(&input) stringByAppendingString:@":"]);
}
template <typename T>
T RCTConvertTo(SEL selector, id json)
{
T (*convert)(id, SEL, id) = (__typeof__(convert))objc_msgSend;
return convert([RCTConvert class], selector, json);
}
} // namespace
ObjCInteropTurboModule::ObjCInteropTurboModule(const ObjCTurboModule::InitParams &params)
: ObjCTurboModule(params), constantsCache_(jsi::Value::undefined())
{
std::vector<ExportedMethod> methods = parseExportedMethods(name_, [params.instance class]);
methodDescriptors_.reserve(methods.size());
NSMutableDictionary<NSString *, NSArray<NSString *> *> *methodArgTypeNames = [NSMutableDictionary new];
methodArgumentTypeNames_ = methodArgTypeNames;
for (const ExportedMethod &method : methods) {
const int numArgs = [method.argumentTypes count];
const bool isPromiseMethod =
numArgs >= 2 && [method.argumentTypes[numArgs - 1] isEqualToString:@"RCTPromiseRejectBlock"];
const int jsArgCount = isPromiseMethod ? numArgs - 2 : numArgs;
/**
* In the TurboModule system, only promises and voids are special. So, set those.
* In the else case, just assume ObjectKind. This will be ignored by the interop layer.
* In the else case, the interop layer will just call into ::convertReturnIdToJSIValue()
*/
const TurboModuleMethodValueKind returnKind = isPromiseMethod ? PromiseKind
: method.returnType == @encode(void) ? VoidKind
: ObjectKind;
methodMap_[[method.methodName UTF8String]] = MethodMetadata{static_cast<size_t>(jsArgCount), nullptr};
for (NSUInteger i = 0; i < numArgs; i += 1) {
NSString *typeName = method.argumentTypes[i];
if ([typeName hasPrefix:@"JS::"]) {
NSString *rctCxxConvertSelector =
[[typeName stringByReplacingOccurrencesOfString:@"::" withString:@"_"] stringByAppendingString:@":"];
setMethodArgConversionSelector(method.methodName, i, rctCxxConvertSelector);
}
}
methodArgTypeNames[method.methodName] = method.argumentTypes;
methodDescriptors_.push_back({
.methodName = [method.methodName UTF8String],
.selector = method.selector,
.jsArgCount = jsArgCount,
.jsReturnKind = returnKind,
});
}
if ([params.instance respondsToSelector:@selector(constantsToExport)]) {
methodDescriptors_.push_back({
.methodName = "getConstants", .selector = @selector(constantsToExport), .jsArgCount = 0,
.jsReturnKind = ObjectKind,
});
} else {
static SEL getConstantsSelector = NSSelectorFromString(@"getConstants");
if ([params.instance respondsToSelector:getConstantsSelector]) {
methodDescriptors_.push_back({
.methodName = "getConstants",
.selector = getConstantsSelector,
.jsArgCount = 0,
.jsReturnKind = ObjectKind,
});
}
}
}
jsi::Value ObjCInteropTurboModule::create(jsi::Runtime &runtime, const jsi::PropNameID &propName)
{
for (size_t i = 0; i < methodDescriptors_.size(); i += 1) {
if (methodDescriptors_[i].methodName == propName.utf8(runtime)) {
if (propName.utf8(runtime) == "getConstants") {
return jsi::Function::createFromHostFunction(
runtime,
propName,
static_cast<unsigned int>(methodDescriptors_[i].jsArgCount),
[this, i](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) mutable {
if (!this->constantsCache_.isUndefined()) {
return jsi::Value(rt, this->constantsCache_);
}
// TODO: Dispatch getConstants to the main queue, if the module requires main queue setup
jsi::Value ret = this->invokeObjCMethod(
rt,
this->methodDescriptors_[i].jsReturnKind,
this->methodDescriptors_[i].methodName,
this->methodDescriptors_[i].selector,
args,
count);
bool isRetValid = ret.isUndefined() || ret.isNull() ||
(ret.isObject() && !ret.asObject(rt).isFunction(rt) && !ret.asObject(rt).isArray(rt));
if (!isRetValid) {
std::string methodJsSignature = name_ + ".getConstants()";
std::string errorPrefix = methodJsSignature + ": ";
throw jsi::JSError(
rt,
errorPrefix + "Expected return value to be null, undefined, or a plain object. But, got: " +
getType(rt, ret));
}
if (ret.isUndefined() || ret.isNull()) {
this->constantsCache_ = jsi::Object(rt);
} else {
this->constantsCache_ = jsi::Value(rt, ret);
}
return ret;
});
}
return jsi::Function::createFromHostFunction(
runtime,
propName,
static_cast<unsigned int>(methodDescriptors_[i].jsArgCount),
[this, i](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) {
return this->invokeObjCMethod(
rt,
this->methodDescriptors_[i].jsReturnKind,
this->methodDescriptors_[i].methodName,
this->methodDescriptors_[i].selector,
args,
count);
});
}
}
jsi::Object constants = getConstants(runtime).asObject(runtime);
jsi::Value constant = constants.getProperty(runtime, propName);
if (!constant.isUndefined()) {
// TODO(T145105887): Output warning. Tried to access a constant as a
// property on the native module object. Please migrate to getConstants().
}
return constant;
}
void ObjCInteropTurboModule::setInvocationArg(
jsi::Runtime &runtime,
const char *methodNameCStr,
const std::string &objCArgType,
const jsi::Value &jsiArg,
size_t index,
NSInvocation *inv,
NSMutableArray *retainedObjectsForInvocation)
{
NSString *methodName = @(methodNameCStr);
std::string methodJsSignature = name_ + "." + methodNameCStr + "()";
NSString *argumentType = getArgumentTypeName(runtime, methodName, index);
std::string errorPrefix = methodJsSignature + ": Error while converting JavaScript argument " +
std::to_string(index) + " to Objective C type " + [argumentType UTF8String] + ". ";
SEL selector = selectorForType(argumentType);
if ([RCTConvert respondsToSelector:selector]) {
id objCArg = TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_);
if (objCArgType == @encode(char)) {
char arg = RCTConvertTo<char>(selector, objCArg);
[inv setArgument:&arg atIndex:(index) + 2];
return;
}
if (objCArgType == @encode(unsigned char)) {
unsigned char arg = RCTConvertTo<unsigned char>(selector, objCArg);
[inv setArgument:&arg atIndex:(index) + 2];
return;
}
if (objCArgType == @encode(short)) {
short arg = RCTConvertTo<short>(selector, objCArg);
[inv setArgument:&arg atIndex:(index) + 2];
return;
}
if (objCArgType == @encode(unsigned short)) {
unsigned short arg = RCTConvertTo<unsigned short>(selector, objCArg);
[inv setArgument:&arg atIndex:(index) + 2];
return;
}
if (objCArgType == @encode(int)) {
int arg = RCTConvertTo<int>(selector, objCArg);
[inv setArgument:&arg atIndex:(index) + 2];
return;
}
if (objCArgType == @encode(unsigned int)) {
unsigned int arg = RCTConvertTo<unsigned int>(selector, objCArg);
[inv setArgument:&arg atIndex:(index) + 2];
return;
}
if (objCArgType == @encode(long)) {
long arg = RCTConvertTo<long>(selector, objCArg);
[inv setArgument:&arg atIndex:(index) + 2];
return;
}
if (objCArgType == @encode(unsigned long)) {
unsigned long arg = RCTConvertTo<unsigned long>(selector, objCArg);
[inv setArgument:&arg atIndex:(index) + 2];
return;
}
if (objCArgType == @encode(long long)) {
long long arg = RCTConvertTo<long long>(selector, objCArg);
[inv setArgument:&arg atIndex:(index) + 2];
return;
}
if (objCArgType == @encode(unsigned long long)) {
unsigned long long arg = RCTConvertTo<unsigned long long>(selector, objCArg);
[inv setArgument:&arg atIndex:(index) + 2];
return;
}
if (objCArgType == @encode(float)) {
float arg = RCTConvertTo<float>(selector, objCArg);
[inv setArgument:&arg atIndex:(index) + 2];
return;
}
if (objCArgType == @encode(double)) {
double arg = RCTConvertTo<double>(selector, objCArg);
[inv setArgument:&arg atIndex:(index) + 2];
return;
}
if (objCArgType == @encode(BOOL)) {
BOOL arg = RCTConvertTo<BOOL>(selector, objCArg);
[inv setArgument:&arg atIndex:(index) + 2];
return;
}
if (objCArgType == @encode(SEL)) {
SEL arg = RCTConvertTo<SEL>(selector, objCArg);
[inv setArgument:&arg atIndex:(index) + 2];
return;
}
if (objCArgType == @encode(const char *)) {
const char *arg = RCTConvertTo<const char *>(selector, objCArg);
[inv setArgument:&arg atIndex:(index) + 2];
return;
}
if (objCArgType == @encode(void *)) {
void *arg = RCTConvertTo<void *>(selector, objCArg);
[inv setArgument:&arg atIndex:(index) + 2];
return;
}
if (objCArgType == @encode(id)) {
id arg = RCTConvertTo<id>(selector, objCArg);
if (arg) {
[retainedObjectsForInvocation addObject:arg];
}
[inv setArgument:&arg atIndex:(index) + 2];
return;
}
if (objCArgType[0] == _C_STRUCT_B) {
NSMethodSignature *typeSignature = [RCTConvert methodSignatureForSelector:selector];
NSInvocation *typeInvocation = [NSInvocation invocationWithMethodSignature:typeSignature];
typeInvocation.selector = selector;
typeInvocation.target = [RCTConvert class];
void *returnValue = malloc(typeSignature.methodReturnLength);
if (!returnValue) {
// CWE - 391 : Unchecked error condition
// https://www.cvedetails.com/cwe-details/391/Unchecked-Error-Condition.html
// https://eli.thegreenplace.net/2009/10/30/handling-out-of-memory-conditions-in-c
abort();
}
[typeInvocation setArgument:&objCArg atIndex:2];
[typeInvocation invoke];
[typeInvocation getReturnValue:returnValue];
[inv setArgument:returnValue atIndex:index + 2];
free(returnValue);
return;
}
const char *BLOCK_TYPE = @encode(__typeof__(^{
}));
if (objCArgType == BLOCK_TYPE) {
/**
* RCTModuleMethod doesn't actually call into RCTConvert in this case.
*/
id arg = [objCArg copy];
if (arg) {
[retainedObjectsForInvocation addObject:arg];
}
[inv setArgument:&arg atIndex:(index) + 2];
return;
}
throw jsi::JSError(runtime, errorPrefix + "Objective C type " + [argumentType UTF8String] + " is unsupported.");
}
if ([argumentType isEqualToString:@"RCTResponseSenderBlock"]) {
if (!(jsiArg.isObject() && jsiArg.asObject(runtime).isFunction(runtime))) {
throw jsi::JSError(
runtime, errorPrefix + "JavaScript argument must be a function. Got " + getType(runtime, jsiArg));
}
RCTResponseSenderBlock arg =
(RCTResponseSenderBlock)TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_);
if (arg) {
[retainedObjectsForInvocation addObject:arg];
}
[inv setArgument:&arg atIndex:(index) + 2];
return;
}
if ([argumentType isEqualToString:@"RCTResponseErrorBlock"]) {
if (!(jsiArg.isObject() && jsiArg.asObject(runtime).isFunction(runtime))) {
throw jsi::JSError(
runtime, errorPrefix + "JavaScript argument must be a function. Got " + getType(runtime, jsiArg));
}
RCTResponseSenderBlock senderBlock =
(RCTResponseSenderBlock)TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_);
RCTResponseErrorBlock arg = ^(NSError *error) {
senderBlock(@[ RCTJSErrorFromNSError(error) ]);
};
[retainedObjectsForInvocation addObject:arg];
[inv setArgument:&arg atIndex:(index) + 2];
return;
}
if ([argumentType isEqualToString:@"RCTPromiseResolveBlock"] ||
[argumentType isEqualToString:@"RCTPromiseRejectBlock"]) {
throw jsi::JSError(
runtime,
errorPrefix + "The TurboModule interop layer should not convert JavaScript arguments to " +
[argumentType UTF8String] +
" inside ObjCinteropTurboModule::setInvocationArg(). Please report this as an issue.");
}
if ([argumentType hasPrefix:@"JS::"]) {
NSString *selectorNameForCxxType =
[[argumentType stringByReplacingOccurrencesOfString:@"::" withString:@"_"] stringByAppendingString:@":"];
selector = NSSelectorFromString(selectorNameForCxxType);
bool isPlainObject = jsiArg.isObject() && !jsiArg.asObject(runtime).isFunction(runtime) &&
!jsiArg.asObject(runtime).isArray(runtime);
if (!isPlainObject) {
throw jsi::JSError(
runtime, errorPrefix + "JavaScript argument must be an object. Got " + getType(runtime, jsiArg));
}
id arg = TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_);
RCTManagedPointer *(*convert)(id, SEL, id) = (__typeof__(convert))objc_msgSend;
RCTManagedPointer *box = convert([RCTCxxConvert class], selector, arg);
void *pointer = box.voidPointer;
[inv setArgument:&pointer atIndex:index + 2];
[retainedObjectsForInvocation addObject:box];
return;
}
}
jsi::Value ObjCInteropTurboModule::convertReturnIdToJSIValue(
jsi::Runtime &runtime,
const char *methodNameCStr,
TurboModuleMethodValueKind returnType,
id result)
{
std::string methodJsSignature = name_ + "." + methodNameCStr + "()";
std::string errorPrefix =
methodJsSignature + ": Error while converting return Objective C value to JavaScript type. ";
if (returnType == VoidKind) {
return jsi::Value::undefined();
}
if (result == (id)kCFNull || result == nil) {
return jsi::Value::null();
}
jsi::Value returnValue = TurboModuleConvertUtils::convertObjCObjectToJSIValue(runtime, result);
if (!returnValue.isUndefined()) {
return returnValue;
}
throw jsi::JSError(runtime, methodJsSignature + "Objective C type was unsupported.");
}
NSString *ObjCInteropTurboModule::getArgumentTypeName(jsi::Runtime &runtime, NSString *methodName, int argIndex)
{
const char *methodNameCStr = [methodName UTF8String];
std::string methodJsSignature = name_ + "." + methodNameCStr + "()";
std::string errorPrefix =
methodJsSignature + ": Error while trying to get Objective C type of parameter " + std::to_string(argIndex) + ".";
if (methodArgumentTypeNames_[methodName] == nil) {
throw jsi::JSError(runtime, errorPrefix + "No parameter types found for method.");
}
if ([methodArgumentTypeNames_[methodName] count] <= argIndex) {
int paramCount = [methodArgumentTypeNames_[methodName] count];
throw jsi::JSError(runtime, errorPrefix + "Method has only " + std::to_string(paramCount) + " parameter types.");
}
return methodArgumentTypeNames_[methodName][argIndex];
}
bool ObjCInteropTurboModule::exportsConstants()
{
for (size_t i = 0; i < methodDescriptors_.size(); i += 1) {
if (methodDescriptors_[i].methodName == "getConstants") {
return true;
}
}
return false;
}
const jsi::Value &ObjCInteropTurboModule::getConstants(jsi::Runtime &runtime)
{
if (!constantsCache_.isUndefined()) {
return constantsCache_;
}
if (!exportsConstants()) {
constantsCache_ = jsi::Object(runtime);
return constantsCache_;
}
jsi::Value getConstantsProp = get(runtime, jsi::PropNameID::forAscii(runtime, "getConstants"));
if (getConstantsProp.isObject()) {
jsi::Object getConstantsObj = getConstantsProp.asObject(runtime);
if (getConstantsObj.isFunction(runtime)) {
jsi::Function getConstantsFn = getConstantsObj.asFunction(runtime);
getConstantsFn.call(runtime);
return constantsCache_;
}
}
// Unable to invoke the getConstants() method.
// Maybe the module didn't define a getConstants() method.
// Default constants to {}, so no constants are spread into the NativeModule
constantsCache_ = jsi::Object(runtime);
return constantsCache_;
}
std::vector<facebook::jsi::PropNameID> ObjCInteropTurboModule::getPropertyNames(facebook::jsi::Runtime &runtime)
{
std::vector<facebook::jsi::PropNameID> propNames = ObjCTurboModule::getPropertyNames(runtime);
jsi::Object constants = getConstants(runtime).asObject(runtime);
jsi::Array constantNames = constants.getPropertyNames(runtime);
for (size_t i = 0; i < constantNames.size(runtime); i += 1) {
jsi::Value constantName = constantNames.getValueAtIndex(runtime, i);
if (constantName.isString()) {
propNames.push_back(jsi::PropNameID::forString(runtime, constantName.asString(runtime)));
}
}
return propNames;
}
} // namespace react
} // namespace facebook
@@ -30,6 +30,7 @@ class Instance;
namespace TurboModuleConvertUtils {
jsi::Value convertObjCObjectToJSIValue(jsi::Runtime &runtime, id value);
id convertJSIValueToObjCObject(jsi::Runtime &runtime, const jsi::Value &value, std::shared_ptr<CallInvoker> jsInvoker);
}
/**
@@ -61,12 +62,52 @@ class JSI_EXPORT ObjCTurboModule : public TurboModule {
protected:
void setMethodArgConversionSelector(NSString *methodName, int argIndex, NSString *fnName);
/**
* Why is this virtual?
*
* Purpose: Converts native module method returns from Objective C values to JavaScript values.
*
* ObjCTurboModule uses TurboModuleMethodValueKind to convert returns from Objective C values to JavaScript values.
* ObjCInteropTurboModule just blindly converts returns from Objective C values to JavaScript values by runtime type,
* because it cannot infer TurboModuleMethodValueKind from the RCT_EXPORT_METHOD annotations.
*/
virtual jsi::Value convertReturnIdToJSIValue(
jsi::Runtime &runtime,
const char *methodName,
TurboModuleMethodValueKind returnType,
id result);
/**
* Why is this virtual?
*
* Purpose: Get a native module method's argument's type, given the method name, and argument index.
*
* ObjCInteropTurboModule computes the argument type names eagerly on module init. So, make this method virtual. That
* way, ObjCInteropTurboModule doesn't end up computing the argument types twice: once on module init, and second on
* method dispatch.
*/
virtual NSString *getArgumentTypeName(jsi::Runtime &runtime, NSString *methodName, int argIndex);
/**
* Why is this virtual?
*
* Purpose: Convert arguments from JavaScript values to Objective C values. Assign the Objective C argument to the
* method invocation.
*
* ObjCInteropTurboModule relies heavily on RCTConvert to convert arguments from JavaScript values to Objective C
* values. ObjCTurboModule tries to minimize reliance on RCTConvert: RCTConvert uses the RCT_EXPORT_METHOD macros,
* which we want to remove long term from React Native.
*/
virtual void setInvocationArg(
jsi::Runtime &runtime,
const char *methodName,
const std::string &objCArgType,
const jsi::Value &arg,
size_t i,
NSInvocation *inv,
NSMutableArray *retainedObjectsForInvocation);
private:
// Does the NativeModule dispatch async methods to the JS thread?
const bool isSyncModule_;
@@ -74,6 +115,8 @@ class JSI_EXPORT ObjCTurboModule : public TurboModule {
/**
* TODO(ramanpreet):
* Investigate an optimization that'll let us get rid of this NSMutableDictionary.
* Perhaps, have the code-generated TurboModule subclass implement
* getMethodArgConversionSelector below.
*/
NSMutableDictionary<NSString *, NSMutableArray *> *methodArgConversionSelectors_;
NSDictionary<NSString *, NSArray<NSString *> *> *methodArgumentTypeNames_;
@@ -81,7 +124,6 @@ class JSI_EXPORT ObjCTurboModule : public TurboModule {
bool isMethodSync(TurboModuleMethodValueKind returnType);
BOOL hasMethodArgConversionSelector(NSString *methodName, int argIndex);
SEL getMethodArgConversionSelector(NSString *methodName, int argIndex);
NSString *getArgumentTypeName(jsi::Runtime &runtime, NSString *methodName, int argIndex);
NSInvocation *createMethodInvocation(
jsi::Runtime &runtime,
bool isSync,
@@ -90,14 +132,6 @@ class JSI_EXPORT ObjCTurboModule : public TurboModule {
const jsi::Value *args,
size_t count,
NSMutableArray *retainedObjectsForInvocation);
void setInvocationArg(
jsi::Runtime &runtime,
const char *methodName,
const std::string &objCArgType,
const jsi::Value &arg,
size_t i,
NSInvocation *inv,
NSMutableArray *retainedObjectsForInvocation);
id performMethodInvocation(
jsi::Runtime &runtime,
bool isSync,
@@ -105,8 +105,6 @@ jsi::Value convertObjCObjectToJSIValue(jsi::Runtime &runtime, id value)
return jsi::Value::undefined();
}
static id
convertJSIValueToObjCObject(jsi::Runtime &runtime, const jsi::Value &value, std::shared_ptr<CallInvoker> jsInvoker);
static NSString *convertJSIStringToNSString(jsi::Runtime &runtime, const jsi::String &value)
{
return [NSString stringWithUTF8String:value.utf8(runtime).c_str()];
@@ -144,8 +142,7 @@ convertJSIObjectToNSDictionary(jsi::Runtime &runtime, const jsi::Object &value,
static RCTResponseSenderBlock
convertJSIFunctionToCallback(jsi::Runtime &runtime, const jsi::Function &value, std::shared_ptr<CallInvoker> jsInvoker);
static id
convertJSIValueToObjCObject(jsi::Runtime &runtime, const jsi::Value &value, std::shared_ptr<CallInvoker> jsInvoker)
id convertJSIValueToObjCObject(jsi::Runtime &runtime, const jsi::Value &value, std::shared_ptr<CallInvoker> jsInvoker)
{
if (value.isUndefined() || value.isNull()) {
return nil;
@@ -23,6 +23,7 @@
#import <React/RCTModuleData.h>
#import <React/RCTPerformanceLogger.h>
#import <React/RCTUtils.h>
#import <ReactCommon/RCTInteropTurboModule.h>
#import <ReactCommon/RuntimeExecutor.h>
#import <ReactCommon/TurboCxxModule.h>
#import <ReactCommon/TurboModulePerfLogger.h>