From 89eb095ec2ee7e0707814864660ac64a3851e377 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Wed, 1 Jul 2026 18:44:31 -0400 Subject: [PATCH] fix: prevent native crash when a transformer worklet throws RunTransformer called the transformer through runSync and used the result unconditionally. In release builds runSync is a raw jsi Function::call, so an error thrown by the worklet propagated as jsi::JSError through the UIKit text input delegate / Android TextWatcher and aborted the app. In debug builds the worklets call guard reports the error to LogBox and returns undefined, so the following asObject call threw with the same result. Catch jsi errors around the transformer invocation and result extraction, log them to the UI runtime console, and fall back to leaving the input untransformed. Add a throwing transformer card to the example app to exercise the path. --- cpp/TransformerTextInputRuntime.cpp | 75 ++++++++++++++++++++--------- example/src/App.tsx | 21 ++++++++ 2 files changed, 73 insertions(+), 23 deletions(-) diff --git a/cpp/TransformerTextInputRuntime.cpp b/cpp/TransformerTextInputRuntime.cpp index 07a67e7..80be8e4 100644 --- a/cpp/TransformerTextInputRuntime.cpp +++ b/cpp/TransformerTextInputRuntime.cpp @@ -4,6 +4,19 @@ namespace rntti { namespace { std::weak_ptr gUiRuntime; + +void LogTransformerError(jsi::Runtime &runtime, const std::string &message) { + try { + auto console = runtime.global().getPropertyAsObject(runtime, "console"); + auto errorFunction = console.getPropertyAsFunction(runtime, "error"); + errorFunction.call( + runtime, + jsi::String::createFromUtf8( + runtime, "[rntti] Transformer threw an error: " + message)); + } catch (const jsi::JSIException &) { + // console is not guaranteed to exist on the worklet runtime. + } +} } // namespace void SetUIWorkletRuntime( @@ -59,31 +72,47 @@ std::optional RunTransformer( auto transformerFunction = transformerValue.asObject(jsiRuntime).asFunction(jsiRuntime); - auto resultValue = uiRuntime->runSync( - transformerFunction, - jsi::String::createFromUtf8(jsiRuntime, value), - jsi::Value(selection.start), - jsi::Value(selection.end), - jsi::Value(transform)); - - TransformResult result; - auto resultObject = resultValue.asObject(jsiRuntime); - auto valueProp = resultObject.getProperty(jsiRuntime, "value"); - if (valueProp.isString()) { - result.value = valueProp.asString(jsiRuntime).utf8(jsiRuntime); - } - auto selectionProp = resultObject.getProperty(jsiRuntime, "selection"); - if (selectionProp.isObject()) { - auto selectionObject = selectionProp.asObject(jsiRuntime); - auto startProp = selectionObject.getProperty(jsiRuntime, "start"); - auto endProp = selectionObject.getProperty(jsiRuntime, "end"); - result.selection = SelectionRange{ - static_cast(startProp.asNumber()), - static_cast(endProp.asNumber())}; - } + try { + auto resultValue = uiRuntime->runSync( + transformerFunction, + jsi::String::createFromUtf8(jsiRuntime, value), + jsi::Value(selection.start), + jsi::Value(selection.end), + jsi::Value(transform)); + + // In debug builds runSync guards the call: a throwing transformer is + // reported to LogBox and undefined is returned instead of a result + // object. + if (!resultValue.isObject()) { + return std::nullopt; + } - return result; + TransformResult result; + auto resultObject = resultValue.asObject(jsiRuntime); + auto valueProp = resultObject.getProperty(jsiRuntime, "value"); + if (valueProp.isString()) { + result.value = valueProp.asString(jsiRuntime).utf8(jsiRuntime); + } + + auto selectionProp = resultObject.getProperty(jsiRuntime, "selection"); + if (selectionProp.isObject()) { + auto selectionObject = selectionProp.asObject(jsiRuntime); + auto startProp = selectionObject.getProperty(jsiRuntime, "start"); + auto endProp = selectionObject.getProperty(jsiRuntime, "end"); + result.selection = SelectionRange{ + static_cast(startProp.asNumber()), + static_cast(endProp.asNumber())}; + } + + return result; + } catch (const jsi::JSError &error) { + LogTransformerError(jsiRuntime, error.getMessage()); + return std::nullopt; + } catch (const jsi::JSIException &error) { + LogTransformerError(jsiRuntime, error.what()); + return std::nullopt; + } } } // namespace rntti diff --git a/example/src/App.tsx b/example/src/App.tsx index 6d79418..ccaf7ca 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -34,6 +34,18 @@ const usernameTransformer = new Transformer(({ value }) => { return { value: result }; }); +// Returns an out-of-bounds selection, which makes the library's selection +// validation throw on every change. Typing in this input must log the error +// instead of crashing the app. +const throwingTransformer = new Transformer(({ value }) => { + 'worklet'; + + return { + value, + selection: { start: value.length + 5, end: value.length + 5 }, + }; +}); + const phoneNumberTransformer = new PhoneNumberTransformer(); const creditCardTransformer = new PatternTransformer({ @@ -241,6 +253,15 @@ function AppContent() { /> + + ); }