Skip to content

Commit

Permalink
Support mixed types only for C++
Browse files Browse the repository at this point in the history
Summary:
There are cases where we want to pass arbitrary types to a TurboModule, which may then handle the values appropriately, but we haven't supported this use case. Since C++ TurboModules can accept any `jsi::Value` (unlike Java/ObjC) and we have real-world need for this (otherwise we must require JSON serialization), this now allows `mixed` (`unknown` in TypeScript) for C++-only TurboModules.

Changelog:
[General][Added] C++ TurboModule methods can now use mixed types

Reviewed By: RSNara

Differential Revision: D36611299

fbshipit-source-id: bbf29dfcc6aed67e213bb3eab06537c18c7db1fe
  • Loading branch information
appden authored and facebook-github-bot committed May 25, 2022
1 parent b2aa415 commit 3c569f5
Show file tree
Hide file tree
Showing 21 changed files with 415 additions and 22 deletions.
7 changes: 6 additions & 1 deletion packages/react-native-codegen/src/CodegenSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,10 @@ export type NativeModulePromiseTypeAnnotation = $ReadOnly<{
type: 'PromiseTypeAnnotation',
}>;

export type NativeModuleMixedTypeAnnotation = $ReadOnly<{
type: 'MixedTypeAnnotation',
}>;

export type NativeModuleBaseTypeAnnotation =
| NativeModuleStringTypeAnnotation
| NativeModuleNumberTypeAnnotation
Expand All @@ -306,7 +310,8 @@ export type NativeModuleBaseTypeAnnotation =
| ReservedTypeAnnotation
| NativeModuleTypeAliasTypeAnnotation
| NativeModuleArrayTypeAnnotation<Nullable<NativeModuleBaseTypeAnnotation>>
| NativeModuleObjectTypeAnnotation;
| NativeModuleObjectTypeAnnotation
| NativeModuleMixedTypeAnnotation;

export type NativeModuleParamTypeAnnotation =
| NativeModuleBaseTypeAnnotation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ function serializeArg(
realTypeAnnotation = resolveAlias(realTypeAnnotation.name);
}

