Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/build-natives.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions src/native/linux/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
60 changes: 29 additions & 31 deletions src/native/linux/jni_bridge.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include <stdint.h>
#include <dlfcn.h>

#include "jni_utf8.h"
#include "sni.h"

/* ========================================================================== */
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down Expand Up @@ -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
Expand All @@ -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 ──────────────────────────────────────────────────────── */
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
}

Expand All @@ -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;
}

Expand All @@ -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;
}

Expand All @@ -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;
}

Expand Down Expand Up @@ -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);
}

/* ========================================================================== */
Expand Down
47 changes: 12 additions & 35 deletions src/native/macos/MacTrayBridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#import <stdlib.h>
#import <objc/runtime.h>
#import <AppKit/AppKit.h>
#include "jni_utf8.h"
#include "tray.h"

/* ========================================================================== */
Expand Down Expand Up @@ -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;
Expand All @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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);
}

/* ========================================================================== */
Expand Down
1 change: 1 addition & 0 deletions src/native/macos/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
87 changes: 87 additions & 0 deletions src/native/shared/jni_utf8.h
Original file line number Diff line number Diff line change
@@ -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 <jni.h>
#include <stdlib.h>
#include <stddef.h>
#include <stdint.h>

/* 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 */
38 changes: 38 additions & 0 deletions src/native/shared/run_jni_utf8_test.sh
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading