- name
- turbo-modules
- description
- TurboModule spec, JSI binding injection, and platform native module entry points. Use when adding a new native method to NativeAudioAPIModule.ts, wiring a new top-level HostObject through AudioAPIModuleInstaller, debugging "native module not found" or "undefined is not a function" for a global, or understanding the install() bootstrap flow on iOS and Android. Trigger phrases: "native module not found", "install()", "NativeAudioAPIModule", "injectJSIBindings", "AudioAPIModuleInstaller", "JSI global", "TurboModule spec", "module bootstrap", "undefined is not a function".
# Skill: TurboModules & JSI Installation
## When to Use TurboModule vs HostObjects
**Rule**: if it needs `AVAudioSession`, `AudioManager`, `MediaSession`, or OS permissions → TurboModule. If it's audio processing → HostObject installed by `AudioAPIModuleInstaller`.
| Use TurboModule method | Use HostObject property/method |
|---|---|
| Platform system APIs (audio session, permissions, notifications, device routing) | All audio graph operations (create/connect/disconnect nodes, AudioParam, scheduling) |
| One-time initialization (`install`) | Real-time playback control |
| Features that need native UI thread or system callbacks | Features that only need the JSI runtime |
| Android-only or iOS-only behavior | Cross-platform C++ engine behavior |
Most audio nodes do **not** need a TurboModule method — they are exposed entirely through JSI HostObjects injected by `injectJSIBindings`.
---
## Reference
- RN TurboModules (new arch): https://reactnative.dev/docs/the-new-architecture/pure-cxx-modules
- RN JSI: https://reactnative.dev/docs/the-new-architecture/architecture-glossary#javascript-interface-jsi
- fbjni (Android JNI bridge): https://github.com/facebookincubator/fbjni
---
## The Big Picture
Most audio functionality is exposed **not** through TurboModule method calls but through **JSI HostObjects** placed directly on the JS global object. The TurboModule's job is narrow:
1. Be available early via the standard RN module system
2. Provide `install()` — one synchronous call that injects HostObject factories onto `globalThis`
3. Expose platform-specific system APIs (audio session, permissions, notifications, devices) that cannot be done through pure JSI
```mermaid
flowchart TD
A["JS: NativeAudioAPIModule.install()"]
subgraph ios["iOS — AudioAPIModule.mm"]
B["init AudioSessionManager · AudioEngine\nget jsi::Runtime* · CallInvoker"]
end
subgraph android["Android — AudioAPIModule.kt + .cpp"]
C["MediaSessionManager.initialize()\nJNI: injectJSIBindings()"]
end
D["AudioAPIModuleInstaller::injectJSIBindings\n(runtime, callInvoker, eventRegistry, uiRuntime?)"]
subgraph globals["globalThis — set by injectJSIBindings"]
G["createAudioContext\ncreateOfflineAudioContext\ncreateAudioRecorder\ncreateAudioBuffer\ncreateAudioDecoder\ncreateAudioFileUtils\nAudioEventEmitter HostObject"]
end
A --> ios
A --> android
ios --> D
android --> D
D --> globals
```
After `install()` returns, TypeScript code can call `globalThis.createAudioContext(sampleRate)` and get back a HostObject — no serialization, no bridge round-trip.
---
## TurboModule Spec (`src/specs/NativeAudioAPIModule.ts`)
Defines the TypeScript interface for the native module. Codegen (React Native's codegen) generates the native binding glue from this file.
```ts
interface Spec extends TurboModule {
install(): boolean; // synchronous — MUST run first
getDevicePreferredSampleRate(): number; // synchronous
setAudioSessionActivity(enabled: boolean): Promise<boolean>;
setAudioSessionOptions(...): void;
observeAudioInterruptions(focusType, enabled): void;
requestRecordingPermissions(): Promise<PermissionStatus>;
// ... audio devices, notifications ...
}
const NativeAudioAPIModule = TurboModuleRegistry.get<Spec>('AudioAPIModule')!;
```
**`install()` is synchronous** (`RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD` on iOS, `override fun install(): Boolean` on Android). It must complete before the app tries to use any audio API. It is called once during module bootstrap in `src/AudioAPIModule/index.ts`.
**Web mock** (`src/specs/NativeAudioAPIModule.web.ts`): on the web platform, the whole spec is mocked with no-ops and resolved promises (e.g. permissions always return `'Granted'`). This file replaces the native spec at bundle time for web builds.
---
## `AudioAPIModuleInstaller` (`common/cpp/audioapi/AudioAPIModuleInstaller.h`)
The single C++ class that owns JSI injection. It is `#include`-d by both iOS (`.mm`) and Android (`.cpp`) native modules, keeping the injection logic platform-agnostic.
```cpp
class AudioAPIModuleInstaller {
public:
static void injectJSIBindings(
jsi::Runtime *jsiRuntime,
const std::shared_ptr<react::CallInvoker> &jsCallInvoker,
const std::shared_ptr<AudioEventHandlerRegistry> &audioEventHandlerRegistry,
std::shared_ptr<worklets::WorkletRuntime> uiRuntime = nullptr);
};
```
Each `get*Function()` private method creates a `jsi::Function` via `jsi::Function::createFromHostFunction()`. The lambda captures the dependencies it needs (`jsCallInvoker`, `audioEventHandlerRegistry`, `uiRuntime`) by value (shared_ptr).
**Worklets guard**: functions that need the worklets runtime (AudioContext, OfflineAudioContext) have an `#if RN_AUDIO_API_ENABLE_WORKLETS` branch:
```cpp
#if RN_AUDIO_API_ENABLE_WORKLETS
auto runtimeRegistry = RuntimeRegistry{.uiRuntime = uiRuntime};
if (count > 1 && args[1].isObject()) {
runtimeRegistry.audioRuntime = worklets::extractWorkletRuntime(runtime, args[1]);
}
#else
auto runtimeRegistry = RuntimeRegistry{};
#endif
```
**Adding a new top-level global**: add a `static jsi::Function getCreateXxxFunction(...)` private method and a `setProperty("createXxx", ...)` call in `injectJSIBindings`. This is only needed for objects that JS creates directly (not objects created as properties of another HostObject).
**Construction-time options** (e.g. `new AudioRecorder({ androidInputPreset, iosVoiceProcessing })`) travel as positional args of these factory functions — there is no options object and no TurboModule method involved. Three rules:
- Parse defensively — `if (count > N && args[N].isBool())` — and keep the no-arg behavior as the default, so an older JS bundle against a newer binary still works.
- Update `src/AudioAPIModule/globals.d.ts` in the same change; it is the only type contract for these globals.
- Platform-specific options are passed to *both* platforms and consumed by the `#ifdef ANDROID` branch in the HostObject constructor; the other platform ignores its counterpart. Name them with the platform prefix (`androidInputPreset`, `iosVoiceProcessing`) so the asymmetry is visible from JS.
---
## iOS Native Module (`AudioAPIModule.mm`)
Uses ObjC++ with `RCT_EXPORT_MODULE` and `RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD`.
**Key steps in `install`**:
1. Allocate platform managers (`AudioSessionManager`, `AudioEngine`, `SystemNotificationManager`, `NotificationRegistry`)
2. Get `jsi::Runtime *` from the bridge: `reinterpret_cast<facebook::jsi::Runtime *>(self.bridge.runtime)`
3. Get `CallInvoker` — different paths for old Bridge vs New Architecture:
```objc
#if defined(RCT_NEW_ARCH_ENABLED)
auto jsCallInvoker = _callInvoker.callInvoker;
#else
auto jsCallInvoker = self.bridge.jsCallInvoker;
#endif
```
4. Create `AudioEventHandlerRegistry` (owns JS callbacks, needs runtime + callInvoker)
5. Call `AudioAPIModuleInstaller::injectJSIBindings(...)`
**Lazy `AVAudioEngine` invariant**: allocating the `AudioEngine` ObjC object at install does NOT create the underlying `AVAudioEngine`. It is created on demand by `createAudioEngineIfNeeded` (first node attach, engine start, rebuild), so apps that only use session management and notifications never allocate it. Every reader of `self.audioEngine` inside `AudioEngine.mm` must either nil-guard or call `createAudioEngineIfNeeded` first, and system-driven restart paths (`restartAudioEngine`) must not resurrect an engine when there is no tracked graph.
**New Architecture support** (`getTurboModule`):
```objc
#ifdef RCT_NEW_ARCH_ENABLED
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeAudioAPIModuleSpecJSI>(params);
}
#endif
```
This connects the ObjC implementation to the codegen-generated C++ TurboModule spec.
**`methodQueue`**: the module runs on a dedicated serial queue (`com.swmansion.audioapi.MainModuleQueue`), not the main thread.
**`invokeHandlerWithEventName:eventBody:`**: called from Objective-C system callbacks (audio session interruption, volume change, etc.) to fire events back into JS. It converts `NSDictionary` to `std::unordered_map<std::string, EventValue>` and calls `audioEventHandlerRegistry_->invokeHandlerWithEventBody(...)`.
---
## Android Native Module (`AudioAPIModule.kt` + `AudioAPIModule.cpp`)
Android uses **fbjni HybridObject** — a pattern where a Kotlin class holds a C++ peer via `HybridData`. The Kotlin class handles the Java/Kotlin TurboModule protocol; the C++ peer owns the JSI runtime pointer and call invoker.
### Initialization flow
```
AudioAPIModule.kt (init block)
├── System.loadLibrary("react-native-audio-api") // load .so
├── get CallInvokerHolderImpl from reactContext
├── get WorkletsModule if worklets enabled
└── mHybridData = initHybrid(workletsModule, jsContext, callInvokerHolder)
│
▼
AudioAPIModule.cpp::initHybrid (JNI)
├── unwrap jsCallInvoker from holder
├── reinterpret_cast<jsi::Runtime *>(jsContext)
├── [if worklets] get WorkletsModuleProxy
└── makeCxxInstance(...) → creates C++ AudioAPIModule peer
└── stores: jsiRuntime_, jsCallInvoker_, audioEventHandlerRegistry_
```
```
AudioAPIModule.kt::install()
├── MediaSessionManager.initialize(...)
├── NativeFileInfo.initialize(...)
└── injectJSIBindings() // external fun → JNI call
│
▼
AudioAPIModule.cpp::injectJSIBindings()
└── AudioAPIModuleInstaller::injectJSIBindings(...)
```
**`external fun`**: Kotlin keyword for JNI-implemented methods. These are registered in `AudioAPIModule.cpp::registerNatives()`:
```cpp
void AudioAPIModule::registerNatives() {
registerHybrid({
makeNativeMethod("initHybrid", AudioAPIModule::initHybrid),
makeNativeMethod("injectJSIBindings", AudioAPIModule::injectJSIBindings),
makeNativeMethod("invokeHandlerWithEventNameAndEventBody", ...),
});
}
```
`registerNatives()` is called from `android/src/main/cpp/audioapi/android/OnLoad.cpp` at `.so` load time.
**`invokeHandlerWithEventNameAndEventBody`**: called from Kotlin (MediaSessionManager callbacks) to fire events into JS. Takes a Java `Map<String, Object>` and converts to `std::unordered_map<std::string, EventValue>`.
---
## Two-Architecture Paths Summary
| | Old Bridge | New Architecture (TurboModules/Fabric) |
|---|---|---|
| iOS CallInvoker source | `self.bridge.jsCallInvoker` | `_callInvoker.callInvoker` |
| iOS runtime source | `self.bridge.runtime` (cast) | same |
| iOS codegen | `RCT_EXPORT_MODULE` only | `+ getTurboModule:` implemented |
| Android CallInvoker | `reactContext.jsCallInvokerHolder` | same |
| Android codegen | `NativeAudioAPIModuleSpec` base class | same (generated) |
The JSI injection itself (`AudioAPIModuleInstaller`) is identical for both architectures — it only needs the `jsi::Runtime *` and `CallInvoker`, which are obtained differently but behave the same.
---
## Pure C++ TurboModule library (`react-native-audio-worklets`)
Extension packages that expose a **C++-only** TurboModule (no JNI implementation) use a hybrid Android setup:
| Piece | Role |
|---|---|
| `react-native.config.js` `cxxModule*` fields | Registers `NativeAudioWorkletsModule` in app autolinking (`autolinking_cxxModuleProvider`) |
| `android/build.gradle` + `com.facebook.react` | Runs Codegen → `react_codegen_rnaudioworklets` (JSI spec headers) |
| `AudioWorkletsPackage.kt` | Stub `BaseReactPackage` so Gradle autolinking includes the library project |
| Root `CMakeLists.txt` | Static `react-native-audio-worklets` target; linked into `libappmodules.so` via autolinking |
| `common/cpp/NativeAudioWorkletsModule.h` | Forwarding header — autolinking includes `<NativeAudioWorkletsModule.h>` |
iOS uses `NativeAudioWorkletsModuleProvider.mm` + `codegenConfig.ios.modulesProvider` instead.
RN guide: https://reactnative.dev/docs/the-new-architecture/pure-cxx-modules
---
*Maintenance: see [maintenance.md](maintenance.md).*
在 GitHub 查看