From 1b3f9eac3da2cf5108a7afecd86ad088361887f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=89=E5=AE=B9?= Date: Sat, 19 Sep 2026 16:51:54 +0800 Subject: [PATCH] fix(jni): hand the native bridges standard UTF-8, not modified UTF-8 (#441) GetStringUTFChars returns JNI's modified UTF-8, where a supplementary character arrives as its two UTF-16 surrogates encoded separately. All three bridges passed that straight to consumers that expect real UTF-8: MultiByteToWideChar on Windows turned an emoji label into mojibake, and sd-bus rejected the GetLayout reply with -EINVAL on Linux, which cost the whole menu rather than one item. Replace the per-bridge string handling with a shared jni_utf8_dup, which reads UTF-16 via GetStringChars and encodes it itself, mapping unpaired surrogates to U+FFFD so the result is always valid UTF-8. Labels, tooltips, titles, icon paths and shortcuts all go through it. Verified on GNOME: with the previous library GetLayout failed with -EINVAL for an emoji label; it now returns the layout with correct UTF-8. Co-Authored-By: Claude Opus 5 --- .github/workflows/build-natives.yaml | 4 + src/native/linux/build.sh | 1 + src/native/linux/jni_bridge.c | 60 +++++++-------- src/native/macos/MacTrayBridge.m | 47 +++--------- src/native/macos/build.sh | 1 + src/native/shared/jni_utf8.h | 87 +++++++++++++++++++++ src/native/shared/run_jni_utf8_test.sh | 38 ++++++++++ src/native/shared/test_jni_utf8.c | 101 +++++++++++++++++++++++++ src/native/windows/CMakeLists.txt | 3 + src/native/windows/jni_bridge.c | 26 ++----- 10 files changed, 283 insertions(+), 85 deletions(-) create mode 100644 src/native/shared/jni_utf8.h create mode 100755 src/native/shared/run_jni_utf8_test.sh create mode 100644 src/native/shared/test_jni_utf8.c diff --git a/.github/workflows/build-natives.yaml b/.github/workflows/build-natives.yaml index 585ef6ee..59fb6581 100644 --- a/.github/workflows/build-natives.yaml +++ b/.github/workflows/build-natives.yaml @@ -82,6 +82,10 @@ jobs: working-directory: src/native/linux run: bash run_pixmap_test.sh + - name: JNI strings are standard UTF-8 (issue #441) + working-directory: src/native/shared + run: bash run_jni_utf8_test.sh + - name: Upload Linux library uses: actions/upload-artifact@v4 with: diff --git a/src/native/linux/build.sh b/src/native/linux/build.sh index b6908bb4..0b8aa44d 100755 --- a/src/native/linux/build.sh +++ b/src/native/linux/build.sh @@ -64,6 +64,7 @@ echo "Compiling jni_bridge.c..." gcc -c -o "$SCRIPT_DIR/jni_bridge.o" \ -fPIC -O2 -Wall -Wextra -Wno-unused-parameter \ -I "$SCRIPT_DIR" \ + -I "$SCRIPT_DIR/../shared" \ -I "$JNI_INCLUDE" \ -I "$JNI_INCLUDE_LINUX" \ "$SCRIPT_DIR/jni_bridge.c" diff --git a/src/native/linux/jni_bridge.c b/src/native/linux/jni_bridge.c index 07338cad..5c813cfb 100644 --- a/src/native/linux/jni_bridge.c +++ b/src/native/linux/jni_bridge.c @@ -17,6 +17,7 @@ #include #include +#include "jni_utf8.h" #include "sni.h" /* ========================================================================== */ @@ -166,10 +167,7 @@ Java_dev_nucleusframework_composenativetray_lib_linux_LinuxNativeBridge_nativeCr { (void)clazz; - const char *tip = NULL; - if (tooltip) { - tip = (*env)->GetStringUTFChars(env, tooltip, NULL); - } + char *tip = jni_utf8_dup(env, tooltip); const uint8_t *icon_data = NULL; jsize icon_len = 0; @@ -183,7 +181,7 @@ Java_dev_nucleusframework_composenativetray_lib_linux_LinuxNativeBridge_nativeCr sni_tray *tray = sni_tray_create(icon_data, (size_t)icon_len, tip); if (icon_buf) (*env)->ReleaseByteArrayElements(env, iconBytes, icon_buf, JNI_ABORT); - if (tip) (*env)->ReleaseStringUTFChars(env, tooltip, tip); + free(tip); return (jlong)(uintptr_t)tray; } @@ -249,9 +247,9 @@ Java_dev_nucleusframework_composenativetray_lib_linux_LinuxNativeBridge_nativeSe (void)clazz; sni_tray *tray = (sni_tray *)(uintptr_t)handle; if (!tray) return; - const char *utf = title ? (*env)->GetStringUTFChars(env, title, NULL) : NULL; - sni_tray_set_title(tray, utf); - if (utf) (*env)->ReleaseStringUTFChars(env, title, utf); + char *t = jni_utf8_dup(env, title); + sni_tray_set_title(tray, t); + free(t); } JNIEXPORT void JNICALL @@ -261,9 +259,9 @@ Java_dev_nucleusframework_composenativetray_lib_linux_LinuxNativeBridge_nativeSe (void)clazz; sni_tray *tray = (sni_tray *)(uintptr_t)handle; if (!tray) return; - const char *utf = tooltip ? (*env)->GetStringUTFChars(env, tooltip, NULL) : NULL; - sni_tray_set_tooltip(tray, utf); - if (utf) (*env)->ReleaseStringUTFChars(env, tooltip, utf); + char *tt = jni_utf8_dup(env, tooltip); + sni_tray_set_tooltip(tray, tt); + free(tt); } /* ── Callbacks ──────────────────────────────────────────────────────── */ @@ -358,11 +356,11 @@ Java_dev_nucleusframework_composenativetray_lib_linux_LinuxNativeBridge_nativeAd (void)clazz; sni_tray *tray = (sni_tray *)(uintptr_t)handle; if (!tray) return 0; - const char *t = title ? (*env)->GetStringUTFChars(env, title, NULL) : NULL; - const char *tt = tooltip ? (*env)->GetStringUTFChars(env, tooltip, NULL) : NULL; + char *t = jni_utf8_dup(env, title); + char *tt = jni_utf8_dup(env, tooltip); uint32_t id = sni_tray_add_menu_item(tray, t, tt); - if (t) (*env)->ReleaseStringUTFChars(env, title, t); - if (tt) (*env)->ReleaseStringUTFChars(env, tooltip, tt); + free(t); + free(tt); return (jint)id; } @@ -373,11 +371,11 @@ Java_dev_nucleusframework_composenativetray_lib_linux_LinuxNativeBridge_nativeAd (void)clazz; sni_tray *tray = (sni_tray *)(uintptr_t)handle; if (!tray) return 0; - const char *t = title ? (*env)->GetStringUTFChars(env, title, NULL) : NULL; - const char *tt = tooltip ? (*env)->GetStringUTFChars(env, tooltip, NULL) : NULL; + char *t = jni_utf8_dup(env, title); + char *tt = jni_utf8_dup(env, tooltip); uint32_t id = sni_tray_add_menu_item_checkbox(tray, t, tt, checked ? 1 : 0); - if (t) (*env)->ReleaseStringUTFChars(env, title, t); - if (tt) (*env)->ReleaseStringUTFChars(env, tooltip, tt); + free(t); + free(tt); return (jint)id; } @@ -398,11 +396,11 @@ Java_dev_nucleusframework_composenativetray_lib_linux_LinuxNativeBridge_nativeAd (void)clazz; sni_tray *tray = (sni_tray *)(uintptr_t)handle; if (!tray) return 0; - const char *t = title ? (*env)->GetStringUTFChars(env, title, NULL) : NULL; - const char *tt = tooltip ? (*env)->GetStringUTFChars(env, tooltip, NULL) : NULL; + char *t = jni_utf8_dup(env, title); + char *tt = jni_utf8_dup(env, tooltip); uint32_t id = sni_tray_add_sub_menu_item(tray, (uint32_t)parentId, t, tt); - if (t) (*env)->ReleaseStringUTFChars(env, title, t); - if (tt) (*env)->ReleaseStringUTFChars(env, tooltip, tt); + free(t); + free(tt); return (jint)id; } @@ -414,11 +412,11 @@ Java_dev_nucleusframework_composenativetray_lib_linux_LinuxNativeBridge_nativeAd (void)clazz; sni_tray *tray = (sni_tray *)(uintptr_t)handle; if (!tray) return 0; - const char *t = title ? (*env)->GetStringUTFChars(env, title, NULL) : NULL; - const char *tt = tooltip ? (*env)->GetStringUTFChars(env, tooltip, NULL) : NULL; + char *t = jni_utf8_dup(env, title); + char *tt = jni_utf8_dup(env, tooltip); uint32_t id = sni_tray_add_sub_menu_item_checkbox(tray, (uint32_t)parentId, t, tt, checked ? 1 : 0); - if (t) (*env)->ReleaseStringUTFChars(env, title, t); - if (tt) (*env)->ReleaseStringUTFChars(env, tooltip, tt); + free(t); + free(tt); return (jint)id; } @@ -440,9 +438,9 @@ Java_dev_nucleusframework_composenativetray_lib_linux_LinuxNativeBridge_nativeIt (void)clazz; sni_tray *tray = (sni_tray *)(uintptr_t)handle; if (!tray) return 0; - const char *t = title ? (*env)->GetStringUTFChars(env, title, NULL) : NULL; + char *t = jni_utf8_dup(env, title); int ok = sni_tray_item_set_title(tray, (uint32_t)id, t); - if (t) (*env)->ReleaseStringUTFChars(env, title, t); + free(t); return (jint)ok; } @@ -521,11 +519,11 @@ Java_dev_nucleusframework_composenativetray_lib_linux_LinuxNativeBridge_nativeIt (void)clazz; sni_tray *tray = (sni_tray *)(uintptr_t)handle; if (!tray) return; - const char *k = key ? (*env)->GetStringUTFChars(env, key, NULL) : NULL; + char *k = jni_utf8_dup(env, key); sni_tray_item_set_shortcut(tray, (uint32_t)id, k, ctrl ? 1 : 0, shift ? 1 : 0, alt ? 1 : 0, superMod ? 1 : 0); - if (k) (*env)->ReleaseStringUTFChars(env, key, k); + free(k); } /* ========================================================================== */ diff --git a/src/native/macos/MacTrayBridge.m b/src/native/macos/MacTrayBridge.m index a2c7e13d..c9cce5d3 100644 --- a/src/native/macos/MacTrayBridge.m +++ b/src/native/macos/MacTrayBridge.m @@ -11,6 +11,7 @@ #import #import #import +#include "jni_utf8.h" #include "tray.h" /* ========================================================================== */ @@ -214,13 +215,8 @@ JNIEXPORT jlong JNICALL Java_dev_nucleusframework_composenativetray_lib_mac_MacN struct tray *t = (struct tray *)calloc(1, sizeof(struct tray)); if (!t) return 0; - const char *iconUtf = (*env)->GetStringUTFChars(env, iconPath, NULL); - t->icon_filepath = strdup(iconUtf); - (*env)->ReleaseStringUTFChars(env, iconPath, iconUtf); - - const char *tooltipUtf = (*env)->GetStringUTFChars(env, tooltip, NULL); - t->tooltip = strdup(tooltipUtf); - (*env)->ReleaseStringUTFChars(env, tooltip, tooltipUtf); + t->icon_filepath = jni_utf8_dup(env, iconPath); + t->tooltip = jni_utf8_dup(env, tooltip); t->menu = NULL; t->cb = NULL; @@ -246,9 +242,7 @@ JNIEXPORT void JNICALL Java_dev_nucleusframework_composenativetray_lib_mac_MacNa struct tray *t = (struct tray *)(uintptr_t)handle; if (!t) return; free((void *)t->icon_filepath); - const char *utf = (*env)->GetStringUTFChars(env, iconPath, NULL); - t->icon_filepath = strdup(utf); - (*env)->ReleaseStringUTFChars(env, iconPath, utf); + t->icon_filepath = jni_utf8_dup(env, iconPath); } JNIEXPORT void JNICALL Java_dev_nucleusframework_composenativetray_lib_mac_MacNativeBridge_nativeSetTrayTooltip( @@ -258,9 +252,7 @@ JNIEXPORT void JNICALL Java_dev_nucleusframework_composenativetray_lib_mac_MacNa struct tray *t = (struct tray *)(uintptr_t)handle; if (!t) return; free((void *)t->tooltip); - const char *utf = (*env)->GetStringUTFChars(env, tooltip, NULL); - t->tooltip = strdup(utf); - (*env)->ReleaseStringUTFChars(env, tooltip, utf); + t->tooltip = jni_utf8_dup(env, tooltip); } JNIEXPORT void JNICALL Java_dev_nucleusframework_composenativetray_lib_mac_MacNativeBridge_nativeSetTrayCallback( @@ -378,17 +370,8 @@ JNIEXPORT void JNICALL Java_dev_nucleusframework_composenativetray_lib_mac_MacNa free((void *)item->text); free((void *)item->icon_filepath); - const char *textUtf = (*env)->GetStringUTFChars(env, text, NULL); - item->text = strdup(textUtf); - (*env)->ReleaseStringUTFChars(env, text, textUtf); - - if (iconPath != NULL) { - const char *iconUtf = (*env)->GetStringUTFChars(env, iconPath, NULL); - item->icon_filepath = strdup(iconUtf); - (*env)->ReleaseStringUTFChars(env, iconPath, iconUtf); - } else { - item->icon_filepath = NULL; - } + item->text = jni_utf8_dup(env, text); + item->icon_filepath = jni_utf8_dup(env, iconPath); item->disabled = (int)disabled; item->checked = (int)checked; @@ -416,13 +399,7 @@ JNIEXPORT void JNICALL Java_dev_nucleusframework_composenativetray_lib_mac_MacNa free((void *)item->key_equivalent); - if (keyEquivalent != NULL) { - const char *utf = (*env)->GetStringUTFChars(env, keyEquivalent, NULL); - item->key_equivalent = strdup(utf); - (*env)->ReleaseStringUTFChars(env, keyEquivalent, utf); - } else { - item->key_equivalent = NULL; - } + item->key_equivalent = jni_utf8_dup(env, keyEquivalent); item->key_equivalent_mod_mask = (unsigned long)modifierMask; } @@ -541,11 +518,11 @@ JNIEXPORT void JNICALL Java_dev_nucleusframework_composenativetray_lib_mac_MacNa (void)clazz; struct tray *t = (struct tray *)(uintptr_t)handle; if (!t) return; - const char *lightUtf = (*env)->GetStringUTFChars(env, lightIcon, NULL); - const char *darkUtf = (*env)->GetStringUTFChars(env, darkIcon, NULL); + char *lightUtf = jni_utf8_dup(env, lightIcon); + char *darkUtf = jni_utf8_dup(env, darkIcon); tray_set_icons_for_appearance(t, lightUtf, darkUtf); - (*env)->ReleaseStringUTFChars(env, lightIcon, lightUtf); - (*env)->ReleaseStringUTFChars(env, darkIcon, darkUtf); + free(lightUtf); + free(darkUtf); } /* ========================================================================== */ diff --git a/src/native/macos/build.sh b/src/native/macos/build.sh index 2389c9d5..a61d4187 100755 --- a/src/native/macos/build.sh +++ b/src/native/macos/build.sh @@ -44,6 +44,7 @@ build_arch() { -I "$JNI_INCLUDE" \ -I "$JNI_INCLUDE_DARWIN" \ -I "$SCRIPT_DIR" \ + -I "$SCRIPT_DIR/../shared" \ -fobjc-arc \ "$SCRIPT_DIR/MacTrayBridge.m" diff --git a/src/native/shared/jni_utf8.h b/src/native/shared/jni_utf8.h new file mode 100644 index 00000000..26e43078 --- /dev/null +++ b/src/native/shared/jni_utf8.h @@ -0,0 +1,87 @@ +/* + * jni_utf8.h – jstring to standard UTF-8 conversion, shared by all JNI bridges. + * + * GetStringUTFChars hands back JNI's "modified UTF-8", not standard UTF-8: a + * supplementary character (emoji, rare CJK, ...) comes out as its two UTF-16 + * surrogates encoded separately, 3 bytes each, and U+0000 comes out as 0xC0 0x80. + * Native consumers that expect real UTF-8 then break: MultiByteToWideChar on + * Windows substitutes replacement characters, and sd-bus on Linux rejects the + * whole message with -EINVAL, which costs the entire menu, not just one label. + * + * Every bridge therefore reads UTF-16 with GetStringChars and encodes it here. + */ + +#ifndef COMPOSENATIVETRAY_JNI_UTF8_H +#define COMPOSENATIVETRAY_JNI_UTF8_H + +#include +#include +#include +#include + +/* Encodes one code point into at most 4 bytes; returns the number written. */ +static inline size_t jni_utf8_encode(uint32_t cp, char *out) { + if (cp < 0x80) { + out[0] = (char)cp; + return 1; + } + if (cp < 0x800) { + out[0] = (char)(0xC0 | (cp >> 6)); + out[1] = (char)(0x80 | (cp & 0x3F)); + return 2; + } + if (cp < 0x10000) { + out[0] = (char)(0xE0 | (cp >> 12)); + out[1] = (char)(0x80 | ((cp >> 6) & 0x3F)); + out[2] = (char)(0x80 | (cp & 0x3F)); + return 3; + } + out[0] = (char)(0xF0 | (cp >> 18)); + out[1] = (char)(0x80 | ((cp >> 12) & 0x3F)); + out[2] = (char)(0x80 | ((cp >> 6) & 0x3F)); + out[3] = (char)(0x80 | (cp & 0x3F)); + return 4; +} + +/* + * Returns a freshly allocated, NUL-terminated standard UTF-8 copy of jstr, or + * NULL when jstr is NULL or the allocation fails. Free it with free(). + * + * Unpaired surrogates cannot be represented in UTF-8 and become U+FFFD, so the + * result is always valid UTF-8 whatever the Java string contains. An embedded + * U+0000 truncates the copy, since the consumers are all C string APIs. + */ +static inline char *jni_utf8_dup(JNIEnv *env, jstring jstr) { + if (!jstr) return NULL; + + const jchar *utf16 = (*env)->GetStringChars(env, jstr, NULL); + if (!utf16) return NULL; + jsize units = (*env)->GetStringLength(env, jstr); + + /* Three bytes per UTF-16 unit is the worst case: a surrogate pair is two + * units and costs four bytes, every other unit costs at most three. */ + char *out = (char *)malloc((size_t)units * 3 + 1); + if (!out) { + (*env)->ReleaseStringChars(env, jstr, utf16); + return NULL; + } + + size_t written = 0; + for (jsize i = 0; i < units; i++) { + uint32_t cp = utf16[i]; + if (cp >= 0xD800 && cp <= 0xDBFF && i + 1 < units && + utf16[i + 1] >= 0xDC00 && utf16[i + 1] <= 0xDFFF) { + cp = 0x10000 + ((cp - 0xD800) << 10) + (utf16[++i] - 0xDC00); + } else if (cp >= 0xD800 && cp <= 0xDFFF) { + cp = 0xFFFD; + } + if (cp == 0) break; + written += jni_utf8_encode(cp, out + written); + } + out[written] = '\0'; + + (*env)->ReleaseStringChars(env, jstr, utf16); + return out; +} + +#endif /* COMPOSENATIVETRAY_JNI_UTF8_H */ diff --git a/src/native/shared/run_jni_utf8_test.sh b/src/native/shared/run_jni_utf8_test.sh new file mode 100755 index 00000000..17012c0d --- /dev/null +++ b/src/native/shared/run_jni_utf8_test.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# Build and run the issue #441 modified-UTF-8 regression test. +# Header-only: needs jni.h, nothing else. + +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +BIN="$SCRIPT_DIR/test_jni_utf8" + +if [ -z "${JAVA_HOME:-}" ]; then + for jdk in /usr/lib/jvm/java-*-openjdk-amd64 /usr/lib/jvm/java-*-openjdk /usr/lib/jvm/default-java /usr/lib/jvm/default; do + if [ -f "$jdk/include/jni.h" ]; then + JAVA_HOME="$jdk" + break + fi + done +fi +if [ -z "${JAVA_HOME:-}" ] || [ ! -f "$JAVA_HOME/include/jni.h" ]; then + echo "ERROR: JAVA_HOME not found or jni.h missing. Install a JDK or set JAVA_HOME." + exit 1 +fi + +case "$(uname -s)" in + Darwin) JNI_MD_INCLUDE="$JAVA_HOME/include/darwin" ;; + *) JNI_MD_INCLUDE="$JAVA_HOME/include/linux" ;; +esac + +echo "Compiling modified-UTF-8 test..." +cc -O2 -g -Wall -Wextra -Werror \ + -I "$SCRIPT_DIR" \ + -I "$JAVA_HOME/include" \ + -I "$JNI_MD_INCLUDE" \ + "$SCRIPT_DIR/test_jni_utf8.c" \ + -o "$BIN" + +"$BIN" +status=$? +rm -f "$BIN" +exit $status diff --git a/src/native/shared/test_jni_utf8.c b/src/native/shared/test_jni_utf8.c new file mode 100644 index 00000000..70cf3758 --- /dev/null +++ b/src/native/shared/test_jni_utf8.c @@ -0,0 +1,101 @@ +/* + * test_jni_utf8.c – regression test for issue #441. + * + * jni_utf8_dup must produce standard UTF-8, not JNI's modified UTF-8: a + * supplementary character has to come out as one 4-byte sequence, not as two + * 3-byte surrogate sequences. Windows renders the latter as mojibake and + * sd-bus rejects it outright, which drops the whole Linux menu. + * + * Runs without a JVM: a stub JNIEnv serves UTF-16 straight from the test. + */ + +#include "jni_utf8.h" + +#include +#include + +/* ── Stub JNIEnv: a jstring is just a NUL-terminated jchar array ─────────── */ + +static jsize stub_GetStringLength(JNIEnv *env, jstring str) { + (void)env; + const jchar *s = (const jchar *)str; + jsize n = 0; + while (s[n]) n++; + return n; +} + +static const jchar *stub_GetStringChars(JNIEnv *env, jstring str, jboolean *isCopy) { + (void)env; + if (isCopy) *isCopy = JNI_FALSE; + return (const jchar *)str; +} + +static void stub_ReleaseStringChars(JNIEnv *env, jstring str, const jchar *chars) { + (void)env; (void)str; (void)chars; +} + +static JNIEnv *make_stub_env(struct JNINativeInterface_ *fns) { + static JNIEnv env; + memset(fns, 0, sizeof(*fns)); + fns->GetStringLength = stub_GetStringLength; + fns->GetStringChars = stub_GetStringChars; + fns->ReleaseStringChars = stub_ReleaseStringChars; + env = fns; + return &env; +} + +/* ── Cases ──────────────────────────────────────────────────────────────── */ + +static int failures = 0; + +static void expect(JNIEnv *env, const char *what, const jchar *utf16, const char *expected) { + char *got = jni_utf8_dup(env, (jstring)utf16); + if (!got || strcmp(got, expected) != 0) { + fprintf(stderr, "FAIL: %s → \"%s\", expected \"%s\"\n", + what, got ? got : "(null)", expected); + failures++; + } + free(got); +} + +int main(void) { + struct JNINativeInterface_ fns; + JNIEnv *env = make_stub_env(&fns); + + /* "Exit" – plain ASCII */ + const jchar ascii[] = { 'E', 'x', 'i', 't', 0 }; + expect(env, "ASCII", ascii, "Exit"); + + /* "退出" – BMP, three bytes each; worked before the fix too */ + const jchar cjk[] = { 0x9000, 0x51FA, 0 }; + expect(env, "CJK", cjk, "\xE9\x80\x80\xE5\x87\xBA"); + + /* "🚀 Launch" – U+1F680 is a surrogate pair and must become F0 9F 9A 80 */ + const jchar rocket[] = { 0xD83D, 0xDE80, ' ', 'L', 'a', 'u', 'n', 'c', 'h', 0 }; + expect(env, "supplementary", rocket, "\xF0\x9F\x9A\x80 Launch"); + + /* U+20BB7 then U+1F525 – two consecutive surrogate pairs */ + const jchar pairs[] = { 0xD842, 0xDFB7, 0xD83D, 0xDD25, 0 }; + expect(env, "consecutive pairs", pairs, "\xF0\xA0\xAE\xB7\xF0\x9F\x94\xA5"); + + /* A lone high surrogate has no UTF-8 form and becomes U+FFFD */ + const jchar lone_high[] = { 0xD83D, 'x', 0 }; + expect(env, "unpaired high surrogate", lone_high, "\xEF\xBF\xBDx"); + + /* A lone low surrogate likewise */ + const jchar lone_low[] = { 'x', 0xDE80, 0 }; + expect(env, "unpaired low surrogate", lone_low, "x\xEF\xBF\xBD"); + + /* U+00E9 and U+20AC cover the two- and three-byte boundaries */ + const jchar mixed[] = { 0x00E9, 0x20AC, 0 }; + expect(env, "two/three byte", mixed, "\xC3\xA9\xE2\x82\xAC"); + + if (jni_utf8_dup(env, NULL) != NULL) { + fprintf(stderr, "FAIL: NULL jstring did not map to NULL\n"); + failures++; + } + + if (failures) return 1; + printf("PASS: jni_utf8_dup emits standard UTF-8 for all cases\n"); + return 0; +} diff --git a/src/native/windows/CMakeLists.txt b/src/native/windows/CMakeLists.txt index eb10ca36..2c86e82e 100644 --- a/src/native/windows/CMakeLists.txt +++ b/src/native/windows/CMakeLists.txt @@ -37,6 +37,9 @@ file(MAKE_DIRECTORY ${OUTPUT_DIR}) find_package(JNI REQUIRED) include_directories(${JNI_INCLUDE_DIRS}) +# Shared helpers used by every platform bridge (jni_utf8.h) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../shared) + # Add sources list(APPEND SRCS ${CMAKE_CURRENT_SOURCE_DIR}/tray_windows.c diff --git a/src/native/windows/jni_bridge.c b/src/native/windows/jni_bridge.c index c141a6a6..62806791 100644 --- a/src/native/windows/jni_bridge.c +++ b/src/native/windows/jni_bridge.c @@ -17,6 +17,7 @@ #include #include +#include "jni_utf8.h" #include "tray.h" /* ========================================================================== */ @@ -144,19 +145,6 @@ static void menu_opened_cb_trampoline(struct tray *t) { if (runnable) invokeRunnable(runnable); } -/* ========================================================================== */ -/* Helper: duplicate UTF-8 string from JNI */ -/* ========================================================================== */ - -static char *jni_strdup(JNIEnv *env, jstring jstr) { - if (!jstr) return NULL; - const char *utf = (*env)->GetStringUTFChars(env, jstr, NULL); - if (!utf) return NULL; - char *copy = _strdup(utf); - (*env)->ReleaseStringUTFChars(env, jstr, utf); - return copy; -} - /* ========================================================================== */ /* JNI exports: Tray lifecycle */ /* ========================================================================== */ @@ -168,8 +156,8 @@ Java_dev_nucleusframework_composenativetray_lib_windows_WindowsNativeBridge_nati (void)clazz; struct tray *t = (struct tray *)calloc(1, sizeof(struct tray)); if (!t) return 0; - t->icon_filepath = jni_strdup(env, iconPath); - t->tooltip = jni_strdup(env, tooltip); + t->icon_filepath = jni_utf8_dup(env, iconPath); + t->tooltip = jni_utf8_dup(env, tooltip); t->cb = NULL; t->menu = NULL; return (jlong)(uintptr_t)t; @@ -201,7 +189,7 @@ Java_dev_nucleusframework_composenativetray_lib_windows_WindowsNativeBridge_nati struct tray *t = (struct tray *)(uintptr_t)handle; if (!t) return; free((void *)t->icon_filepath); - t->icon_filepath = jni_strdup(env, iconPath); + t->icon_filepath = jni_utf8_dup(env, iconPath); } JNIEXPORT void JNICALL @@ -212,7 +200,7 @@ Java_dev_nucleusframework_composenativetray_lib_windows_WindowsNativeBridge_nati struct tray *t = (struct tray *)(uintptr_t)handle; if (!t) return; free((void *)t->tooltip); - t->tooltip = jni_strdup(env, tooltip); + t->tooltip = jni_utf8_dup(env, tooltip); } JNIEXPORT void JNICALL @@ -322,8 +310,8 @@ Java_dev_nucleusframework_composenativetray_lib_windows_WindowsNativeBridge_nati free(item->text); free(item->icon_path); - item->text = jni_strdup(env, text); - item->icon_path = jni_strdup(env, iconPath); + item->text = jni_utf8_dup(env, text); + item->icon_path = jni_utf8_dup(env, iconPath); item->disabled = (int)disabled; item->checked = (int)checked; item->cb = NULL;