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..923b473caa --- /dev/null +++ b/packages/react-native-executorch/cpp/core/conversions.cpp @@ -0,0 +1,119 @@ +#include "conversions.h" +#include +#include + +namespace rnexecutorch::core::conversions { + +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 auto kMinInt32Double = static_cast(std::numeric_limits::min()); +constexpr auto kMaxInt32Double = 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) { + 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) { + 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) { + 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) || 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); +} + +template <> +uint8_t asType(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) { + double v = asType(rt, ctx, val); + if (std::isnan(v) || std::isinf(v) || v != std::trunc(v) || v < kMinUint8Double || v > kMaxUint8Double) { + throw jsi::JSError(rt, ctx + " must be an integer between 0 and 255"); + } + 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); +} + +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); +} + +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/conversions.h b/packages/react-native-executorch/cpp/core/conversions.h new file mode 100644 index 0000000000..eb6ac799a6 --- /dev/null +++ b/packages/react-native-executorch/cpp/core/conversions.h @@ -0,0 +1,139 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include + +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) = 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())) { + 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())); +} + +/** + * 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())) { + 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); +} + +/** + * 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) { + auto arr = asType(rt, ctx, val); + 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; +} + +/** + * 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()); + 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/model.cpp b/packages/react-native-executorch/cpp/core/model.cpp index ba50d9d12b..9e5ca8d9c7 100644 --- a/packages/react-native-executorch/cpp/core/model.cpp +++ b/packages/react-native-executorch/cpp/core/model.cpp @@ -1,21 +1,78 @@ #include "model.h" -#include "dtype.h" -#include "tensor.h" #include +#include +#include #include +#include + +#include "dtype.h" +#include "tensor_helpers.h" + +#include #include #include #include +namespace { +namespace jsi = facebook::jsi; +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, std::format("{}: {}", ctx, executorch::runtime::to_string(result.error()))); + } + return std::move(result.get()); +} + +types::DType +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, ctx + ": Unsupported tensor dtype: " + 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; -using TensorHostObject = rnexecutorch::core::tensor::TensorHostObject; +namespace conversions = rnexecutorch::core::conversions; + +using 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) { @@ -41,15 +98,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 = unwrap(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; } @@ -62,14 +115,12 @@ 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)"); } - if (!args[0].isString()) { - throw jsi::JSError(rt, "getMethodMeta: Expected arg0 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"); @@ -79,90 +130,48 @@ 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 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); - } + auto methodName = conversions::asType(rt, "getMethodMeta: methodName", args[0]); + 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 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); - } - 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 ctx = std::format("getMethodMeta: 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 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); - } - 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 ctx = std::format("getMethodMeta: 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 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); - } - usesBackendMap.setProperty(rt, backendName.get(), methodMeta->uses_backend(backendName.get())); + for (size_t i = 0; i < methodMeta.num_backends(); ++i) { + auto ctx = std::format("getMethodMeta: backend name [{}]", i); + const auto *backendName = unwrap(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 { - const std::string dtypeStr = rnexecutorch::core::types::toString(rnexecutorch::core::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 = 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); - } - 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 ctx = std::format("getMethodMeta: 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 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); - } - 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 ctx = std::format("getMethodMeta: output tensor meta [{}]", i); + auto tensorMeta = unwrap(rt, ctx, 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); @@ -181,18 +190,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,227 +199,122 @@ 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 methodMeta = self->etModule_->method_meta(methodName); - auto inputsArray = args[1].asObject(rt).asArray(rt); + auto methodName = conversions::asType(rt, "execute: methodName", args[0]); + auto methodMeta = unwrap(rt, "execute", self->etModule_->method_meta(methodName)); - 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); - } + 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()) { - 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); + 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())); } - 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, "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())); - } - - 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])); - } - } - }; - - 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 ctx = std::format("execute: inputs[{}]", i); + auto tag = unwrap(rt, ctx, methodMeta.input_tag(i)); auto val = inputsArray.getValueAtIndex(rt, i); - 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); - } - - 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; - } + switch (tag) { 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 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) { - 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: inputs[" + std::to_string(i) + - "] is currently in use"); + throw jsi::JSError(rt, "execute: Tensor aliasing detected. " + "The same tensor was passed multiple times."); } - - 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); - } - - validateTensor(rt, tensorHostObject.get(), tensorMeta, "inputs[" + std::to_string(i) + "]"); - + tensorLocks.emplace_back(tensor::tryLockUnique(rt, ctx, tensorHostObject)); 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] = 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] = 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] = conversions::asType(rt, ctx, val); break; - } - default: { - throw jsi::JSError(rt, "execute: Unsupported input type for inputs[" + std::to_string(i) + "]"); - } + case executorch::runtime::Tag::None: + inputs[i] = executorch::runtime::EValue(); + break; + default: + throw jsi::JSError(rt, std::format("{}: Unsupported input type: {}", + ctx, executorch::runtime::tag_to_string(tag))); } } auto startTime = std::chrono::high_resolution_clock::now(); - auto result = self->etModule_->execute(methodName, inputs); + 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 = "Execution of method '" + methodName + "' took " + std::to_string(durationMs) + " ms"; + 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 - 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."); - } - - auto outputTensorsArray = args[2].asObject(rt).asArray(rt); - auto jsOutputArray = jsi::Array(rt, result->size()); + auto result = unwrap(rt, std::format("execute: Method '{}' failed (check getMethodMeta() " + "for required backends and getRegisteredBackends() " + "for registered ones)", + methodName), + std::move(executeResult)); + 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()); - break; - } case executorch::runtime::Tag::Tensor: { if (tensorOutputIdx >= outputTensorsArray.size(rt)) { throw jsi::JSError(rt, "execute: Not enough tensor output placeholders in outputTensors"); } + auto ctx = std::format("execute: outputTensors[{}]", tensorOutputIdx); 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"); - } + 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) { - 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(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"); - } - - 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); - } - - validateTensor(rt, tensorHostObject.get(), tensorMeta, "outputTensors[" + std::to_string(tensorOutputIdx) + "]"); - + tensorLocks.emplace_back(tensor::tryLockUnique(rt, ctx, tensorHostObject)); std::memcpy(tensorHostObject->data_.get(), 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; - } - default: { - throw jsi::JSError(rt, "execute: Unsupported return type"); - } + case executorch::runtime::Tag::None: + jsOutputArray.setValueAtIndex(rt, index, jsi::Value::null()); + break; + default: + throw jsi::JSError(rt, std::format("execute: Unsupported return type: {}", + executorch::runtime::tag_to_string(output.tag))); } ++index; @@ -470,23 +362,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)"); - } - - if (!args[0].isString()) { - throw jsi::JSError(rt, "loadModel: Expected arg0 to be a string"); + throw jsi::JSError(rt, "loadModel: Usage: loadModel(path)"); } - 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); + auto modelPath = conversions::asType(rt, "loadModel: path", args[0]); + try { + return jsi::Object::createFromHostObject(rt, std::make_shared(modelPath)); + } catch (const std::exception &e) { + throw jsi::JSError(rt, std::format("loadModel: {}", e.what())); } - - return jsi::Object::createFromHostObject(rt, 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..5339752533 100644 --- a/packages/react-native-executorch/cpp/core/tensor.cpp +++ b/packages/react-native-executorch/cpp/core/tensor.cpp @@ -1,35 +1,46 @@ #include "tensor.h" + +#include +#include +#include #include +#include #include +#include +#include + +#include "core/conversions.h" +#include "dtype.h" +#include "tensor_helpers.h" + +#include namespace rnexecutorch::core::tensor { -namespace jsi = facebook::jsi; +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, 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; + numel_(std::accumulate(shape.begin(), shape.end(), static_cast(1), std::multiplies<>())), + size_(numel_ * types::elementSize(dtype)) { // 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)); + 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,61 +54,35 @@ 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 = tensor::fromJs(rt, "copyTo: dst", args[0], std::nullopt, std::nullopt); - auto dst = args[0].asObject(rt).getHostObject(rt); + checkNotSameTensor(rt, "copyTo: self", self, "copyTo: dst", dst); + auto srcLock = tryLockShared(rt, "copyTo: self", self); + auto dstLock = tryLockUnique(rt, "copyTo: dst", dst); - if (self.get() == dst.get()) { - throw jsi::JSError(rt, "copyTo: In-place operations (src == dst) are not supported."); + jsi::Object optsObj(rt); + if (count == 2) { + optsObj = conversions::asType(rt, "copyTo: options", args[1]); } - - 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"); + size_t offset = getOptionalProperty(rt, "copyTo: options", optsObj, "offset").value_or(0); + if (offset > self->numel_) { + throw jsi::JSError(rt, "copyTo: offset is out of bounds for src tensor"); } - if (!dst->data_) { - throw jsi::JSError(rt, "copyTo: dst tensor has been disposed"); + 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"); } - 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()); - } + const auto elemSize = types::elementSize(self->dtype_); - copyLen -= srcOffset; - - if (optsObj.hasProperty(rt, "length")) { - copyLen = static_cast(optsObj.getProperty(rt, "length").asNumber()); - } - } - - 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_); - 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)); + return jsi::Value(rt, args[0]); }; return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "copyTo"), 1, fnBody); } @@ -109,54 +94,25 @@ 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()); - } + auto dataObj = conversions::asType(rt, "setData: array", args[0]); + 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)); - 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"); - } + auto lock = tryLockUnique(rt, "setData: self", self); - if (!self->data_) { - throw jsi::JSError(rt, "setData: Tensor has been disposed"); + 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_) { - 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); - return jsi::Value(rt, thisVal.asObject(rt)); + return jsi::Value(rt, thisVal); }; return jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, "setData"), 1, fnBody); } @@ -168,54 +124,25 @@ 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 = conversions::asType(rt, "getData: array", args[0]); + 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)); - 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); + auto lock = tryLockShared(rt, "getData: self", self); - 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"); - } - - if (!self->data_) { - throw jsi::JSError(rt, "getData: Tensor has been disposed"); + 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_) { - 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); - 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); } @@ -227,11 +154,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); @@ -253,16 +176,12 @@ 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); + auto fn = conversions::asType(rt, "throughIf: fn", args[1]); std::vector fnArgs; fnArgs.reserve(count - 1); @@ -301,8 +220,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 +241,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..b679e767d9 100644 --- a/packages/react-native-executorch/cpp/core/tensor.h +++ b/packages/react-native-executorch/cpp/core/tensor.h @@ -1,19 +1,21 @@ #pragma once +#include #include #include -#include #include #include +#include "dtype.h" + #include -#include -#include - -#include "dtype.h" +#include 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_; - std::vector shape_; - size_t numel_; + const types::DType dtype_; + const std::vector shape_; + const size_t numel_; + const size_t size_; - 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/core/tensor_helpers.cpp b/packages/react-native-executorch/cpp/core/tensor_helpers.cpp new file mode 100644 index 0000000000..54c9e23064 --- /dev/null +++ b/packages/react-native-executorch/cpp/core/tensor_helpers.cpp @@ -0,0 +1,132 @@ +#include "tensor_helpers.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) { + 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 == t2) { + throw jsi::JSError(rt, std::format("{} and {} cannot be the same tensor", name1, name2)); + } +} + +namespace { +std::string shapeToString(const SymbolicShape &shape) { + std::string s; + for (const auto &dim : shape) { + if (std::holds_alternative(dim)) { + s += std::to_string(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) : ""); + } + s += ", "; + } + if (!shape.empty()) { + s.pop_back(); + s.pop_back(); + } + return "[" + s + "]"; +} +} // namespace + +std::shared_ptr +fromJs(jsi::Runtime &rt, const std::string &name, const jsi::Value &value, + std::optional expectedDtype, const std::optional &expectedShape) { + + auto obj = conversions::asType(rt, name, value); + if (!obj.isHostObject(rt)) { + throw jsi::JSError(rt, name + " must be a Tensor"); + } + + auto tensor = obj.getHostObject(rt); + 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) { + return tensor; + } + + 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 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 new file mode 100644 index 0000000000..9004e6f262 --- /dev/null +++ b/packages/react-native-executorch/cpp/core/tensor_helpers.h @@ -0,0 +1,122 @@ +#pragma once + +#include +#include +#include +#include +#include +#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 = 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> +inline std::shared_ptr +fromJs(jsi::Runtime &rt, const std::string &name, const jsi::Value &value, + std::optional expectedDtype, const Range &expectedShape) { + 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 501b9245c1..593c86bbd2 100644 --- a/packages/react-native-executorch/cpp/extensions/cv/box_ops.cpp +++ b/packages/react-native-executorch/cpp/extensions/cv/box_ops.cpp @@ -3,21 +3,27 @@ #include #include #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::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, {"N", 4}); + auto scores = tensor::fromJs(rt, "nms: scores", args[1], DType::float32, {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); + 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{}; @@ -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())); + throw jsi::JSError(rt, std::format("nms: {}", 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"); - } - - 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"); - } + 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_); - 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"); - } + 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); - 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); - - 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..25716c576c 100644 --- a/packages/react-native-executorch/cpp/extensions/cv/image_ops.cpp +++ b/packages/react-native-executorch/cpp/extensions/cv/image_ops.cpp @@ -2,19 +2,25 @@ #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; -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 +65,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"); - } + 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]}); - 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"); - } + 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); - 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"); - } - - 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 = 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"); const int32_t srcH = src->shape_[0]; const int32_t srcW = src->shape_[1]; @@ -142,29 +88,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 +198,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"); - } + 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'"}); - 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 = 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 +224,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 +241,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, {"H", "W", "C"}); + auto dst = tensor::fromJs(rt, "toChannelsFirst: dst", args[1], src->dtype_, {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 +283,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"); - } - - if (!args[1].isObject() || !args[1].asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "toChannelsLast: dst must be a Tensor"); - } + 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]}); - 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 +325,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"); - } + 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_); - if (!args[1].isObject() || !args[1].asObject(rt).isHostObject(rt)) { - throw jsi::JSError(rt, "normalize: dst must be a Tensor"); - } + 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 (!args[2].isObject()) { - throw jsi::JSError(rt, "normalize: options must be an object"); - } + auto opts = conversions::asType(rt, "normalize: options", args[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, "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: options", 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(); - } + std::ranges::fill(result, conversions::asType(rt, std::format("normalize: options.{}", optName), val)); } 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 +389,28 @@ 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"); - } - - auto src = args[0].asObject(rt).getHostObject(rt); - auto dst = args[1].asObject(rt).getHostObject(rt); - constexpr size_t numRgbaChannels = 4; + auto colormapArray = conversions::asType(rt, "applyColormap: colormap", args[2]); + 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}); - 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"); - } - 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)"); - } + 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); - if (!args[2].isObject() || !args[2].asObject(rt).isArray(rt)) { - throw jsi::JSError(rt, "applyColormap: colormap must be an array"); - } + constexpr size_t numRgbaChannels = 4; - 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..a1ceab4518 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 "core/conversions.h" + #include 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. @@ -69,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"); @@ -82,20 +82,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 +100,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 +115,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 +123,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()); @@ -184,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"); @@ -197,11 +168,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 +186,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 +195,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()); @@ -280,19 +245,15 @@ 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)"); - } - - if (!args[0].isString()) { - throw jsi::JSError(rt, "loadTokenizer: Expected arg0 to be a string"); + throw jsi::JSError(rt, "loadTokenizer: Usage: loadTokenizer(path)"); } - 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);