From afb14e527eaa31ddd84414729d747c44423e866c Mon Sep 17 00:00:00 2001 From: Bartosz Hanc Date: Thu, 30 Jul 2026 21:20:38 +0200 Subject: [PATCH] fix(cpp): improve error messages in core and extensions using std::format --- .../cpp/core/conversions.h | 15 +++++++++------ .../react-native-executorch/cpp/core/tensor.cpp | 16 +++++++++++----- .../cpp/extensions/cv/box_ops.cpp | 7 ++++--- .../cpp/extensions/cv/image_ops.cpp | 11 ++++++++--- .../cpp/extensions/math/operations.cpp | 6 ++++-- .../cpp/extensions/nlp/tokenizer.cpp | 6 +++--- .../cpp/extensions/speech/operations.cpp | 11 ++++++++--- 7 files changed, 47 insertions(+), 25 deletions(-) diff --git a/packages/react-native-executorch/cpp/core/conversions.h b/packages/react-native-executorch/cpp/core/conversions.h index d55b995dc7..e42ae08764 100644 --- a/packages/react-native-executorch/cpp/core/conversions.h +++ b/packages/react-native-executorch/cpp/core/conversions.h @@ -138,7 +138,8 @@ std::vector fromJsiTypedArray(jsi::Runtime &rt, const std::string &ctx, const const size_t byteLength = getOptionalProperty(rt, ctx, obj, "byteLength").value_or(buffer.size(rt)); if (byteOffset > buffer.size(rt) || byteLength > buffer.size(rt) - byteOffset) { - throw jsi::JSError(rt, ctx + " has out-of-bounds byteOffset/byteLength for its ArrayBuffer"); + throw jsi::JSError(rt, std::format("{}: out-of-bounds byteOffset ({}) or byteLength ({}) for ArrayBuffer of size {}", + ctx, byteOffset, byteLength, buffer.size(rt))); } if (byteLength % sizeof(T) != 0) { throw jsi::JSError(rt, std::format("{}: byteLength is not a multiple of sizeof(T)={}", ctx, sizeof(T))); @@ -149,6 +150,9 @@ std::vector fromJsiTypedArray(jsi::Runtime &rt, const std::string &ctx, const return vec; } +template +inline constexpr bool kAlwaysFalse = false; + /** * Converts a std::vector of values to a new facebook::jsi::Array. * Handles strings, booleans, and numeric types appropriately. @@ -165,17 +169,16 @@ jsi::Array toJsiArray(jsi::Runtime &rt, const std::vector &vec) { 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]))); + } else if constexpr (std::is_arithmetic_v) { arr.setValueAtIndex(rt, i, jsi::Value(static_cast(vec[i]))); + } else { + static_assert(kAlwaysFalse, "Unsupported vector element type for toJsiArray"); } } return arr; } -template -inline constexpr bool kAlwaysFalse = false; - /** * Maps an arithmetic C++ type to the name of the JS TypedArray constructor whose * elements have the same layout (e.g. int32_t -> "Int32Array"). 64-bit integers diff --git a/packages/react-native-executorch/cpp/core/tensor.cpp b/packages/react-native-executorch/cpp/core/tensor.cpp index 5339752533..b382d5269b 100644 --- a/packages/react-native-executorch/cpp/core/tensor.cpp +++ b/packages/react-native-executorch/cpp/core/tensor.cpp @@ -64,20 +64,24 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) if (count == 2) { optsObj = conversions::asType(rt, "copyTo: options", args[1]); } + size_t offset = 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"); + throw jsi::JSError(rt, std::format("copyTo: offset {} is out of bounds for src tensor of size {} elements", + offset, self->numel_)); } size_t length = getOptionalProperty(rt, "copyTo: options", optsObj, "length").value_or(self->numel_ - offset); if (length > self->numel_ - offset) { - throw jsi::JSError(rt, "copyTo: length is out of bounds for the given offset of the src tensor"); + throw jsi::JSError(rt, std::format("copyTo: length {} is out of bounds for offset {} of src tensor (numel {})", + length, offset, self->numel_)); } const auto elemSize = types::elementSize(self->dtype_); if (length * elemSize != dst->size_) { - throw jsi::JSError(rt, "copyTo: size mismatch between copy byte size and dst tensor size"); + throw jsi::JSError(rt, std::format("copyTo: size mismatch between copy size ({} bytes) and dst tensor size ({} bytes)", + length * elemSize, dst->size_)); } std::memcpy(dst->data_.get(), self->data_.get() + (offset * elemSize), length * elemSize); @@ -102,7 +106,8 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) auto lock = tryLockUnique(rt, "setData: self", self); if (byteOffset > buffer.size(rt) || byteLength > buffer.size(rt) - byteOffset) { - throw jsi::JSError(rt, "setData: Out of bounds offset/length for buffer"); + throw jsi::JSError(rt, std::format("setData: Out of bounds offset ({}) or length ({}) for buffer of size {}", + byteOffset, byteLength, buffer.size(rt))); } if (byteLength != self->size_) { @@ -132,7 +137,8 @@ jsi::Value TensorHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) auto lock = tryLockShared(rt, "getData: self", self); if (byteOffset > buffer.size(rt) || byteLength > buffer.size(rt) - byteOffset) { - throw jsi::JSError(rt, "getData: Out of bounds offset/length for buffer"); + throw jsi::JSError(rt, std::format("getData: Out of bounds offset ({}) or length ({}) for buffer of size {}", + byteOffset, byteLength, buffer.size(rt))); } if (byteLength != self->size_) { 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 1c6a624db8..674fba7088 100644 --- a/packages/react-native-executorch/cpp/extensions/cv/box_ops.cpp +++ b/packages/react-native-executorch/cpp/extensions/cv/box_ops.cpp @@ -44,7 +44,7 @@ BoxFormat parseBoxFormat(const std::string &s) { if (s == "cxcywh") { return BoxFormat::CXCYWH; } - throw std::invalid_argument("unsupported boxFormat '" + s + "'"); + throw std::invalid_argument(std::format("unsupported boxFormat '{}'. Expected 'xyxy', 'xywh', or 'cxcywh'", s)); } enum class NmsType { @@ -59,7 +59,7 @@ NmsType parseNmsType(const std::string &s) { if (s == "weighted") { return NmsType::Weighted; } - throw std::invalid_argument("unsupported nmsType '" + s + "'"); + throw std::invalid_argument(std::format("unsupported nmsType '{}'. Expected 'standard' or 'weighted'", s)); } constexpr size_t kBoxCoords = 4; @@ -223,7 +223,8 @@ void install_restrictToBox(jsi::Runtime &rt, jsi::Object &module) { auto boxVec = conversions::asVector(rt, "restrictToBox: boxTuple", args[2]); if (boxVec.size() != kBoxCoords) { - throw jsi::JSError(rt, "restrictToBox: boxTuple must contain exactly 4 coordinates"); + throw jsi::JSError(rt, std::format("restrictToBox: boxTuple must contain exactly 4 coordinates (got {})", + boxVec.size())); } auto boxFormatStr = conversions::asType(rt, "restrictToBox: format", args[3]); 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 151050475a..b9bad0be55 100644 --- a/packages/react-native-executorch/cpp/extensions/cv/image_ops.cpp +++ b/packages/react-native-executorch/cpp/extensions/cv/image_ops.cpp @@ -41,7 +41,9 @@ int interpToFlag(const std::string &interp) { if (interp == "lanczos") { return ::cv::INTER_LANCZOS4; } - throw std::invalid_argument("unsupported interpolation '" + interp + "'"); + throw std::invalid_argument(std::format("unsupported interpolation '{}'. Expected" + " 'nearest', 'area', 'linear', 'cubic', or 'lanczos'", + interp)); } struct FitBox { @@ -189,7 +191,9 @@ int codeToColorConversionFlag(const std::string &code) { if (code == "GRAY2BGRA") { return ::cv::COLOR_GRAY2BGRA; } - throw std::invalid_argument("cvtColor: unsupported color conversion code '" + code + "'"); + throw std::invalid_argument(std::format("cvtColor: unsupported color conversion code '{}'." + " Common values are 'RGB2BGR', 'BGR2RGB', 'RGBA2RGB', 'RGB2GRAY', etc.", + code)); } } // namespace @@ -414,7 +418,8 @@ void install_applyColormap(jsi::Runtime &rt, jsi::Object &module) { for (size_t i = 0; i < numColors; ++i) { auto colorVec = conversions::asVector(rt, "applyColormap: colormap entry", colormapArray.getValueAtIndex(rt, i)); if (colorVec.size() != numRgbaChannels) { - throw jsi::JSError(rt, "applyColormap: colormap entry must be an RGBA color array of size 4"); + throw jsi::JSError(rt, std::format("applyColormap: colormap entry must be an RGBA color array of size 4 (got size {})", + colorVec.size())); } for (size_t c = 0; c < numRgbaChannels; ++c) { lut[i][c] = colorVec[c]; diff --git a/packages/react-native-executorch/cpp/extensions/math/operations.cpp b/packages/react-native-executorch/cpp/extensions/math/operations.cpp index 76fb2f8e40..0ecb9caa28 100644 --- a/packages/react-native-executorch/cpp/extensions/math/operations.cpp +++ b/packages/react-native-executorch/cpp/extensions/math/operations.cpp @@ -73,7 +73,8 @@ void install_softmax(jsi::Runtime &rt, jsi::Object &module) { axis += rank; } if (axis < 0 || axis >= rank) { - throw jsi::JSError(rt, "softmax: axis is out of range"); + throw jsi::JSError(rt, std::format("softmax: axis {} out of range for tensor of rank {}", + axis, rank)); } const auto axisIdx = static_cast(axis); @@ -152,7 +153,8 @@ void install_argmax(jsi::Runtime &rt, jsi::Object &module) { axis += rank; } if (axis < 0 || axis >= rank) { - throw jsi::JSError(rt, "argmax: axis is out of range"); + throw jsi::JSError(rt, std::format("argmax: axis {} out of range for tensor of rank {}", + axis, rank)); } const auto axisIdx = static_cast(axis); diff --git a/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.cpp b/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.cpp index fa036e9b01..2469960a9b 100644 --- a/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.cpp +++ b/packages/react-native-executorch/cpp/extensions/nlp/tokenizer.cpp @@ -62,13 +62,13 @@ TokenizerHostObject::TokenizerHostObject(std::string tokenizerPath) tokenizer_(std::make_unique()) { auto error = tokenizer_->load(tokenizerPath_); if (error != tokenizers::Error::Ok) { - throw std::runtime_error("Failed to load tokenizer from '" + tokenizerPath_ + - "': " + toString(error)); + throw std::runtime_error(std::format("Failed to load tokenizer from '{}': {}", + tokenizerPath_, toString(error))); } } std::unique_lock TokenizerHostObject::tryLockUnique(jsi::Runtime &rt, - std::string_view context) { + std::string_view context) { std::unique_lock lock(mutex_, std::try_to_lock); if (!lock.owns_lock()) { throw jsi::JSError(rt, std::format("{} is currently in use", context)); diff --git a/packages/react-native-executorch/cpp/extensions/speech/operations.cpp b/packages/react-native-executorch/cpp/extensions/speech/operations.cpp index 516b0c5046..7d8eb61359 100644 --- a/packages/react-native-executorch/cpp/extensions/speech/operations.cpp +++ b/packages/react-native-executorch/cpp/extensions/speech/operations.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "core/tensor.h" @@ -41,16 +42,20 @@ void install_extractFrames(jsi::Runtime &rt, jsi::Object &module) { const auto chunkFrames = static_cast(dst->shape_[0]); const auto fftLength = static_cast(dst->shape_[1]); if (frameLength > fftLength) { - throw jsi::JSError(rt, "extractFrames: hann length exceeds dst fftLength"); + throw jsi::JSError(rt, std::format("extractFrames: hann length ({}) exceeds dst fftLength ({})", + frameLength, fftLength)); } if (numFrames > chunkFrames) { - throw jsi::JSError(rt, "extractFrames: numFrames out of dst frame capacity"); + throw jsi::JSError(rt, std::format("extractFrames: numFrames ({}) exceeds dst frame capacity ({})", + numFrames, chunkFrames)); } if (numFrames > 0) { const uint64_t lastSample = (numFrames - 1) * hopLength + frameLength - 1; if (lastSample >= waveform->numel_) { - throw jsi::JSError(rt, "extractFrames: frame window out of waveform bounds"); + throw jsi::JSError(rt, std::format("extractFrames: frame window (last sample index {})" + " exceeds waveform bounds (numel {})", + lastSample, waveform->numel_)); } }