Java 內部字串採用 UTF-16 (char[]),C/C++ 常用 `char*` (單位字節、平台編碼) 或 `wchar_t*` (寬字元、Windows 為 UTF-16、Linux 為 UTF-32)。JNI 提供 `jstring` 不透明型別,必須透過 JNI 函數在三種編碼間轉換。錯誤的編碼假設會導致亂碼、記憶體洩漏、甚至 JVM 崩潰。本文系統整理 JNI 字串處理的編碼模型、關鍵 API、C++ 封裝類別與實戰陷阱。
| 層級 | 型別 | 編碼 | 位元寬度 | 備註 |
|---|---|---|---|---|
| Java | String / char[] | UTF-16 | 16 bits / char | 補充字元佔 2 個 char (surrogate pair) |
| JNI | jstring | UTF-16 (內部) | 不透明 | 不可直接存取內容 |
| JNI API | GetStringChars | UTF-16 (jchar*) | 16 bits | 需 ReleaseStringChars |
| JNI API | GetStringUTFChars | Modified UTF-8 | 8 bits/byte | 需 ReleaseStringUTFChars,非標準 UTF-8 |
| C/C++ Windows | wchar_t* | UTF-16 | 16 bits | Windows API 標準 |
| C/C++ Linux | wchar_t* | UTF-32 | 32 bits | glibc 標準 |
| C/C++ 通用 | char* | Locale 相關 (CP_ACP/UTF-8) | 8 bits | 需明確指定編碼 |
// 取得長度 (字元數,非字節數)
jsize GetStringLength(JNIEnv*, jstring); // UTF-16 字元數
jsize GetStringUTFLength(JNIEnv*, jstring); // Modified UTF-8 字節數
// 取得內容指標 (需配對 Release)
const jchar* GetStringChars(JNIEnv*, jstring, jboolean* isCopy);
const char* GetStringUTFChars(JNIEnv*, jstring, jboolean* isCopy);
void ReleaseStringChars(JNIEnv*, jstring, const jchar*);
void ReleaseStringUTFChars(JNIEnv*, jstring, const char*);
// 建立新 jstring
jstring NewString(JNIEnv*, const jchar*, jsize); // 從 UTF-16
jstring NewStringUTF(JNIEnv*, const char*); // 從 Modified UTF-8
// 關鍵:GetStringUTFRegion / GetStringRegion (無需 Release,複製到預分配緩衝區)
void GetStringRegion(JNIEnv*, jstring, jsize start, jsize len, jchar* buf);
void GetStringUTFRegion(JNIEnv*, jstring, jsize start, jsize len, char* buf);
class JniUtf16String {
JNIEnv* env_;
jstring jstr_;
const jchar* chars_;
bool copied_;
public:
JniUtf16String(JNIEnv* env, jstring jstr) : env_(env), jstr_(jstr) {
chars_ = env_->GetStringChars(jstr_, &copied_);
}
~JniUtf16String() {
if (chars_) env_->ReleaseStringChars(jstr_, chars_);
}
// 禁止複製,允許移動
JniUtf16String(const JniUtf16String&) = delete;
JniUtf16String& operator=(const JniUtf16String&) = delete;
JniUtf16String(JniUtf16String&& other) noexcept
: env_(other.env_), jstr_(other.jstr_), chars_(other.chars_), copied_(other.copied_) {
other.chars_ = nullptr;
}
const jchar* get() const { return chars_; }
jsize length() const { return env_->GetStringLength(jstr_); }
// 轉換為 std::u16string (C++20) 或 std::basic_string
std::u16string toU16String() const {
return std::u16string(chars_, chars_ + length());
}
// Windows: 直接轉 wchar_t* (UTF-16 相容)
#ifdef _WIN32
std::wstring toWString() const {
return std::wstring(reinterpret_cast(chars_), length());
}
#endif
};
class JniModifiedUtf8String {
JNIEnv* env_;
jstring jstr_;
const char* utfChars_;
bool copied_;
public:
JniModifiedUtf8String(JNIEnv* env, jstring jstr) : env_(env), jstr_(jstr) {
utfChars_ = env_->GetStringUTFChars(jstr_, &copied_);
}
~JniModifiedUtf8String() {
if (utfChars_) env_->ReleaseStringUTFChars(jstr_, utfChars_);
}
JniModifiedUtf8String(const JniModifiedUtf8String&) = delete;
JniModifiedUtf8String& operator=(const JniModifiedUtf8String&) = delete;
const char* get() const { return utfChars_; }
jsize byteLength() const { return env_->GetStringUTFLength(jstr_); }
// 轉換為標準 UTF-8 (需處理 Modified UTF-8 差異)
std::string toStdUtf8() const {
// 簡易版:僅處理非 \u0000 情況,完整轉換建議用 ICU 或自行實作
return std::string(utfChars_, utfChars_ + byteLength());
}
};
class JniStringConverter {
JNIEnv* env_;
public:
explicit JniStringConverter(JNIEnv* env) : env_(env) {}
// Java String -> C++ std::string (平台編碼 / UTF-8)
std::string toStdString(jstring jstr, bool useUtf8 = true) {
if (!jstr) return "";
if (useUtf8) {
// 方案 A: GetStringUTFRegion 避免 Release 配對風險
jsize len = env_->GetStringUTFLength(jstr);
std::string buf(len, '\0');
env_->GetStringUTFRegion(jstr, 0, len, buf.data());
// 注意:這是 Modified UTF-8,若需標準 UTF-8 需額外轉換
return buf;
} else {
// 方案 B: UTF-16 -> 平台編碼 (Windows CP_ACP / Linux UTF-8)
JniUtf16String u16(env_, jstr);
return utf16ToPlatform(u16.get(), u16.length());
}
}
// C++ std::string -> Java String
jstring fromStdString(const std::string& str, bool fromUtf8 = true) {
if (fromUtf8) {
// 假設輸入為標準 UTF-8,需轉為 Modified UTF-8
// 簡易做法:若無 \u0000 和補充字元,直接用 NewStringUTF
return env_->NewStringUTF(str.c_str());
} else {
// 平台編碼 -> UTF-16
return platformToUtf16(str);
}
}
private:
// Windows: MultiByteToWideChar / WideCharToMultiByte
// Linux: iconv / ICU
std::string utf16ToPlatform(const jchar* u16, jsize len) { /* 平台相關實作 */ return ""; }
jstring platformToUtf16(const std::string& str) { /* 平台相關實作 */ return nullptr; }
};
#include
#include
#include
// jchar* (UTF-16) -> std::string (系統 ANSI / CP_ACP)
std::string utf16ToAnsi(const jchar* u16, jsize len) {
if (len == 0) return "";
int needed = WideCharToMultiByte(CP_ACP, 0,
reinterpret_cast(u16), len,
nullptr, 0, nullptr, nullptr);
std::string result(needed, '\0');
WideCharToMultiByte(CP_ACP, 0,
reinterpret_cast(u16), len,
result.data(), needed, nullptr, nullptr);
return result;
}
// std::string (系統 ANSI) -> jstring (UTF-16)
jstring ansiToJstring(JNIEnv* env, const std::string& ansi) {
if (ansi.empty()) return env->NewStringUTF("");
int needed = MultiByteToWideChar(CP_ACP, 0, ansi.c_str(), -1, nullptr, 0);
std::vector wbuf(needed);
MultiByteToWideChar(CP_ACP, 0, ansi.c_str(), -1, wbuf.data(), needed);
return env->NewString(reinterpret_cast(wbuf.data()), needed - 1); // 排除 null 結尾
}
// jstring -> std::wstring (UTF-16,Windows 原生)
std::wstring jstringToWstring(JNIEnv* env, jstring jstr) {
JniUtf16String u16(env, jstr);
return std::wstring(reinterpret_cast(u16.get()), u16.length());
}
#include
#include
#include
#include
class IconvConverter {
iconv_t cd_;
public:
IconvConverter(const char* toCode, const char* fromCode) {
cd_ = iconv_open(toCode, fromCode);
if (cd_ == (iconv_t)-1) throw std::runtime_error("iconv_open failed");
}
~IconvConverter() { if (cd_ != (iconv_t)-1) iconv_close(cd_); }
std::string convert(const std::string& input) {
std::vector outbuf(input.size() * 4 + 1);
char* inptr = const_cast(input.data());
size_t inleft = input.size();
char* outptr = outbuf.data();
size_t outleft = outbuf.size();
if (iconv(cd_, &inptr, &inleft, &outptr, &outleft) == (size_t)-1)
throw std::runtime_error("iconv failed: " + std::string(strerror(errno)));
return std::string(outbuf.data(), outbuf.size() - outleft);
}
};
// 使用範例:UTF-16LE -> UTF-8
std::string utf16leToUtf8(const jchar* u16, jsize len) {
std::string utf16le(reinterpret_cast(u16), len * 2);
IconvConverter conv("UTF-8", "UTF-16LE");
return conv.convert(utf16le);
}
ICU 是業界標準 Unicode 處理庫,支援完整編碼轉換、排序、正規化、日期時間格式化。Android NDK 內建,Linux/macOS 可透過套件管理安裝 (`libicu-dev`)。
// ICU 範例:UTF-16 -> UTF-8 (標準)
#include
#include
std::string icuUtf16ToUtf8(const jchar* u16, jsize len) {
UErrorCode status = U_ZERO_ERROR;
UConverter* conv = ucnv_open("UTF-8", &status);
if (U_FAILURE(status)) return "";
int32_t destCapacity = len * 3 + 1; // UTF-8 最多 3 bytes/字元
std::string dest(destCapacity, '\0');
int32_t destLen = ucnv_fromUChars(conv, dest.data(), destCapacity,
reinterpret_cast(u16), len, &status);
ucnv_close(conv);
if (U_FAILURE(status)) return "";
dest.resize(destLen);
return dest;
}
public class JniStringDemo {
static { System.loadLibrary("jni_string_demo"); }
// 原生方法宣告
public native String processString(String input);
public native void printString(String input);
public static void main(String[] args) {
JniStringDemo demo = new JniStringDemo();
String result = demo.processString("測試中文:Hello JNI!");
System.out.println("Java 收到結果: " + result);
demo.printString("直接列印測試");
}
}
#include
#include
#include // Android 專用,桌面端用 std::cout
#define LOG_TAG "JniStringDemo"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
// 1. 實作原生邏輯
static jstring JavaProcessString(JNIEnv* env, jobject obj, jstring input) {
if (!input) return env->NewStringUTF("");
// 方案:使用 GetStringUTFRegion 取得 Modified UTF-8 (避免 Release 配對)
jsize len = env->GetStringUTFLength(input);
std::string utf8Input(len, '\0');
env->GetStringUTFRegion(input, 0, len, utf8Input.data());
// 業務邏輯:這裡簡單加上前綴
std::string result = "[C++處理] " + utf8Input;
// 回傳:NewStringUTF 期望 Modified UTF-8
// 注意:若 result 含有 \u0000 或補充字元,需手動轉 Modified UTF-8
return env->NewStringUTF(result.c_str());
}
static void JavaPrintString(JNIEnv* env, jobject obj, jstring input) {
// 取得 UTF-16 直接列印 (Windows wchar_t 相容)
JniUtf16String u16(env, input);
#ifdef _WIN32
std::wcout << L"[C++列印] " << std::wstring(reinterpret_cast(u16.get()), u16.length()) << std::endl;
#else
// Linux: 轉 UTF-8 列印
std::string utf8 = icuUtf16ToUtf8(u16.get(), u16.length());
LOGI("%s", utf8.c_str());
#endif
}
// 2. JNINativeMethod 註冊表
static JNINativeMethod methods[] = {
{"processString", "(Ljava/lang/String;)Ljava/lang/String;", (void*)JavaProcessString},
{"printString", "(Ljava/lang/String;)V", (void*)JavaPrintString},
};
// 3. JNI_OnLoad 自動註冊 (優於 javah 產生的標頭檔)
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env;
if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK)
return JNI_ERR;
jclass clazz = env->FindClass("JniStringDemo");
if (!clazz) return JNI_ERR;
if (env->RegisterNatives(clazz, methods, sizeof(methods)/sizeof(methods[0])) < 0)
return JNI_ERR;
return JNI_VERSION_1_6;
}
| 症狀 | 可能原因 | 解決方案 |
|---|---|---|
| 中文顯示亂碼 ( 或 方塊) | 編碼不匹配:Java UTF-16 ↔ C 當地編碼/UTF-8 混淆 | 統一使用 UTF-16 (jchar*) 傳遞,或明確指定雙邊編碼為 UTF-8 (需處理 Modified UTF-8 差異) |
| JVM 崩潰 (SIGSEGV / EXCEPTION_ACCESS_VIOLATION) | 使用已 Release 的指標、雙重 Release、傳遞 NULL 給 NewString | 使用 RAII 封裝、檢查 NULL、啟用 `-Xcheck:jni` 執行 |
| 記憶體洩漏 (Native Heap 成長) | 忘記 ReleaseStringChars/UTFChars、未 DeleteLocalRef | RAII 封裝、定期檢查 `jcmd |
| 補充字元 (Emoji、生僻字) 截斷/錯誤 | Modified UTF-8 編碼補充字元為 6 字節,標準 UTF-8 為 4 字節 | 避免使用 GetStringUTFChars 處理含補充字元字串,改用 UTF-16 路徑 |
| 效能差 (大量字串複製) | 頻繁 GetStringChars/NewString 分配記憶體 | 批次處理、使用 GetStringRegion 重用緩衝區、考慮零複製設計 (Direct ByteBuffer) |
Foreign Function & Memory API (JEP 442/454),提供型別安全的原生記憶體存取、函數呼叫、MemorySegment、Linker,未來將取代 JNI 成為標準。
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
try (Arena arena = Arena.ofConfined()) {
// 取得 C 庫函數處理字串
Linker linker = Linker.nativeLinker();
SymbolLookup libc = SymbolLookup.loaderLookup();
// 假設 C 函數:const char* process(const char* input);
MethodHandle process = linker.downcallHandle(
libc.find("process").orElseThrow(),
FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)
);
// Java String -> C 字串 (UTF-8, 以 null 結尾)
MemorySegment cInput = arena.allocateFrom("測試字串".getBytes(StandardCharsets.UTF_8));
// 呼叫原生函數
MemorySegment cResult = (MemorySegment) process.invokeExact(cInput);
// C 字串 -> Java String
String result = cResult.getUtf8String(0);
System.out.println("Panama 結果: " + result);
}
JNI 字串處理的核心在於 明確編碼契約、嚴格生命週期管理、平台差異隔離。建議採用:
記住:在 JNI 邊界,每一個字串傳遞都是一次編碼協議的協商。文件化協議、編寫測試案例覆蓋邊界字元 (空字串、ASCII、中文、Emoji、補充字元、超長字串),才能構建穩健的混合語言系統。