Skip to content

Commit db09e6b

Browse files
authored
feat: add experimental c++ turbo module (#938)
1 parent a6b16a0 commit db09e6b

15 files changed

Lines changed: 237 additions & 42 deletions

File tree

.github/workflows/build-templates.yml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ jobs:
3434
type:
3535
- name: turbo-module
3636
language: kotlin-objc
37+
- name: turbo-module
38+
language: cpp
3739
- name: fabric-view
3840
language: kotlin-objc
3941
- name: nitro-module
@@ -151,14 +153,14 @@ jobs:
151153
run: |
152154
# Build Android for only some matrices to skip redundant builds
153155
if [[ ${{ matrix.os }} =~ ubuntu ]]; then
154-
if [[ ${{ matrix.type.name }} == *-view && ${{ matrix.type.language }} == *-objc ]] || [[ ${{ matrix.type.name }} == *-module && ${{ matrix.type.language }} == *-objc ]] || [[ ${{ matrix.type.name }} == nitro-* ]]; then
156+
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
155157
echo "android_build=1" >> $GITHUB_ENV
156158
fi
157159
fi
158160
159161
# Build iOS for only some matrices to skip redundant builds
160162
if [[ ${{ matrix.os }} =~ macos ]]; then
161-
if [[ ${{ matrix.type.name }} == *-view && ${{ matrix.type.language }} == kotlin-* ]] || [[ ${{ matrix.type.name }} == *-module && ${{ matrix.type.language }} == kotlin-* ]] || [[ ${{ matrix.type.name }} == nitro-* ]]; then
163+
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
162164
echo "ios_build=1" >> $GITHUB_ENV
163165
fi
164166
fi
@@ -168,6 +170,11 @@ jobs:
168170
working-directory: ${{ env.work_dir }}
169171
run: yarn nitrogen
170172

173+
- name: Generate codegen native code
174+
if: matrix.type.language == 'cpp'
175+
working-directory: ${{ env.work_dir }}
176+
run: yarn bob build --target codegen
177+
171178
- name: Cache turborepo
172179
if: env.android_build == 1 || env.ios_build == 1
173180
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3

docs/pages/create.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,5 @@ Once the project is created, you can follow the official React Native docs to le
9595
- [Fabric Components](https://reactnative.dev/docs/fabric-native-components-introduction)
9696

9797
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).
98+
99+
> 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.

packages/create-react-native-library/src/exampleApp/dependencies.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import sortObjectKeys from '../utils/sortObjectKeys';
44

55
type PackageJson = {
66
devDependencies?: Record<string, string>;
7+
'react-native-builder-bob'?: {
8+
targets?: (string | [string, unknown])[];
9+
};
710
};
811

912
export async function alignDependencyVersionsWithExampleApp(
@@ -21,6 +24,15 @@ export async function alignDependencyVersionsWithExampleApp(
2124
'@react-native/babel-preset',
2225
];
2326

27+
const usesCodegen =
28+
pkg['react-native-builder-bob']?.targets?.some((target) =>
29+
Array.isArray(target) ? target[0] === 'codegen' : target === 'codegen'
30+
) ?? false;
31+
32+
if (usesCodegen) {
33+
PACKAGES_TO_COPY.push('@react-native-community/cli');
34+
}
35+
2436
const devDependencies: Record<string, string> = {};
2537

2638
PACKAGES_TO_COPY.forEach((name) => {

packages/create-react-native-library/src/exampleApp/generateExampleApp.ts

Lines changed: 3 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -327,51 +327,16 @@ export default async function generateExampleApp({
327327
spaces: 2,
328328
});
329329

330-
if (config.example !== 'expo') {
331-
let gradleProperties = await fs.readFile(
332-
path.join(directory, 'android', 'gradle.properties'),
333-
'utf8'
334-
);
335-
336-
// Disable Jetifier.
337-
// Remove this when the app template is updated.
338-
gradleProperties = gradleProperties.replace(
339-
'android.enableJetifier=true',
340-
'android.enableJetifier=false'
341-
);
342-
343-
// Enable new arch for iOS and Android
344-
// iOS
345-
// Add ENV['RCT_NEW_ARCH_ENABLED'] = 1 on top of example/ios/Podfile
330+
if (config.example === 'vanilla' && config.project.cpp) {
346331
const podfile = await fs.readFile(
347332
path.join(directory, 'ios', 'Podfile'),
348333
'utf8'
349334
);
350335

351336
await fs.writeFile(
352337
path.join(directory, 'ios', 'Podfile'),
353-
"ENV['RCT_NEW_ARCH_ENABLED'] = '1'\n\n" + podfile
354-
);
355-
356-
// Android
357-
// Make sure newArchEnabled=true is present in android/gradle.properties
358-
if (gradleProperties.split('\n').includes('#newArchEnabled=true')) {
359-
gradleProperties = gradleProperties.replace(
360-
'#newArchEnabled=true',
361-
'newArchEnabled=true'
362-
);
363-
} else if (gradleProperties.split('\n').includes('newArchEnabled=false')) {
364-
gradleProperties = gradleProperties.replace(
365-
'newArchEnabled=false',
366-
'newArchEnabled=true'
367-
);
368-
} else if (!gradleProperties.split('\n').includes('newArchEnabled=true')) {
369-
gradleProperties += '\nnewArchEnabled=true';
370-
}
371-
372-
await fs.writeFile(
373-
path.join(directory, 'android', 'gradle.properties'),
374-
gradleProperties
338+
"ENV['RCT_USE_RN_DEP'] = '1' # Needed to make iOS build work for C++ module\n\n" +
339+
podfile
375340
);
376341
}
377342
}

packages/create-react-native-library/src/prompt.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export type Answers = NonNullable<Awaited<ReturnType<typeof prompt.show>>>;
1111

1212
export type ExampleApp = 'test-app' | 'expo' | 'vanilla' | undefined;
1313

14-
export type ProjectLanguages = 'kotlin-objc' | 'kotlin-swift' | 'js';
14+
export type ProjectLanguages = 'kotlin-objc' | 'kotlin-swift' | 'cpp' | 'js';
1515

1616
export type ProjectType =
1717
| 'turbo-module'
@@ -56,6 +56,7 @@ const LANGUAGE_CHOICES: {
5656
title: string;
5757
value: ProjectLanguages;
5858
types: ProjectType[];
59+
description?: string;
5960
}[] = [
6061
{
6162
title: 'Kotlin & Swift',
@@ -67,6 +68,11 @@ const LANGUAGE_CHOICES: {
6768
value: 'kotlin-objc',
6869
types: ['turbo-module', 'fabric-view'],
6970
},
71+
{
72+
title: 'C++ (Experimental)',
73+
value: 'cpp',
74+
types: ['turbo-module'],
75+
},
7076
{
7177
title: 'JavaScript for Android, iOS & Web',
7278
value: 'js',
@@ -299,9 +305,14 @@ export const prompt = create(['[name]'], {
299305
choices: LANGUAGE_CHOICES.map((choice) => ({
300306
title: choice.title,
301307
value: choice.value,
308+
description: choice.description,
302309
skip: (): boolean => {
303310
const answers = prompt.read();
304311

312+
if (choice.value === 'cpp' && answers.local === true) {
313+
return true;
314+
}
315+
305316
if (typeof answers.type === 'string') {
306317
return !choice.types.includes(answers.type);
307318
}
@@ -319,6 +330,15 @@ export const prompt = create(['[name]'], {
319330
title: choice.title,
320331
value: choice.value,
321332
description: choice.description,
333+
skip: (): boolean => {
334+
const answers = prompt.read();
335+
336+
if (answers.languages === 'cpp') {
337+
return choice.value !== 'vanilla';
338+
}
339+
340+
return false;
341+
},
322342
})),
323343
required: true,
324344
skip: (): boolean => {

packages/create-react-native-library/src/template.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export type TemplateConfiguration = {
2929
package_cpp: string;
3030
identifier: string;
3131
native: boolean;
32+
cpp: boolean;
3233
swift: boolean;
3334
viewConfig: ViewConfig;
3435
moduleConfig: ModuleConfig;
@@ -75,6 +76,7 @@ const EXAMPLE_NATIVE_COMMON_FILES = path.resolve(
7576
'../templates/example-native-common'
7677
);
7778
const NITRO_COMMON_FILES = path.resolve(__dirname, '../templates/nitro-common');
79+
const CPP_FILES = path.resolve(__dirname, '../templates/cpp-library');
7880

7981
const NATIVE_FILES = {
8082
module_new: path.resolve(__dirname, '../templates/native-library-new'),
@@ -137,6 +139,7 @@ export function generateTemplateConfiguration({
137139
package_cpp: pack.replace(/\./g, '_'),
138140
identifier: slug.replace(/[^a-z0-9]+/g, '-').replace(/^-/, ''),
139141
native: languages !== 'js',
142+
cpp: languages === 'cpp',
140143
swift: languages === 'kotlin-swift',
141144
viewConfig: getViewConfig(type),
142145
moduleConfig: getModuleConfig(type),
@@ -214,6 +217,31 @@ export async function applyTemplates(
214217
}
215218
}
216219
} else {
220+
if (answers.languages === 'cpp') {
221+
if (config.example === 'expo') {
222+
await applyTemplate(config, EXAMPLE_EXPO_FILES, folder);
223+
224+
if (config.project.native) {
225+
await applyTemplate(config, EXAMPLE_NATIVE_COMMON_FILES, folder);
226+
}
227+
} else if (config.example != null) {
228+
await applyTemplate(config, EXAMPLE_BARE_FILES, folder);
229+
230+
if (config.project.native) {
231+
await applyTemplate(config, EXAMPLE_NATIVE_COMMON_FILES, folder);
232+
}
233+
}
234+
235+
await applyTemplate(config, NATIVE_FILES['module_new'], folder);
236+
await applyTemplate(config, CPP_FILES, folder);
237+
238+
if (config.example === 'expo' || config.tools.includes('vite')) {
239+
await applyTemplate(config, JS_FILES, folder);
240+
}
241+
242+
return;
243+
}
244+
217245
await applyTemplate(config, NATIVE_COMMON_FILES, folder);
218246

219247
if (config.example === 'expo') {

packages/create-react-native-library/templates/common/$.github/workflows/ci.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ jobs:
7979
- name: Generate nitrogen code
8080
run: yarn nitrogen
8181
<% } -%>
82+
<% if (project.cpp) { -%>
83+
84+
- name: Generate codegen native code
85+
run: yarn bob build --target codegen
86+
<% } -%>
8287

8388
- name: Cache turborepo for Android
8489
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
@@ -152,6 +157,11 @@ jobs:
152157
- name: Generate nitrogen code
153158
run: yarn nitrogen
154159
<% } -%>
160+
<% if (project.cpp) { -%>
161+
162+
- name: Generate codegen native code
163+
run: yarn bob build --target codegen
164+
<% } -%>
155165

156166
- name: Cache turborepo for iOS
157167
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3

packages/create-react-native-library/templates/common/$package.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@
6969
"registry": "https://registry.npmjs.org/"
7070
},
7171
"devDependencies": {
72+
<% if (project.cpp) { -%>
73+
"@react-native-community/cli": "^20.1.2",
74+
<% } -%>
7275
"@react-native/babel-preset": "0.83.0",
7376
"@types/react": "^19.2.0",
7477
"del-cli": "^7.0.0",
@@ -122,14 +125,22 @@
122125
{
123126
"project": "tsconfig.build.json"
124127
}
125-
]
128+
]<% if (project.cpp) { -%>,
129+
"codegen"
130+
<% } -%>
126131
]
127132
<% if (project.moduleConfig === 'turbo-modules' || project.viewConfig === 'fabric-view') { -%>
128133
},
129134
"codegenConfig": {
130135
"name": "<%- project.name -%><%- project.viewConfig !== null ? 'View': '' -%>Spec",
131136
"type": "<%- project.viewConfig !== null ? 'all': 'modules' -%>",
132137
"jsSrcsDir": "src",
138+
<% if (project.cpp) { -%>
139+
"outputDir": {
140+
"ios": "ios/generated",
141+
"android": "android/generated"
142+
},
143+
<% } -%>
133144
"android": {
134145
"javaPackageName": "com.<%- project.package %>"
135146
<% if (project.viewConfig === 'fabric-view') { -%>
@@ -142,6 +153,9 @@
142153
}
143154
<% } -%>
144155
}
156+
<% if (project.cpp) { -%>,
157+
"includesGeneratedCode": true
158+
<% } -%>
145159
<% } -%>
146160
}
147161
}

packages/create-react-native-library/templates/common/CONTRIBUTING.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,21 @@ To invoke **Nitrogen**, use the following command:
3737
yarn nitrogen
3838
```
3939

40+
<% } -%>
41+
<% if (project.cpp) { -%>
42+
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.
43+
44+
Run **Codegen** in following cases:
45+
46+
- When you make changes to `src/Native<%- project.name -%>.ts`.
47+
- When running the project for the first time (since the generated files are not committed to the repository).
48+
49+
To invoke **Codegen**, use the following command:
50+
51+
```sh
52+
yarn bob build --target codegen
53+
```
54+
4055
<% } -%>
4156
The [example app](/example/) demonstrates usage of the library. You need to run it to test any changes you make.
4257

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
cmake_minimum_required(VERSION 3.4.1)
2+
project(<%- project.name -%>)
3+
4+
set (CMAKE_VERBOSE_MAKEFILE ON)
5+
6+
add_library(
7+
<%- project.identifier -%>
8+
STATIC
9+
../cpp/<%- project.name -%>Impl.cpp
10+
)
11+
12+
set_target_properties(
13+
<%- project.identifier -%> PROPERTIES
14+
CXX_STANDARD 20
15+
CXX_STANDARD_REQUIRED ON
16+
CXX_EXTENSIONS OFF
17+
)
18+
19+
target_include_directories(
20+
<%- project.identifier -%>
21+
PUBLIC
22+
../cpp
23+
)
24+
25+
target_link_libraries(
26+
<%- project.identifier -%>
27+
jsi
28+
reactnative
29+
react_codegen_<%- project.name -%>Spec
30+
)

0 commit comments

Comments
 (0)