diff --git a/ai-core/libs/v8/llama-v8-release.aar b/ai-core/libs/v8/llama-v8-release.aar index 86381579..06fb4742 100644 Binary files a/ai-core/libs/v8/llama-v8-release.aar and b/ai-core/libs/v8/llama-v8-release.aar differ diff --git a/ai-core/llama-impl/src/main/cpp/llama-android.cpp b/ai-core/llama-impl/src/main/cpp/llama-android.cpp index 3089b2e6..97535219 100644 --- a/ai-core/llama-impl/src/main/cpp/llama-android.cpp +++ b/ai-core/llama-impl/src/main/cpp/llama-android.cpp @@ -8,9 +8,8 @@ #include #include #include +#include #include "llama.h" -#include -#include #include "common.h" #define TAG "llama-android.cpp" @@ -87,24 +86,76 @@ static std::atomic g_n_ctx(4096); static std::atomic g_kv_cache_reuse(true); static std::vector g_cached_tokens; +// Converts standard UTF-8 to UTF-16. NewStringUTF() is unusable here because it +// expects modified UTF-8 (CESU-8), so 4-byte sequences such as emoji would mangle. +// Invalid bytes become '?' so a truncated sequence cannot corrupt the remainder. +// @param text NUL-terminated UTF-8, may be null +// @return a new local reference to the converted string static jstring new_jstring_utf8(JNIEnv *env, const char *text) { if (!text) { return env->NewStringUTF(""); } - try { - std::wstring_convert, char16_t> converter; - std::u16string u16 = converter.from_bytes(text); - return env->NewString(reinterpret_cast(u16.data()), - static_cast(u16.size())); - } catch (const std::range_error &) { - std::string sanitized; - sanitized.reserve(strlen(text)); - for (const unsigned char ch: std::string(text)) { - sanitized.push_back(ch < 0x80 ? static_cast(ch) : '?'); + const auto *bytes = reinterpret_cast(text); + std::u16string u16; + u16.reserve(strlen(text)); + + while (*bytes != 0x00) { + uint32_t cp; + int num; + + if ((bytes[0] & 0x80) == 0x00) { + // U+0000 to U+007F + cp = bytes[0]; + num = 1; + } else if ((bytes[0] & 0xE0) == 0xC0) { + // U+0080 to U+07FF + cp = bytes[0] & 0x1Fu; + num = 2; + } else if ((bytes[0] & 0xF0) == 0xE0) { + // U+0800 to U+FFFF + cp = bytes[0] & 0x0Fu; + num = 3; + } else if ((bytes[0] & 0xF8) == 0xF0) { + // U+10000 to U+10FFFF + cp = bytes[0] & 0x07u; + num = 4; + } else { + u16.push_back(u'?'); + bytes += 1; + continue; + } + + bool ok = true; + for (int i = 1; i < num; ++i) { + if ((bytes[i] & 0xC0) != 0x80) { + // Resync here; this also covers the terminator, so we never overrun. + ok = false; + num = i; + break; + } + cp = (cp << 6) | (bytes[i] & 0x3Fu); } - return env->NewStringUTF(sanitized.c_str()); + + if (!ok || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) { + u16.push_back(u'?'); + bytes += num; + continue; + } + + if (cp <= 0xFFFF) { + u16.push_back(static_cast(cp)); + } else { + // Split an astral code point into a UTF-16 surrogate pair. + cp -= 0x10000; + u16.push_back(static_cast(0xD800 + (cp >> 10))); + u16.push_back(static_cast(0xDC00 + (cp & 0x3FFu))); + } + bytes += num; } + + return env->NewString(reinterpret_cast(u16.data()), + static_cast(u16.size())); } extern "C" diff --git a/ai-core/scripts/rebuild-llama-aar.sh b/ai-core/scripts/rebuild-llama-aar.sh index 5f37d231..628b4119 100755 --- a/ai-core/scripts/rebuild-llama-aar.sh +++ b/ai-core/scripts/rebuild-llama-aar.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # -# Regenerate the prebuilt llama.cpp AAR consumed by ai-core-plugin. +# Regenerate the prebuilt llama.cpp AAR consumed by the ai-core plugin. # # You only need this after bumping the llama.cpp submodule (i.e. when your fork # is updated). A normal plugin build does NOT use this script — it consumes the @@ -14,6 +14,7 @@ set -euo pipefail # Run from the ai-core/ project root regardless of where it's invoked. cd "$(dirname "$0")/.." +# Destinations must match the paths build.gradle.kts declares as dependencies. AAR_DST="libs/v8/llama-v8-release.aar" AAR_SRC="llama-impl/build/outputs/aar/llama-impl-release.aar" API_DST="libs/llama-api.jar" @@ -25,7 +26,16 @@ git submodule update --init --recursive echo "==> Building :llama-impl (native lib) and :llama-api (interface jar)" ./gradlew :llama-impl:assembleRelease :llama-api:jar -echo "==> Copying artifacts into ai-core/libs" +# Fail loudly rather than copying a stale artifact from a previous run. +for src in "$AAR_SRC" "$API_SRC"; do + if [ ! -f "$src" ]; then + echo "error: expected build output is missing: $src" >&2 + exit 1 + fi +done + +echo "==> Copying artifacts into libs/" +mkdir -p "$(dirname "$AAR_DST")" "$(dirname "$API_DST")" cp "$AAR_SRC" "$AAR_DST" cp "$API_SRC" "$API_DST"