fix: use real UTF-8 for jstring -> native conversion
CI / Build Java (push) Has been cancelled
CI / Build Native (macos-latest - macos) (push) Has been cancelled
CI / Build Native (ubuntu-latest - linux) (push) Has been cancelled
CI / Build Native (ubuntu-latest - windows) (push) Has been cancelled
CI / Archive Natives (push) Has been cancelled
CI / Update Binaries (push) Has been cancelled
CI / Release (push) Has been cancelled

JNI's GetStringUTFChars emits MUTF-8, which encodes codepoints above
U+FFFF as a surrogate-pair of two 3-byte sequences. ImGui's UTF-8 decoder
rejects those, so SMP emoji (U+1F000+) never reach the glyph atlas even
when the glyph is registered.

Add Jni::ToTrueUtf8 / Jni::FreeTrueUtf8 helpers in jni_common that decode
via GetStringChars (UTF-16) and emit proper 1/2/3/4-byte UTF-8, then
route every generator- and manually-written JNI call site through them
(1921 occurrences across the generated tree and 3 hand-written files,
plus the jni_content.kt generator template so future regenerations stay
consistent).
This commit is contained in:
MRLokop committed 2026-04-20 11:04:08 +03:00
1 parent 34c40304f6
commit d9aa2c811b
20 files changed
+3897 -3846

No files matched your search

