Skip to content

Commit a5ade8b

Browse files
Use GetStringRegion for JavaTurboModule string returns
1 parent 434a306 commit a5ade8b

3 files changed

Lines changed: 104 additions & 5 deletions

File tree

‎packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -828,12 +828,19 @@ jsi::Value JavaTurboModule::invokeJavaMethod(
828828
jsi::Value returnValue = jsi::Value::null();
829829
if (returnString != nullptr) {
830830
jsize length = env->GetStringLength(returnString);
831-
const jchar* chars = env->GetStringChars(returnString, nullptr);
831+
// GetStringRegion: no GetStringChars/ReleaseStringChars pair needed.
832+
constexpr size_t kStackChars = 256;
833+
char16_t stackChars[kStackChars];
834+
std::u16string heapChars;
835+
char16_t* chars = stackChars;
836+
if (static_cast<size_t>(length) > kStackChars) {
837+
heapChars.resize(static_cast<size_t>(length));
838+
chars = heapChars.data();
839+
}
840+
env->GetStringRegion(
841+
returnString, 0, length, reinterpret_cast<jchar*>(chars));
832842
auto jsiString = jsi::String::createFromUtf16(
833-
runtime,
834-
reinterpret_cast<const char16_t*>(chars),
835-
static_cast<size_t>(length));
836-
env->ReleaseStringChars(returnString, chars);
843+
runtime, chars, static_cast<size_t>(length));
837844
returnValue = jsi::Value(runtime, jsiString);
838845
}
839846

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
appId: ${APP_ID} # iOS: com.meta.RNTester.localDevelopment | Android: com.facebook.react.uiapp
2+
---
3+
- launchApp
4+
- extendedWaitUntil:
5+
visible: 'Components'
6+
timeout: 30000
7+
- openLink: rntester://example/TurboModuleExample
8+
- extendedWaitUntil:
9+
visible: 'getStringRoundTrip'
10+
timeout: 30000
11+
- tapOn: 'getStringRoundTrip'
12+
# 200 rounds x (17 fixed + 2 random) strings: ASCII, Latin-1, CJK, RTL, emoji, ZWJ, combining marks,
13+
# 255/256/257 stack-buffer boundary, 5000 and 100000 chars, random) + null.
14+
- extendedWaitUntil:
15+
visible: '.*3800/3800 ok.*'
16+
timeout: 180000
17+
- assertNotVisible: '.*FAIL.*'

‎packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ type Examples =
4747
| 'getRootTag'
4848
| 'getSet'
4949
| 'getString'
50+
| 'getStringRoundTrip'
51+
| 'getStringControlChars'
5052
| 'getUnion'
5153
| 'getUnsafeObject'
5254
| 'getValue'
@@ -69,6 +71,49 @@ type ErrorExamples =
6971
| 'promiseAssert'
7072
| 'installJSIBindings';
7173

74+
const STRING_ROUND_TRIP_ROUNDS = 200;
75+
const STRING_ROUND_TRIP_INPUTS = [
76+
'',
77+
'a',
78+
'hello',
79+
'h\u00e9llo \u00e7\u00e3\u00f5',
80+
'\u65e5\u672c\u8a9e\u30c6\u30ad\u30b9\u30c8',
81+
'\u041f\u0440\u0438\u0432\u0435\u0442',
82+
'\u0645\u0631\u062d\u0628\u0627',
83+
'\ud83d\ude00',
84+
'\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d\udc66',
85+
'\ud83c\uddee\ud83c\uddf3',
86+
'e\u0301',
87+
'x'.repeat(255),
88+
'x'.repeat(256),
89+
'x'.repeat(257),
90+
'\ud83d\ude00'.repeat(200),
91+
'x'.repeat(5000),
92+
'\u65e5'.repeat(100000),
93+
];
94+
95+
// Kept out of the Maestro-driven test: the demo Toast shows the input and
96+
// uiautomator cannot serialize NUL or lone surrogates.
97+
const CONTROL_CHAR_INPUTS = ['\ud800', '\udc00x', 'a\u0000b'];
98+
99+
function randomUnicodeString(): string {
100+
let out = '';
101+
const length = Math.floor(Math.random() * 3000);
102+
for (let i = 0; i < length; i++) {
103+
const kind = Math.random();
104+
const codePoint =
105+
kind < 0.4
106+
? 32 + Math.floor(Math.random() * 95)
107+
: kind < 0.6
108+
? 0x80 + Math.floor(Math.random() * 0x780)
109+
: kind < 0.8
110+
? 0x1000 + Math.floor(Math.random() * 0xc000)
111+
: 0x1f300 + Math.floor(Math.random() * 0x300);
112+
out += String.fromCodePoint(codePoint);
113+
}
114+
return out;
115+
}
116+
72117
class SampleTurboModuleExample extends React.Component<{}, State> {
73118
static contextType: React.Context<RootTag> = RootTagContext;
74119
eventSubscriptions: EventSubscription[] = [];
@@ -120,6 +165,36 @@ class SampleTurboModuleExample extends React.Component<{}, State> {
120165
NativeSampleTurboModule.getObject({a: 1, b: 'foo', c: null}),
121166
getRootTag: () => NativeSampleTurboModule.getRootTag(this.context),
122167
getString: () => NativeSampleTurboModule.getString('Hello'),
168+
getStringRoundTrip: () => {
169+
const expected =
170+
STRING_ROUND_TRIP_ROUNDS * (STRING_ROUND_TRIP_INPUTS.length + 2);
171+
let pass = 0;
172+
let failure = '';
173+
for (let round = 0; round < STRING_ROUND_TRIP_ROUNDS; round++) {
174+
const inputs = [
175+
...STRING_ROUND_TRIP_INPUTS,
176+
randomUnicodeString(),
177+
randomUnicodeString(),
178+
];
179+
for (const input of inputs) {
180+
const output = NativeSampleTurboModule.getString(input);
181+
if (output === input) {
182+
pass++;
183+
} else if (failure === '') {
184+
failure = ` FAIL len ${input.length} -> ${
185+
output == null ? 'null' : output.length
186+
}`;
187+
}
188+
}
189+
}
190+
// $FlowFixMe[incompatible-call] null must round-trip as null
191+
const nullOk = NativeSampleTurboModule.getString(null) === null;
192+
return `${pass}/${expected} ok${nullOk ? '' : ' null FAIL'}${failure}`;
193+
},
194+
getStringControlChars: () =>
195+
CONTROL_CHAR_INPUTS.map(
196+
input => NativeSampleTurboModule.getString(input) === input,
197+
).join(','),
123198
getUnsafeObject: () =>
124199
NativeSampleTurboModule.getUnsafeObject({a: 1, b: 'foo', c: null}),
125200
getValue: () =>

0 commit comments

Comments
 (0)