Skip to content

[RNE Rewrite] feat: add input validation and JSI conversion utilities#1296

Draft
barhanc wants to merge 3 commits into
rne-rewritefrom
@bh/issue-1270
Draft

[RNE Rewrite] feat: add input validation and JSI conversion utilities#1296
barhanc wants to merge 3 commits into
rne-rewritefrom
@bh/issue-1270

Conversation

@barhanc

@barhanc barhanc commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

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 coercions
  • cpp/core/tensor_helpers.{h,cpp}: tensor acquisition, locking, and shape/dtype validation

Motivation

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::Value had its own copy of the same defensive checks, cast chains, and error-message construction. A bug or policy change (e.g. how null is 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, and tokenizer that accepted a Tensor argument re-implemented the same sequence: unwrap the jsi::Value, cast to TensorHostObject, acquire the appropriate
shared/exclusive lock, and assert dtype + shape constraints. This led to subtle inconsistencies in error messages and locking strategies between operations.

Introduces a breaking change?

  • Yes
  • No

Type of change

  • Bug fix (change which fixes an issue)
  • New feature (change which adds functionality)
  • Documentation update (improves or adds clarity to existing documentation)
  • Other (chores, tests, code style improvements etc.)

Tested on

  • iOS
  • Android

Testing instructions

  • Build all example apps on both Android and iOS and test that nothing is broken.

Screenshots

Related issues

Closes #1270

Checklist

  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have updated the documentation accordingly
  • My changes generate no new warnings

Additional notes

Since this PR changes quite a lot of stuff in core/ it can also be used for #1268

@barhanc barhanc self-assigned this Jul 1, 2026
@barhanc

barhanc commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

@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.

@barhanc barhanc requested a review from msluszniak July 1, 2026 15:09

@msluszniak msluszniak left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +29 to +36
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);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not t1 == t2?

Comment on lines +62 to +72
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 += "]";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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 += "]";

@msluszniak msluszniak left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another batch of comments.

throw jsi::JSError(rt, std::format("{}: Failed to get input tag: {}", ctx, errorMsg));
}

switch (tag.get()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This input switch dropped the old Tag::None case (accept nullEValue()), 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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 = "[";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {})",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[RNE Rewrite] Add macros for input validation in native C++ implementation

2 participants