diff --git a/.agents/skills/README.md b/.agents/skills/README.md index 4f9d99a0de..a1cc363ff4 100644 --- a/.agents/skills/README.md +++ b/.agents/skills/README.md @@ -8,5 +8,6 @@ This directory contains specialized skills (recipes) to guide contributors and A - [Add Native Extension](./add-native-extension/SKILL.md) — C++ operations and JSI bindings. - [Add Task Pipeline](./add-task-pipeline/SKILL.md) — TypeScript task pipelines and React hooks. - [Model Schema Validation](./model-schema-validation/SKILL.md) — Model specs, dynamic shapes, and schema validation. +- [Error Handling](./error-handling/SKILL.md) — Error codes, throwing across worklet and JSI boundaries, and catching. - [Verify and Build](./verify-and-build/SKILL.md) — TypeScript typechecking, native rebuilding, and troubleshooting. - [Skills Maintenance](./skills-maintenance/SKILL.md) — Keeping skills synchronized with core primitives. diff --git a/.agents/skills/add-native-extension/SKILL.md b/.agents/skills/add-native-extension/SKILL.md index a845107911..75011839d2 100644 --- a/.agents/skills/add-native-extension/SKILL.md +++ b/.agents/skills/add-native-extension/SKILL.md @@ -30,6 +30,7 @@ Before writing any C++ code, ensure you adhere to the following principles: - **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 `RnExecuTorchException` and register through `error::guarded(...)`. See the [Error Handling Skill](../error-handling/SKILL.md). --- @@ -61,10 +62,12 @@ namespace rnexecutorch::extensions:: - Convert primitive parameters using `conversions::asType`. - 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 as an `RnExecuTorchException` 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 @@ -73,6 +76,11 @@ namespace rnexecutorch::extensions:: 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::error::RnExecuTorchErrorCode; + using rnexecutorch::core::error::RnExecuTorchException; using rnexecutorch::core::types::DType; void install_customOp(jsi::Runtime &rt, jsi::Object &module) @@ -83,7 +91,7 @@ namespace rnexecutorch::extensions:: // 1. Strict argument count validation (No default values here!) if (count != 3) { - throw jsi::JSError(rt, "Usage: customOp(src, dst, factor)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "Usage: customOp(src, dst, factor)"); } // 2. Validate, extract input/output tensors and check DType/Shape constraints using fromJs @@ -114,7 +122,9 @@ namespace rnexecutorch::extensions:: return jsi::Value(rt, args[1]); }; - module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 3, fnBody)); + // 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))); } } ``` @@ -192,6 +202,9 @@ When adding a native extension, verify that: - [ ] 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 as `RnExecuTorchException(RnExecuTorchErrorCode::X, ...)`; 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 its `using` declarations 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//utils/.ts`. - [ ] The TypeScript wrapper imports and uses `rnexecutorchJsi` instead of the global `__rnexecutorch_jsi__`. diff --git a/.agents/skills/add-task-pipeline/SKILL.md b/.agents/skills/add-task-pipeline/SKILL.md index f8116dfa40..f935c34c91 100644 --- a/.agents/skills/add-task-pipeline/SKILL.md +++ b/.agents/skills/add-task-pipeline/SKILL.md @@ -75,6 +75,7 @@ When implementing task constructors like `create` (e.g. `createClassifier` - **Do NOT access tensors by index:** Avoid using `tensors[0]` or `tensors[1]` throughout the function body. Always destructure and name them explicitly. - **Do NOT define extra inner helper functions:** You must define **exactly two** inner functions inside the `create` constructor: the `dispose` function and the task `worklet` executor function. **Push back hard against implementing any other helper closures inside the constructor scope.** Placing other helper functions (especially those that are called from inside the worklet and use the `create` scope variables) inside `create` creates implicit dependencies and closures that capture variables, making the code extremely difficult to reason about and debug. +- **Do NOT throw bare `Error`:** Every failure needs a code. Use `RnExecuTorchError('CODE', msg)`, which works both in the `create` body and inside the worklet executor. See the [Error Handling Skill](../error-handling/SKILL.md). - **Do NOT leak raw Tensors to consumers:** The returned methods must never return raw `Tensor` objects to the API consumer. Always convert output data to standard JavaScript values/objects before returning. - **Do NOT cross thread boundaries unnecessarily:** Minimize passing heavy objects between JS and the Worklet thread to avoid serialization overhead. - **Do NOT treat the `.pte` model as an unchangeable black box:** Reshape the model's inputs and outputs during the PyTorch export phase to make the mobile client pipeline as lightweight as possible. Do not make input/output contracts so specific that they break extensibility. @@ -229,6 +230,7 @@ When adding a task pipeline or React hook, verify that: - [ ] The constructor contains exactly two inner functions (the `dispose` function and the worklet executor). - [ ] Auxiliary helpers are defined outside the constructor and marked with the `'worklet';` directive if run on the worklet runtime. - [ ] Raw `Tensor` objects are never returned to the consumer. +- [ ] Every throw uses `RnExecuTorchError('CODE', message)`. - [ ] Data configurations that genuinely vary across models (e.g. thresholds, labels) are configurable dynamically via the TypeScript task options. - [ ] Every parameter is bucketed per Principle 6: varies across variants → option; fixed by the export → `const` in the task file; per-call choice → executor argument. - [ ] No exposed option has exactly one valid value, and no two `models.ts` variants pass an identical options object. diff --git a/.agents/skills/core-guidelines/SKILL.md b/.agents/skills/core-guidelines/SKILL.md index 2510c5b475..7b29ff634e 100644 --- a/.agents/skills/core-guidelines/SKILL.md +++ b/.agents/skills/core-guidelines/SKILL.md @@ -73,6 +73,7 @@ Use the following index to locate the specific procedural guides for your task: | **Create a task pipeline or hook** | [SKILL.md](../add-task-pipeline/SKILL.md) | Guide to building end-to-end TS pipelines (e.g. object detection) and exposing them via React hooks. | | **Verify, rebuild, or troubleshoot changes** | [SKILL.md](../verify-and-build/SKILL.md) | Workflows for rebuilding TS/C++ and resolving common JSI runtime errors. | | **Validate model constraints & schemas** | [SKILL.md](../model-schema-validation/SKILL.md) | Guide on specifying model specs, dynamic shapes, and runtime constraints for model validation. | +| **Throw, catch, or classify an error** | [SKILL.md](../error-handling/SKILL.md) | The error code set, `RnExecuTorchError`, C++ `RnExecuTorchException`/`guarded`, and adding a code. | | **Maintain or refactor codebase patterns** | [SKILL.md](../skills-maintenance/SKILL.md) | Guide to keeping workspace skills in sync with codebase state to prevent documentation decay. | --- @@ -80,6 +81,7 @@ Use the following index to locate the specific procedural guides for your task: ## 💡 Key Coding Conventions - **Worklets**: Ensure all TypeScript functions directly wrapping native JSI calls start with the `"worklet";` directive so they are compatible with worklet-based libraries (e.g., React Native Reanimated). +- **Errors**: Every failure the library raises carries a string `code`. Use `RnExecuTorchError('CODE', msg)` in TypeScript (a function, not a class, so it survives worklet boundaries) and `RnExecuTorchException` + `error::guarded(...)` in C++. Never branch on message text. See the [Error Handling Skill](../error-handling/SKILL.md). - **Memory Management**: When writing native C++ code with JSI, pay close attention to JSI reference management and handle ExecuTorch lifecycle states safely. - **Keep Core Clean**: Always build on top of core primitives. Do not modify files in `cpp/core/` or `src/core/` unless you are fixing a bug in the foundational runtime. diff --git a/.agents/skills/error-handling/SKILL.md b/.agents/skills/error-handling/SKILL.md new file mode 100644 index 0000000000..32af4ec67d --- /dev/null +++ b/.agents/skills/error-handling/SKILL.md @@ -0,0 +1,177 @@ +--- +name: error-handling +description: Use when throwing, catching, or classifying errors anywhere in the library (TypeScript, worklets, C++/JSI), or when adding a new error code. +metadata: + id: error_handling + scope: src/core/error.ts, cpp/core/error.* +--- + +# Skill: Error Handling + +Every failure the library raises carries a machine-readable `code`. Consumers branch on +the code; the message is for humans and may be reworded in any release. + +**Never make control flow depend on message text.** A regex over `e.message` is the exact +failure this design exists to prevent. + +`src/core/error.ts` is the source of truth. `cpp/core/error.h` mirrors it **by hand**, the +same way the rest of the TS/JSI interface is mirrored (DType definitions, schema types, host object +interfaces). Nothing here is generated. If you add a code, add it in both files. + +--- + +## 🚦 Throwing + +One factory, usable everywhere including inside worklets: + +```typescript +import { RnExecuTorchError } from '../../../core/error'; + +throw RnExecuTorchError('INVALID_ARGUMENT', `topk must be non-negative, got ${topk}`); +``` + +`RnExecuTorchError` is a function, not a class. Worklet runtimes are separate JavaScript +runtimes and a value thrown on one does not keep its class identity or prototype chain +when it travels to another, so a class would only survive some of the paths that throw. +The factory returns a plain `Error` with `name`, `code`, and optionally +`etRuntimeErrorCode` attached, which is exactly what crosses every boundary intact. + +Write messages the way the rest of the codebase does: prefix with the function name and +include the offending values (`` `execute: Unknown method '${methodName}'` ``). The code +says what class of problem it is; the message says which values caused it. + +--- + +## 🎣 Catching + +`isRnExecuTorchError(err, code?)` narrows, and takes an optional code so you rarely need a +second comparison: + +```typescript +import { isRnExecuTorchError } from '../core/error'; + +try { + await classifier.classify(image); +} catch (e) { + if (isRnExecuTorchError(e, 'RESOURCE_BUSY')) return; // a run is already in flight + throw e; +} +``` + +It is duck-typed and marked `'worklet'`, so it works on both runtimes and on values that +crossed the JSI boundary. + +--- + +## 🔢 The Code Set + +```text +LOAD_FAILED EXECUTION_FAILED SCHEMA_MISMATCH INVALID_ARGUMENT INVALID_STATE +RESOURCE_DISPOSED RESOURCE_BUSY DOWNLOAD_FAILED DOWNLOAD_ABORTED UNKNOWN +``` + +The set is deliberately coarse. **A distinct code earns its place only when an app can +genuinely recover differently from it** (retry a download, wait for a busy resource, +re-create a disposed one). Everything else is a category that exists so crash reporters +can group failures, and the detail belongs in the message. + +Concretely: do not add a code for a specific subsystem (a tokenizer failure is +`EXECUTION_FAILED` or `LOAD_FAILED`) or for a variant of "the caller got it wrong" +(`INVALID_ARGUMENT` already covers unsupported languages, bad ranges, and wrong types). +Subdividing non-recoverable failures buys nothing over the message. + +Adding one means editing `VALID_ERROR_CODES` in `src/core/error.ts` **and** both the +`RnExecuTorchErrorCode` enum and `errorCodeToString` in `cpp/core/error.h`. + +--- + +## ⚙️ C++ / JSI + +Two rules, both mandatory: + +1. **Never throw `jsi::JSError` from library code.** Throw `RnExecuTorchException`. + `guarded` is the only place that turns an exception into a JavaScript value, so a code + cannot be lost on the way out. +2. **Wrap every host function registration in `error::guarded(...)`.** Without it the code + is lost, including on the synchronous worklet path that VisionCamera frame processors + use, which has no `wrapAsync` fallback. + +```cpp +#include "core/error.h" + +namespace rnexecutorch::extensions:: { +namespace jsi = facebook::jsi; +// Required in extensions::*, where sibling lookup does not reach core. +// OMIT this alias inside rnexecutorch::core::*, where unqualified `error` +// already resolves to rnexecutorch::core::error (clang-tidy fails on a dead alias). +namespace error = rnexecutorch::core::error; +using rnexecutorch::core::error::RnExecuTorchErrorCode; +using rnexecutorch::core::error::RnExecuTorchException; + +void install_customOp(jsi::Runtime &rt, jsi::Object &module) { + const auto *name = "customOp"; + auto fnBody = [](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value { + if (count != 3) { + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, + "Usage: customOp(src, dst, factor)"); + } + // ... + return jsi::Value(rt, args[1]); + }; + + module.setProperty(rt, name, + jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 3, + error::guarded(fnBody))); +} +} // namespace +``` + +`RnExecuTorchException` has a second constructor taking an `executorch::runtime::Error`, +which travels to JS as `etRuntimeErrorCode` for diagnostics. Unwrapping an ExecuTorch +`Result` is done with a small file-local `unwrap` helper (see `cpp/core/model.cpp`), not a +shared utility. + +`guard` deliberately lets an existing `jsi::JSError` pass through untouched, so an error +thrown by an app's own callback (for example inside `tensor.through(fn)`) is never +rewritten with our name and code. + +**Placement warning:** put `#include "core/error.h"` and the `using` declarations at file +scope, never inside an `#if defined(__ANDROID__)` / `#elif defined(__APPLE__)` branch. Code +inside a platform branch compiles on your machine and breaks on every other target, and a +macOS-only local syntax check will not catch it. + +--- + +## 📱 Example Apps + +Leave error handling in `apps/` alone. They are a testing ground, so failures should be +surfaced raw (`e.message`, `String(e)`) rather than translated into friendly copy. See +issue #1288 for the separate discussion about splitting user-facing examples out. + +--- + +## 🚫 Avoid / Anti-Patterns + +- **Do NOT branch on message text.** No `/disposed/i.test(msg)`, no `msg.includes(...)`. + Use the code. +- **Do NOT throw bare `Error` or `jsi::JSError`** from library code. +- **Do NOT make `RnExecuTorchError` a class.** It has to survive worklet boundaries. +- **Do NOT add a code for a failure an app cannot recover from differently.** Enrich the + message instead. +- **Do NOT generate the codes.** They are mirrored by hand, like the rest of the TS/JSI + interface. + +--- + +## 📋 Verification Checklist + +When adding or changing error handling, verify that: + +- [ ] Every new throw site uses `RnExecuTorchError('CODE', message)` (TS) or `RnExecuTorchException` (C++). +- [ ] No raw `jsi::JSError` / `std::runtime_error` / `std::invalid_argument` was introduced. +- [ ] Every new `createFromHostFunction` registration is wrapped in `error::guarded(...)`. +- [ ] The `namespace error = ...` alias is present in `extensions::*` and absent in `core::*`. +- [ ] `#include "core/error.h"` and its `using` declarations sit at file scope, outside any preprocessor branch. +- [ ] Catch sites use `isRnExecuTorchError(e, 'CODE')` rather than matching on the message. +- [ ] Any new code was justified by a distinct recovery path, and added to `src/core/error.ts` **and** `cpp/core/error.h` (enum + `errorCodeToString`). +- [ ] Error handling in `apps/` was left surfacing raw errors. diff --git a/.agents/skills/model-schema-validation/SKILL.md b/.agents/skills/model-schema-validation/SKILL.md index c102fd72ef..9b6cfd2254 100644 --- a/.agents/skills/model-schema-validation/SKILL.md +++ b/.agents/skills/model-schema-validation/SKILL.md @@ -220,6 +220,15 @@ const { dims } = validateSpec(model.schema, { --- +## ⚠️ Failure Codes + +`validateSpec` and the native spec validation raise `SCHEMA_MISMATCH` (the spec-builder +helpers `ConstantDim` / `RangeDim` / `EnumDim` raise `INVALID_ARGUMENT` for their own +arguments). A runtime constraint violated by the tensors actually passed to `execute` +raises `INVALID_ARGUMENT`, not `SCHEMA_MISMATCH`: the model is fine, the call is not. See the [Error Handling Skill](../error-handling/SKILL.md). + +--- + ## 📋 Verification Checklist When specifying model schema validations, verify that: @@ -229,3 +238,4 @@ When specifying model schema validations, verify that: - [ ] Symbol values are extracted using `dims.constant(...)`, `dims.range(...)`, or `dims.enum(...)`. - [ ] Multiple shape variants (e.g. `batched` vs `unbatched`) are provided when supported. - [ ] Input and output constraints map accurately to model specifications. +- [ ] New validation failures throw `SCHEMA_MISMATCH` (or `INVALID_ARGUMENT` when the caller's own arguments are at fault), never a bare `Error`. diff --git a/.agents/skills/verify-and-build/SKILL.md b/.agents/skills/verify-and-build/SKILL.md index 8fe6018f61..2db33c5888 100644 --- a/.agents/skills/verify-and-build/SKILL.md +++ b/.agents/skills/verify-and-build/SKILL.md @@ -79,6 +79,8 @@ CLANG_TIDY=$(brew --prefix llvm)/bin/clang-tidy yarn workspace react-native-exec - Requires ExecuTorch headers provisioned under `third-party/include` and `yarn install` (for JSI headers). - Any finding fails the run (`--warnings-as-errors`). Suppress a deliberate, reviewed finding with `// NOLINTNEXTLINE(check-name)` rather than relaxing the shared config. +- **Homebrew LLVM is newer than CI's clang-tidy and reports checks CI does not have.** Before "fixing" a local finding, copy the unmodified base-branch version of the file in under a temp name and lint that too. A finding that also fires there is pre-existing and is not yours to fix in your PR. +- **A local macOS-only syntax check cannot see platform-conditional code.** Anything inside `#if defined(__ANDROID__)` / `#elif defined(__APPLE__)` compiles locally and can still fail CI on another target. Check by eye that new includes and `using` declarations sit at file scope, outside every preprocessor branch. ### 2. `check-cpp-warnings.sh` — clangd warning set @@ -175,6 +177,7 @@ This project does **not** bundle local `.pte` model files inside the React Nativ - **Do NOT run code without verification:** Do not test TypeScript changes in the app without first running `yarn typecheck` (verify types) and `yarn prepare` (build target bundles). - **Do NOT skip native rebuilds after C++ edits:** If any C++ files or config bindings are added/modified, do not attempt to run the app without executing `pod install` (for iOS) or letting Gradle sync (for Android). - **Do NOT run `lint:cpp` with the system `clang-tidy`**: Use the Homebrew LLVM binary: `CLANG_TIDY=$(brew --prefix llvm)/bin/clang-tidy yarn workspace react-native-executorch lint:cpp`. +- **Do NOT trust a green macOS-only syntax check for platform-conditional C++:** see the clang-tidy notes above. - **Do NOT log complex objects inside worklets:** Avoid passing complex circular objects directly to `console.log()` inside functions annotated with `'worklet';` as it can hang or crash the worklet runtime thread. - **Do NOT bundle local `.pte` files in the repository:** Do not commit heavy model binaries to the git repository. Always host them on Hugging Face and register their metadata in `src/models.ts`. diff --git a/.eslintrc.js b/.eslintrc.js index c171cca868..e0f544159c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,6 +2,7 @@ const path = require('path'); const VALID_CATEGORIES = [ 'Constants', + 'Errors', 'Hooks', 'Types', 'Typescript API', diff --git a/packages/react-native-executorch/cpp/core/conversions.cpp b/packages/react-native-executorch/cpp/core/conversions.cpp index 923b473caa..0022e05953 100644 --- a/packages/react-native-executorch/cpp/core/conversions.cpp +++ b/packages/react-native-executorch/cpp/core/conversions.cpp @@ -2,6 +2,12 @@ #include #include +#include "core/error.h" +namespace { +using rnexecutorch::core::error::RnExecuTorchErrorCode; +using rnexecutorch::core::error::RnExecuTorchException; +} // namespace + namespace rnexecutorch::core::conversions { constexpr auto kMaxInt64Double = static_cast(std::numeric_limits::max()); @@ -15,9 +21,9 @@ constexpr auto kMinUint8Double = static_cast(std::numeric_limits(std::numeric_limits::max()); template <> -double asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) { +double asType(jsi::Runtime & /*rt*/, const std::string &ctx, const jsi::Value &val) { if (!val.isNumber()) { - throw jsi::JSError(rt, ctx + " must be a number"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, ctx + " must be a number"); } return val.asNumber(); } @@ -31,7 +37,7 @@ template <> int32_t asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) { double v = asType(rt, ctx, val); if (std::isnan(v) || std::isinf(v) || v != std::trunc(v) || v < kMinInt32Double || v > kMaxInt32Double) { - throw jsi::JSError(rt, ctx + " must be a 32-bit integer"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, ctx + " must be a 32-bit integer"); } return static_cast(v); } @@ -40,7 +46,7 @@ template <> int64_t asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) { double v = asType(rt, ctx, val); if (std::isnan(v) || std::isinf(v) || v != std::trunc(v) || v < kMinInt64Double || v >= kMaxInt64Double) { - throw jsi::JSError(rt, ctx + " must be a 64-bit integer"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, ctx + " must be a 64-bit integer"); } return static_cast(v); } @@ -49,7 +55,7 @@ template <> uint64_t asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) { double v = asType(rt, ctx, val); if (std::isnan(v) || std::isinf(v) || v != std::trunc(v) || v < 0.0 || v >= kMaxUint64Double) { - throw jsi::JSError(rt, ctx + " must be a non-negative integer"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, ctx + " must be a non-negative integer"); } return static_cast(v); } @@ -58,15 +64,15 @@ template <> uint8_t asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) { double v = asType(rt, ctx, val); if (std::isnan(v) || std::isinf(v) || v != std::trunc(v) || v < kMinUint8Double || v > kMaxUint8Double) { - throw jsi::JSError(rt, ctx + " must be an integer between 0 and 255"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, ctx + " must be an integer between 0 and 255"); } return static_cast(v); } template <> -bool asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) { +bool asType(jsi::Runtime & /*rt*/, const std::string &ctx, const jsi::Value &val) { if (!val.isBool()) { - throw jsi::JSError(rt, ctx + " must be a boolean"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, ctx + " must be a boolean"); } return val.asBool(); } @@ -74,7 +80,7 @@ bool asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &va template <> std::string asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) { if (!val.isString()) { - throw jsi::JSError(rt, ctx + " must be a string"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, ctx + " must be a string"); } return val.asString(rt).utf8(rt); } @@ -87,7 +93,7 @@ jsi::Value asType(jsi::Runtime &rt, const std::string & /*ctx*/, con template <> jsi::ArrayBuffer asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) { if (!val.isObject() || !val.asObject(rt).isArrayBuffer(rt)) { - throw jsi::JSError(rt, ctx + " must be an ArrayBuffer"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, ctx + " must be an ArrayBuffer"); } return val.asObject(rt).getArrayBuffer(rt); } @@ -95,7 +101,7 @@ jsi::ArrayBuffer asType(jsi::Runtime &rt, const std::string &c template <> jsi::Object asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) { if (!val.isObject()) { - throw jsi::JSError(rt, ctx + " must be an object"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, ctx + " must be an object"); } return val.asObject(rt); } @@ -103,7 +109,7 @@ jsi::Object asType(jsi::Runtime &rt, const std::string &ctx, const template <> jsi::Array asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) { if (!val.isObject() || !val.asObject(rt).isArray(rt)) { - throw jsi::JSError(rt, ctx + " must be an Array"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, ctx + " must be an Array"); } return val.asObject(rt).asArray(rt); } @@ -111,7 +117,7 @@ jsi::Array asType(jsi::Runtime &rt, const std::string &ctx, const js template <> jsi::Function asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) { if (!val.isObject() || !val.asObject(rt).isFunction(rt)) { - throw jsi::JSError(rt, ctx + " must be a function"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, ctx + " must be a function"); } return val.asObject(rt).asFunction(rt); } diff --git a/packages/react-native-executorch/cpp/core/conversions.h b/packages/react-native-executorch/cpp/core/conversions.h index 50262c3e73..8b65d33893 100644 --- a/packages/react-native-executorch/cpp/core/conversions.h +++ b/packages/react-native-executorch/cpp/core/conversions.h @@ -9,6 +9,7 @@ #include #include +#include "core/error.h" #include namespace rnexecutorch::core::conversions { @@ -61,7 +62,7 @@ DECLARE_ASTYPE_SPECIALIZATION(jsi::ArrayBuffer); template T getRequiredProperty(jsi::Runtime &rt, const std::string &ctx, const jsi::Object &obj, const std::string &propName) { if (!obj.hasProperty(rt, propName.c_str())) { - throw jsi::JSError(rt, std::format("{}: option '{}' is required", ctx, propName)); + throw error::RnExecuTorchException(error::RnExecuTorchErrorCode::InvalidArgument, std::format("{}: option '{}' is required", ctx, propName)); } return asType(rt, std::format("{}: option '{}'", ctx, propName), obj.getProperty(rt, propName.c_str())); } @@ -138,11 +139,11 @@ std::vector fromJsiTypedArray(jsi::Runtime &rt, const std::string &ctx, const const size_t byteLength = getOptionalProperty(rt, ctx, obj, "byteLength").value_or(buffer.size(rt)); if (byteOffset > buffer.size(rt) || byteLength > buffer.size(rt) - byteOffset) { - throw jsi::JSError(rt, std::format("{}: out-of-bounds byteOffset ({}) or byteLength ({}) for ArrayBuffer of size {}", - ctx, byteOffset, byteLength, buffer.size(rt))); + throw error::RnExecuTorchException(error::RnExecuTorchErrorCode::InvalidArgument, std::format("{}: out-of-bounds byteOffset ({}) or byteLength ({}) for ArrayBuffer of size {}", + ctx, byteOffset, byteLength, buffer.size(rt))); } if (byteLength % sizeof(T) != 0) { - throw jsi::JSError(rt, std::format("{}: byteLength is not a multiple of sizeof(T)={}", ctx, sizeof(T))); + throw error::RnExecuTorchException(error::RnExecuTorchErrorCode::InvalidArgument, std::format("{}: byteLength is not a multiple of sizeof(T)={}", ctx, sizeof(T))); } std::vector vec(byteLength / sizeof(T)); diff --git a/packages/react-native-executorch/cpp/core/dtype.cpp b/packages/react-native-executorch/cpp/core/dtype.cpp index 8c57332de8..fc05984330 100644 --- a/packages/react-native-executorch/cpp/core/dtype.cpp +++ b/packages/react-native-executorch/cpp/core/dtype.cpp @@ -1,6 +1,12 @@ #include "dtype.h" #include +#include "core/error.h" +namespace { +using rnexecutorch::core::error::RnExecuTorchErrorCode; +using rnexecutorch::core::error::RnExecuTorchException; +} // namespace + namespace rnexecutorch::core::types { DType dtypeFromString(const std::string &s) { if (s == "uint8") { @@ -15,7 +21,7 @@ DType dtypeFromString(const std::string &s) { if (s == "float32") { return DType::float32; } - throw std::invalid_argument("Unsupported dtype: '" + s + "'. Expected 'uint8', 'int32', 'int64', or 'float32'"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "Unsupported dtype: '" + s + "'. Expected 'uint8', 'int32', 'int64', or 'float32'"); } std::string dtypeToString(DType dtype) { @@ -55,7 +61,7 @@ DType dtypeFromScalarType(executorch::aten::ScalarType st) { case executorch::aten::ScalarType::Float: return DType::float32; default: - throw std::invalid_argument("Unsupported ScalarType"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "Unsupported ScalarType"); } } diff --git a/packages/react-native-executorch/cpp/core/error.cpp b/packages/react-native-executorch/cpp/core/error.cpp new file mode 100644 index 0000000000..e7981195ba --- /dev/null +++ b/packages/react-native-executorch/cpp/core/error.cpp @@ -0,0 +1,19 @@ +#include "error.h" + +namespace rnexecutorch::core::error { + +void throwJsiRnExecuTorchError(jsi::Runtime &rt, const RnExecuTorchException &e) { + auto errorCtor = rt.global().getPropertyAsFunction(rt, "Error"); + auto errObj = errorCtor.call(rt, jsi::String::createFromUtf8(rt, e.what())).asObject(rt); + + errObj.setProperty(rt, "name", jsi::String::createFromUtf8(rt, "RnExecuTorchError")); + errObj.setProperty(rt, "code", jsi::String::createFromUtf8(rt, errorCodeToString(e.code_))); + + if (e.etRuntimeErrorCode_.has_value()) { + errObj.setProperty(rt, "etRuntimeErrorCode", *e.etRuntimeErrorCode_); + } + + throw jsi::JSError(rt, jsi::Value(std::move(errObj))); +} + +} // namespace rnexecutorch::core::error diff --git a/packages/react-native-executorch/cpp/core/error.h b/packages/react-native-executorch/cpp/core/error.h new file mode 100644 index 0000000000..017d11f4af --- /dev/null +++ b/packages/react-native-executorch/cpp/core/error.h @@ -0,0 +1,129 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace rnexecutorch::core::error { +namespace jsi = facebook::jsi; + +/** + * Mirrors the `RnExecuTorchErrorCode` union in `src/core/error.ts`, which is the + * source of truth. Keep the two in sync by hand, the same way the rest of the + * TS/JSI interface is mirrored. + */ +enum class RnExecuTorchErrorCode { + LoadFailed, + ExecutionFailed, + SchemaMismatch, + InvalidArgument, + InvalidState, + ResourceDisposed, + ResourceBusy, + DownloadFailed, + DownloadAborted, + Unknown +}; + +constexpr const char *errorCodeToString(RnExecuTorchErrorCode code) { + switch (code) { + case RnExecuTorchErrorCode::LoadFailed: + return "LOAD_FAILED"; + case RnExecuTorchErrorCode::ExecutionFailed: + return "EXECUTION_FAILED"; + case RnExecuTorchErrorCode::SchemaMismatch: + return "SCHEMA_MISMATCH"; + case RnExecuTorchErrorCode::InvalidArgument: + return "INVALID_ARGUMENT"; + case RnExecuTorchErrorCode::InvalidState: + return "INVALID_STATE"; + case RnExecuTorchErrorCode::ResourceDisposed: + return "RESOURCE_DISPOSED"; + case RnExecuTorchErrorCode::ResourceBusy: + return "RESOURCE_BUSY"; + case RnExecuTorchErrorCode::DownloadFailed: + return "DOWNLOAD_FAILED"; + case RnExecuTorchErrorCode::DownloadAborted: + return "DOWNLOAD_ABORTED"; + default: + return "UNKNOWN"; + } +} + +/** + * The exception every failure in the native layer is raised as. + * + * Native code never throws a jsi::JSError directly. `guarded` is the only place + * that turns an exception into a JavaScript value, so a code can never be lost + * on the way out. + */ +class RnExecuTorchException : public std::runtime_error { +public: + explicit RnExecuTorchException(RnExecuTorchErrorCode code, const std::string &message) + : std::runtime_error(message), code_(code) {} + + explicit RnExecuTorchException(RnExecuTorchErrorCode code, const std::string &message, + executorch::runtime::Error etError) + : std::runtime_error(message), code_(code), + etRuntimeErrorCode_(static_cast(etError)) {} + + RnExecuTorchErrorCode code_; + /** + * The originating executorch::runtime::Error, when the failure came out of + * the ExecuTorch runtime. Kept apart from `code_` so upstream's numbering + * stays independent of ours. + */ + std::optional etRuntimeErrorCode_; +}; + +/** + * Throws `e` into JavaScript as an Error carrying `name`, `code`, and (when the + * failure came from the ExecuTorch runtime) `etRuntimeErrorCode`. These are the + * fields `isRnExecuTorchError` on the TypeScript side reads. + */ +[[noreturn]] void throwJsiRnExecuTorchError(jsi::Runtime &rt, const RnExecuTorchException &e); + +/** + * Runs `fn`, translating anything it throws into a coded JavaScript Error. + */ +template +auto guard(jsi::Runtime &rt, Fn &&fn) -> decltype(fn()) { + try { + return std::forward(fn)(); + } catch (const RnExecuTorchException &e) { + throwJsiRnExecuTorchError(rt, e); + } catch (const jsi::JSError &) { + // Already a JavaScript value, and possibly one thrown by user code + // called back into. Pass it through untouched. + throw; + } catch (const std::exception &e) { + throwJsiRnExecuTorchError(rt, RnExecuTorchException(RnExecuTorchErrorCode::Unknown, e.what())); + } catch (...) { + throwJsiRnExecuTorchError( + rt, RnExecuTorchException(RnExecuTorchErrorCode::Unknown, "Unknown native exception occurred")); + } +} + +/** + * Wraps a JSI host function so that anything thrown inside it, including an + * RnExecuTorchException raised deeper in the native stack, reaches JavaScript + * as a coded Error. + * + * Apply this at every `createFromHostFunction` call site. Doing it here rather + * than inside each body keeps the guarantee in one place: a body that forgets + * to catch cannot silently drop a code. + */ +inline jsi::HostFunctionType guarded(jsi::HostFunctionType fn) { + return [fn = std::move(fn)](jsi::Runtime &rt, const jsi::Value &thisVal, + const jsi::Value *args, size_t count) -> jsi::Value { + return guard(rt, [&] { return fn(rt, thisVal, args, count); }); + }; +} + +} // namespace rnexecutorch::core::error diff --git a/packages/react-native-executorch/cpp/core/model.cpp b/packages/react-native-executorch/cpp/core/model.cpp index 642ca68727..61982e7203 100644 --- a/packages/react-native-executorch/cpp/core/model.cpp +++ b/packages/react-native-executorch/cpp/core/model.cpp @@ -10,6 +10,7 @@ #include #include "dtype.h" +#include "error.h" #include "schema.h" #include "tensor_helpers.h" @@ -20,20 +21,16 @@ #include namespace { -namespace jsi = facebook::jsi; -template -T unwrap(const std::string &ctx, executorch::runtime::Result result) { - if (!result.ok()) { - throw std::runtime_error(std::format("{}: {}", ctx, executorch::runtime::to_string(result.error()))); - } - return std::move(result.get()); -} +using rnexecutorch::core::error::RnExecuTorchErrorCode; +using rnexecutorch::core::error::RnExecuTorchException; template -T unwrap(jsi::Runtime &rt, const std::string &ctx, executorch::runtime::Result result) { +T unwrap(RnExecuTorchErrorCode code, const std::string &ctx, executorch::runtime::Result result) { if (!result.ok()) { - throw jsi::JSError(rt, std::format("{}: {}", ctx, executorch::runtime::to_string(result.error()))); + throw RnExecuTorchException(code, + std::format("{}: {}", ctx, executorch::runtime::to_string(result.error())), + result.error()); } return std::move(result.get()); } @@ -50,22 +47,24 @@ ModelHostObject::ModelHostObject(const std::string &modelPath) : modelPath_(modelPath), etModule_(std::make_unique(modelPath)) { - auto error = etModule_->load(); + auto loadError = etModule_->load(); if (!etModule_->is_loaded()) { - const std::string errorMsg = executorch::runtime::to_string(error); - throw std::runtime_error(std::format("Failed to load model from '{}': {}", - modelPath_, errorMsg)); + const std::string errorMsg = executorch::runtime::to_string(loadError); + throw RnExecuTorchException(RnExecuTorchErrorCode::LoadFailed, + std::format("Failed to load model from '{}': {}", modelPath_, errorMsg), + loadError); } - const auto methodNames = unwrap("method names", etModule_->method_names()); + const auto methodNames = unwrap(RnExecuTorchErrorCode::LoadFailed, "method names", etModule_->method_names()); schema::ModelSpec overrideSpec; if (methodNames.contains(kGetModelSchemaMethod)) { auto ctx = std::format("Execute '{}'", kGetModelSchemaMethod); - auto result = unwrap(ctx, etModule_->execute(kGetModelSchemaMethod)); + auto result = unwrap(RnExecuTorchErrorCode::LoadFailed, ctx, etModule_->execute(kGetModelSchemaMethod)); if (result.empty() || result[0].tag != executorch::runtime::Tag::String) { - throw std::runtime_error(std::format("{} must return a single string value", ctx)); + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, + std::format("{} must return a single string value", ctx)); } auto jsonStr = std::string(result[0].toString()); @@ -74,7 +73,7 @@ ModelHostObject::ModelHostObject(const std::string &modelPath) for (const auto &methodName : methodNames) { auto ctx = std::format("Method '{}'", methodName); - auto methodMeta = unwrap(ctx, etModule_->method_meta(methodName)); + auto methodMeta = unwrap(RnExecuTorchErrorCode::LoadFailed, ctx, etModule_->method_meta(methodName)); spec_[methodName] = schema::methodSpecFromMetadata(methodMeta); backends_[methodName] = schema::getUsedBackends(methodMeta); @@ -106,21 +105,21 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { auto self = shared_from_this(); auto fnBody = [self](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value { if (count != 3) { - throw jsi::JSError(rt, "execute: Usage: execute(methodName, inputs, outputTensors)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "execute: Usage: execute(methodName, inputs, outputTensors)"); } std::unique_lock lock(self->mutex_, std::try_to_lock); if (!lock.owns_lock()) { - throw jsi::JSError(rt, "execute: Model is currently in use"); + throw RnExecuTorchException(RnExecuTorchErrorCode::ResourceBusy, "execute: Model is currently in use"); } if (!self->etModule_) { - throw jsi::JSError(rt, "execute: Model has been disposed"); + throw RnExecuTorchException(RnExecuTorchErrorCode::ResourceDisposed, "execute: Model has been disposed"); } auto methodName = conversions::asType(rt, "execute: methodName", args[0]); if (!self->spec_.contains(methodName)) { - throw jsi::JSError(rt, std::format("execute: Unknown method '{}'", methodName)); + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, std::format("execute: Unknown method '{}'", methodName)); } const auto &methodSpec = self->spec_.at(methodName); @@ -128,8 +127,9 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { auto outputTensorsArray = conversions::asType(rt, "execute: outputTensors", args[2]); if (inputsArray.size(rt) != methodSpec.inputs.size()) { - throw jsi::JSError(rt, std::format("execute: Incorrect size for inputs of method '{}': got {}, expected {}", - methodName, inputsArray.size(rt), methodSpec.inputs.size())); + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, + std::format("execute: Incorrect size for inputs of method '{}': got {}, expected {}", + methodName, inputsArray.size(rt), methodSpec.inputs.size())); } std::vector inputs(methodSpec.inputs.size()); @@ -148,8 +148,8 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { auto tensorHostObject = tensor::fromJs(rt, ctx, val, tSpec.dtype, tSpec.shape); if (!lockedTensors.insert(tensorHostObject.get()).second) { - throw jsi::JSError(rt, "execute: Tensor aliasing detected. " - "The same tensor was passed multiple times."); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "execute: Tensor aliasing detected. " + "The same tensor was passed multiple times."); } tensorLocks.emplace_back(tensor::tryLockUnique(rt, ctx, tensorHostObject)); inputShapes.push_back(tensorHostObject->shape_); @@ -169,8 +169,9 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { inputs[i] = executorch::runtime::EValue(); break; default: - throw jsi::JSError(rt, std::format("{}: Unsupported input type: {}", - ctx, executorch::runtime::tag_to_string(tag))); + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, + std::format("{}: Unsupported input type: {}", + ctx, executorch::runtime::tag_to_string(tag))); } } @@ -189,28 +190,29 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { logFn.callWithThis(rt, consoleObj, {jsi::String::createFromUtf8(rt, info)}); #endif - auto result = unwrap(rt, std::format("execute: Method '{}' failed.\n" - "\n" - "Common causes:\n" - " 1. Backend not registered\n" - " Ensure backends from `model.backends` are registered\n" - " in the ExecuTorch runtime\n" - " (use `getRegisteredBackends()` to check registered backends).\n" - "\n" - " 2. Shape/constraint mismatch\n" - " If the model uses dynamic shapes or runtime constraints\n" - " (e.g. equality between dimensions), export a companion\n" - " method returning a JSON model spec\n" - " (see `src/core/schema.ts` for the JSON structure).\n" - " Without it, validation falls back to static metadata\n" - " from ExecuTorch which only contains upper bounds and\n" - " does not capture runtime constraints.\n" - "\n" - " 3. Bad model export\n" - " The model export itself might be broken or invalid.\n" - "\n" - "Error", - methodName), + auto result = unwrap(RnExecuTorchErrorCode::ExecutionFailed, + std::format("execute: Method '{}' failed.\n" + "\n" + "Common causes:\n" + " 1. Backend not registered\n" + " Ensure backends from `model.backends` are registered\n" + " in the ExecuTorch runtime\n" + " (use `getRegisteredBackends()` to check registered backends).\n" + "\n" + " 2. Shape/constraint mismatch\n" + " If the model uses dynamic shapes or runtime constraints\n" + " (e.g. equality between dimensions), export a companion\n" + " method returning a JSON model spec\n" + " (see `src/core/schema.ts` for the JSON structure).\n" + " Without it, validation falls back to static metadata\n" + " from ExecuTorch which only contains upper bounds and\n" + " does not capture runtime constraints.\n" + "\n" + " 3. Bad model export\n" + " The model export itself might be broken or invalid.\n" + "\n" + "Error", + methodName), std::move(executeResult)); auto jsOutputArray = jsi::Array(rt, result.size()); @@ -221,9 +223,10 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { switch (output.tag) { case executorch::runtime::Tag::Tensor: { if (tensorOutputIdx >= outputTensorsArray.size(rt)) { - throw jsi::JSError(rt, std::format("execute: Not enough tensor output placeholders in outputTensors" - " (provided {}, expected at least {})", - outputTensorsArray.size(rt), tensorOutputIdx + 1)); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, + std::format("execute: Not enough tensor output placeholders in outputTensors" + " (provided {}, expected at least {})", + outputTensorsArray.size(rt), tensorOutputIdx + 1)); } auto ctx = std::format("execute: outputTensors[{}]", tensorOutputIdx); @@ -234,8 +237,8 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { auto tensorHostObject = tensor::fromJs(rt, ctx, val, dtype, shape); if (!lockedTensors.insert(tensorHostObject.get()).second) { - throw jsi::JSError(rt, "execute: Tensor aliasing detected. " - "The same tensor was passed multiple times."); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "execute: Tensor aliasing detected. " + "The same tensor was passed multiple times."); } tensorLocks.emplace_back(tensor::tryLockUnique(rt, ctx, tensorHostObject)); std::memcpy(tensorHostObject->data_.get(), @@ -262,8 +265,9 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { jsOutputArray.setValueAtIndex(rt, outputIdx, jsi::String::createFromUtf8(rt, std::string(output.toString()))); break; default: - throw jsi::JSError(rt, std::format("execute: Unsupported return type: {}", - executorch::runtime::tag_to_string(output.tag))); + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, + std::format("execute: Unsupported return type: {}", + executorch::runtime::tag_to_string(output.tag))); } ++outputIdx; @@ -271,27 +275,27 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { return jsOutputArray; }; - return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "execute"), 3, fnBody); + return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "execute"), 3, error::guarded(fnBody)); } if (nameStr == "dispose") { auto self = shared_from_this(); - auto fnBody = [self](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value * /*args*/, size_t count) -> jsi::Value { + auto fnBody = [self](jsi::Runtime & /*rt*/, const jsi::Value & /*thisVal*/, const jsi::Value * /*args*/, size_t count) -> jsi::Value { if (count != 0) { - throw jsi::JSError(rt, "dispose: Usage: dispose()"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "dispose: Usage: dispose()"); } std::unique_lock lock(self->mutex_); if (!self->etModule_) { - throw jsi::JSError(rt, "dispose: Model has already been disposed"); + throw RnExecuTorchException(RnExecuTorchErrorCode::ResourceDisposed, "dispose: Model has already been disposed"); } self->etModule_.reset(); return jsi::Value::undefined(); }; - return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "dispose"), 0, fnBody); + return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "dispose"), 0, error::guarded(fnBody)); } return jsi::Value::undefined(); @@ -311,17 +315,13 @@ void install_loadModel(jsi::Runtime &rt, jsi::Object &module) { const auto *name = "loadModel"; auto fnBody = [](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value { if (count != 1) { - throw jsi::JSError(rt, "loadModel: Usage: loadModel(path)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "loadModel: Usage: loadModel(path)"); } auto modelPath = conversions::asType(rt, "loadModel: path", args[0]); - try { - return jsi::Object::createFromHostObject(rt, std::make_shared(modelPath)); - } catch (const std::exception &e) { - throw jsi::JSError(rt, std::format("loadModel: {}", e.what())); - } + return jsi::Object::createFromHostObject(rt, std::make_shared(modelPath)); }; - auto fn = jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 1, fnBody); + auto fn = jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 1, error::guarded(fnBody)); module.setProperty(rt, name, fn); } diff --git a/packages/react-native-executorch/cpp/core/schema.cpp b/packages/react-native-executorch/cpp/core/schema.cpp index 24e8dd882b..9a6b765aa9 100644 --- a/packages/react-native-executorch/cpp/core/schema.cpp +++ b/packages/react-native-executorch/cpp/core/schema.cpp @@ -9,6 +9,12 @@ #include "dtype.h" +#include "core/error.h" +namespace { +using rnexecutorch::core::error::RnExecuTorchErrorCode; +using rnexecutorch::core::error::RnExecuTorchException; +} // namespace + namespace nlohmann { template <> // Tag lives in executorch::runtime; adl_serializer is the @@ -47,7 +53,7 @@ struct adl_serializer { if (s == "ListTensor") { return executorch::runtime::Tag::ListTensor; } - throw std::runtime_error(std::format("unknown param kind '{}'", s)); + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, std::format("unknown param kind '{}'", s)); } static void to_json(json &j, executorch::runtime::Tag t) { j = executorch::runtime::tag_to_string(t); @@ -71,7 +77,7 @@ struct overloaded : Ts... { template T unwrap(const std::string &ctx, executorch::runtime::Result result) { if (!result.ok()) { - throw std::runtime_error(std::format("{}: {}", ctx, executorch::runtime::to_string(result.error()))); + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, std::format("{}: {}", ctx, executorch::runtime::to_string(result.error()))); } return std::move(result.get()); } @@ -98,7 +104,7 @@ void from_json(const json &j, ConcreteDim &d) { } else if (kind == "enum") { d = EnumDim{.choices = j.at("choices").get>()}; } else { - throw std::runtime_error(std::format("unsupported dim kind '{}'", kind)); + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, std::format("unsupported dim kind '{}'", kind)); } } // NOLINTNEXTLINE(misc-use-internal-linkage): ADL requires external linkage. @@ -139,7 +145,7 @@ void from_json(const json &j, RuntimeConstraint &c) { } else if (kind == "linear") { c = j.get(); } else { - throw std::runtime_error(std::format("unknown constraint kind '{}'", kind)); + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, std::format("unknown constraint kind '{}'", kind)); } } // NOLINTNEXTLINE(misc-use-internal-linkage): ADL requires external linkage. @@ -167,7 +173,7 @@ ModelSpec parseModelSpecJson(const std::string &ctx, const std::string &jsonStr) try { return json::parse(jsonStr).get(); } catch (const std::exception &e) { - throw std::runtime_error(std::format("{}: {}", ctx, e.what())); + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, std::format("{}: {}", ctx, e.what())); } } @@ -296,27 +302,27 @@ void validateConcreteDim(const ConcreteDim &dim, const std::string &ctx) { std::visit(overloaded{ [&](int32_t c) { if (c <= 0) { - throw std::runtime_error(std::format("{}: constant dim must be positive", ctx)); + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, std::format("{}: constant dim must be positive", ctx)); } }, [&](const RangeDim &r) { if (r.min <= 0) { - throw std::runtime_error(std::format("{}: range min must be positive", ctx)); + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, std::format("{}: range min must be positive", ctx)); } if (r.max < r.min) { - throw std::runtime_error(std::format("{}: range max must be >= min", ctx)); + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, std::format("{}: range max must be >= min", ctx)); } if (r.step <= 0) { - throw std::runtime_error(std::format("{}: range step must be positive", ctx)); + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, std::format("{}: range step must be positive", ctx)); } }, [&](const EnumDim &e) { if (e.choices.empty()) { - throw std::runtime_error(std::format("{}: enum must have at least one choice", ctx)); + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, std::format("{}: enum must have at least one choice", ctx)); } for (const auto &choice : e.choices) { if (choice <= 0) { - throw std::runtime_error(std::format("{}: enum choices must be positive", ctx)); + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, std::format("{}: enum choices must be positive", ctx)); } } }, @@ -345,14 +351,14 @@ void validateTensorParam(const ParamSpec ¶m, const std::string &ctx) { auto metaDtype = types::dtypeFromScalarType(tensorMeta.scalar_type()); if (param.dtype != metaDtype) { - throw std::runtime_error(std::format("{}: dtype mismatch (spec type '{}' != compiled metadata type '{}')", - ctx, types::dtypeToString(param.dtype), types::dtypeToString(metaDtype))); + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, std::format("{}: dtype mismatch (spec type '{}' != compiled metadata type '{}')", + ctx, types::dtypeToString(param.dtype), types::dtypeToString(metaDtype))); } auto metaShape = tensorMeta.sizes(); if (param.shape.size() != metaShape.size()) { - throw std::runtime_error(std::format("{}: rank mismatch (spec rank {} != compiled metadata rank {})", - ctx, param.shape.size(), metaShape.size())); + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, std::format("{}: rank mismatch (spec rank {} != compiled metadata rank {})", + ctx, param.shape.size(), metaShape.size())); } for (size_t d = 0; d < param.shape.size(); ++d) { @@ -361,20 +367,20 @@ void validateTensorParam(const ParamSpec ¶m, std::visit(overloaded{ [&](int32_t c) { if (c != bound) { - throw std::runtime_error(std::format("{}: shape[{}] mismatch (spec constant {} != compiled bound {})", + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, std::format("{}: shape[{}] mismatch (spec constant {} != compiled bound {})", ctx, d, c, bound)); } }, [&](const RangeDim &r) { if (r.max > bound) { - throw std::runtime_error(std::format("{}: shape[{}] range max {} exceeds compiled bound {}", + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, std::format("{}: shape[{}] range max {} exceeds compiled bound {}", ctx, d, r.max, bound)); } }, [&](const EnumDim &e) { for (const auto choice : e.choices) { if (choice > bound) { - throw std::runtime_error(std::format("{}: shape[{}] enum choice {} exceeds compiled bound {}", + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, std::format("{}: shape[{}] enum choice {} exceeds compiled bound {}", ctx, d, choice, bound)); } } @@ -392,10 +398,10 @@ void validateDimRef(const DimRef &ref, bool isInput = (ref.paramSide == ParamSide::input); const auto &ranks = isInput ? inputRanks : outputRanks; if (std::cmp_greater_equal(ref.tensorIdx, ranks.size())) { - throw std::runtime_error(std::format("{}: tensorIdx {} out of range", ctx, ref.tensorIdx)); + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, std::format("{}: tensorIdx {} out of range", ctx, ref.tensorIdx)); } if (std::cmp_greater_equal(ref.dimIdx, ranks[static_cast(ref.tensorIdx)])) { - throw std::runtime_error(std::format("{}: dimIdx {} out of range", ctx, ref.dimIdx)); + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, std::format("{}: dimIdx {} out of range", ctx, ref.dimIdx)); } } @@ -421,7 +427,7 @@ void validateConstraintSpecs(const MethodSpec &spec, const std::string &ctx) { std::visit(overloaded{ [&](const EqualityConstraint &eq) { if (eq.dims.size() < 2) { - throw std::runtime_error(std::format("{}: equality needs at least two dims", cctx)); + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, std::format("{}: equality needs at least two dims", cctx)); } for (const auto &dim : eq.dims) { validateDimRef(dim, inputRanks, outputRanks, cctx); @@ -446,9 +452,9 @@ void validateParamsAgainstMeta(const std::vector ¶ms, : unwrap(pctx, meta.output_tag(i)); if (params[i].tag != tagResult) { - throw std::runtime_error(std::format("{}: tag mismatch (spec tag {} != compiled metadata tag {})", - pctx, executorch::runtime::tag_to_string(params[i].tag), - executorch::runtime::tag_to_string(tagResult))); + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, std::format("{}: tag mismatch (spec tag {} != compiled metadata tag {})", + pctx, executorch::runtime::tag_to_string(params[i].tag), + executorch::runtime::tag_to_string(tagResult))); } if (tagResult == Tag::Tensor) { @@ -466,12 +472,12 @@ void validateSpec(const MethodSpec &spec, const std::string &ctx) { if (spec.inputs.size() != meta.num_inputs()) { - throw std::runtime_error(std::format("{}: input count mismatch (spec has {}, model metadata has {})", - ctx, spec.inputs.size(), meta.num_inputs())); + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, std::format("{}: input count mismatch (spec has {}, model metadata has {})", + ctx, spec.inputs.size(), meta.num_inputs())); } if (spec.outputs.size() != meta.num_outputs()) { - throw std::runtime_error(std::format("{}: output count mismatch (spec has {}, model metadata has {})", - ctx, spec.outputs.size(), meta.num_outputs())); + throw RnExecuTorchException(RnExecuTorchErrorCode::SchemaMismatch, std::format("{}: output count mismatch (spec has {}, model metadata has {})", + ctx, spec.outputs.size(), meta.num_outputs())); } validateSpecDimDomains(spec, ctx); @@ -489,7 +495,7 @@ int32_t getInputDimValue(const DimRef &ref, } // namespace -void validateRuntimeConstraints(jsi::Runtime &rt, +void validateRuntimeConstraints(jsi::Runtime & /*rt*/, const std::vector &constraints, const std::vector> &inputShapes, const std::string &ctx) { @@ -510,7 +516,7 @@ void validateRuntimeConstraints(jsi::Runtime &rt, } for (size_t j = 1; j < inputVals.size(); ++j) { if (inputVals[j] != inputVals[0]) { - throw jsi::JSError(rt, std::format("{}: equality constraint violated (dimension value {} != {})", + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("{}: equality constraint violated (dimension value {} != {})", cctx, inputVals[0], inputVals[j])); } } @@ -523,7 +529,7 @@ void validateRuntimeConstraints(jsi::Runtime &rt, int32_t lhs = getInputDimValue(lin.dimLhs, inputShapes); int32_t rhs = getInputDimValue(lin.dimRhs, inputShapes); if (lhs != lin.coefficients[0] * rhs + lin.coefficients[1]) { - throw jsi::JSError(rt, std::format("{}: linear constraint violated (LHS {} != {} * RHS {} + {})", + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("{}: linear constraint violated (LHS {} != {} * RHS {} + {})", cctx, lhs, lin.coefficients[0], rhs, lin.coefficients[1])); } }, diff --git a/packages/react-native-executorch/cpp/core/tensor.cpp b/packages/react-native-executorch/cpp/core/tensor.cpp index 8973e5475d..8b4e931bf0 100644 --- a/packages/react-native-executorch/cpp/core/tensor.cpp +++ b/packages/react-native-executorch/cpp/core/tensor.cpp @@ -15,6 +15,12 @@ #include +#include "core/error.h" +namespace { +using rnexecutorch::core::error::RnExecuTorchErrorCode; +using rnexecutorch::core::error::RnExecuTorchException; +} // namespace + namespace rnexecutorch::core::tensor { namespace types = rnexecutorch::core::types; namespace conversions = rnexecutorch::core::conversions; @@ -51,7 +57,7 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) auto self = shared_from_this(); auto fnBody = [self](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value { if (count != 1 && count != 2) { - throw jsi::JSError(rt, "copyTo: Usage: copyTo(dst, options?)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "copyTo: Usage: copyTo(dst, options?)"); } auto dst = tensor::fromJs(rt, "copyTo: dst", args[0], std::nullopt, std::nullopt); @@ -67,35 +73,35 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) size_t offset = getOptionalProperty(rt, "copyTo: options", optsObj, "offset").value_or(0); if (offset > self->numel_) { - throw jsi::JSError(rt, std::format("copyTo: offset {} is out of bounds for src tensor of size {} elements", - offset, self->numel_)); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("copyTo: offset {} is out of bounds for src tensor of size {} elements", + offset, self->numel_)); } size_t length = getOptionalProperty(rt, "copyTo: options", optsObj, "length").value_or(self->numel_ - offset); if (length > self->numel_ - offset) { - throw jsi::JSError(rt, std::format("copyTo: length {} is out of bounds for offset {} of src tensor (numel {})", - length, offset, self->numel_)); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("copyTo: length {} is out of bounds for offset {} of src tensor (numel {})", + length, offset, self->numel_)); } const auto elemSize = types::elementSize(self->dtype_); if (length * elemSize != dst->size_) { - throw jsi::JSError(rt, std::format("copyTo: size mismatch between copy size ({} bytes) and dst tensor size ({} bytes)", - length * elemSize, dst->size_)); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("copyTo: size mismatch between copy size ({} bytes) and dst tensor size ({} bytes)", + length * elemSize, dst->size_)); } std::memcpy(dst->data_.get(), self->data_.get() + (offset * elemSize), length * elemSize); return jsi::Value(rt, args[0]); }; - return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "copyTo"), 1, fnBody); + return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "copyTo"), 1, error::guarded(fnBody)); } if (nameStr == "setData") { auto self = shared_from_this(); auto fnBody = [self](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) -> jsi::Value { if (count != 1) { - throw jsi::JSError(rt, "setData: Usage: setData(array)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "setData: Usage: setData(array)"); } auto dataObj = conversions::asType(rt, "setData: array", args[0]); @@ -106,27 +112,27 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) auto lock = tryLockUnique(rt, "setData: self", self); if (byteOffset > buffer.size(rt) || byteLength > buffer.size(rt) - byteOffset) { - throw jsi::JSError(rt, std::format("setData: Out of bounds offset ({}) or length ({}) for buffer of size {}", - byteOffset, byteLength, buffer.size(rt))); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("setData: Out of bounds offset ({}) or length ({}) for buffer of size {}", + byteOffset, byteLength, buffer.size(rt))); } if (byteLength != self->size_) { - throw jsi::JSError(rt, std::format("setData: Data size mismatch: TypedArray is {} bytes, but Tensor requires {} bytes.", - byteLength, self->size_)); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("setData: Data size mismatch: TypedArray is {} bytes, but Tensor requires {} bytes.", + byteLength, self->size_)); } std::memcpy(self->data_.get(), buffer.data(rt) + byteOffset, byteLength); return jsi::Value(rt, thisVal); }; - return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "setData"), 1, fnBody); + return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "setData"), 1, error::guarded(fnBody)); } if (nameStr == "getData") { auto self = shared_from_this(); auto fnBody = [self](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value { if (count != 1) { - throw jsi::JSError(rt, "getData: Usage: getData(array)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "getData: Usage: getData(array)"); } auto dataObj = conversions::asType(rt, "getData: array", args[0]); @@ -137,27 +143,27 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) auto lock = tryLockShared(rt, "getData: self", self); if (byteOffset > buffer.size(rt) || byteLength > buffer.size(rt) - byteOffset) { - throw jsi::JSError(rt, std::format("getData: Out of bounds offset ({}) or length ({}) for buffer of size {}", - byteOffset, byteLength, buffer.size(rt))); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("getData: Out of bounds offset ({}) or length ({}) for buffer of size {}", + byteOffset, byteLength, buffer.size(rt))); } if (byteLength != self->size_) { - throw jsi::JSError(rt, std::format("getData: Data size mismatch: TypedArray is {} bytes, but Tensor requires {} bytes.", - byteLength, self->size_)); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("getData: Data size mismatch: TypedArray is {} bytes, but Tensor requires {} bytes.", + byteLength, self->size_)); } std::memcpy(buffer.data(rt) + byteOffset, self->data_.get(), byteLength); return jsi::Value(rt, args[0]); }; - return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "getData"), 1, fnBody); + return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "getData"), 1, error::guarded(fnBody)); } if (nameStr == "through") { auto self = shared_from_this(); auto fnBody = [self](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) -> jsi::Value { if (count < 1) { - throw jsi::JSError(rt, "through: Usage: through(fn, ...args)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "through: Usage: through(fn, ...args)"); } auto fn = conversions::asType(rt, "through: fn", args[0]); @@ -172,14 +178,14 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) return fn.call(rt, static_cast(fnArgs.data()), fnArgs.size()); }; - return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "through"), 1, fnBody); + return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "through"), 1, error::guarded(fnBody)); } if (nameStr == "throughIf") { auto self = shared_from_this(); auto fnBody = [self](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) -> jsi::Value { if (count < 2) { - throw jsi::JSError(rt, "throughIf: Usage: throughIf(pred, fn, ...args)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "throughIf: Usage: throughIf(pred, fn, ...args)"); } const bool pred = conversions::asType(rt, "throughIf: pred", args[0]); @@ -199,20 +205,20 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) return fn.call(rt, static_cast(fnArgs.data()), fnArgs.size()); }; - return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "throughIf"), 2, fnBody); + return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "throughIf"), 2, error::guarded(fnBody)); } if (nameStr == "dispose") { auto self = shared_from_this(); - auto fnBody = [self](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value * /*args*/, size_t count) -> jsi::Value { + auto fnBody = [self](jsi::Runtime & /*rt*/, const jsi::Value & /*thisVal*/, const jsi::Value * /*args*/, size_t count) -> jsi::Value { if (count != 0) { - throw jsi::JSError(rt, "dispose: Usage: dispose()"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "dispose: Usage: dispose()"); } std::unique_lock lock(self->mutex_); if (!self->data_) { - throw jsi::JSError(rt, "dispose: Tensor has already been disposed"); + throw RnExecuTorchException(RnExecuTorchErrorCode::ResourceDisposed, "dispose: Tensor has already been disposed"); } self->tensor_.reset(); @@ -220,7 +226,7 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) return jsi::Value::undefined(); }; - return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "dispose"), 0, fnBody); + return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "dispose"), 0, error::guarded(fnBody)); } return jsi::Value::undefined(); @@ -244,22 +250,22 @@ void install_createTensor(jsi::Runtime &rt, jsi::Object &module) { const auto *name = "createTensor"; auto fnBody = [](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value { if (count != 2) { - throw jsi::JSError(rt, "createTensor: Usage: createTensor(shape, dtype)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "createTensor: Usage: createTensor(shape, dtype)"); } auto shape = conversions::asVector(rt, "createTensor: shape", args[0]); if (std::ranges::any_of(shape, [](auto dim) { return dim <= 0; })) { - throw jsi::JSError(rt, "createTensor: Shape dimensions must be positive integers"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "createTensor: Shape dimensions must be positive integers"); } try { const auto dtype = types::dtypeFromString(conversions::asType(rt, "createTensor: dtype", args[1])); return jsi::Object::createFromHostObject(rt, std::make_shared(shape, dtype)); } catch (const std::exception &e) { - throw jsi::JSError(rt, std::format("createTensor: Error creating tensor: {}", e.what())); + throw RnExecuTorchException(RnExecuTorchErrorCode::Unknown, std::format("createTensor: Error creating tensor: {}", e.what())); } }; - auto fn = jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 2, fnBody); + auto fn = jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 2, error::guarded(fnBody)); module.setProperty(rt, name, fn); } diff --git a/packages/react-native-executorch/cpp/core/tensor_helpers.cpp b/packages/react-native-executorch/cpp/core/tensor_helpers.cpp index 861b6de1b0..6fa5f5bcdf 100644 --- a/packages/react-native-executorch/cpp/core/tensor_helpers.cpp +++ b/packages/react-native-executorch/cpp/core/tensor_helpers.cpp @@ -11,39 +11,45 @@ #include "core/schema.h" #include "dtype.h" +#include "core/error.h" +namespace { +using rnexecutorch::core::error::RnExecuTorchErrorCode; +using rnexecutorch::core::error::RnExecuTorchException; +} // namespace + namespace rnexecutorch::core::tensor { namespace types = rnexecutorch::core::types; namespace conversions = rnexecutorch::core::conversions; std::shared_lock -tryLockShared(jsi::Runtime &rt, const std::string &ctx, const std::shared_ptr &tensor) { +tryLockShared(jsi::Runtime & /*rt*/, const std::string &ctx, const std::shared_ptr &tensor) { std::shared_lock lock(tensor->mutex_, std::try_to_lock); if (!lock.owns_lock()) { - throw jsi::JSError(rt, std::format("{} tensor is currently in use", ctx)); + throw RnExecuTorchException(RnExecuTorchErrorCode::ResourceBusy, std::format("{} tensor is currently in use", ctx)); } if (!tensor->data_) { - throw jsi::JSError(rt, std::format("{} tensor has been disposed", ctx)); + throw RnExecuTorchException(RnExecuTorchErrorCode::ResourceDisposed, std::format("{} tensor has been disposed", ctx)); } return lock; } std::unique_lock -tryLockUnique(jsi::Runtime &rt, const std::string &ctx, const std::shared_ptr &tensor) { +tryLockUnique(jsi::Runtime & /*rt*/, const std::string &ctx, const std::shared_ptr &tensor) { std::unique_lock lock(tensor->mutex_, std::try_to_lock); if (!lock.owns_lock()) { - throw jsi::JSError(rt, std::format("{} tensor is currently in use", ctx)); + throw RnExecuTorchException(RnExecuTorchErrorCode::ResourceBusy, std::format("{} tensor is currently in use", ctx)); } if (!tensor->data_) { - throw jsi::JSError(rt, std::format("{} tensor has been disposed", ctx)); + throw RnExecuTorchException(RnExecuTorchErrorCode::ResourceDisposed, std::format("{} tensor has been disposed", ctx)); } return lock; } -void checkNotSameTensor(jsi::Runtime &rt, +void checkNotSameTensor(jsi::Runtime & /*rt*/, const std::string &ctx1, const std::shared_ptr &t1, const std::string &ctx2, const std::shared_ptr &t2) { if (t1 == t2) { - throw jsi::JSError(rt, std::format("{} and {} cannot be the same tensor", ctx1, ctx2)); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("{} and {} cannot be the same tensor", ctx1, ctx2)); } } @@ -91,7 +97,7 @@ fromJs(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &value, auto obj = conversions::asType(rt, ctx, value); if (!obj.isHostObject(rt)) { - throw jsi::JSError(rt, ctx + " must be a Tensor"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, ctx + " must be a Tensor"); } auto tensor = obj.getHostObject(rt); @@ -99,8 +105,8 @@ fromJs(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &value, const auto &shape = tensor->shape_; if (expectedDtype && dtype != *expectedDtype) { - throw jsi::JSError(rt, std::format("{} must be of type {} (got {})", - ctx, types::dtypeToString(*expectedDtype), types::dtypeToString(dtype))); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("{} must be of type {} (got {})", + ctx, types::dtypeToString(*expectedDtype), types::dtypeToString(dtype))); } if (!expectedShape) { @@ -108,8 +114,8 @@ fromJs(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &value, } if (shape.size() != expectedShape->size()) { - throw jsi::JSError(rt, std::format("{} must have shape {} (expected {} dimensions, got {})", - ctx, shapeToString(*expectedShape), expectedShape->size(), shape.size())); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("{} must have shape {} (expected {} dimensions, got {})", + ctx, shapeToString(*expectedShape), expectedShape->size(), shape.size())); } std::unordered_map symbolBinding; @@ -121,34 +127,34 @@ fromJs(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &value, std::visit(overloaded{ [&](const std::string &symbol) { if (symbolBinding.contains(symbol) && symbolBinding[symbol] != shape[i]) { - throw jsi::JSError(rt, std::format("{} must have shape {} (symbol {} mismatch: expected {}, got {})", + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("{} must have shape {} (symbol {} mismatch: expected {}, got {})", ctx, shapeToString(*expectedShape), symbol, symbolBinding[symbol], shape[i])); } symbolBinding[symbol] = shape[i]; }, [&](int32_t val) { if (shape[i] != val) { - throw jsi::JSError(rt, std::format("{} must have shape {} (dim {} mismatch: expected {}, got {})", + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("{} must have shape {} (dim {} mismatch: expected {}, got {})", ctx, shapeToString(*expectedShape), i, val, shape[i])); } }, [&](const schema::RangeDim &range) { if (shape[i] < range.min) { - throw jsi::JSError(rt, std::format("{} must have shape {} (dim {} out of range: {} < min {})", + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("{} must have shape {} (dim {} out of range: {} < min {})", ctx, shapeToString(*expectedShape), i, shape[i], range.min)); } if (shape[i] > range.max) { - throw jsi::JSError(rt, std::format("{} must have shape {} (dim {} out of range: {} > max {})", + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("{} must have shape {} (dim {} out of range: {} > max {})", ctx, shapeToString(*expectedShape), i, shape[i], range.max)); } if ((shape[i] - range.min) % range.step != 0) { - throw jsi::JSError(rt, std::format("{} must have shape {} (dim {} must be min({}) + k*step({}), got {})", + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("{} must have shape {} (dim {} must be min({}) + k*step({}), got {})", ctx, shapeToString(*expectedShape), i, range.min, range.step, shape[i])); } }, [&](const schema::EnumDim &enumeration) { if (std::ranges::find(enumeration.choices, shape[i]) == enumeration.choices.end()) { - throw jsi::JSError(rt, std::format("{} must have shape {} (dim {} not allowed: got {})", + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("{} must have shape {} (dim {} not allowed: got {})", ctx, shapeToString(*expectedShape), i, shape[i])); } }, diff --git a/packages/react-native-executorch/cpp/core/utils.cpp b/packages/react-native-executorch/cpp/core/utils.cpp index 095caf4a63..31631260f8 100644 --- a/packages/react-native-executorch/cpp/core/utils.cpp +++ b/packages/react-native-executorch/cpp/core/utils.cpp @@ -5,6 +5,8 @@ #include #include +#include "core/error.h" + #if defined(__ANDROID__) #include #elif defined(__APPLE__) @@ -14,6 +16,9 @@ namespace rnexecutorch::core::utils { namespace jsi = facebook::jsi; +using rnexecutorch::core::error::RnExecuTorchErrorCode; +using rnexecutorch::core::error::RnExecuTorchException; + namespace { // Detects an Android emulator / iOS simulator. On Android no single property // covers every image, so we check three: the build fingerprint (`generic...` @@ -78,7 +83,7 @@ void install_getExecuTorchRegisteredBackends(jsi::Runtime &rt, jsi::Object &modu const auto *name = "getExecuTorchRegisteredBackends"; auto fnBody = [](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value * /*args*/, size_t count) -> jsi::Value { if (count != 0) { - throw jsi::JSError(rt, "Usage: getExecuTorchRegisteredBackends()"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "Usage: getExecuTorchRegisteredBackends()"); } auto registeredCount = executorch::runtime::get_num_registered_backends(); @@ -87,13 +92,13 @@ void install_getExecuTorchRegisteredBackends(jsi::Runtime &rt, jsi::Object &modu auto backendName = executorch::runtime::get_backend_name(i); if (!backendName.ok()) { const std::string errorMsg = executorch::runtime::to_string(backendName.error()); - throw jsi::JSError(rt, "Failed to get backend name: " + errorMsg); + throw RnExecuTorchException(RnExecuTorchErrorCode::Unknown, "Failed to get backend name: " + errorMsg); } jsArray.setValueAtIndex(rt, i, jsi::String::createFromUtf8(rt, backendName.get())); } return jsArray; }; - auto fn = jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 0, fnBody); + auto fn = jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 0, error::guarded(fnBody)); module.setProperty(rt, name, fn); } diff --git a/packages/react-native-executorch/cpp/extensions/cv/box_ops.cpp b/packages/react-native-executorch/cpp/extensions/cv/box_ops.cpp index 674fba7088..15798b1320 100644 --- a/packages/react-native-executorch/cpp/extensions/cv/box_ops.cpp +++ b/packages/react-native-executorch/cpp/extensions/cv/box_ops.cpp @@ -20,6 +20,13 @@ #include +#include "core/error.h" +namespace { +namespace error = rnexecutorch::core::error; +using rnexecutorch::core::error::RnExecuTorchErrorCode; +using rnexecutorch::core::error::RnExecuTorchException; +} // namespace + namespace rnexecutorch::extensions::cv::box_ops { namespace jsi = facebook::jsi; namespace tensor = rnexecutorch::core::tensor; @@ -44,7 +51,7 @@ BoxFormat parseBoxFormat(const std::string &s) { if (s == "cxcywh") { return BoxFormat::CXCYWH; } - throw std::invalid_argument(std::format("unsupported boxFormat '{}'. Expected 'xyxy', 'xywh', or 'cxcywh'", s)); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("unsupported boxFormat '{}'. Expected 'xyxy', 'xywh', or 'cxcywh'", s)); } enum class NmsType { @@ -59,7 +66,7 @@ NmsType parseNmsType(const std::string &s) { if (s == "weighted") { return NmsType::Weighted; } - throw std::invalid_argument(std::format("unsupported nmsType '{}'. Expected 'standard' or 'weighted'", s)); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("unsupported nmsType '{}'. Expected 'standard' or 'weighted'", s)); } constexpr size_t kBoxCoords = 4; @@ -84,7 +91,7 @@ void install_nms(jsi::Runtime &rt, jsi::Object &module) { const auto *name = "nms"; auto fnBody = [](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value { if (count < 3) { - throw jsi::JSError(rt, "Usage: nms(boxes, scores, options)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "Usage: nms(boxes, scores, options)"); } auto boxes = tensor::fromJs(rt, "nms: boxes", args[0], DType::float32, {"N", 4}); @@ -106,7 +113,7 @@ void install_nms(jsi::Runtime &rt, jsi::Object &module) { nmsType = parseNmsType(nmsTypeStr); boxFormat = parseBoxFormat(boxFormatStr); } catch (const std::invalid_argument &e) { - throw jsi::JSError(rt, std::format("nms: {}", e.what())); + throw RnExecuTorchException(RnExecuTorchErrorCode::Unknown, std::format("nms: {}", e.what())); } std::int32_t numAnchors = scores->shape_[0]; @@ -204,14 +211,14 @@ void install_nms(jsi::Runtime &rt, jsi::Object &module) { } }; - module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 3, fnBody)); + module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 3, error::guarded(fnBody))); } void install_restrictToBox(jsi::Runtime &rt, jsi::Object &module) { const auto *name = "restrictToBox"; auto fnBody = [](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value { if (count != 4) { - throw jsi::JSError(rt, "Usage: restrictToBox(src, dst, boxTuple, format)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "Usage: restrictToBox(src, dst, boxTuple, format)"); } auto src = tensor::fromJs(rt, "restrictToBox: src", args[0], std::nullopt, {"H", "W", "C"}); @@ -223,8 +230,8 @@ void install_restrictToBox(jsi::Runtime &rt, jsi::Object &module) { auto boxVec = conversions::asVector(rt, "restrictToBox: boxTuple", args[2]); if (boxVec.size() != kBoxCoords) { - throw jsi::JSError(rt, std::format("restrictToBox: boxTuple must contain exactly 4 coordinates (got {})", - boxVec.size())); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("restrictToBox: boxTuple must contain exactly 4 coordinates (got {})", + boxVec.size())); } auto boxFormatStr = conversions::asType(rt, "restrictToBox: format", args[3]); @@ -232,7 +239,7 @@ void install_restrictToBox(jsi::Runtime &rt, jsi::Object &module) { try { boxFormat = parseBoxFormat(boxFormatStr); } catch (const std::invalid_argument &e) { - throw jsi::JSError(rt, std::format("restrictToBox: {}", e.what())); + throw RnExecuTorchException(RnExecuTorchErrorCode::Unknown, std::format("restrictToBox: {}", e.what())); } auto [xmin, ymin, xmax, ymax] = decodeToXyxy(std::span(boxVec), boxFormat); @@ -267,13 +274,13 @@ void install_restrictToBox(jsi::Runtime &rt, jsi::Object &module) { srcMat(roi).copyTo(dstMat(roi)); } } catch (const std::exception &e) { - throw jsi::JSError(rt, "restrictToBox: " + std::string(e.what())); + throw RnExecuTorchException(RnExecuTorchErrorCode::Unknown, "restrictToBox: " + std::string(e.what())); } return jsi::Value(rt, args[1]); }; - module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 4, fnBody)); + module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 4, error::guarded(fnBody))); } } // namespace rnexecutorch::extensions::cv::box_ops diff --git a/packages/react-native-executorch/cpp/extensions/cv/image_ops.cpp b/packages/react-native-executorch/cpp/extensions/cv/image_ops.cpp index b9bad0be55..ea5b9f6105 100644 --- a/packages/react-native-executorch/cpp/extensions/cv/image_ops.cpp +++ b/packages/react-native-executorch/cpp/extensions/cv/image_ops.cpp @@ -17,6 +17,13 @@ #include +#include "core/error.h" +namespace { +namespace error = rnexecutorch::core::error; +using rnexecutorch::core::error::RnExecuTorchErrorCode; +using rnexecutorch::core::error::RnExecuTorchException; +} // namespace + namespace rnexecutorch::extensions::cv::image_ops { namespace jsi = facebook::jsi; namespace tensor = rnexecutorch::core::tensor; @@ -41,9 +48,9 @@ int interpToFlag(const std::string &interp) { if (interp == "lanczos") { return ::cv::INTER_LANCZOS4; } - throw std::invalid_argument(std::format("unsupported interpolation '{}'. Expected" - " 'nearest', 'area', 'linear', 'cubic', or 'lanczos'", - interp)); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("unsupported interpolation '{}'. Expected" + " 'nearest', 'area', 'linear', 'cubic', or 'lanczos'", + interp)); } struct FitBox { @@ -66,7 +73,7 @@ void install_resize(jsi::Runtime &rt, jsi::Object &module) { const auto *name = "resize"; auto fnBody = [](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value { if (count != 3) { - throw jsi::JSError(rt, "Usage: resize(src, dst, options)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "Usage: resize(src, dst, options)"); } auto src = tensor::fromJs(rt, "resize: src", args[0], std::nullopt, {"H", "W", "C"}); @@ -93,7 +100,7 @@ void install_resize(jsi::Runtime &rt, jsi::Object &module) { cvType = CV_MAKETYPE(dtypeToCvDepth(src->dtype_), channels); interpFlag = interpToFlag(interp); } catch (const std::exception &e) { - throw jsi::JSError(rt, "resize: " + std::string(e.what())); + throw RnExecuTorchException(RnExecuTorchErrorCode::Unknown, "resize: " + std::string(e.what())); } try { @@ -115,18 +122,18 @@ void install_resize(jsi::Runtime &rt, jsi::Object &module) { ::cv::resize(srcMat, scaled, ::cv::Size(fit.w, fit.h), 0, 0, interpFlag); scaled(::cv::Rect(fit.offX, fit.offY, dstW, dstH)).copyTo(dstMat); } else { - throw jsi::JSError(rt, "resize: unknown mode '" + mode + "'. Use 'stretch', 'letterbox', or 'crop'"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "resize: unknown mode '" + mode + "'. Use 'stretch', 'letterbox', or 'crop'"); } - } catch (const jsi::JSError &) { + } catch (const RnExecuTorchException &) { throw; } catch (const std::exception &e) { - throw jsi::JSError(rt, "resize: " + std::string(e.what())); + throw RnExecuTorchException(RnExecuTorchErrorCode::Unknown, "resize: " + std::string(e.what())); } return jsi::Value(rt, args[1]); }; - module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 3, fnBody)); + module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 3, error::guarded(fnBody))); } namespace { @@ -191,9 +198,9 @@ int codeToColorConversionFlag(const std::string &code) { if (code == "GRAY2BGRA") { return ::cv::COLOR_GRAY2BGRA; } - throw std::invalid_argument(std::format("cvtColor: unsupported color conversion code '{}'." - " Common values are 'RGB2BGR', 'BGR2RGB', 'RGBA2RGB', 'RGB2GRAY', etc.", - code)); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("cvtColor: unsupported color conversion code '{}'." + " Common values are 'RGB2BGR', 'BGR2RGB', 'RGBA2RGB', 'RGB2GRAY', etc.", + code)); } } // namespace @@ -201,7 +208,7 @@ void install_cvtColor(jsi::Runtime &rt, jsi::Object &module) { const auto *name = "cvtColor"; auto fnBody = [](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value { if (count != 3) { - throw jsi::JSError(rt, "Usage: cvtColor(src, dst, code)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "Usage: cvtColor(src, dst, code)"); } auto src = tensor::fromJs(rt, "cvtColor: src", args[0], std::nullopt, {"H", "W", "C"}); @@ -231,20 +238,20 @@ void install_cvtColor(jsi::Runtime &rt, jsi::Object &module) { ::cv::cvtColor(srcMat, dstMat, flag); } catch (const std::exception &e) { - throw jsi::JSError(rt, "cvtColor: " + std::string(e.what())); + throw RnExecuTorchException(RnExecuTorchErrorCode::Unknown, "cvtColor: " + std::string(e.what())); } return jsi::Value(rt, args[1]); }; - module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 3, fnBody)); + module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 3, error::guarded(fnBody))); } void install_toChannelsFirst(jsi::Runtime &rt, jsi::Object &module) { const auto *name = "toChannelsFirst"; auto fnBody = [](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value { if (count != 2) { - throw jsi::JSError(rt, "Usage: toChannelsFirst(src, dst)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "Usage: toChannelsFirst(src, dst)"); } auto src = tensor::fromJs(rt, "toChannelsFirst: src", args[0], std::nullopt, {"H", "W", "C"}); @@ -275,20 +282,20 @@ void install_toChannelsFirst(jsi::Runtime &rt, jsi::Object &module) { std::ranges::copy(plane, dstBytes.subspan(i * planeBytes, planeBytes).begin()); } } catch (const std::exception &e) { - throw jsi::JSError(rt, "toChannelsFirst: " + std::string(e.what())); + throw RnExecuTorchException(RnExecuTorchErrorCode::Unknown, "toChannelsFirst: " + std::string(e.what())); } return jsi::Value(rt, args[1]); }; - module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 2, fnBody)); + module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 2, error::guarded(fnBody))); } void install_toChannelsLast(jsi::Runtime &rt, jsi::Object &module) { const auto *name = "toChannelsLast"; auto fnBody = [](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value { if (count != 2) { - throw jsi::JSError(rt, "Usage: toChannelsLast(src, dst)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "Usage: toChannelsLast(src, dst)"); } auto src = tensor::fromJs(rt, "toChannelsLast: src", args[0], std::nullopt, {"C", "H", "W"}); @@ -318,20 +325,20 @@ void install_toChannelsLast(jsi::Runtime &rt, jsi::Object &module) { ::cv::Mat dstMat(srcH, srcW, CV_MAKETYPE(cvDepth, srcC), dst->data_.get()); ::cv::merge(channels, dstMat); } catch (const std::exception &e) { - throw jsi::JSError(rt, "toChannelsLast: " + std::string(e.what())); + throw RnExecuTorchException(RnExecuTorchErrorCode::Unknown, "toChannelsLast: " + std::string(e.what())); } return jsi::Value(rt, args[1]); }; - module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 2, fnBody)); + module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 2, error::guarded(fnBody))); } void install_normalize(jsi::Runtime &rt, jsi::Object &module) { const auto *name = "normalize"; auto fnBody = [](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value { if (count != 3) { - throw jsi::JSError(rt, "Usage: normalize(src, dst, options)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "Usage: normalize(src, dst, options)"); } auto src = tensor::fromJs(rt, "normalize: src", args[0], std::nullopt, {"C", "H", "W"}); @@ -355,7 +362,7 @@ void install_normalize(jsi::Runtime &rt, jsi::Object &module) { } else { auto arr = conversions::asVector(rt, std::format("normalize: options.{}", optName), val); if (arr.size() != static_cast(c)) { - throw jsi::JSError(rt, std::format("normalize: options.{} array length must be exactly equal to channels", optName)); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("normalize: options.{} array length must be exactly equal to channels", optName)); } result = std::move(arr); } @@ -387,20 +394,20 @@ void install_normalize(jsi::Runtime &rt, jsi::Object &module) { srcChannel.convertTo(dstChannel, dstDepthType, alpha[ch], beta[ch]); } } catch (const std::exception &e) { - throw jsi::JSError(rt, "normalize: " + std::string(e.what())); + throw RnExecuTorchException(RnExecuTorchErrorCode::Unknown, "normalize: " + std::string(e.what())); } return jsi::Value(rt, args[1]); }; - module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 3, fnBody)); + module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 3, error::guarded(fnBody))); } void install_applyColormap(jsi::Runtime &rt, jsi::Object &module) { const auto *name = "applyColormap"; auto fnBody = [](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value { if (count != 3) { - throw jsi::JSError(rt, "Usage: applyColormap(src, dst, colormap)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "Usage: applyColormap(src, dst, colormap)"); } auto colormapArray = conversions::asType(rt, "applyColormap: colormap", args[2]); @@ -418,8 +425,8 @@ void install_applyColormap(jsi::Runtime &rt, jsi::Object &module) { for (size_t i = 0; i < numColors; ++i) { auto colorVec = conversions::asVector(rt, "applyColormap: colormap entry", colormapArray.getValueAtIndex(rt, i)); if (colorVec.size() != numRgbaChannels) { - throw jsi::JSError(rt, std::format("applyColormap: colormap entry must be an RGBA color array of size 4 (got size {})", - colorVec.size())); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("applyColormap: colormap entry must be an RGBA color array of size 4 (got size {})", + colorVec.size())); } for (size_t c = 0; c < numRgbaChannels; ++c) { lut[i][c] = colorVec[c]; @@ -432,9 +439,9 @@ void install_applyColormap(jsi::Runtime &rt, jsi::Object &module) { for (size_t i = 0; i < srcData.size(); ++i) { const int32_t idx = srcData[i]; if (idx < 0 || std::cmp_greater_equal(idx, numColors)) { - throw jsi::JSError(rt, "applyColormap: tensor contains class index (" + - std::to_string(idx) + ") that exceeds provided colormap size (" + - std::to_string(numColors) + ")"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "applyColormap: tensor contains class index (" + + std::to_string(idx) + ") that exceeds provided colormap size (" + + std::to_string(numColors) + ")"); } std::ranges::copy(lut[static_cast(idx)], dstData.subspan(i * numRgbaChannels, numRgbaChannels).begin()); @@ -442,6 +449,6 @@ void install_applyColormap(jsi::Runtime &rt, jsi::Object &module) { return jsi::Value(rt, args[1]); }; - module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 3, fnBody)); + module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 3, error::guarded(fnBody))); } } // namespace rnexecutorch::extensions::cv::image_ops diff --git a/packages/react-native-executorch/cpp/extensions/cv/utils.h b/packages/react-native-executorch/cpp/extensions/cv/utils.h index 132663a0a3..3b76b6583c 100644 --- a/packages/react-native-executorch/cpp/extensions/cv/utils.h +++ b/packages/react-native-executorch/cpp/extensions/cv/utils.h @@ -1,6 +1,7 @@ #pragma once #include "core/dtype.h" +#include "core/error.h" #include #include @@ -24,7 +25,7 @@ inline int dtypeToCvDepth(rnexecutorch::core::types::DType dtype) { case rnexecutorch::core::types::DType::int64: break; } - throw std::invalid_argument("unsupported dtype"); + throw core::error::RnExecuTorchException(core::error::RnExecuTorchErrorCode::InvalidArgument, "unsupported dtype"); } } // namespace rnexecutorch::extensions::cv diff --git a/packages/react-native-executorch/cpp/extensions/math/operations.cpp b/packages/react-native-executorch/cpp/extensions/math/operations.cpp index 0ecb9caa28..5512f034b6 100644 --- a/packages/react-native-executorch/cpp/extensions/math/operations.cpp +++ b/packages/react-native-executorch/cpp/extensions/math/operations.cpp @@ -12,6 +12,13 @@ #include "core/tensor.h" #include "core/tensor_helpers.h" +#include "core/error.h" +namespace { +namespace error = rnexecutorch::core::error; +using rnexecutorch::core::error::RnExecuTorchErrorCode; +using rnexecutorch::core::error::RnExecuTorchException; +} // namespace + namespace rnexecutorch::extensions::math { namespace jsi = facebook::jsi; namespace conversions = rnexecutorch::core::conversions; @@ -23,7 +30,7 @@ void install_sigmoid(jsi::Runtime &rt, jsi::Object &module) { const auto *name = "sigmoid"; auto fnBody = [](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value { if (count != 2) { - throw jsi::JSError(rt, "Usage: sigmoid(src, dst)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "Usage: sigmoid(src, dst)"); } auto src = tensor::fromJs(rt, "sigmoid: src", args[0], DType::float32, std::nullopt); @@ -43,14 +50,14 @@ void install_sigmoid(jsi::Runtime &rt, jsi::Object &module) { return jsi::Value(rt, args[1]); }; - module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 2, fnBody)); + module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 2, error::guarded(fnBody))); } void install_softmax(jsi::Runtime &rt, jsi::Object &module) { const auto *name = "softmax"; auto fnBody = [](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value { if (count != 3) { - throw jsi::JSError(rt, "Usage: softmax(src, dst, axis)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "Usage: softmax(src, dst, axis)"); } auto src = tensor::fromJs(rt, "softmax: src", args[0], DType::float32, std::nullopt); @@ -61,7 +68,7 @@ void install_softmax(jsi::Runtime &rt, jsi::Object &module) { auto dstLock = tensor::tryLockUnique(rt, "softmax: dst", dst); if (src->shape_.empty()) { - throw jsi::JSError(rt, "softmax: src must have at least one dimension"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "softmax: src must have at least one dimension"); } int axis = conversions::asType(rt, "softmax: axis", args[2]); @@ -73,8 +80,8 @@ void install_softmax(jsi::Runtime &rt, jsi::Object &module) { axis += rank; } if (axis < 0 || axis >= rank) { - throw jsi::JSError(rt, std::format("softmax: axis {} out of range for tensor of rank {}", - axis, rank)); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("softmax: axis {} out of range for tensor of rank {}", + axis, rank)); } const auto axisIdx = static_cast(axis); @@ -83,7 +90,7 @@ void install_softmax(jsi::Runtime &rt, jsi::Object &module) { const auto axisDim = static_cast(src->shape_[axisIdx]); if (axisDim == 0) { - throw jsi::JSError(rt, "softmax: axis dimension must be greater than zero"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "softmax: axis dimension must be greater than zero"); } size_t outer = 1; @@ -127,14 +134,14 @@ void install_softmax(jsi::Runtime &rt, jsi::Object &module) { return jsi::Value(rt, args[1]); }; - module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 3, fnBody)); + module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 3, error::guarded(fnBody))); } void install_argmax(jsi::Runtime &rt, jsi::Object &module) { const auto *name = "argmax"; auto fnBody = [](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value { if (count != 3) { - throw jsi::JSError(rt, "Usage: argmax(src, dst, axis)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "Usage: argmax(src, dst, axis)"); } auto src = tensor::fromJs(rt, "argmax: src", args[0], DType::float32, std::nullopt); @@ -153,22 +160,22 @@ void install_argmax(jsi::Runtime &rt, jsi::Object &module) { axis += rank; } if (axis < 0 || axis >= rank) { - throw jsi::JSError(rt, std::format("argmax: axis {} out of range for tensor of rank {}", - axis, rank)); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("argmax: axis {} out of range for tensor of rank {}", + axis, rank)); } const auto axisIdx = static_cast(axis); auto dstExpectedShape = src->shape_; dstExpectedShape[axisIdx] = 1; if (dst->shape_ != dstExpectedShape) { - throw jsi::JSError(rt, "argmax: dst shape must match src shape but with axis dimension 1"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "argmax: dst shape must match src shape but with axis dimension 1"); } const std::span srcData(reinterpret_cast(src->data_.get()), src->numel_); const auto axisDim = static_cast(src->shape_[axisIdx]); if (axisDim == 0) { - throw jsi::JSError(rt, "argmax: axis dimension must be greater than zero"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "argmax: axis dimension must be greater than zero"); } size_t outer = 1; @@ -207,14 +214,14 @@ void install_argmax(jsi::Runtime &rt, jsi::Object &module) { return jsi::Value(rt, args[1]); }; - module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 3, fnBody)); + module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 3, error::guarded(fnBody))); } void install_threshold(jsi::Runtime &rt, jsi::Object &module) { const auto *name = "threshold"; auto fnBody = [](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value { if (count != 3) { - throw jsi::JSError(rt, "Usage: threshold(src, dst, threshold)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "Usage: threshold(src, dst, threshold)"); } auto src = tensor::fromJs(rt, "threshold: src", args[0], DType::float32, std::nullopt); @@ -236,7 +243,7 @@ void install_threshold(jsi::Runtime &rt, jsi::Object &module) { return jsi::Value(rt, args[1]); }; - module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 3, fnBody)); + module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 3, error::guarded(fnBody))); } } // namespace rnexecutorch::extensions::math diff --git a/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.cpp b/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.cpp index 2469960a9b..0cd32aa68d 100644 --- a/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.cpp +++ b/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.cpp @@ -9,6 +9,13 @@ #include +#include "core/error.h" +namespace { +namespace error = rnexecutorch::core::error; +using rnexecutorch::core::error::RnExecuTorchErrorCode; +using rnexecutorch::core::error::RnExecuTorchException; +} // namespace + namespace rnexecutorch::extensions::nlp::tokenizer { namespace jsi = facebook::jsi; namespace conversions = rnexecutorch::core::conversions; @@ -49,9 +56,9 @@ std::string toString(tokenizers::Error error) { } template -T unwrap(jsi::Runtime &rt, const std::string &ctx, tokenizers::Result result) { +T unwrap(const std::string &ctx, tokenizers::Result result) { if (!result.ok()) { - throw jsi::JSError(rt, std::format("{}: {}", ctx, toString(result.error()))); + throw RnExecuTorchException(RnExecuTorchErrorCode::ExecutionFailed, std::format("{}: {}", ctx, toString(result.error()))); } return std::move(result.get()); } @@ -62,19 +69,19 @@ TokenizerHostObject::TokenizerHostObject(std::string tokenizerPath) tokenizer_(std::make_unique()) { auto error = tokenizer_->load(tokenizerPath_); if (error != tokenizers::Error::Ok) { - throw std::runtime_error(std::format("Failed to load tokenizer from '{}': {}", - tokenizerPath_, toString(error))); + throw RnExecuTorchException(RnExecuTorchErrorCode::ExecutionFailed, std::format("Failed to load tokenizer from '{}': {}", + tokenizerPath_, toString(error))); } } -std::unique_lock TokenizerHostObject::tryLockUnique(jsi::Runtime &rt, +std::unique_lock TokenizerHostObject::tryLockUnique(jsi::Runtime & /*rt*/, std::string_view context) { std::unique_lock lock(mutex_, std::try_to_lock); if (!lock.owns_lock()) { - throw jsi::JSError(rt, std::format("{} is currently in use", context)); + throw RnExecuTorchException(RnExecuTorchErrorCode::ResourceBusy, std::format("{} is currently in use", context)); } if (!tokenizer_) { - throw jsi::JSError(rt, std::format("{} has been disposed", context)); + throw RnExecuTorchException(RnExecuTorchErrorCode::ResourceDisposed, std::format("{} has been disposed", context)); } return lock; } @@ -90,26 +97,26 @@ jsi::Value TokenizerHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &nam auto self = shared_from_this(); auto fnBody = [self](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value { if (count != 1) { - throw jsi::JSError(rt, "encode: Usage: encode(text)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "encode: Usage: encode(text)"); } auto lock = self->tryLockUnique(rt, "encode: Tokenizer"); auto text = conversions::asType(rt, "encode: text", args[0]); - auto tokens = unwrap(rt, "encode: Failed to encode input", + auto tokens = unwrap("encode: Failed to encode input", self->tokenizer_->encode(text, kNumAddedBosTokens, kNumAddedEosTokens)); // Token ids are non-negative and well below 2^31, so int32 is lossless. return conversions::toJsiTypedArray(rt, std::vector(tokens.begin(), tokens.end())); }; - return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "encode"), 1, fnBody); + return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "encode"), 1, error::guarded(fnBody)); } if (nameStr == "decode") { auto self = shared_from_this(); auto fnBody = [self](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value { if (count < 1 || count > 2) { - throw jsi::JSError(rt, "decode: Usage: decode(tokens, skipSpecialTokens?)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "decode: Usage: decode(tokens, skipSpecialTokens?)"); } // skipSpecialTokens is optional and defaults to true. @@ -127,82 +134,82 @@ jsi::Value TokenizerHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &nam return jsi::String::createFromUtf8(rt, ""); } - auto text = unwrap(rt, "decode: Failed to decode tokens", + auto text = unwrap("decode: Failed to decode tokens", self->tokenizer_->decode(tokens, skipSpecialTokens)); return jsi::String::createFromUtf8(rt, text); }; - return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "decode"), 1, fnBody); + return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "decode"), 1, error::guarded(fnBody)); } if (nameStr == "getVocabSize") { auto self = shared_from_this(); auto fnBody = [self](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value * /*args*/, size_t count) -> jsi::Value { if (count != 0) { - throw jsi::JSError(rt, "getVocabSize: Usage: getVocabSize()"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "getVocabSize: Usage: getVocabSize()"); } auto lock = self->tryLockUnique(rt, "getVocabSize: Tokenizer"); return static_cast(self->tokenizer_->vocab_size()); }; - return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "getVocabSize"), 0, fnBody); + return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "getVocabSize"), 0, error::guarded(fnBody)); } if (nameStr == "idToToken") { auto self = shared_from_this(); auto fnBody = [self](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value { if (count != 1) { - throw jsi::JSError(rt, "idToToken: Usage: idToToken(id)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "idToToken: Usage: idToToken(id)"); } auto lock = self->tryLockUnique(rt, "idToToken: Tokenizer"); auto tokenId = conversions::asType(rt, "idToToken: id", args[0]); - auto token = unwrap(rt, "idToToken: Failed to convert id to token", + auto token = unwrap("idToToken: Failed to convert id to token", self->tokenizer_->id_to_piece(tokenId)); return jsi::String::createFromUtf8(rt, token); }; - return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "idToToken"), 1, fnBody); + return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "idToToken"), 1, error::guarded(fnBody)); } if (nameStr == "tokenToId") { auto self = shared_from_this(); auto fnBody = [self](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value { if (count != 1) { - throw jsi::JSError(rt, "tokenToId: Usage: tokenToId(token)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "tokenToId: Usage: tokenToId(token)"); } auto lock = self->tryLockUnique(rt, "tokenToId: Tokenizer"); auto token = conversions::asType(rt, "tokenToId: token", args[0]); - auto tokenId = unwrap(rt, "tokenToId: Failed to convert token to id", + auto tokenId = unwrap("tokenToId: Failed to convert token to id", self->tokenizer_->piece_to_id(token)); return static_cast(tokenId); }; - return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "tokenToId"), 1, fnBody); + return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "tokenToId"), 1, error::guarded(fnBody)); } if (nameStr == "dispose") { auto self = shared_from_this(); - auto fnBody = [self](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value * /*args*/, size_t count) -> jsi::Value { + auto fnBody = [self](jsi::Runtime & /*rt*/, const jsi::Value & /*thisVal*/, const jsi::Value * /*args*/, size_t count) -> jsi::Value { if (count != 0) { - throw jsi::JSError(rt, "dispose: Usage: dispose()"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "dispose: Usage: dispose()"); } std::unique_lock lock(self->mutex_); if (!self->tokenizer_) { - throw jsi::JSError(rt, "dispose: Tokenizer has already been disposed"); + throw RnExecuTorchException(RnExecuTorchErrorCode::ResourceDisposed, "dispose: Tokenizer has already been disposed"); } self->tokenizer_.reset(); return jsi::Value::undefined(); }; - return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "dispose"), 0, fnBody); + return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "dispose"), 0, error::guarded(fnBody)); } return jsi::Value::undefined(); @@ -224,7 +231,7 @@ void install_loadTokenizer(jsi::Runtime &rt, jsi::Object &module) { const auto *name = "loadTokenizer"; auto fnBody = [](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value { if (count != 1) { - throw jsi::JSError(rt, "loadTokenizer: Usage: loadTokenizer(path)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "loadTokenizer: Usage: loadTokenizer(path)"); } auto tokenizerPath = conversions::asType(rt, "loadTokenizer: path", args[0]); @@ -232,10 +239,10 @@ void install_loadTokenizer(jsi::Runtime &rt, jsi::Object &module) { auto tokenizerInstance = std::make_shared(tokenizerPath); return jsi::Object::createFromHostObject(rt, tokenizerInstance); } catch (const std::exception &e) { - throw jsi::JSError(rt, std::format("loadTokenizer: {}", e.what())); + throw RnExecuTorchException(RnExecuTorchErrorCode::ExecutionFailed, std::format("loadTokenizer: {}", e.what())); } }; - auto fn = jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 1, fnBody); + auto fn = jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 1, error::guarded(fnBody)); module.setProperty(rt, name, fn); } diff --git a/packages/react-native-executorch/cpp/extensions/speech/operations.cpp b/packages/react-native-executorch/cpp/extensions/speech/operations.cpp index 7d8eb61359..56a85dd56d 100644 --- a/packages/react-native-executorch/cpp/extensions/speech/operations.cpp +++ b/packages/react-native-executorch/cpp/extensions/speech/operations.cpp @@ -9,6 +9,13 @@ #include "core/tensor.h" #include "core/tensor_helpers.h" +#include "core/error.h" +namespace { +namespace error = rnexecutorch::core::error; +using rnexecutorch::core::error::RnExecuTorchErrorCode; +using rnexecutorch::core::error::RnExecuTorchException; +} // namespace + namespace rnexecutorch::extensions::speech { namespace jsi = facebook::jsi; namespace conversions = rnexecutorch::core::conversions; @@ -19,7 +26,7 @@ void install_extractFrames(jsi::Runtime &rt, jsi::Object &module) { const auto *name = "extractFrames"; auto fnBody = [](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value { if (count != 4) { - throw jsi::JSError(rt, "Usage: extractFrames(waveform, hann, dst, options)"); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "Usage: extractFrames(waveform, hann, dst, options)"); } auto waveform = tensor::fromJs(rt, "extractFrames: waveform", args[0], DType::float32, {"length"}); @@ -42,20 +49,20 @@ void install_extractFrames(jsi::Runtime &rt, jsi::Object &module) { const auto chunkFrames = static_cast(dst->shape_[0]); const auto fftLength = static_cast(dst->shape_[1]); if (frameLength > fftLength) { - throw jsi::JSError(rt, std::format("extractFrames: hann length ({}) exceeds dst fftLength ({})", - frameLength, fftLength)); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("extractFrames: hann length ({}) exceeds dst fftLength ({})", + frameLength, fftLength)); } if (numFrames > chunkFrames) { - throw jsi::JSError(rt, std::format("extractFrames: numFrames ({}) exceeds dst frame capacity ({})", - numFrames, chunkFrames)); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("extractFrames: numFrames ({}) exceeds dst frame capacity ({})", + numFrames, chunkFrames)); } if (numFrames > 0) { const uint64_t lastSample = (numFrames - 1) * hopLength + frameLength - 1; if (lastSample >= waveform->numel_) { - throw jsi::JSError(rt, std::format("extractFrames: frame window (last sample index {})" - " exceeds waveform bounds (numel {})", - lastSample, waveform->numel_)); + throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, std::format("extractFrames: frame window (last sample index {})" + " exceeds waveform bounds (numel {})", + lastSample, waveform->numel_)); } } @@ -91,6 +98,6 @@ void install_extractFrames(jsi::Runtime &rt, jsi::Object &module) { return jsi::Value(rt, args[2]); }; - module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 4, fnBody)); + module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 4, error::guarded(fnBody))); } } // namespace rnexecutorch::extensions::speech diff --git a/packages/react-native-executorch/src/core/error.ts b/packages/react-native-executorch/src/core/error.ts new file mode 100644 index 0000000000..048e722e91 --- /dev/null +++ b/packages/react-native-executorch/src/core/error.ts @@ -0,0 +1,126 @@ +/** + * Errors raised by React Native ExecuTorch. + * + * This module is the source of truth for the error contract; `cpp/core/error.h` + * mirrors it by hand, the same way the rest of the TS/JSI interface is mirrored. + * + * Errors are plain `Error` objects with extra fields rather than a class. + * Worklet runtimes are separate JavaScript runtimes and a value thrown on one + * does not keep its class identity or prototype chain when it travels to + * another, so a class would only work on some of the paths that can throw. + * @packageDocumentation + */ + +/** + * Every error code the library can raise. + * + * Deliberately coarse. A distinct code earns its place only when an app can + * genuinely recover differently from it (retry a download, wait for a busy + * resource, re-create a disposed one). Everything else is a category that + * exists so crash reporters can group failures, and the detail lives in the + * message. + * @category Errors + */ +export const VALID_ERROR_CODES = [ + 'LOAD_FAILED', + 'EXECUTION_FAILED', + 'SCHEMA_MISMATCH', + 'INVALID_ARGUMENT', + 'INVALID_STATE', + 'RESOURCE_DISPOSED', + 'RESOURCE_BUSY', + 'DOWNLOAD_FAILED', + 'DOWNLOAD_ABORTED', + 'UNKNOWN', +] as const; + +/** + * Machine-readable classification of an {@link RnExecuTorchError}. Branch on + * this rather than on the message, which is written for humans and can be + * reworded in any release. + * @category Errors + */ +export type RnExecuTorchErrorCode = (typeof VALID_ERROR_CODES)[number]; + +/** + * An error raised by React Native ExecuTorch: a standard `Error` carrying a + * {@link RnExecuTorchErrorCode}. + * @category Errors + * @typeParam C The specific code, narrowed by {@link isRnExecuTorchError}. + */ +export type RnExecuTorchError = Error & { + name: 'RnExecuTorchError'; + code: C; + /** + * The raw `executorch::runtime::Error` value when the failure came out of the + * ExecuTorch runtime, absent otherwise. Diagnostic only: upstream's code space + * moves independently of ours. + */ + etRuntimeErrorCode?: number; +}; + +/** + * Builds an {@link RnExecuTorchError}. Safe to call from anywhere, including + * inside a worklet. + * @category Errors + * @param code The classification to attach. + * @param message A human-readable description. Include the offending values. + * @param etRuntimeErrorCode The raw ExecuTorch runtime error, when there is one. + * @returns An `Error` carrying `code`. + */ +export function RnExecuTorchError( + code: C, + message: string, + etRuntimeErrorCode?: number +): RnExecuTorchError { + 'worklet'; + const err = new Error(message) as RnExecuTorchError; + err.name = 'RnExecuTorchError'; + err.code = code; + + if (etRuntimeErrorCode !== undefined) { + err.etRuntimeErrorCode = etRuntimeErrorCode; + } + + return err; +} + +/** + * Narrows an unknown caught value to an {@link RnExecuTorchError}, optionally + * requiring a specific code. + * + * Duck-typed so it holds for errors that crossed a worklet or JSI boundary, + * where class identity is gone. + * @category Errors + * @param err The caught value. + * @param code When given, also requires the error to carry exactly this code. + * @returns Whether `err` is an `RnExecuTorchError` (of code `code`, if given). + * @example + * ```typescript + * try { + * await classifier.classify(image); + * } catch (e) { + * if (isRnExecuTorchError(e, 'RESOURCE_BUSY')) return; // a run is in flight + * throw e; + * } + * ``` + */ +export function isRnExecuTorchError( + err: unknown, + code?: C +): err is RnExecuTorchError { + 'worklet'; + if (err === null || typeof err !== 'object') { + return false; + } + if (!('name' in err) || err.name !== 'RnExecuTorchError') { + return false; + } + if (!('code' in err) || typeof err.code !== 'string') { + return false; + } + if (!(VALID_ERROR_CODES as readonly string[]).includes(err.code)) { + return false; + } + return code === undefined || err.code === code; +} diff --git a/packages/react-native-executorch/src/core/runtime.ts b/packages/react-native-executorch/src/core/runtime.ts index ff8edd20bc..d4f4335003 100644 --- a/packages/react-native-executorch/src/core/runtime.ts +++ b/packages/react-native-executorch/src/core/runtime.ts @@ -3,6 +3,7 @@ import { runOnRuntimeAsync, type WorkletRuntime, } from 'react-native-worklets'; +import { isRnExecuTorchError, RnExecuTorchError } from './error'; /** * The default background worklet runtime used for all model execution. @@ -23,8 +24,8 @@ export const defaultWorkletRuntime = createWorkletRuntime({ * * The wrapper serializes arguments, dispatches the worklet to the target * runtime, awaits the result, and re-throws any error thrown inside the worklet - * as a standard JS `Error`. This keeps heavy native operations (model loading, - * tensor computation) off the React Native JS thread. + * as an {@link RnExecuTorchError}. This keeps heavy native operations (model + * loading, tensor computation) off the React Native JS thread. * @category Utils * @typeParam Args The tuple of argument types of `fn`. * @typeParam R The return type of `fn`. @@ -33,7 +34,7 @@ export const defaultWorkletRuntime = createWorkletRuntime({ * @param runtime The worklet runtime to dispatch `fn` to. Defaults to * {@link defaultWorkletRuntime}. * @returns An async function with the same signature as `fn` that resolves to - * `fn`'s return value or rejects with an `Error` if `fn` throws. + * `fn`'s return value or rejects with an `RnExecuTorchError` if `fn` throws. */ export function wrapAsync( fn: (...args: Args) => R, @@ -47,13 +48,36 @@ export function wrapAsync( try { return { ok: true, value: fn(...argsArray) }; } catch (e: any) { - return { ok: false, error: e?.message ?? String(e) }; + // Only plain data survives the hop back to the React Native runtime: + // class identity, the prototype chain, and the stack do not. Carry the + // fields needed to rebuild the error on the other side. + let error; + if (isRnExecuTorchError(e)) { + error = { + name: 'RnExecuTorchError', + code: e.code, + message: e.message, + etRuntimeErrorCode: e.etRuntimeErrorCode, + }; + } else { + error = e?.message ?? String(e); + } + return { ok: false, error }; } }, args ); - if (!result.ok) throw new Error(result.error); + if (!result.ok) { + if (isRnExecuTorchError(result.error)) { + throw RnExecuTorchError( + result.error.code, + result.error.message, + result.error.etRuntimeErrorCode + ); + } + throw new Error(result.error); + } return result.value!; }; } diff --git a/packages/react-native-executorch/src/core/schema.ts b/packages/react-native-executorch/src/core/schema.ts index f6a83314c0..f5e4f7c28b 100644 --- a/packages/react-native-executorch/src/core/schema.ts +++ b/packages/react-native-executorch/src/core/schema.ts @@ -73,6 +73,7 @@ * @packageDocumentation */ import type { DType } from './tensor'; +import { RnExecuTorchError } from './error'; // ======================================================== // Parameter specs @@ -260,7 +261,10 @@ export const DynamicDim = (symbol: string): SymbolicDim => { */ export const ConstantDim = (value: number): ConcreteDim => { if (value <= 0 || !Number.isInteger(value)) { - throw new Error(`Invalid value (${value}): must be a positive integer.`); + throw RnExecuTorchError( + 'INVALID_ARGUMENT', + `Invalid value (${value}): must be a positive integer.` + ); } return { kind: 'constant', value }; }; @@ -274,7 +278,7 @@ export const ConstantDim = (value: number): ConcreteDim => { */ export const EnumDim = (choices: readonly number[]): ConcreteDim => { if (choices.some((dim) => dim <= 0 || !Number.isInteger(dim))) { - throw new Error(`Invalid enum choice: must be a positive integer`); + throw RnExecuTorchError('INVALID_ARGUMENT', `Invalid enum choice: must be a positive integer`); } return { kind: 'enum', choices }; }; @@ -291,16 +295,28 @@ export const EnumDim = (choices: readonly number[]): ConcreteDim => { */ export const RangeDim = (min: number, max: number, step?: number): ConcreteDim => { if (min <= 0 || !Number.isInteger(min)) { - throw new Error(`Invalid range min (${min}): must be a positive integer.`); + throw RnExecuTorchError( + 'INVALID_ARGUMENT', + `Invalid range min (${min}): must be a positive integer.` + ); } if (max < min) { - throw new Error(`Invalid range [${min}, ${max}]: max cannot be less than min.`); + throw RnExecuTorchError( + 'INVALID_ARGUMENT', + `Invalid range [${min}, ${max}]: max cannot be less than min.` + ); } if (!Number.isInteger(max)) { - throw new Error(`Invalid range max (${max}): must be a positive integer.`); + throw RnExecuTorchError( + 'INVALID_ARGUMENT', + `Invalid range max (${max}): must be a positive integer.` + ); } if (step !== undefined && (step <= 0 || !Number.isInteger(step))) { - throw new Error(`Invalid range step (${step}): must be a positive integer.`); + throw RnExecuTorchError( + 'INVALID_ARGUMENT', + `Invalid range step (${step}): must be a positive integer.` + ); } return { kind: 'range', range: { min, max, step: step ?? 1 } }; }; @@ -380,21 +396,21 @@ function matchDim( ): void { if (sDim.kind === 'constant' && cDim.kind === 'constant') { if (sDim.value !== cDim.value) { - throw new Error(`${ctx}: Constant dimension mismatch.`); + throw RnExecuTorchError('SCHEMA_MISMATCH', `${ctx}: Constant dimension mismatch.`); } return; } if (sDim.kind === 'range' && cDim.kind === 'range') { if (!rangesEqual(sDim.range, cDim.range)) { - throw new Error(`${ctx}: Range dimension mismatch.`); + throw RnExecuTorchError('SCHEMA_MISMATCH', `${ctx}: Range dimension mismatch.`); } return; } if (sDim.kind === 'enum' && cDim.kind === 'enum') { if (!choicesEqual(sDim.choices, cDim.choices)) { - throw new Error(`${ctx}: Enum dimension mismatch.`); + throw RnExecuTorchError('SCHEMA_MISMATCH', `${ctx}: Enum dimension mismatch.`); } return; } @@ -403,7 +419,10 @@ function matchDim( const bind = bindings.get(sDim.symbol); if (bind) { if (bind.kind !== 'constant' || bind.value !== cDim.value) { - throw new Error(`${ctx}: Symbol '${sDim.symbol}' has inconsistent bindings.`); + throw RnExecuTorchError( + 'SCHEMA_MISMATCH', + `${ctx}: Symbol '${sDim.symbol}' has inconsistent bindings.` + ); } return; } @@ -419,7 +438,10 @@ function matchDim( const consistentEnum = bind.kind === 'enum' && cDim.kind === 'enum' && choicesEqual(bind.choices, cDim.choices); if (!consistentRange && !consistentEnum) { - throw new Error(`${ctx}: Symbol '${sDim.symbol}' has inconsistent bindings.`); + throw RnExecuTorchError( + 'SCHEMA_MISMATCH', + `${ctx}: Symbol '${sDim.symbol}' has inconsistent bindings.` + ); } return; } @@ -427,7 +449,10 @@ function matchDim( return; } - throw new Error(`${ctx}: Cannot match symbolic '${sDim.kind}' with concrete '${cDim.kind}'.`); + throw RnExecuTorchError( + 'SCHEMA_MISMATCH', + `${ctx}: Cannot match symbolic '${sDim.kind}' with concrete '${cDim.kind}'.` + ); } function matchMethodSpecs( @@ -437,10 +462,10 @@ function matchMethodSpecs( ctx: string ): void { if (allowedMethodSpec.inputs.length !== exportedMethodSpec.inputs.length) { - throw new Error(`${ctx}: Input count mismatch.`); + throw RnExecuTorchError('SCHEMA_MISMATCH', `${ctx}: Input count mismatch.`); } if (allowedMethodSpec.outputs.length !== exportedMethodSpec.outputs.length) { - throw new Error(`${ctx}: Output count mismatch.`); + throw RnExecuTorchError('SCHEMA_MISMATCH', `${ctx}: Output count mismatch.`); } const allowedParamSpecs = [...allowedMethodSpec.inputs, ...allowedMethodSpec.outputs]; @@ -455,7 +480,7 @@ function matchMethodSpecs( const exportedParamSpec = exportedParamSpecs[p]!; if (allowedParamSpec.kind !== exportedParamSpec.kind) { - throw new Error(`${paramSpecCtx}: Param spec kind mismatch.`); + throw RnExecuTorchError('SCHEMA_MISMATCH', `${paramSpecCtx}: Param spec kind mismatch.`); } if (allowedParamSpec.kind !== 'Tensor') continue; @@ -464,11 +489,11 @@ function matchMethodSpecs( const exportedTensorSpec = exportedParamSpec as TensorSpec; if (allowedTensorSpec.dtype !== exportedTensorSpec.dtype) { - throw new Error(`${paramSpecCtx}: DType mismatch.`); + throw RnExecuTorchError('SCHEMA_MISMATCH', `${paramSpecCtx}: DType mismatch.`); } if (allowedTensorSpec.shape.length !== exportedTensorSpec.shape.length) { - throw new Error(`${paramSpecCtx}: Rank mismatch.`); + throw RnExecuTorchError('SCHEMA_MISMATCH', `${paramSpecCtx}: Rank mismatch.`); } for (let d = 0; d < allowedTensorSpec.shape.length; ++d) { @@ -486,7 +511,10 @@ function matchModelSpecsSymbols( for (const [methodName, allowedMethodSpec] of Object.entries(allowedModelSpec)) { const exportedMethodSpec = exportedModelSpec[methodName]; if (!exportedMethodSpec) { - throw new Error(`Method '${methodName}' not found in exported model spec.`); + throw RnExecuTorchError( + 'SCHEMA_MISMATCH', + `Method '${methodName}' not found in exported model spec.` + ); } matchMethodSpecs(allowedMethodSpec, exportedMethodSpec, bindings, `Method '${methodName}'`); } @@ -540,12 +568,18 @@ function resolveDim(methodSpec: MethodSpec, ref: DimRe const tensorSpec = tensorSpecs[ref.tensorIdx]; if (!tensorSpec) { - throw new Error(`Invalid DimRef (${JSON.stringify(ref)}): tensor index out of range.`); + throw RnExecuTorchError( + 'SCHEMA_MISMATCH', + `Invalid DimRef (${JSON.stringify(ref)}): tensor index out of range.` + ); } const dim = tensorSpec.shape[ref.dimIdx]; if (!dim) { - throw new Error(`Invalid DimRef (${JSON.stringify(ref)}): dimension index out of range.`); + throw RnExecuTorchError( + 'SCHEMA_MISMATCH', + `Invalid DimRef (${JSON.stringify(ref)}): dimension index out of range.` + ); } return dim; @@ -558,7 +592,10 @@ function matchRuntimeConstraints( for (const [methodName, allowedMethodSpec] of Object.entries(allowedModelSpec)) { const exportedMethodSpec = exportedModelSpec[methodName]; if (!exportedMethodSpec) { - throw new Error(`Method '${methodName}' not found in exported model spec.`); + throw RnExecuTorchError( + 'SCHEMA_MISMATCH', + `Method '${methodName}' not found in exported model spec.` + ); } const unclaimed = [...exportedMethodSpec.runtimeConstraints]; @@ -566,13 +603,19 @@ function matchRuntimeConstraints( for (const [idx, constraint] of allowedMethodSpec.runtimeConstraints.entries()) { const find = unclaimed.findIndex((c) => constraintsEqual(c, constraint)); if (find === -1) { - throw new Error(`Constraint ${idx}: Not declared by the exported model spec.`); + throw RnExecuTorchError( + 'SCHEMA_MISMATCH', + `Constraint ${idx}: Not declared by the exported model spec.` + ); } unclaimed.splice(find, 1); } if (unclaimed.length > 0) { - throw new Error(`'${methodName}': Exported spec declares unexpected runtime constraints`); + throw RnExecuTorchError( + 'SCHEMA_MISMATCH', + `'${methodName}': Exported spec declares unexpected runtime constraints` + ); } } } @@ -593,7 +636,10 @@ function validateSymbolKindConsistency(modelSpec: ModelSpec): void const existing = symbolKinds.get(dim.symbol); if (existing && existing !== dim.kind) { - throw new Error(`Invalid spec: '${dim.symbol}' is used as both 'static' and 'dynamic'.`); + throw RnExecuTorchError( + 'SCHEMA_MISMATCH', + `Invalid spec: '${dim.symbol}' is used as both 'static' and 'dynamic'.` + ); } symbolKinds.set(dim.symbol, dim.kind); } @@ -609,7 +655,7 @@ function validateConstraintCorrectness(modelSpec: ModelSpec): void if (constraint.kind === 'linear') { const [A, B] = constraint.coefficients; if (!Number.isInteger(A) || !Number.isInteger(B)) { - throw new Error(`${ctx}: Coefficients must be integers.`); + throw RnExecuTorchError('SCHEMA_MISMATCH', `${ctx}: Coefficients must be integers.`); } resolveDim(methodSpec, constraint.dimLhs); resolveDim(methodSpec, constraint.dimRhs); @@ -617,7 +663,10 @@ function validateConstraintCorrectness(modelSpec: ModelSpec): void if (constraint.kind === 'equality') { if (constraint.dims.length < 2) { - throw new Error(`${ctx}: Equality requires at least two dimensions.`); + throw RnExecuTorchError( + 'SCHEMA_MISMATCH', + `${ctx}: Equality requires at least two dimensions.` + ); } constraint.dims.forEach((ref) => resolveDim(methodSpec, ref)); } @@ -640,26 +689,41 @@ function validateDimDomains(modelSpec: ModelSpec): void { if (dim.kind === 'constant') { if (dim.value <= 0 || !Number.isInteger(dim.value)) { - throw new Error(`${ctx}: constant dim must be a positive integer.`); + throw RnExecuTorchError( + 'SCHEMA_MISMATCH', + `${ctx}: constant dim must be a positive integer.` + ); } } if (dim.kind === 'range') { if (dim.range.min <= 0 || !Number.isInteger(dim.range.min)) { - throw new Error(`${ctx}: range min must be a positive integer.`); + throw RnExecuTorchError( + 'SCHEMA_MISMATCH', + `${ctx}: range min must be a positive integer.` + ); } if (dim.range.max < dim.range.min || !Number.isInteger(dim.range.max)) { - throw new Error(`${ctx}: range max must be >= min.`); + throw RnExecuTorchError('SCHEMA_MISMATCH', `${ctx}: range max must be >= min.`); } if (dim.range.step <= 0 || !Number.isInteger(dim.range.step)) { - throw new Error(`${ctx}: range step must be a positive integer.`); + throw RnExecuTorchError( + 'SCHEMA_MISMATCH', + `${ctx}: range step must be a positive integer.` + ); } } if (dim.kind === 'enum') { if (dim.choices.length === 0) { - throw new Error(`${ctx}: enum must have at least one choice.`); + throw RnExecuTorchError( + 'SCHEMA_MISMATCH', + `${ctx}: enum must have at least one choice.` + ); } if (dim.choices.some((c) => c <= 0 || !Number.isInteger(c))) { - throw new Error(`${ctx}: enum choices must be positive integers.`); + throw RnExecuTorchError( + 'SCHEMA_MISMATCH', + `${ctx}: enum choices must be positive integers.` + ); } } } @@ -780,17 +844,23 @@ export function validateSpec { const dim = bindings.get(name); if (!dim) { - throw new Error(`Symbol '${name}' not found in bindings.`); + throw RnExecuTorchError('SCHEMA_MISMATCH', `Symbol '${name}' not found in bindings.`); } if (kind) { if (kind === 'dynamic') { if (dim.kind === 'constant') { - throw new Error(`Symbol '${name}' is 'constant', expected 'dynamic'.`); + throw RnExecuTorchError( + 'SCHEMA_MISMATCH', + `Symbol '${name}' is 'constant', expected 'dynamic'.` + ); } return dim; } if (dim.kind !== kind) { - throw new Error(`Symbol '${name}' is '${dim.kind}', expected '${kind}'.`); + throw RnExecuTorchError( + 'SCHEMA_MISMATCH', + `Symbol '${name}' is '${dim.kind}', expected '${kind}'.` + ); } } if (dim.kind === 'constant') return dim.value; @@ -820,5 +890,8 @@ export function validateSpec( const outShape = { batched: [1, N], unbatched: [N] }[variant]; if (modelOpts.labels.length !== N) { - throw new Error( + throw RnExecuTorchError( + 'INVALID_ARGUMENT', `Classifier labels length (${modelOpts.labels.length}) must match model output dimension (${N}).` ); } @@ -133,7 +135,10 @@ export async function createClassifier( ): Classification[] => { 'worklet'; if (options?.topk !== undefined && options.topk < 0) { - throw new Error(`Classifier topk option must be non-negative`); + throw RnExecuTorchError( + 'INVALID_ARGUMENT', + `Classifier topk option must be non-negative, got ${options.topk}` + ); } const tInput = preprocessor.process(input); model.execute('forward', [tInput], [tLogits]); diff --git a/packages/react-native-executorch/src/extensions/cv/tasks/instanceSegmentation.ts b/packages/react-native-executorch/src/extensions/cv/tasks/instanceSegmentation.ts index 732b57c04c..68bc1fa00e 100644 --- a/packages/react-native-executorch/src/extensions/cv/tasks/instanceSegmentation.ts +++ b/packages/react-native-executorch/src/extensions/cv/tasks/instanceSegmentation.ts @@ -17,6 +17,7 @@ import { type BoundingBox, type BoxFormat, } from '../ops/boxes'; +import { RnExecuTorchError } from '../../../core/error'; export type { BoxFormat }; @@ -215,7 +216,8 @@ export async function createInstanceSegmenter( const label = modelOpts.labels[classIdx]; if (label === undefined) { - throw new Error( + throw RnExecuTorchError( + 'INVALID_ARGUMENT', `InstanceSegmenter: Predicted class index ${classIdx} is ` + `out of bounds for labels array of size ${modelOpts.labels.length}.` ); diff --git a/packages/react-native-executorch/src/extensions/cv/tasks/objectDetection.ts b/packages/react-native-executorch/src/extensions/cv/tasks/objectDetection.ts index 2b93dbe16d..60cff1b272 100644 --- a/packages/react-native-executorch/src/extensions/cv/tasks/objectDetection.ts +++ b/packages/react-native-executorch/src/extensions/cv/tasks/objectDetection.ts @@ -9,6 +9,7 @@ import type { ResizeMode } from '../ops/image'; import type { ImageBuffer } from '../image'; import { createImagePreprocessor, type ImagePreprocessorOptions } from './preprocessing'; import { nms, scaleBox, decodeBox, type BoundingBox, type BoxFormat } from '../ops/boxes'; +import { RnExecuTorchError } from '../../../core/error'; export type { BoxFormat }; @@ -176,7 +177,8 @@ export async function createObjectDetector( const label = modelOpts.labels[classIdx]; if (label === undefined) { - throw new Error( + throw RnExecuTorchError( + 'INVALID_ARGUMENT', `ObjectDetector: Predicted class index ${classIdx} is out of bounds for` + `labels array of size ${modelOpts.labels.length}.` ); diff --git a/packages/react-native-executorch/src/extensions/cv/tasks/preprocessing.ts b/packages/react-native-executorch/src/extensions/cv/tasks/preprocessing.ts index ab3d1c7bac..2530d4efbe 100644 --- a/packages/react-native-executorch/src/extensions/cv/tasks/preprocessing.ts +++ b/packages/react-native-executorch/src/extensions/cv/tasks/preprocessing.ts @@ -12,6 +12,7 @@ import { toChannelsFirst, normalize, } from '../ops/image'; +import { RnExecuTorchError } from '../../../core/error'; /** * Options for configuring the image preprocessor pipeline. @@ -71,7 +72,8 @@ export function createImagePreprocessor( const isRank3 = outputShape.length === 3 && outputShape[0] === numRgbChannels; const isRank4 = outputShape.length === 4 && outputShape[1] === numRgbChannels; if (!isRank3 && !isRank4) { - throw new Error( + throw RnExecuTorchError( + 'SCHEMA_MISMATCH', `preprocessor: got shape [${outputShape}], expected [${numRgbChannels}, H, W] or [1, ${numRgbChannels}, H, W]` ); } diff --git a/packages/react-native-executorch/src/extensions/cv/tasks/semanticSegmentation.ts b/packages/react-native-executorch/src/extensions/cv/tasks/semanticSegmentation.ts index bf73266255..85961ff662 100644 --- a/packages/react-native-executorch/src/extensions/cv/tasks/semanticSegmentation.ts +++ b/packages/react-native-executorch/src/extensions/cv/tasks/semanticSegmentation.ts @@ -16,6 +16,7 @@ import { type InterpolationMethod, } from '../ops/image'; import { sigmoid, argmax } from '../../math'; +import { RnExecuTorchError } from '../../../core/error'; /** * Options for configuring a semantic segmenter preprocessor and label @@ -159,7 +160,8 @@ export async function createSemanticSegmenter( }); if (nClasses > 1 && modelOpts.labels.length !== nClasses) { - throw new Error( + throw RnExecuTorchError( + 'INVALID_ARGUMENT', `Model outputs ${nClasses} classes, but ${modelOpts.labels.length} labels were provided in the configuration.` ); } diff --git a/packages/react-native-executorch/src/extensions/nlp/tasks/textEmbedding.ts b/packages/react-native-executorch/src/extensions/nlp/tasks/textEmbedding.ts index 55df6e9919..d84774515a 100644 --- a/packages/react-native-executorch/src/extensions/nlp/tasks/textEmbedding.ts +++ b/packages/react-native-executorch/src/extensions/nlp/tasks/textEmbedding.ts @@ -6,6 +6,7 @@ import { validateSpec, DynamicDim as Dyn, method, i64, f32, constr } from '../.. import { wrapAsync } from '../../../core/runtime'; import { loadTokenizer } from '../tokenizer'; +import { RnExecuTorchError } from '../../../core/error'; /** * Model configuration required to instantiate a text embedder task runner. @@ -115,7 +116,10 @@ export async function createTextEmbedder( const text = (prompt ?? defaultPrompt ?? '') + input; const ids = tokenizer.encode(text); if (ids.length === 0) { - throw new Error('createTextEmbedder: input tokenized to zero tokens'); + throw RnExecuTorchError( + 'INVALID_ARGUMENT', + 'createTextEmbedder: input tokenized to zero tokens' + ); } const len = Math.min(ids.length, seqLen.max); diff --git a/packages/react-native-executorch/src/extensions/speech/tasks/supertonicTextToSpeech.ts b/packages/react-native-executorch/src/extensions/speech/tasks/supertonicTextToSpeech.ts index b24d44fcd5..92d53ba761 100644 --- a/packages/react-native-executorch/src/extensions/speech/tasks/supertonicTextToSpeech.ts +++ b/packages/react-native-executorch/src/extensions/speech/tasks/supertonicTextToSpeech.ts @@ -17,6 +17,7 @@ import { type SupertonicLanguage, } from '../utils/supertonicUtils'; import { partition } from '../utils/textPartitioner'; +import { RnExecuTorchError } from '../../../core/error'; export { SUPERTONIC_SUPPORTED_LANGUAGES, type SupertonicVoiceStyle, type SupertonicLanguage }; @@ -352,21 +353,27 @@ export async function createSupertonicTextToSpeech( options: SupertonicTtsOptions ): AsyncGenerator { if (isSynthesizing) { - throw new Error('synthesize: Synthesis is already in progress.'); + throw RnExecuTorchError('INVALID_STATE', 'synthesize: Synthesis is already in progress.'); } if (!text || !text.trim()) { - throw new Error('synthesize: Input text cannot be empty.'); + throw RnExecuTorchError('INVALID_ARGUMENT', 'synthesize: Input text cannot be empty.'); } const speed = options.speed ?? DEFAULT_SPEED; if (speed < MIN_SPEED || speed > MAX_SPEED) { - throw new Error(`synthesize: speed must be between ${MIN_SPEED} and ${MAX_SPEED}.`); + throw RnExecuTorchError( + 'INVALID_ARGUMENT', + `synthesize: speed must be between ${MIN_SPEED} and ${MAX_SPEED}.` + ); } const totalSteps = options.totalSteps ?? DEFAULT_TOTAL_STEPS; if (!Number.isInteger(totalSteps) || totalSteps <= 0) { - throw new Error('synthesize: totalSteps must be a positive integer.'); + throw RnExecuTorchError( + 'INVALID_ARGUMENT', + 'synthesize: totalSteps must be a positive integer.' + ); } const voiceStyle = @@ -376,7 +383,10 @@ export async function createSupertonicTextToSpeech( const maxChunkLength = options.maxChunkLength ?? getDefaultMaxChunkLength(options.lang); if (maxChunkLength > MAX_CHUNK_LENGTH_CAP) { - throw new Error(`synthesize: maxChunkLength cannot exceed ${MAX_CHUNK_LENGTH_CAP}.`); + throw RnExecuTorchError( + 'INVALID_ARGUMENT', + `synthesize: maxChunkLength cannot exceed ${MAX_CHUNK_LENGTH_CAP}.` + ); } const cleanedText = cleanText(text); diff --git a/packages/react-native-executorch/src/extensions/speech/tasks/whisperSpeechToText.ts b/packages/react-native-executorch/src/extensions/speech/tasks/whisperSpeechToText.ts index 814ced7612..54fb6582df 100644 --- a/packages/react-native-executorch/src/extensions/speech/tasks/whisperSpeechToText.ts +++ b/packages/react-native-executorch/src/extensions/speech/tasks/whisperSpeechToText.ts @@ -13,6 +13,7 @@ import { type FsmnVadModel, type VadStreamOptions, } from './fsmnVoiceActivityDetection'; +import { RnExecuTorchError } from '../../../core/error'; /** * Sample rate (Hz) Whisper models expect their input waveform to be at. @@ -232,7 +233,10 @@ export async function createWhisperSpeechToText', `<|${options.language}|>`, '<|transcribe|>', '<|notimestamps|>']; if (!isEnglishOnly && tokenizer.tokenToId(`<|${options.language}|>`) === undefined) { - throw new Error(`Language "${options.language}" is not recognized.`); + throw RnExecuTorchError( + 'INVALID_ARGUMENT', + `Language "${options.language}" is not recognized.` + ); } const promptTokens = promptTokenStrings.map((token) => tokenizer.tokenToId(token)); const maxNewTokens = MAX_SEQ_LEN - promptTokens.length; @@ -306,7 +310,7 @@ export async function createWhisperSpeechToText ): AsyncGenerator<{ committed: string; nonCommitted: string }> { if (isStreaming) { - throw new Error('Streaming is already in progress'); + throw RnExecuTorchError('INVALID_STATE', 'Streaming is already in progress'); } isStreaming = true; audioBuffer = new Float32Array(0); diff --git a/packages/react-native-executorch/src/extensions/speech/utils/supertonicUtils.ts b/packages/react-native-executorch/src/extensions/speech/utils/supertonicUtils.ts index 2db7d482d0..a2fd48745a 100644 --- a/packages/react-native-executorch/src/extensions/speech/utils/supertonicUtils.ts +++ b/packages/react-native-executorch/src/extensions/speech/utils/supertonicUtils.ts @@ -1,3 +1,4 @@ +import { RnExecuTorchError } from '../../../core/error'; /** * Ported from supertone-inc/supertonic (MIT License) * Source: https://github.com/supertone-inc/supertonic @@ -127,7 +128,7 @@ export function formatChunk(chunk: string, lang?: string): string { if (lang && lang !== 'na') { if (!SUPERTONIC_SUPPORTED_LANGUAGES.includes(lang as SupertonicLanguage)) { - throw new Error(`formatChunk: Unsupported language: ${lang}`); + throw RnExecuTorchError('INVALID_ARGUMENT', `formatChunk: Unsupported language: ${lang}`); } processed = `<${lang}>${processed}`; } @@ -186,7 +187,7 @@ export type SupertonicVoiceStyle = { export function parseVoiceStyle(json: any): SupertonicVoiceStyle { 'worklet'; if (!json?.style_ttl?.data || !json?.style_dp?.data) { - throw new Error('parseVoiceStyle: Invalid voice style JSON format.'); + throw RnExecuTorchError('LOAD_FAILED', 'parseVoiceStyle: Invalid voice style JSON format.'); } return { styleDp: new Float32Array((json.style_dp.data as number[][][]).flat(2)), diff --git a/packages/react-native-executorch/src/extensions/speech/utils/textPartitioner.ts b/packages/react-native-executorch/src/extensions/speech/utils/textPartitioner.ts index 8530b7e2ae..a047c35b01 100644 --- a/packages/react-native-executorch/src/extensions/speech/utils/textPartitioner.ts +++ b/packages/react-native-executorch/src/extensions/speech/utils/textPartitioner.ts @@ -1,3 +1,4 @@ +import { RnExecuTorchError } from '../../../core/error'; type Tag = 'eos' | 'pause' | 'whitespace'; // Punctuation Regex Patterns @@ -100,7 +101,10 @@ export function partition(text: string, limit: number, options?: PartitionOption } if (limit < MIN_PARTITION_LIMIT) { - throw new Error(`partition: limit ${limit} is below minimum ${MIN_PARTITION_LIMIT}`); + throw RnExecuTorchError( + 'INVALID_ARGUMENT', + `partition: limit ${limit} is below minimum ${MIN_PARTITION_LIMIT}` + ); } const prioritizeInitialTtfa = options?.prioritizeInitialTtfa ?? false; @@ -185,7 +189,10 @@ export function partition(text: string, limit: number, options?: PartitionOption } if (minCost[breakpoints.length - 1] === Infinity) { - throw new Error(`partition: text cannot be divided into chunks of length <= ${limit}`); + throw RnExecuTorchError( + 'INVALID_ARGUMENT', + `partition: text cannot be divided into chunks of length <= ${limit}` + ); } const cuts: number[] = []; diff --git a/packages/react-native-executorch/src/fetcher/fetcher.ts b/packages/react-native-executorch/src/fetcher/fetcher.ts index b5158bd7b9..8bbc9f963a 100644 --- a/packages/react-native-executorch/src/fetcher/fetcher.ts +++ b/packages/react-native-executorch/src/fetcher/fetcher.ts @@ -2,6 +2,7 @@ import { Platform } from 'react-native'; import RNBlobUtil from 'react-native-blob-util'; import * as telemetry from './telemetry'; +import { RnExecuTorchError } from '../core/error'; const IS_ANDROID = Platform.OS === 'android'; @@ -74,17 +75,9 @@ async function remoteSize(url: string): Promise { } } -/** - * Raised when a {@link download} is cancelled through its `signal`. Internal: - * consumers should keep matching on `error.name === 'AbortError'`, which stays - * the standard `AbortSignal` contract. - */ -export class AbortError extends Error { - constructor(message = 'The download was aborted.') { - super(message); - this.name = 'AbortError'; - } -} +// Raised when a download is cancelled through its `signal`. Internal: callers +// match on the DOWNLOAD_ABORTED code via isRnExecuTorchError. +const abortError = () => RnExecuTorchError('DOWNLOAD_ABORTED', 'The download was aborted.'); type OnBytes = (received: number, total: number) => void; @@ -117,7 +110,7 @@ const inFlight = new Map(); // same temporary file — on Android the second one's opening `unlink` would // delete the first one's partially downloaded data. async function downloadUrl(url: string, cb: DownloadUrlCallbacks): Promise { - if (cb.signal?.aborted) throw new AbortError(); + if (cb.signal?.aborted) throw abortError(); const dest = cachePathFor(url); @@ -183,7 +176,7 @@ function joinDownload(entry: InFlightDownload, cb: DownloadUrlCallbacks): Promis if (cb.onBytes) entry.listeners.delete(cb.onBytes); entry.callers -= 1; if (entry.callers === 0) entry.controller.abort(); - reject(new AbortError()); + reject(abortError()); }; cb.signal?.addEventListener('abort', onAbort); @@ -205,7 +198,7 @@ async function downloadUrlViaAndroidDownloadManager( const tmp = `${dest}.downloading`; await RNBlobUtil.fs.unlink(tmp).catch(() => {}); - if (cb.signal?.aborted) throw new AbortError(); + if (cb.signal?.aborted) throw abortError(); const task = RNBlobUtil.config({ addAndroidDownloads: { @@ -232,7 +225,7 @@ async function downloadUrlViaAndroidDownloadManager( await task; } catch (e) { await RNBlobUtil.fs.unlink(tmp).catch(() => {}); - throw cb.signal?.aborted ? new AbortError() : e; + throw cb.signal?.aborted ? abortError() : e; } finally { cb.signal?.removeEventListener('abort', onAbort); } @@ -241,7 +234,7 @@ async function downloadUrlViaAndroidDownloadManager( const size = await fileSize(tmp); if (size <= 0) { await RNBlobUtil.fs.unlink(tmp).catch(() => {}); - throw new Error(`Download of ${url} failed (empty response).`); + throw RnExecuTorchError('DOWNLOAD_FAILED', `Download of ${url} failed (empty response).`); } await RNBlobUtil.fs.mv(tmp, dest); return dest; @@ -269,7 +262,7 @@ async function downloadUrlViaIosStream( const headers: Record = {}; if (offset > 0) headers.Range = `bytes=${offset}-`; - if (cb.signal?.aborted) throw new AbortError(); + if (cb.signal?.aborted) throw abortError(); const task = RNBlobUtil.config({ path: target, fileCache: true }).fetch('GET', url, headers); const onAbort = () => task.cancel(); @@ -289,7 +282,7 @@ async function downloadUrlViaIosStream( // Network drop / cancel. Keep the fresh partial for a future resume, but // discard a resumed chunk — its offset assumptions may not hold. if (offset > 0) await RNBlobUtil.fs.unlink(target).catch(() => {}); - throw cb.signal?.aborted ? new AbortError() : e; + throw cb.signal?.aborted ? abortError() : e; } finally { cb.signal?.removeEventListener('abort', onAbort); } @@ -300,7 +293,10 @@ async function downloadUrlViaIosStream( await RNBlobUtil.fs.unlink(target).catch(() => {}); } else if (status >= 400) { await RNBlobUtil.fs.unlink(target).catch(() => {}); - throw new Error(`Download of ${url} failed with HTTP status ${status}.`); + throw RnExecuTorchError( + 'DOWNLOAD_FAILED', + `Download of ${url} failed with HTTP status ${status}.` + ); } else if (offset > 0) { if (status === 206) { // Server honored the range: append the new bytes onto the partial. diff --git a/packages/react-native-executorch/src/hooks/useResourceDownload.ts b/packages/react-native-executorch/src/hooks/useResourceDownload.ts index 610dee7120..58df8b69aa 100644 --- a/packages/react-native-executorch/src/hooks/useResourceDownload.ts +++ b/packages/react-native-executorch/src/hooks/useResourceDownload.ts @@ -1,5 +1,6 @@ import { useState, useEffect, useMemo } from 'react'; -import { download, AbortError } from '../fetcher/fetcher'; +import { download } from '../fetcher/fetcher'; +import { isRnExecuTorchError } from '../core/error'; /** * Options accepted by {@link useResourceDownload} and by every `use` hook @@ -75,7 +76,7 @@ export function useResourceDownload(config: T, options?: ResourceOptions) { setDownloadProgress(100); }) .catch((e) => { - if (!isMounted || e instanceof AbortError) return; + if (!isMounted || isRnExecuTorchError(e, 'DOWNLOAD_ABORTED')) return; setDownloadError(e instanceof Error ? e : new Error(String(e))); }); diff --git a/packages/react-native-executorch/src/index.ts b/packages/react-native-executorch/src/index.ts index b92a2def39..5728ca1beb 100644 --- a/packages/react-native-executorch/src/index.ts +++ b/packages/react-native-executorch/src/index.ts @@ -38,6 +38,7 @@ export * from './extensions/speech/tasks/whisperSpeechToText'; export * from './extensions/speech/tasks/supertonicTextToSpeech'; // Core primitives — for library builders and power users +export * from './core/error'; export * from './core/model'; export * from './core/tensor'; export * from './core/runtime'; diff --git a/packages/react-native-executorch/src/native/bridge.ts b/packages/react-native-executorch/src/native/bridge.ts index 6c9cd2e4d3..c3d5f6bb73 100644 --- a/packages/react-native-executorch/src/native/bridge.ts +++ b/packages/react-native-executorch/src/native/bridge.ts @@ -1,3 +1,5 @@ +import { RnExecuTorchError } from '../core/error'; + const globalObj = globalThis as any; if (!globalObj.__rnexecutorch_jsi__) { @@ -8,5 +10,5 @@ if (!globalObj.__rnexecutorch_jsi__) { export const rnexecutorchJsi = globalObj.__rnexecutorch_jsi__; if (!rnexecutorchJsi) { - throw new Error("JSI global object '__rnexecutorch_jsi__' is not registered."); + throw RnExecuTorchError('UNKNOWN', "JSI global object '__rnexecutorch_jsi__' is not registered."); } diff --git a/scripts/errors.config.ts b/scripts/errors.config.ts deleted file mode 100644 index 7096d5e5f8..0000000000 --- a/scripts/errors.config.ts +++ /dev/null @@ -1,223 +0,0 @@ -// Single source of truth for error codes -// Run `yarn codegen:errors` to generate C++ and TypeScript enums - -export const errorDefinitions = { - // ========================================================================= - // General React Native ExecuTorch errors (0x60–0x7f) - // ========================================================================= - /** - * An umbrella-error that is thrown usually when something unexpected happens, for example a 3rd-party library error. - */ - UnknownError: 0x65, - /** - * Thrown when a user tries to run a model that is not yet downloaded or loaded into memory. - */ - ModuleNotLoaded: 0x66, - /** - * An error ocurred when saving a file. This could be, for instance a result image from an image model. - */ - FileWriteFailed: 0x67, - /** - * Thrown when a user tries to run a model that is currently processing. It is only allowed to run a single model prediction at a time. - */ - ModelGenerating: 0x68, - /* - * Thrown when a language is passed to a multi-language model that is not supported. For example OCR or Speech To Text. - */ - LanguageNotSupported: 0x69, - /* - * Thrown when config parameters passed to a model are invalid. For example, when LLM's topp is outside of range [0, 1]. - */ - InvalidConfig: 0x70, - /* - * Thrown when React Native ExecuTorch threadpool problem occurs. - */ - ThreadPoolError: 0x71, - /* - * Thrown when a file read operation failed. This could be invalid image url passed to image models, or unsupported format. - */ - FileReadFailed: 0x72, - /* - * Thrown when the size of model output is unexpected. If you're using your custom model with any of the pre-defined modules, please verify docs or source code for the expected model I/O contract. - */ - InvalidModelOutput: 0x73, - /* - * Thrown when the dimensions of input tensors don't match the model's expected dimensions. - */ - WrongDimensions: 0x74, - /* - * Thrown when the input passed to our APIs is invalid, for example when passing an empty message array to LLM's generate(). - */ - InvalidUserInput: 0x75, - /* - * Thrown when the number of downloaded files is unexpected, due to download interruptions. - */ - DownloadInterrupted: 0x76, - /* - * Thrown when a feature or platform is not supported in the current environment. - */ - PlatformNotSupported: 0x77, - /* - * Thrown when the type of model source passed by the user is invalid. - */ - InvalidModelSource: 0x78, - /* - * Thrown when the number of passed inputs to the model is different than the model metadata specifies. If you're using your custom model with any of the pre-defined modules, please verify docs or source code for the expected model I/O contract. - */ - UnexpectedNumInputs: 0x79, - /* - * Thrown when an error occurs with the tokenizer or tokenization process. - */ - TokenizerError: 0x7a, - - // ========================================================================= - // Speech to Text errors (0xa0–0xaf) - // ========================================================================= - /* - * Thrown when there's a configuration mismatch between multilingual and language settings in Speech-to-Text models. - */ - MultilingualConfiguration: 0xa0, - /* - * Thrown when streaming transcription is attempted but audio data chunk is missing. - */ - MissingDataChunk: 0xa1, - /* - * Thrown when trying to stop or insert data into a stream that hasn't been started. - */ - StreamingNotStarted: 0xa2, - /* - * Thrown when trying to start a new streaming session while another is already in progress. - */ - StreamingInProgress: 0xa3, - - // ========================================================================= - // Resource Fetcher errors (0xb0–0xbf) - // ========================================================================= - /** - * Thrown when a resource fails to download. This could be due to invalid URL, or for example a network problem. - */ - ResourceFetcherDownloadFailed: 0xb4, - /** - * Thrown when a user tries to trigger a download that's already in progress. - */ - ResourceFetcherDownloadInProgress: 0xb5, - /** - * Thrown when trying to pause a download that is already paused. - */ - ResourceFetcherAlreadyPaused: 0xb6, - /** - * Thrown when trying to resume a download that is already ongoing. - */ - ResourceFetcherAlreadyOngoing: 0xb7, - /** - * Thrown when trying to pause, resume, or cancel a download that is not active. - */ - ResourceFetcherNotActive: 0xb8, - /** - * Thrown when required URI information is missing for a download operation. - */ - ResourceFetcherMissingUri: 0xb9, - /** - * Thrown when trying to load resources without fetcher initialization. - */ - ResourceFetcherAdapterNotInitialized: 0xba, - /** - * Thrown when the platform does not support the resource fetching feature, which is required for downloading models and other resources in React Native ExecuTorch. - */ - ResourceFetcherPlatformNotSupported: 0xbb, - /** - * Thrown when the Expo resource fetcher entry point is incompatible with the installed expo-file-system version. The default entry point requires the new Directory/File API (Expo SDK 56+); use the `/legacy` entry point on older SDKs. - */ - ResourceFetcherFileSystemApiUnavailable: 0xbc, - - // ========================================================================= - // ExecuTorch mapped errors (0x00–0x3f) - // Based on: https://github.com/pytorch/executorch/blob/main/runtime/core/error.h - // ========================================================================= - - // System errors (0x00–0x0f) - /** - * Status indicating a successful operation. - */ - Ok: 0x00, - /** - * An internal error occurred. - */ - Internal: 0x01, - /** - * Status indicating the executor is in an invalid state for a targeted operation. - */ - InvalidState: 0x02, - /** - * Status indicating there are no more steps of execution to run - */ - EndOfMethod: 0x03, - /** - * Status indicating a resource has already been loaded. - */ - AlreadyLoaded: 0x04, - - // Logical errors (0x10–0x1f) - /** - * Operation is not supported in the current context. - */ - NotSupported: 0x10, - /** - * Operation is not yet implemented. - */ - NotImplemented: 0x11, - /** - * User provided an invalid argument. - */ - InvalidArgument: 0x12, - /** - * Object is an invalid type for the operation. - */ - InvalidType: 0x13, - /** - * Operator(s) missing in the operator registry. - */ - OperatorMissing: 0x14, - - // Resource errors (0x20–0x2f) - /** - * Requested resource could not be found. - */ - NotFound: 0x20, - /** - * Could not allocate the requested memory. - */ - MemoryAllocationFailed: 0x21, - /** - * Could not access a resource. - */ - AccessFailed: 0x22, - /** - * Error caused by the contents of a program. - */ - InvalidProgram: 0x23, - /** - * Error caused by the contents of external data. - */ - InvalidExternalData: 0x24, - /** - * Does not have enough resources to perform the requested operation. - */ - OutOfResources: 0x25, - - // Delegate errors (0x30–0x3f) - /** - * Init stage: Backend receives an incompatible delegate version. - */ - DelegateInvalidCompatibility: 0x30, - /** - * Init stage: Backend fails to allocate memory. - */ - DelegateMemoryAllocationFailed: 0x31, - /** - * Execute stage: The handle is invalid. - */ - DelegateInvalidHandle: 0x32, -} as const; - -export type ErrorName = keyof typeof errorDefinitions; diff --git a/scripts/generate-errors.ts b/scripts/generate-errors.ts deleted file mode 100755 index b12b658343..0000000000 --- a/scripts/generate-errors.ts +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env ts-node - -import { errorDefinitions } from './errors.config'; -import * as fs from 'fs'; -import * as path from 'path'; - -const REPO_ROOT = path.join(__dirname, '..'); - -function extractComments(): Map { - const configPath = path.join(__dirname, 'errors.config.ts'); - const content = fs.readFileSync(configPath, 'utf-8'); - const comments = new Map(); - - // Match JSDoc comments followed by error name - const commentPattern = /\/\*\*?\s*([\s\S]*?)\s*\*\/\s*(\w+):/g; - let match; - - while ((match = commentPattern.exec(content)) !== null) { - const commentText = match[1] - .split('\n') - .map((line) => line.replace(/^\s*\*\s?/, '').trim()) - .filter((line) => line.length > 0) - .join('\n * '); - const errorName = match[2]; - comments.set(errorName, commentText); - } - - return comments; -} - -function generateCppEnum() { - const comments = extractComments(); - - // Filter out ExecuTorch mapped errors (0x00-0x32) for C++ - const execuTorchErrorCodes = new Set([ - 'Ok', - 'Internal', - 'InvalidState', - 'EndOfMethod', - 'AlreadyLoaded', - 'NotSupported', - 'NotImplemented', - 'InvalidArgument', - 'InvalidType', - 'OperatorMissing', - 'NotFound', - 'MemoryAllocationFailed', - 'AccessFailed', - 'InvalidProgram', - 'InvalidExternalData', - 'OutOfResources', - 'DelegateInvalidCompatibility', - 'DelegateMemoryAllocationFailed', - 'DelegateInvalidHandle', - ]); - - const entries = Object.entries(errorDefinitions) - .filter(([name]) => !execuTorchErrorCodes.has(name)) - .map(([name, code]) => { - const comment = comments.get(name); - if (comment) { - return ` /**\n * ${comment}\n */\n ${name} = ${code},`; - } - return ` ${name} = ${code},`; - }) - .join('\n'); - - const cpp = `#pragma once - -// Auto-generated from scripts/errors.config.ts -// DO NOT EDIT MANUALLY - Run 'yarn codegen:errors' to regenerate - -#include - -namespace rnexecutorch { - -enum class RnExecutorchErrorCode : int32_t { -${entries} -}; - -} // namespace rnexecutorch -`; - - const outputPath = path.join( - REPO_ROOT, - 'packages/react-native-executorch/common/rnexecutorch/ErrorCodes.h' - ); - fs.writeFileSync(outputPath, cpp); - console.log(`Generated C++ enum: ${outputPath}`); -} - -function generateTypeScriptEnum() { - const comments = extractComments(); - const entries = Object.entries(errorDefinitions) - .map(([name, code]) => { - const comment = comments.get(name); - if (comment) { - return ` /**\n * ${comment}\n */\n ${name} = ${code},`; - } - return ` ${name} = ${code},`; - }) - .join('\n'); - - const ts = `// Auto-generated from scripts/errors.config.ts -// DO NOT EDIT MANUALLY - Run 'yarn codegen:errors' to regenerate - -export enum RnExecutorchErrorCode { -${entries} -} -`; - - const outputPath = path.join( - REPO_ROOT, - 'packages/react-native-executorch/src/errors/ErrorCodes.ts' - ); - fs.writeFileSync(outputPath, ts); - console.log(`Generated TypeScript enum: ${outputPath}`); -} - -function main() { - console.log('Generating error code enums...\n'); - generateCppEnum(); - generateTypeScriptEnum(); - console.log('\n✨ Done!'); -} - -main();