Skip to main content

host-objects

Covers how to create, structure, and maintain JSI HostObjects that bridge C++ audio nodes to JavaScript in react-native-audio-api. Explains naming conventions, property/method exposure via macros, shadow state for JS↔audio-thread communication, JSI argument parsing, return value patterns, memory pressure, factory wiring in BaseAudioContextHostObject, and common pitfalls. Use this skill when creating a new audio node HostObject, modifying existing HostObject get/set/call logic, wiring a new node into the context factory, or debugging JSI-related crashes and type errors. Trigger phrases: "add HostObject", "create JSI bridge", "expose C++ node to JS", "shadow state", "scheduleAudioEvent from setter", "JSI property getter", "HostObject crashes", "new audio node".

소스 정보

저장소
software-mansion/react-native-audio-api
최근 소스 활동
2026년 9월 28일 10:56
감지된 SKILL.md 언어
영어
스타
848
포크
94

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

파일 탐색기
3 개 파일

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
host-objects
description
Covers how to create, structure, and maintain JSI HostObjects that bridge C++ audio nodes to JavaScript in react-native-audio-api. Explains naming conventions, property/method exposure via macros, shadow state for JS↔audio-thread communication, JSI argument parsing, return value patterns, memory pressure, factory wiring in BaseAudioContextHostObject, and common pitfalls. Use this skill when creating a new audio node HostObject, modifying existing HostObject get/set/call logic, wiring a new node into the context factory, or debugging JSI-related crashes and type errors. Trigger phrases: "add HostObject", "create JSI bridge", "expose C++ node to JS", "shadow state", "scheduleAudioEvent from setter", "JSI property getter", "HostObject crashes", "new audio node".
# Skill: HostObjects Scope: C++ JSI HostObject layer — `packages/react-native-audio-api/common/cpp/audioapi/HostObjects/` HostObjects are the middle layer between the TypeScript API and the C++ audio engine. They expose C++ audio node state and methods to JavaScript via JSI (no bridge serialization), and route state changes to the audio thread via a lock-free SPSC event queue. Golden references: `GainNodeHostObject.h/.cpp` (effect node), `OscillatorNodeHostObject.h/.cpp` (source node). Mirror their structure for any new HostObject. See [full examples](examples.md) for annotated implementations. --- ## Critical Pitfalls — Read Before Writing Any Code - **NEVER read from `node_` in a getter** if the property can be written by the audio thread. Use shadow state or atomics instead. - **NEVER call `node_->someMethod()` directly from a setter** — always schedule via `scheduleAudioEvent`. The audio thread may be mid-render. - **ALWAYS register getters/setters/functions in the constructor.** Anything not added to `addGetters`/`addSetters`/`addFunctions` is silently missing from JS. - **Match property names exactly.** The string in `JSI_EXPORT_PROPERTY_GETTER` becomes the JS property name. A typo means the property doesn't exist in JS. - **Clear callback IDs in the destructor** for any HO that registers audio events. Otherwise the audio thread fires into a destroyed JS function. - **Call `setExternalMemoryPressure`** when returning HOs or typed arrays backed by large native buffers. - **Use `jsiutils::throwException()`** from `audioapi/jsi/JsiUtils.h` when throwing a named JS error (for example, `InvalidAccessError`) instead of rebuilding the `Error` object in a HostObject. - **Shadow state must be initialized** from `options` in the constructor — JS may read a property before ever setting it. - **Typed node pointers are `T *const`.** Mirror `AudioNode *const audioNode_` — declare as `GainNode *const gainNode_;` (const pointer, mutable object), initialize in the ctor initializer list via `typedAudioNode<T>(node_)`. Never use `T *foo_ = nullptr`. --- ## Three-Layer Architecture Every audio node has three layers. HostObject is the middle one: ```mermaid flowchart TD TS["TypeScript class\n(src/core/)"] HO["HostObject\n(C++ — audioapi::HostObject subclass)"] AN["AudioNode\n(C++ core — audio thread)"] TS <--> |"JSI — direct memory, no serialization"| HO HO <--> |"shared_ptr\nscheduleAudioEvent → SPSC"| AN ``` There is **no strong typing** between the C++ HostObject and the TypeScript interface. Alignment is by convention — property names and function signatures must match manually. --- ## Directory Structure ``` HostObjects/ ├── AudioNodeHostObject.h/.cpp # Base for all audio node HOs ├── AudioParamHostObject.h/.cpp # AudioParam wrapper ├── BaseAudioContextHostObject.h/.cpp # Factory — all createXxx() methods live here ├── AudioContextHostObject.h/.cpp # Realtime context (adds close/resume/suspend) ├── OfflineAudioContextHostObject.h/.cpp ├── analysis/ │ └── AnalyserNodeHostObject.h/.cpp ├── destinations/ │ └── AudioDestinationNodeHostObject.h ├── effects/ │ ├── GainNodeHostObject.h/.cpp │ ├── BiquadFilterNodeHostObject.h/.cpp │ ├── DelayNodeHostObject.h/.cpp │ ├── IIRFilterNodeHostObject.h/.cpp │ ├── StereoPannerNodeHostObject.h/.cpp │ ├── PannerNodeHostObject.h/.cpp │ ├── WaveShaperNodeHostObject.h/.cpp │ ├── ConvolverNodeHostObject.h/.cpp │ ├── ChannelMergerNodeHostObject.h/.cpp # Composite: routes inputs to slot hosts │ ├── ChannelSplitterNodeHostObject.h/.cpp # Composite: routes outputs from slot hosts │ ├── WorkletNodeHostObject.h/.cpp │ └── WorkletProcessingNodeHostObject.h/.cpp ├── sources/ │ ├── AudioScheduledSourceNodeHostObject.h/.cpp # Base for timed sources │ ├── AudioBufferBaseSourceNodeHostObject.h/.cpp # Base for buffer sources │ ├── OscillatorNodeHostObject.h/.cpp │ ├── AudioBufferSourceNodeHostObject.h/.cpp │ ├── AudioBufferQueueSourceNodeHostObject.h/.cpp │ ├── ConstantSourceNodeHostObject.h/.cpp │ ├── AudioBufferHostObject.h/.cpp # Data container, not a node │ └── RecorderAdapterNodeHostObject.h/.cpp ├── inputs/ │ └── AudioRecorderHostObject.h/.cpp ├── events/ │ └── AudioEventHandlerRegistryHostObject.h/.cpp └── utils/ ├── JsEnumParser.h/.cpp # Enum ↔ string conversions ├── NodeOptionsParser.h # Parses JS option objects into C++ structs │ # Use setNumericOption (NumericOption concept) for scalar numbers ├── AudioDecoderHostObject.h/.cpp └── AudioFileUtilsHostObject.h/.cpp ``` --- ## Macro System All HostObjects use macros defined in `jsi/HostObject.h`. Always use these — never write raw JSI dispatch code. ### Declaration macros (in .h) ```cpp JSI_PROPERTY_GETTER_DECL(gain) // jsi::Value gain(jsi::Runtime &runtime) JSI_PROPERTY_SETTER_DECL(gain) // void gain(jsi::Runtime &runtime, const jsi::Value &value) JSI_HOST_FUNCTION_DECL(setValueAtTime) // jsi::Value setValueAtTime(jsi::Runtime &, const jsi::Value &, const jsi::Value *, size_t) ``` ### Implementation macros (in .cpp) ```cpp JSI_PROPERTY_GETTER_IMPL(GainNodeHostObject, gain) { ... } JSI_PROPERTY_SETTER_IMPL(GainNodeHostObject, gain) { ... } JSI_HOST_FUNCTION_IMPL(GainNodeHostObject, setValueAtTime) { ... } ``` ### Registration macros (in constructor) ```cpp addGetters( JSI_EXPORT_PROPERTY_GETTER(GainNodeHostObject, gain)); addSetters( JSI_EXPORT_PROPERTY_SETTER(GainNodeHostObject, fftSize)); addFunctions( JSI_EXPORT_FUNCTION(GainNodeHostObject, connect), JSI_EXPORT_FUNCTION(GainNodeHostObject, disconnect)); ``` **All getters, setters, and functions must be registered in the constructor.** Anything not registered is invisible to JS. --- ## Shadow State Shadow state is the core pattern for JS↔audio-thread communication (introduced in PR #942). ### The Problem The audio node's C++ state is read and written on the **audio thread**. JS runs on a different thread. Without shadow state, reading a property from JS would require either a lock (forbidden on the audio thread) or an atomic (only works for primitives). ### The Solution The HostObject maintains its own **copy** of the node's properties — the shadow state. This copy: - Is read/written **only by the JS thread** - Is always in sync with what JS last set - Is the source of truth for JS reads When JS sets a property: 1. Update the shadow copy immediately (JS thread) 2. Schedule an event on `CrossThreadEventScheduler` that will apply the change on the audio thread When JS reads a property: 1. Return the shadow copy — **do not touch the C++ node** ```cpp // Header — shadow state declared as private member class OscillatorNodeHostObject : public AudioScheduledSourceNodeHostObject { public: JSI_PROPERTY_GETTER_DECL(type); JSI_PROPERTY_SETTER_DECL(type); private: OscillatorType type_; // shadow copy of node_->type_ }; // Getter — returns shadow, never touches audio thread JSI_PROPERTY_GETTER_IMPL(OscillatorNodeHostObject, type) { return jsi::String::createFromUtf8( runtime, js_enum_parser::oscillatorTypeToString(type_)); } // Setter — updates shadow + schedules audio thread update JSI_PROPERTY_SETTER_IMPL(OscillatorNodeHostObject, type) { auto oscillatorNode = std::static_pointer_cast<OscillatorNode>(node_); auto type = js_enum_parser::oscillatorTypeFromString( value.asString(runtime).utf8(runtime)); // 1. Update shadow state (JS thread, immediate) type_ = type; // 2. Schedule audio thread update (lock-free SPSC) auto event = [oscillatorNode, type](BaseAudioContext &) { oscillatorNode->setType(type); }; oscillatorNode->scheduleAudioEvent(std::move(event)); } ``` ### When NOT to use shadow state | Scenario | Pattern | |---|---| | Primitive, only written by JS | Shadow state (standard) | | Non-primitive, only written by JS | Store in TS layer, pass to AudioNode when needed | | Primitive, can be written by audio thread | `std::atomic<T>` on the C++ node; read directly via getter | | Non-primitive, can be written by audio thread | Triple buffer pattern (see `AnalyserNode` for reference) | ### AudioParam is a special case `AudioParam::value_` is `std::atomic<float>` because it can be updated by the audio thread during automation. The HO reads it directly: ```cpp JSI_PROPERTY_GETTER_IMPL(AudioParamHostObject, value) { return {param_->getValue()}; // atomic read, no shadow needed } JSI_PROPERTY_SETTER_IMPL(AudioParamHostObject, value) { auto event = [param = param_, v = static_cast<float>(value.getNumber())](BaseAudioContext &) { param->setValue(v); }; param_->scheduleAudioEvent(std::move(event)); } ``` ### Shadow state must be initialized in the constructor Initialize shadow members from `options` in the constructor — JS may read a property before ever setting it: ```cpp OscillatorNodeHostObject::OscillatorNodeHostObject(...) { type_ = options.type; // Initialize shadow from options } ``` --- ## Argument Parsing ### Primitives ```cpp float v = static_cast<float>(args[0].getNumber()); double d = args[0].getNumber(); int i = static_cast<int>(args[0].getNumber()); bool b = args[0].getBool(); std::string s = args[0].getString(runtime).utf8(runtime); ``` ### Optional arguments — check count first ```cpp // args[2] is optional, default -1 double duration = (count > 2 && !args[2].isUndefined()) ? args[2].getNumber() : -1.0; ``` Use `jsiutils::argToString(runtime, args, count, index, defaultValue)` for optional string args. ### TypedArrays (Float32Array, Uint8Array, etc.) JS typed arrays are passed as objects with a `.buffer` property: ```cpp JSI_HOST_FUNCTION_IMPL(AnalyserNodeHostObject, getByteFrequencyData) { auto arrayBuffer = args[0] .getObject(runtime) .getPropertyAsObject(runtime, "buffer") .getArrayBuffer(runtime); auto data = arrayBuffer.data(runtime); auto length = static_cast<int>(arrayBuffer.size(runtime)); auto analyserNode = std::static_pointer_cast<AnalyserNode>(node_); analyserNode->getByteFrequencyData(data, length); return jsi::Value::undefined(); } ``` For Float32Arrays (reinterpret the bytes): ```cpp auto rawValues = reinterpret_cast<float *>(arrayBuffer.data(runtime)); auto length = static_cast<int>(arrayBuffer.size(runtime) / sizeof(float)); ``` ### HostObject arguments (node-to-node) ```cpp JSI_HOST_FUNCTION_IMPL(AudioNodeHostObject, connect) { auto obj = args[0].getObject(runtime); if (obj.isHostObject<AudioNodeHostObject>(runtime)) { auto other = obj.getHostObject<AudioNodeHostObject>(runtime); node_->connect(other->node_); } else if (obj.isHostObject<AudioParamHostObject>(runtime)) { auto param = obj.getHostObject<AudioParamHostObject>(runtime); node_->connect(param->param_); } return jsi::Value::undefined(); } ``` ### Extracting a HostObject's inner C++ object ```cpp auto periodicWave = args[0] .getObject(runtime) .getHostObject<PeriodicWaveHostObject>(runtime); oscillatorNode->setPeriodicWave(periodicWave->periodicWave_); ``` --- ## Return Value Patterns ### Primitives ```cpp return {fftSize_}; // int/float/double return {true}; // bool return jsi::String::createFromUtf8(runtime, "suspended"); // string return jsi::Value::undefined(); // void return jsi::Value::null(); // null ``` ### A HostObject ```cpp JSI_PROPERTY_GETTER_IMPL(GainNodeHostObject, gain) { auto gainNode = std::static_pointer_cast<GainNode>(node_); auto gainParam = std::make_shared<AudioParamHostObject>(gainNode->getGainParam()); return jsi::Object::createFromHostObject(runtime, gainParam); } ``` ### A plain JS object ```cpp auto result = jsi::Object(runtime); result.setProperty(runtime, "status", jsi::String::createFromUtf8(runtime, "success")); result.setProperty(runtime, "path", jsi::String::createFromUtf8(runtime, path)); return result; ``` ### A Float32Array wrapping native memory ```cpp JSI_HOST_FUNCTION_IMPL(AudioBufferHostObject, getChannelData) { auto channel = static_cast<int>(args[0].getNumber()); auto audioArrayBuffer = audioBuffer_->getSharedChannel(channel); auto arrayBuffer = jsi::ArrayBuffer(runtime, audioArrayBuffer); auto float32ArrayCtor = runtime.global() .getPropertyAsFunction(runtime, "Float32Array"); auto float32Array = float32ArrayCtor .callAsConstructor(runtime, arrayBuffer) .getObject(runtime); float32Array.setExternalMemoryPressure(runtime, audioArrayBuffer->size()); return float32Array; } ``` ### External memory pressure Call `setExternalMemoryPressure` whenever returning a HostObject or typed array that wraps a large native buffer. This lets the JS GC schedule collection correctly: ```cpp jsiObject.setExternalMemoryPressure(runtime, bufferHostObject->getSizeInBytes()); ``` --- ## Enum Parsing Use `JsEnumParser` (`utils/JsEnumParser.h`) for all enum ↔ string conversions. Never hardcode strings. ```cpp // String → enum auto type = js_enum_parser::oscillatorTypeFromString( value.asString(runtime).utf8(runtime)); // Enum → string return jsi::String::createFromUtf8( runtime, js_enum_parser::oscillatorTypeToString(type_)); ``` When adding a new enum, add both directions to `JsEnumParser`. --- ## Destructor: Clearing Callbacks When a HostObject is garbage collected, registered audio callbacks must be cleared. Otherwise the audio thread fires into a destroyed JS function. ```cpp AudioScheduledSourceNodeHostObject::~AudioScheduledSourceNodeHostObject() { auto node = std::static_pointer_cast<AudioScheduledSourceNode>(node_);
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기