Create a Turbo Module
A Turbo Module can be written in a standalone library, or as part of an app package. We call these "standalone" and "in-app" Turbo Modules, respectively.
The Vega SDK provides templates to create Turbo Modules in standalone libraries, but you can choose which way is best for your project. The steps below cover both ways. When you are done, the directory structure will be similar to the following.
[project-root]/
├─ kepler/
│ ├─ turbo-modules/
│ │ ├─ generated/
│ │ │ ├─ (C++ specifications)
│ │ │
│ │ ├─ (C++ implementations)
│ │
│ ├─ AutoLinkInit.cpp (autolink setup)
│
├─ src/
│ ├─ turbo-modules/
│ │ ├─ (TypeScript interface files)
│ │
│ ├─ App.tsx (only for in-app Turbo Modules)
│
├─ tst/
│ ├─ (JS test files)
│
├─ .eslintrc
├─ .prettierrc
├─ babel.config.js
├─ CMakeLists.txt
├─ jest.config.json
├─ metro.config.js
├─ package.json
├─ react-native.config.js
├─ tsconfig.json
├─ tsconfig-build.json
Setup
In-app Turbo Module
For an in-app module, follow the instructions in Build an App to create your starting app.
Standalone Turbo Module
For a standalone module, you can start from a Turbo Module template. From the root of your package, run the command below to see a list of available templates.
kepler project list-templates
For a Turbo Module, you will pick one of the following to generate a project:
idl-turbo-module
: If you are consuming an OS API (IDL)basic-turbo-module
If you are creating a Turbo Module from your own code
When the command succeeds, the output directory will contain a new Turbo Module project.
IDL Turbo Module
kepler project generate -t idl-turbo-module -n <CamelCaseProjectName> --idlPackage <ReverseDNSNameForIDLPackage>
An IDL Turbo Module template accepts the following additional args:
-
--idlPackage
: The IDL package to consume in the Turbo Module. This is formatted in reverse DNS notation, for examplecom.amazondeveloper.kepler.foo
. -
--outputDir
: [Optional] The directory in which to generate the project. If not specified, the project is generated in the folder in which you ran the command.
The project generated by the template only declares a dependency on the supplied OKIDL interface. Functional consumption of the interface requires code changes in the Turbo Module C++ implementation files by the developer. However, even without these changes, the project will find the interface requested and build successfully after it is generated from the templates.
Example
kepler project generate -t idl-turbo-module -n TestIdlTm --idlPackage com.amazon.kepler.i18n.text.message_format_classic --interfacePackage VegaI18nLibraryInterface
Plain Turbo Module
kepler project generate -t turbomodule -n <CamelCaseProjectName> --idlPackage <ReverseDNSNameForIDLPackage>
A plain Turbo Module template accepts the following additional arguements:
--outputDir
: [Optional] The directory in which to generate the project. If not specified, the project is generated in the folder in which you ran the command.
Build a Turbo Module
Step 1: Install TurbomoduleAPI
If you created your Turbo Module using a template, the @amzn/keplerscript-turbomodule-api
package is already listed under the dependencies section of your package.json and you can skip this step.
From the root of your package, run the following command.
npm install --save @amzn/keplerscript-turbomodule-api
Step 2: Create interface definitions in TypeScript
If you used a Turbo Module template, this file already exists, but you must to modify it to suit your needs. The file NativePinger.ts (note: The file is named based on the name you passed to the template) defines the native module's interface to JavaScript. By convention, it is located in src/turbo-modules
.
The interface should look similar to this:
import type {VegaTurboModule} from '@amzn/keplerscript-turbomodule-api';
import {TurboModuleRegistry} from '@amzn/keplerscript-turbomodule-api';
export interface Pinger extends VegaTurboModule {
ping: () => number;
}
export default TurboModuleRegistry.getEnforcing<Pinger>('Pinger');
The previous codes defines an interface called Pinger
which exposes a single function ping
that returns a number. For more information on what return types are available, see JavaScript types in native code.
TurboModuleRegistry.getEnforcing
uses 'Pinger'
as a lookup key to load the module's native object, and raises an exception if it is not found. Then, we bind the Pinger
interface to the returned object.
TurboModuleRegistry
, nor does it have to match C++ class or file names used later. The codegen tool in the next step produces a matching entry in the C++ stub code. However, you will have to use this name again in the autolinking configuration later.Step 3: Run codegen to generate native scaffolding
The TurbomoduleAPI library includes a codegen tool that allows you to generate C++ scaffolding based on your TypeScript interface. The generated code defines the C++ specification for your Turbo Module. To use codegen, run the following commands.
npx keplerscript-turbomodule-api codegen src/turbo-modules/<NativePinger.ts> \
-o vega/0.21/turbo-modules/ \
--new \
--namespace PingerTurboModule
Not Implemented
errors at runtime unless you add an implementation in the next step.Codegen only accepts one TypeScript interface at a time; if you are authoring multiple Turbo Modules in the same package, you can run codegen multiple times with different arguments.
More information about the codegen tool is available by running npx keplerscript-turbomodule-api codegen -h
or from the Vega Codegen FAQ.
Step 4: Implement C++ functions
Typically, you won't need to manually modify the files created by codegen under the kepler/turbo-modules/generated/
directory. If you do, be aware that rerunning codegen will overwrite these files. To avoid losing updates, cache your modified files elsewhere and reapply your modifications after rerunning codegen.
The C++ (.h/.cpp
) files in the kepler/turbo-modules/
directory are where you will implement the interface. You will need to fill out function bodies generated by the previous step.
// kepler/turbo-modules/Pinger.cpp
double Pinger::ping() {
return 0;
}
TurboModuleRegistry
key which is read by Codegen, you will have other existing files in kepler/turbo-modules
such as TestIdlTm.h/cpp
. These can be removed in favor of the files created by Codegen.IDL
If your Turbo Module uses an OS API via an IDL, you need to add imports, instantiate the IDL components, and use the API as needed.
Example
Add the imports.
// kepler/turbo-modules/Pinger.h
// Imports for IDL framework headers (APMF)
#include <apmf/apmf_base.h>
#include <apmf/ptr.h>
#include <apmf/process.h>
...
class Pinger: public com::amazon::kepler::turbomodule::VegaTurboModule {
public:
...
// Add a member variable
apmf::Ptr<apmf::iface::com::amazon::apmf::IStandard> component;
...
}
Instantiate the IDL components and use the API.
// kepler/turbo-modules/Pinger.cpp
// Imports for the IDL package(s) you want to use.
// Note that IHello.h is an example and likely doesn't exist.
#include <apmf/iface/com/amazon/kepler/foo/IHello.h>
using apmf::iface::com::amazon::kepler::foo::IHello;
...
Pinger::Pinger() {
// Initialize `component` in the constructor
auto const process = apmf::GetProcessObject();
this->component = process->getComponent("/com.amazondeveloper.kepler.foo");
}
...
// You can use `component` to access APIs.
// Note that below invocation is not guaranteed to be available and is only meant as an example.
auto const hello = this->component.TryQueryInterface<IHello>();
return hello->getHelloString();
...
com.amazondeveloper.kepler.foo
should match the --idlPackage
you passed to the template, if any.
For a more complete example, see Internationalization Developer Guide.
Step 5: Implement JavaScript layer
The TypeScript interface you create provides type information that allows the native object to be called from JavaScript. You might want an additional layer of JavaScript logic over the native methods for operations such as error handling, JavaScript logging, or argument preprocessing for optional parameters or any unsupported types. Even if you don't need such logic right now, you can allow for more flexibility to address future API changes if you add a basic wrapper to separate the JavaScript and native APIs.
import PingerModule from './NativePinger';
class Pinger {
// This just forwards to the native module, but you can do more complex operations,
// or even provide JS-only utility functions which don't need the native module.
ping(): number {
return PingerModule.ping();
}
}
export default new Pinger();
You would export an instance of the wrapper for users of the Turbo Module, providing a layer between users and the native object. For example, a standalone Turbo Module package might have export {default as Pinger} from './turbo-modules/Pinger'
in its index.ts
.
Adding JavaScript tests
You can text your JavaScript code with a framework such as Jest. The C++ Turbo Module instance will not exist when running tests with Jest, so you need to mock it for your JavaScript tests.
The following example show how you can mcok your C++ Turbo Module instance.
// In a test file,
jest.mock('<path-to-NativePinger.ts>');
You can add a manual mock for your Turbo Module under a __mocks__
subdirectory adjacent to the module, such as src/turbo-modules/__mocks__/NativePinger.ts
.
Mocks can be provided to your Turbo Module's consumers to facilitate their own testing. For example, you can provide a mocks file which they can pass to Jest's setupFiles
.
Step 6: Add build configurations
If you have generated your Turbo Module from a template, this step should already be completed. However, you might still find the information useful if you made adjustments to the code provided by the template or specified additional codegen parameters.
Autolinking
Vega Turbo Modules are autolinked, meaning the native module is not directly linked by applications, but instead loaded by React Native for Vega according to the app's dependencies and package configuration. As a Turbo Module developer, you will need to add some Turbo Module registration and configuration code.
Step 1: Register the module in AutoLinkInit.cpp
In kepler/AutoLinkInit.cpp
, you should have the following code, adjusted for your Turbo Module.
#include <Vega/turbomodule/VegaTurboModuleRegistration.h>
// Header files for your module implementations
#include "turbo-modules/Pinger.h"
extern "C" {
__attribute__((visibility("default"))) void
autoLinkVegaTurboModulesV1() noexcept {
KEPLER_REGISTER_TURBO_MODULE(
PingerTurboModule, /* namespace */
Pinger /* classname */
);
// If your package has multiple Turbo Modules, continue registering them here.
}
}
The namespace and classname must match the arguments passed to codegen. If you didn't pass a classname, codegen uses the string from TurboModuleRegistry.getEnforcing
, and you can use KEPLER_REGISTER_TURBO_MODULE(namespace, classname)
as above. If you passed a classname which is different from the string used by TurboModuleRegistry
, you can use the KEPLER_REGISTER_TURBO_MODULE_WITH_NAME(TurboModuleRegistry namestring, namespace, classname)
macro.
KEPLER_REGISTER_TURBO_MODULE(
"myName", /* TurboModuleRegistry string */
PingerTurboModule, /* namespace */
PingerModule /* classname */
);
step 2: Add metadata in react-native.config.js
All Turbo Module types specify their Autolinking configuration in react-native.config.js.
For standalone Turbo Module Libraries.
module.exports = {
dependency: {
platforms: {
kepler: {
autolink: {
"com.companyname.pinger": { // This name is only used for autolinking metadata.
// We recommend reverse-DNS to avoid naming conflicts.
"libraryName": "libPinger.so", // This comes from CMakeLists library name
"provider": "application",
"linkDynamic": true,
"turbomodules": ["Pinger"] // This comes from TurboModuleRegistry.
// Expand this list if you have multiple Turbo Modules.
}
},
},
},
},
};
For an in-app Turbo Module.
{
...
"kepler": {
"keplerInitializationSharedLibrary": "com.companyname.pinger",
"autolink": {
"com.companyname.pinger": {
"libraryName": "libPinger.so",
"provider": "application",
"linkDynamic": true,
"turbomodules": ["Pinger"]
}
}
...
}
}
Dynamic linking
linkDynamic
controls whether the autolink library is loaded at app launch (increasing launch latency), or loaded when the Turbo Module is requested by JavaScript. Regardless of the setting in your library, an application can override the value in their own react-native.config.js
to what works better for their needs. If you're not sure, set the default to true
.
CMakeLists.txt
The CMakeLists.txt
defines the build process for your project and contains commands and instructions that CMake uses to generate "makefiles", or project files, for various compilers and IDEs. You should have a CMakeLists.txt
in your project root similar to the following.
cmake_minimum_required(VERSION 3.19)
set (CMAKE_POSITION_INDEPENDENT_CODE ON)
project(pinger-module # This is the library name
VERSION 1.0
LANGUAGES CXX C)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Add any additional header files here
set(HEADERS
kepler/turbo-modules/generated/PingerSpec.h
kepler/turbo-modules/Pinger.h
)
# Add any additional source files here
set(SOURCES
kepler/turbo-modules/generated/PingerSpec.cpp
kepler/turbo-modules/Pinger.cpp
kepler/AutoLinkInit.cpp
)
find_package(turbomoduleAPI CONFIG REQUIRED)
kepler_add_turbo_module_library(pinger-module ${HEADERS} ${SOURCES})
# For IDL
# find_package(Apmf REQUIRED)
# find_package(Vega REQUIRED)
#
# kepler_add_idl_import_library(
# idl_pkg
# PACKAGES
# <idlPackage> # Such as com.amazon.kepler.i18n.text.message_format_classic
# # Add additional IDL packages here
# )
#
# )
# target_link_libraries(pinger-module PRIVATE idl_pkg)
install(TARGETS pinger-module)
This example uses a single CMakeLists.txt file for all of the project's sources, but you can choose to split it per your project needs.
Build targeting
For in-app Turbo Modules, build targeting should be done following the app setup instructions.
Build targeting can be specified via package.json
, or via CLI build flags.
Step 7: Build the Turbo Module
npm install
npm pack
You can tweak with the package.json
scripts to run commands commonly used in your team, or piecemeal run existing scripts.
Step 8: Use your new Turbo Module in an app
For an in-app Turbo Module, you can import the module with a relative path and call it as you would any standard JS module.
import Pinger from ‘<path-to-Pinger.ts>’;
Pinger.ping();
For a standalone Turbo Module library, you need to first install the library package to your app. You can either install the .tgz file generated by npm pack, or use a setup like yarn workspaces or symbolic linking to consume the Turbo Module library without publishing it to a registry.
npm install --save <path-to-pinger.tgz / path-to-Pinger-package / ‘@amzn/pinger’>
Then, import and use it as you would from any standard JS package.
// If your Turbo Module is the default export
import Pinger from ‘pinger’;
// If your Turbo Module is a named export
import { Pinger } from ‘pinger’;
Pinger.ping()
You’ll need to mock the native Turbo Module for your app tests to pass with Jest.
jest.mock(‘@amzn/pinger/dist/turbo-modules/NativePinger’);
Rebuilding
If you install a standalone Turbo Module to an app using the .tgz file generated by npm pack, and later make changes to the Turbo Module, you need to reinstall the new .tgz after rebuilding the Turbo Module.
This process is generally:
- Make Turbo Module code changes
- Rebuild the Turbo Module (npm pack)
- Run
npm install <path-to-.tgz> from the app package
- Rebuild the app
If the app is using the standalone Turbo Module via symbolic linking (meaning the dependency was added via npm install <path-to-TM-package>, or the app's package.json shows a directory path instead of a path to the .tgz), or uses a setup like yarn workspaces, then you don’t need the re-install step to propagate changes from Turbo Module to app.
Related topics
Last updated: Sep 30, 2025