[RNE Rewrite] feat: add input validation and JSI conversion utilities#1296
[RNE Rewrite] feat: add input validation and JSI conversion utilities#1296barhanc wants to merge 3 commits into
Conversation
|
@msluszniak This is still a draft and there are still quite some rough edges to polish, but could you take a look if this approach is something you would agree on and if there are any other aspects I missed. I was thinking that we could also use this PR for #1268 since adding the validation/conversion helpers modifies quite a lot of core code so it would be good to review and streamline also the other aspects. |
msluszniak
left a comment
There was a problem hiding this comment.
I like this concept. I combines elegance of short macros with flexibility of manual code. Just very small nits, will dive into it later today / tomorrow. I like usage of std::format, we previously tried use it in old codebase, but because it required some version of iOS we eventually dropped it, but I don't think it will be a case now.
| template <> | ||
| uint64_t asType<uint64_t>(jsi::Runtime &rt, const std::string &ctx, const jsi::Value &val) { | ||
| double v = asType<double>(rt, ctx, val); | ||
| if (std::isnan(v) || v < 0.0) { | ||
| throw jsi::JSError(rt, ctx + " must be a non-negative integer"); | ||
| } | ||
| return static_cast<uint64_t>(v); | ||
| } |
There was a problem hiding this comment.
there should also be check if double is in range of uint64_t, otherwise cast is undefined behaviour. This is the case for all analogical casts scenarios.
| void checkNotSameTensor(jsi::Runtime &rt, | ||
| const std::string &name1, const std::shared_ptr<TensorHostObject> &t1, | ||
| const std::string &name2, const std::shared_ptr<TensorHostObject> &t2) { | ||
| if (t1.get() == t2.get()) { |
| if (i > 0) { | ||
| shapeStr += ", "; | ||
| } | ||
| const auto &dim = expectedShape->at(i); | ||
| if (std::holds_alternative<int32_t>(dim)) { | ||
| shapeStr += std::to_string(std::get<int32_t>(dim)); | ||
| } else { | ||
| shapeStr += std::get<std::string>(dim); | ||
| } | ||
| } | ||
| shapeStr += "]"; |
There was a problem hiding this comment.
| if (i > 0) { | |
| shapeStr += ", "; | |
| } | |
| const auto &dim = expectedShape->at(i); | |
| if (std::holds_alternative<int32_t>(dim)) { | |
| shapeStr += std::to_string(std::get<int32_t>(dim)); | |
| } else { | |
| shapeStr += std::get<std::string>(dim); | |
| } | |
| } | |
| shapeStr += "]"; | |
| const auto &dim = expectedShape->at(i); | |
| if (std::holds_alternative<int32_t>(dim)) { | |
| shapeStr += std::to_string(std::get<int32_t>(dim)); | |
| } else { | |
| shapeStr += std::get<std::string>(dim); | |
| } | |
| shapeStr += ", "; | |
| } | |
| shapeStr.pop_back(); | |
| shapeStr += "]"; |
| throw jsi::JSError(rt, std::format("{}: Failed to get input tag: {}", ctx, errorMsg)); | ||
| } | ||
|
|
||
| switch (tag.get()) { |
There was a problem hiding this comment.
This input switch dropped the old Tag::None case (accept null → EValue()), so None-tagged/optional inputs now hit default: and throw "Unsupported input type". The output switch below still handles None. Was dropping it intentional?
| copyLen = static_cast<size_t>(optsObj.getProperty(rt, "length").asNumber()); | ||
| } | ||
| } | ||
| const jsi::Object optsObj = (count == 2) ? args[1].asObject(rt) : jsi::Object(rt); |
There was a problem hiding this comment.
If count == 2 but args[1] isn't an object, asObject(rt) throws a generic JSI error; the old code silently ignored a non-object opts arg. Minor, but worth a contextual check to match the rest of the PR.
| byteLength = static_cast<size_t>(byteLengthValue.asNumber()); | ||
| } | ||
| const auto dataObj = args[0].asObject(rt); | ||
| const auto buffer = dataObj.getProperty(rt, "buffer").asObject(rt).getArrayBuffer(rt); |
There was a problem hiding this comment.
getData/setData lost the old "Expected a TypedArray with a 'buffer' property" message — a non-TypedArray now yields a generic JSI error. Slightly ironic for a PR standardizing errors; consider an asTypedArray helper in conversions.
| throw jsi::JSError(rt, "through: First argument must be a function"); | ||
| } | ||
|
|
||
| auto fn = args[0].asObject(rt).asFunction(rt); |
There was a problem hiding this comment.
through/throughIf lost the explicit "must be a function" guard; a non-function arg now throws a generic asFunction error instead of the contextual one.
| } | ||
|
|
||
| if (expectedShape) { | ||
| std::string shapeStr = "["; |
There was a problem hiding this comment.
shapeStr is built unconditionally on every call, including the success path — an avoidable allocation for per-frame CV ops. Move it into the error branches (or a lazy lambda).
| } else { | ||
| const auto &symbol = std::get<std::string>(dim); | ||
| if (symbolMap.contains(symbol) && shape[i] != symbolMap[symbol]) { | ||
| throw jsi::JSError(rt, std::format("{} must have shape {} (dim '{}' mismatch: expected {}, got {})", |
There was a problem hiding this comment.
The message says dim '{}' but is fed i, so it prints the index (dim '2') rather than the symbol name (dim 'N'). Pass symbol instead of i.
Description
Refactors the C++ layer by pulling two families of scattered, copy-pasted utilities out of individual translation units and into purpose-built, reusable modules:
cpp/core/conversions.{h,cpp}JSI -> C++ type coercionscpp/core/tensor_helpers.{h,cpp}: tensor acquisition, locking, and shape/dtype validationMotivation
The old code had two recurring problems that became impossible to ignore as the extension surface grew:
JSI coercion was inlined everywhere
Every file that needed to read a number, string, or array out of a
jsi::Valuehad its own copy of the same defensive checks, cast chains, and error-message construction. A bug or policy change (e.g. hownullis treated, or which format an error message takes) had to be hunted down and patched in every translation unit independently.Tensor validation was duplicated across all extension operations
Every operation in
box_ops,image_ops,operations, andtokenizerthat accepted aTensorargument re-implemented the same sequence: unwrap thejsi::Value, cast toTensorHostObject, acquire the appropriateshared/exclusive lock, and assert dtype + shape constraints. This led to subtle inconsistencies in error messages and locking strategies between operations.
Introduces a breaking change?
Type of change
Tested on
Testing instructions
Screenshots
Related issues
Closes #1270
Checklist
Additional notes
Since this PR changes quite a lot of stuff in
core/it can also be used for #1268