function wrap(suffix) {
function wrap(callback) {
const val = `args[${index}]`;
const expression = `${val}${suffix}`;
const expression = callback(val);

if (nullable) {
return `${val}.isNull() || ${val}.isUndefined() ? std::nullopt : std::make_optional(${expression})`;
Expand All @@ -129,33 +129,35 @@ function serializeArg(
case 'ReservedTypeAnnotation':
switch (realTypeAnnotation.name) {
case 'RootTag':
return wrap('.getNumber()');
return wrap(val => `${val}.getNumber()`);
default:
(realTypeAnnotation.name: empty);
throw new Error(
`Unknown prop type for "${arg.name}, found: ${realTypeAnnotation.name}"`,
);
}
case 'StringTypeAnnotation':
return wrap('.asString(rt)');
return wrap(val => `${val}.asString(rt)`);
case 'BooleanTypeAnnotation':
return wrap('.asBool()');
return wrap(val => `${val}.asBool()`);
case 'NumberTypeAnnotation':
return wrap('.asNumber()');
return wrap(val => `${val}.asNumber()`);
case 'FloatTypeAnnotation':
return wrap('.asNumber()');
return wrap(val => `${val}.asNumber()`);
case 'DoubleTypeAnnotation':
return wrap('.asNumber()');
return wrap(val => `${val}.asNumber()`);
case 'Int32TypeAnnotation':
return wrap('.asNumber()');
return wrap(val => `${val}.asNumber()`);
case 'ArrayTypeAnnotation':
return wrap('.asObject(rt).asArray(rt)');
return wrap(val => `${val}.asObject(rt).asArray(rt)`);
case 'FunctionTypeAnnotation':
return wrap('.asObject(rt).asFunction(rt)');
return wrap(val => `${val}.asObject(rt).asFunction(rt)`);
case 'GenericObjectTypeAnnotation':
return wrap('.asObject(rt)');
return wrap(val => `${val}.asObject(rt)`);
case 'ObjectTypeAnnotation':
return wrap('.asObject(rt)');
return wrap(val => `${val}.asObject(rt)`);
case 'MixedTypeAnnotation':
return wrap(val => `jsi::Value(rt, ${val})`);
default:
(realTypeAnnotation.type: empty);
throw new Error(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ function translatePrimitiveJSTypeToCpp(
return wrap('jsi::Function');
case 'PromiseTypeAnnotation':
return wrap('jsi::Value');
case 'MixedTypeAnnotation':
return wrap('jsi::Value');
default:
(realTypeAnnotation.type: empty);
throw new Error(createErrorMessage(realTypeAnnotation.type));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ function translateFunctionParamToJavaType(
imports.add('com.facebook.react.bridge.Callback');
return 'Callback';
default:
(realTypeAnnotation.type: empty);
(realTypeAnnotation.type: 'MixedTypeAnnotation');
throw new Error(createErrorMessage(realTypeAnnotation.type));
}
}
Expand Down Expand Up @@ -220,7 +220,7 @@ function translateFunctionReturnTypeToJavaType(
imports.add('com.facebook.react.bridge.WritableArray');
return wrapIntoNullableIfNeeded('WritableArray');
default:
(realTypeAnnotation.type: empty);
(realTypeAnnotation.type: 'MixedTypeAnnotation');
throw new Error(createErrorMessage(realTypeAnnotation.type));
}
}
Expand Down Expand Up @@ -272,7 +272,7 @@ function getFalsyReturnStatementFromReturnType(
case 'ArrayTypeAnnotation':
return 'return null;';
default:
(realTypeAnnotation.type: empty);
(realTypeAnnotation.type: 'MixedTypeAnnotation');
throw new Error(createErrorMessage(realTypeAnnotation.type));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ function translateReturnTypeToKind(
case 'ArrayTypeAnnotation':
return 'ArrayKind';
default:
(realTypeAnnotation.type: empty);
(realTypeAnnotation.type: 'MixedTypeAnnotation');
throw new Error(
`Unknown prop type for returning value, found: ${realTypeAnnotation.type}"`,
);
Expand Down Expand Up @@ -226,7 +226,7 @@ function translateParamTypeToJniType(
case 'FunctionTypeAnnotation':
return 'Lcom/facebook/react/bridge/Callback;';
default:
(realTypeAnnotation.type: empty);
(realTypeAnnotation.type: 'MixedTypeAnnotation');
throw new Error(
`Unknown prop type for method arg, found: ${realTypeAnnotation.type}"`,
);
Expand Down Expand Up @@ -278,7 +278,7 @@ function translateReturnTypeToJniType(
case 'ArrayTypeAnnotation':
return 'Lcom/facebook/react/bridge/WritableArray;';
default:
(realTypeAnnotation.type: empty);
(realTypeAnnotation.type: 'MixedTypeAnnotation');
throw new Error(
`Unknown prop type for method return type, found: ${realTypeAnnotation.type}"`,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ class StructCollector {
this._insertAlias(typeAnnotation.name, structContext, resolveAlias);
return wrapNullable(nullable, typeAnnotation);
}
case 'MixedTypeAnnotation':
throw new Error('Mixed types are unsupported in structs');
default: {
return wrapNullable(nullable, typeAnnotation);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ function getReturnObjCType(
case 'GenericObjectTypeAnnotation':
return wrapIntoNullableIfNeeded('NSDictionary *');
default:
(typeAnnotation.type: empty);
(typeAnnotation.type: 'MixedTypeAnnotation');
throw new Error(
`Unsupported return type for ${methodName}. Found: ${typeAnnotation.type}`,
);
Expand Down Expand Up @@ -378,7 +378,7 @@ function getReturnJSType(
case 'GenericObjectTypeAnnotation':
return 'ObjectKind';
default:
(typeAnnotation.type: empty);
(typeAnnotation.type: 'MixedTypeAnnotation');
throw new Error(
`Unsupported return type for ${methodName}. Found: ${typeAnnotation.type}`,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1502,6 +1502,40 @@ const REAL_MODULE_EXAMPLE: SchemaType = {
},
};

const CXX_ONLY_NATIVE_MODULES: SchemaType = {
modules: {
NativeSampleTurboModule: {
type: 'NativeModule',
aliases: {},
spec: {
properties: [
{
name: 'getMixed',
optional: false,
typeAnnotation: {
type: 'FunctionTypeAnnotation',
returnTypeAnnotation: {
type: 'MixedTypeAnnotation',
},
params: [
{
name: 'arg',
optional: false,
typeAnnotation: {
type: 'MixedTypeAnnotation',
},
},
],
},
},
],
},
moduleNames: ['SampleTurboModuleCxx'],
excludedPlatforms: ['iOS', 'android'],
},
},
};

const SAMPLE_WITH_UPPERCASE_NAME: SchemaType = {
modules: {
NativeSampleTurboModule: {
Expand All @@ -1522,5 +1556,6 @@ module.exports = {
simple_native_modules: SIMPLE_NATIVE_MODULES,
native_modules_with_type_aliases: NATIVE_MODULES_WITH_TYPE_ALIASES,
real_module_example: REAL_MODULE_EXAMPLE,
cxx_only_native_modules: CXX_ONLY_NATIVE_MODULES,
SampleWithUppercaseName: SAMPLE_WITH_UPPERCASE_NAME,
};
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,38 @@ NativeSampleTurboModuleCxxSpecJSI::NativeSampleTurboModuleCxxSpecJSI(std::shared
}
} // namespace react
} // namespace facebook
",
}
`;
exports[`GenerateModuleCpp can generate fixture cxx_only_native_modules 1`] = `
Map {
"cxx_only_native_modulesJSI-generated.cpp" => "/**
* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
*
* Do not edit this file as changes may cause incorrect behavior and will be lost
* once the code is regenerated.
*
* @generated by codegen project: GenerateModuleH.js
*/
#include \\"cxx_only_native_modulesJSI.h\\"
namespace facebook {
namespace react {
static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getMixed(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
return static_cast<NativeSampleTurboModuleCxxSpecJSI *>(&turboModule)->getMixed(rt, jsi::Value(rt, args[0]));
}
NativeSampleTurboModuleCxxSpecJSI::NativeSampleTurboModuleCxxSpecJSI(std::shared_ptr<CallInvoker> jsInvoker)
: TurboModule(\\"SampleTurboModuleCxx\\", jsInvoker) {
methodMap_[\\"getMixed\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getMixed};
}
} // namespace react
} // namespace facebook
",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,74 @@ private:
}
`;
exports[`GenerateModuleH can generate fixture cxx_only_native_modules 1`] = `
Map {
"cxx_only_native_modulesJSI.h" => "/**
* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
*
* Do not edit this file as changes may cause incorrect behavior and will be lost
* once the code is regenerated.
*
* @generated by codegen project: GenerateModuleH.js
*/
#pragma once
#include <ReactCommon/TurboModule.h>
#include <react/bridging/Bridging.h>
namespace facebook {
namespace react {
class JSI_EXPORT NativeSampleTurboModuleCxxSpecJSI : public TurboModule {
protected:
NativeSampleTurboModuleCxxSpecJSI(std::shared_ptr<CallInvoker> jsInvoker);
public:
virtual jsi::Value getMixed(jsi::Runtime &rt, jsi::Value arg) = 0;
};
template <typename T>
class JSI_EXPORT NativeSampleTurboModuleCxxSpec : public TurboModule {
public:
jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override {
return delegate_.get(rt, propName);
}
protected:
NativeSampleTurboModuleCxxSpec(std::shared_ptr<CallInvoker> jsInvoker)
: TurboModule(\\"SampleTurboModuleCxx\\", jsInvoker),
delegate_(static_cast<T*>(this), jsInvoker) {}
private:
class Delegate : public NativeSampleTurboModuleCxxSpecJSI {
public:
Delegate(T *instance, std::shared_ptr<CallInvoker> jsInvoker) :
NativeSampleTurboModuleCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {}
jsi::Value getMixed(jsi::Runtime &rt, jsi::Value arg) override {
static_assert(
bridging::getParameterCount(&T::getMixed) == 2,
\\"Expected getMixed(...) to have 2 parameters\\");
return bridging::callFromJs<jsi::Value>(
rt, &T::getMixed, jsInvoker_, instance_, std::move(arg));
}
private:
T *instance_;
};
Delegate delegate_;
};
} // namespace react
} // namespace facebook
",
}
`;
exports[`GenerateModuleH can generate fixture empty_native_modules 1`] = `
Map {
"empty_native_modulesJSI.h" => "/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,41 @@ inline facebook::react::LazyVector<JS::NativeSampleTurboModule::SpecGetArraysOpt
id const p = _v[@\\"arrayOfObjects\\"];
return RCTBridgingToVec(p, ^JS::NativeSampleTurboModule::SpecGetArraysOptionsArrayOfObjectsElement(id itemValue_0) { return JS::NativeSampleTurboModule::SpecGetArraysOptionsArrayOfObjectsElement(itemValue_0); });
}
",
}
`;
exports[`GenerateModuleHObjCpp can generate fixture cxx_only_native_modules 1`] = `
Map {
"cxx_only_native_modules.h" => "/**
* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
*
* Do not edit this file as changes may cause incorrect behavior and will be lost
* once the code is regenerated.
*
* @generated by codegen project: GenerateModuleObjCpp
*
* We create an umbrella header (and corresponding implementation) here since
* Cxx compilation in BUCK has a limitation: source-code producing genrule()s
* must have a single output. More files => more genrule()s => slower builds.
*/
#ifndef __cplusplus
#error This file must be compiled as Obj-C++. If you are importing it, you must change your file extension to .mm.
#endif
#import <Foundation/Foundation.h>
#import <RCTRequired/RCTRequired.h>
#import <RCTTypeSafety/RCTConvertHelpers.h>
#import <RCTTypeSafety/RCTTypedModuleConstants.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTCxxConvert.h>
#import <React/RCTManagedPointer.h>
#import <ReactCommon/RCTTurboModule.h>
#import <optional>
#import <vector>
",
}
`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ public abstract class NativeSampleTurboModuleSpec extends ReactContextBaseJavaMo
}
`;
exports[`GenerateModuleJavaSpec can generate fixture cxx_only_native_modules 1`] = `Map {}`;
exports[`GenerateModuleJavaSpec can generate fixture empty_native_modules 1`] = `
Map {
"java/com/facebook/fbreact/specs/NativeSampleTurboModuleSpec.java" => "
Expand Down
Loading

0 comments on commit 3c569f5

Please sign in to comment.