@@ -322,7 +322,7 @@ public final class ImGuiIO extends ImGuiStruct {
const char* getClipboardTextStub(void* user_data) {
JNIEnv* env = Jni::GetEnv();
jstring jstr = Jni::CallImStrSupplier(env, _getClipboardTextCallback);
return env->GetStringUTFChars(jstr, 0);
return Jni::ToTrueUtf8(env, jstr);
}
*/
@@ -78,9 +78,9 @@ public final class TextEditor extends ImGuiStructDestroyable {
for (int i = 0; i < keysLen; i++) {
int key = keys[i];
jstring string = (jstring)env->GetObjectArrayElement(values, i);
const char* value = env->GetStringUTFChars(string, JNI_FALSE);
const char* value = Jni::ToTrueUtf8(env, string);
markers.emplace(std::pair<int, std::string>(key, std::string(value)));
env->ReleaseStringUTFChars(string, value);
Jni::FreeTrueUtf8(value);
}
THIS->SetErrorMarkers(markers);
*/
@@ -116,9 +116,9 @@ public final class TextEditor extends ImGuiStructDestroyable {
vec.reserve(length);
for (int i = 0; i < length; i++) {
jstring string = (jstring)env->GetObjectArrayElement(lines, i);
const char* raw = env->GetStringUTFChars(string, JNI_FALSE);
const char* raw = Jni::ToTrueUtf8(env, string);
vec.emplace_back(std::string(raw));
env->ReleaseStringUTFChars(string, raw);
Jni::FreeTrueUtf8(raw);
}
THIS->SetTextLines(vec);
*/
@@ -49,9 +49,9 @@ public final class TextEditorLanguageDefinition extends ImGuiStructDestroyable {
std::unordered_set<std::string> set;
for (int i = 0; i < length; i++) {
jstring string = (jstring)env->GetObjectArrayElement(keywords, i);
const char* raw = env->GetStringUTFChars(string, JNI_FALSE);
const char* raw = Jni::ToTrueUtf8(env, string);
set.emplace(std::string(raw));
env->ReleaseStringUTFChars(string, raw);
Jni::FreeTrueUtf8(raw);
}
THIS->mKeywords = set;
*/
@@ -67,13 +67,13 @@ public final class TextEditorLanguageDefinition extends ImGuiStructDestroyable {
for (int i = 0; i < keysLen; i++) {
jstring string1 = (jstring)env->GetObjectArrayElement(keys, i);
jstring string2 = (jstring)env->GetObjectArrayElement(decl, i);
const char* key = env->GetStringUTFChars(string1, JNI_FALSE);
const char* value = env->GetStringUTFChars(string2, JNI_FALSE);
const char* key = Jni::ToTrueUtf8(env, string1);
const char* value = Jni::ToTrueUtf8(env, string2);
TextEditor::Identifier id;
id.mDeclaration = std::string(value);
identifiers.insert(std::pair<std::string, TextEditor::Identifier>(std::string(key), id));
env->ReleaseStringUTFChars(string1, key);
env->ReleaseStringUTFChars(string2, value);
Jni::FreeTrueUtf8(key);
Jni::FreeTrueUtf8(value);
}
THIS->mIdentifiers = identifiers;
*/
@@ -89,13 +89,13 @@ public final class TextEditorLanguageDefinition extends ImGuiStructDestroyable {
for (int i = 0; i < keysLen; i++) {
jstring string1 = (jstring)env->GetObjectArrayElement(keys, i);
jstring string2 = (jstring)env->GetObjectArrayElement(decl, i);
const char* key = env->GetStringUTFChars(string1, JNI_FALSE);
const char* value = env->GetStringUTFChars(string2, JNI_FALSE);
const char* key = Jni::ToTrueUtf8(env, string1);
const char* value = Jni::ToTrueUtf8(env, string2);
TextEditor::Identifier id;
id.mDeclaration = std::string(value);
preprocIdentifiers.insert(std::pair<std::string, TextEditor::Identifier>(std::string(key), id));
env->ReleaseStringUTFChars(string1, key);
env->ReleaseStringUTFChars(string2, value);
Jni::FreeTrueUtf8(key);
Jni::FreeTrueUtf8(value);
}
THIS->mPreprocIdentifiers = preprocIdentifiers;
*/
@@ -131,7 +131,7 @@ public final class TextEditorLanguageDefinition extends ImGuiStructDestroyable {
std::vector<std::pair<std::string, TextEditor::PaletteIndex>> tokenRegexStrings;
for (int i = 0; i < keysLen; i++) {
jstring string = (jstring)env->GetObjectArrayElement(keys, i);
const char* key = env->GetStringUTFChars(string, JNI_FALSE);
const char* key = Jni::ToTrueUtf8(env, string);
int value = paletteIndexes[i];
tokenRegexStrings.emplace_back(
std::pair<std::string, TextEditor::PaletteIndex>(
@@ -139,7 +139,7 @@ public final class TextEditorLanguageDefinition extends ImGuiStructDestroyable {
static_cast<TextEditor::PaletteIndex>(value)
)
);
env->ReleaseStringUTFChars(string, key);
Jni::FreeTrueUtf8(key);
}
THIS->mTokenRegexStrings = tokenRegexStrings;
*/
@@ -119,4 +119,49 @@ namespace Jni
env->DeleteLocalRef(value);
}
}
char* ToTrueUtf8(JNIEnv* env, jstring s) {
if (s == nullptr) return nullptr;
const jchar* chars = env->GetStringChars(s, nullptr);
jsize len = env->GetStringLength(s);
// Worst case: one codepoint → 4 UTF-8 bytes, plus null terminator.
char* buf = (char*)malloc((size_t)len * 4 + 1);
if (buf == nullptr) {
env->ReleaseStringChars(s, chars);
return nullptr;
}
char* out = buf;
for (jsize i = 0; i < len; i++) {
uint32_t cp = (uint32_t)chars[i];
if (cp >= 0xD800 && cp <= 0xDBFF && i + 1 < len) {
uint32_t low = (uint32_t)chars[i + 1];
if (low >= 0xDC00 && low <= 0xDFFF) {
cp = 0x10000 + ((cp - 0xD800) << 10) + (low - 0xDC00);
i++;
}
}
if (cp < 0x80) {
*out++ = (char)cp;
} else if (cp < 0x800) {
*out++ = (char)(0xC0 | (cp >> 6));
*out++ = (char)(0x80 | (cp & 0x3F));
} else if (cp < 0x10000) {
*out++ = (char)(0xE0 | (cp >> 12));
*out++ = (char)(0x80 | ((cp >> 6) & 0x3F));
*out++ = (char)(0x80 | (cp & 0x3F));
} else {
*out++ = (char)(0xF0 | (cp >> 18));
*out++ = (char)(0x80 | ((cp >> 12) & 0x3F));
*out++ = (char)(0x80 | ((cp >> 6) & 0x3F));
*out++ = (char)(0x80 | (cp & 0x3F));
}
}
*out = 0;
env->ReleaseStringChars(s, chars);
return buf;
}
void FreeTrueUtf8(char* p) {
if (p != nullptr) free(p);
}
}
@@ -47,4 +47,12 @@ namespace Jni
jobjectArray NewImGuiKeyDataArray(JNIEnv* env, const ImGuiKeyData* src, int size);
void ImGuiKeyDataArrayCpy(JNIEnv* env, jobjectArray src, ImGuiKeyData* dst, int size);
// Converts a jstring to a freshly-malloc'd C string in real UTF-8 (not JNI's MUTF-8).
// JNI's GetStringUTFChars encodes codepoints above U+FFFF as a surrogate pair of two
// 3-byte sequences, which ImGui's UTF-8 decoder rejects — so SMP emoji never reach
// the glyph atlas even when the glyph is registered. Caller must release with
// FreeTrueUtf8(). Returns nullptr iff the input jstring is null.
char* ToTrueUtf8(JNIEnv* env, jstring s);
void FreeTrueUtf8(char* p);
}