From 0d63b92a73b75480fa711c9508283f4e89ed686e Mon Sep 17 00:00:00 2001 From: Satyajit Sahoo Date: Tue, 7 Apr 2026 15:34:56 +0200 Subject: [PATCH] feat: add experimental c++ turbo module --- .github/workflows/build-templates.yml | 11 ++++- docs/pages/create.md | 2 + .../src/exampleApp/dependencies.ts | 12 ++++++ .../src/exampleApp/generateExampleApp.ts | 41 ++----------------- .../create-react-native-library/src/prompt.ts | 22 +++++++++- .../src/template.ts | 28 +++++++++++++ .../common/$.github/workflows/ci.yml | 10 +++++ .../templates/common/$package.json | 16 +++++++- .../templates/common/CONTRIBUTING.md | 15 +++++++ .../cpp-library/android/CMakeLists.txt | 30 ++++++++++++++ .../cpp/{%- project.name %}Impl.cpp | 18 ++++++++ .../cpp-library/cpp/{%- project.name %}Impl.h | 17 ++++++++ .../templates/cpp-library/ios/OnLoad.mm | 22 ++++++++++ .../cpp-library/react-native.config.js | 15 +++++++ .../cpp-library/{%- project.name %}.podspec | 20 +++++++++ 15 files changed, 237 insertions(+), 42 deletions(-) create mode 100644 packages/create-react-native-library/templates/cpp-library/android/CMakeLists.txt create mode 100644 packages/create-react-native-library/templates/cpp-library/cpp/{%- project.name %}Impl.cpp create mode 100644 packages/create-react-native-library/templates/cpp-library/cpp/{%- project.name %}Impl.h create mode 100644 packages/create-react-native-library/templates/cpp-library/ios/OnLoad.mm create mode 100644 packages/create-react-native-library/templates/cpp-library/react-native.config.js create mode 100644 packages/create-react-native-library/templates/cpp-library/{%- project.name %}.podspec diff --git a/.github/workflows/build-templates.yml b/.github/workflows/build-templates.yml index 4e70f23ab..adcfb6a4c 100644 --- a/.github/workflows/build-templates.yml +++ b/.github/workflows/build-templates.yml @@ -34,6 +34,8 @@ jobs: type: - name: turbo-module language: kotlin-objc + - name: turbo-module + language: cpp - name: fabric-view language: kotlin-objc - name: nitro-module @@ -151,14 +153,14 @@ jobs: run: | # Build Android for only some matrices to skip redundant builds if [[ ${{ matrix.os }} =~ ubuntu ]]; then - if [[ ${{ matrix.type.name }} == *-view && ${{ matrix.type.language }} == *-objc ]] || [[ ${{ matrix.type.name }} == *-module && ${{ matrix.type.language }} == *-objc ]] || [[ ${{ matrix.type.name }} == nitro-* ]]; then + if [[ ${{ matrix.type.name }} == *-view && ${{ matrix.type.language }} == *-objc ]] || [[ ${{ matrix.type.name }} == *-module && ( ${{ matrix.type.language }} == *-objc || ${{ matrix.type.language }} == cpp ) ]] || [[ ${{ matrix.type.name }} == nitro-* ]]; then echo "android_build=1" >> $GITHUB_ENV fi fi # Build iOS for only some matrices to skip redundant builds if [[ ${{ matrix.os }} =~ macos ]]; then - if [[ ${{ matrix.type.name }} == *-view && ${{ matrix.type.language }} == kotlin-* ]] || [[ ${{ matrix.type.name }} == *-module && ${{ matrix.type.language }} == kotlin-* ]] || [[ ${{ matrix.type.name }} == nitro-* ]]; then + if [[ ${{ matrix.type.name }} == *-view && ${{ matrix.type.language }} == kotlin-* ]] || [[ ${{ matrix.type.name }} == *-module && ( ${{ matrix.type.language }} == kotlin-* || ${{ matrix.type.language }} == cpp ) ]] || [[ ${{ matrix.type.name }} == nitro-* ]]; then echo "ios_build=1" >> $GITHUB_ENV fi fi @@ -168,6 +170,11 @@ jobs: working-directory: ${{ env.work_dir }} run: yarn nitrogen + - name: Generate codegen native code + if: matrix.type.language == 'cpp' + working-directory: ${{ env.work_dir }} + run: yarn bob build --target codegen + - name: Cache turborepo if: env.android_build == 1 || env.ios_build == 1 uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 diff --git a/docs/pages/create.md b/docs/pages/create.md index b00dcf210..c5dae2469 100644 --- a/docs/pages/create.md +++ b/docs/pages/create.md @@ -95,3 +95,5 @@ Once the project is created, you can follow the official React Native docs to le - [Fabric Components](https://reactnative.dev/docs/fabric-native-components-introduction) Turbo Modules and Fabric components don't have native support for Swift. If you want to write the iOS implementation in Swift for a Turbo Module or Fabric View, see [Swift with Turbo Modules and Fabric](./swift-new-architecture.md). + +> Note: The C++ template is currently experimental and only works with `includesGeneratedCode: true`. This can make it incompatible with React Native versions other than the one used to generate the codegen files. See [Including Generated Code into Libraries](https://reactnative.dev/docs/the-new-architecture/codegen-cli#including-generated-code-into-libraries) in the React Native docs for more details. diff --git a/packages/create-react-native-library/src/exampleApp/dependencies.ts b/packages/create-react-native-library/src/exampleApp/dependencies.ts index 13d859a4b..897031d46 100644 --- a/packages/create-react-native-library/src/exampleApp/dependencies.ts +++ b/packages/create-react-native-library/src/exampleApp/dependencies.ts @@ -4,6 +4,9 @@ import sortObjectKeys from '../utils/sortObjectKeys'; type PackageJson = { devDependencies?: Record; + 'react-native-builder-bob'?: { + targets?: (string | [string, unknown])[]; + }; }; export async function alignDependencyVersionsWithExampleApp( @@ -21,6 +24,15 @@ export async function alignDependencyVersionsWithExampleApp( '@react-native/babel-preset', ]; + const usesCodegen = + pkg['react-native-builder-bob']?.targets?.some((target) => + Array.isArray(target) ? target[0] === 'codegen' : target === 'codegen' + ) ?? false; + + if (usesCodegen) { + PACKAGES_TO_COPY.push('@react-native-community/cli'); + } + const devDependencies: Record = {}; PACKAGES_TO_COPY.forEach((name) => { diff --git a/packages/create-react-native-library/src/exampleApp/generateExampleApp.ts b/packages/create-react-native-library/src/exampleApp/generateExampleApp.ts index a789eb146..c18c4f379 100644 --- a/packages/create-react-native-library/src/exampleApp/generateExampleApp.ts +++ b/packages/create-react-native-library/src/exampleApp/generateExampleApp.ts @@ -327,22 +327,7 @@ export default async function generateExampleApp({ spaces: 2, }); - if (config.example !== 'expo') { - let gradleProperties = await fs.readFile( - path.join(directory, 'android', 'gradle.properties'), - 'utf8' - ); - - // Disable Jetifier. - // Remove this when the app template is updated. - gradleProperties = gradleProperties.replace( - 'android.enableJetifier=true', - 'android.enableJetifier=false' - ); - - // Enable new arch for iOS and Android - // iOS - // Add ENV['RCT_NEW_ARCH_ENABLED'] = 1 on top of example/ios/Podfile + if (config.example === 'vanilla' && config.project.cpp) { const podfile = await fs.readFile( path.join(directory, 'ios', 'Podfile'), 'utf8' @@ -350,28 +335,8 @@ export default async function generateExampleApp({ await fs.writeFile( path.join(directory, 'ios', 'Podfile'), - "ENV['RCT_NEW_ARCH_ENABLED'] = '1'\n\n" + podfile - ); - - // Android - // Make sure newArchEnabled=true is present in android/gradle.properties - if (gradleProperties.split('\n').includes('#newArchEnabled=true')) { - gradleProperties = gradleProperties.replace( - '#newArchEnabled=true', - 'newArchEnabled=true' - ); - } else if (gradleProperties.split('\n').includes('newArchEnabled=false')) { - gradleProperties = gradleProperties.replace( - 'newArchEnabled=false', - 'newArchEnabled=true' - ); - } else if (!gradleProperties.split('\n').includes('newArchEnabled=true')) { - gradleProperties += '\nnewArchEnabled=true'; - } - - await fs.writeFile( - path.join(directory, 'android', 'gradle.properties'), - gradleProperties + "ENV['RCT_USE_RN_DEP'] = '1' # Needed to make iOS build work for C++ module\n\n" + + podfile ); } } diff --git a/packages/create-react-native-library/src/prompt.ts b/packages/create-react-native-library/src/prompt.ts index 620c4c531..5cf586ce3 100644 --- a/packages/create-react-native-library/src/prompt.ts +++ b/packages/create-react-native-library/src/prompt.ts @@ -11,7 +11,7 @@ export type Answers = NonNullable>>; export type ExampleApp = 'test-app' | 'expo' | 'vanilla' | undefined; -export type ProjectLanguages = 'kotlin-objc' | 'kotlin-swift' | 'js'; +export type ProjectLanguages = 'kotlin-objc' | 'kotlin-swift' | 'cpp' | 'js'; export type ProjectType = | 'turbo-module' @@ -56,6 +56,7 @@ const LANGUAGE_CHOICES: { title: string; value: ProjectLanguages; types: ProjectType[]; + description?: string; }[] = [ { title: 'Kotlin & Swift', @@ -67,6 +68,11 @@ const LANGUAGE_CHOICES: { value: 'kotlin-objc', types: ['turbo-module', 'fabric-view'], }, + { + title: 'C++ (Experimental)', + value: 'cpp', + types: ['turbo-module'], + }, { title: 'JavaScript for Android, iOS & Web', value: 'js', @@ -299,9 +305,14 @@ export const prompt = create(['[name]'], { choices: LANGUAGE_CHOICES.map((choice) => ({ title: choice.title, value: choice.value, + description: choice.description, skip: (): boolean => { const answers = prompt.read(); + if (choice.value === 'cpp' && answers.local === true) { + return true; + } + if (typeof answers.type === 'string') { return !choice.types.includes(answers.type); } @@ -319,6 +330,15 @@ export const prompt = create(['[name]'], { title: choice.title, value: choice.value, description: choice.description, + skip: (): boolean => { + const answers = prompt.read(); + + if (answers.languages === 'cpp') { + return choice.value !== 'vanilla'; + } + + return false; + }, })), required: true, skip: (): boolean => { diff --git a/packages/create-react-native-library/src/template.ts b/packages/create-react-native-library/src/template.ts index d57e23a34..c13c3c977 100644 --- a/packages/create-react-native-library/src/template.ts +++ b/packages/create-react-native-library/src/template.ts @@ -29,6 +29,7 @@ export type TemplateConfiguration = { package_cpp: string; identifier: string; native: boolean; + cpp: boolean; swift: boolean; viewConfig: ViewConfig; moduleConfig: ModuleConfig; @@ -75,6 +76,7 @@ const EXAMPLE_NATIVE_COMMON_FILES = path.resolve( '../templates/example-native-common' ); const NITRO_COMMON_FILES = path.resolve(__dirname, '../templates/nitro-common'); +const CPP_FILES = path.resolve(__dirname, '../templates/cpp-library'); const NATIVE_FILES = { module_new: path.resolve(__dirname, '../templates/native-library-new'), @@ -137,6 +139,7 @@ export function generateTemplateConfiguration({ package_cpp: pack.replace(/\./g, '_'), identifier: slug.replace(/[^a-z0-9]+/g, '-').replace(/^-/, ''), native: languages !== 'js', + cpp: languages === 'cpp', swift: languages === 'kotlin-swift', viewConfig: getViewConfig(type), moduleConfig: getModuleConfig(type), @@ -214,6 +217,31 @@ export async function applyTemplates( } } } else { + if (answers.languages === 'cpp') { + if (config.example === 'expo') { + await applyTemplate(config, EXAMPLE_EXPO_FILES, folder); + + if (config.project.native) { + await applyTemplate(config, EXAMPLE_NATIVE_COMMON_FILES, folder); + } + } else if (config.example != null) { + await applyTemplate(config, EXAMPLE_BARE_FILES, folder); + + if (config.project.native) { + await applyTemplate(config, EXAMPLE_NATIVE_COMMON_FILES, folder); + } + } + + await applyTemplate(config, NATIVE_FILES['module_new'], folder); + await applyTemplate(config, CPP_FILES, folder); + + if (config.example === 'expo' || config.tools.includes('vite')) { + await applyTemplate(config, JS_FILES, folder); + } + + return; + } + await applyTemplate(config, NATIVE_COMMON_FILES, folder); if (config.example === 'expo') { diff --git a/packages/create-react-native-library/templates/common/$.github/workflows/ci.yml b/packages/create-react-native-library/templates/common/$.github/workflows/ci.yml index b6cf8b81c..5f9b12860 100644 --- a/packages/create-react-native-library/templates/common/$.github/workflows/ci.yml +++ b/packages/create-react-native-library/templates/common/$.github/workflows/ci.yml @@ -79,6 +79,11 @@ jobs: - name: Generate nitrogen code run: yarn nitrogen <% } -%> +<% if (project.cpp) { -%> + + - name: Generate codegen native code + run: yarn bob build --target codegen +<% } -%> - name: Cache turborepo for Android uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 @@ -152,6 +157,11 @@ jobs: - name: Generate nitrogen code run: yarn nitrogen <% } -%> +<% if (project.cpp) { -%> + + - name: Generate codegen native code + run: yarn bob build --target codegen +<% } -%> - name: Cache turborepo for iOS uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 diff --git a/packages/create-react-native-library/templates/common/$package.json b/packages/create-react-native-library/templates/common/$package.json index 8ec303a4e..dc33466b6 100644 --- a/packages/create-react-native-library/templates/common/$package.json +++ b/packages/create-react-native-library/templates/common/$package.json @@ -69,6 +69,9 @@ "registry": "https://registry.npmjs.org/" }, "devDependencies": { +<% if (project.cpp) { -%> + "@react-native-community/cli": "^20.1.2", +<% } -%> "@react-native/babel-preset": "0.83.0", "@types/react": "^19.2.0", "del-cli": "^7.0.0", @@ -122,7 +125,9 @@ { "project": "tsconfig.build.json" } - ] + ]<% if (project.cpp) { -%>, + "codegen" +<% } -%> ] <% if (project.moduleConfig === 'turbo-modules' || project.viewConfig === 'fabric-view') { -%> }, @@ -130,6 +135,12 @@ "name": "<%- project.name -%><%- project.viewConfig !== null ? 'View': '' -%>Spec", "type": "<%- project.viewConfig !== null ? 'all': 'modules' -%>", "jsSrcsDir": "src", +<% if (project.cpp) { -%> + "outputDir": { + "ios": "ios/generated", + "android": "android/generated" + }, +<% } -%> "android": { "javaPackageName": "com.<%- project.package %>" <% if (project.viewConfig === 'fabric-view') { -%> @@ -142,6 +153,9 @@ } <% } -%> } +<% if (project.cpp) { -%>, + "includesGeneratedCode": true +<% } -%> <% } -%> } } diff --git a/packages/create-react-native-library/templates/common/CONTRIBUTING.md b/packages/create-react-native-library/templates/common/CONTRIBUTING.md index 78cdab2b4..e1dd10fff 100644 --- a/packages/create-react-native-library/templates/common/CONTRIBUTING.md +++ b/packages/create-react-native-library/templates/common/CONTRIBUTING.md @@ -37,6 +37,21 @@ To invoke **Nitrogen**, use the following command: yarn nitrogen ``` +<% } -%> +<% if (project.cpp) { -%> +You need to run React Native Codegen to generate the native scaffolding for the C++ Turbo Module. The example app will not build without these generated files. + +Run **Codegen** in following cases: + +- When you make changes to `src/Native<%- project.name -%>.ts`. +- When running the project for the first time (since the generated files are not committed to the repository). + +To invoke **Codegen**, use the following command: + +```sh +yarn bob build --target codegen +``` + <% } -%> The [example app](/example/) demonstrates usage of the library. You need to run it to test any changes you make. diff --git a/packages/create-react-native-library/templates/cpp-library/android/CMakeLists.txt b/packages/create-react-native-library/templates/cpp-library/android/CMakeLists.txt new file mode 100644 index 000000000..1432d65c6 --- /dev/null +++ b/packages/create-react-native-library/templates/cpp-library/android/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.4.1) +project(<%- project.name -%>) + +set (CMAKE_VERBOSE_MAKEFILE ON) + +add_library( + <%- project.identifier -%> + STATIC + ../cpp/<%- project.name -%>Impl.cpp +) + +set_target_properties( + <%- project.identifier -%> PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF +) + +target_include_directories( + <%- project.identifier -%> + PUBLIC + ../cpp +) + +target_link_libraries( + <%- project.identifier -%> + jsi + reactnative + react_codegen_<%- project.name -%>Spec +) diff --git a/packages/create-react-native-library/templates/cpp-library/cpp/{%- project.name %}Impl.cpp b/packages/create-react-native-library/templates/cpp-library/cpp/{%- project.name %}Impl.cpp new file mode 100644 index 000000000..0481fa9c8 --- /dev/null +++ b/packages/create-react-native-library/templates/cpp-library/cpp/{%- project.name %}Impl.cpp @@ -0,0 +1,18 @@ +#include "<%- project.name -%>Impl.h" + +namespace facebook::react { + +<%- project.name -%>Impl::<%- project.name -%>Impl( + std::shared_ptr jsInvoker +) + : Native<%- project.name -%>CxxSpec(std::move(jsInvoker)) {} + +double <%- project.name -%>Impl::multiply( + jsi::Runtime& rt, + double a, + double b +) { + return a * b; +} + +} diff --git a/packages/create-react-native-library/templates/cpp-library/cpp/{%- project.name %}Impl.h b/packages/create-react-native-library/templates/cpp-library/cpp/{%- project.name %}Impl.h new file mode 100644 index 000000000..e4b69d606 --- /dev/null +++ b/packages/create-react-native-library/templates/cpp-library/cpp/{%- project.name %}Impl.h @@ -0,0 +1,17 @@ +#pragma once + +#include <<%- project.name -%>SpecJSI.h> + +#include + +namespace facebook::react { + +class <%- project.name -%>Impl + : public Native<%- project.name -%>CxxSpec<<%- project.name -%>Impl> { +public: + <%- project.name -%>Impl(std::shared_ptr jsInvoker); + + double multiply(jsi::Runtime& rt, double a, double b); +}; + +} diff --git a/packages/create-react-native-library/templates/cpp-library/ios/OnLoad.mm b/packages/create-react-native-library/templates/cpp-library/ios/OnLoad.mm new file mode 100644 index 000000000..8caa4c2a1 --- /dev/null +++ b/packages/create-react-native-library/templates/cpp-library/ios/OnLoad.mm @@ -0,0 +1,22 @@ +#import +#import "<%- project.name -%>Impl.h" +#import + +@interface <%- project.name -%>OnLoad : NSObject +@end + +@implementation <%- project.name -%>OnLoad + +using namespace facebook::react; + ++ (void)load +{ + registerCxxModuleToGlobalModuleMap( + std::string(<%- project.name -%>Impl::kModuleName), + [](std::shared_ptr jsInvoker) { + return std::make_shared<<%- project.name -%>Impl>(jsInvoker); + } + ); +} + +@end diff --git a/packages/create-react-native-library/templates/cpp-library/react-native.config.js b/packages/create-react-native-library/templates/cpp-library/react-native.config.js new file mode 100644 index 000000000..d0e6274ff --- /dev/null +++ b/packages/create-react-native-library/templates/cpp-library/react-native.config.js @@ -0,0 +1,15 @@ +/** + * @type {import('@react-native-community/cli-types').UserDependencyConfig} + */ +module.exports = { + dependency: { + platforms: { + android: { + cmakeListsPath: 'generated/jni/CMakeLists.txt', + cxxModuleCMakeListsModuleName: '<%- project.identifier -%>', + cxxModuleCMakeListsPath: 'CMakeLists.txt', + cxxModuleHeaderName: '<%- project.name -%>Impl', + }, + }, + }, +}; diff --git a/packages/create-react-native-library/templates/cpp-library/{%- project.name %}.podspec b/packages/create-react-native-library/templates/cpp-library/{%- project.name %}.podspec new file mode 100644 index 000000000..8ee7ecb54 --- /dev/null +++ b/packages/create-react-native-library/templates/cpp-library/{%- project.name %}.podspec @@ -0,0 +1,20 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) + +Pod::Spec.new do |s| + s.name = "<%- project.name -%>" + s.version = package["version"] + s.summary = package["description"] + s.homepage = package["homepage"] + s.license = package["license"] + s.authors = package["author"] + + s.platforms = { :ios => min_ios_version_supported } + s.source = { :git => "<%- repo -%>.git", :tag => "#{s.version}" } + + s.source_files = "ios/**/*.{h,m,mm}", "cpp/**/*.{hpp,cpp,c,h}", "ios/generated/*.{h,cpp,mm}" + s.private_header_files = "ios/**/*.h" + + install_modules_dependencies(s) +end