Skip to main content

turbo-modules

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".

来源信息

仓库
software-mansion/react-native-audio-api
最近来源活动
2026年8月25日 09:43
检测到的 SKILL.md 语言
英语
星标
848
分支
94

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

文件资源管理器
2 个文件

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
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 查看