Skip to content

Commit

Permalink
Add codegen for C++ TurboModule automatic type conversions
Browse files Browse the repository at this point in the history
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
  • Loading branch information
appden authored and facebook-github-bot committed Mar 11, 2022
1 parent 6697b7b commit 31f0796
Show file tree
Hide file tree
Showing 3 changed files with 634 additions and 54 deletions.
10 changes: 10 additions & 0 deletions ReactCommon/react/bridging/Class.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,14 @@ T callFromJs(
}
}

template <typename R, typename... Args>
constexpr size_t getParameterCount(R (*)(Args...)) {
return sizeof...(Args);
}

template <typename C, typename R, typename... Args>
constexpr size_t getParameterCount(R (C::*)(Args...)) {
return sizeof...(Args);
}

} // namespace facebook::react::bridging
166 changes: 112 additions & 54 deletions packages/react-native-codegen/src/generators/modules/GenerateModuleH.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
SchemaType,
NativeModuleTypeAnnotation,
NativeModuleFunctionTypeAnnotation,
NativeModulePropertyShape,
} from '../../CodegenSchema';

import type {AliasResolver} from './Utils';
Expand All @@ -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).
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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 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({
const modules = Object.keys(nativeModules).flatMap(hasteModuleName => {
const {
aliases,
spec: {properties},
moduleNames: [moduleName],
} = nativeModules[hasteModuleName];
const resolveAlias = createAliasResolver(aliases);

return [
ModuleClassDeclarationTemplate({
hasteModuleName,
moduleProperties: properties.map(prop =>
translatePropertyToCpp(prop, resolveAlias, true),
),
}),
ModuleSpecClassDeclarationTemplate({
hasteModuleName,
moduleProperties: traversedProperties,
});
})
.join('\n');
moduleName,
moduleProperties: properties.map(prop =>
translatePropertyToCpp(prop, resolveAlias),
),
}),
];
});

const fileName = `${libraryName}JSI.h`;
const replacedTemplate = FileTemplate({modules});
Expand Down
Loading

0 comments on commit 31f0796

Please sign in to comment.