Skip to content

Emoji in menu labels are garbled on Windows and break the whole menu on Linux (Modified UTF-8 from GetStringUTFChars) #441

Description

@xchacha20-poly1305

Description

Menu item labels that contain characters outside the Basic Multilingual Plane (most emoji, e.g. 🇺🇸, 🚀) are not handled correctly by the native bridges:

  • Windows: the emoji is shown as garbage characters. The rest of the label and other items are fine.
  • Linux (GNOME + gnome-shell-extension-appindicator): the tray menu does not open at all as long as any item label contains an emoji.

Characters inside the BMP, such as CJK text, work correctly on both platforms.

Version: dev.nucleusframework:composenativetray 2.1.6

Reproduction

Tray(
    icon = painterResource(Res.drawable.icon),
    tooltip = "Demo",
    menuContent = {
        Item(label = "🚀 Launch") {}
        Item(label = "Exit") {}
    },
)
  • Windows: open the tray menu. The first item shows mojibake instead of 🚀.
  • GNOME with the AppIndicator extension: click the tray icon. No menu appears.

Cause

The native bridges read Java strings with GetStringUTFChars. That function returns JNI's Modified UTF-8, not standard UTF-8: a supplementary character is encoded as its two UTF-16 surrogates, 3 bytes each (6 bytes in total), instead of one 4-byte sequence. The bytes are then passed on as if they were UTF-8.

Windows — src/native/windows/jni_bridge.c:

static char *jni_strdup(JNIEnv *env, jstring jstr) {
    const char *utf = (*env)->GetStringUTFChars(env, jstr, NULL);
    char *copy = _strdup(utf);
    ...
}

tray_windows.c converts the result with MultiByteToWideChar(CP_UTF8, ...), which treats the surrogate byte sequences as invalid and emits replacement characters. The same helper is used for the tooltip and the icon path, so an emoji in the tooltip or a non-BMP character in the path would also be affected.

Linux — src/native/linux/jni_bridge.c passes the GetStringUTFChars result straight to sni.c. In append_menu_layout, sd_bus_message_append(reply, "{sv}", "label", "s", item->label) fails with -EINVAL, because sd-bus validates that s values are valid UTF-8. menu_get_layout then returns that error for the entire GetLayout call, so the host receives no layout and shows no menu, not just a broken item. Title and tooltip go through the same path.

macOS — src/native/macos/MacTrayBridge.m also uses GetStringUTFChars. I have not tested it, but it has the same kind of input.

Suggested fix

Stop using GetStringUTFChars for text and produce standard UTF-8 instead. Two possible approaches:

  1. In native code, read UTF-16 with GetStringChars / GetStringLength and convert it:
    • Windows: pass it to the W APIs directly, or use WideCharToMultiByte(CP_UTF8, ...).
    • Linux / macOS: a small shared UTF-16 → UTF-8 helper, or [NSString stringWithCharacters:length:] on macOS.
  2. On the Kotlin side, pass text.encodeToByteArray() (with a trailing NUL) as ByteArray to the native methods, so every platform receives standard UTF-8 without extra native conversion code.

Environment

  • ComposeNativeTray 2.1.6
  • Windows: Windows 11
  • Linux: GNOME 50.5, gnome-shell-extension-appindicator 65

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions