Skip to main content

add-native-extension

Use when adding a C++ extension, writing native JSI host functions, registering functions in C++ install maps, or compiling native code.

소스 정보

저장소
software-mansion/react-native-executorch
최근 소스 활동
2026년 9월 5일 17:39
감지된 SKILL.md 언어
영어
스타
1,758
포크
97

설치 방법

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

소스 파일 검토

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

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
add-native-extension
description
Use when adding a C++ extension, writing native JSI host functions, registering functions in C++ install maps, or compiling native code.
metadata
{"id":"add_native_extension","scope":"cpp/extensions/*, src/extensions/*"}
# Skill: Add a Native C++ Extension & JSI Bindings Use this guide to add custom, performance-critical native operations in C++ and expose them to TypeScript via React Native JSI. --- ## 🚦 Architectural Guidelines Before writing any C++ code, ensure you adhere to the following principles: 1. **Amdahl's Law & Premature Optimization**: - Evaluate what percentage of total inference/pipeline time the processing step occupies. If the preprocessing/postprocessing step takes `< 5%` of the total inference budget, write it in **pure TypeScript** to reduce codebase complexity and maintenance overhead. 2. **Destination Tensors & Local Memory**: - **Local Memory is Allowed**: You can allocate temporary native C++ memory (such as stack variables, `std::vector`s, or dynamic memory cleaned up before the function exits) for intermediate calculations. - **Destination Tensors**: If the operation writes dense output, the destination tensor must be pre-allocated by the caller (in TypeScript) and passed as an argument (e.g., `sigmoid(src, dst)`). - **Primitive Array Returns**: If the operation produces variable-sized non-dense outputs (like bounding box indices in Non-Maximum Suppression (NMS)), return a plain `jsi::Array` of primitives (like indices or coordinates). - _Example_: `nms(boxes, scores, options)` returns a `jsi::Array` of indices (e.g., `[0, 4, 12]`) rather than a new tensor. This avoids all native memory management overhead for variable-sized outputs. ## 🚫 Avoid / Anti-Patterns - **Do NOT return implicitly allocated JSI Tensors:** Never return newly created `TensorHostObject` instances from C++. This forces the JavaScript layer to reason about their garbage collection and manual lifetimes, leading to native memory leaks. - **Do NOT define default parameters in C++:** Native C++ functions must never define default argument values (e.g. `axis = -1`). Define all default values explicitly in the TypeScript wrapper layer instead. - **Do NOT perform in-place mutation without safety checks:** Never allow inputs and outputs to share the same underlying instance. - **Do NOT throw raw `jsi::JSError`, `std::runtime_error`, or `std::invalid_argument`:** They reach JavaScript with no error code. Throw `error::X(message)` and register through `error::guarded(...)`. See the [Error Handling Skill](../error-handling/SKILL.md). --- --- ## 🛠️ Step-by-Step Implementation ### Step 1: Create the Native Operation Files Under `cpp/extensions/<domain>/`, create or modify the header and implementation files for your operations: #### 1. Header (`cpp/extensions/<domain>/operations.h`) Keep the header clean and specify exact JSI install functions: ```cpp #pragma once #include <jsi/jsi.h> namespace rnexecutorch::extensions::<domain> { void install_customOp(facebook::jsi::Runtime &rt, facebook::jsi::Object &module); } ``` #### 2. Source (`cpp/extensions/<domain>/operations.cpp`) - Validate, type-check, and extract input/output tensors using `tensor::fromJs`, specifying expected `DType` and shape constraints where possible. - Convert primitive parameters using `conversions::asType<T>`. - Prevent in-place mutations (aliasing) using `tensor::checkNotSameTensor`. - Lock tensors for thread-safe access using `tensor::tryLockShared` (for inputs) and `tensor::tryLockUnique` (for outputs), which also ensure the underlying memory buffer has not been disposed. - Raise every failure through a code factory, `error::X(message)`, and wrap the registration in `error::guarded(...)`, so the error reaches JavaScript with a `code`. See the [Error Handling Skill](../error-handling/SKILL.md). ```cpp #include "operations.h" #include "core/conversions.h" #include "core/error.h" #include "core/tensor_helpers.h" #include <algorithm> namespace rnexecutorch::extensions::<domain> { namespace jsi = facebook::jsi; namespace conversions = rnexecutorch::core::conversions; namespace tensor = rnexecutorch::core::tensor; // Required under extensions::*; OMIT inside rnexecutorch::core::*, where // unqualified `error` already resolves to the sibling namespace. namespace error = rnexecutorch::core::error; using rnexecutorch::core::types::DType; void install_customOp(jsi::Runtime &rt, jsi::Object &module) { auto name = "customOp"; auto fnBody = [](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) -> jsi::Value { // 1. Strict argument count validation (No default values here!) if (count != 3) { throw error::InvalidArgument("customOp: Usage: customOp(src, dst, factor)"); } // 2. Validate, extract input/output tensors and check DType/Shape constraints using fromJs auto src = tensor::fromJs(rt, "customOp: src", args[0], DType::float32, std::nullopt); auto dst = tensor::fromJs(rt, "customOp: dst", args[1], DType::float32, src->shape_); // Extract and convert arguments using conversions::asType double factor = conversions::asType<double>(rt, "customOp: factor", args[2]); // 3. Prevent in-place mutations (aliasing) tensor::checkNotSameTensor(rt, "customOp: src", src, "customOp: dst", dst); // 4. Lock underlying buffers and verify they aren't disposed auto srcLock = tensor::tryLockShared(rt, "customOp: src", src); auto dstLock = tensor::tryLockUnique(rt, "customOp: dst", dst); // 5. Perform the computation const float *srcData = reinterpret_cast<const float *>(src->data_.get()); float *dstData = reinterpret_cast<float *>(dst->data_.get()); size_t size = src->numel_; for (size_t i = 0; i < size; ++i) { dstData[i] = srcData[i] * static_cast<float>(factor); } // Always return the destination tensor (args[1]) as the JSI result return jsi::Value(rt, args[1]); }; // error::guarded turns any RnExecuTorchException raised in the native stack into a // JS Error carrying `code`. Never register a bare fnBody. module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 3, error::guarded(fnBody))); } } ``` --- ### Step 2: Register in Extension and Core JSI Installs 1. **Extension Register** (`cpp/extensions/<domain>/install.cpp`): ```cpp #include "install.h" #include "operations.h" namespace rnexecutorch::extensions::<domain> { void install(facebook::jsi::Runtime &rt, facebook::jsi::Object &module) { facebook::jsi::Object subModule(rt); install_customOp(rt, subModule); module.setProperty(rt, "<domain>", subModule); } } ``` 2. **Core Register** ([cpp/RnExecutorch.cpp](../../../packages/react-native-executorch/cpp/RnExecutorch.cpp)): ```cpp #include "extensions/<domain>/install.h" // ... inside rnexecutorch::install ... rnexecutorch::extensions::<domain>::install(jsiRuntime, module); ``` --- ### Step 3: TypeScript Bridge & Wrappers **Where the wrapper file goes — general vs. model-specific ops:** - **Domain-general ops** (reusable across every task in the domain, e.g. `image_ops`, `box_ops`) go in the domain's shared op files (`src/extensions/<domain>/ops.ts`), re-exported from `src/extensions/<domain>/index.ts`. - **Model- or task-specific ops** (only meaningful to one model/pipeline, e.g. FSMN-VAD framing) **must** go under `src/extensions/<domain>/utils/<name>.ts` — one file per model/task, named for it (e.g. `vadUtils.ts`, `supertonicUtils.ts`). Keep them out of the shared `ops.ts` so it stays a home for genuinely reusable ops, and group each model's helpers together under `utils/`. Re-export them from the domain `index.ts` too so power users can reach them. Then, in that wrapper file: - **Use the `rnexecutorchJsi` Symbol**: You must import and interact with native bindings using the `rnexecutorchJsi` symbol exported from [src/native/bridge.ts](../../../packages/react-native-executorch/src/native/bridge.ts). **Do not** reference the global `__rnexecutorch_jsi__` directly throughout your wrapper files. - Expose the TypeScript wrapper. - Handle default values here instead of the C++ layer. - Mark wrapper functions with the `"worklet";` directive. ```typescript import { rnexecutorchJsi } from '../native/bridge'; import { type Tensor } from '../core/tensor'; /** * Applies a custom operation scaling the src tensor by factor. * @param src Input Tensor. * @param dst Pre-allocated Destination Tensor. * @param factor Scale factor. Defaults to 1.0. */ export function customOp(src: Tensor, dst: Tensor, factor: number = 1.0): Tensor { 'worklet'; return rnexecutorchJsi.<domain>.customOp(src, dst, factor); } ``` --- ## 📋 Verification Checklist When adding a native extension, verify that: - [ ] You only implemented in C++ if the operation takes `> 5%` of the total inference budget. - [ ] No JSI Tensors are implicitly allocated and returned in the C++ code. - [ ] Input and output tensors are extracted and validated using `tensor::fromJs` with strict expected `DType` and shape constraints where possible. - [ ] Primitive arguments are extracted and converted using `conversions::asType<T>`. - [ ] In-place mutation is explicitly prevented using `tensor::checkNotSameTensor`. - [ ] Input and output tensors are locked using `tensor::tryLockShared` and `tensor::tryLockUnique` respectively. - [ ] No default parameter values are defined in the C++ header/source files. - [ ] Every failure is raised through a factory, `error::X(message)`; no raw `jsi::JSError` / `std::runtime_error` / `std::invalid_argument` was introduced. - [ ] The host function is registered through `error::guarded(fnBody)`, not a bare `fnBody`. - [ ] `#include "core/error.h"` and the `namespace error = ...` alias sit at file scope, outside any `#if defined(__ANDROID__)` / `#elif defined(__APPLE__)` branch. - [ ] The custom operation install function is registered in both the domain `install` function and core [cpp/RnExecutorch.cpp](../../../packages/react-native-executorch/cpp/RnExecutorch.cpp). - [ ] The TypeScript wrapper lives in the right place: domain-general ops in the shared `ops.ts`, model-/task-specific ops under `src/extensions/<domain>/utils/<name>.ts`. - [ ] The TypeScript wrapper imports and uses `rnexecutorchJsi` instead of the global `__rnexecutorch_jsi__`. - [ ] The TypeScript wrapper is marked with the `"worklet";` directive and defines all default parameter values.
GitHub에서 보기