Add codegen for C++ TurboModule automatic type conversions

Summary:
This adds the *option* for C++ TurboModules to use a `*CxxSpec<T>` base class that extends the existing (and unchanged) corresponding `*CxxSpecJSI` base class with code-generated methods that use `bridging::calFromJs` to safely convert types between JSI and C++. If a type conversion cannot be made, then it will fail to compile.

Changelog:
[General][Added] - Automatic type conversions for C++ TurboModules

Reviewed By: christophpurrer

Differential Revision: D34780512

fbshipit-source-id: 58b34533c40652db8e3aea43804ceb73bcbe97a5
This commit is contained in:
Scott Kyle
2022-03-11 12:47:51 -08:00
committed by Facebook GitHub Bot
parent 6697b7bb90
commit 31f0796237
3 changed files with 633 additions and 53 deletions
@@ -15,6 +15,7 @@ import type {
SchemaType,
NativeModuleTypeAnnotation,
NativeModuleFunctionTypeAnnotation,
NativeModulePropertyShape,
} from '../../CodegenSchema';
import type {AliasResolver} from './Utils';
@@ -27,21 +28,58 @@ type FilesOutput = Map<string, string>;
const ModuleClassDeclarationTemplate = ({
hasteModuleName,
moduleProperties,
}: $ReadOnly<{hasteModuleName: string, moduleProperties: string}>) => {
}: $ReadOnly<{hasteModuleName: string, moduleProperties: string[]}>) => {
return `class JSI_EXPORT ${hasteModuleName}CxxSpecJSI : public TurboModule {
protected:
${hasteModuleName}CxxSpecJSI(std::shared_ptr<CallInvoker> jsInvoker);
public:
${indent(moduleProperties, 2)}
${indent(moduleProperties.join('\n'), 2)}
};`;
};
const ModuleSpecClassDeclarationTemplate = ({
hasteModuleName,
moduleName,
moduleProperties,
}: $ReadOnly<{
hasteModuleName: string,
moduleName: string,
moduleProperties: string[],
}>) => {
return `template <typename T>
class JSI_EXPORT ${hasteModuleName}CxxSpec : public TurboModule {
public:
jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override {
return delegate_.get(rt, propName);
}
protected:
${hasteModuleName}CxxSpec(std::shared_ptr<CallInvoker> jsInvoker)
: TurboModule("${moduleName}", jsInvoker),
delegate_(static_cast<T*>(this), jsInvoker) {}
private:
class Delegate : public ${hasteModuleName}CxxSpecJSI {
public:
Delegate(T *instance, std::shared_ptr<CallInvoker> jsInvoker) :
${hasteModuleName}CxxSpecJSI(std::move(jsInvoker)), instance_(instance) {}
${indent(moduleProperties.join('\n'), 4)}
private:
T *instance_;
};
Delegate delegate_;
};`;
};
const FileTemplate = ({
modules,
}: $ReadOnly<{
modules: string,
modules: string[],
}>) => {
return `/**
* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
@@ -55,11 +93,12 @@ const FileTemplate = ({
#pragma once
#include <ReactCommon/TurboModule.h>
#include <react/bridging/Bridging.h>
namespace facebook {
namespace react {
${modules}
${modules.join('\n\n')}
} // namespace react
} // namespace facebook
@@ -118,8 +157,52 @@ function translatePrimitiveJSTypeToCpp(
}
}
const propertyTemplate =
'virtual ::_RETURN_VALUE_:: ::_PROPERTY_NAME_::(jsi::Runtime &rt::_ARGS_::) = 0;';
function translatePropertyToCpp(
prop: NativeModulePropertyShape,
resolveAlias: AliasResolver,
abstract: boolean = false,
) {
const [propTypeAnnotation] =
unwrapNullable<NativeModuleFunctionTypeAnnotation>(prop.typeAnnotation);
const params = propTypeAnnotation.params.map(
param => `std::move(${param.name})`,
);
const paramTypes = propTypeAnnotation.params.map(param => {
const translatedParam = translatePrimitiveJSTypeToCpp(
param.typeAnnotation,
typeName =>
`Unsupported type for param "${param.name}" in ${prop.name}. Found: ${typeName}`,
resolveAlias,
);
return `${translatedParam} ${param.name}`;
});
const returnType = translatePrimitiveJSTypeToCpp(
propTypeAnnotation.returnTypeAnnotation,
typeName => `Unsupported return type for ${prop.name}. Found: ${typeName}`,
resolveAlias,
);
// The first param will always be the runtime reference.
paramTypes.unshift('jsi::Runtime &rt');
const method = `${returnType} ${prop.name}(${paramTypes.join(', ')})`;
if (abstract) {
return `virtual ${method} = 0;`;
}
return `${method} override {
static_assert(
bridging::getParameterCount(&T::${prop.name}) == ${paramTypes.length},
"Expected ${prop.name}(...) to have ${paramTypes.length} parameters");
return bridging::callFromJs<${returnType}>(
rt, &T::${prop.name}, jsInvoker_, ${['instance_', ...params].join(', ')});
}`;
}
module.exports = {
generate(
@@ -130,55 +213,30 @@ module.exports = {
): FilesOutput {
const nativeModules = getModules(schema);
const modules = Object.keys(nativeModules)
.map(hasteModuleName => {
const {
aliases,
spec: {properties},
} = nativeModules[hasteModuleName];
const resolveAlias = createAliasResolver(aliases);
const modules = Object.keys(nativeModules).flatMap(hasteModuleName => {
const {
aliases,
spec: {properties},
moduleNames: [moduleName],
} = nativeModules[hasteModuleName];
const resolveAlias = createAliasResolver(aliases);
const traversedProperties = properties
.map(prop => {
const [propTypeAnnotation] =
unwrapNullable<NativeModuleFunctionTypeAnnotation>(
prop.typeAnnotation,
);
const traversedArgs = propTypeAnnotation.params
.map(param => {
const translatedParam = translatePrimitiveJSTypeToCpp(
param.typeAnnotation,
typeName =>
`Unsupported type for param "${param.name}" in ${prop.name}. Found: ${typeName}`,
resolveAlias,
);
return `${translatedParam} ${param.name}`;
})
.join(', ');
return propertyTemplate
.replace('::_PROPERTY_NAME_::', prop.name)
.replace(
'::_RETURN_VALUE_::',
translatePrimitiveJSTypeToCpp(
propTypeAnnotation.returnTypeAnnotation,
typeName =>
`Unsupported return type for ${prop.name}. Found: ${typeName}`,
resolveAlias,
),
)
.replace(
'::_ARGS_::',
traversedArgs === '' ? '' : ', ' + traversedArgs,
);
})
.join('\n');
return ModuleClassDeclarationTemplate({
return [
ModuleClassDeclarationTemplate({
hasteModuleName,
moduleProperties: traversedProperties,
});
})
.join('\n');
moduleProperties: properties.map(prop =>
translatePropertyToCpp(prop, resolveAlias, true),
),
}),
ModuleSpecClassDeclarationTemplate({
hasteModuleName,
moduleName,
moduleProperties: properties.map(prop =>
translatePropertyToCpp(prop, resolveAlias),
),
}),
];
});
const fileName = `${libraryName}JSI.h`;
const replacedTemplate = FileTemplate({modules});