From 6d77d09472525c6e8c194f8a3176647120a2356a Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 9 Nov 2026 11:13:14 +0500 Subject: [PATCH] perf(android): pass strings across the bridge as UTF-16 --- .../jsi/callbackhandlers/CallbackHandlers.cpp | 4 -- .../ffi/jni/jsi/conversion/ArgConverter.cpp | 45 ------------------- .../ffi/jni/jsi/conversion/ArgConverter.h | 22 +++++---- .../jsi/conversion/ArrayElementAccessor.cpp | 4 -- .../ffi/jni/jsi/metadata/FieldAccessor.cpp | 4 -- .../callbackhandlers/CallbackHandlers.cpp | 6 +-- .../ffi/jni/napi/conversion/ArgConverter.h | 18 ++++++-- .../napi/conversion/ArrayElementAccessor.cpp | 6 +-- .../ffi/jni/napi/metadata/FieldAccessor.cpp | 6 +-- NativeScript/jsi/hermes/HermesRuntime.h | 29 ++++++++++++ NativeScript/jsi/jsc/JSCRuntime.h | 2 + NativeScript/jsi/jsc/JSCValue.cpp | 16 +++++++ NativeScript/jsi/quickjs/QuickJSRuntime.h | 2 + NativeScript/jsi/quickjs/QuickJSValue.cpp | 23 ++++++++++ NativeScript/jsi/v8/V8Runtime.h | 2 + NativeScript/jsi/v8/V8Value.cpp | 17 +++++++ vendor/jsc/jsc-api.cpp | 2 +- 17 files changed, 123 insertions(+), 85 deletions(-) diff --git a/NativeScript/ffi/jni/jsi/callbackhandlers/CallbackHandlers.cpp b/NativeScript/ffi/jni/jsi/callbackhandlers/CallbackHandlers.cpp index 4638f8985..bda4ac273 100644 --- a/NativeScript/ffi/jni/jsi/callbackhandlers/CallbackHandlers.cpp +++ b/NativeScript/ffi/jni/jsi/callbackhandlers/CallbackHandlers.cpp @@ -386,10 +386,6 @@ JsValue CallbackHandlers::CallJavaMethod(JsRuntime &rt, const JsValue &caller, c result = jEnv.CallCharMethodA(callerJavaObject, mid, javaArgs); } - // The napi tree round-trips the jchar through a jstring and takes one - // byte of its UTF-8 form, which truncates anything outside ASCII. - // engine::String is UTF-8 only, so the transcode is explicit here and - // matches every other jchar path in this tree. return ArgConverter::convertToJsString(rt, &result, 1); } case MethodReturnType::Short: { diff --git a/NativeScript/ffi/jni/jsi/conversion/ArgConverter.cpp b/NativeScript/ffi/jni/jsi/conversion/ArgConverter.cpp index 9f1d895a2..e7929e6fe 100644 --- a/NativeScript/ffi/jni/jsi/conversion/ArgConverter.cpp +++ b/NativeScript/ffi/jni/jsi/conversion/ArgConverter.cpp @@ -239,51 +239,6 @@ ArgConverter::TypeLongOperationsCache *ArgConverter::GetTypeLongCache(JsRuntime return cache; } -JsValue ArgConverter::convertToJsString(JsRuntime &rt, const jchar *data, int length) { - if (data == nullptr || length <= 0) { - return convertToJsString(rt, std::string()); - } - - // Strict UTF-16 -> UTF-8, matching what napi_create_string_utf16 did inside - // the engine. Unpaired surrogates are emitted as U+FFFD rather than dropped, - // so a lone jchar (Type::Char, which is exactly one code unit) still yields a - // one-character JS string. - std::string utf8; - utf8.reserve((size_t) length); - for (int i = 0; i < length; i++) { - uint32_t cp = data[i]; - if (cp >= 0xD800 && cp <= 0xDBFF && i + 1 < length) { - uint32_t low = data[i + 1]; - if (low >= 0xDC00 && low <= 0xDFFF) { - cp = 0x10000 + ((cp - 0xD800) << 10) + (low - 0xDC00); - i++; - } else { - cp = 0xFFFD; - } - } else if (cp >= 0xD800 && cp <= 0xDFFF) { - cp = 0xFFFD; - } - - if (cp < 0x80) { - utf8.push_back((char) cp); - } else if (cp < 0x800) { - utf8.push_back((char) (0xC0 | (cp >> 6))); - utf8.push_back((char) (0x80 | (cp & 0x3F))); - } else if (cp < 0x10000) { - utf8.push_back((char) (0xE0 | (cp >> 12))); - utf8.push_back((char) (0x80 | ((cp >> 6) & 0x3F))); - utf8.push_back((char) (0x80 | (cp & 0x3F))); - } else { - utf8.push_back((char) (0xF0 | (cp >> 18))); - utf8.push_back((char) (0x80 | ((cp >> 12) & 0x3F))); - utf8.push_back((char) (0x80 | ((cp >> 6) & 0x3F))); - utf8.push_back((char) (0x80 | (cp & 0x3F))); - } - } - - return convertToJsString(rt, utf8); -} - u16string ArgConverter::ConvertToUtf16String(JsRuntime &rt, const JsValue &s) { if (!s.isString()) { return {}; diff --git a/NativeScript/ffi/jni/jsi/conversion/ArgConverter.h b/NativeScript/ffi/jni/jsi/conversion/ArgConverter.h index 2c1ddb390..aec02fe45 100644 --- a/NativeScript/ffi/jni/jsi/conversion/ArgConverter.h +++ b/NativeScript/ffi/jni/jsi/conversion/ArgConverter.h @@ -31,10 +31,10 @@ namespace tns { if (value == nullptr) return js_util::null(); JEnv jenv; - auto chars = jenv.GetStringUTFChars(value, JNI_FALSE); - auto length = jenv.GetStringUTFLength(value); + jsize length = jenv.GetStringLength(value); + const jchar *chars = jenv.GetStringChars(value, nullptr); auto jsString = convertToJsString(rt, chars, length); - jenv.ReleaseStringUTFChars(value, chars); + jenv.ReleaseStringChars(value, chars); return jsString; } @@ -65,14 +65,20 @@ namespace tns { static std::u16string ConvertToUtf16String(JsRuntime &rt, const JsValue &s); inline static jstring ConvertToJavaString(JsRuntime &rt, const JsValue &jsValue) { + char16_t stackBuffer[256]; + size_t length = jsValue.copyUtf16(rt, stackBuffer, std::size(stackBuffer)); JEnv jenv; - return jenv.NewStringUTF(js_util::get_string_value(rt, jsValue).c_str()); + if (length <= std::size(stackBuffer)) { + return jenv.NewString(reinterpret_cast(stackBuffer), static_cast(length)); + } + std::u16string heapBuffer(length, u'\0'); + jsValue.copyUtf16(rt, heapBuffer.data(), length); + return jenv.NewString(reinterpret_cast(heapBuffer.data()), static_cast(length)); } - // engine::String is UTF-8 only, so a UTF-16 payload (every jchar/jstring - // that reaches here) is transcoded on the way in rather than handed to a - // napi_create_string_utf16 equivalent. - static JsValue convertToJsString(JsRuntime &rt, const jchar *data, int length); + inline static JsValue convertToJsString(JsRuntime &rt, const jchar *data, int length) { + return JsValue::createStringFromUtf16(rt, reinterpret_cast(data), (size_t) length); + } inline static JsValue convertToJsString(JsRuntime &rt, const std::string &s) { return JsValue::createStringFromUtf8(rt, s.data(), s.size()); diff --git a/NativeScript/ffi/jni/jsi/conversion/ArrayElementAccessor.cpp b/NativeScript/ffi/jni/jsi/conversion/ArrayElementAccessor.cpp index 625ba4ca9..a153c1c05 100644 --- a/NativeScript/ffi/jni/jsi/conversion/ArrayElementAccessor.cpp +++ b/NativeScript/ffi/jni/jsi/conversion/ArrayElementAccessor.cpp @@ -48,10 +48,6 @@ JsValue ArrayElementAccessor::GetArrayElement(JsRuntime &rt, const JsValue &arra case 'C': { jchar v; jenv.GetCharArrayRegion((jcharArray) arr, startIndex, length, &v); - // The napi tree round-trips the jchar through a jstring and then takes - // one byte of its UTF-8 form, which truncates anything outside ASCII. - // engine::String is UTF-8 only, so the transcode is explicit here and - // is the same one every other jchar path in this tree uses. return ArgConverter::convertToJsString(rt, &v, 1); } case 'S': { diff --git a/NativeScript/ffi/jni/jsi/metadata/FieldAccessor.cpp b/NativeScript/ffi/jni/jsi/metadata/FieldAccessor.cpp index b7c0be426..4ddf60110 100644 --- a/NativeScript/ffi/jni/jsi/metadata/FieldAccessor.cpp +++ b/NativeScript/ffi/jni/jsi/metadata/FieldAccessor.cpp @@ -89,10 +89,6 @@ FieldAccessor::GetJavaField(JsRuntime &rt, const JsValue &target, FieldCallbackD result = jEnv.GetCharField(targetJavaObject, fieldId); } - // The napi tree round-trips the jchar through a jstring and takes - // one byte of its UTF-8 form, which truncates anything outside - // ASCII. engine::String is UTF-8 only, so the transcode is - // explicit here and matches every other jchar path in this tree. return ArgConverter::convertToJsString(rt, &result, 1); } case 'S': { // short diff --git a/NativeScript/ffi/jni/napi/callbackhandlers/CallbackHandlers.cpp b/NativeScript/ffi/jni/napi/callbackhandlers/CallbackHandlers.cpp index f050d6afa..3dff0ff57 100644 --- a/NativeScript/ffi/jni/napi/callbackhandlers/CallbackHandlers.cpp +++ b/NativeScript/ffi/jni/napi/callbackhandlers/CallbackHandlers.cpp @@ -384,11 +384,7 @@ napi_value CallbackHandlers::CallJavaMethod(napi_env env, napi_value caller, con result = jEnv.CallCharMethodA(callerJavaObject, mid, javaArgs); } - JniLocalRef str(jEnv.NewString(&result, 1)); - jboolean bol = true; - const char *resP = jEnv.GetStringUTFChars(str, &bol); - returnValue = ArgConverter::convertToJsString(env, resP, 1); - jEnv.ReleaseStringUTFChars(str, resP); + returnValue = ArgConverter::convertToJsString(env, &result, 1); break; } case MethodReturnType::Short: { diff --git a/NativeScript/ffi/jni/napi/conversion/ArgConverter.h b/NativeScript/ffi/jni/napi/conversion/ArgConverter.h index ea6eb9e81..c7e270a09 100644 --- a/NativeScript/ffi/jni/napi/conversion/ArgConverter.h +++ b/NativeScript/ffi/jni/napi/conversion/ArgConverter.h @@ -30,10 +30,10 @@ namespace tns { if (value == nullptr) return napi_util::null(env); JEnv jenv; - auto chars = jenv.GetStringUTFChars(value,JNI_FALSE); - auto length = jenv.GetStringUTFLength(value); + jsize length = jenv.GetStringLength(value); + const jchar *chars = jenv.GetStringChars(value, nullptr); auto jsString = convertToJsString(env, chars, length); - jenv.ReleaseStringUTFChars(value, chars); + jenv.ReleaseStringChars(value, chars); return jsString; } @@ -64,8 +64,18 @@ namespace tns { static std::u16string ConvertToUtf16String(napi_env env, napi_value s); inline static jstring ConvertToJavaString(napi_env env, napi_value jsValue) { + size_t length = 0; + napi_get_value_string_utf16(env, jsValue, nullptr, 0, &length); + char16_t stackBuffer[256]; + std::u16string heapBuffer; + char16_t *buffer = stackBuffer; + if (length >= std::size(stackBuffer)) { + heapBuffer.resize(length + 1); + buffer = heapBuffer.data(); + } + napi_get_value_string_utf16(env, jsValue, buffer, length + 1, &length); JEnv jenv; - return jenv.NewStringUTF(napi_util::get_string_value(env, jsValue, 0)); + return jenv.NewString(reinterpret_cast(buffer), static_cast(length)); } inline static napi_value convertToJsString(napi_env env, const jchar *data, int length) { diff --git a/NativeScript/ffi/jni/napi/conversion/ArrayElementAccessor.cpp b/NativeScript/ffi/jni/napi/conversion/ArrayElementAccessor.cpp index 6d5781966..f7b47c653 100644 --- a/NativeScript/ffi/jni/napi/conversion/ArrayElementAccessor.cpp +++ b/NativeScript/ffi/jni/napi/conversion/ArrayElementAccessor.cpp @@ -56,11 +56,7 @@ napi_value ArrayElementAccessor::GetArrayElement(napi_env env, napi_value array, case 'C': { jchar v; jenv.GetCharArrayRegion((jcharArray) arr, startIndex, length, &v); - JniLocalRef s(jenv.NewString(&v, 1)); - jboolean isCopy = false; - const char* singleChar = jenv.GetStringUTFChars(s, &isCopy); - NAPI_GUARD(napi_create_string_utf8(env, singleChar, 1, &value)) {} - jenv.ReleaseStringUTFChars(s, singleChar); + value = ArgConverter::convertToJsString(env, &v, 1); break; } case 'S': { diff --git a/NativeScript/ffi/jni/napi/metadata/FieldAccessor.cpp b/NativeScript/ffi/jni/napi/metadata/FieldAccessor.cpp index 7cd01694a..bf12961f9 100644 --- a/NativeScript/ffi/jni/napi/metadata/FieldAccessor.cpp +++ b/NativeScript/ffi/jni/napi/metadata/FieldAccessor.cpp @@ -97,11 +97,7 @@ FieldAccessor::GetJavaField(napi_env env, napi_value target, FieldCallbackData * result = jEnv.GetCharField(targetJavaObject, fieldId); } - JniLocalRef str(jEnv.NewString(&result, 1)); - jboolean bol = true; - const char *resP = jEnv.GetStringUTFChars(str, &bol); - fieldResult = ArgConverter::convertToJsString(env, resP, 1); - jEnv.ReleaseStringUTFChars(str, resP); + fieldResult = ArgConverter::convertToJsString(env, &result, 1); break; } case 'S': { // short diff --git a/NativeScript/jsi/hermes/HermesRuntime.h b/NativeScript/jsi/hermes/HermesRuntime.h index a01a29ea2..71b3f5668 100644 --- a/NativeScript/jsi/hermes/HermesRuntime.h +++ b/NativeScript/jsi/hermes/HermesRuntime.h @@ -46,6 +46,7 @@ #include +#include #include #include #include @@ -405,6 +406,8 @@ class Value { // engine so the shared bridge can call one name. std::string utf8(Runtime& runtime) const; static Value createStringFromUtf8(Runtime& runtime, const char* data, size_t length); + static Value createStringFromUtf16(Runtime& runtime, const char16_t* data, size_t length); + size_t copyUtf16(Runtime& runtime, char16_t* buffer, size_t capacity) const; // The jsi handle behind this value, materialising one for the inline scalar // kinds. Returned by value because the scalar kinds have no handle to refer @@ -913,6 +916,32 @@ inline Value Runtime::evaluateJavaScript(std::shared_ptr buffer, // --- String ---------------------------------------------------------------- +inline Value Value::createStringFromUtf16(Runtime& runtime, const char16_t* data, size_t length) { + return hermesengine::guard(runtime, [&] { + return Value::fromStorage(hermesengine::makeStorage(::facebook::jsi::Value( + ::facebook::jsi::String::createFromUtf16(runtime.jsi(), data != nullptr ? data : u"", length)))); + }); +} + +inline size_t Value::copyUtf16(Runtime& runtime, char16_t* buffer, size_t capacity) const { + String string = asString(runtime); + return hermesengine::guard(runtime, [&] { + size_t length = 0; + auto append = [&](bool ascii, const void* data, size_t count) { + size_t copied = length < capacity ? std::min(count, capacity - length) : 0; + if (ascii) { + const char* chars = static_cast(data); + for (size_t i = 0; i < copied; i++) buffer[length + i] = static_cast(chars[i]); + } else { + std::memcpy(buffer + length, data, copied * sizeof(char16_t)); + } + length += count; + }; + string.storage_->string(runtime.jsi()).getStringData(runtime.jsi(), append); + return length; + }); +} + inline String String::createFromUtf8(Runtime& runtime, const char* value) { const char* text = value != nullptr ? value : ""; return createFromUtf8(runtime, reinterpret_cast(text), diff --git a/NativeScript/jsi/jsc/JSCRuntime.h b/NativeScript/jsi/jsc/JSCRuntime.h index 137a55d60..5ab756a30 100644 --- a/NativeScript/jsi/jsc/JSCRuntime.h +++ b/NativeScript/jsi/jsc/JSCRuntime.h @@ -661,6 +661,8 @@ class Value { // engine so the shared bridge can call one name. std::string utf8(Runtime& runtime) const; static Value createStringFromUtf8(Runtime& runtime, const char* data, size_t length); + static Value createStringFromUtf16(Runtime& runtime, const char16_t* data, size_t length); + size_t copyUtf16(Runtime& runtime, char16_t* buffer, size_t capacity) const; JSValueRef local(Runtime& runtime) const { switch (kind_) { diff --git a/NativeScript/jsi/jsc/JSCValue.cpp b/NativeScript/jsi/jsc/JSCValue.cpp index e68f07fc6..10096fdb8 100644 --- a/NativeScript/jsi/jsc/JSCValue.cpp +++ b/NativeScript/jsi/jsc/JSCValue.cpp @@ -219,6 +219,22 @@ Value Value::createStringFromUtf8(Runtime& runtime, const char* data, size_t len reinterpret_cast(data), length)); } +Value Value::createStringFromUtf16(Runtime& runtime, const char16_t* data, size_t length) { + JSStringRef string = JSStringCreateWithCharacters(reinterpret_cast(data), length); + Value result(runtime, String(runtime, string)); + JSStringRelease(string); + return result; +} + +size_t Value::copyUtf16(Runtime& runtime, char16_t* buffer, size_t capacity) const { + JSStringRef string = JSValueToStringCopy(runtime.context(), local(runtime), nullptr); + if (string == nullptr) return 0; + size_t length = JSStringGetLength(string); + std::memcpy(buffer, JSStringGetCharactersPtr(string), std::min(length, capacity) * sizeof(JSChar)); + JSStringRelease(string); + return length; +} + BigInt Value::getBigInt(Runtime& runtime) const { return BigInt(runtime, local(runtime)); } Function Object::getPropertyAsFunction(Runtime& runtime, const char* name) const { diff --git a/NativeScript/jsi/quickjs/QuickJSRuntime.h b/NativeScript/jsi/quickjs/QuickJSRuntime.h index 3665be003..7c2338d6b 100644 --- a/NativeScript/jsi/quickjs/QuickJSRuntime.h +++ b/NativeScript/jsi/quickjs/QuickJSRuntime.h @@ -576,6 +576,8 @@ class Value { // scope-rooted, so unlike V8 this still needs owning storage -- but it does // not need a second reference. static Value createStringFromUtf8(Runtime& runtime, const char* data, size_t length); + static Value createStringFromUtf16(Runtime& runtime, const char16_t* data, size_t length); + size_t copyUtf16(Runtime& runtime, char16_t* buffer, size_t capacity) const; JSValue local(Runtime& runtime) const { switch (kind_) { diff --git a/NativeScript/jsi/quickjs/QuickJSValue.cpp b/NativeScript/jsi/quickjs/QuickJSValue.cpp index 89dc0276c..6e6ab3ada 100644 --- a/NativeScript/jsi/quickjs/QuickJSValue.cpp +++ b/NativeScript/jsi/quickjs/QuickJSValue.cpp @@ -101,6 +101,29 @@ Value Value::createStringFromUtf8(Runtime& runtime, const char* data, size_t len JS_NewStringLen(runtime.context(), data != nullptr ? data : "", length); return result; } + +Value Value::createStringFromUtf16(Runtime& runtime, const char16_t* data, size_t length) { + Value result; + result.kind_ = quickjsengine::ValueStorage::Kind::QuickJS; + result.storage_ = + std::make_shared(quickjsengine::ValueStorage::Kind::QuickJS); + result.storage_->context = runtime.context(); + result.storage_->value = JS_NewStringUTF16( + runtime.context(), reinterpret_cast(data != nullptr ? data : u""), length); + return result; +} + +size_t Value::copyUtf16(Runtime& runtime, char16_t* buffer, size_t capacity) const { + JSContext* context = runtime.context(); + JSValue value = local(runtime); + size_t length = 0; + const uint16_t* units = JS_ToCStringLenUTF16(context, &length, value); + JS_FreeValue(context, value); + if (units == nullptr) return 0; + std::memcpy(buffer, units, std::min(length, capacity) * sizeof(char16_t)); + JS_FreeCStringUTF16(context, units); + return length; +} BigInt Value::getBigInt(Runtime& runtime) const { JSValue value = local(runtime); BigInt result(runtime, value); diff --git a/NativeScript/jsi/v8/V8Runtime.h b/NativeScript/jsi/v8/V8Runtime.h index 0e23746e9..dacb73ad4 100644 --- a/NativeScript/jsi/v8/V8Runtime.h +++ b/NativeScript/jsi/v8/V8Runtime.h @@ -699,6 +699,8 @@ class Value { // Contract: the result must not outlive the current HandleScope. Callers that // need an owning value keep using String::createFromUtf8. static Value createStringFromUtf8(Runtime& runtime, const char* data, size_t length); + static Value createStringFromUtf16(Runtime& runtime, const char16_t* data, size_t length); + size_t copyUtf16(Runtime& runtime, char16_t* buffer, size_t capacity) const; v8::Local local(Runtime& runtime) const; diff --git a/NativeScript/jsi/v8/V8Value.cpp b/NativeScript/jsi/v8/V8Value.cpp index 4c9b129b0..a48b2176f 100644 --- a/NativeScript/jsi/v8/V8Value.cpp +++ b/NativeScript/jsi/v8/V8Value.cpp @@ -263,6 +263,23 @@ Value Value::createStringFromUtf8(Runtime& runtime, const char* data, size_t len .ToLocalChecked()); } +Value Value::createStringFromUtf16(Runtime& runtime, const char16_t* data, size_t length) { + v8::Isolate* isolate = runtime.isolate(); + return Value::borrowed( + isolate, v8::String::NewFromTwoByte(isolate, + reinterpret_cast(data != nullptr ? data : u""), + v8::NewStringType::kNormal, static_cast(length)) + .ToLocalChecked()); +} + +size_t Value::copyUtf16(Runtime& runtime, char16_t* buffer, size_t capacity) const { + v8::Local string = local(runtime).As(); + size_t length = static_cast(string->Length()); + string->WriteV2(runtime.isolate(), 0, static_cast(std::min(length, capacity)), + reinterpret_cast(buffer), v8::String::WriteFlags::kNone); + return length; +} + BigInt Value::getBigInt(Runtime& runtime) const { return BigInt(runtime, local(runtime).As()); } diff --git a/vendor/jsc/jsc-api.cpp b/vendor/jsc/jsc-api.cpp index f2839d2a1..6e41403ae 100644 --- a/vendor/jsc/jsc-api.cpp +++ b/vendor/jsc/jsc-api.cpp @@ -88,7 +88,7 @@ class JSString { size_t length{JSStringGetLength(_string)}; const JSChar* chars{JSStringGetCharactersPtr(_string)}; size_t size{std::min(length, bufsize - 1)}; - std::memcpy(buf, chars, size); + std::memcpy(buf, chars, size * sizeof(JSChar)); buf[size] = 0; if (result != nullptr) { *result = size;