From a76d7e953f2b66c11e7adfbef3d3746123c9f3be Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Wed, 1 Jul 2026 16:31:49 +0200 Subject: [PATCH 01/25] cpp/core: extract conversions and tensor_helpers modules --- .../cpp/core/conversions.cpp | 71 +++++++++++++ .../cpp/core/conversions.h | 74 ++++++++++++++ .../cpp/core/tensor_helpers.cpp | 99 +++++++++++++++++++ .../cpp/core/tensor_helpers.h | 43 ++++++++ 4 files changed, 287 insertions(+) create mode 100644 packages/react-native-executorch/cpp/core/conversions.cpp create mode 100644 packages/react-native-executorch/cpp/core/conversions.h create mode 100644 packages/react-native-executorch/cpp/core/tensor_helpers.cpp create mode 100644 packages/react-native-executorch/cpp/core/tensor_helpers.h diff --git a/packages/react-native-executorch/cpp/core/conversions.cpp b/packages/react-native-executorch/cpp/core/conversions.cpp new file mode 100644 index 0000000000..83f20a8470 --- /dev/null +++ b/packages/react-native-executorch/cpp/core/conversions.cpp @@ -0,0 +1,71 @@ +#include "conversions.h" +#include + +namespace rnexecutorch::core::conversions { + +template <> +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"); + } + return val.asNumber(); +} + +template <> +float asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) { + return static_cast(asType(rt, ctx, val)); +} + +template <> +int32_t asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) { + return static_cast(asType(rt, ctx, val)); +} + +template <> +int64_t asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) { + return static_cast(asType(rt, ctx, val)); +} + +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) || v < 0.0) { + throw jsi::JSError(rt, ctx + " must be a non-negative integer"); + } + return static_cast(v); +} + +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) || v < 0.0 || v > 255.0) { + throw jsi::JSError(rt, ctx + " must be between 0 and 255"); + } + return static_cast(v); +} + +template <> +size_t asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) { + double v = asType(rt, ctx, val); + if (std::isnan(v) || v < 0.0) { + throw jsi::JSError(rt, ctx + " must be a non-negative integer"); + } + return static_cast(v); +} + +template <> +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"); + } + return val.asBool(); +} + +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"); + } + return val.asString(rt).utf8(rt); +} +} // namespace rnexecutorch::core::conversions diff --git a/packages/react-native-executorch/cpp/core/conversions.h b/packages/react-native-executorch/cpp/core/conversions.h new file mode 100644 index 0000000000..b72e6f072e --- /dev/null +++ b/packages/react-native-executorch/cpp/core/conversions.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace rnexecutorch::core::conversions { +namespace jsi = facebook::jsi; + +template +T asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val); + +template <> +inline jsi::Value asType(jsi::Runtime &rt, const std::string & /*ctx*/, const jsi::Value &val) { + return jsi::Value(rt, val); +} + +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)); + } + return asType(rt, std::format("{}: option '{}'", ctx, propName), obj.getProperty(rt, propName.c_str())); +} + +template +std::optional getOptionalProperty(jsi::Runtime &rt, const std::string &ctx, const jsi::Object &obj, const std::string &propName) { + if (!obj.hasProperty(rt, propName.c_str())) { + return std::nullopt; + } + auto val = obj.getProperty(rt, propName.c_str()); + if (val.isUndefined() || val.isNull()) { + return std::nullopt; + } + return asType(rt, std::format("{}: option '{}'", ctx, propName), val); +} + +template +std::vector asVector(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) { + if (!val.isObject() || !val.asObject(rt).isArray(rt)) { + throw jsi::JSError(rt, std::format("{} must be an Array", ctx)); + } + auto arr = val.asObject(rt).asArray(rt); + std::vector vec; + const size_t len = arr.size(rt); + vec.reserve(len); + for (size_t i = 0; i < len; ++i) { + vec.push_back(asType(rt, std::format("{}[{}]", ctx, i), arr.getValueAtIndex(rt, i))); + } + return vec; +} + +template +jsi::Array toJsiArray(jsi::Runtime &rt, const std::vector &vec) { + jsi::Array arr(rt, vec.size()); + for (size_t i = 0; i < vec.size(); ++i) { + if constexpr (std::is_same_v) { + arr.setValueAtIndex(rt, i, jsi::String::createFromUtf8(rt, vec[i])); + } else if constexpr (std::is_same_v) { + arr.setValueAtIndex(rt, i, jsi::Value(vec[i])); + } else { + arr.setValueAtIndex(rt, i, jsi::Value(static_cast(vec[i]))); + } + } + return arr; +} + +} // namespace rnexecutorch::core::conversions diff --git a/packages/react-native-executorch/cpp/core/tensor_helpers.cpp b/packages/react-native-executorch/cpp/core/tensor_helpers.cpp new file mode 100644 index 0000000000..d61a2a6dec --- /dev/null +++ b/packages/react-native-executorch/cpp/core/tensor_helpers.cpp @@ -0,0 +1,99 @@ +#include "tensor_helpers.h" +#include "dtype.h" + +#include +#include + +namespace rnexecutorch::core::tensor { +namespace types = rnexecutorch::core::types; + +std::shared_lock +tryLockShared(jsi::Runtime &rt, const std::string &name, 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", name)); + } + if (!tensor->data_) { + throw jsi::JSError(rt, std::format("{} tensor has been disposed", name)); + } + return lock; +} + +std::unique_lock +tryLockUnique(jsi::Runtime &rt, const std::string &name, 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", name)); + } + if (!tensor->data_) { + throw jsi::JSError(rt, std::format("{} tensor has been disposed", name)); + } + return lock; +} + +void checkNotSameTensor(jsi::Runtime &rt, + const std::string &name1, const std::shared_ptr &t1, + const std::string &name2, const std::shared_ptr &t2) { + if (t1.get() == t2.get()) { + throw jsi::JSError(rt, std::format("{} and {} cannot be the same tensor", name1, name2)); + } +} + +std::shared_ptr +fromJs(jsi::Runtime &rt, const std::string &name, const jsi::Value &value, + std::optional expectedDtype, const std::optional &expectedShape) { + + if (!value.isObject() || !value.asObject(rt).isHostObject(rt)) { + throw jsi::JSError(rt, name + " must be a Tensor"); + } + + auto tensor = value.asObject(rt).getHostObject(rt); + auto dtype = tensor->dtype_; + auto shape = tensor->shape_; + + if (expectedDtype && dtype != *expectedDtype) { + throw jsi::JSError(rt, std::format("{} must be of type {}", + name, types::toString(*expectedDtype))); + } + + if (expectedShape) { + std::string shapeStr = "["; + for (size_t i = 0; i < expectedShape->size(); ++i) { + if (i > 0) { + shapeStr += ", "; + } + const auto &dim = expectedShape->at(i); + if (std::holds_alternative(dim)) { + shapeStr += std::to_string(std::get(dim)); + } else { + shapeStr += std::get(dim); + } + } + shapeStr += "]"; + + if (shape.size() != expectedShape->size()) { + throw jsi::JSError(rt, std::format("{} must have shape {} (expected {} dimensions, got {})", + name, shapeStr, expectedShape->size(), shape.size())); + } + + std::unordered_map symbolMap; + for (size_t i = 0; i < expectedShape->size(); ++i) { + const auto &dim = expectedShape->at(i); + if (std::holds_alternative(dim)) { + if (shape[i] != std::get(dim)) { + throw jsi::JSError(rt, std::format("{} must have shape {} (dim {} mismatch: expected {}, got {})", + name, shapeStr, i, std::get(dim), shape[i])); + } + } else { + const auto &symbol = std::get(dim); + if (symbolMap.contains(symbol) && shape[i] != symbolMap[symbol]) { + throw jsi::JSError(rt, std::format("{} must have shape {} (dim '{}' mismatch: expected {}, got {})", + name, shapeStr, i, symbolMap[symbol], shape[i])); + } + symbolMap[symbol] = shape[i]; + } + } + } + return tensor; +} +} // namespace rnexecutorch::core::tensor diff --git a/packages/react-native-executorch/cpp/core/tensor_helpers.h b/packages/react-native-executorch/cpp/core/tensor_helpers.h new file mode 100644 index 0000000000..b8a0262054 --- /dev/null +++ b/packages/react-native-executorch/cpp/core/tensor_helpers.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "conversions.h" +#include "dtype.h" +#include "tensor.h" + +namespace rnexecutorch::core::tensor { +namespace jsi = facebook::jsi; + +using rnexecutorch::core::types::DType; +using SymbolicShape = std::vector>; + +[[nodiscard]] std::shared_lock +tryLockShared(jsi::Runtime &rt, const std::string &name, const std::shared_ptr &tensor); + +[[nodiscard]] std::unique_lock +tryLockUnique(jsi::Runtime &rt, const std::string &name, const std::shared_ptr &tensor); + +void checkNotSameTensor(jsi::Runtime &rt, + const std::string &name1, const std::shared_ptr &t1, + const std::string &name2, const std::shared_ptr &t2); + +std::shared_ptr +fromJs(jsi::Runtime &rt, const std::string &name, const jsi::Value &value, + std::optional expectedDtype, const std::optional &expectedShape); + +inline std::shared_ptr +fromJs(jsi::Runtime &rt, const std::string &name, const jsi::Value &value, + std::optional expectedDtype, const std::vector &expectedShape) { + SymbolicShape convertedShape(expectedShape.begin(), expectedShape.end()); + return fromJs(rt, name, value, expectedDtype, std::move(convertedShape)); +} +} // namespace rnexecutorch::core::tensor From b70d91a7f5301cede45e64d881bab64e02a4bec7 Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Wed, 1 Jul 2026 16:32:02 +0200 Subject: [PATCH 02/25] cpp: migrate callers to conversions and tensor_helpers --- .../cpp/core/model.cpp | 193 +++---- .../react-native-executorch/cpp/core/model.h | 17 +- .../cpp/core/tensor.cpp | 223 ++------ .../react-native-executorch/cpp/core/tensor.h | 26 +- .../cpp/extensions/cv/box_ops.cpp | 169 ++---- .../cpp/extensions/cv/image_ops.cpp | 520 ++++-------------- .../cpp/extensions/math/operations.cpp | 204 +------ .../cpp/extensions/nlp/tokenizer.cpp | 63 +-- 8 files changed, 362 insertions(+), 1053 deletions(-) diff --git a/packages/react-native-executorch/cpp/core/model.cpp b/packages/react-native-executorch/cpp/core/model.cpp index ba50d9d12b..4854b9ef39 100644 --- a/packages/react-native-executorch/cpp/core/model.cpp +++ b/packages/react-native-executorch/cpp/core/model.cpp @@ -1,8 +1,9 @@ #include "model.h" #include "dtype.h" -#include "tensor.h" +#include "tensor_helpers.h" #include +#include #include #include @@ -11,11 +12,19 @@ namespace rnexecutorch::core::model { namespace jsi = facebook::jsi; +namespace types = rnexecutorch::core::types; +namespace conversions = rnexecutorch::core::conversions; + using TensorHostObject = rnexecutorch::core::tensor::TensorHostObject; ModelHostObject::ModelHostObject(const std::string &modelPath) : modelPath_(modelPath), etModule_(std::make_unique(modelPath)) { + auto error = 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: {}", errorMsg)); + } } jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { @@ -79,11 +88,11 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { throw jsi::JSError(rt, "getMethodMeta: Model has been disposed"); } - auto methodName = args[0].asString(rt).utf8(rt); + auto methodName = conversions::asType(rt, "getMethodMeta: methodName", args[0]); auto methodMeta = self->etModule_->method_meta(methodName); if (!methodMeta.ok()) { const std::string errorMsg = executorch::runtime::to_string(methodMeta.error()); - throw jsi::JSError(rt, "getMethodMeta: Failed to get method meta: " + errorMsg); + throw jsi::JSError(rt, std::format("getMethodMeta: Failed to get method meta: {}", errorMsg)); } auto inputTagsArray = jsi::Array(rt, methodMeta->num_inputs()); @@ -91,7 +100,7 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { auto tag = methodMeta->input_tag(i); if (!tag.ok()) { const std::string errorMsg = executorch::runtime::to_string(tag.error()); - throw jsi::JSError(rt, "getMethodMeta: Failed to get input tag for input " + std::to_string(i) + ": " + errorMsg); + throw jsi::JSError(rt, std::format("getMethodMeta: Failed to get input tag for input {}: {}", i, errorMsg)); } inputTagsArray.setValueAtIndex(rt, i, jsi::String::createFromUtf8(rt, executorch::runtime::tag_to_string(tag.get()))); } @@ -101,7 +110,7 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { auto tag = methodMeta->output_tag(i); if (!tag.ok()) { const std::string errorMsg = executorch::runtime::to_string(tag.error()); - throw jsi::JSError(rt, "getMethodMeta: Failed to get output tag for output " + std::to_string(i) + ": " + errorMsg); + throw jsi::JSError(rt, std::format("getMethodMeta: Failed to get output tag for output {}: {}", i, errorMsg)); } outputTagsArray.setValueAtIndex(rt, i, jsi::String::createFromUtf8(rt, executorch::runtime::tag_to_string(tag.get()))); } @@ -111,7 +120,7 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { auto backendName = methodMeta->get_backend_name(i); if (!backendName.ok()) { const std::string errorMsg = executorch::runtime::to_string(backendName.error()); - throw jsi::JSError(rt, "getMethodMeta: Failed to get backend name for backend " + std::to_string(i) + ": " + errorMsg); + throw jsi::JSError(rt, std::format("getMethodMeta: Failed to get backend name for backend {}: {}", i, errorMsg)); } usesBackendMap.setProperty(rt, backendName.get(), methodMeta->uses_backend(backendName.get())); } @@ -123,7 +132,7 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { jsTensorMeta.setProperty(rt, "nbytes", static_cast(tensorMeta.nbytes())); try { - const std::string dtypeStr = rnexecutorch::core::types::toString(rnexecutorch::core::types::fromScalarType(tensorMeta.scalar_type())); + const std::string dtypeStr = types::toString(types::fromScalarType(tensorMeta.scalar_type())); jsTensorMeta.setProperty(rt, "dtype", jsi::String::createFromUtf8(rt, dtypeStr)); } catch (const std::exception &) { jsTensorMeta.setProperty(rt, "dtype", jsi::String::createFromUtf8(rt, "not supported")); @@ -143,7 +152,7 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { auto tensorMeta = methodMeta->input_tensor_meta(i); if (!tensorMeta.ok()) { const std::string errorMsg = executorch::runtime::to_string(tensorMeta.error()); - throw jsi::JSError(rt, "getMethodMeta: Failed to get tensor meta for input " + std::to_string(i) + ": " + errorMsg); + throw jsi::JSError(rt, std::format("getMethodMeta: Failed to get tensor meta for input {}: {}", i, errorMsg)); } inputTensorMetaArray.setValueAtIndex(rt, i, tensorMetaToJS(rt, tensorMeta.get())); } @@ -153,13 +162,12 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { auto tensorMeta = methodMeta->output_tensor_meta(i); if (!tensorMeta.ok()) { const std::string errorMsg = executorch::runtime::to_string(tensorMeta.error()); - throw jsi::JSError(rt, "getMethodMeta: Failed to get tensor meta for output " + std::to_string(i) + ": " + errorMsg); + throw jsi::JSError(rt, std::format("getMethodMeta: Failed to get tensor meta for output {}: {}", i, errorMsg)); } outputTensorMetaArray.setValueAtIndex(rt, i, tensorMetaToJS(rt, tensorMeta.get())); } auto jsMeta = jsi::Object(rt); - jsMeta.setProperty(rt, "name", jsi::String::createFromUtf8(rt, methodMeta->name())); jsMeta.setProperty(rt, "numInputs", static_cast(methodMeta->num_inputs())); jsMeta.setProperty(rt, "numOutputs", static_cast(methodMeta->num_outputs())); @@ -181,18 +189,6 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { throw jsi::JSError(rt, "execute: Usage: execute(methodName, inputs, outputTensors)"); } - if (!args[0].isString()) { - throw jsi::JSError(rt, "execute: Expected arg0 to be a string"); - } - - if (!args[1].isObject() || !args[1].asObject(rt).isArray(rt)) { - throw jsi::JSError(rt, "execute: Expected arg1 to be an array"); - } - - if (!args[2].isObject() || !args[2].asObject(rt).isArray(rt)) { - throw jsi::JSError(rt, "execute: Expected arg2 to be an array"); - } - std::unique_lock lock(self->mutex_, std::try_to_lock); if (!lock.owns_lock()) { throw jsi::JSError(rt, "execute: Model is currently in use"); @@ -202,20 +198,20 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { throw jsi::JSError(rt, "execute: Model has been disposed"); } - auto methodName = args[0].asString(rt).utf8(rt); + auto methodName = conversions::asType(rt, "execute: methodName", args[0]); auto methodMeta = self->etModule_->method_meta(methodName); auto inputsArray = args[1].asObject(rt).asArray(rt); + auto outputTensorsArray = args[2].asObject(rt).asArray(rt); if (!methodMeta.ok()) { const std::string errorMsg = executorch::runtime::to_string(methodMeta.error()); - throw jsi::JSError(rt, "execute: Failed to get method meta for '" + methodName + "': " + errorMsg); + throw jsi::JSError(rt, std::format("execute: Failed to get method meta for '{}': {}", + methodName, errorMsg)); } if (inputsArray.size(rt) != methodMeta->num_inputs()) { - const std::string errorMsg = "execute: Incorrect size for inputs: got " + - std::to_string(inputsArray.size(rt)) + - ", expected " + std::to_string(methodMeta->num_inputs()); - throw jsi::JSError(rt, errorMsg); + throw jsi::JSError(rt, std::format("execute: Incorrect size for inputs: got {}, expected {}", + inputsArray.size(rt), methodMeta->num_inputs())); } auto validateTensor = [](jsi::Runtime &rt, @@ -223,22 +219,19 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { const executorch::runtime::Result &tensorMeta, const std::string &identifier) { if (tensorMeta->scalar_type() != tensorHostObject->tensor_->dtype()) { - throw jsi::JSError(rt, "execute: Tensor dtype mismatch for " + identifier); + throw jsi::JSError(rt, std::format("execute: Tensor dtype mismatch for {}", identifier)); } if (tensorMeta->sizes().size() != tensorHostObject->shape_.size()) { - throw jsi::JSError(rt, "execute: Tensor rank mismatch for " + identifier + - ": expected rank " + std::to_string(tensorMeta->sizes().size()) + - " but got " + std::to_string(tensorHostObject->shape_.size())); + throw jsi::JSError(rt, std::format("execute: Tensor rank mismatch for {}: expected rank {} but got {}", + identifier, tensorMeta->sizes().size(), tensorHostObject->shape_.size())); } auto ndim = tensorHostObject->tensor_->sizes().size(); for (size_t j = 0; j < ndim; ++j) { if (tensorMeta->sizes()[j] != tensorHostObject->shape_[j]) { - throw jsi::JSError(rt, "execute: Tensor shape mismatch for " + identifier + - ": expected dimension " + std::to_string(j) + " to be " + - std::to_string(tensorMeta->sizes()[j]) + " but got " + - std::to_string(tensorHostObject->shape_[j])); + throw jsi::JSError(rt, std::format("execute: Tensor shape mismatch for {}: expected dimension {} to be {} but got {}", + identifier, j, tensorMeta->sizes()[j], tensorHostObject->shape_[j])); } } }; @@ -250,32 +243,17 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { for (size_t i = 0; i < methodMeta->num_inputs(); ++i) { auto tag = methodMeta->input_tag(i); auto val = inputsArray.getValueAtIndex(rt, i); + const auto ident = std::format("inputs[{}]", i); + const auto ctx = std::format("execute: {}", ident); if (!tag.ok()) { const std::string errorMsg = executorch::runtime::to_string(tag.error()); - throw jsi::JSError(rt, "execute: Failed to get input tag for inputs[" + - std::to_string(i) + "]: " + errorMsg); + throw jsi::JSError(rt, std::format("{}: Failed to get input tag: {}", ctx, errorMsg)); } switch (tag.get()) { - case executorch::runtime::Tag::None: { - if (!val.isNull()) { - throw jsi::JSError(rt, "execute: Expected inputs[" + - std::to_string(i) + "] to be null"); - } - inputs[i] = executorch::runtime::EValue(); - break; - } case executorch::runtime::Tag::Tensor: { - if (!val.isObject() || !val.asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "execute: Expected inputs[" + - std::to_string(i) + "] to be a TensorHostObject"); - } - - auto tensorHostObject = val.asObject(rt).getHostObject(rt); - if (!tensorHostObject->data_) { - throw jsi::JSError(rt, "execute: inputs[" + std::to_string(i) + "] has been disposed"); - } + auto tensorHostObject = tensor::fromJs(rt, ctx, val, std::nullopt, std::nullopt); if (!lockedTensors.insert(tensorHostObject.get()).second) { throw jsi::JSError(rt, "execute: Tensor aliasing detected. The same tensor was passed multiple times."); @@ -283,50 +261,36 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { tensorLocks.emplace_back(tensorHostObject->mutex_, std::try_to_lock); if (!tensorLocks.back().owns_lock()) { - throw jsi::JSError(rt, "execute: inputs[" + std::to_string(i) + - "] is currently in use"); + throw jsi::JSError(rt, std::format("{} is currently in use", ctx)); + } + + if (!tensorHostObject->data_) { + throw jsi::JSError(rt, std::format("{} has been disposed", ctx)); } auto tensorMeta = methodMeta->input_tensor_meta(i); if (!tensorMeta.ok()) { const std::string errorMsg = executorch::runtime::to_string(tensorMeta.error()); - throw jsi::JSError(rt, "execute: Failed to get tensor meta for inputs[" + - std::to_string(i) + "]: " + errorMsg); + throw jsi::JSError(rt, std::format("{}: Failed to get tensor meta: {}", ctx, errorMsg)); } - validateTensor(rt, tensorHostObject.get(), tensorMeta, "inputs[" + std::to_string(i) + "]"); + validateTensor(rt, tensorHostObject.get(), tensorMeta, ident); inputs[i] = tensorHostObject->tensor_; break; } - case executorch::runtime::Tag::Double: { - if (!val.isNumber()) { - throw jsi::JSError(rt, "execute: Expected inputs[" + - std::to_string(i) + "] to be a number"); - } - inputs[i] = executorch::runtime::EValue(val.asNumber()); + case executorch::runtime::Tag::Double: + inputs[i] = executorch::runtime::EValue(conversions::asType(rt, ctx, val)); break; - } - case executorch::runtime::Tag::Int: { - if (!val.isNumber()) { - throw jsi::JSError(rt, "execute: Expected inputs[" + - std::to_string(i) + "] to be a number"); - } - inputs[i] = executorch::runtime::EValue(static_cast(val.asNumber())); + case executorch::runtime::Tag::Int: + inputs[i] = executorch::runtime::EValue(conversions::asType(rt, ctx, val)); break; - } - case executorch::runtime::Tag::Bool: { - if (!val.isBool()) { - throw jsi::JSError(rt, "execute: Expected inputs[" + - std::to_string(i) + "] to be a boolean"); - } - inputs[i] = executorch::runtime::EValue(val.asBool()); + case executorch::runtime::Tag::Bool: + inputs[i] = executorch::runtime::EValue(conversions::asType(rt, ctx, val)); break; - } - default: { - throw jsi::JSError(rt, "execute: Unsupported input type for inputs[" + std::to_string(i) + "]"); - } + default: + throw jsi::JSError(rt, std::format("{}: Unsupported input type", ctx)); } } @@ -338,19 +302,20 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { auto durationMs = std::chrono::duration_cast(finishTime - startTime).count(); auto consoleObj = rt.global().getProperty(rt, "console").asObject(rt); auto logFn = consoleObj.getProperty(rt, "log").asObject(rt).asFunction(rt); - auto info = "Execution of method '" + methodName + "' took " + std::to_string(durationMs) + " ms"; + auto info = std::format("Execution of method '{}' took {} ms", methodName, durationMs); logFn.callWithThis(rt, consoleObj, {jsi::String::createFromUtf8(rt, info)}); #endif if (!result.ok()) { const std::string errorMsg = executorch::runtime::to_string(result.error()); - throw jsi::JSError(rt, "execute: Method '" + methodName + "' execution failed: " + errorMsg + - ". This may be due to missing required backends - use getMethodMeta()" + - " to check required backends and getExecuTorchRegisteredBackends()" + - " to check which backends are registered in the runtime."); + throw jsi::JSError(rt, std::format("execute: Method '{}' execution failed: {}. " + "This may be due to missing required backends - " + "use getMethodMeta() to check required backends and " + "getExecuTorchRegisteredBackends() to check " + "which backends are registered in the runtime.", + methodName, errorMsg)); } - auto outputTensorsArray = args[2].asObject(rt).asArray(rt); auto jsOutputArray = jsi::Array(rt, result->size()); size_t index = 0; @@ -367,37 +332,33 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { throw jsi::JSError(rt, "execute: Not enough tensor output placeholders in outputTensors"); } - auto val = outputTensorsArray.getValueAtIndex(rt, tensorOutputIdx); - if (!val.isObject() || !val.asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "execute: Expected outputTensors[" + - std::to_string(tensorOutputIdx) + "] to be a TensorHostObject"); - } - - auto tensorHostObject = val.asObject(rt).getHostObject(rt); - if (!tensorHostObject->data_) { - throw jsi::JSError(rt, "execute: outputTensors[" + std::to_string(tensorOutputIdx) + "] has been disposed"); - } + const auto val = outputTensorsArray.getValueAtIndex(rt, tensorOutputIdx); + const auto ident = std::format("outputTensors[{}]", tensorOutputIdx); + const auto ctx = std::format("execute: {}", ident); + auto tensorHostObject = tensor::fromJs(rt, ctx, val, std::nullopt, std::nullopt); if (!lockedTensors.insert(tensorHostObject.get()).second) { throw jsi::JSError(rt, "execute: Tensor aliasing detected. The same tensor was passed multiple times."); } tensorLocks.emplace_back(tensorHostObject->mutex_, std::try_to_lock); if (!tensorLocks.back().owns_lock()) { - throw jsi::JSError(rt, "execute: outputTensors[" + - std::to_string(tensorOutputIdx) + - "] is currently in use"); + throw jsi::JSError(rt, std::format("{} is currently in use", ctx)); + } + + if (!tensorHostObject->data_) { + throw jsi::JSError(rt, std::format("{} has been disposed", ctx)); } auto tensorMeta = methodMeta->output_tensor_meta(index); if (!tensorMeta.ok()) { const std::string errorMsg = executorch::runtime::to_string(tensorMeta.error()); - throw jsi::JSError(rt, "execute: Failed to get tensor meta for output at index " + - std::to_string(index) + ": " + errorMsg); + throw jsi::JSError(rt, std::format("execute: Failed to get tensor meta for output at index {}: {}", + index, errorMsg)); } - validateTensor(rt, tensorHostObject.get(), tensorMeta, "outputTensors[" + std::to_string(tensorOutputIdx) + "]"); + validateTensor(rt, tensorHostObject.get(), tensorMeta, ident); std::memcpy(tensorHostObject->data_.get(), output.toTensor().const_data_ptr(), @@ -470,23 +431,15 @@ 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(arg0)"); + throw jsi::JSError(rt, "loadModel: Usage: loadModel(path)"); } - if (!args[0].isString()) { - throw jsi::JSError(rt, "loadModel: Expected arg0 to be a string"); + 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())); } - - auto modelPath = args[0].asString(rt).utf8(rt); - auto modelInstance = std::make_shared(modelPath); - - auto error = modelInstance->etModule_->load(); - if (!modelInstance->etModule_->is_loaded()) { - const std::string errorMsg = executorch::runtime::to_string(error); - throw jsi::JSError(rt, "loadModel: Failed to load model: " + errorMsg); - } - - return jsi::Object::createFromHostObject(rt, modelInstance); }; auto fn = jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 1, fnBody); diff --git a/packages/react-native-executorch/cpp/core/model.h b/packages/react-native-executorch/cpp/core/model.h index 4cc8bd0647..af8d23dd61 100644 --- a/packages/react-native-executorch/cpp/core/model.h +++ b/packages/react-native-executorch/cpp/core/model.h @@ -10,6 +10,7 @@ #include namespace rnexecutorch::core::model { +namespace jsi = facebook::jsi; /** * JSI HostObject wrapping an ExecuTorch Model instance * (`executorch::extension::Module`). @@ -18,17 +19,19 @@ namespace rnexecutorch::core::model { * retrieving method names, executing inference runs, and disposing of native * resources. */ -class ModelHostObject : public facebook::jsi::HostObject, public std::enable_shared_from_this { +class ModelHostObject : public jsi::HostObject, + public std::enable_shared_from_this { public: + explicit ModelHostObject(const std::string &modelPath); + + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &name) override; + std::vector getPropertyNames(jsi::Runtime &rt) override; + +private: std::string modelPath_; std::unique_ptr etModule_; std::mutex mutex_; - - explicit ModelHostObject(const std::string &modelPath); - - facebook::jsi::Value get(facebook::jsi::Runtime &rt, const facebook::jsi::PropNameID &name) override; - std::vector getPropertyNames(facebook::jsi::Runtime &rt) override; }; -void install_loadModel(facebook::jsi::Runtime &rt, facebook::jsi::Object &module); +void install_loadModel(jsi::Runtime &rt, jsi::Object &module); } // namespace rnexecutorch::core::model diff --git a/packages/react-native-executorch/cpp/core/tensor.cpp b/packages/react-native-executorch/cpp/core/tensor.cpp index bdc66f2f60..1e1107964f 100644 --- a/packages/react-native-executorch/cpp/core/tensor.cpp +++ b/packages/react-native-executorch/cpp/core/tensor.cpp @@ -1,35 +1,43 @@ #include "tensor.h" +#include "dtype.h" +#include "tensor_helpers.h" + +#include +#include +#include #include +#include #include +#include +#include +#include +#include + +#include namespace rnexecutorch::core::tensor { -namespace jsi = facebook::jsi; +namespace types = rnexecutorch::core::types; +namespace conversions = rnexecutorch::core::conversions; -TensorHostObject::TensorHostObject(const std::vector &shape, rnexecutorch::core::types::DType dtype) +TensorHostObject::TensorHostObject(const std::vector &shape, DType dtype) : dtype_(dtype), shape_(shape), - numel_(std::accumulate(shape.begin(), shape.end(), static_cast(1), std::multiplies<>())) { - const auto elemSize = rnexecutorch::core::types::elementSize(dtype); - - size_ = numel_ * elemSize; - // NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays,modernize-avoid-c-arrays): owning runtime-sized byte buffer - data_ = std::make_unique(size_); - tensor_ = executorch::extension::from_blob(data_.get(), shape_, rnexecutorch::core::types::toScalarType(dtype)); + numel_(std::accumulate(shape.begin(), shape.end(), static_cast(1), std::multiplies<>())), + size_(numel_ * types::elementSize(dtype)) { + data_ = std::make_unique(size_); // NOLINT(cppcoreguidelines-avoid-c-arrays,modernize-avoid-c-arrays): + // owning runtime-sized byte buffer + tensor_ = executorch::extension::from_blob(data_.get(), shape_, types::toScalarType(dtype)); } jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { auto nameStr = name.utf8(rt); if (nameStr == "shape") { - auto jsArray = jsi::Array(rt, shape_.size()); - for (size_t i = 0; i < shape_.size(); ++i) { - jsArray.setValueAtIndex(rt, i, static_cast(shape_[i])); - } - return jsArray; + return conversions::toJsiArray(rt, shape_); } if (nameStr == "dtype") { - return jsi::String::createFromUtf8(rt, rnexecutorch::core::types::toString(dtype_)); + return jsi::String::createFromUtf8(rt, types::toString(dtype_)); } if (nameStr == "numel") { @@ -43,54 +51,22 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) throw jsi::JSError(rt, "copyTo: Usage: copyTo(dst, options?)"); } - if (!args[0].isObject() || !args[0].asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "copyTo: Expected dst to be a Tensor"); - } - - auto dst = args[0].asObject(rt).getHostObject(rt); - - if (self.get() == dst.get()) { - throw jsi::JSError(rt, "copyTo: In-place operations (src == dst) are not supported."); - } - - std::shared_lock srcLock(self->mutex_, std::try_to_lock); - if (!srcLock.owns_lock()) { - throw jsi::JSError(rt, "copyTo: src tensor is currently in use"); - } - - std::unique_lock dstLock(dst->mutex_, std::try_to_lock); - if (!dstLock.owns_lock()) { - throw jsi::JSError(rt, "copyTo: dst tensor is currently in use"); - } - - if (!self->data_) { - throw jsi::JSError(rt, "copyTo: src tensor has been disposed"); - } - - if (!dst->data_) { - throw jsi::JSError(rt, "copyTo: dst tensor has been disposed"); - } + auto dst = fromJs(rt, "copyTo: dst", args[0], std::nullopt, std::nullopt); - size_t srcOffset = 0; - size_t copyLen = self->numel_; - if (count == 2 && args[1].isObject()) { - auto optsObj = args[1].asObject(rt); - if (optsObj.hasProperty(rt, "offset")) { - srcOffset = static_cast(optsObj.getProperty(rt, "offset").asNumber()); - } + checkNotSameTensor(rt, "copyTo: self", self, "copyTo: dst", dst); + auto srcLock = tryLockShared(rt, "copyTo: self", self); + auto dstLock = tryLockUnique(rt, "copyTo: dst", dst); - copyLen -= srcOffset; - - if (optsObj.hasProperty(rt, "length")) { - copyLen = static_cast(optsObj.getProperty(rt, "length").asNumber()); - } - } + const jsi::Object optsObj = (count == 2 && args[1].isObject()) ? args[1].asObject(rt) : jsi::Object(rt); + const size_t srcOffset = conversions::getOptionalProperty(rt, "copyTo", optsObj, "offset").value_or(0); + const size_t copyLen = conversions::getOptionalProperty(rt, "copyTo", optsObj, "length").value_or(self->numel_ - srcOffset); if (srcOffset + copyLen > self->numel_) { throw jsi::JSError(rt, "copyTo: out of bounds offset and length for src tensor"); } - const auto elemSize = rnexecutorch::core::types::elementSize(self->dtype_); + const auto elemSize = types::elementSize(self->dtype_); + if (copyLen * elemSize != dst->size_) { throw jsi::JSError(rt, "copyTo: size mismatch between copy byte size and dst tensor size"); } @@ -109,49 +85,16 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) throw jsi::JSError(rt, "setData: Usage: setData(array)"); } - if (!args[0].isObject()) { - throw jsi::JSError(rt, "setData: Expected array to be an object (TypedArray)"); - } - - const jsi::Object dataObj = args[0].asObject(rt); - if (!dataObj.hasProperty(rt, "buffer")) { - throw jsi::JSError(rt, "setData: Expected a TypedArray with a 'buffer' property"); - } - - const jsi::ArrayBuffer buffer = dataObj.getProperty(rt, "buffer").asObject(rt).getArrayBuffer(rt); - size_t byteOffset = 0; - size_t byteLength = buffer.size(rt); - - if (dataObj.hasProperty(rt, "byteOffset")) { - auto byteOffsetValue = dataObj.getProperty(rt, "byteOffset"); - if (!byteOffsetValue.isNumber()) { - throw jsi::JSError(rt, "setData: Expected 'byteOffset' to be a number"); - } - byteOffset = static_cast(byteOffsetValue.asNumber()); - } - - if (dataObj.hasProperty(rt, "byteLength")) { - auto byteLengthValue = dataObj.getProperty(rt, "byteLength"); - if (!byteLengthValue.isNumber()) { - throw jsi::JSError(rt, "setData: Expected 'byteLength' to be a number"); - } - byteLength = static_cast(byteLengthValue.asNumber()); - } + const auto dataObj = args[0].asObject(rt); + const auto buffer = dataObj.getProperty(rt, "buffer").asObject(rt).getArrayBuffer(rt); + size_t byteOffset = conversions::getOptionalProperty(rt, "setData", dataObj, "byteOffset").value_or(0); + size_t byteLength = conversions::getOptionalProperty(rt, "setData", dataObj, "byteLength").value_or(buffer.size(rt)); - std::unique_lock lock(self->mutex_, std::try_to_lock); - if (!lock.owns_lock()) { - throw jsi::JSError(rt, "setData: Tensor is currently in use and cannot be written to"); - } - - if (!self->data_) { - throw jsi::JSError(rt, "setData: Tensor has been disposed"); - } + auto lock = tryLockUnique(rt, "setData: self", self); if (byteLength != self->size_) { - const std::string errorMsg = "setData: Data size mismatch: TypedArray is " + std::to_string(byteLength) + - " bytes, but Tensor requires " + std::to_string(self->size_) + - " bytes."; - throw jsi::JSError(rt, errorMsg); + throw jsi::JSError(rt, 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); @@ -168,49 +111,16 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) throw jsi::JSError(rt, "getData: Usage: getData(array)"); } - if (!args[0].isObject()) { - throw jsi::JSError(rt, "getData: Expected array to be an object (TypedArray)"); - } - const jsi::Object dataObj = args[0].asObject(rt); - if (!dataObj.hasProperty(rt, "buffer")) { - throw jsi::JSError(rt, "getData: Expected a TypedArray with a 'buffer' property"); - } - const jsi::ArrayBuffer buffer = dataObj.getProperty(rt, "buffer").asObject(rt).getArrayBuffer(rt); - size_t byteOffset = 0; - size_t byteLength = buffer.size(rt); - - if (dataObj.hasProperty(rt, "byteOffset")) { - auto byteOffsetValue = dataObj.getProperty(rt, "byteOffset"); - if (!byteOffsetValue.isNumber()) { - throw jsi::JSError(rt, "getData: Expected 'byteOffset' to be a number"); - } - byteOffset = static_cast(byteOffsetValue.asNumber()); - } - - if (dataObj.hasProperty(rt, "byteLength")) { - auto byteLengthValue = dataObj.getProperty(rt, "byteLength"); - if (!byteLengthValue.isNumber()) { - throw jsi::JSError(rt, "getData: Expected 'byteLength' to be a number"); - } - byteLength = static_cast(byteLengthValue.asNumber()); - } - - std::shared_lock lock(self->mutex_, std::try_to_lock); - if (!lock.owns_lock()) { - throw jsi::JSError(rt, "getData: Tensor is currently in use and cannot be read"); - } + size_t byteOffset = conversions::getOptionalProperty(rt, "getData", dataObj, "byteOffset").value_or(0); + size_t byteLength = conversions::getOptionalProperty(rt, "getData", dataObj, "byteLength").value_or(buffer.size(rt)); - if (!self->data_) { - throw jsi::JSError(rt, "getData: Tensor has been disposed"); - } + auto lock = tryLockShared(rt, "getData: self", self); if (byteLength != self->size_) { - const std::string errorMsg = "getData: Data size mismatch: TypedArray is " + std::to_string(byteLength) + - " bytes, but Tensor requires " + std::to_string(self->size_) + - " bytes."; - throw jsi::JSError(rt, errorMsg); + throw jsi::JSError(rt, 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); @@ -227,10 +137,6 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) throw jsi::JSError(rt, "through: Usage: through(fn, ...args)"); } - if (!args[0].isObject() || !args[0].asObject(rt).isFunction(rt)) { - throw jsi::JSError(rt, "through: First argument must be a function"); - } - auto fn = args[0].asObject(rt).asFunction(rt); std::vector fnArgs; @@ -253,15 +159,11 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) throw jsi::JSError(rt, "throughIf: Usage: throughIf(pred, fn, ...args)"); } - const bool pred = args[0].asBool(); + const bool pred = conversions::asType(rt, "throughIf: pred", args[0]); if (!pred) { return jsi::Value(rt, thisVal); } - if (!args[1].isObject() || !args[1].asObject(rt).isFunction(rt)) { - throw jsi::JSError(rt, "throughIf: Second argument must be a function"); - } - auto fn = args[1].asObject(rt).asFunction(rt); std::vector fnArgs; @@ -301,8 +203,8 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) return jsi::Value::undefined(); } -std::vector TensorHostObject::getPropertyNames(jsi::Runtime &rt) { - std::vector properties; +std::vector TensorHostObject::getPropertyNames(jsi::Runtime &rt) { + std::vector properties; properties.push_back(jsi::PropNameID::forAscii(rt, "shape")); properties.push_back(jsi::PropNameID::forAscii(rt, "dtype")); properties.push_back(jsi::PropNameID::forAscii(rt, "numel")); @@ -322,35 +224,16 @@ void install_createTensor(jsi::Runtime &rt, jsi::Object &module) { throw jsi::JSError(rt, "createTensor: Usage: createTensor(shape, dtype)"); } - if (!args[0].isObject() || !args[0].asObject(rt).isArray(rt)) { - throw jsi::JSError(rt, "createTensor: Expected shape as an array of integers"); - } - - if (!args[1].isString()) { - throw jsi::JSError(rt, "createTensor: Expected dtype as a string"); - } - - auto shapeArray = args[0].asObject(rt).asArray(rt); - std::vector shape; - for (size_t i = 0; i < shapeArray.length(rt); ++i) { - auto dimValue = shapeArray.getValueAtIndex(rt, i); - if (!dimValue.isNumber()) { - throw jsi::JSError(rt, "createTensor: Shape array must contain only numbers"); - } - - if (dimValue.asNumber() <= 0) { - throw jsi::JSError(rt, "createTensor: Shape dimensions must be positive integers"); - } - - shape.push_back(static_cast(dimValue.asNumber())); + 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"); } try { - const auto dtype = rnexecutorch::core::types::parseDType(args[1].asString(rt).utf8(rt)); - auto tensorHostObject = std::make_shared(shape, dtype); - return jsi::Object::createFromHostObject(rt, tensorHostObject); + const auto dtype = types::parseDType(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, "createTensor: Error creating tensor: " + std::string(e.what())); + throw jsi::JSError(rt, std::format("createTensor: Error creating tensor: {}", e.what())); } }; auto fn = jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 2, fnBody); diff --git a/packages/react-native-executorch/cpp/core/tensor.h b/packages/react-native-executorch/cpp/core/tensor.h index 27c9af1199..84fd64c5d2 100644 --- a/packages/react-native-executorch/cpp/core/tensor.h +++ b/packages/react-native-executorch/cpp/core/tensor.h @@ -4,16 +4,18 @@ #include #include #include +#include #include +#include #include -#include -#include - #include "dtype.h" namespace rnexecutorch::core::tensor { +namespace jsi = facebook::jsi; +namespace types = rnexecutorch::core::types; + /** * JSI HostObject wrapping an ExecuTorch TensorPtr instance. * @@ -21,23 +23,25 @@ namespace rnexecutorch::core::tensor { * dtype, numel), writing data from array buffers, reading data to array * buffers, and disposing of underlying memory. */ -class TensorHostObject : public facebook::jsi::HostObject, public std::enable_shared_from_this { +class TensorHostObject : public jsi::HostObject, + public std::enable_shared_from_this { public: - rnexecutorch::core::types::DType dtype_; + types::DType dtype_; std::vector shape_; size_t numel_; - size_t size_; - std::unique_ptr data_; // NOLINT(cppcoreguidelines-avoid-c-arrays,modernize-avoid-c-arrays): owning runtime-sized byte buffer + + // NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays,modernize-avoid-c-arrays): owning runtime-sized byte buffer + std::unique_ptr data_; executorch::extension::TensorPtr tensor_; std::shared_mutex mutex_; - TensorHostObject(const std::vector &shape, rnexecutorch::core::types::DType dtype); + TensorHostObject(const std::vector &shape, types::DType dtype); - facebook::jsi::Value get(facebook::jsi::Runtime &rt, const facebook::jsi::PropNameID &name) override; - std::vector getPropertyNames(facebook::jsi::Runtime &rt) override; + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &name) override; + std::vector getPropertyNames(jsi::Runtime &rt) override; }; -void install_createTensor(facebook::jsi::Runtime &rt, facebook::jsi::Object &module); +void install_createTensor(jsi::Runtime &rt, jsi::Object &module); } // namespace rnexecutorch::core::tensor 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 501b9245c1..fbc740b9b6 100644 --- a/packages/react-native-executorch/cpp/extensions/cv/box_ops.cpp +++ b/packages/react-native-executorch/cpp/extensions/cv/box_ops.cpp @@ -3,7 +3,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -13,11 +15,15 @@ #include "core/dtype.h" #include "core/tensor.h" +#include "core/tensor_helpers.h" #include "utils.h" namespace rnexecutorch::extensions::cv::box_ops { namespace jsi = facebook::jsi; -using rnexecutorch::core::tensor::TensorHostObject; +namespace tensor = rnexecutorch::core::tensor; +namespace conversions = rnexecutorch::core::conversions; + +using rnexecutorch::core::types::DType; namespace { enum class BoxFormat { @@ -75,35 +81,18 @@ void install_nms(jsi::Runtime &rt, jsi::Object &module) { throw jsi::JSError(rt, "Usage: nms(boxes, scores, options)"); } - if (!args[0].isObject() || !args[0].asObject(rt).isHostObject(rt) || - !args[1].isObject() || !args[1].asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "nms: boxes and scores must be Tensors"); - } - - if (!args[2].isObject()) { - throw jsi::JSError(rt, "nms: options must be an object"); - } - - auto boxes = args[0].asObject(rt).getHostObject(rt); - auto scores = args[1].asObject(rt).getHostObject(rt); - auto opts = args[2].asObject(rt); - - if (boxes.get() == scores.get()) { - throw jsi::JSError(rt, "nms: boxes and scores cannot be the same tensor."); - } - - if (!opts.hasProperty(rt, "iouThreshold") || - !opts.hasProperty(rt, "boxFormat") || - !opts.hasProperty(rt, "confidenceThreshold") || - !opts.hasProperty(rt, "nmsType")) { - throw jsi::JSError(rt, "nms: options must specify iouThreshold, boxFormat, confidenceThreshold, and nmsType"); - } + auto boxes = tensor::fromJs(rt, "nms: boxes", args[0], DType::float32, tensor::SymbolicShape{"N", 4}); + auto scores = tensor::fromJs(rt, "nms: scores", args[1], DType::float32, tensor::SymbolicShape{boxes->shape_[0]}); - const float iouThreshold = static_cast(opts.getProperty(rt, "iouThreshold").asNumber()); - const float confidenceThreshold = static_cast(opts.getProperty(rt, "confidenceThreshold").asNumber()); + tensor::checkNotSameTensor(rt, "nms: boxes", boxes, "nms: scores", scores); + auto boxesLock = tensor::tryLockShared(rt, "nms: boxes", boxes); + auto scoresLock = tensor::tryLockShared(rt, "nms: scores", scores); - const std::string nmsTypeStr = opts.getProperty(rt, "nmsType").asString(rt).utf8(rt); - const std::string boxFormatStr = opts.getProperty(rt, "boxFormat").asString(rt).utf8(rt); + const auto opts = args[2].asObject(rt); + const auto nmsTypeStr = conversions::getRequiredProperty(rt, "nms", opts, "nmsType"); + const auto boxFormatStr = conversions::getRequiredProperty(rt, "nms", opts, "boxFormat"); + const auto iouThreshold = conversions::getRequiredProperty(rt, "nms", opts, "iouThreshold"); + const auto confidenceThreshold = conversions::getRequiredProperty(rt, "nms", opts, "confidenceThreshold"); NmsType nmsType{}; BoxFormat boxFormat{}; @@ -111,37 +100,10 @@ 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, "nms: " + std::string(e.what())); - } - - std::shared_lock boxesLock(boxes->mutex_, std::try_to_lock); - std::shared_lock scoresLock(scores->mutex_, std::try_to_lock); - - if (!boxesLock.owns_lock() || !scoresLock.owns_lock()) { - throw jsi::JSError(rt, "nms: one of the tensors is currently locked"); + throw jsi::JSError(rt, std::format("nms: {}", e.what())); } - if (!boxes->data_ || !scores->data_) { - throw jsi::JSError(rt, "nms: tensors must not be disposed"); - } - - if (scores->shape_.size() != 1) { - throw jsi::JSError(rt, "nms: scores must be a 1D tensor with shape [N]"); - } std::int32_t numAnchors = scores->shape_[0]; - - if (boxes->shape_.size() != 2 || boxes->shape_[1] != 4) { - throw jsi::JSError(rt, "nms: boxes must be a 2D tensor with shape [N, 4]"); - } - - if (boxes->shape_[0] != numAnchors) { - throw jsi::JSError(rt, "nms: boxes and scores must have the same number of elements"); - } - - if (boxes->dtype_ != rnexecutorch::core::types::DType::float32 || scores->dtype_ != rnexecutorch::core::types::DType::float32) { - throw jsi::JSError(rt, "nms: boxes and scores must have dtype float32"); - } - const auto *boxesPtr = reinterpret_cast(boxes->data_.get()); const auto *scoresPtr = reinterpret_cast(scores->data_.get()); @@ -233,11 +195,7 @@ void install_nms(jsi::Runtime &rt, jsi::Object &module) { case NmsType::Weighted: { jsi::Array resultGroups = jsi::Array(rt, groups.size()); for (size_t i = 0; i < groups.size(); ++i) { - const jsi::Array singleGroup = jsi::Array(rt, groups[i].size()); - for (size_t j = 0; j < groups[i].size(); ++j) { - singleGroup.setValueAtIndex(rt, j, jsi::Value(static_cast(groups[i][j]))); - } - resultGroups.setValueAtIndex(rt, i, singleGroup); + resultGroups.setValueAtIndex(rt, i, conversions::toJsiArray(rt, groups[i])); } return resultGroups; } @@ -254,57 +212,33 @@ void install_restrictToBox(jsi::Runtime &rt, jsi::Object &module) { throw jsi::JSError(rt, "Usage: restrictToBox(src, dst, boxTuple, format)"); } - if (!args[0].isObject() || !args[0].asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "restrictToBox: src must be a Tensor"); - } - - if (!args[1].isObject() || !args[1].asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "restrictToBox: dst must be a Tensor"); - } - - if (!args[2].isObject() || !args[2].asObject(rt).isArray(rt)) { - throw jsi::JSError(rt, "restrictToBox: boxTuple must be an Array"); - } - - if (!args[3].isString()) { - throw jsi::JSError(rt, "restrictToBox: format must be a String"); - } + auto src = tensor::fromJs(rt, "restrictToBox: src", args[0], std::nullopt, tensor::SymbolicShape{"H", "W", "C"}); + auto dst = tensor::fromJs(rt, "restrictToBox: dst", args[1], src->dtype_, src->shape_); - auto src = args[0].asObject(rt).getHostObject(rt); - auto dst = args[1].asObject(rt).getHostObject(rt); - auto boxTuple = args[2].asObject(rt).asArray(rt); - std::string boxFormatStr = args[3].asString(rt).utf8(rt); + tensor::checkNotSameTensor(rt, "restrictToBox: src", src, "restrictToBox: dst", dst); + auto srcLock = tensor::tryLockShared(rt, "restrictToBox: src", src); + auto dstLock = tensor::tryLockUnique(rt, "restrictToBox: dst", dst); - if (boxTuple.size(rt) != 4) { + auto boxVec = conversions::asVector(rt, "restrictToBox: boxTuple", args[2]); + if (boxVec.size() != 4) { throw jsi::JSError(rt, "restrictToBox: boxTuple must contain exactly 4 coordinates"); } + auto boxFormatStr = conversions::asType(rt, "restrictToBox: format", args[3]); BoxFormat boxFormat{}; try { boxFormat = parseBoxFormat(boxFormatStr); } catch (const std::invalid_argument &e) { - throw jsi::JSError(rt, "restrictToBox: " + std::string(e.what())); + throw jsi::JSError(rt, std::format("restrictToBox: {}", e.what())); } - float a = static_cast(boxTuple.getValueAtIndex(rt, 0).asNumber()); - float b = static_cast(boxTuple.getValueAtIndex(rt, 1).asNumber()); - float c = static_cast(boxTuple.getValueAtIndex(rt, 2).asNumber()); - float d = static_cast(boxTuple.getValueAtIndex(rt, 3).asNumber()); + float a = boxVec[0]; + float b = boxVec[1]; + float c = boxVec[2]; + float d = boxVec[3]; auto [xmin, ymin, xmax, ymax] = decodeToXyxy(a, b, c, d, boxFormat); - if (src.get() == dst.get()) { - throw jsi::JSError(rt, "restrictToBox: In-place operations (src == dst) are not supported."); - } - - if (src->shape_ != dst->shape_) { - throw jsi::JSError(rt, "restrictToBox: src and dst must have the same shape"); - } - - if (src->shape_.size() != 3) { - throw jsi::JSError(rt, "restrictToBox: src must be a 3D tensor of shape [H, W, C]"); - } - int32_t H = src->shape_[0]; int32_t W = src->shape_[1]; int32_t C = src->shape_[2]; @@ -321,36 +255,21 @@ void install_restrictToBox(jsi::Runtime &rt, jsi::Object &module) { bool isEmpty = (x2 < x1) || (y2 < y1); - if (src->dtype_ != dst->dtype_) { - throw jsi::JSError(rt, "restrictToBox: src and dst must have the same dtype"); - } - - std::shared_lock srcLock(src->mutex_, std::try_to_lock); - std::unique_lock dstLock(dst->mutex_, std::try_to_lock); - if (!srcLock.owns_lock() || !dstLock.owns_lock()) { - throw jsi::JSError(rt, "restrictToBox: tensors in use"); - } - - if (!src->data_ || !dst->data_) { - throw jsi::JSError(rt, "restrictToBox: tensors must not be disposed"); - } - - int32_t cvType{}; try { - cvType = CV_MAKETYPE(dtypeToCvDepth(src->dtype_), C); - } catch (const std::invalid_argument &e) { - throw jsi::JSError(rt, "restrictToBox: " + std::string(e.what())); - } + const int32_t cvType = CV_MAKETYPE(dtypeToCvDepth(src->dtype_), C); - ::cv::Mat srcMat(H, W, cvType, src->data_.get()); - ::cv::Mat dstMat(H, W, cvType, dst->data_.get()); + ::cv::Mat srcMat(H, W, cvType, src->data_.get()); + ::cv::Mat dstMat(H, W, cvType, dst->data_.get()); - dstMat.setTo(::cv::Scalar::all(0)); - if (!isEmpty) { - int32_t boxW = x2 - x1 + 1; - int32_t boxH = y2 - y1 + 1; - ::cv::Rect roi(x1, y1, boxW, boxH); - srcMat(roi).copyTo(dstMat(roi)); + dstMat.setTo(::cv::Scalar::all(0)); + if (!isEmpty) { + int32_t boxW = x2 - x1 + 1; + int32_t boxH = y2 - y1 + 1; + ::cv::Rect roi(x1, y1, boxW, boxH); + srcMat(roi).copyTo(dstMat(roi)); + } + } catch (const std::exception &e) { + throw jsi::JSError(rt, "restrictToBox: " + std::string(e.what())); } return jsi::Value(rt, args[1]); 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 13bf66cd81..c2676d92f2 100644 --- a/packages/react-native-executorch/cpp/extensions/cv/image_ops.cpp +++ b/packages/react-native-executorch/cpp/extensions/cv/image_ops.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -10,11 +11,15 @@ #include "core/dtype.h" #include "core/tensor.h" +#include "core/tensor_helpers.h" #include "utils.h" namespace rnexecutorch::extensions::cv::image_ops { namespace jsi = facebook::jsi; -using TensorHostObject = rnexecutorch::core::tensor::TensorHostObject; +namespace tensor = rnexecutorch::core::tensor; +namespace conversions = rnexecutorch::core::conversions; + +using rnexecutorch::core::types::DType; namespace { int interpToFlag(const std::string &interp) { @@ -59,77 +64,17 @@ void install_resize(jsi::Runtime &rt, jsi::Object &module) { throw jsi::JSError(rt, "Usage: resize(src, dst, options)"); } - if (!args[0].isObject() || !args[0].asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "resize: src must be a Tensor"); - } - - if (!args[1].isObject() || !args[1].asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "resize: dst must be a Tensor"); - } - - if (!args[2].isObject()) { - throw jsi::JSError(rt, "resize: options must be an object"); - } + auto src = tensor::fromJs(rt, "resize: src", args[0], std::nullopt, tensor::SymbolicShape{"H", "W", "C"}); + auto dst = tensor::fromJs(rt, "resize: dst", args[1], src->dtype_, tensor::SymbolicShape{"H'", "W'", src->shape_[2]}); - auto src = args[0].asObject(rt).getHostObject(rt); - auto dst = args[1].asObject(rt).getHostObject(rt); - - if (src.get() == dst.get()) { - throw jsi::JSError(rt, "resize: In-place operations (src == dst) are not supported."); - } - auto opts = args[2].asObject(rt); - - if (!opts.hasProperty(rt, "mode") || !opts.getProperty(rt, "mode").isString()) { - throw jsi::JSError(rt, "resize: options.mode is required and must be a string"); - } + tensor::checkNotSameTensor(rt, "resize: src", src, "resize: dst", dst); + auto srcLock = tensor::tryLockShared(rt, "resize: src", src); + auto dstLock = tensor::tryLockUnique(rt, "resize: dst", dst); - if (!opts.hasProperty(rt, "interpolation") || !opts.getProperty(rt, "interpolation").isString()) { - throw jsi::JSError(rt, "resize: options.interpolation is required and must be a string"); - } - - if (!opts.hasProperty(rt, "padValue") || !opts.getProperty(rt, "padValue").isNumber()) { - throw jsi::JSError(rt, "resize: options.padValue is required and must be a number"); - } - - auto mode = opts.getProperty(rt, "mode").asString(rt).utf8(rt); - auto interp = opts.getProperty(rt, "interpolation").asString(rt).utf8(rt); - const double padValue = opts.getProperty(rt, "padValue").asNumber(); - - if (src->shape_.size() != 3) { - throw jsi::JSError(rt, "resize: src must be [H, W, C]"); - } - - if (dst->shape_.size() != 3) { - throw jsi::JSError(rt, "resize: dst must be [H, W, C]"); - } - - if (src->shape_[2] != dst->shape_[2]) { - throw jsi::JSError(rt, "resize: src and dst must have the same number of channels"); - } - - if (src->dtype_ != dst->dtype_) { - throw jsi::JSError(rt, "resize: src and dst must have the same dtype"); - } - - // shared on src (read-only), unique on dst (write) - - std::shared_lock srcLock(src->mutex_, std::try_to_lock); - if (!srcLock.owns_lock()) { - throw jsi::JSError(rt, "resize: src tensor is currently in use"); - } - - std::unique_lock dstLock(dst->mutex_, std::try_to_lock); - if (!dstLock.owns_lock()) { - throw jsi::JSError(rt, "resize: dst tensor is currently in use"); - } - - if (!src->data_) { - throw jsi::JSError(rt, "resize: src tensor has been disposed"); - } - - if (!dst->data_) { - throw jsi::JSError(rt, "resize: dst tensor has been disposed"); - } + const auto opts = args[2].asObject(rt); + const auto mode = conversions::getRequiredProperty(rt, "resize: options", opts, "mode"); + const auto interp = conversions::getRequiredProperty(rt, "resize: options", opts, "interpolation"); + const auto padValue = conversions::getRequiredProperty(rt, "resize: options", opts, "padValue"); const int32_t srcH = src->shape_[0]; const int32_t srcW = src->shape_[1]; @@ -142,29 +87,35 @@ void install_resize(jsi::Runtime &rt, jsi::Object &module) { try { cvType = CV_MAKETYPE(dtypeToCvDepth(src->dtype_), channels); interpFlag = interpToFlag(interp); - } catch (const std::invalid_argument &e) { + } catch (const std::exception &e) { throw jsi::JSError(rt, "resize: " + std::string(e.what())); } - const ::cv::Mat srcMat(srcH, srcW, cvType, src->data_.get()); - ::cv::Mat dstMat(dstH, dstW, cvType, dst->data_.get()); - - if (mode == "stretch") { - ::cv::resize(srcMat, dstMat, dstMat.size(), 0, 0, interpFlag); - } else if (mode == "letterbox") { - const FitBox fit = computeFit(srcW, srcH, dstW, dstH, /*inner=*/true); - - dstMat.setTo(::cv::Scalar::all(padValue)); - ::cv::Mat roi = dstMat(::cv::Rect(fit.offX, fit.offY, fit.w, fit.h)); - ::cv::resize(srcMat, roi, roi.size(), 0, 0, interpFlag); - } else if (mode == "crop") { - const FitBox fit = computeFit(srcW, srcH, dstW, dstH, /*inner=*/false); - - ::cv::Mat scaled; - ::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'"); + try { + const ::cv::Mat srcMat(srcH, srcW, cvType, src->data_.get()); + ::cv::Mat dstMat(dstH, dstW, cvType, dst->data_.get()); + + if (mode == "stretch") { + ::cv::resize(srcMat, dstMat, dstMat.size(), 0, 0, interpFlag); + } else if (mode == "letterbox") { + const FitBox fit = computeFit(srcW, srcH, dstW, dstH, /*inner=*/true); + + dstMat.setTo(::cv::Scalar::all(padValue)); + ::cv::Mat roi = dstMat(::cv::Rect(fit.offX, fit.offY, fit.w, fit.h)); + ::cv::resize(srcMat, roi, roi.size(), 0, 0, interpFlag); + } else if (mode == "crop") { + const FitBox fit = computeFit(srcW, srcH, dstW, dstH, /*inner=*/false); + + ::cv::Mat scaled; + ::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'"); + } + } catch (const jsi::JSError &) { + throw; + } catch (const std::exception &e) { + throw jsi::JSError(rt, "resize: " + std::string(e.what())); } return jsi::Value(rt, args[1]); @@ -246,65 +197,20 @@ void install_cvtColor(jsi::Runtime &rt, jsi::Object &module) { throw jsi::JSError(rt, "Usage: cvtColor(src, dst, code)"); } - if (!args[0].isObject() || !args[0].asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "cvtColor: src must be a Tensor"); - } - - if (!args[1].isObject() || !args[1].asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "cvtColor: dst must be a Tensor"); - } - - if (!args[2].isString()) { - throw jsi::JSError(rt, "cvtColor: code must be a string"); - } + auto src = tensor::fromJs(rt, "cvtColor: src", args[0], std::nullopt, tensor::SymbolicShape{"H", "W", "C"}); + auto dst = tensor::fromJs(rt, "cvtColor: dst", args[1], src->dtype_, tensor::SymbolicShape{src->shape_[0], src->shape_[1], "C'"}); - auto src = args[0].asObject(rt).getHostObject(rt); - auto dst = args[1].asObject(rt).getHostObject(rt); - - if (src.get() == dst.get()) { - throw jsi::JSError(rt, "cvtColor: In-place operations (src == dst) are not supported."); - } - auto code = args[2].asString(rt).utf8(rt); - - if (src->shape_.size() != 3) { - throw jsi::JSError(rt, "cvtColor: src must be a 3D tensor [H, W, C]"); - } - - if (dst->shape_.size() != 3) { - throw jsi::JSError(rt, "cvtColor: dst must be a 3D tensor [H, W, C]"); - } - - if (src->shape_[0] != dst->shape_[0] || src->shape_[1] != dst->shape_[1]) { - throw jsi::JSError(rt, "cvtColor: src and dst spatial dimensions (H, W) must match"); - } - - if (src->dtype_ != dst->dtype_) { - throw jsi::JSError(rt, "cvtColor: src and dst must have the same dtype"); - } - - std::shared_lock srcLock(src->mutex_, std::try_to_lock); - if (!srcLock.owns_lock()) { - throw jsi::JSError(rt, "cvtColor: src tensor is currently in use"); - } - - std::unique_lock dstLock(dst->mutex_, std::try_to_lock); - if (!dstLock.owns_lock()) { - throw jsi::JSError(rt, "cvtColor: dst tensor is currently in use"); - } - - if (!src->data_) { - throw jsi::JSError(rt, "cvtColor: src tensor has been disposed"); - } - - if (!dst->data_) { - throw jsi::JSError(rt, "cvtColor: dst tensor has been disposed"); - } + tensor::checkNotSameTensor(rt, "cvtColor: src", src, "cvtColor: dst", dst); + auto srcLock = tensor::tryLockShared(rt, "cvtColor: src", src); + auto dstLock = tensor::tryLockUnique(rt, "cvtColor: dst", dst); const int32_t srcH = src->shape_[0]; const int32_t srcW = src->shape_[1]; const int32_t srcC = src->shape_[2]; const int32_t dstC = dst->shape_[2]; + auto code = conversions::asType(rt, "cvtColor: code", args[2]); + int cvSrcType{}; int cvDstType{}; int flag{}; @@ -317,7 +223,7 @@ void install_cvtColor(jsi::Runtime &rt, jsi::Object &module) { ::cv::Mat dstMat(srcH, srcW, cvDstType, dst->data_.get()); ::cv::cvtColor(srcMat, dstMat, flag); - } catch (const std::invalid_argument &e) { + } catch (const std::exception &e) { throw jsi::JSError(rt, "cvtColor: " + std::string(e.what())); } @@ -334,79 +240,33 @@ void install_toChannelsFirst(jsi::Runtime &rt, jsi::Object &module) { throw jsi::JSError(rt, "Usage: toChannelsFirst(src, dst)"); } - if (!args[0].isObject() || !args[0].asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "toChannelsFirst: src must be a Tensor"); - } - - if (!args[1].isObject() || !args[1].asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "toChannelsFirst: dst must be a Tensor"); - } - - auto src = args[0].asObject(rt).getHostObject(rt); - auto dst = args[1].asObject(rt).getHostObject(rt); - - if (src.get() == dst.get()) { - throw jsi::JSError(rt, "toChannelsFirst: In-place operations (src == dst) are not supported."); - } - - if (src->shape_.size() != 3) { - throw jsi::JSError(rt, "toChannelsFirst: src must be a 3D tensor [H, W, C]"); - } + auto src = tensor::fromJs(rt, "toChannelsFirst: src", args[0], std::nullopt, tensor::SymbolicShape{"H", "W", "C"}); + auto dst = tensor::fromJs(rt, "toChannelsFirst: dst", args[1], src->dtype_, tensor::SymbolicShape{src->shape_[2], src->shape_[0], src->shape_[1]}); - if (src->dtype_ != dst->dtype_) { - throw jsi::JSError(rt, "toChannelsFirst: src and dst must have the same dtype"); - } + tensor::checkNotSameTensor(rt, "toChannelsFirst: src", src, "toChannelsFirst: dst", dst); + auto srcLock = tensor::tryLockShared(rt, "toChannelsFirst: src", src); + auto dstLock = tensor::tryLockUnique(rt, "toChannelsFirst: dst", dst); const int32_t srcH = src->shape_[0]; const int32_t srcW = src->shape_[1]; const int32_t srcC = src->shape_[2]; - if (dst->shape_.size() != 3) { - throw jsi::JSError(rt, "toChannelsFirst: dst must be a 3D tensor [C, H, W]"); - } - const int32_t dstC = dst->shape_[0]; - const int32_t dstH = dst->shape_[1]; - const int32_t dstW = dst->shape_[2]; - - if (srcH != dstH || srcW != dstW || srcC != dstC) { - throw jsi::JSError(rt, "toChannelsFirst: src and dst spatial dimensions and channel counts must match"); - } - - std::shared_lock srcLock(src->mutex_, std::try_to_lock); - if (!srcLock.owns_lock()) { - throw jsi::JSError(rt, "toChannelsFirst: src tensor is currently in use"); - } - - std::unique_lock dstLock(dst->mutex_, std::try_to_lock); - if (!dstLock.owns_lock()) { - throw jsi::JSError(rt, "toChannelsFirst: dst tensor is currently in use"); - } - - if (!src->data_) { - throw jsi::JSError(rt, "toChannelsFirst: src tensor has been disposed"); - } - - if (!dst->data_) { - throw jsi::JSError(rt, "toChannelsFirst: dst tensor has been disposed"); - } - - int cvType{}; try { - cvType = CV_MAKETYPE(dtypeToCvDepth(src->dtype_), srcC); - } catch (const std::invalid_argument &e) { - throw jsi::JSError(rt, "toChannelsFirst: " + std::string(e.what())); - } + const int cvType = CV_MAKETYPE(dtypeToCvDepth(src->dtype_), srcC); - const ::cv::Mat srcMat(srcH, srcW, cvType, src->data_.get()); - std::vector<::cv::Mat> channels; - ::cv::split(srcMat, channels); + const ::cv::Mat srcMat(srcH, srcW, cvType, src->data_.get()); + std::vector<::cv::Mat> channels; + ::cv::split(srcMat, channels); - const size_t hw = static_cast(srcH) * static_cast(srcW); - const size_t elemSize = rnexecutorch::core::types::elementSize(src->dtype_); - uint8_t *dstPtr = dst->data_.get(); + const size_t hw = static_cast(srcH) * static_cast(srcW); + const size_t elemSize = rnexecutorch::core::types::elementSize(src->dtype_); + uint8_t *dstPtr = dst->data_.get(); - for (size_t i = 0; std::cmp_less(i, srcC); ++i) { - std::memcpy(dstPtr + i * hw * elemSize, channels[i].data, hw * elemSize); + for (size_t i = 0; std::cmp_less(i, srcC); ++i) { + std::memcpy(dstPtr + i * hw * elemSize, channels[i].data, hw * elemSize); + } + } catch (const std::exception &e) { + throw jsi::JSError(rt, "toChannelsFirst: " + std::string(e.what())); } return jsi::Value(rt, args[1]); @@ -422,81 +282,35 @@ void install_toChannelsLast(jsi::Runtime &rt, jsi::Object &module) { throw jsi::JSError(rt, "Usage: toChannelsLast(src, dst)"); } - if (!args[0].isObject() || !args[0].asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "toChannelsLast: src must be a Tensor"); - } + auto src = tensor::fromJs(rt, "toChannelsLast: src", args[0], std::nullopt, tensor::SymbolicShape{"C", "H", "W"}); + auto dst = tensor::fromJs(rt, "toChannelsLast: dst", args[1], src->dtype_, tensor::SymbolicShape{src->shape_[1], src->shape_[2], src->shape_[0]}); - if (!args[1].isObject() || !args[1].asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "toChannelsLast: dst must be a Tensor"); - } - - auto src = args[0].asObject(rt).getHostObject(rt); - auto dst = args[1].asObject(rt).getHostObject(rt); - - if (src.get() == dst.get()) { - throw jsi::JSError(rt, "toChannelsLast: In-place operations (src == dst) are not supported."); - } - - if (src->shape_.size() != 3) { - throw jsi::JSError(rt, "toChannelsLast: src must be a 3D tensor [C, H, W]"); - } - - if (src->dtype_ != dst->dtype_) { - throw jsi::JSError(rt, "toChannelsLast: src and dst must have the same dtype"); - } + tensor::checkNotSameTensor(rt, "toChannelsLast: src", src, "toChannelsLast: dst", dst); + auto srcLock = tensor::tryLockShared(rt, "toChannelsLast: src", src); + auto dstLock = tensor::tryLockUnique(rt, "toChannelsLast: dst", dst); const int32_t srcC = src->shape_[0]; const int32_t srcH = src->shape_[1]; const int32_t srcW = src->shape_[2]; - if (dst->shape_.size() != 3) { - throw jsi::JSError(rt, "toChannelsLast: dst must be a 3D tensor [H, W, C]"); - } - const int32_t dstH = dst->shape_[0]; - const int32_t dstW = dst->shape_[1]; - const int32_t dstC = dst->shape_[2]; - - if (srcH != dstH || srcW != dstW || srcC != dstC) { - throw jsi::JSError(rt, "toChannelsLast: src and dst spatial dimensions and channel counts must match"); - } - - std::shared_lock srcLock(src->mutex_, std::try_to_lock); - if (!srcLock.owns_lock()) { - throw jsi::JSError(rt, "toChannelsLast: src tensor is currently in use"); - } - - std::unique_lock dstLock(dst->mutex_, std::try_to_lock); - if (!dstLock.owns_lock()) { - throw jsi::JSError(rt, "toChannelsLast: dst tensor is currently in use"); - } + try { + const int cvDepth = dtypeToCvDepth(src->dtype_); - if (!src->data_) { - throw jsi::JSError(rt, "toChannelsLast: src tensor has been disposed"); - } + const size_t hw = static_cast(srcH) * static_cast(srcW); + const size_t elemSize = rnexecutorch::core::types::elementSize(src->dtype_); + uint8_t *srcPtr = src->data_.get(); - if (!dst->data_) { - throw jsi::JSError(rt, "toChannelsLast: dst tensor has been disposed"); - } + std::vector<::cv::Mat> channels; + for (size_t i = 0; std::cmp_less(i, srcC); ++i) { + channels.emplace_back(srcH, srcW, cvDepth, srcPtr + i * hw * elemSize); + } - int cvDepth{}; - try { - cvDepth = dtypeToCvDepth(src->dtype_); - } catch (const std::invalid_argument &e) { + ::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())); } - const size_t hw = static_cast(srcH) * static_cast(srcW); - const size_t elemSize = rnexecutorch::core::types::elementSize(src->dtype_); - uint8_t *srcPtr = src->data_.get(); - - std::vector<::cv::Mat> channels; - for (size_t i = 0; std::cmp_less(i, srcC); ++i) { - channels.emplace_back(srcH, srcW, cvDepth, srcPtr + i * hw * elemSize); - } - - ::cv::Mat dstMat(dstH, dstW, CV_MAKETYPE(cvDepth, dstC), dst->data_.get()); - ::cv::merge(channels, dstMat); - return jsi::Value(rt, args[1]); }; @@ -510,114 +324,55 @@ void install_normalize(jsi::Runtime &rt, jsi::Object &module) { throw jsi::JSError(rt, "Usage: normalize(src, dst, options)"); } - if (!args[0].isObject() || !args[0].asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "normalize: src must be a Tensor"); - } - - if (!args[1].isObject() || !args[1].asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "normalize: dst must be a Tensor"); - } - - if (!args[2].isObject()) { - throw jsi::JSError(rt, "normalize: options must be an object"); - } + auto src = tensor::fromJs(rt, "normalize: src", args[0], std::nullopt, tensor::SymbolicShape{"C", "H", "W"}); + auto dst = tensor::fromJs(rt, "normalize: dst", args[1], std::nullopt, src->shape_); - auto src = args[0].asObject(rt).getHostObject(rt); - auto dst = args[1].asObject(rt).getHostObject(rt); + tensor::checkNotSameTensor(rt, "normalize: src", src, "normalize: dst", dst); + auto srcLock = tensor::tryLockShared(rt, "normalize: src", src); + auto dstLock = tensor::tryLockUnique(rt, "normalize: dst", dst); - if (src.get() == dst.get()) { - throw jsi::JSError(rt, "normalize: In-place operations (src == dst) are not supported."); - } auto opts = args[2].asObject(rt); - if (src->shape_.size() != 3) { - throw jsi::JSError(rt, "normalize: src must be a 3D tensor [C, H, W]"); - } - - int32_t c = src->shape_[0]; + const int32_t c = src->shape_[0]; const int32_t h = src->shape_[1]; const int32_t w = src->shape_[2]; - if (dst->shape_.size() != 3 || - dst->shape_[0] != c || - dst->shape_[1] != h || - dst->shape_[2] != w) { - throw jsi::JSError(rt, "normalize: src and dst shapes must match exactly ([C, H, W])"); - } - - auto getNormalizeOption = [&](const char *name) -> std::vector { - if (!opts.hasProperty(rt, name)) { - throw jsi::JSError(rt, "normalize: options." + std::string(name) + " is required"); - } - - auto val = opts.getProperty(rt, name); + auto getNormalizeOption = [&](const char *optName) -> std::vector { + auto val = conversions::getRequiredProperty(rt, "normalize", opts, optName); std::vector result(static_cast(c)); - if (val.isNumber()) { std::ranges::fill(result, val.asNumber()); - } else if (val.isObject() && val.asObject(rt).isArray(rt)) { - auto arr = val.asObject(rt).asArray(rt); - if (arr.length(rt) != static_cast(c)) { - throw jsi::JSError(rt, "normalize: options." + std::string(name) + - " array length must be exactly equal to channels"); - } - for (size_t i = 0; std::cmp_less(i, c); ++i) { - auto item = arr.getValueAtIndex(rt, i); - if (!item.isNumber()) { - throw jsi::JSError(rt, "normalize: options." + std::string(name) + - " array must contain only numbers"); - } - result[i] = item.asNumber(); - } } else { - throw jsi::JSError(rt, "normalize: options." + std::string(name) + - " must be a number or an array of numbers"); + 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)); + } + result = std::move(arr); } - return result; }; std::vector alpha = getNormalizeOption("alpha"); std::vector beta = getNormalizeOption("beta"); - std::shared_lock srcLock(src->mutex_, std::try_to_lock); - if (!srcLock.owns_lock()) { - throw jsi::JSError(rt, "normalize: src tensor is currently in use"); - } - - std::unique_lock dstLock(dst->mutex_, std::try_to_lock); - if (!dstLock.owns_lock()) { - throw jsi::JSError(rt, "normalize: dst tensor is currently in use"); - } - - if (!src->data_) { - throw jsi::JSError(rt, "normalize: src tensor has been disposed"); - } - - if (!dst->data_) { - throw jsi::JSError(rt, "normalize: dst tensor has been disposed"); - } - - int srcDepthType{}; - int dstDepthType{}; try { - srcDepthType = dtypeToCvDepth(src->dtype_); - dstDepthType = dtypeToCvDepth(dst->dtype_); - } catch (const std::invalid_argument &e) { - throw jsi::JSError(rt, "normalize: " + std::string(e.what())); - } + const int srcDepthType = dtypeToCvDepth(src->dtype_); + const int dstDepthType = dtypeToCvDepth(dst->dtype_); - const size_t srcElemSize = rnexecutorch::core::types::elementSize(src->dtype_); - const size_t dstElemSize = rnexecutorch::core::types::elementSize(dst->dtype_); - uint8_t *srcPtr = src->data_.get(); - uint8_t *dstPtr = dst->data_.get(); + const size_t srcElemSize = rnexecutorch::core::types::elementSize(src->dtype_); + const size_t dstElemSize = rnexecutorch::core::types::elementSize(dst->dtype_); + uint8_t *srcPtr = src->data_.get(); + uint8_t *dstPtr = dst->data_.get(); - const size_t plane = static_cast(h) * static_cast(w); - for (size_t ch = 0; std::cmp_less(ch, c); ++ch) { - const ::cv::Mat srcChannel(h, w, srcDepthType, srcPtr + ch * plane * srcElemSize); - ::cv::Mat dstChannel(h, w, dstDepthType, dstPtr + ch * plane * dstElemSize); + const size_t plane = static_cast(h) * static_cast(w); + for (size_t ch = 0; std::cmp_less(ch, c); ++ch) { + const ::cv::Mat srcChannel(h, w, srcDepthType, srcPtr + ch * plane * srcElemSize); + ::cv::Mat dstChannel(h, w, dstDepthType, dstPtr + ch * plane * dstElemSize); - srcChannel.convertTo(dstChannel, dstDepthType, alpha[ch], beta[ch]); + srcChannel.convertTo(dstChannel, dstDepthType, alpha[ch], beta[ch]); + } + } catch (const std::exception &e) { + throw jsi::JSError(rt, "normalize: " + std::string(e.what())); } return jsi::Value(rt, args[1]); @@ -633,66 +388,35 @@ void install_applyColormap(jsi::Runtime &rt, jsi::Object &module) { throw jsi::JSError(rt, "Usage: applyColormap(src, dst, colormap)"); } - if (!args[0].isObject() || !args[0].asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "applyColormap: src must be a Tensor"); - } - if (!args[1].isObject() || !args[1].asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "applyColormap: dst must be a Tensor"); + if (!args[2].isObject() || !args[2].asObject(rt).isArray(rt)) { + throw jsi::JSError(rt, "applyColormap: colormap must be an array"); } - auto src = args[0].asObject(rt).getHostObject(rt); - auto dst = args[1].asObject(rt).getHostObject(rt); - constexpr size_t numRgbaChannels = 4; + auto src = tensor::fromJs(rt, "applyColormap: src", args[0], DType::int32, std::nullopt); + auto dst = tensor::fromJs(rt, "applyColormap: dst", args[1], DType::uint8, std::nullopt); - if (src->dtype_ != rnexecutorch::core::types::DType::int32) { - throw jsi::JSError(rt, "applyColormap: src must be int32"); - } - if (dst->dtype_ != rnexecutorch::core::types::DType::uint8) { - throw jsi::JSError(rt, "applyColormap: dst must be uint8"); - } + tensor::checkNotSameTensor(rt, "applyColormap: src", src, "applyColormap: dst", dst); + auto srcLock = tensor::tryLockShared(rt, "applyColormap: src", src); + auto dstLock = tensor::tryLockUnique(rt, "applyColormap: dst", dst); + + constexpr size_t numRgbaChannels = 4; if (dst->numel_ != src->numel_ * numRgbaChannels) { throw jsi::JSError(rt, "applyColormap: dst must have exactly 4 times the number of elements as src (RGBA channels)"); } - if (!args[2].isObject() || !args[2].asObject(rt).isArray(rt)) { - throw jsi::JSError(rt, "applyColormap: colormap must be an array"); - } - auto colormapArray = args[2].asObject(rt).asArray(rt); const size_t numColors = colormapArray.size(rt); std::vector> lut(numColors); for (size_t i = 0; i < numColors; ++i) { - auto colorVal = colormapArray.getValueAtIndex(rt, i); - if (!colorVal.isObject() || !colorVal.asObject(rt).isArray(rt)) { - throw jsi::JSError(rt, "applyColormap: colormap entry must be an array"); - } - auto color = colorVal.asObject(rt).asArray(rt); - if (color.size(rt) != numRgbaChannels) { + auto colorVec = conversions::asVector(rt, "applyColormap: colormap entry", colormapArray.getValueAtIndex(rt, i)); + if (colorVec.size() != numRgbaChannels) { throw jsi::JSError(rt, "applyColormap: colormap entry must be an RGBA color array of size 4"); } for (size_t c = 0; c < numRgbaChannels; ++c) { - auto channelVal = color.getValueAtIndex(rt, c); - if (!channelVal.isNumber()) { - throw jsi::JSError(rt, "applyColormap: colormap channel value must be a number"); - } - const double val = channelVal.asNumber(); - if (std::isnan(val) || val < 0.0 || val > 255.0) { - throw jsi::JSError(rt, "applyColormap: colormap channel value must be between 0 and 255"); - } - lut[i][c] = static_cast(val); + lut[i][c] = colorVec[c]; } } - std::shared_lock srcLock(src->mutex_, std::try_to_lock); - std::unique_lock dstLock(dst->mutex_, std::try_to_lock); - if (!srcLock.owns_lock() || !dstLock.owns_lock()) { - throw jsi::JSError(rt, "applyColormap: tensors in use"); - } - - if (!src->data_ || !dst->data_) { - throw jsi::JSError(rt, "applyColormap: tensor has been disposed"); - } - const size_t pixels = src->numel_; const auto *srcData = reinterpret_cast(src->data_.get()); diff --git a/packages/react-native-executorch/cpp/extensions/math/operations.cpp b/packages/react-native-executorch/cpp/extensions/math/operations.cpp index 4607af49b9..739f2ac626 100644 --- a/packages/react-native-executorch/cpp/extensions/math/operations.cpp +++ b/packages/react-native-executorch/cpp/extensions/math/operations.cpp @@ -3,13 +3,18 @@ #include #include #include +#include #include #include "core/tensor.h" +#include "core/tensor_helpers.h" namespace rnexecutorch::extensions::math { namespace jsi = facebook::jsi; -using TensorHostObject = rnexecutorch::core::tensor::TensorHostObject; +namespace conversions = rnexecutorch::core::conversions; + +namespace tensor = rnexecutorch::core::tensor; +using rnexecutorch::core::types::DType; void install_sigmoid(jsi::Runtime &rt, jsi::Object &module) { const auto *name = "sigmoid"; @@ -18,50 +23,12 @@ void install_sigmoid(jsi::Runtime &rt, jsi::Object &module) { throw jsi::JSError(rt, "Usage: sigmoid(src, dst)"); } - if (!args[0].isObject() || !args[0].asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "sigmoid: src must be a Tensor"); - } - - if (!args[1].isObject() || !args[1].asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "sigmoid: dst must be a Tensor"); - } - - auto src = args[0].asObject(rt).getHostObject(rt); - auto dst = args[1].asObject(rt).getHostObject(rt); - - if (src.get() == dst.get()) { - throw jsi::JSError(rt, "sigmoid: In-place operations (src == dst) are not supported."); - } - - if (src->shape_ != dst->shape_) { - throw jsi::JSError(rt, "sigmoid: src and dst must have the same shape"); - } - - if (src->dtype_ != dst->dtype_) { - throw jsi::JSError(rt, "sigmoid: src and dst must have the same dtype"); - } - - if (src->dtype_ != rnexecutorch::core::types::DType::float32) { - throw jsi::JSError(rt, "sigmoid: only float32 tensors are supported"); - } - - std::shared_lock srcLock(src->mutex_, std::try_to_lock); - if (!srcLock.owns_lock()) { - throw jsi::JSError(rt, "sigmoid: src tensor is currently in use"); - } - - std::unique_lock dstLock(dst->mutex_, std::try_to_lock); - if (!dstLock.owns_lock()) { - throw jsi::JSError(rt, "sigmoid: dst tensor is currently in use"); - } - - if (!src->data_) { - throw jsi::JSError(rt, "sigmoid: src tensor has been disposed"); - } + auto src = tensor::fromJs(rt, "sigmoid: src", args[0], DType::float32, std::nullopt); + auto dst = tensor::fromJs(rt, "sigmoid: dst", args[1], DType::float32, src->shape_); - if (!dst->data_) { - throw jsi::JSError(rt, "sigmoid: dst tensor has been disposed"); - } + tensor::checkNotSameTensor(rt, "sigmoid: src", src, "sigmoid: dst", dst); + auto srcLock = tensor::tryLockShared(rt, "sigmoid: src", src); + auto dstLock = tensor::tryLockUnique(rt, "sigmoid: dst", dst); const auto countElements = src->numel_; const auto *srcData = reinterpret_cast(src->data_.get()); @@ -84,42 +51,18 @@ void install_softmax(jsi::Runtime &rt, jsi::Object &module) { throw jsi::JSError(rt, "Usage: softmax(src, dst, axis)"); } - if (!args[0].isObject() || !args[0].asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "softmax: src must be a Tensor"); - } - - if (!args[1].isObject() || !args[1].asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "softmax: dst must be a Tensor"); - } - - auto src = args[0].asObject(rt).getHostObject(rt); - auto dst = args[1].asObject(rt).getHostObject(rt); + auto src = tensor::fromJs(rt, "softmax: src", args[0], DType::float32, std::nullopt); + auto dst = tensor::fromJs(rt, "softmax: dst", args[1], DType::float32, src->shape_); - if (src.get() == dst.get()) { - throw jsi::JSError(rt, "softmax: In-place operations (src == dst) are not supported."); - } - - if (src->shape_ != dst->shape_) { - throw jsi::JSError(rt, "softmax: src and dst must have the same shape"); - } - - if (src->dtype_ != dst->dtype_) { - throw jsi::JSError(rt, "softmax: src and dst must have the same dtype"); - } - - if (src->dtype_ != rnexecutorch::core::types::DType::float32) { - throw jsi::JSError(rt, "softmax: only float32 tensors are supported"); - } + tensor::checkNotSameTensor(rt, "softmax: src", src, "softmax: dst", dst); + auto srcLock = tensor::tryLockShared(rt, "softmax: src", src); + auto dstLock = tensor::tryLockUnique(rt, "softmax: dst", dst); if (src->shape_.empty()) { throw jsi::JSError(rt, "softmax: src must have at least one dimension"); } - if (!args[2].isNumber()) { - throw jsi::JSError(rt, "softmax: axis must be a number"); - } - - int axis = static_cast(args[2].asNumber()); + int axis = conversions::asType(rt, "softmax: axis", args[2]); const int rank = static_cast(src->shape_.size()); // Support negative axis indices like numpy (e.g., axis=-1 means last @@ -132,24 +75,6 @@ void install_softmax(jsi::Runtime &rt, jsi::Object &module) { } const auto axisIdx = static_cast(axis); - std::shared_lock srcLock(src->mutex_, std::try_to_lock); - if (!srcLock.owns_lock()) { - throw jsi::JSError(rt, "softmax: src tensor is currently in use"); - } - - std::unique_lock dstLock(dst->mutex_, std::try_to_lock); - if (!dstLock.owns_lock()) { - throw jsi::JSError(rt, "softmax: dst tensor is currently in use"); - } - - if (!src->data_) { - throw jsi::JSError(rt, "softmax: src tensor has been disposed"); - } - - if (!dst->data_) { - throw jsi::JSError(rt, "softmax: dst tensor has been disposed"); - } - const auto *srcData = reinterpret_cast(src->data_.get()); auto *dstData = reinterpret_cast(dst->data_.get()); @@ -203,34 +128,14 @@ void install_argmax(jsi::Runtime &rt, jsi::Object &module) { throw jsi::JSError(rt, "Usage: argmax(src, dst, axis)"); } - if (!args[0].isObject() || !args[0].asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "argmax: src must be a Tensor"); - } - - if (!args[1].isObject() || !args[1].asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "argmax: dst must be a Tensor"); - } - - auto src = args[0].asObject(rt).getHostObject(rt); - auto dst = args[1].asObject(rt).getHostObject(rt); + auto src = tensor::fromJs(rt, "argmax: src", args[0], DType::float32, std::nullopt); + auto dst = tensor::fromJs(rt, "argmax: dst", args[1], DType::int32, std::nullopt); - if (src.get() == dst.get()) { - throw jsi::JSError(rt, "argmax: In-place operations (src == dst) are not supported."); - } - - if (src->dtype_ != rnexecutorch::core::types::DType::float32) { - throw jsi::JSError(rt, "argmax: src must be float32"); - } + tensor::checkNotSameTensor(rt, "argmax: src", src, "argmax: dst", dst); + auto srcLock = tensor::tryLockShared(rt, "argmax: src", src); + auto dstLock = tensor::tryLockUnique(rt, "argmax: dst", dst); - if (dst->dtype_ != rnexecutorch::core::types::DType::int32) { - throw jsi::JSError(rt, "argmax: dst must be int32"); - } - - if (!args[2].isNumber()) { - throw jsi::JSError(rt, "argmax: axis must be a number"); - } - - int axis = static_cast(args[2].asNumber()); + int axis = conversions::asType(rt, "argmax: axis", args[2]); const int rank = static_cast(src->shape_.size()); // Support negative axis indices like numpy (e.g., axis=-1 means last @@ -249,20 +154,6 @@ void install_argmax(jsi::Runtime &rt, jsi::Object &module) { throw jsi::JSError(rt, "argmax: dst shape must match src shape but with axis dimension 1"); } - std::shared_lock srcLock(src->mutex_, std::try_to_lock); - std::unique_lock dstLock(dst->mutex_, std::try_to_lock); - if (!srcLock.owns_lock() || !dstLock.owns_lock()) { - throw jsi::JSError(rt, "argmax: tensors in use"); - } - - if (!src->data_) { - throw jsi::JSError(rt, "argmax: src tensor has been disposed"); - } - - if (!dst->data_) { - throw jsi::JSError(rt, "argmax: dst tensor has been disposed"); - } - const auto *srcData = reinterpret_cast(src->data_.get()); const auto axisDim = static_cast(src->shape_[axisIdx]); @@ -310,51 +201,14 @@ void install_threshold(jsi::Runtime &rt, jsi::Object &module) { throw jsi::JSError(rt, "Usage: threshold(src, dst, threshold)"); } - if (!args[0].isObject() || !args[0].asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "threshold: src must be a Tensor"); - } - - if (!args[1].isObject() || !args[1].asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "threshold: dst must be a Tensor"); - } - - if (!args[2].isNumber()) { - throw jsi::JSError(rt, "threshold: threshold must be a number"); - } - - auto src = args[0].asObject(rt).getHostObject(rt); - auto dst = args[1].asObject(rt).getHostObject(rt); - auto thresholdVal = static_cast(args[2].asNumber()); + auto src = tensor::fromJs(rt, "threshold: src", args[0], DType::float32, std::nullopt); + auto dst = tensor::fromJs(rt, "threshold: dst", args[1], DType::float32, src->shape_); - if (src.get() == dst.get()) { - throw jsi::JSError(rt, "threshold: In-place operations (src == dst) are not supported."); - } - - if (src->shape_ != dst->shape_) { - throw jsi::JSError(rt, "threshold: src and dst must have the same shape"); - } + tensor::checkNotSameTensor(rt, "threshold: src", src, "threshold: dst", dst); + auto srcLock = tensor::tryLockShared(rt, "threshold: src", src); + auto dstLock = tensor::tryLockUnique(rt, "threshold: dst", dst); - if (src->dtype_ != rnexecutorch::core::types::DType::float32) { - throw jsi::JSError(rt, "threshold: src must be a float32 tensor"); - } - - if (dst->dtype_ != rnexecutorch::core::types::DType::float32) { - throw jsi::JSError(rt, "threshold: dst must be a float32 tensor"); - } - - std::shared_lock srcLock(src->mutex_, std::try_to_lock); - std::unique_lock dstLock(dst->mutex_, std::try_to_lock); - if (!srcLock.owns_lock() || !dstLock.owns_lock()) { - throw jsi::JSError(rt, "threshold: tensors in use"); - } - - if (!src->data_) { - throw jsi::JSError(rt, "threshold: src tensor has been disposed"); - } - - if (!dst->data_) { - throw jsi::JSError(rt, "threshold: dst tensor has been disposed"); - } + auto thresholdVal = conversions::asType(rt, "threshold: threshold", args[2]); const auto *srcData = reinterpret_cast(src->data_.get()); auto *dstData = reinterpret_cast(dst->data_.get()); diff --git a/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.cpp b/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.cpp index 4e55a78434..c48123bda2 100644 --- a/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.cpp +++ b/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.cpp @@ -1,13 +1,17 @@ #include "tokenizer.h" #include +#include #include #include #include +#include "core/conversions.h" + namespace rnexecutorch::extensions::nlp::tokenizer { namespace jsi = facebook::jsi; +namespace conversions = rnexecutorch::core::conversions; namespace { // Number of BOS/EOS tokens to add on top of what the tokenizer.json defines. @@ -82,20 +86,13 @@ jsi::Value TokenizerHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &nam throw jsi::JSError(rt, "encode: Tokenizer has been disposed"); } - auto text = args[0].asString(rt).utf8(rt); + auto text = conversions::asType(rt, "encode: text", args[0]); auto result = self->tokenizer_->encode(text, kNumAddedBosTokens, kNumAddedEosTokens); if (!result.ok()) { - throw jsi::JSError(rt, "encode: Failed to encode input: " + - toString(result.error())); - } - - const auto &ids = result.get(); - auto jsArray = jsi::Array(rt, ids.size()); - for (size_t i = 0; i < ids.size(); ++i) { - jsArray.setValueAtIndex(rt, i, static_cast(ids[i])); + throw jsi::JSError(rt, std::format("encode: Failed to encode input: {}", toString(result.error()))); } - return jsArray; + return conversions::toJsiArray(rt, result.get()); }; return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "encode"), 1, fnBody); } @@ -107,17 +104,10 @@ jsi::Value TokenizerHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &nam throw jsi::JSError(rt, "decode: Usage: decode(tokens, skipSpecialTokens?)"); } - if (!args[0].isObject() || !args[0].asObject(rt).isArray(rt)) { - throw jsi::JSError(rt, "decode: Expected arg0 to be an array"); - } - // skipSpecialTokens is optional and defaults to true. bool skipSpecialTokens = true; if (count == 2 && !args[1].isUndefined()) { - if (!args[1].isBool()) { - throw jsi::JSError(rt, "decode: Expected arg1 to be a boolean"); - } - skipSpecialTokens = args[1].asBool(); + skipSpecialTokens = conversions::asType(rt, "decode: skipSpecialTokens", args[1]); } std::unique_lock lock(self->mutex_, std::try_to_lock); @@ -129,17 +119,7 @@ jsi::Value TokenizerHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &nam throw jsi::JSError(rt, "decode: Tokenizer has been disposed"); } - auto tokensArray = args[0].asObject(rt).asArray(rt); - - std::vector tokens; - tokens.reserve(tokensArray.size(rt)); - for (size_t i = 0; i < tokensArray.size(rt); ++i) { - auto val = tokensArray.getValueAtIndex(rt, i); - if (!val.isNumber()) { - throw jsi::JSError(rt, "decode: Expected tokens[" + std::to_string(i) + "] to be a number"); - } - tokens.push_back(static_cast(val.asNumber())); - } + auto tokens = conversions::asVector(rt, "decode: tokens", args[0]); if (tokens.empty()) { return jsi::String::createFromUtf8(rt, ""); @@ -147,8 +127,7 @@ jsi::Value TokenizerHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &nam auto result = self->tokenizer_->decode(tokens, skipSpecialTokens); if (!result.ok()) { - throw jsi::JSError(rt, "decode: Failed to decode tokens: " + - toString(result.error())); + throw jsi::JSError(rt, std::format("decode: Failed to decode tokens: {}", toString(result.error()))); } return jsi::String::createFromUtf8(rt, result.get()); @@ -197,11 +176,10 @@ jsi::Value TokenizerHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &nam throw jsi::JSError(rt, "idToToken: Tokenizer has been disposed"); } - auto tokenId = static_cast(args[0].asNumber()); + auto tokenId = conversions::asType(rt, "idToToken: id", args[0]); auto result = self->tokenizer_->id_to_piece(tokenId); if (!result.ok()) { - throw jsi::JSError(rt, "idToToken: Failed to convert id to token: " + - toString(result.error())); + throw jsi::JSError(rt, std::format("idToToken: Failed to convert id to token: {}", toString(result.error()))); } return jsi::String::createFromUtf8(rt, result.get()); @@ -216,10 +194,6 @@ jsi::Value TokenizerHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &nam throw jsi::JSError(rt, "tokenToId: Usage: tokenToId(token)"); } - if (!args[0].isString()) { - throw jsi::JSError(rt, "tokenToId: Expected arg0 to be a string"); - } - std::unique_lock lock(self->mutex_, std::try_to_lock); if (!lock.owns_lock()) { throw jsi::JSError(rt, "tokenToId: Tokenizer is currently in use"); @@ -229,11 +203,10 @@ jsi::Value TokenizerHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &nam throw jsi::JSError(rt, "tokenToId: Tokenizer has been disposed"); } - auto token = args[0].asString(rt).utf8(rt); + auto token = conversions::asType(rt, "tokenToId: token", args[0]); auto result = self->tokenizer_->piece_to_id(token); if (!result.ok()) { - throw jsi::JSError(rt, "tokenToId: Failed to convert token to id: " + - toString(result.error())); + throw jsi::JSError(rt, std::format("tokenToId: Failed to convert token to id: {}", toString(result.error()))); } return static_cast(result.get()); @@ -283,16 +256,12 @@ void install_loadTokenizer(jsi::Runtime &rt, jsi::Object &module) { throw jsi::JSError(rt, "loadTokenizer: Usage: loadTokenizer(arg0)"); } - if (!args[0].isString()) { - throw jsi::JSError(rt, "loadTokenizer: Expected arg0 to be a string"); - } - - auto tokenizerPath = args[0].asString(rt).utf8(rt); + auto tokenizerPath = conversions::asType(rt, "loadTokenizer: path", args[0]); try { auto tokenizerInstance = std::make_shared(tokenizerPath); return jsi::Object::createFromHostObject(rt, tokenizerInstance); } catch (const std::exception &e) { - throw jsi::JSError(rt, std::string("loadTokenizer: ") + e.what()); + throw jsi::JSError(rt, std::format("loadTokenizer: {}", e.what())); } }; auto fn = jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 1, fnBody); From 84c703cce96cbda97b2e4072caa4de88797a35d3 Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Wed, 1 Jul 2026 17:00:37 +0200 Subject: [PATCH 03/25] cpp/core: fix size_t/uint64_t duplicate specialization on Linux --- .../cpp/core/conversions.cpp | 15 +++++++-------- .../react-native-executorch/cpp/core/tensor.cpp | 14 +++++++------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/react-native-executorch/cpp/core/conversions.cpp b/packages/react-native-executorch/cpp/core/conversions.cpp index 83f20a8470..ec1dd78abf 100644 --- a/packages/react-native-executorch/cpp/core/conversions.cpp +++ b/packages/react-native-executorch/cpp/core/conversions.cpp @@ -44,14 +44,13 @@ uint8_t asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Val return static_cast(v); } -template <> -size_t asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) { - double v = asType(rt, ctx, val); - if (std::isnan(v) || v < 0.0) { - throw jsi::JSError(rt, ctx + " must be a non-negative integer"); - } - return static_cast(v); -} +// Note: size_t is intentionally not specialised here. +// On Linux x86_64 size_t and uint64_t are both `unsigned long`, so adding a +// size_t specialisation would produce a duplicate-explicit-specialisation ODR +// error on that platform. asType routes through the uint64_t +// specialisation on Linux (same type) and is a separate instantiation on +// macOS (where uint64_t is unsigned long long). Either way the semantics — +// non-negative double → cast — are identical. template <> bool asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) { diff --git a/packages/react-native-executorch/cpp/core/tensor.cpp b/packages/react-native-executorch/cpp/core/tensor.cpp index 1e1107964f..a196f23626 100644 --- a/packages/react-native-executorch/cpp/core/tensor.cpp +++ b/packages/react-native-executorch/cpp/core/tensor.cpp @@ -57,9 +57,9 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) auto srcLock = tryLockShared(rt, "copyTo: self", self); auto dstLock = tryLockUnique(rt, "copyTo: dst", dst); - const jsi::Object optsObj = (count == 2 && args[1].isObject()) ? args[1].asObject(rt) : jsi::Object(rt); - const size_t srcOffset = conversions::getOptionalProperty(rt, "copyTo", optsObj, "offset").value_or(0); - const size_t copyLen = conversions::getOptionalProperty(rt, "copyTo", optsObj, "length").value_or(self->numel_ - srcOffset); + const jsi::Object optsObj = (count == 2) ? args[1].asObject(rt) : jsi::Object(rt); + const size_t srcOffset = conversions::getOptionalProperty(rt, "copyTo", optsObj, "offset").value_or(0); + const size_t copyLen = conversions::getOptionalProperty(rt, "copyTo", optsObj, "length").value_or(self->numel_ - srcOffset); if (srcOffset + copyLen > self->numel_) { throw jsi::JSError(rt, "copyTo: out of bounds offset and length for src tensor"); @@ -87,8 +87,8 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) const auto dataObj = args[0].asObject(rt); const auto buffer = dataObj.getProperty(rt, "buffer").asObject(rt).getArrayBuffer(rt); - size_t byteOffset = conversions::getOptionalProperty(rt, "setData", dataObj, "byteOffset").value_or(0); - size_t byteLength = conversions::getOptionalProperty(rt, "setData", dataObj, "byteLength").value_or(buffer.size(rt)); + size_t byteOffset = conversions::getOptionalProperty(rt, "setData", dataObj, "byteOffset").value_or(0); + size_t byteLength = conversions::getOptionalProperty(rt, "setData", dataObj, "byteLength").value_or(buffer.size(rt)); auto lock = tryLockUnique(rt, "setData: self", self); @@ -113,8 +113,8 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) const jsi::Object dataObj = args[0].asObject(rt); const jsi::ArrayBuffer buffer = dataObj.getProperty(rt, "buffer").asObject(rt).getArrayBuffer(rt); - size_t byteOffset = conversions::getOptionalProperty(rt, "getData", dataObj, "byteOffset").value_or(0); - size_t byteLength = conversions::getOptionalProperty(rt, "getData", dataObj, "byteLength").value_or(buffer.size(rt)); + size_t byteOffset = conversions::getOptionalProperty(rt, "getData", dataObj, "byteOffset").value_or(0); + size_t byteLength = conversions::getOptionalProperty(rt, "getData", dataObj, "byteLength").value_or(buffer.size(rt)); auto lock = tryLockShared(rt, "getData: self", self); From cdfbbf063f4c6e040208a130387be3bdf89f2ecd Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Thu, 2 Jul 2026 13:30:42 +0200 Subject: [PATCH 04/25] style: add explicit `tensor::fromJs` --- packages/react-native-executorch/cpp/core/tensor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-executorch/cpp/core/tensor.cpp b/packages/react-native-executorch/cpp/core/tensor.cpp index a196f23626..7cf822ed5b 100644 --- a/packages/react-native-executorch/cpp/core/tensor.cpp +++ b/packages/react-native-executorch/cpp/core/tensor.cpp @@ -51,7 +51,7 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) throw jsi::JSError(rt, "copyTo: Usage: copyTo(dst, options?)"); } - auto dst = fromJs(rt, "copyTo: dst", args[0], std::nullopt, std::nullopt); + auto dst = tensor::fromJs(rt, "copyTo: dst", args[0], std::nullopt, std::nullopt); checkNotSameTensor(rt, "copyTo: self", self, "copyTo: dst", dst); auto srcLock = tryLockShared(rt, "copyTo: self", self); From e60c5dfdc926783489c452b49c9fac3aed57fd6c Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Thu, 2 Jul 2026 16:07:42 +0200 Subject: [PATCH 05/25] feat: Add RangeDim support to C++ SymbolicShape and refactor model runtime error handling --- .../cpp/core/model.cpp | 250 ++++++------------ .../cpp/core/tensor_helpers.cpp | 100 ++++--- .../cpp/core/tensor_helpers.h | 16 +- 3 files changed, 162 insertions(+), 204 deletions(-) diff --git a/packages/react-native-executorch/cpp/core/model.cpp b/packages/react-native-executorch/cpp/core/model.cpp index 4854b9ef39..ee5e1e1c45 100644 --- a/packages/react-native-executorch/cpp/core/model.cpp +++ b/packages/react-native-executorch/cpp/core/model.cpp @@ -3,13 +3,37 @@ #include "tensor_helpers.h" #include +#include #include +#include #include +#include #include #include #include +namespace { +template +T getOrThrow(facebook::jsi::Runtime &rt, const std::string &ctx, + executorch::runtime::Result result) { + if (!result.ok()) { + throw facebook::jsi::JSError(rt, std::format("{}: {}", ctx, executorch::runtime::to_string(result.error()))); + } + return std::move(result.get()); +} + +rnexecutorch::core::types::DType +getDTypeOrThrow(facebook::jsi::Runtime &rt, const std::string &ctx, + executorch::aten::ScalarType scalarType) { + try { + return rnexecutorch::core::types::fromScalarType(scalarType); + } catch (const std::exception &e) { + throw facebook::jsi::JSError(rt, std::format("{}: Unsupported tensor dtype: {}", ctx, e.what())); + } +} +} // namespace + namespace rnexecutorch::core::model { namespace jsi = facebook::jsi; namespace types = rnexecutorch::core::types; @@ -50,15 +74,11 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { throw jsi::JSError(rt, "getMethodNames: Model has been disposed"); } - auto methodNames = self->etModule_->method_names(); - if (!methodNames.ok()) { - const std::string errorMsg = executorch::runtime::to_string(methodNames.error()); - throw jsi::JSError(rt, "getMethodNames: Failed to get method names: " + errorMsg); - } + auto methodNames = getOrThrow(rt, "getMethodNames", self->etModule_->method_names()); - auto jsArray = jsi::Array(rt, methodNames->size()); + auto jsArray = jsi::Array(rt, methodNames.size()); size_t index = 0; - for (const auto &methodName : methodNames.get()) { + for (const auto &methodName : methodNames) { jsArray.setValueAtIndex(rt, index, jsi::String::createFromUtf8(rt, methodName)); ++index; } @@ -76,7 +96,7 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { } if (!args[0].isString()) { - throw jsi::JSError(rt, "getMethodMeta: Expected arg0 to be a string"); + throw jsi::JSError(rt, "getMethodMeta: Expected methodName to be a string"); } std::unique_lock lock(self->mutex_, std::try_to_lock); @@ -89,40 +109,24 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { } auto methodName = conversions::asType(rt, "getMethodMeta: methodName", args[0]); - auto methodMeta = self->etModule_->method_meta(methodName); - if (!methodMeta.ok()) { - const std::string errorMsg = executorch::runtime::to_string(methodMeta.error()); - throw jsi::JSError(rt, std::format("getMethodMeta: Failed to get method meta: {}", errorMsg)); - } + auto methodMeta = getOrThrow(rt, "getMethodMeta", self->etModule_->method_meta(methodName)); - auto inputTagsArray = jsi::Array(rt, methodMeta->num_inputs()); - for (size_t i = 0; i < methodMeta->num_inputs(); ++i) { - auto tag = methodMeta->input_tag(i); - if (!tag.ok()) { - const std::string errorMsg = executorch::runtime::to_string(tag.error()); - throw jsi::JSError(rt, std::format("getMethodMeta: Failed to get input tag for input {}: {}", i, errorMsg)); - } - inputTagsArray.setValueAtIndex(rt, i, jsi::String::createFromUtf8(rt, executorch::runtime::tag_to_string(tag.get()))); + auto inputTagsArray = jsi::Array(rt, methodMeta.num_inputs()); + for (size_t i = 0; i < methodMeta.num_inputs(); ++i) { + auto tag = getOrThrow(rt, std::format("getMethodMeta: input tag [{}]", i), methodMeta.input_tag(i)); + inputTagsArray.setValueAtIndex(rt, i, jsi::String::createFromUtf8(rt, executorch::runtime::tag_to_string(tag))); } - auto outputTagsArray = jsi::Array(rt, methodMeta->num_outputs()); - for (size_t i = 0; i < methodMeta->num_outputs(); ++i) { - auto tag = methodMeta->output_tag(i); - if (!tag.ok()) { - const std::string errorMsg = executorch::runtime::to_string(tag.error()); - throw jsi::JSError(rt, std::format("getMethodMeta: Failed to get output tag for output {}: {}", i, errorMsg)); - } - outputTagsArray.setValueAtIndex(rt, i, jsi::String::createFromUtf8(rt, executorch::runtime::tag_to_string(tag.get()))); + auto outputTagsArray = jsi::Array(rt, methodMeta.num_outputs()); + for (size_t i = 0; i < methodMeta.num_outputs(); ++i) { + auto tag = getOrThrow(rt, std::format("getMethodMeta: output tag [{}]", i), methodMeta.output_tag(i)); + outputTagsArray.setValueAtIndex(rt, i, jsi::String::createFromUtf8(rt, executorch::runtime::tag_to_string(tag))); } auto usesBackendMap = jsi::Object(rt); - for (size_t i = 0; i < methodMeta->num_backends(); ++i) { - auto backendName = methodMeta->get_backend_name(i); - if (!backendName.ok()) { - const std::string errorMsg = executorch::runtime::to_string(backendName.error()); - throw jsi::JSError(rt, std::format("getMethodMeta: Failed to get backend name for backend {}: {}", i, errorMsg)); - } - usesBackendMap.setProperty(rt, backendName.get(), methodMeta->uses_backend(backendName.get())); + for (size_t i = 0; i < methodMeta.num_backends(); ++i) { + const auto *backendName = getOrThrow(rt, std::format("getMethodMeta: backend name [{}]", i), methodMeta.get_backend_name(i)); + usesBackendMap.setProperty(rt, backendName, methodMeta.uses_backend(backendName)); } auto tensorMetaToJS = [](jsi::Runtime &rt, const executorch::runtime::TensorInfo &tensorMeta) -> jsi::Object { @@ -132,7 +136,7 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { jsTensorMeta.setProperty(rt, "nbytes", static_cast(tensorMeta.nbytes())); try { - const std::string dtypeStr = types::toString(types::fromScalarType(tensorMeta.scalar_type())); + auto dtypeStr = types::toString(types::fromScalarType(tensorMeta.scalar_type())); jsTensorMeta.setProperty(rt, "dtype", jsi::String::createFromUtf8(rt, dtypeStr)); } catch (const std::exception &) { jsTensorMeta.setProperty(rt, "dtype", jsi::String::createFromUtf8(rt, "not supported")); @@ -147,30 +151,22 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { return jsTensorMeta; }; - auto inputTensorMetaArray = jsi::Array(rt, methodMeta->num_inputs()); - for (size_t i = 0; i < methodMeta->num_inputs(); ++i) { - auto tensorMeta = methodMeta->input_tensor_meta(i); - if (!tensorMeta.ok()) { - const std::string errorMsg = executorch::runtime::to_string(tensorMeta.error()); - throw jsi::JSError(rt, std::format("getMethodMeta: Failed to get tensor meta for input {}: {}", i, errorMsg)); - } - inputTensorMetaArray.setValueAtIndex(rt, i, tensorMetaToJS(rt, tensorMeta.get())); + auto inputTensorMetaArray = jsi::Array(rt, methodMeta.num_inputs()); + for (size_t i = 0; i < methodMeta.num_inputs(); ++i) { + auto tensorMeta = getOrThrow(rt, std::format("getMethodMeta: input tensor meta [{}]", i), methodMeta.input_tensor_meta(i)); + inputTensorMetaArray.setValueAtIndex(rt, i, tensorMetaToJS(rt, tensorMeta)); } - auto outputTensorMetaArray = jsi::Array(rt, methodMeta->num_outputs()); - for (size_t i = 0; i < methodMeta->num_outputs(); ++i) { - auto tensorMeta = methodMeta->output_tensor_meta(i); - if (!tensorMeta.ok()) { - const std::string errorMsg = executorch::runtime::to_string(tensorMeta.error()); - throw jsi::JSError(rt, std::format("getMethodMeta: Failed to get tensor meta for output {}: {}", i, errorMsg)); - } - outputTensorMetaArray.setValueAtIndex(rt, i, tensorMetaToJS(rt, tensorMeta.get())); + auto outputTensorMetaArray = jsi::Array(rt, methodMeta.num_outputs()); + for (size_t i = 0; i < methodMeta.num_outputs(); ++i) { + auto tensorMeta = getOrThrow(rt, std::format("getMethodMeta: output tensor meta [{}]", i), methodMeta.output_tensor_meta(i)); + outputTensorMetaArray.setValueAtIndex(rt, i, tensorMetaToJS(rt, tensorMeta)); } auto jsMeta = jsi::Object(rt); - jsMeta.setProperty(rt, "name", jsi::String::createFromUtf8(rt, methodMeta->name())); - jsMeta.setProperty(rt, "numInputs", static_cast(methodMeta->num_inputs())); - jsMeta.setProperty(rt, "numOutputs", static_cast(methodMeta->num_outputs())); + jsMeta.setProperty(rt, "name", jsi::String::createFromUtf8(rt, methodMeta.name())); + jsMeta.setProperty(rt, "numInputs", static_cast(methodMeta.num_inputs())); + jsMeta.setProperty(rt, "numOutputs", static_cast(methodMeta.num_outputs())); jsMeta.setProperty(rt, "inputTags", inputTagsArray); jsMeta.setProperty(rt, "outputTags", outputTagsArray); jsMeta.setProperty(rt, "usesBackend", usesBackendMap); @@ -199,83 +195,37 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { } auto methodName = conversions::asType(rt, "execute: methodName", args[0]); - auto methodMeta = self->etModule_->method_meta(methodName); + auto methodMeta = getOrThrow(rt, std::format("execute: method meta for '{}'", methodName), + self->etModule_->method_meta(methodName)); + auto inputsArray = args[1].asObject(rt).asArray(rt); auto outputTensorsArray = args[2].asObject(rt).asArray(rt); - if (!methodMeta.ok()) { - const std::string errorMsg = executorch::runtime::to_string(methodMeta.error()); - throw jsi::JSError(rt, std::format("execute: Failed to get method meta for '{}': {}", - methodName, errorMsg)); - } - - if (inputsArray.size(rt) != methodMeta->num_inputs()) { + if (inputsArray.size(rt) != methodMeta.num_inputs()) { throw jsi::JSError(rt, std::format("execute: Incorrect size for inputs: got {}, expected {}", - inputsArray.size(rt), methodMeta->num_inputs())); + inputsArray.size(rt), methodMeta.num_inputs())); } - auto validateTensor = [](jsi::Runtime &rt, - const TensorHostObject *tensorHostObject, - const executorch::runtime::Result &tensorMeta, - const std::string &identifier) { - if (tensorMeta->scalar_type() != tensorHostObject->tensor_->dtype()) { - throw jsi::JSError(rt, std::format("execute: Tensor dtype mismatch for {}", identifier)); - } - - if (tensorMeta->sizes().size() != tensorHostObject->shape_.size()) { - throw jsi::JSError(rt, std::format("execute: Tensor rank mismatch for {}: expected rank {} but got {}", - identifier, tensorMeta->sizes().size(), tensorHostObject->shape_.size())); - } - - auto ndim = tensorHostObject->tensor_->sizes().size(); - for (size_t j = 0; j < ndim; ++j) { - if (tensorMeta->sizes()[j] != tensorHostObject->shape_[j]) { - throw jsi::JSError(rt, std::format("execute: Tensor shape mismatch for {}: expected dimension {} to be {} but got {}", - identifier, j, tensorMeta->sizes()[j], tensorHostObject->shape_[j])); - } - } - }; - - auto inputs = std::vector(methodMeta->num_inputs()); + std::vector inputs(methodMeta.num_inputs()); std::vector> tensorLocks; std::unordered_set lockedTensors; - for (size_t i = 0; i < methodMeta->num_inputs(); ++i) { - auto tag = methodMeta->input_tag(i); + for (size_t i = 0; i < methodMeta.num_inputs(); ++i) { + auto tag = getOrThrow(rt, std::format("execute: inputs[{}] tag", i), methodMeta.input_tag(i)); auto val = inputsArray.getValueAtIndex(rt, i); - const auto ident = std::format("inputs[{}]", i); - const auto ctx = std::format("execute: {}", ident); - - if (!tag.ok()) { - const std::string errorMsg = executorch::runtime::to_string(tag.error()); - throw jsi::JSError(rt, std::format("{}: Failed to get input tag: {}", ctx, errorMsg)); - } + auto ctx = std::format("execute: inputs[{}]", i); - switch (tag.get()) { + switch (tag) { case executorch::runtime::Tag::Tensor: { - auto tensorHostObject = tensor::fromJs(rt, ctx, val, std::nullopt, std::nullopt); + auto tensorMeta = getOrThrow(rt, std::format("{}: tensor meta", ctx), methodMeta.input_tensor_meta(i)); + auto expectedDtype = getDTypeOrThrow(rt, ctx, tensorMeta.scalar_type()); + auto tensorHostObject = tensor::fromJs(rt, ctx, val, expectedDtype, tensorMeta.sizes()); if (!lockedTensors.insert(tensorHostObject.get()).second) { - throw jsi::JSError(rt, "execute: Tensor aliasing detected. The same tensor was passed multiple times."); - } - - tensorLocks.emplace_back(tensorHostObject->mutex_, std::try_to_lock); - if (!tensorLocks.back().owns_lock()) { - throw jsi::JSError(rt, std::format("{} is currently in use", ctx)); - } - - if (!tensorHostObject->data_) { - throw jsi::JSError(rt, std::format("{} has been disposed", ctx)); - } - - auto tensorMeta = methodMeta->input_tensor_meta(i); - - if (!tensorMeta.ok()) { - const std::string errorMsg = executorch::runtime::to_string(tensorMeta.error()); - throw jsi::JSError(rt, std::format("{}: Failed to get tensor meta: {}", ctx, errorMsg)); + throw jsi::JSError(rt, "execute: Tensor aliasing detected. " + "The same tensor was passed multiple times."); } - - validateTensor(rt, tensorHostObject.get(), tensorMeta, ident); + tensorLocks.emplace_back(tensor::tryLockUnique(rt, ctx, tensorHostObject)); inputs[i] = tensorHostObject->tensor_; break; @@ -294,34 +244,18 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { } } - auto startTime = std::chrono::high_resolution_clock::now(); - auto result = self->etModule_->execute(methodName, inputs); - auto finishTime = std::chrono::high_resolution_clock::now(); - -#ifdef EXECUTORCH_ENABLE_EXECUTION_PROFILING - auto durationMs = std::chrono::duration_cast(finishTime - startTime).count(); - auto consoleObj = rt.global().getProperty(rt, "console").asObject(rt); - auto logFn = consoleObj.getProperty(rt, "log").asObject(rt).asFunction(rt); - auto info = std::format("Execution of method '{}' took {} ms", methodName, durationMs); - logFn.callWithThis(rt, consoleObj, {jsi::String::createFromUtf8(rt, info)}); -#endif - - if (!result.ok()) { - const std::string errorMsg = executorch::runtime::to_string(result.error()); - throw jsi::JSError(rt, std::format("execute: Method '{}' execution failed: {}. " - "This may be due to missing required backends - " - "use getMethodMeta() to check required backends and " - "getExecuTorchRegisteredBackends() to check " - "which backends are registered in the runtime.", - methodName, errorMsg)); - } + const auto result = getOrThrow(rt, std::format("execute: Method '{}' failed (check getMethodMeta() " + "for required backends and getRegisteredBackends() " + "for registered ones)", + methodName), + self->etModule_->execute(methodName, inputs)); - auto jsOutputArray = jsi::Array(rt, result->size()); + auto jsOutputArray = jsi::Array(rt, result.size()); size_t index = 0; size_t tensorOutputIdx = 0; - for (const auto &output : result.get()) { + for (const auto &output : result) { switch (output.tag) { case executorch::runtime::Tag::None: { jsOutputArray.setValueAtIndex(rt, index, jsi::Value::null()); @@ -332,37 +266,19 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { throw jsi::JSError(rt, "execute: Not enough tensor output placeholders in outputTensors"); } - const auto val = outputTensorsArray.getValueAtIndex(rt, tensorOutputIdx); - const auto ident = std::format("outputTensors[{}]", tensorOutputIdx); - const auto ctx = std::format("execute: {}", ident); + auto val = outputTensorsArray.getValueAtIndex(rt, tensorOutputIdx); + auto ctx = std::format("execute: outputTensors[{}]", tensorOutputIdx); + + auto tensorMeta = getOrThrow(rt, std::format("{}: tensor meta", ctx), methodMeta.output_tensor_meta(index)); + auto expectedDtype = getDTypeOrThrow(rt, ctx, tensorMeta.scalar_type()); + auto tensorHostObject = tensor::fromJs(rt, ctx, val, expectedDtype, tensorMeta.sizes()); - auto tensorHostObject = tensor::fromJs(rt, ctx, val, std::nullopt, std::nullopt); if (!lockedTensors.insert(tensorHostObject.get()).second) { throw jsi::JSError(rt, "execute: Tensor aliasing detected. The same tensor was passed multiple times."); } + tensorLocks.emplace_back(tensor::tryLockUnique(rt, ctx, tensorHostObject)); - tensorLocks.emplace_back(tensorHostObject->mutex_, std::try_to_lock); - if (!tensorLocks.back().owns_lock()) { - throw jsi::JSError(rt, std::format("{} is currently in use", ctx)); - } - - if (!tensorHostObject->data_) { - throw jsi::JSError(rt, std::format("{} has been disposed", ctx)); - } - - auto tensorMeta = methodMeta->output_tensor_meta(index); - - if (!tensorMeta.ok()) { - const std::string errorMsg = executorch::runtime::to_string(tensorMeta.error()); - throw jsi::JSError(rt, std::format("execute: Failed to get tensor meta for output at index {}: {}", - index, errorMsg)); - } - - validateTensor(rt, tensorHostObject.get(), tensorMeta, ident); - - std::memcpy(tensorHostObject->data_.get(), - output.toTensor().const_data_ptr(), - output.toTensor().nbytes()); + std::memcpy(tensorHostObject->data_.get(), output.toTensor().const_data_ptr(), output.toTensor().nbytes()); jsOutputArray.setValueAtIndex(rt, index, jsi::Object::createFromHostObject(rt, tensorHostObject)); ++tensorOutputIdx; diff --git a/packages/react-native-executorch/cpp/core/tensor_helpers.cpp b/packages/react-native-executorch/cpp/core/tensor_helpers.cpp index d61a2a6dec..3b5502440d 100644 --- a/packages/react-native-executorch/cpp/core/tensor_helpers.cpp +++ b/packages/react-native-executorch/cpp/core/tensor_helpers.cpp @@ -39,6 +39,32 @@ void checkNotSameTensor(jsi::Runtime &rt, } } +namespace { + +std::string shapeToString(const SymbolicShape &expectedShape) { + std::string s; + for (size_t i = 0; i < expectedShape.size(); ++i) { + if (i > 0) { + s += ", "; + } + const auto &dim = expectedShape.at(i); + if (std::holds_alternative(dim)) { + s += std::format("{}", std::get(dim)); + } else if (std::holds_alternative(dim)) { + s += std::get(dim); + } else { + const auto &rangeDim = std::get(dim); + s += std::format("[{}..{}{}]", + rangeDim.min, + rangeDim.max, + rangeDim.step ? std::format(" step {}", *rangeDim.step) : ""); + } + } + return std::format("[{}]", s); +} + +} // namespace + std::shared_ptr fromJs(jsi::Runtime &rt, const std::string &name, const jsi::Value &value, std::optional expectedDtype, const std::optional &expectedShape) { @@ -48,52 +74,56 @@ fromJs(jsi::Runtime &rt, const std::string &name, const jsi::Value &value, } auto tensor = value.asObject(rt).getHostObject(rt); - auto dtype = tensor->dtype_; - auto shape = tensor->shape_; + const auto &dtype = tensor->dtype_; + const auto &shape = tensor->shape_; if (expectedDtype && dtype != *expectedDtype) { throw jsi::JSError(rt, std::format("{} must be of type {}", name, types::toString(*expectedDtype))); } - if (expectedShape) { - std::string shapeStr = "["; - for (size_t i = 0; i < expectedShape->size(); ++i) { - if (i > 0) { - shapeStr += ", "; - } - const auto &dim = expectedShape->at(i); - if (std::holds_alternative(dim)) { - shapeStr += std::to_string(std::get(dim)); - } else { - shapeStr += std::get(dim); - } - } - shapeStr += "]"; + if (!expectedShape) { + return tensor; + } - if (shape.size() != expectedShape->size()) { - throw jsi::JSError(rt, std::format("{} must have shape {} (expected {} dimensions, got {})", - name, shapeStr, expectedShape->size(), shape.size())); - } + if (shape.size() != expectedShape->size()) { + throw jsi::JSError(rt, std::format("{} must have shape {} (expected {} dimensions, got {})", + name, shapeToString(*expectedShape), expectedShape->size(), shape.size())); + } - std::unordered_map symbolMap; - for (size_t i = 0; i < expectedShape->size(); ++i) { - const auto &dim = expectedShape->at(i); - if (std::holds_alternative(dim)) { - if (shape[i] != std::get(dim)) { - throw jsi::JSError(rt, std::format("{} must have shape {} (dim {} mismatch: expected {}, got {})", - name, shapeStr, i, std::get(dim), shape[i])); - } - } else { - const auto &symbol = std::get(dim); - if (symbolMap.contains(symbol) && shape[i] != symbolMap[symbol]) { - throw jsi::JSError(rt, std::format("{} must have shape {} (dim '{}' mismatch: expected {}, got {})", - name, shapeStr, i, symbolMap[symbol], shape[i])); - } - symbolMap[symbol] = shape[i]; + std::unordered_map symbolToConcrete; + for (size_t i = 0; i < expectedShape->size(); ++i) { + const auto &dim = expectedShape->at(i); + if (std::holds_alternative(dim)) { + const auto expected = std::get(dim); + if (shape[i] != expected) { + throw jsi::JSError(rt, std::format("{} must have shape {} (dim {} mismatch: expected {}, got {})", + name, shapeToString(*expectedShape), i, expected, shape[i])); + } + } else if (std::holds_alternative(dim)) { + const auto &symbol = std::get(dim); + if (symbolToConcrete.contains(symbol) && shape[i] != symbolToConcrete[symbol]) { + throw jsi::JSError(rt, std::format("{} must have shape {} (dim '{}' mismatch: expected {}, got {})", + name, shapeToString(*expectedShape), i, symbolToConcrete[symbol], shape[i])); + } + symbolToConcrete[symbol] = shape[i]; + } else { + const auto &rangeDim = std::get(dim); + if (shape[i] < rangeDim.min) { + throw jsi::JSError(rt, std::format("{} must have shape {} (dim {} out of range: {} < min {})", + name, shapeToString(*expectedShape), i, shape[i], rangeDim.min)); + } + if (shape[i] > rangeDim.max) { + throw jsi::JSError(rt, std::format("{} must have shape {} (dim {} out of range: {} > max {})", + name, shapeToString(*expectedShape), i, shape[i], rangeDim.max)); + } + if (rangeDim.step && (shape[i] - rangeDim.min) % *rangeDim.step != 0) { + throw jsi::JSError(rt, std::format("{} must have shape {} (dim {} must be min({}) + k*step({}), got {})", + name, shapeToString(*expectedShape), i, rangeDim.min, *rangeDim.step, shape[i])); } } } + return tensor; } } // namespace rnexecutorch::core::tensor diff --git a/packages/react-native-executorch/cpp/core/tensor_helpers.h b/packages/react-native-executorch/cpp/core/tensor_helpers.h index b8a0262054..67444638fa 100644 --- a/packages/react-native-executorch/cpp/core/tensor_helpers.h +++ b/packages/react-native-executorch/cpp/core/tensor_helpers.h @@ -1,9 +1,11 @@ #pragma once +#include #include #include #include #include +#include #include #include #include @@ -18,7 +20,14 @@ namespace rnexecutorch::core::tensor { namespace jsi = facebook::jsi; using rnexecutorch::core::types::DType; -using SymbolicShape = std::vector>; + +struct RangeDim { + int32_t min; + int32_t max; + std::optional step; +}; + +using SymbolicShape = std::vector>; [[nodiscard]] std::shared_lock tryLockShared(jsi::Runtime &rt, const std::string &name, const std::shared_ptr &tensor); @@ -34,9 +43,12 @@ std::shared_ptr fromJs(jsi::Runtime &rt, const std::string &name, const jsi::Value &value, std::optional expectedDtype, const std::optional &expectedShape); +template + requires std::ranges::input_range && + std::convertible_to, int32_t> inline std::shared_ptr fromJs(jsi::Runtime &rt, const std::string &name, const jsi::Value &value, - std::optional expectedDtype, const std::vector &expectedShape) { + std::optional expectedDtype, const Range &expectedShape) { SymbolicShape convertedShape(expectedShape.begin(), expectedShape.end()); return fromJs(rt, name, value, expectedDtype, std::move(convertedShape)); } From 139eb27c9e06ca708d6b01015c4b64700d18745a Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Thu, 2 Jul 2026 16:31:53 +0200 Subject: [PATCH 06/25] fix: fix error message --- packages/react-native-executorch/cpp/core/tensor_helpers.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-executorch/cpp/core/tensor_helpers.cpp b/packages/react-native-executorch/cpp/core/tensor_helpers.cpp index 3b5502440d..3cd88f17f2 100644 --- a/packages/react-native-executorch/cpp/core/tensor_helpers.cpp +++ b/packages/react-native-executorch/cpp/core/tensor_helpers.cpp @@ -103,7 +103,7 @@ fromJs(jsi::Runtime &rt, const std::string &name, const jsi::Value &value, } else if (std::holds_alternative(dim)) { const auto &symbol = std::get(dim); if (symbolToConcrete.contains(symbol) && shape[i] != symbolToConcrete[symbol]) { - throw jsi::JSError(rt, std::format("{} must have shape {} (dim '{}' mismatch: expected {}, got {})", + throw jsi::JSError(rt, std::format("{} must have shape {} (dim {} mismatch: expected {}, got {})", name, shapeToString(*expectedShape), i, symbolToConcrete[symbol], shape[i])); } symbolToConcrete[symbol] = shape[i]; From 7305cad828a0e2a52b58d7886cbd42765413a7c9 Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Thu, 2 Jul 2026 17:25:22 +0200 Subject: [PATCH 07/25] refactor(cpp/core): clean up model execution and error context handling --- .../cpp/core/model.cpp | 140 ++++++++++-------- 1 file changed, 75 insertions(+), 65 deletions(-) diff --git a/packages/react-native-executorch/cpp/core/model.cpp b/packages/react-native-executorch/cpp/core/model.cpp index ee5e1e1c45..cde2fe37c8 100644 --- a/packages/react-native-executorch/cpp/core/model.cpp +++ b/packages/react-native-executorch/cpp/core/model.cpp @@ -14,29 +14,51 @@ #include namespace { +namespace jsi = facebook::jsi; +namespace types = rnexecutorch::core::types; + template -T getOrThrow(facebook::jsi::Runtime &rt, const std::string &ctx, - executorch::runtime::Result result) { +T getOrThrow(jsi::Runtime &rt, const std::string &ctx, executorch::runtime::Result result) { if (!result.ok()) { - throw facebook::jsi::JSError(rt, std::format("{}: {}", ctx, executorch::runtime::to_string(result.error()))); + throw jsi::JSError(rt, std::format("{}: {}", ctx, executorch::runtime::to_string(result.error()))); } return std::move(result.get()); } -rnexecutorch::core::types::DType -getDTypeOrThrow(facebook::jsi::Runtime &rt, const std::string &ctx, - executorch::aten::ScalarType scalarType) { +types::DType +getDTypeOrThrow(jsi::Runtime &rt, const std::string &ctx, executorch::aten::ScalarType scalarType) { try { - return rnexecutorch::core::types::fromScalarType(scalarType); + return types::fromScalarType(scalarType); } catch (const std::exception &e) { - throw facebook::jsi::JSError(rt, std::format("{}: Unsupported tensor dtype: {}", ctx, e.what())); + throw jsi::JSError(rt, std::format("{}: Unsupported tensor dtype: {}", ctx, e.what())); } } + +jsi::Object tensorMetaToJs(jsi::Runtime &rt, const executorch::runtime::TensorInfo &tensorMeta) { + auto jsTensorMeta = jsi::Object(rt); + jsTensorMeta.setProperty(rt, "name", jsi::String::createFromUtf8(rt, std::string(tensorMeta.name()))); + jsTensorMeta.setProperty(rt, "ndim", static_cast(tensorMeta.sizes().size())); + jsTensorMeta.setProperty(rt, "nbytes", static_cast(tensorMeta.nbytes())); + + try { + auto dtypeStr = types::toString(types::fromScalarType(tensorMeta.scalar_type())); + jsTensorMeta.setProperty(rt, "dtype", jsi::String::createFromUtf8(rt, dtypeStr)); + } catch (const std::exception &) { + jsTensorMeta.setProperty(rt, "dtype", jsi::String::createFromUtf8(rt, "not supported")); + } + + auto jsShapeArray = jsi::Array(rt, tensorMeta.sizes().size()); + for (size_t i = 0; i < tensorMeta.sizes().size(); ++i) { + jsShapeArray.setValueAtIndex(rt, i, static_cast(tensorMeta.sizes()[i])); + } + jsTensorMeta.setProperty(rt, "shape", jsShapeArray); + + return jsTensorMeta; +} } // namespace namespace rnexecutorch::core::model { namespace jsi = facebook::jsi; -namespace types = rnexecutorch::core::types; namespace conversions = rnexecutorch::core::conversions; using TensorHostObject = rnexecutorch::core::tensor::TensorHostObject; @@ -91,6 +113,8 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { if (nameStr == "getMethodMeta") { auto self = shared_from_this(); auto fnBody = [self](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value { + using executorch::runtime::tag_to_string; + if (count != 1) { throw jsi::JSError(rt, "getMethodMeta: Usage: getMethodMeta(methodName)"); } @@ -113,54 +137,37 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { auto inputTagsArray = jsi::Array(rt, methodMeta.num_inputs()); for (size_t i = 0; i < methodMeta.num_inputs(); ++i) { - auto tag = getOrThrow(rt, std::format("getMethodMeta: input tag [{}]", i), methodMeta.input_tag(i)); - inputTagsArray.setValueAtIndex(rt, i, jsi::String::createFromUtf8(rt, executorch::runtime::tag_to_string(tag))); + auto ctx = std::format("getMethodMeta: input tag [{}]", i); + auto tag = getOrThrow(rt, ctx, methodMeta.input_tag(i)); + inputTagsArray.setValueAtIndex(rt, i, jsi::String::createFromUtf8(rt, tag_to_string(tag))); } auto outputTagsArray = jsi::Array(rt, methodMeta.num_outputs()); for (size_t i = 0; i < methodMeta.num_outputs(); ++i) { - auto tag = getOrThrow(rt, std::format("getMethodMeta: output tag [{}]", i), methodMeta.output_tag(i)); - outputTagsArray.setValueAtIndex(rt, i, jsi::String::createFromUtf8(rt, executorch::runtime::tag_to_string(tag))); + auto ctx = std::format("getMethodMeta: output tag [{}]", i); + auto tag = getOrThrow(rt, ctx, methodMeta.output_tag(i)); + outputTagsArray.setValueAtIndex(rt, i, jsi::String::createFromUtf8(rt, tag_to_string(tag))); } auto usesBackendMap = jsi::Object(rt); for (size_t i = 0; i < methodMeta.num_backends(); ++i) { - const auto *backendName = getOrThrow(rt, std::format("getMethodMeta: backend name [{}]", i), methodMeta.get_backend_name(i)); + auto ctx = std::format("getMethodMeta: backend name [{}]", i); + const auto *backendName = getOrThrow(rt, ctx, methodMeta.get_backend_name(i)); usesBackendMap.setProperty(rt, backendName, methodMeta.uses_backend(backendName)); } - auto tensorMetaToJS = [](jsi::Runtime &rt, const executorch::runtime::TensorInfo &tensorMeta) -> jsi::Object { - auto jsTensorMeta = jsi::Object(rt); - jsTensorMeta.setProperty(rt, "name", jsi::String::createFromUtf8(rt, std::string(tensorMeta.name()))); - jsTensorMeta.setProperty(rt, "ndim", static_cast(tensorMeta.sizes().size())); - jsTensorMeta.setProperty(rt, "nbytes", static_cast(tensorMeta.nbytes())); - - try { - auto dtypeStr = types::toString(types::fromScalarType(tensorMeta.scalar_type())); - jsTensorMeta.setProperty(rt, "dtype", jsi::String::createFromUtf8(rt, dtypeStr)); - } catch (const std::exception &) { - jsTensorMeta.setProperty(rt, "dtype", jsi::String::createFromUtf8(rt, "not supported")); - } - - auto jsShapeArray = jsi::Array(rt, tensorMeta.sizes().size()); - for (size_t i = 0; i < tensorMeta.sizes().size(); ++i) { - jsShapeArray.setValueAtIndex(rt, i, static_cast(tensorMeta.sizes()[i])); - } - jsTensorMeta.setProperty(rt, "shape", jsShapeArray); - - return jsTensorMeta; - }; - auto inputTensorMetaArray = jsi::Array(rt, methodMeta.num_inputs()); for (size_t i = 0; i < methodMeta.num_inputs(); ++i) { - auto tensorMeta = getOrThrow(rt, std::format("getMethodMeta: input tensor meta [{}]", i), methodMeta.input_tensor_meta(i)); - inputTensorMetaArray.setValueAtIndex(rt, i, tensorMetaToJS(rt, tensorMeta)); + auto ctx = std::format("getMethodMeta: input tensor meta [{}]", i); + auto tensorMeta = getOrThrow(rt, ctx, methodMeta.input_tensor_meta(i)); + inputTensorMetaArray.setValueAtIndex(rt, i, tensorMetaToJs(rt, tensorMeta)); } auto outputTensorMetaArray = jsi::Array(rt, methodMeta.num_outputs()); for (size_t i = 0; i < methodMeta.num_outputs(); ++i) { - auto tensorMeta = getOrThrow(rt, std::format("getMethodMeta: output tensor meta [{}]", i), methodMeta.output_tensor_meta(i)); - outputTensorMetaArray.setValueAtIndex(rt, i, tensorMetaToJS(rt, tensorMeta)); + auto ctx = std::format("getMethodMeta: output tensor meta [{}]", i); + auto tensorMeta = getOrThrow(rt, ctx, methodMeta.output_tensor_meta(i)); + outputTensorMetaArray.setValueAtIndex(rt, i, tensorMetaToJs(rt, tensorMeta)); } auto jsMeta = jsi::Object(rt); @@ -195,8 +202,7 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { } auto methodName = conversions::asType(rt, "execute: methodName", args[0]); - auto methodMeta = getOrThrow(rt, std::format("execute: method meta for '{}'", methodName), - self->etModule_->method_meta(methodName)); + auto methodMeta = getOrThrow(rt, "execute", self->etModule_->method_meta(methodName)); auto inputsArray = args[1].asObject(rt).asArray(rt); auto outputTensorsArray = args[2].asObject(rt).asArray(rt); @@ -211,9 +217,9 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { std::unordered_set lockedTensors; for (size_t i = 0; i < methodMeta.num_inputs(); ++i) { - auto tag = getOrThrow(rt, std::format("execute: inputs[{}] tag", i), methodMeta.input_tag(i)); - auto val = inputsArray.getValueAtIndex(rt, i); auto ctx = std::format("execute: inputs[{}]", i); + auto tag = getOrThrow(rt, ctx, methodMeta.input_tag(i)); + auto val = inputsArray.getValueAtIndex(rt, i); switch (tag) { case executorch::runtime::Tag::Tensor: { @@ -226,63 +232,62 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { "The same tensor was passed multiple times."); } tensorLocks.emplace_back(tensor::tryLockUnique(rt, ctx, tensorHostObject)); - inputs[i] = tensorHostObject->tensor_; break; } case executorch::runtime::Tag::Double: - inputs[i] = executorch::runtime::EValue(conversions::asType(rt, ctx, val)); + inputs[i] = conversions::asType(rt, ctx, val); break; case executorch::runtime::Tag::Int: - inputs[i] = executorch::runtime::EValue(conversions::asType(rt, ctx, val)); + inputs[i] = conversions::asType(rt, ctx, val); break; case executorch::runtime::Tag::Bool: - inputs[i] = executorch::runtime::EValue(conversions::asType(rt, ctx, val)); + inputs[i] = conversions::asType(rt, ctx, val); + break; + case executorch::runtime::Tag::None: + inputs[i] = executorch::runtime::EValue(); break; default: - throw jsi::JSError(rt, std::format("{}: Unsupported input type", ctx)); + throw jsi::JSError(rt, std::format("{}: Unsupported input type: {}", + ctx, executorch::runtime::tag_to_string(tag))); } } - const auto result = getOrThrow(rt, std::format("execute: Method '{}' failed (check getMethodMeta() " - "for required backends and getRegisteredBackends() " - "for registered ones)", - methodName), - self->etModule_->execute(methodName, inputs)); + auto result = getOrThrow(rt, std::format("execute: Method '{}' failed (check getMethodMeta() " + "for required backends and getRegisteredBackends() " + "for registered ones)", + methodName), + self->etModule_->execute(methodName, inputs)); auto jsOutputArray = jsi::Array(rt, result.size()); - size_t index = 0; size_t tensorOutputIdx = 0; for (const auto &output : result) { switch (output.tag) { - case executorch::runtime::Tag::None: { - jsOutputArray.setValueAtIndex(rt, index, jsi::Value::null()); - break; - } case executorch::runtime::Tag::Tensor: { if (tensorOutputIdx >= outputTensorsArray.size(rt)) { throw jsi::JSError(rt, "execute: Not enough tensor output placeholders in outputTensors"); } - auto val = outputTensorsArray.getValueAtIndex(rt, tensorOutputIdx); auto ctx = std::format("execute: outputTensors[{}]", tensorOutputIdx); + auto val = outputTensorsArray.getValueAtIndex(rt, tensorOutputIdx); auto tensorMeta = getOrThrow(rt, std::format("{}: tensor meta", ctx), methodMeta.output_tensor_meta(index)); auto expectedDtype = getDTypeOrThrow(rt, ctx, tensorMeta.scalar_type()); auto tensorHostObject = tensor::fromJs(rt, ctx, val, expectedDtype, tensorMeta.sizes()); if (!lockedTensors.insert(tensorHostObject.get()).second) { - throw jsi::JSError(rt, "execute: Tensor aliasing detected. The same tensor was passed multiple times."); + throw jsi::JSError(rt, "execute: Tensor aliasing detected. " + "The same tensor was passed multiple times."); } tensorLocks.emplace_back(tensor::tryLockUnique(rt, ctx, tensorHostObject)); - - std::memcpy(tensorHostObject->data_.get(), output.toTensor().const_data_ptr(), output.toTensor().nbytes()); + std::memcpy(tensorHostObject->data_.get(), + output.toTensor().const_data_ptr(), + output.toTensor().nbytes()); jsOutputArray.setValueAtIndex(rt, index, jsi::Object::createFromHostObject(rt, tensorHostObject)); ++tensorOutputIdx; - break; } case executorch::runtime::Tag::Double: { @@ -297,8 +302,13 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { jsOutputArray.setValueAtIndex(rt, index, output.toBool()); break; } + case executorch::runtime::Tag::None: { + jsOutputArray.setValueAtIndex(rt, index, jsi::Value::null()); + break; + } default: { - throw jsi::JSError(rt, "execute: Unsupported return type"); + throw jsi::JSError(rt, std::format("execute: Unsupported return type: {}", + executorch::runtime::tag_to_string(output.tag))); } } From f5511f193c3ba925b52d43261081d8a263ba184d Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Thu, 2 Jul 2026 17:25:36 +0200 Subject: [PATCH 08/25] refactor(cpp/core): clean up shape and tensor comparisons in tensor helpers --- .../cpp/core/tensor_helpers.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/react-native-executorch/cpp/core/tensor_helpers.cpp b/packages/react-native-executorch/cpp/core/tensor_helpers.cpp index 3cd88f17f2..5343c78221 100644 --- a/packages/react-native-executorch/cpp/core/tensor_helpers.cpp +++ b/packages/react-native-executorch/cpp/core/tensor_helpers.cpp @@ -34,20 +34,20 @@ tryLockUnique(jsi::Runtime &rt, const std::string &name, const std::shared_ptr &t1, const std::string &name2, const std::shared_ptr &t2) { - if (t1.get() == t2.get()) { + if (t1 == t2) { throw jsi::JSError(rt, std::format("{} and {} cannot be the same tensor", name1, name2)); } } namespace { -std::string shapeToString(const SymbolicShape &expectedShape) { +std::string shapeToString(const SymbolicShape &shape) { std::string s; - for (size_t i = 0; i < expectedShape.size(); ++i) { + for (size_t i = 0; i < shape.size(); ++i) { if (i > 0) { s += ", "; } - const auto &dim = expectedShape.at(i); + const auto &dim = shape.at(i); if (std::holds_alternative(dim)) { s += std::format("{}", std::get(dim)); } else if (std::holds_alternative(dim)) { @@ -78,8 +78,7 @@ fromJs(jsi::Runtime &rt, const std::string &name, const jsi::Value &value, const auto &shape = tensor->shape_; if (expectedDtype && dtype != *expectedDtype) { - throw jsi::JSError(rt, std::format("{} must be of type {}", - name, types::toString(*expectedDtype))); + throw jsi::JSError(rt, std::format("{} must be of type {}", name, types::toString(*expectedDtype))); } if (!expectedShape) { @@ -92,8 +91,10 @@ fromJs(jsi::Runtime &rt, const std::string &name, const jsi::Value &value, } std::unordered_map symbolToConcrete; + for (size_t i = 0; i < expectedShape->size(); ++i) { const auto &dim = expectedShape->at(i); + if (std::holds_alternative(dim)) { const auto expected = std::get(dim); if (shape[i] != expected) { From 28adfc5366ae8584020ec262b286ead7dc4d05f5 Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Thu, 2 Jul 2026 17:44:22 +0200 Subject: [PATCH 09/25] refactor(cpp): restore parameter validation and clean up redundant type checks --- .../cpp/core/model.cpp | 11 ++++--- .../cpp/core/tensor.cpp | 31 ++++++++++++++++--- .../cpp/extensions/nlp/tokenizer.cpp | 8 ----- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/packages/react-native-executorch/cpp/core/model.cpp b/packages/react-native-executorch/cpp/core/model.cpp index cde2fe37c8..4a760ad4c6 100644 --- a/packages/react-native-executorch/cpp/core/model.cpp +++ b/packages/react-native-executorch/cpp/core/model.cpp @@ -119,10 +119,6 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { throw jsi::JSError(rt, "getMethodMeta: Usage: getMethodMeta(methodName)"); } - if (!args[0].isString()) { - throw jsi::JSError(rt, "getMethodMeta: Expected methodName to be a string"); - } - std::unique_lock lock(self->mutex_, std::try_to_lock); if (!lock.owns_lock()) { throw jsi::JSError(rt, "getMethodMeta: Model is currently in use"); @@ -204,6 +200,13 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { auto methodName = conversions::asType(rt, "execute: methodName", args[0]); auto methodMeta = getOrThrow(rt, "execute", self->etModule_->method_meta(methodName)); + if (!args[1].isObject() || !args[1].asObject(rt).isArray(rt)) { + throw jsi::JSError(rt, "execute: Expected inputs to be an Array"); + } + if (!args[2].isObject() || !args[2].asObject(rt).isArray(rt)) { + throw jsi::JSError(rt, "execute: Expected outputTensors to be an Array"); + } + auto inputsArray = args[1].asObject(rt).asArray(rt); auto outputTensorsArray = args[2].asObject(rt).asArray(rt); diff --git a/packages/react-native-executorch/cpp/core/tensor.cpp b/packages/react-native-executorch/cpp/core/tensor.cpp index 7cf822ed5b..b5d8a5ec3a 100644 --- a/packages/react-native-executorch/cpp/core/tensor.cpp +++ b/packages/react-native-executorch/cpp/core/tensor.cpp @@ -57,6 +57,9 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) auto srcLock = tryLockShared(rt, "copyTo: self", self); auto dstLock = tryLockUnique(rt, "copyTo: dst", dst); + if (count == 2 && !args[1].isObject()) { + throw jsi::JSError(rt, "copyTo: Expected options to be an object"); + } const jsi::Object optsObj = (count == 2) ? args[1].asObject(rt) : jsi::Object(rt); const size_t srcOffset = conversions::getOptionalProperty(rt, "copyTo", optsObj, "offset").value_or(0); const size_t copyLen = conversions::getOptionalProperty(rt, "copyTo", optsObj, "length").value_or(self->numel_ - srcOffset); @@ -85,8 +88,15 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) throw jsi::JSError(rt, "setData: Usage: setData(array)"); } - const auto dataObj = args[0].asObject(rt); - const auto buffer = dataObj.getProperty(rt, "buffer").asObject(rt).getArrayBuffer(rt); + if (!args[0].isObject()) { + throw jsi::JSError(rt, "setData: Expected array to be an object (TypedArray)"); + } + auto dataObj = args[0].asObject(rt); + auto bufferVal = conversions::getRequiredProperty(rt, "setData", dataObj, "buffer"); + if (!bufferVal.isObject() || !bufferVal.asObject(rt).isArrayBuffer(rt)) { + throw jsi::JSError(rt, "setData: option 'buffer' must be an ArrayBuffer"); + } + auto buffer = bufferVal.asObject(rt).getArrayBuffer(rt); size_t byteOffset = conversions::getOptionalProperty(rt, "setData", dataObj, "byteOffset").value_or(0); size_t byteLength = conversions::getOptionalProperty(rt, "setData", dataObj, "byteLength").value_or(buffer.size(rt)); @@ -111,8 +121,15 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) throw jsi::JSError(rt, "getData: Usage: getData(array)"); } - const jsi::Object dataObj = args[0].asObject(rt); - const jsi::ArrayBuffer buffer = dataObj.getProperty(rt, "buffer").asObject(rt).getArrayBuffer(rt); + if (!args[0].isObject()) { + throw jsi::JSError(rt, "getData: Expected array to be an object (TypedArray)"); + } + auto dataObj = args[0].asObject(rt); + auto bufferVal = conversions::getRequiredProperty(rt, "getData", dataObj, "buffer"); + if (!bufferVal.isObject() || !bufferVal.asObject(rt).isArrayBuffer(rt)) { + throw jsi::JSError(rt, "getData: option 'buffer' must be an ArrayBuffer"); + } + auto buffer = bufferVal.asObject(rt).getArrayBuffer(rt); size_t byteOffset = conversions::getOptionalProperty(rt, "getData", dataObj, "byteOffset").value_or(0); size_t byteLength = conversions::getOptionalProperty(rt, "getData", dataObj, "byteLength").value_or(buffer.size(rt)); @@ -137,6 +154,9 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) throw jsi::JSError(rt, "through: Usage: through(fn, ...args)"); } + if (!args[0].isObject() || !args[0].asObject(rt).isFunction(rt)) { + throw jsi::JSError(rt, "through: First argument must be a function"); + } auto fn = args[0].asObject(rt).asFunction(rt); std::vector fnArgs; @@ -164,6 +184,9 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) return jsi::Value(rt, thisVal); } + if (!args[1].isObject() || !args[1].asObject(rt).isFunction(rt)) { + throw jsi::JSError(rt, "throughIf: Second argument must be a function"); + } auto fn = args[1].asObject(rt).asFunction(rt); std::vector fnArgs; diff --git a/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.cpp b/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.cpp index c48123bda2..cd3813c94b 100644 --- a/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.cpp +++ b/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.cpp @@ -73,10 +73,6 @@ jsi::Value TokenizerHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &nam throw jsi::JSError(rt, "encode: Usage: encode(text)"); } - if (!args[0].isString()) { - throw jsi::JSError(rt, "encode: Expected arg0 to be a string"); - } - std::unique_lock lock(self->mutex_, std::try_to_lock); if (!lock.owns_lock()) { throw jsi::JSError(rt, "encode: Tokenizer is currently in use"); @@ -163,10 +159,6 @@ jsi::Value TokenizerHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &nam throw jsi::JSError(rt, "idToToken: Usage: idToToken(id)"); } - if (!args[0].isNumber()) { - throw jsi::JSError(rt, "idToToken: Expected arg0 to be a number"); - } - std::unique_lock lock(self->mutex_, std::try_to_lock); if (!lock.owns_lock()) { throw jsi::JSError(rt, "idToToken: Tokenizer is currently in use"); From 086fc5c72462272d32fc24ebdd1fe12c926e918b Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Thu, 2 Jul 2026 18:17:12 +0200 Subject: [PATCH 10/25] refactor(cpp): implement strict range-checked template conversions for JS types --- .../cpp/core/conversions.cpp | 51 ++++++++++++++----- .../cpp/core/conversions.h | 15 ++---- 2 files changed, 43 insertions(+), 23 deletions(-) diff --git a/packages/react-native-executorch/cpp/core/conversions.cpp b/packages/react-native-executorch/cpp/core/conversions.cpp index ec1dd78abf..9136fe3983 100644 --- a/packages/react-native-executorch/cpp/core/conversions.cpp +++ b/packages/react-native-executorch/cpp/core/conversions.cpp @@ -1,8 +1,19 @@ #include "conversions.h" #include +#include namespace rnexecutorch::core::conversions { +constexpr double kMaxInt64Double = static_cast(std::numeric_limits::max()); +constexpr double kMinInt64Double = static_cast(std::numeric_limits::min()); +constexpr double kMaxUint64Double = static_cast(std::numeric_limits::max()); + +constexpr double kMinInt32Double = static_cast(std::numeric_limits::min()); +constexpr double kMaxInt32Double = static_cast(std::numeric_limits::max()); + +constexpr double kMinUint8Double = static_cast(std::numeric_limits::min()); +constexpr double kMaxUint8Double = static_cast(std::numeric_limits::max()); + template <> double asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) { if (!val.isNumber()) { @@ -18,18 +29,26 @@ float asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value & template <> int32_t asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) { - return static_cast(asType(rt, ctx, 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"); + } + return static_cast(v); } template <> int64_t asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) { - return static_cast(asType(rt, ctx, 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"); + } + return static_cast(v); } 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) || v < 0.0) { + 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"); } return static_cast(v); @@ -38,20 +57,12 @@ uint64_t asType(jsi::Runtime &rt, const std::string &ctx, const jsi::V 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) || v < 0.0 || v > 255.0) { - throw jsi::JSError(rt, ctx + " must be between 0 and 255"); + 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"); } return static_cast(v); } -// Note: size_t is intentionally not specialised here. -// On Linux x86_64 size_t and uint64_t are both `unsigned long`, so adding a -// size_t specialisation would produce a duplicate-explicit-specialisation ODR -// error on that platform. asType routes through the uint64_t -// specialisation on Linux (same type) and is a separate instantiation on -// macOS (where uint64_t is unsigned long long). Either way the semantics — -// non-negative double → cast — are identical. - template <> bool asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) { if (!val.isBool()) { @@ -67,4 +78,18 @@ std::string asType(jsi::Runtime &rt, const std::string &ctx, const } return val.asString(rt).utf8(rt); } + +template <> +jsi::Value asType(jsi::Runtime &rt, const std::string & /*ctx*/, const jsi::Value &val) { + return jsi::Value(rt, val); +} + +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"); + } + return val.asObject(rt).getArrayBuffer(rt); +} + } // namespace rnexecutorch::core::conversions diff --git a/packages/react-native-executorch/cpp/core/conversions.h b/packages/react-native-executorch/cpp/core/conversions.h index b72e6f072e..36d32914df 100644 --- a/packages/react-native-executorch/cpp/core/conversions.h +++ b/packages/react-native-executorch/cpp/core/conversions.h @@ -16,17 +16,12 @@ namespace jsi = facebook::jsi; template T asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val); -template <> -inline jsi::Value asType(jsi::Runtime &rt, const std::string & /*ctx*/, const jsi::Value &val) { - return jsi::Value(rt, val); -} - 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 jsi::JSError(rt, ctx + ": option '" + propName + "' is required"); } - return asType(rt, std::format("{}: option '{}'", ctx, propName), obj.getProperty(rt, propName.c_str())); + return asType(rt, ctx + ": option '" + propName + "'", obj.getProperty(rt, propName.c_str())); } template @@ -38,20 +33,20 @@ std::optional getOptionalProperty(jsi::Runtime &rt, const std::string &ctx, c if (val.isUndefined() || val.isNull()) { return std::nullopt; } - return asType(rt, std::format("{}: option '{}'", ctx, propName), val); + return asType(rt, ctx + ": option '" + propName + "'", val); } template std::vector asVector(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) { if (!val.isObject() || !val.asObject(rt).isArray(rt)) { - throw jsi::JSError(rt, std::format("{} must be an Array", ctx)); + throw jsi::JSError(rt, ctx + " must be an Array"); } auto arr = val.asObject(rt).asArray(rt); std::vector vec; const size_t len = arr.size(rt); vec.reserve(len); for (size_t i = 0; i < len; ++i) { - vec.push_back(asType(rt, std::format("{}[{}]", ctx, i), arr.getValueAtIndex(rt, i))); + vec.push_back(asType(rt, ctx + "[" + std::to_string(i) + "]", arr.getValueAtIndex(rt, i))); } return vec; } From 051c6ac87eec35a34a61b82745bc12946eb454ad Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Thu, 2 Jul 2026 18:17:24 +0200 Subject: [PATCH 11/25] refactor(cpp): restore execution error messages and use getRequiredProperty for ArrayBuffer --- .../cpp/core/model.cpp | 46 ++++++++++--------- .../cpp/core/tensor.cpp | 12 +---- 2 files changed, 26 insertions(+), 32 deletions(-) diff --git a/packages/react-native-executorch/cpp/core/model.cpp b/packages/react-native-executorch/cpp/core/model.cpp index 4a760ad4c6..e396b01fa0 100644 --- a/packages/react-native-executorch/cpp/core/model.cpp +++ b/packages/react-native-executorch/cpp/core/model.cpp @@ -18,19 +18,19 @@ namespace jsi = facebook::jsi; namespace types = rnexecutorch::core::types; template -T getOrThrow(jsi::Runtime &rt, const std::string &ctx, executorch::runtime::Result result) { +T unwrap(jsi::Runtime &rt, 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 jsi::JSError(rt, ctx + ": " + executorch::runtime::to_string(result.error())); } return std::move(result.get()); } types::DType -getDTypeOrThrow(jsi::Runtime &rt, const std::string &ctx, executorch::aten::ScalarType scalarType) { +fromScalarType(jsi::Runtime &rt, const std::string &ctx, executorch::aten::ScalarType scalarType) { try { return types::fromScalarType(scalarType); } catch (const std::exception &e) { - throw jsi::JSError(rt, std::format("{}: Unsupported tensor dtype: {}", ctx, e.what())); + throw jsi::JSError(rt, ctx + ": Unsupported tensor dtype: " + e.what()); } } @@ -96,7 +96,7 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { throw jsi::JSError(rt, "getMethodNames: Model has been disposed"); } - auto methodNames = getOrThrow(rt, "getMethodNames", self->etModule_->method_names()); + auto methodNames = unwrap(rt, "getMethodNames", self->etModule_->method_names()); auto jsArray = jsi::Array(rt, methodNames.size()); size_t index = 0; @@ -129,40 +129,40 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { } auto methodName = conversions::asType(rt, "getMethodMeta: methodName", args[0]); - auto methodMeta = getOrThrow(rt, "getMethodMeta", self->etModule_->method_meta(methodName)); + auto methodMeta = unwrap(rt, "getMethodMeta", self->etModule_->method_meta(methodName)); auto inputTagsArray = jsi::Array(rt, methodMeta.num_inputs()); for (size_t i = 0; i < methodMeta.num_inputs(); ++i) { auto ctx = std::format("getMethodMeta: input tag [{}]", i); - auto tag = getOrThrow(rt, ctx, methodMeta.input_tag(i)); + auto tag = unwrap(rt, ctx, methodMeta.input_tag(i)); inputTagsArray.setValueAtIndex(rt, i, jsi::String::createFromUtf8(rt, tag_to_string(tag))); } auto outputTagsArray = jsi::Array(rt, methodMeta.num_outputs()); for (size_t i = 0; i < methodMeta.num_outputs(); ++i) { auto ctx = std::format("getMethodMeta: output tag [{}]", i); - auto tag = getOrThrow(rt, ctx, methodMeta.output_tag(i)); + auto tag = unwrap(rt, ctx, methodMeta.output_tag(i)); outputTagsArray.setValueAtIndex(rt, i, jsi::String::createFromUtf8(rt, tag_to_string(tag))); } auto usesBackendMap = jsi::Object(rt); for (size_t i = 0; i < methodMeta.num_backends(); ++i) { auto ctx = std::format("getMethodMeta: backend name [{}]", i); - const auto *backendName = getOrThrow(rt, ctx, methodMeta.get_backend_name(i)); + const auto *backendName = unwrap(rt, ctx, methodMeta.get_backend_name(i)); usesBackendMap.setProperty(rt, backendName, methodMeta.uses_backend(backendName)); } auto inputTensorMetaArray = jsi::Array(rt, methodMeta.num_inputs()); for (size_t i = 0; i < methodMeta.num_inputs(); ++i) { auto ctx = std::format("getMethodMeta: input tensor meta [{}]", i); - auto tensorMeta = getOrThrow(rt, ctx, methodMeta.input_tensor_meta(i)); + auto tensorMeta = unwrap(rt, ctx, methodMeta.input_tensor_meta(i)); inputTensorMetaArray.setValueAtIndex(rt, i, tensorMetaToJs(rt, tensorMeta)); } auto outputTensorMetaArray = jsi::Array(rt, methodMeta.num_outputs()); for (size_t i = 0; i < methodMeta.num_outputs(); ++i) { auto ctx = std::format("getMethodMeta: output tensor meta [{}]", i); - auto tensorMeta = getOrThrow(rt, ctx, methodMeta.output_tensor_meta(i)); + auto tensorMeta = unwrap(rt, ctx, methodMeta.output_tensor_meta(i)); outputTensorMetaArray.setValueAtIndex(rt, i, tensorMetaToJs(rt, tensorMeta)); } @@ -198,7 +198,7 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { } auto methodName = conversions::asType(rt, "execute: methodName", args[0]); - auto methodMeta = getOrThrow(rt, "execute", self->etModule_->method_meta(methodName)); + auto methodMeta = unwrap(rt, "execute", self->etModule_->method_meta(methodName)); if (!args[1].isObject() || !args[1].asObject(rt).isArray(rt)) { throw jsi::JSError(rt, "execute: Expected inputs to be an Array"); @@ -221,13 +221,13 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { for (size_t i = 0; i < methodMeta.num_inputs(); ++i) { auto ctx = std::format("execute: inputs[{}]", i); - auto tag = getOrThrow(rt, ctx, methodMeta.input_tag(i)); + auto tag = unwrap(rt, ctx, methodMeta.input_tag(i)); auto val = inputsArray.getValueAtIndex(rt, i); switch (tag) { case executorch::runtime::Tag::Tensor: { - auto tensorMeta = getOrThrow(rt, std::format("{}: tensor meta", ctx), methodMeta.input_tensor_meta(i)); - auto expectedDtype = getDTypeOrThrow(rt, ctx, tensorMeta.scalar_type()); + auto tensorMeta = unwrap(rt, ctx + ": tensor meta", methodMeta.input_tensor_meta(i)); + auto expectedDtype = fromScalarType(rt, ctx, tensorMeta.scalar_type()); auto tensorHostObject = tensor::fromJs(rt, ctx, val, expectedDtype, tensorMeta.sizes()); if (!lockedTensors.insert(tensorHostObject.get()).second) { @@ -256,11 +256,13 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { } } - auto result = getOrThrow(rt, std::format("execute: Method '{}' failed (check getMethodMeta() " - "for required backends and getRegisteredBackends() " - "for registered ones)", - methodName), - self->etModule_->execute(methodName, inputs)); + auto result = unwrap(rt, std::format("execute: Method '{}' execution failed. " + "This may be due to missing required backends - " + "use getMethodMeta() to check required backends and " + "getExecuTorchRegisteredBackends() to check " + "which backends are registered in the runtime. Error:", + methodName), + self->etModule_->execute(methodName, inputs)); auto jsOutputArray = jsi::Array(rt, result.size()); size_t index = 0; @@ -276,8 +278,8 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { auto ctx = std::format("execute: outputTensors[{}]", tensorOutputIdx); auto val = outputTensorsArray.getValueAtIndex(rt, tensorOutputIdx); - auto tensorMeta = getOrThrow(rt, std::format("{}: tensor meta", ctx), methodMeta.output_tensor_meta(index)); - auto expectedDtype = getDTypeOrThrow(rt, ctx, tensorMeta.scalar_type()); + auto tensorMeta = unwrap(rt, ctx + ": tensor meta", methodMeta.output_tensor_meta(index)); + auto expectedDtype = fromScalarType(rt, ctx, tensorMeta.scalar_type()); auto tensorHostObject = tensor::fromJs(rt, ctx, val, expectedDtype, tensorMeta.sizes()); if (!lockedTensors.insert(tensorHostObject.get()).second) { diff --git a/packages/react-native-executorch/cpp/core/tensor.cpp b/packages/react-native-executorch/cpp/core/tensor.cpp index b5d8a5ec3a..414a6e187d 100644 --- a/packages/react-native-executorch/cpp/core/tensor.cpp +++ b/packages/react-native-executorch/cpp/core/tensor.cpp @@ -92,11 +92,7 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) throw jsi::JSError(rt, "setData: Expected array to be an object (TypedArray)"); } auto dataObj = args[0].asObject(rt); - auto bufferVal = conversions::getRequiredProperty(rt, "setData", dataObj, "buffer"); - if (!bufferVal.isObject() || !bufferVal.asObject(rt).isArrayBuffer(rt)) { - throw jsi::JSError(rt, "setData: option 'buffer' must be an ArrayBuffer"); - } - auto buffer = bufferVal.asObject(rt).getArrayBuffer(rt); + auto buffer = conversions::getRequiredProperty(rt, "setData", dataObj, "buffer"); size_t byteOffset = conversions::getOptionalProperty(rt, "setData", dataObj, "byteOffset").value_or(0); size_t byteLength = conversions::getOptionalProperty(rt, "setData", dataObj, "byteLength").value_or(buffer.size(rt)); @@ -125,11 +121,7 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) throw jsi::JSError(rt, "getData: Expected array to be an object (TypedArray)"); } auto dataObj = args[0].asObject(rt); - auto bufferVal = conversions::getRequiredProperty(rt, "getData", dataObj, "buffer"); - if (!bufferVal.isObject() || !bufferVal.asObject(rt).isArrayBuffer(rt)) { - throw jsi::JSError(rt, "getData: option 'buffer' must be an ArrayBuffer"); - } - auto buffer = bufferVal.asObject(rt).getArrayBuffer(rt); + auto buffer = conversions::getRequiredProperty(rt, "getData", dataObj, "buffer"); size_t byteOffset = conversions::getOptionalProperty(rt, "getData", dataObj, "byteOffset").value_or(0); size_t byteLength = conversions::getOptionalProperty(rt, "getData", dataObj, "byteLength").value_or(buffer.size(rt)); From 0762abeea48fd597e47700654b922c125e27d30f Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Thu, 2 Jul 2026 18:23:23 +0200 Subject: [PATCH 12/25] refactor(cpp): simplify shapeToString using range-based loop and pop_back --- .../cpp/core/tensor_helpers.cpp | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/react-native-executorch/cpp/core/tensor_helpers.cpp b/packages/react-native-executorch/cpp/core/tensor_helpers.cpp index 5343c78221..27a09b46d8 100644 --- a/packages/react-native-executorch/cpp/core/tensor_helpers.cpp +++ b/packages/react-native-executorch/cpp/core/tensor_helpers.cpp @@ -40,16 +40,11 @@ void checkNotSameTensor(jsi::Runtime &rt, } namespace { - std::string shapeToString(const SymbolicShape &shape) { std::string s; - for (size_t i = 0; i < shape.size(); ++i) { - if (i > 0) { - s += ", "; - } - const auto &dim = shape.at(i); + for (const auto &dim : shape) { if (std::holds_alternative(dim)) { - s += std::format("{}", std::get(dim)); + s += std::to_string(std::get(dim)); } else if (std::holds_alternative(dim)) { s += std::get(dim); } else { @@ -59,10 +54,14 @@ std::string shapeToString(const SymbolicShape &shape) { rangeDim.max, rangeDim.step ? std::format(" step {}", *rangeDim.step) : ""); } + s += ", "; } - return std::format("[{}]", s); + if (!shape.empty()) { + s.pop_back(); + s.pop_back(); + } + return "[" + s + "]"; } - } // namespace std::shared_ptr From a01f7ec33f063332853ce9f3f09b164075d28b14 Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Thu, 2 Jul 2026 18:29:52 +0200 Subject: [PATCH 13/25] refactor(cpp): preserve output tensor JS object identity and clean up switch braces in model.cpp --- .../react-native-executorch/cpp/core/model.cpp | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/react-native-executorch/cpp/core/model.cpp b/packages/react-native-executorch/cpp/core/model.cpp index e396b01fa0..301b5f749b 100644 --- a/packages/react-native-executorch/cpp/core/model.cpp +++ b/packages/react-native-executorch/cpp/core/model.cpp @@ -291,31 +291,26 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { output.toTensor().const_data_ptr(), output.toTensor().nbytes()); - jsOutputArray.setValueAtIndex(rt, index, jsi::Object::createFromHostObject(rt, tensorHostObject)); + jsOutputArray.setValueAtIndex(rt, index, val); ++tensorOutputIdx; break; } - case executorch::runtime::Tag::Double: { + case executorch::runtime::Tag::Double: jsOutputArray.setValueAtIndex(rt, index, output.toDouble()); break; - } - case executorch::runtime::Tag::Int: { + case executorch::runtime::Tag::Int: jsOutputArray.setValueAtIndex(rt, index, static_cast(output.toInt())); break; - } - case executorch::runtime::Tag::Bool: { + case executorch::runtime::Tag::Bool: jsOutputArray.setValueAtIndex(rt, index, output.toBool()); break; - } - case executorch::runtime::Tag::None: { + case executorch::runtime::Tag::None: jsOutputArray.setValueAtIndex(rt, index, jsi::Value::null()); break; - } - default: { + default: throw jsi::JSError(rt, std::format("execute: Unsupported return type: {}", executorch::runtime::tag_to_string(output.tag))); } - } ++index; } From fb4a7ed35ea5951bc15f94814d742dacace52c37 Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Thu, 2 Jul 2026 18:51:42 +0200 Subject: [PATCH 14/25] refactor(cpp): restore execution profiling and original short execute error message in model.cpp --- .../cpp/core/model.cpp | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/react-native-executorch/cpp/core/model.cpp b/packages/react-native-executorch/cpp/core/model.cpp index 301b5f749b..f0ceeea241 100644 --- a/packages/react-native-executorch/cpp/core/model.cpp +++ b/packages/react-native-executorch/cpp/core/model.cpp @@ -256,13 +256,23 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { } } - auto result = unwrap(rt, std::format("execute: Method '{}' execution failed. " - "This may be due to missing required backends - " - "use getMethodMeta() to check required backends and " - "getExecuTorchRegisteredBackends() to check " - "which backends are registered in the runtime. Error:", + auto startTime = std::chrono::high_resolution_clock::now(); + auto executeResult = self->etModule_->execute(methodName, inputs); + auto finishTime = std::chrono::high_resolution_clock::now(); + +#ifdef EXECUTORCH_ENABLE_EXECUTION_PROFILING + auto durationMs = std::chrono::duration_cast(finishTime - startTime).count(); + auto consoleObj = rt.global().getProperty(rt, "console").asObject(rt); + auto logFn = consoleObj.getProperty(rt, "log").asObject(rt).asFunction(rt); + auto info = std::format("Execution of method '{}' took {} ms", methodName, durationMs); + logFn.callWithThis(rt, consoleObj, {jsi::String::createFromUtf8(rt, info)}); +#endif + + auto result = unwrap(rt, std::format("execute: Method '{}' failed (check getMethodMeta() " + "for required backends and getRegisteredBackends() " + "for registered ones)", methodName), - self->etModule_->execute(methodName, inputs)); + std::move(executeResult)); auto jsOutputArray = jsi::Array(rt, result.size()); size_t index = 0; From db99e733fe7668cfdb02afd761988f637eb4850c Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Thu, 2 Jul 2026 20:14:57 +0200 Subject: [PATCH 15/25] style: fix using declaration in model.cpp --- packages/react-native-executorch/cpp/core/model.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-executorch/cpp/core/model.cpp b/packages/react-native-executorch/cpp/core/model.cpp index f0ceeea241..86f1d1e94c 100644 --- a/packages/react-native-executorch/cpp/core/model.cpp +++ b/packages/react-native-executorch/cpp/core/model.cpp @@ -61,7 +61,7 @@ namespace rnexecutorch::core::model { namespace jsi = facebook::jsi; namespace conversions = rnexecutorch::core::conversions; -using TensorHostObject = rnexecutorch::core::tensor::TensorHostObject; +using rnexecutorch::core::tensor::TensorHostObject; ModelHostObject::ModelHostObject(const std::string &modelPath) : modelPath_(modelPath), From 91c2864d6d3a11359b92b53974900d9bf1d641ec Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Thu, 2 Jul 2026 20:38:32 +0200 Subject: [PATCH 16/25] refactor(cpp): replace manual JSI type checking with asType specializations --- .../cpp/core/conversions.cpp | 24 +++++++++++++ .../cpp/core/model.cpp | 11 ++---- .../cpp/core/tensor.cpp | 36 +++++++------------ 3 files changed, 38 insertions(+), 33 deletions(-) diff --git a/packages/react-native-executorch/cpp/core/conversions.cpp b/packages/react-native-executorch/cpp/core/conversions.cpp index 9136fe3983..184cd2636a 100644 --- a/packages/react-native-executorch/cpp/core/conversions.cpp +++ b/packages/react-native-executorch/cpp/core/conversions.cpp @@ -92,4 +92,28 @@ jsi::ArrayBuffer asType(jsi::Runtime &rt, const std::string &c return val.asObject(rt).getArrayBuffer(rt); } +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"); + } + return val.asObject(rt); +} + +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"); + } + return val.asObject(rt).asArray(rt); +} + +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"); + } + return val.asObject(rt).asFunction(rt); +} + } // namespace rnexecutorch::core::conversions diff --git a/packages/react-native-executorch/cpp/core/model.cpp b/packages/react-native-executorch/cpp/core/model.cpp index 86f1d1e94c..f23d07947a 100644 --- a/packages/react-native-executorch/cpp/core/model.cpp +++ b/packages/react-native-executorch/cpp/core/model.cpp @@ -200,15 +200,8 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { auto methodName = conversions::asType(rt, "execute: methodName", args[0]); auto methodMeta = unwrap(rt, "execute", self->etModule_->method_meta(methodName)); - if (!args[1].isObject() || !args[1].asObject(rt).isArray(rt)) { - throw jsi::JSError(rt, "execute: Expected inputs to be an Array"); - } - if (!args[2].isObject() || !args[2].asObject(rt).isArray(rt)) { - throw jsi::JSError(rt, "execute: Expected outputTensors to be an Array"); - } - - auto inputsArray = args[1].asObject(rt).asArray(rt); - auto outputTensorsArray = args[2].asObject(rt).asArray(rt); + auto inputsArray = conversions::asType(rt, "execute: inputs", args[1]); + auto outputTensorsArray = conversions::asType(rt, "execute: outputTensors", args[2]); if (inputsArray.size(rt) != methodMeta.num_inputs()) { throw jsi::JSError(rt, std::format("execute: Incorrect size for inputs: got {}, expected {}", diff --git a/packages/react-native-executorch/cpp/core/tensor.cpp b/packages/react-native-executorch/cpp/core/tensor.cpp index 414a6e187d..f1c333f763 100644 --- a/packages/react-native-executorch/cpp/core/tensor.cpp +++ b/packages/react-native-executorch/cpp/core/tensor.cpp @@ -57,24 +57,24 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) auto srcLock = tryLockShared(rt, "copyTo: self", self); auto dstLock = tryLockUnique(rt, "copyTo: dst", dst); - if (count == 2 && !args[1].isObject()) { - throw jsi::JSError(rt, "copyTo: Expected options to be an object"); + jsi::Object optsObj(rt); + if (count == 2) { + optsObj = conversions::asType(rt, "copyTo: options", args[1]); } - const jsi::Object optsObj = (count == 2) ? args[1].asObject(rt) : jsi::Object(rt); - const size_t srcOffset = conversions::getOptionalProperty(rt, "copyTo", optsObj, "offset").value_or(0); - const size_t copyLen = conversions::getOptionalProperty(rt, "copyTo", optsObj, "length").value_or(self->numel_ - srcOffset); + size_t offset = conversions::getOptionalProperty(rt, "copyTo", optsObj, "offset").value_or(0); + size_t length = conversions::getOptionalProperty(rt, "copyTo", optsObj, "length").value_or(self->numel_ - offset); - if (srcOffset + copyLen > self->numel_) { + if (offset + length > self->numel_) { throw jsi::JSError(rt, "copyTo: out of bounds offset and length for src tensor"); } const auto elemSize = types::elementSize(self->dtype_); - if (copyLen * elemSize != dst->size_) { + if (length * elemSize != dst->size_) { throw jsi::JSError(rt, "copyTo: size mismatch between copy byte size and dst tensor size"); } - std::memcpy(dst->data_.get(), self->data_.get() + (srcOffset * elemSize), copyLen * elemSize); + std::memcpy(dst->data_.get(), self->data_.get() + (offset * elemSize), length * elemSize); return jsi::Value(rt, args[0].asObject(rt)); }; @@ -88,10 +88,7 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) throw jsi::JSError(rt, "setData: Usage: setData(array)"); } - if (!args[0].isObject()) { - throw jsi::JSError(rt, "setData: Expected array to be an object (TypedArray)"); - } - auto dataObj = args[0].asObject(rt); + auto dataObj = conversions::asType(rt, "setData: array", args[0]); auto buffer = conversions::getRequiredProperty(rt, "setData", dataObj, "buffer"); size_t byteOffset = conversions::getOptionalProperty(rt, "setData", dataObj, "byteOffset").value_or(0); size_t byteLength = conversions::getOptionalProperty(rt, "setData", dataObj, "byteLength").value_or(buffer.size(rt)); @@ -117,10 +114,7 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) throw jsi::JSError(rt, "getData: Usage: getData(array)"); } - if (!args[0].isObject()) { - throw jsi::JSError(rt, "getData: Expected array to be an object (TypedArray)"); - } - auto dataObj = args[0].asObject(rt); + auto dataObj = conversions::asType(rt, "getData: array", args[0]); auto buffer = conversions::getRequiredProperty(rt, "getData", dataObj, "buffer"); size_t byteOffset = conversions::getOptionalProperty(rt, "getData", dataObj, "byteOffset").value_or(0); size_t byteLength = conversions::getOptionalProperty(rt, "getData", dataObj, "byteLength").value_or(buffer.size(rt)); @@ -146,10 +140,7 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) throw jsi::JSError(rt, "through: Usage: through(fn, ...args)"); } - if (!args[0].isObject() || !args[0].asObject(rt).isFunction(rt)) { - throw jsi::JSError(rt, "through: First argument must be a function"); - } - auto fn = args[0].asObject(rt).asFunction(rt); + auto fn = conversions::asType(rt, "through: fn", args[0]); std::vector fnArgs; fnArgs.reserve(count); @@ -176,10 +167,7 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) return jsi::Value(rt, thisVal); } - if (!args[1].isObject() || !args[1].asObject(rt).isFunction(rt)) { - throw jsi::JSError(rt, "throughIf: Second argument must be a function"); - } - auto fn = args[1].asObject(rt).asFunction(rt); + auto fn = conversions::asType(rt, "throughIf: fn", args[1]); std::vector fnArgs; fnArgs.reserve(count - 1); From 13e4bcd3ba9f9f6fedca354ead8381b8a7ce04f2 Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Fri, 3 Jul 2026 02:18:35 +0200 Subject: [PATCH 17/25] refactor(cpp): use macro for template specializations and delete primary asType template --- .../cpp/core/conversions.h | 81 +++++++++++++++++-- 1 file changed, 75 insertions(+), 6 deletions(-) diff --git a/packages/react-native-executorch/cpp/core/conversions.h b/packages/react-native-executorch/cpp/core/conversions.h index 36d32914df..13f9c61ece 100644 --- a/packages/react-native-executorch/cpp/core/conversions.h +++ b/packages/react-native-executorch/cpp/core/conversions.h @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -13,9 +12,50 @@ namespace rnexecutorch::core::conversions { namespace jsi = facebook::jsi; +/** + * Converts a facebook::jsi::Value to a specified target C++ or JSI type. Throws + * a facebook::jsi::JSError with contextual error info if the conversion fails. + * + * @tparam T The target type to convert to. + * @param rt The JSI runtime instance. + * @param ctx Context description used to generate helpful error messages. + * @param val The JSI value to convert. + * @return The converted value of type T. + */ template -T asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val); +T asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) = delete; +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage): macro is used for succinct template specialization declarations +#define DECLARE_ASTYPE_SPECIALIZATION(Type) \ + template <> \ + Type asType(jsi::Runtime & rt, const std::string &ctx, const jsi::Value &val) +DECLARE_ASTYPE_SPECIALIZATION(double); +DECLARE_ASTYPE_SPECIALIZATION(float); +DECLARE_ASTYPE_SPECIALIZATION(int32_t); +DECLARE_ASTYPE_SPECIALIZATION(int64_t); +DECLARE_ASTYPE_SPECIALIZATION(uint64_t); +DECLARE_ASTYPE_SPECIALIZATION(uint8_t); +DECLARE_ASTYPE_SPECIALIZATION(bool); +DECLARE_ASTYPE_SPECIALIZATION(std::string); +DECLARE_ASTYPE_SPECIALIZATION(jsi::Value); +DECLARE_ASTYPE_SPECIALIZATION(jsi::Object); +DECLARE_ASTYPE_SPECIALIZATION(jsi::Array); +DECLARE_ASTYPE_SPECIALIZATION(jsi::Function); +DECLARE_ASTYPE_SPECIALIZATION(jsi::ArrayBuffer); +#undef DECLARE_ASTYPE_SPECIALIZATION + +/** + * Retrieves a required property from a JSI object, converting it to the + * specified type. Throws a facebook::jsi::JSError if the property is missing or + * of an incorrect type. + * + * @tparam T The target type to convert the property to. + * @param rt The JSI runtime instance. + * @param ctx Context description used for error messages. + * @param obj The JSI object containing the property. + * @param propName The name of the property to retrieve. + * @return The converted property value. + */ 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())) { @@ -24,6 +64,18 @@ T getRequiredProperty(jsi::Runtime &rt, const std::string &ctx, const jsi::Objec return asType(rt, ctx + ": option '" + propName + "'", obj.getProperty(rt, propName.c_str())); } +/** + * Retrieves an optional property from a JSI object, returning std::nullopt if + * the property is missing, null, or undefined. Throws a facebook::jsi::JSError + * if the property exists but cannot be converted to the target type. + * + * @tparam T The target type to convert the property to if present. + * @param rt The JSI runtime instance. + * @param ctx Context description used for error messages. + * @param obj The JSI object containing the property. + * @param propName The name of the property to retrieve. + * @return An optional containing the converted property value, or std::nullopt. + */ template std::optional getOptionalProperty(jsi::Runtime &rt, const std::string &ctx, const jsi::Object &obj, const std::string &propName) { if (!obj.hasProperty(rt, propName.c_str())) { @@ -36,12 +88,20 @@ std::optional getOptionalProperty(jsi::Runtime &rt, const std::string &ctx, c return asType(rt, ctx + ": option '" + propName + "'", val); } +/** + * Converts a JSI Array to a std::vector of the specified type. Converts each + * element of the array individually and propagates index details in the error + * context. + * + * @tparam T The target element type. + * @param rt The JSI runtime instance. + * @param ctx Context description used for error messages. + * @param val The JSI value (must be an Array). + * @return A std::vector containing the converted elements. + */ template std::vector asVector(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"); - } - auto arr = val.asObject(rt).asArray(rt); + auto arr = asType(rt, ctx, val); std::vector vec; const size_t len = arr.size(rt); vec.reserve(len); @@ -51,6 +111,15 @@ std::vector asVector(jsi::Runtime &rt, const std::string &ctx, const jsi::Val return vec; } +/** + * Converts a std::vector of values to a new facebook::jsi::Array. + * Handles strings, booleans, and numeric types appropriately. + * + * @tparam T The element type in the vector. + * @param rt The JSI runtime instance. + * @param vec The source vector to convert. + * @return A new facebook::jsi::Array containing the elements from the vector. + */ template jsi::Array toJsiArray(jsi::Runtime &rt, const std::vector &vec) { jsi::Array arr(rt, vec.size()); From 1b0caa3281fe0de60615748867ac0b1785c14c74 Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Fri, 3 Jul 2026 02:18:40 +0200 Subject: [PATCH 18/25] refactor(cpp): safe copy/move return for jsi::Value and const correctness in TensorHostObject --- .../cpp/core/tensor.cpp | 37 ++++++++++--------- .../react-native-executorch/cpp/core/tensor.h | 16 ++++---- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/packages/react-native-executorch/cpp/core/tensor.cpp b/packages/react-native-executorch/cpp/core/tensor.cpp index f1c333f763..3248e2f87f 100644 --- a/packages/react-native-executorch/cpp/core/tensor.cpp +++ b/packages/react-native-executorch/cpp/core/tensor.cpp @@ -1,6 +1,4 @@ #include "tensor.h" -#include "dtype.h" -#include "tensor_helpers.h" #include #include @@ -10,8 +8,10 @@ #include #include #include -#include -#include + +#include "core/conversions.h" +#include "dtype.h" +#include "tensor_helpers.h" #include @@ -19,13 +19,16 @@ namespace rnexecutorch::core::tensor { namespace types = rnexecutorch::core::types; namespace conversions = rnexecutorch::core::conversions; +using rnexecutorch::core::conversions::getOptionalProperty; +using rnexecutorch::core::conversions::getRequiredProperty; + TensorHostObject::TensorHostObject(const std::vector &shape, DType dtype) : dtype_(dtype), shape_(shape), numel_(std::accumulate(shape.begin(), shape.end(), static_cast(1), std::multiplies<>())), size_(numel_ * types::elementSize(dtype)) { - data_ = std::make_unique(size_); // NOLINT(cppcoreguidelines-avoid-c-arrays,modernize-avoid-c-arrays): - // owning runtime-sized byte buffer + // NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays,modernize-avoid-c-arrays): owning runtime-sized byte buffer + data_ = std::make_unique(size_); tensor_ = executorch::extension::from_blob(data_.get(), shape_, types::toScalarType(dtype)); } @@ -61,8 +64,8 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) if (count == 2) { optsObj = conversions::asType(rt, "copyTo: options", args[1]); } - size_t offset = conversions::getOptionalProperty(rt, "copyTo", optsObj, "offset").value_or(0); - size_t length = conversions::getOptionalProperty(rt, "copyTo", optsObj, "length").value_or(self->numel_ - offset); + size_t offset = getOptionalProperty(rt, "copyTo: options", optsObj, "offset").value_or(0); + size_t length = getOptionalProperty(rt, "copyTo: options", optsObj, "length").value_or(self->numel_ - offset); if (offset + length > self->numel_) { throw jsi::JSError(rt, "copyTo: out of bounds offset and length for src tensor"); @@ -76,7 +79,7 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) std::memcpy(dst->data_.get(), self->data_.get() + (offset * elemSize), length * elemSize); - return jsi::Value(rt, args[0].asObject(rt)); + return jsi::Value(rt, args[0]); }; return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "copyTo"), 1, fnBody); } @@ -89,9 +92,9 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) } auto dataObj = conversions::asType(rt, "setData: array", args[0]); - auto buffer = conversions::getRequiredProperty(rt, "setData", dataObj, "buffer"); - size_t byteOffset = conversions::getOptionalProperty(rt, "setData", dataObj, "byteOffset").value_or(0); - size_t byteLength = conversions::getOptionalProperty(rt, "setData", dataObj, "byteLength").value_or(buffer.size(rt)); + auto buffer = getRequiredProperty(rt, "setData: array", dataObj, "buffer"); + size_t byteOffset = getOptionalProperty(rt, "setData: array", dataObj, "byteOffset").value_or(0); + size_t byteLength = getOptionalProperty(rt, "setData: array", dataObj, "byteLength").value_or(buffer.size(rt)); auto lock = tryLockUnique(rt, "setData: self", self); @@ -102,7 +105,7 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) std::memcpy(self->data_.get(), buffer.data(rt) + byteOffset, byteLength); - return jsi::Value(rt, thisVal.asObject(rt)); + return jsi::Value(rt, thisVal); }; return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "setData"), 1, fnBody); } @@ -115,9 +118,9 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) } auto dataObj = conversions::asType(rt, "getData: array", args[0]); - auto buffer = conversions::getRequiredProperty(rt, "getData", dataObj, "buffer"); - size_t byteOffset = conversions::getOptionalProperty(rt, "getData", dataObj, "byteOffset").value_or(0); - size_t byteLength = conversions::getOptionalProperty(rt, "getData", dataObj, "byteLength").value_or(buffer.size(rt)); + auto buffer = getRequiredProperty(rt, "getData: array", dataObj, "buffer"); + size_t byteOffset = getOptionalProperty(rt, "getData: array", dataObj, "byteOffset").value_or(0); + size_t byteLength = getOptionalProperty(rt, "getData: array", dataObj, "byteLength").value_or(buffer.size(rt)); auto lock = tryLockShared(rt, "getData: self", self); @@ -128,7 +131,7 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) std::memcpy(buffer.data(rt) + byteOffset, self->data_.get(), byteLength); - return jsi::Value(rt, args[0].asObject(rt)); + return jsi::Value(rt, args[0]); }; return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "getData"), 1, fnBody); } diff --git a/packages/react-native-executorch/cpp/core/tensor.h b/packages/react-native-executorch/cpp/core/tensor.h index 84fd64c5d2..b679e767d9 100644 --- a/packages/react-native-executorch/cpp/core/tensor.h +++ b/packages/react-native-executorch/cpp/core/tensor.h @@ -1,16 +1,16 @@ #pragma once +#include #include #include -#include #include -#include #include -#include +#include "dtype.h" + #include -#include "dtype.h" +#include namespace rnexecutorch::core::tensor { namespace jsi = facebook::jsi; @@ -26,10 +26,10 @@ namespace types = rnexecutorch::core::types; class TensorHostObject : public jsi::HostObject, public std::enable_shared_from_this { public: - types::DType dtype_; - std::vector shape_; - size_t numel_; - size_t size_; + const types::DType dtype_; + const std::vector shape_; + const size_t numel_; + const size_t size_; // NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays,modernize-avoid-c-arrays): owning runtime-sized byte buffer std::unique_ptr data_; From 05f298b9f507f4c3788edb0945094bf95d10314c Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Fri, 3 Jul 2026 02:18:44 +0200 Subject: [PATCH 19/25] refactor(cpp): implement declarative shape validation in applyColormap and improve fromJs error reporting --- .../cpp/core/tensor_helpers.cpp | 9 ++- .../cpp/core/tensor_helpers.h | 60 +++++++++++++++++-- .../cpp/extensions/cv/image_ops.cpp | 28 ++++----- 3 files changed, 73 insertions(+), 24 deletions(-) diff --git a/packages/react-native-executorch/cpp/core/tensor_helpers.cpp b/packages/react-native-executorch/cpp/core/tensor_helpers.cpp index 27a09b46d8..54c9e23064 100644 --- a/packages/react-native-executorch/cpp/core/tensor_helpers.cpp +++ b/packages/react-native-executorch/cpp/core/tensor_helpers.cpp @@ -1,11 +1,13 @@ #include "tensor_helpers.h" -#include "dtype.h" #include #include +#include "dtype.h" + namespace rnexecutorch::core::tensor { namespace types = rnexecutorch::core::types; +namespace conversions = rnexecutorch::core::conversions; std::shared_lock tryLockShared(jsi::Runtime &rt, const std::string &name, const std::shared_ptr &tensor) { @@ -68,11 +70,12 @@ std::shared_ptr fromJs(jsi::Runtime &rt, const std::string &name, const jsi::Value &value, std::optional expectedDtype, const std::optional &expectedShape) { - if (!value.isObject() || !value.asObject(rt).isHostObject(rt)) { + auto obj = conversions::asType(rt, name, value); + if (!obj.isHostObject(rt)) { throw jsi::JSError(rt, name + " must be a Tensor"); } - auto tensor = value.asObject(rt).getHostObject(rt); + auto tensor = obj.getHostObject(rt); const auto &dtype = tensor->dtype_; const auto &shape = tensor->shape_; diff --git a/packages/react-native-executorch/cpp/core/tensor_helpers.h b/packages/react-native-executorch/cpp/core/tensor_helpers.h index 67444638fa..8634e61c3f 100644 --- a/packages/react-native-executorch/cpp/core/tensor_helpers.h +++ b/packages/react-native-executorch/cpp/core/tensor_helpers.h @@ -8,41 +8,93 @@ #include #include #include +#include #include -#include - #include "conversions.h" #include "dtype.h" #include "tensor.h" +#include + namespace rnexecutorch::core::tensor { namespace jsi = facebook::jsi; using rnexecutorch::core::types::DType; struct RangeDim { - int32_t min; - int32_t max; + int32_t min = 0; + int32_t max = 0; std::optional step; }; using SymbolicShape = std::vector>; +/** + * Tries to acquire a shared (read) lock on the underlying tensor resource. + * Throws a facebook::jsi::JSError if the lock is currently held uniquely by + * another thread or if the tensor has already been disposed. + * + * @param rt The JSI runtime instance. + * @param name The name/context of the tensor for error messages. + * @param tensor Shared pointer to the TensorHostObject. + * @return A shared lock protecting the tensor data. + */ [[nodiscard]] std::shared_lock tryLockShared(jsi::Runtime &rt, const std::string &name, const std::shared_ptr &tensor); +/** + * Tries to acquire a unique (write) lock on the underlying tensor resource. + * Throws a facebook::jsi::JSError if the lock is currently held by another + * thread or if the tensor has already been disposed. + * + * @param rt The JSI runtime instance. + * @param name The name/context of the tensor for error messages. + * @param tensor Shared pointer to the TensorHostObject. + * @return A unique lock protecting the tensor data. + */ [[nodiscard]] std::unique_lock tryLockUnique(jsi::Runtime &rt, const std::string &name, const std::shared_ptr &tensor); +/** + * Validates that two JSI Tensor parameters do not point to the exact same + * underlying TensorHostObject instance, preventing mutation aliasing issues. + * Throws a facebook::jsi::JSError if they are the same tensor. + * + * @param rt The JSI runtime instance. + * @param name1 Context name of the first tensor. + * @param t1 The first tensor. + * @param name2 Context name of the second tensor. + * @param t2 The second tensor. + */ void checkNotSameTensor(jsi::Runtime &rt, const std::string &name1, const std::shared_ptr &t1, const std::string &name2, const std::shared_ptr &t2); +/** + * Extracts, type-checks, and shape-validates a TensorHostObject from a JSI + * Value parameter. Throws a facebook::jsi::JSError with precise details if type + * checking or shape validation fails. + * + * @param rt The JSI runtime instance. + * @param name Parameter name for contextual error messages. + * @param value The JSI value to extract the Tensor from. + * @param expectedDtype Optional expected DType constraint. + * @param expectedShape Optional expected shape (SymbolicShape) constraint. + * @return Shared pointer to the validated TensorHostObject. + */ std::shared_ptr fromJs(jsi::Runtime &rt, const std::string &name, const jsi::Value &value, std::optional expectedDtype, const std::optional &expectedShape); +/** + * @overload + * + * Convenience wrapper that accepts any concrete C++ range as the expected shape + * (e.g. std::vector). + * + * @tparam Range The type of the expected shape container. + */ template requires std::ranges::input_range && std::convertible_to, int32_t> 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 c2676d92f2..5133e3480e 100644 --- a/packages/react-native-executorch/cpp/extensions/cv/image_ops.cpp +++ b/packages/react-native-executorch/cpp/extensions/cv/image_ops.cpp @@ -2,18 +2,19 @@ #include #include -#include +#include +#include #include #include #include -#include - #include "core/dtype.h" #include "core/tensor.h" #include "core/tensor_helpers.h" #include "utils.h" +#include + namespace rnexecutorch::extensions::cv::image_ops { namespace jsi = facebook::jsi; namespace tensor = rnexecutorch::core::tensor; @@ -71,7 +72,7 @@ void install_resize(jsi::Runtime &rt, jsi::Object &module) { auto srcLock = tensor::tryLockShared(rt, "resize: src", src); auto dstLock = tensor::tryLockUnique(rt, "resize: dst", dst); - const auto opts = args[2].asObject(rt); + const auto opts = conversions::asType(rt, "resize: options", args[2]); const auto mode = conversions::getRequiredProperty(rt, "resize: options", opts, "mode"); const auto interp = conversions::getRequiredProperty(rt, "resize: options", opts, "interpolation"); const auto padValue = conversions::getRequiredProperty(rt, "resize: options", opts, "padValue"); @@ -331,17 +332,17 @@ void install_normalize(jsi::Runtime &rt, jsi::Object &module) { auto srcLock = tensor::tryLockShared(rt, "normalize: src", src); auto dstLock = tensor::tryLockUnique(rt, "normalize: dst", dst); - auto opts = args[2].asObject(rt); + auto opts = conversions::asType(rt, "normalize: options", args[2]); const int32_t c = src->shape_[0]; const int32_t h = src->shape_[1]; const int32_t w = src->shape_[2]; auto getNormalizeOption = [&](const char *optName) -> std::vector { - auto val = conversions::getRequiredProperty(rt, "normalize", opts, optName); + auto val = conversions::getRequiredProperty(rt, "normalize: options", opts, optName); std::vector result(static_cast(c)); if (val.isNumber()) { - std::ranges::fill(result, val.asNumber()); + std::ranges::fill(result, conversions::asType(rt, std::format("normalize: options.{}", optName), val)); } else { auto arr = conversions::asVector(rt, std::format("normalize: options.{}", optName), val); if (arr.size() != static_cast(c)) { @@ -388,23 +389,16 @@ void install_applyColormap(jsi::Runtime &rt, jsi::Object &module) { throw jsi::JSError(rt, "Usage: applyColormap(src, dst, colormap)"); } - if (!args[2].isObject() || !args[2].asObject(rt).isArray(rt)) { - throw jsi::JSError(rt, "applyColormap: colormap must be an array"); - } - - auto src = tensor::fromJs(rt, "applyColormap: src", args[0], DType::int32, std::nullopt); - auto dst = tensor::fromJs(rt, "applyColormap: dst", args[1], DType::uint8, std::nullopt); + auto colormapArray = conversions::asType(rt, "applyColormap: colormap", args[2]); + auto src = tensor::fromJs(rt, "applyColormap: src", args[0], DType::int32, tensor::SymbolicShape{"H", "W", 1}); + auto dst = tensor::fromJs(rt, "applyColormap: dst", args[1], DType::uint8, tensor::SymbolicShape{src->shape_[0], src->shape_[1], 4}); tensor::checkNotSameTensor(rt, "applyColormap: src", src, "applyColormap: dst", dst); auto srcLock = tensor::tryLockShared(rt, "applyColormap: src", src); auto dstLock = tensor::tryLockUnique(rt, "applyColormap: dst", dst); constexpr size_t numRgbaChannels = 4; - if (dst->numel_ != src->numel_ * numRgbaChannels) { - throw jsi::JSError(rt, "applyColormap: dst must have exactly 4 times the number of elements as src (RGBA channels)"); - } - auto colormapArray = args[2].asObject(rt).asArray(rt); const size_t numColors = colormapArray.size(rt); std::vector> lut(numColors); for (size_t i = 0; i < numColors; ++i) { From e3edcc03284b558f95d2c614d22fcee5b65808ed Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Fri, 3 Jul 2026 02:18:50 +0200 Subject: [PATCH 20/25] refactor(cpp): model and tokenizer path const correctness, conversions::asType cleanups, and organize includes --- .../react-native-executorch/cpp/core/model.cpp | 12 +++++++----- packages/react-native-executorch/cpp/core/model.h | 2 +- .../cpp/extensions/cv/box_ops.cpp | 14 +++++++------- .../cpp/extensions/nlp/tokenizer.cpp | 6 +++--- .../cpp/extensions/nlp/tokenizer.h | 2 +- 5 files changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/react-native-executorch/cpp/core/model.cpp b/packages/react-native-executorch/cpp/core/model.cpp index f23d07947a..63f5959df9 100644 --- a/packages/react-native-executorch/cpp/core/model.cpp +++ b/packages/react-native-executorch/cpp/core/model.cpp @@ -1,14 +1,16 @@ #include "model.h" -#include "dtype.h" -#include "tensor_helpers.h" #include #include #include -#include #include #include +#include "dtype.h" +#include "tensor_helpers.h" + +#include + #include #include #include @@ -255,8 +257,8 @@ jsi::Value ModelHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { #ifdef EXECUTORCH_ENABLE_EXECUTION_PROFILING auto durationMs = std::chrono::duration_cast(finishTime - startTime).count(); - auto consoleObj = rt.global().getProperty(rt, "console").asObject(rt); - auto logFn = consoleObj.getProperty(rt, "log").asObject(rt).asFunction(rt); + auto consoleObj = conversions::asType(rt, "console", rt.global().getProperty(rt, "console")); + auto logFn = conversions::asType(rt, "console.log", consoleObj.getProperty(rt, "log")); auto info = std::format("Execution of method '{}' took {} ms", methodName, durationMs); logFn.callWithThis(rt, consoleObj, {jsi::String::createFromUtf8(rt, info)}); #endif diff --git a/packages/react-native-executorch/cpp/core/model.h b/packages/react-native-executorch/cpp/core/model.h index af8d23dd61..76e8d1b79d 100644 --- a/packages/react-native-executorch/cpp/core/model.h +++ b/packages/react-native-executorch/cpp/core/model.h @@ -28,7 +28,7 @@ class ModelHostObject : public jsi::HostObject, std::vector getPropertyNames(jsi::Runtime &rt) override; private: - std::string modelPath_; + const std::string modelPath_; std::unique_ptr etModule_; std::mutex mutex_; }; 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 fbc740b9b6..746e4c446e 100644 --- a/packages/react-native-executorch/cpp/extensions/cv/box_ops.cpp +++ b/packages/react-native-executorch/cpp/extensions/cv/box_ops.cpp @@ -11,13 +11,13 @@ #include #include -#include - #include "core/dtype.h" #include "core/tensor.h" #include "core/tensor_helpers.h" #include "utils.h" +#include + namespace rnexecutorch::extensions::cv::box_ops { namespace jsi = facebook::jsi; namespace tensor = rnexecutorch::core::tensor; @@ -88,11 +88,11 @@ void install_nms(jsi::Runtime &rt, jsi::Object &module) { auto boxesLock = tensor::tryLockShared(rt, "nms: boxes", boxes); auto scoresLock = tensor::tryLockShared(rt, "nms: scores", scores); - const auto opts = args[2].asObject(rt); - const auto nmsTypeStr = conversions::getRequiredProperty(rt, "nms", opts, "nmsType"); - const auto boxFormatStr = conversions::getRequiredProperty(rt, "nms", opts, "boxFormat"); - const auto iouThreshold = conversions::getRequiredProperty(rt, "nms", opts, "iouThreshold"); - const auto confidenceThreshold = conversions::getRequiredProperty(rt, "nms", opts, "confidenceThreshold"); + auto opts = conversions::asType(rt, "nms: options", args[2]); + auto nmsTypeStr = conversions::getRequiredProperty(rt, "nms: options", opts, "nmsType"); + auto boxFormatStr = conversions::getRequiredProperty(rt, "nms: options", opts, "boxFormat"); + auto iouThreshold = conversions::getRequiredProperty(rt, "nms: options", opts, "iouThreshold"); + auto confidenceThreshold = conversions::getRequiredProperty(rt, "nms: options", opts, "confidenceThreshold"); NmsType nmsType{}; BoxFormat boxFormat{}; diff --git a/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.cpp b/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.cpp index cd3813c94b..a1ceab4518 100644 --- a/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.cpp +++ b/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.cpp @@ -5,10 +5,10 @@ #include #include -#include - #include "core/conversions.h" +#include + namespace rnexecutorch::extensions::nlp::tokenizer { namespace jsi = facebook::jsi; namespace conversions = rnexecutorch::core::conversions; @@ -245,7 +245,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(arg0)"); + throw jsi::JSError(rt, "loadTokenizer: Usage: loadTokenizer(path)"); } auto tokenizerPath = conversions::asType(rt, "loadTokenizer: path", args[0]); diff --git a/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.h b/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.h index aac5aa2c0c..e317c7f694 100644 --- a/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.h +++ b/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.h @@ -20,7 +20,7 @@ class TokenizerHostObject : public facebook::jsi::HostObject, std::vector getPropertyNames(facebook::jsi::Runtime &rt) override; private: - std::string tokenizerPath_; + const std::string tokenizerPath_; std::unique_ptr tokenizer_; std::mutex mutex_; }; From 463c4e851d39e02c570b5dc2ceba36fa202260e5 Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Fri, 3 Jul 2026 03:00:12 +0200 Subject: [PATCH 21/25] refactor(cpp): add initializer_list overload to fromJs and simplify shape constraints syntax --- .../cpp/core/tensor_helpers.h | 15 +++++++++++++ .../cpp/extensions/cv/box_ops.cpp | 6 ++--- .../cpp/extensions/cv/image_ops.cpp | 22 +++++++++---------- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/packages/react-native-executorch/cpp/core/tensor_helpers.h b/packages/react-native-executorch/cpp/core/tensor_helpers.h index 8634e61c3f..9004e6f262 100644 --- a/packages/react-native-executorch/cpp/core/tensor_helpers.h +++ b/packages/react-native-executorch/cpp/core/tensor_helpers.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -104,4 +105,18 @@ fromJs(jsi::Runtime &rt, const std::string &name, const jsi::Value &value, SymbolicShape convertedShape(expectedShape.begin(), expectedShape.end()); return fromJs(rt, name, value, expectedDtype, std::move(convertedShape)); } + +/** + * @overload + * + * Convenience wrapper that accepts an initializer list of symbolic shape + * elements. Allows passing shape constraints like `{"H", "W", 1}` directly + * without typing SymbolicShape explicitly. + */ +inline std::shared_ptr +fromJs(jsi::Runtime &rt, const std::string &name, const jsi::Value &value, + std::optional expectedDtype, + std::initializer_list> expectedShape) { + return fromJs(rt, name, value, expectedDtype, std::optional(expectedShape)); +} } // namespace rnexecutorch::core::tensor 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 746e4c446e..593c86bbd2 100644 --- a/packages/react-native-executorch/cpp/extensions/cv/box_ops.cpp +++ b/packages/react-native-executorch/cpp/extensions/cv/box_ops.cpp @@ -81,8 +81,8 @@ void install_nms(jsi::Runtime &rt, jsi::Object &module) { throw jsi::JSError(rt, "Usage: nms(boxes, scores, options)"); } - auto boxes = tensor::fromJs(rt, "nms: boxes", args[0], DType::float32, tensor::SymbolicShape{"N", 4}); - auto scores = tensor::fromJs(rt, "nms: scores", args[1], DType::float32, tensor::SymbolicShape{boxes->shape_[0]}); + auto boxes = tensor::fromJs(rt, "nms: boxes", args[0], DType::float32, {"N", 4}); + auto scores = tensor::fromJs(rt, "nms: scores", args[1], DType::float32, {boxes->shape_[0]}); tensor::checkNotSameTensor(rt, "nms: boxes", boxes, "nms: scores", scores); auto boxesLock = tensor::tryLockShared(rt, "nms: boxes", boxes); @@ -212,7 +212,7 @@ void install_restrictToBox(jsi::Runtime &rt, jsi::Object &module) { throw jsi::JSError(rt, "Usage: restrictToBox(src, dst, boxTuple, format)"); } - auto src = tensor::fromJs(rt, "restrictToBox: src", args[0], std::nullopt, tensor::SymbolicShape{"H", "W", "C"}); + auto src = tensor::fromJs(rt, "restrictToBox: src", args[0], std::nullopt, {"H", "W", "C"}); auto dst = tensor::fromJs(rt, "restrictToBox: dst", args[1], src->dtype_, src->shape_); tensor::checkNotSameTensor(rt, "restrictToBox: src", src, "restrictToBox: dst", dst); 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 5133e3480e..25716c576c 100644 --- a/packages/react-native-executorch/cpp/extensions/cv/image_ops.cpp +++ b/packages/react-native-executorch/cpp/extensions/cv/image_ops.cpp @@ -65,8 +65,8 @@ void install_resize(jsi::Runtime &rt, jsi::Object &module) { throw jsi::JSError(rt, "Usage: resize(src, dst, options)"); } - auto src = tensor::fromJs(rt, "resize: src", args[0], std::nullopt, tensor::SymbolicShape{"H", "W", "C"}); - auto dst = tensor::fromJs(rt, "resize: dst", args[1], src->dtype_, tensor::SymbolicShape{"H'", "W'", src->shape_[2]}); + auto src = tensor::fromJs(rt, "resize: src", args[0], std::nullopt, {"H", "W", "C"}); + auto dst = tensor::fromJs(rt, "resize: dst", args[1], src->dtype_, {"H'", "W'", src->shape_[2]}); tensor::checkNotSameTensor(rt, "resize: src", src, "resize: dst", dst); auto srcLock = tensor::tryLockShared(rt, "resize: src", src); @@ -198,8 +198,8 @@ void install_cvtColor(jsi::Runtime &rt, jsi::Object &module) { throw jsi::JSError(rt, "Usage: cvtColor(src, dst, code)"); } - auto src = tensor::fromJs(rt, "cvtColor: src", args[0], std::nullopt, tensor::SymbolicShape{"H", "W", "C"}); - auto dst = tensor::fromJs(rt, "cvtColor: dst", args[1], src->dtype_, tensor::SymbolicShape{src->shape_[0], src->shape_[1], "C'"}); + auto src = tensor::fromJs(rt, "cvtColor: src", args[0], std::nullopt, {"H", "W", "C"}); + auto dst = tensor::fromJs(rt, "cvtColor: dst", args[1], src->dtype_, {src->shape_[0], src->shape_[1], "C'"}); tensor::checkNotSameTensor(rt, "cvtColor: src", src, "cvtColor: dst", dst); auto srcLock = tensor::tryLockShared(rt, "cvtColor: src", src); @@ -241,8 +241,8 @@ void install_toChannelsFirst(jsi::Runtime &rt, jsi::Object &module) { throw jsi::JSError(rt, "Usage: toChannelsFirst(src, dst)"); } - auto src = tensor::fromJs(rt, "toChannelsFirst: src", args[0], std::nullopt, tensor::SymbolicShape{"H", "W", "C"}); - auto dst = tensor::fromJs(rt, "toChannelsFirst: dst", args[1], src->dtype_, tensor::SymbolicShape{src->shape_[2], src->shape_[0], src->shape_[1]}); + auto src = tensor::fromJs(rt, "toChannelsFirst: src", args[0], std::nullopt, {"H", "W", "C"}); + auto dst = tensor::fromJs(rt, "toChannelsFirst: dst", args[1], src->dtype_, {src->shape_[2], src->shape_[0], src->shape_[1]}); tensor::checkNotSameTensor(rt, "toChannelsFirst: src", src, "toChannelsFirst: dst", dst); auto srcLock = tensor::tryLockShared(rt, "toChannelsFirst: src", src); @@ -283,8 +283,8 @@ void install_toChannelsLast(jsi::Runtime &rt, jsi::Object &module) { throw jsi::JSError(rt, "Usage: toChannelsLast(src, dst)"); } - auto src = tensor::fromJs(rt, "toChannelsLast: src", args[0], std::nullopt, tensor::SymbolicShape{"C", "H", "W"}); - auto dst = tensor::fromJs(rt, "toChannelsLast: dst", args[1], src->dtype_, tensor::SymbolicShape{src->shape_[1], src->shape_[2], src->shape_[0]}); + auto src = tensor::fromJs(rt, "toChannelsLast: src", args[0], std::nullopt, {"C", "H", "W"}); + auto dst = tensor::fromJs(rt, "toChannelsLast: dst", args[1], src->dtype_, {src->shape_[1], src->shape_[2], src->shape_[0]}); tensor::checkNotSameTensor(rt, "toChannelsLast: src", src, "toChannelsLast: dst", dst); auto srcLock = tensor::tryLockShared(rt, "toChannelsLast: src", src); @@ -325,7 +325,7 @@ void install_normalize(jsi::Runtime &rt, jsi::Object &module) { throw jsi::JSError(rt, "Usage: normalize(src, dst, options)"); } - auto src = tensor::fromJs(rt, "normalize: src", args[0], std::nullopt, tensor::SymbolicShape{"C", "H", "W"}); + auto src = tensor::fromJs(rt, "normalize: src", args[0], std::nullopt, {"C", "H", "W"}); auto dst = tensor::fromJs(rt, "normalize: dst", args[1], std::nullopt, src->shape_); tensor::checkNotSameTensor(rt, "normalize: src", src, "normalize: dst", dst); @@ -390,8 +390,8 @@ void install_applyColormap(jsi::Runtime &rt, jsi::Object &module) { } auto colormapArray = conversions::asType(rt, "applyColormap: colormap", args[2]); - auto src = tensor::fromJs(rt, "applyColormap: src", args[0], DType::int32, tensor::SymbolicShape{"H", "W", 1}); - auto dst = tensor::fromJs(rt, "applyColormap: dst", args[1], DType::uint8, tensor::SymbolicShape{src->shape_[0], src->shape_[1], 4}); + auto src = tensor::fromJs(rt, "applyColormap: src", args[0], DType::int32, {"H", "W", 1}); + auto dst = tensor::fromJs(rt, "applyColormap: dst", args[1], DType::uint8, {src->shape_[0], src->shape_[1], 4}); tensor::checkNotSameTensor(rt, "applyColormap: src", src, "applyColormap: dst", dst); auto srcLock = tensor::tryLockShared(rt, "applyColormap: src", src); From 45f10971855a3b796abff8136404520c2118e228 Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Fri, 3 Jul 2026 12:17:16 +0200 Subject: [PATCH 22/25] refactor(cpp): remove const from modelPath_ and tokenizerPath_ private fields --- packages/react-native-executorch/cpp/core/model.h | 2 +- packages/react-native-executorch/cpp/extensions/nlp/tokenizer.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-native-executorch/cpp/core/model.h b/packages/react-native-executorch/cpp/core/model.h index 76e8d1b79d..af8d23dd61 100644 --- a/packages/react-native-executorch/cpp/core/model.h +++ b/packages/react-native-executorch/cpp/core/model.h @@ -28,7 +28,7 @@ class ModelHostObject : public jsi::HostObject, std::vector getPropertyNames(jsi::Runtime &rt) override; private: - const std::string modelPath_; + std::string modelPath_; std::unique_ptr etModule_; std::mutex mutex_; }; diff --git a/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.h b/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.h index e317c7f694..aac5aa2c0c 100644 --- a/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.h +++ b/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.h @@ -20,7 +20,7 @@ class TokenizerHostObject : public facebook::jsi::HostObject, std::vector getPropertyNames(facebook::jsi::Runtime &rt) override; private: - const std::string tokenizerPath_; + std::string tokenizerPath_; std::unique_ptr tokenizer_; std::mutex mutex_; }; From 563365e0c89bbdd97a0dfdeb5d94436301e21df2 Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Fri, 3 Jul 2026 12:18:23 +0200 Subject: [PATCH 23/25] refactor(cpp): use auto for static_cast double conversions --- .../cpp/core/conversions.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/react-native-executorch/cpp/core/conversions.cpp b/packages/react-native-executorch/cpp/core/conversions.cpp index 184cd2636a..923b473caa 100644 --- a/packages/react-native-executorch/cpp/core/conversions.cpp +++ b/packages/react-native-executorch/cpp/core/conversions.cpp @@ -4,15 +4,15 @@ namespace rnexecutorch::core::conversions { -constexpr double kMaxInt64Double = static_cast(std::numeric_limits::max()); -constexpr double kMinInt64Double = static_cast(std::numeric_limits::min()); -constexpr double kMaxUint64Double = static_cast(std::numeric_limits::max()); +constexpr auto kMaxInt64Double = static_cast(std::numeric_limits::max()); +constexpr auto kMinInt64Double = static_cast(std::numeric_limits::min()); +constexpr auto kMaxUint64Double = static_cast(std::numeric_limits::max()); -constexpr double kMinInt32Double = static_cast(std::numeric_limits::min()); -constexpr double kMaxInt32Double = static_cast(std::numeric_limits::max()); +constexpr auto kMinInt32Double = static_cast(std::numeric_limits::min()); +constexpr auto kMaxInt32Double = static_cast(std::numeric_limits::max()); -constexpr double kMinUint8Double = static_cast(std::numeric_limits::min()); -constexpr double kMaxUint8Double = static_cast(std::numeric_limits::max()); +constexpr auto kMinUint8Double = static_cast(std::numeric_limits::min()); +constexpr auto kMaxUint8Double = static_cast(std::numeric_limits::max()); template <> double asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) { From 1ba3355cffdb888e4020e14a5bce879e71f03406 Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Fri, 3 Jul 2026 12:20:28 +0200 Subject: [PATCH 24/25] refactor(cpp): use std::format for string construction in conversions and model helpers --- packages/react-native-executorch/cpp/core/conversions.h | 9 +++++---- packages/react-native-executorch/cpp/core/model.cpp | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/react-native-executorch/cpp/core/conversions.h b/packages/react-native-executorch/cpp/core/conversions.h index 13f9c61ece..eb6ac799a6 100644 --- a/packages/react-native-executorch/cpp/core/conversions.h +++ b/packages/react-native-executorch/cpp/core/conversions.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -59,9 +60,9 @@ 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, ctx + ": option '" + propName + "' is required"); + throw jsi::JSError(rt, std::format("{}: option '{}' is required", ctx, propName)); } - return asType(rt, ctx + ": option '" + propName + "'", obj.getProperty(rt, propName.c_str())); + return asType(rt, std::format("{}: option '{}'", ctx, propName), obj.getProperty(rt, propName.c_str())); } /** @@ -85,7 +86,7 @@ std::optional getOptionalProperty(jsi::Runtime &rt, const std::string &ctx, c if (val.isUndefined() || val.isNull()) { return std::nullopt; } - return asType(rt, ctx + ": option '" + propName + "'", val); + return asType(rt, std::format("{}: option '{}'", ctx, propName), val); } /** @@ -106,7 +107,7 @@ std::vector asVector(jsi::Runtime &rt, const std::string &ctx, const jsi::Val const size_t len = arr.size(rt); vec.reserve(len); for (size_t i = 0; i < len; ++i) { - vec.push_back(asType(rt, ctx + "[" + std::to_string(i) + "]", arr.getValueAtIndex(rt, i))); + vec.push_back(asType(rt, std::format("{}[{}]", ctx, i), arr.getValueAtIndex(rt, i))); } return vec; } diff --git a/packages/react-native-executorch/cpp/core/model.cpp b/packages/react-native-executorch/cpp/core/model.cpp index 63f5959df9..9e5ca8d9c7 100644 --- a/packages/react-native-executorch/cpp/core/model.cpp +++ b/packages/react-native-executorch/cpp/core/model.cpp @@ -22,7 +22,7 @@ namespace types = rnexecutorch::core::types; template T unwrap(jsi::Runtime &rt, const std::string &ctx, executorch::runtime::Result result) { if (!result.ok()) { - throw jsi::JSError(rt, ctx + ": " + executorch::runtime::to_string(result.error())); + throw jsi::JSError(rt, std::format("{}: {}", ctx, executorch::runtime::to_string(result.error()))); } return std::move(result.get()); } From d6345d62df7d8c1c708c38396da33771901bedea Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Fri, 3 Jul 2026 12:28:45 +0200 Subject: [PATCH 25/25] fix(core): add bounds checks for offset and length in tensor methods --- .../react-native-executorch/cpp/core/tensor.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/react-native-executorch/cpp/core/tensor.cpp b/packages/react-native-executorch/cpp/core/tensor.cpp index 3248e2f87f..5339752533 100644 --- a/packages/react-native-executorch/cpp/core/tensor.cpp +++ b/packages/react-native-executorch/cpp/core/tensor.cpp @@ -65,10 +65,13 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) optsObj = conversions::asType(rt, "copyTo: options", args[1]); } size_t offset = getOptionalProperty(rt, "copyTo: options", optsObj, "offset").value_or(0); - size_t length = getOptionalProperty(rt, "copyTo: options", optsObj, "length").value_or(self->numel_ - offset); + if (offset > self->numel_) { + throw jsi::JSError(rt, "copyTo: offset is out of bounds for src tensor"); + } - if (offset + length > self->numel_) { - throw jsi::JSError(rt, "copyTo: out of bounds offset and length for src tensor"); + size_t length = getOptionalProperty(rt, "copyTo: options", optsObj, "length").value_or(self->numel_ - offset); + if (length > self->numel_ - offset) { + throw jsi::JSError(rt, "copyTo: length is out of bounds for the given offset of the src tensor"); } const auto elemSize = types::elementSize(self->dtype_); @@ -98,6 +101,10 @@ 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, "setData: Out of bounds offset/length for buffer"); + } + if (byteLength != self->size_) { throw jsi::JSError(rt, std::format("setData: Data size mismatch: TypedArray is {} bytes, but Tensor requires {} bytes.", byteLength, self->size_)); @@ -124,6 +131,10 @@ 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, "getData: Out of bounds offset/length for buffer"); + } + if (byteLength != self->size_) { throw jsi::JSError(rt, std::format("getData: Data size mismatch: TypedArray is {} bytes, but Tensor requires {} bytes.", byteLength, self->size_));