Files
react-native/packages/react-native-codegen/src/generators/RNCodegen.js
T
Christoph Purrer 0fd24c75a1 Simplify C++ TM base classes (#54059)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/54059

Changelog: [Internal]

Right now for a simple spec as
```
import type {CodegenTypes, TurboModule} from 'react-native';

import {TurboModuleRegistry} from 'react-native';

export type ScreenshotManagerOptions = CodegenTypes.UnsafeObject;

export interface Spec extends TurboModule {
  +getConstants: () => {};
  takeScreenshot(
    id: string,
    options: ScreenshotManagerOptions,
  ): Promise<string>;
}

const NativeModule = TurboModuleRegistry.get<Spec>('ScreenshotManager');
export function takeScreenshot(
  id: string,
  options: ScreenshotManagerOptions,
): Promise<string> {
  if (NativeModule != null) {
    return NativeModule.takeScreenshot(id, options);
  }
  return Promise.reject();
}
```

we generate **TWO** `facebook::react::TurboModule` sub classes (`NativeScreenshotManagerCxxSpecJSI` and `NativeScreenshotManagerCxxSpec`) to construct ONE C++ TM.

In particular header
```
#pragma once

#include <ReactCommon/TurboModule.h>
#include <react/bridging/Bridging.h>

namespace facebook::react {

class JSI_EXPORT NativeScreenshotManagerCxxSpecJSI : public TurboModule {
 protected:
  NativeScreenshotManagerCxxSpecJSI(std::shared_ptr<CallInvoker> jsInvoker);

 public:
  virtual jsi::Object getConstants(jsi::Runtime& rt) = 0;
  virtual jsi::Value
  takeScreenshot(jsi::Runtime& rt, jsi::String id, jsi::Object options) = 0;
};

template <typename T>
class JSI_EXPORT NativeScreenshotManagerCxxSpec : public TurboModule {
 public:
  jsi::Value create(jsi::Runtime& rt, const jsi::PropNameID& propName)
      override {
    return delegate_.create(rt, propName);
  }

  std::vector<jsi::PropNameID> getPropertyNames(
      jsi::Runtime& runtime) override {
    return delegate_.getPropertyNames(runtime);
  }

  static constexpr std::string_view kModuleName = "ScreenshotManager";

 protected:
  NativeScreenshotManagerCxxSpec(std::shared_ptr<CallInvoker> jsInvoker)
      : TurboModule(
            std::string{NativeScreenshotManagerCxxSpec::kModuleName},
            jsInvoker),
        delegate_(reinterpret_cast<T*>(this), jsInvoker) {}

 private:
  class Delegate : public NativeScreenshotManagerCxxSpecJSI {
   public:
    Delegate(T* instance, std::shared_ptr<CallInvoker> jsInvoker)
        : NativeScreenshotManagerCxxSpecJSI(std::move(jsInvoker)),
          instance_(instance) {}

    jsi::Object getConstants(jsi::Runtime& rt) override {
      static_assert(
          bridging::getParameterCount(&T::getConstants) == 1,
          "Expected getConstants(...) to have 1 parameters");

      return bridging::callFromJs<jsi::Object>(
          rt, &T::getConstants, jsInvoker_, instance_);
    }
    jsi::Value takeScreenshot(
        jsi::Runtime& rt,
        jsi::String id,
        jsi::Object options) override {
      static_assert(
          bridging::getParameterCount(&T::takeScreenshot) == 3,
          "Expected takeScreenshot(...) to have 3 parameters");

      return bridging::callFromJs<jsi::Value>(
          rt,
          &T::takeScreenshot,
          jsInvoker_,
          instance_,
          std::move(id),
          std::move(options));
    }

   private:
    friend class NativeScreenshotManagerCxxSpec;
    T* instance_;
  };

  Delegate delegate_;
};

} // namespace facebook::react
```
and cpp
```
#include "AppSpecsJSI.h"

namespace facebook::react {

static jsi::Value __hostFunction_NativeScreenshotManagerCxxSpecJSI_getConstants(
    jsi::Runtime& rt,
    TurboModule& turboModule,
    const jsi::Value* args,
    size_t count) {
  return static_cast<NativeScreenshotManagerCxxSpecJSI*>(&turboModule)
      ->getConstants(rt);
}

static jsi::Value
__hostFunction_NativeScreenshotManagerCxxSpecJSI_takeScreenshot(
    jsi::Runtime& rt,
    TurboModule& turboModule,
    const jsi::Value* args,
    size_t count) {
  return static_cast<NativeScreenshotManagerCxxSpecJSI*>(&turboModule)
      ->takeScreenshot(
          rt,
          count <= 0 ? throw jsi::JSError(
                           rt, "Expected argument in position 0 to be passed")
                     : args[0].asString(rt),
          count <= 1 ? throw jsi::JSError(
                           rt, "Expected argument in position 1 to be passed")
                     : args[1].asObject(rt));
}

NativeScreenshotManagerCxxSpecJSI::NativeScreenshotManagerCxxSpecJSI(
    std::shared_ptr<CallInvoker> jsInvoker)
    : TurboModule("ScreenshotManager", jsInvoker) {
  methodMap_["getConstants"] = MethodMetadata{
      0, __hostFunction_NativeScreenshotManagerCxxSpecJSI_getConstants};
  methodMap_["takeScreenshot"] = MethodMetadata{
      2, __hostFunction_NativeScreenshotManagerCxxSpecJSI_takeScreenshot};
}

} // namespace facebook::react
```

The goal of this change is to simplify that and only have **ONE** `facebook::react::TurboModule` base class for a concrete Cxx TM as this header

```
/*
 * 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

template <typename T>
class JSI_EXPORT NativeScreenshotManagerCxxSpec : public TurboModule {
public:
  static constexpr std::string_view kModuleName = "ScreenshotManager";

protected:
  NativeScreenshotManagerCxxSpec(std::shared_ptr<CallInvoker> jsInvoker) : TurboModule(std::string{NativeScreenshotManagerCxxSpec::kModuleName}, jsInvoker) {
    methodMap_["getConstants"] = MethodMetadata {.argCount = 0, .invoker = __getConstants};
    methodMap_["takeScreenshot"] = MethodMetadata {.argCount = 2, .invoker = __takeScreenshot};
  }

private:
  static jsi::Value __getConstants(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* /*args*/, size_t /*count*/) {
    static_assert(
      bridging::getParameterCount(&T::getConstants) == 1,
      "Expected getConstants(...) to have 1 parameters");
    return bridging::callFromJs<jsi::Object>(rt, &T::getConstants,  static_cast<NativeScreenshotManagerCxxSpec*>(&turboModule)->jsInvoker_, static_cast<T*>(&turboModule));
  }

  static jsi::Value __takeScreenshot(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
    static_assert(
      bridging::getParameterCount(&T::takeScreenshot) == 3,
      "Expected takeScreenshot(...) to have 3 parameters");
    return bridging::callFromJs<jsi::Value>(rt, &T::takeScreenshot,  static_cast<NativeScreenshotManagerCxxSpec*>(&turboModule)->jsInvoker_, static_cast<T*>(&turboModule),
      count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asString(rt),
      count <= 1 ? throw jsi::JSError(rt, "Expected argument in position 1 to be passed") : args[1].asObject(rt));
  }
};
```

This reduces the generated code from 101 lines to 31 - **REDUCTION of 2/3**

Reviewed By: javache

Differential Revision: D83810977

fbshipit-source-id: 6d0c0271846c8544399c7050b7331b0180cbbadf
2025-10-13 20:21:32 -07:00

358 lines
11 KiB
JavaScript

/**
* 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.
*
* @flow strict-local
* @format
*/
'use strict';
/*
TODO:
- ViewConfigs should spread in View's valid attributes
*/
import type {SchemaType} from '../CodegenSchema';
const schemaValidator = require('../SchemaValidator.js');
const generateComponentDescriptorCpp = require('./components/GenerateComponentDescriptorCpp.js');
const generateComponentDescriptorH = require('./components/GenerateComponentDescriptorH.js');
const generateComponentHObjCpp = require('./components/GenerateComponentHObjCpp.js');
const generateEventEmitterCpp = require('./components/GenerateEventEmitterCpp.js');
const generateEventEmitterH = require('./components/GenerateEventEmitterH.js');
const generatePropsCpp = require('./components/GeneratePropsCpp.js');
const generatePropsH = require('./components/GeneratePropsH.js');
const generatePropsJavaDelegate = require('./components/GeneratePropsJavaDelegate.js');
const generatePropsJavaInterface = require('./components/GeneratePropsJavaInterface.js');
const generateShadowNodeCpp = require('./components/GenerateShadowNodeCpp.js');
const generateShadowNodeH = require('./components/GenerateShadowNodeH.js');
const generateStateCpp = require('./components/GenerateStateCpp.js');
const generateStateH = require('./components/GenerateStateH.js');
const generateTests = require('./components/GenerateTests.js');
const generateThirdPartyFabricComponentsProviderH = require('./components/GenerateThirdPartyFabricComponentsProviderH.js');
const generateThirdPartyFabricComponentsProviderObjCpp = require('./components/GenerateThirdPartyFabricComponentsProviderObjCpp.js');
const generateViewConfigJs = require('./components/GenerateViewConfigJs.js');
const generateModuleH = require('./modules/GenerateModuleH.js');
const generateModuleJavaSpec = require('./modules/GenerateModuleJavaSpec.js');
const generateModuleJniCpp = require('./modules/GenerateModuleJniCpp.js');
const generateModuleJniH = require('./modules/GenerateModuleJniH.js');
const generateModuleObjCpp = require('./modules/GenerateModuleObjCpp');
const fs = require('fs');
const path = require('path');
const ALL_GENERATORS = {
generateComponentDescriptorH: generateComponentDescriptorH.generate,
generateComponentDescriptorCpp: generateComponentDescriptorCpp.generate,
generateComponentHObjCpp: generateComponentHObjCpp.generate,
generateEventEmitterCpp: generateEventEmitterCpp.generate,
generateEventEmitterH: generateEventEmitterH.generate,
generatePropsCpp: generatePropsCpp.generate,
generatePropsH: generatePropsH.generate,
generateStateCpp: generateStateCpp.generate,
generateStateH: generateStateH.generate,
generateModuleH: generateModuleH.generate,
generateModuleObjCpp: generateModuleObjCpp.generate,
generateModuleJavaSpec: generateModuleJavaSpec.generate,
generateModuleJniCpp: generateModuleJniCpp.generate,
generateModuleJniH: generateModuleJniH.generate,
generatePropsJavaInterface: generatePropsJavaInterface.generate,
generatePropsJavaDelegate: generatePropsJavaDelegate.generate,
generateTests: generateTests.generate,
generateShadowNodeCpp: generateShadowNodeCpp.generate,
generateShadowNodeH: generateShadowNodeH.generate,
generateThirdPartyFabricComponentsProviderObjCpp:
generateThirdPartyFabricComponentsProviderObjCpp.generate,
generateThirdPartyFabricComponentsProviderH:
generateThirdPartyFabricComponentsProviderH.generate,
generateViewConfigJs: generateViewConfigJs.generate,
};
export type FilesOutput = Map<string, string>;
export type GenerateFunction = (
libraryName: string,
schema: SchemaType,
packageName?: string,
assumeNonnull: boolean,
headerPrefix?: string,
includeGetDebugPropsImplementation?: boolean,
) => FilesOutput;
export type LibraryGeneratorsFunctions = $ReadOnly<{
[string]: Array<GenerateFunction>,
}>;
export type LibraryOptions = $ReadOnly<{
libraryName: string,
schema: SchemaType,
outputDirectory: string,
packageName?: string, // Some platforms have a notion of package, which should be configurable.
assumeNonnull: boolean,
useLocalIncludePaths?: boolean,
includeGetDebugPropsImplementation?: boolean,
libraryGenerators?: LibraryGeneratorsFunctions,
}>;
export type SchemasOptions = $ReadOnly<{
schemas: {[string]: SchemaType},
outputDirectory: string,
supportedApplePlatforms?: {[string]: {[string]: boolean}},
}>;
export type LibraryGenerators =
| 'componentsAndroid'
| 'componentsIOS'
| 'descriptors'
| 'events'
| 'props'
| 'states'
| 'tests'
| 'shadow-nodes'
| 'modulesAndroid'
| 'modulesCxx'
| 'modulesIOS';
export type SchemasGenerators = 'providerIOS';
export type LibraryConfig = $ReadOnly<{
generators: Array<LibraryGenerators>,
test?: boolean,
}>;
export type SchemasConfig = $ReadOnly<{
generators: Array<SchemasGenerators>,
test?: boolean,
}>;
const LIBRARY_GENERATORS: LibraryGeneratorsFunctions = {
descriptors: [
generateComponentDescriptorCpp.generate,
generateComponentDescriptorH.generate,
],
events: [generateEventEmitterCpp.generate, generateEventEmitterH.generate],
states: [generateStateCpp.generate, generateStateH.generate],
props: [
generateComponentHObjCpp.generate,
generatePropsCpp.generate,
generatePropsH.generate,
generatePropsJavaInterface.generate,
generatePropsJavaDelegate.generate,
],
// TODO: Refactor this to consolidate various C++ output variation instead of forking per platform.
componentsAndroid: [
// JNI/C++ files
generateComponentDescriptorH.generate,
generateComponentDescriptorCpp.generate,
generateEventEmitterCpp.generate,
generateEventEmitterH.generate,
generatePropsCpp.generate,
generatePropsH.generate,
generateStateCpp.generate,
generateStateH.generate,
generateShadowNodeCpp.generate,
generateShadowNodeH.generate,
// Java files
generatePropsJavaInterface.generate,
generatePropsJavaDelegate.generate,
],
componentsIOS: [
generateComponentDescriptorH.generate,
generateComponentDescriptorCpp.generate,
generateEventEmitterCpp.generate,
generateEventEmitterH.generate,
generateComponentHObjCpp.generate,
generatePropsCpp.generate,
generatePropsH.generate,
generateStateCpp.generate,
generateStateH.generate,
generateShadowNodeCpp.generate,
generateShadowNodeH.generate,
],
modulesAndroid: [
generateModuleJniCpp.generate,
generateModuleJniH.generate,
generateModuleJavaSpec.generate,
],
modulesCxx: [generateModuleH.generate],
modulesIOS: [generateModuleObjCpp.generate],
tests: [generateTests.generate],
'shadow-nodes': [
generateShadowNodeCpp.generate,
generateShadowNodeH.generate,
],
};
const SCHEMAS_GENERATORS = {
providerIOS: [
generateThirdPartyFabricComponentsProviderObjCpp.generate,
generateThirdPartyFabricComponentsProviderH.generate,
],
};
type CodeGenFile = {
name: string,
content: string,
outputDir: string,
};
function writeMapToFiles(map: Array<CodeGenFile>) {
let success = true;
map.forEach(file => {
try {
const location = path.join(file.outputDir, file.name);
const dirName = path.dirname(location);
if (!fs.existsSync(dirName)) {
fs.mkdirSync(dirName, {recursive: true});
}
fs.writeFileSync(location, file.content);
} catch (error) {
success = false;
console.error(`Failed to write ${file.name} to ${file.outputDir}`, error);
}
});
return success;
}
function checkFilesForChanges(generated: Array<CodeGenFile>): boolean {
let hasChanged = false;
generated.forEach(file => {
const location = path.join(file.outputDir, file.name);
const currentContents = fs.readFileSync(location, 'utf8');
if (currentContents !== file.content) {
console.error(`- ${file.name} has changed`);
hasChanged = true;
}
});
return !hasChanged;
}
function checkOrWriteFiles(
generatedFiles: Array<CodeGenFile>,
test: void | boolean,
): boolean {
if (test === true) {
return checkFilesForChanges(generatedFiles);
}
return writeMapToFiles(generatedFiles);
}
module.exports = {
allGenerators: ALL_GENERATORS,
generate(
{
libraryName,
schema,
outputDirectory,
packageName,
assumeNonnull,
useLocalIncludePaths,
includeGetDebugPropsImplementation = false,
libraryGenerators = LIBRARY_GENERATORS,
}: LibraryOptions,
{generators, test}: LibraryConfig,
): boolean {
schemaValidator.validate(schema);
const defaultHeaderPrefix = 'react/renderer/components';
const headerPrefix =
useLocalIncludePaths === true
? ''
: `${defaultHeaderPrefix}/${libraryName}/`;
function composePath(intermediate: string) {
return path.join(outputDirectory, intermediate, libraryName);
}
const componentIOSOutput = composePath(
useLocalIncludePaths === true ? '' : defaultHeaderPrefix,
);
const modulesIOSOutput = composePath('./');
const outputFoldersForGenerators = {
componentsIOS: componentIOSOutput,
modulesIOS: modulesIOSOutput,
descriptors: outputDirectory,
events: outputDirectory,
props: outputDirectory,
states: outputDirectory,
componentsAndroid: outputDirectory,
modulesAndroid: outputDirectory,
modulesCxx: outputDirectory,
tests: outputDirectory,
'shadow-nodes': outputDirectory,
};
const generatedFiles: Array<CodeGenFile> = [];
for (const name of generators) {
for (const generator of libraryGenerators[name]) {
generator(
libraryName,
schema,
packageName,
assumeNonnull,
headerPrefix,
includeGetDebugPropsImplementation,
).forEach((contents: string, fileName: string) => {
generatedFiles.push({
name: fileName,
content: contents,
outputDir: outputFoldersForGenerators[name],
});
});
}
}
return checkOrWriteFiles(generatedFiles, test);
},
generateFromSchemas(
{schemas, outputDirectory, supportedApplePlatforms}: SchemasOptions,
{generators, test}: SchemasConfig,
): boolean {
Object.keys(schemas).forEach(libraryName =>
schemaValidator.validate(schemas[libraryName]),
);
const generatedFiles: Array<CodeGenFile> = [];
for (const name of generators) {
for (const generator of SCHEMAS_GENERATORS[name]) {
generator(schemas, supportedApplePlatforms).forEach(
(contents: string, fileName: string) => {
generatedFiles.push({
name: fileName,
content: contents,
outputDir: outputDirectory,
});
},
);
}
}
return checkOrWriteFiles(generatedFiles, test);
},
generateViewConfig({
libraryName,
schema,
}: Pick<LibraryOptions, 'libraryName' | 'schema'>): string {
schemaValidator.validate(schema);
const result = generateViewConfigJs
.generate(libraryName, schema)
.values()
.next();
if (typeof result.value !== 'string') {
throw new Error(`Failed to generate view config for ${libraryName}`);
}
return result.value;
},
};