From a5ade8be92f314d5b862c19cafe7e5086f5c0f75 Mon Sep 17 00:00:00 2001 From: riteshshukla04 Date: Mon, 21 Sep 2026 13:34:49 +0530 Subject: [PATCH 1/2] Use GetStringRegion for JavaTurboModule string returns --- .../android/ReactCommon/JavaTurboModule.cpp | 17 +++-- .../turbomodule-getstring-roundtrip.yml | 17 +++++ .../TurboModule/SampleTurboModuleExample.js | 75 +++++++++++++++++++ 3 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 packages/rn-tester/.maestro/turbomodule-getstring-roundtrip.yml diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp b/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp index 0db4927a6338..a44d00557a7d 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp @@ -828,12 +828,19 @@ jsi::Value JavaTurboModule::invokeJavaMethod( jsi::Value returnValue = jsi::Value::null(); if (returnString != nullptr) { jsize length = env->GetStringLength(returnString); - const jchar* chars = env->GetStringChars(returnString, nullptr); + // GetStringRegion: no GetStringChars/ReleaseStringChars pair needed. + constexpr size_t kStackChars = 256; + char16_t stackChars[kStackChars]; + std::u16string heapChars; + char16_t* chars = stackChars; + if (static_cast(length) > kStackChars) { + heapChars.resize(static_cast(length)); + chars = heapChars.data(); + } + env->GetStringRegion( + returnString, 0, length, reinterpret_cast(chars)); auto jsiString = jsi::String::createFromUtf16( - runtime, - reinterpret_cast(chars), - static_cast(length)); - env->ReleaseStringChars(returnString, chars); + runtime, chars, static_cast(length)); returnValue = jsi::Value(runtime, jsiString); } diff --git a/packages/rn-tester/.maestro/turbomodule-getstring-roundtrip.yml b/packages/rn-tester/.maestro/turbomodule-getstring-roundtrip.yml new file mode 100644 index 000000000000..582d3feb9113 --- /dev/null +++ b/packages/rn-tester/.maestro/turbomodule-getstring-roundtrip.yml @@ -0,0 +1,17 @@ +appId: ${APP_ID} # iOS: com.meta.RNTester.localDevelopment | Android: com.facebook.react.uiapp +--- +- launchApp +- extendedWaitUntil: + visible: 'Components' + timeout: 30000 +- openLink: rntester://example/TurboModuleExample +- extendedWaitUntil: + visible: 'getStringRoundTrip' + timeout: 30000 +- tapOn: 'getStringRoundTrip' +# 200 rounds x (17 fixed + 2 random) strings: ASCII, Latin-1, CJK, RTL, emoji, ZWJ, combining marks, +# 255/256/257 stack-buffer boundary, 5000 and 100000 chars, random) + null. +- extendedWaitUntil: + visible: '.*3800/3800 ok.*' + timeout: 180000 +- assertNotVisible: '.*FAIL.*' diff --git a/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js b/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js index ddfcc31fb04f..767b77a30785 100644 --- a/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js +++ b/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js @@ -47,6 +47,8 @@ type Examples = | 'getRootTag' | 'getSet' | 'getString' + | 'getStringRoundTrip' + | 'getStringControlChars' | 'getUnion' | 'getUnsafeObject' | 'getValue' @@ -69,6 +71,49 @@ type ErrorExamples = | 'promiseAssert' | 'installJSIBindings'; +const STRING_ROUND_TRIP_ROUNDS = 200; +const STRING_ROUND_TRIP_INPUTS = [ + '', + 'a', + 'hello', + 'h\u00e9llo \u00e7\u00e3\u00f5', + '\u65e5\u672c\u8a9e\u30c6\u30ad\u30b9\u30c8', + '\u041f\u0440\u0438\u0432\u0435\u0442', + '\u0645\u0631\u062d\u0628\u0627', + '\ud83d\ude00', + '\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d\udc66', + '\ud83c\uddee\ud83c\uddf3', + 'e\u0301', + 'x'.repeat(255), + 'x'.repeat(256), + 'x'.repeat(257), + '\ud83d\ude00'.repeat(200), + 'x'.repeat(5000), + '\u65e5'.repeat(100000), +]; + +// Kept out of the Maestro-driven test: the demo Toast shows the input and +// uiautomator cannot serialize NUL or lone surrogates. +const CONTROL_CHAR_INPUTS = ['\ud800', '\udc00x', 'a\u0000b']; + +function randomUnicodeString(): string { + let out = ''; + const length = Math.floor(Math.random() * 3000); + for (let i = 0; i < length; i++) { + const kind = Math.random(); + const codePoint = + kind < 0.4 + ? 32 + Math.floor(Math.random() * 95) + : kind < 0.6 + ? 0x80 + Math.floor(Math.random() * 0x780) + : kind < 0.8 + ? 0x1000 + Math.floor(Math.random() * 0xc000) + : 0x1f300 + Math.floor(Math.random() * 0x300); + out += String.fromCodePoint(codePoint); + } + return out; +} + class SampleTurboModuleExample extends React.Component<{}, State> { static contextType: React.Context = RootTagContext; eventSubscriptions: EventSubscription[] = []; @@ -120,6 +165,36 @@ class SampleTurboModuleExample extends React.Component<{}, State> { NativeSampleTurboModule.getObject({a: 1, b: 'foo', c: null}), getRootTag: () => NativeSampleTurboModule.getRootTag(this.context), getString: () => NativeSampleTurboModule.getString('Hello'), + getStringRoundTrip: () => { + const expected = + STRING_ROUND_TRIP_ROUNDS * (STRING_ROUND_TRIP_INPUTS.length + 2); + let pass = 0; + let failure = ''; + for (let round = 0; round < STRING_ROUND_TRIP_ROUNDS; round++) { + const inputs = [ + ...STRING_ROUND_TRIP_INPUTS, + randomUnicodeString(), + randomUnicodeString(), + ]; + for (const input of inputs) { + const output = NativeSampleTurboModule.getString(input); + if (output === input) { + pass++; + } else if (failure === '') { + failure = ` FAIL len ${input.length} -> ${ + output == null ? 'null' : output.length + }`; + } + } + } + // $FlowFixMe[incompatible-call] null must round-trip as null + const nullOk = NativeSampleTurboModule.getString(null) === null; + return `${pass}/${expected} ok${nullOk ? '' : ' null FAIL'}${failure}`; + }, + getStringControlChars: () => + CONTROL_CHAR_INPUTS.map( + input => NativeSampleTurboModule.getString(input) === input, + ).join(','), getUnsafeObject: () => NativeSampleTurboModule.getUnsafeObject({a: 1, b: 'foo', c: null}), getValue: () => From d32fbc201e592e8411ad8eec889b52a3cba9957a Mon Sep 17 00:00:00 2001 From: riteshshukla04 Date: Mon, 21 Sep 2026 14:39:17 +0530 Subject: [PATCH 2/2] Remove RNTester changes from JavaTurboModule string return fix --- .../turbomodule-getstring-roundtrip.yml | 17 ----- .../TurboModule/SampleTurboModuleExample.js | 75 ------------------- 2 files changed, 92 deletions(-) delete mode 100644 packages/rn-tester/.maestro/turbomodule-getstring-roundtrip.yml diff --git a/packages/rn-tester/.maestro/turbomodule-getstring-roundtrip.yml b/packages/rn-tester/.maestro/turbomodule-getstring-roundtrip.yml deleted file mode 100644 index 582d3feb9113..000000000000 --- a/packages/rn-tester/.maestro/turbomodule-getstring-roundtrip.yml +++ /dev/null @@ -1,17 +0,0 @@ -appId: ${APP_ID} # iOS: com.meta.RNTester.localDevelopment | Android: com.facebook.react.uiapp ---- -- launchApp -- extendedWaitUntil: - visible: 'Components' - timeout: 30000 -- openLink: rntester://example/TurboModuleExample -- extendedWaitUntil: - visible: 'getStringRoundTrip' - timeout: 30000 -- tapOn: 'getStringRoundTrip' -# 200 rounds x (17 fixed + 2 random) strings: ASCII, Latin-1, CJK, RTL, emoji, ZWJ, combining marks, -# 255/256/257 stack-buffer boundary, 5000 and 100000 chars, random) + null. -- extendedWaitUntil: - visible: '.*3800/3800 ok.*' - timeout: 180000 -- assertNotVisible: '.*FAIL.*' diff --git a/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js b/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js index 767b77a30785..ddfcc31fb04f 100644 --- a/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js +++ b/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js @@ -47,8 +47,6 @@ type Examples = | 'getRootTag' | 'getSet' | 'getString' - | 'getStringRoundTrip' - | 'getStringControlChars' | 'getUnion' | 'getUnsafeObject' | 'getValue' @@ -71,49 +69,6 @@ type ErrorExamples = | 'promiseAssert' | 'installJSIBindings'; -const STRING_ROUND_TRIP_ROUNDS = 200; -const STRING_ROUND_TRIP_INPUTS = [ - '', - 'a', - 'hello', - 'h\u00e9llo \u00e7\u00e3\u00f5', - '\u65e5\u672c\u8a9e\u30c6\u30ad\u30b9\u30c8', - '\u041f\u0440\u0438\u0432\u0435\u0442', - '\u0645\u0631\u062d\u0628\u0627', - '\ud83d\ude00', - '\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d\udc66', - '\ud83c\uddee\ud83c\uddf3', - 'e\u0301', - 'x'.repeat(255), - 'x'.repeat(256), - 'x'.repeat(257), - '\ud83d\ude00'.repeat(200), - 'x'.repeat(5000), - '\u65e5'.repeat(100000), -]; - -// Kept out of the Maestro-driven test: the demo Toast shows the input and -// uiautomator cannot serialize NUL or lone surrogates. -const CONTROL_CHAR_INPUTS = ['\ud800', '\udc00x', 'a\u0000b']; - -function randomUnicodeString(): string { - let out = ''; - const length = Math.floor(Math.random() * 3000); - for (let i = 0; i < length; i++) { - const kind = Math.random(); - const codePoint = - kind < 0.4 - ? 32 + Math.floor(Math.random() * 95) - : kind < 0.6 - ? 0x80 + Math.floor(Math.random() * 0x780) - : kind < 0.8 - ? 0x1000 + Math.floor(Math.random() * 0xc000) - : 0x1f300 + Math.floor(Math.random() * 0x300); - out += String.fromCodePoint(codePoint); - } - return out; -} - class SampleTurboModuleExample extends React.Component<{}, State> { static contextType: React.Context = RootTagContext; eventSubscriptions: EventSubscription[] = []; @@ -165,36 +120,6 @@ class SampleTurboModuleExample extends React.Component<{}, State> { NativeSampleTurboModule.getObject({a: 1, b: 'foo', c: null}), getRootTag: () => NativeSampleTurboModule.getRootTag(this.context), getString: () => NativeSampleTurboModule.getString('Hello'), - getStringRoundTrip: () => { - const expected = - STRING_ROUND_TRIP_ROUNDS * (STRING_ROUND_TRIP_INPUTS.length + 2); - let pass = 0; - let failure = ''; - for (let round = 0; round < STRING_ROUND_TRIP_ROUNDS; round++) { - const inputs = [ - ...STRING_ROUND_TRIP_INPUTS, - randomUnicodeString(), - randomUnicodeString(), - ]; - for (const input of inputs) { - const output = NativeSampleTurboModule.getString(input); - if (output === input) { - pass++; - } else if (failure === '') { - failure = ` FAIL len ${input.length} -> ${ - output == null ? 'null' : output.length - }`; - } - } - } - // $FlowFixMe[incompatible-call] null must round-trip as null - const nullOk = NativeSampleTurboModule.getString(null) === null; - return `${pass}/${expected} ok${nullOk ? '' : ' null FAIL'}${failure}`; - }, - getStringControlChars: () => - CONTROL_CHAR_INPUTS.map( - input => NativeSampleTurboModule.getString(input) === input, - ).join(','), getUnsafeObject: () => NativeSampleTurboModule.getUnsafeObject({a: 1, b: 'foo', c: null}), getValue: () =>