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

Informations de source

Dépôt
software-mansion/react-native-audio-api
Dernière activité de la source
28 septembre 2026 à 10:56
Langue détectée de SKILL.md
anglais
Étoiles
848
Forks
94

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Explorateur de fichiers
3 fichiers

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
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_);
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub