首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

JNI 中文字符處理完整指南:編碼轉換、記憶體管理與最佳實務

     編輯:Java 相關知識

前言:JNI 字串傳遞的核心挑戰

Java 內部字串採用 UTF-16 (char[]),C/C++ 常用 `char*` (單位字節、平台編碼) 或 `wchar_t*` (寬字元、Windows 為 UTF-16、Linux 為 UTF-32)。JNI 提供 `jstring` 不透明型別,必須透過 JNI 函數在三種編碼間轉換。錯誤的編碼假設會導致亂碼、記憶體洩漏、甚至 JVM 崩潰。本文系統整理 JNI 字串處理的編碼模型、關鍵 API、C++ 封裝類別與實戰陷阱。

一、編碼模型對照表

層級型別編碼位元寬度備註
JavaString / char[]UTF-1616 bits / char補充字元佔 2 個 char (surrogate pair)
JNIjstringUTF-16 (內部)不透明不可直接存取內容
JNI APIGetStringCharsUTF-16 (jchar*)16 bits需 ReleaseStringChars
JNI APIGetStringUTFCharsModified UTF-88 bits/byte需 ReleaseStringUTFChars,非標準 UTF-8
C/C++ Windowswchar_t*UTF-1616 bitsWindows API 標準
C/C++ Linuxwchar_t*UTF-3232 bitsglibc 標準
C/C++ 通用char*Locale 相關 (CP_ACP/UTF-8)8 bits需明確指定編碼

關鍵差異:Modified UTF-8 vs 標準 UTF-8

  • Java Modified UTF-8:`\u0000` 編碼為 `0xC0 0x80` (兩字節),避免 C 字串截斷;補充字元先轉 surrogate pair 再各自編碼 (6 字節)
  • 標準 UTF-8:`\u0000` 編碼為 `0x00` (單字節);補充字元直接編碼 (4 字節)
  • 結論:JNI `GetStringUTFChars` 回傳 Modified UTF-8,不可直接當作標準 UTF-8 傳給 C 庫函數 (如 `printf`、JSON 解析器)

二、核心 JNI 字串 API 速查


// 取得長度 (字元數,非字節數)
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);

三、C++ RAII 封裝類別:安全管理 JNI 字串生命週期

1. JniUtf16String:對應 GetStringChars / ReleaseStringChars


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
};

2. JniModifiedUtf8String:對應 GetStringUTFChars / ReleaseStringUTFChars


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());
    }
};

3. JniStringConverter:平台無關的雙向轉換樞紐


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; }
};

四、Windows 平台實作細節 (Visual C++ / MinGW)

UTF-16 (jchar*) ↔ 系統 ANSI (CP_ACP) 轉換


#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());
}

五、Linux/macOS 平台實作細節 (glibc / ICU)

使用 iconv 進行編碼轉換


#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 (International Components for Unicode)

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;
}

六、完整 JNI 範例:Java 呼叫 C++ 字串處理

Java 端


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("直接列印測試");
    }
}

C++ 端 (JNI_OnLoad + 註冊原生方法)


#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、未 DeleteLocalRefRAII 封裝、定期檢查 `jcmd VM.native_memory`
補充字元 (Emoji、生僻字) 截斷/錯誤Modified UTF-8 編碼補充字元為 6 字節,標準 UTF-8 為 4 字節避免使用 GetStringUTFChars 處理含補充字元字串,改用 UTF-16 路徑
效能差 (大量字串複製)頻繁 GetStringChars/NewString 分配記憶體批次處理、使用 GetStringRegion 重用緩衝區、考慮零複製設計 (Direct ByteBuffer)

八、現代替代方案:JNA / JNR / Panama (Foreign Function & Memory API)

  • JNA (Java Native Access):純 Java 透過 libffi 呼叫原生庫,無需編譯 C++,自動處理字串編碼 (預設 UTF-8),適合快速整合。
  • JNR-FFI:JNA 現代化分支,效能更好、API 更安全。
  • Project Panama (JDK 21+ Preview):Foreign Function & Memory API (JEP 442/454),提供型別安全的原生記憶體存取、函數呼叫、MemorySegment、Linker,未來將取代 JNI 成為標準。

Panama 字串處理範例 (JDK 21+)


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 字串處理的核心在於 明確編碼契約、嚴格生命週期管理、平台差異隔離。建議採用:

  1. 優先使用 UTF-16 (jchar*) 路徑:避開 Modified UTF-8 坑、相容 Windows wchar_t、支援完整 Unicode。
  2. RAII 封裝所有 JNI 字串操作:確保 `Release*` 配對呼叫,杜絕記憶體洩漏與崩潰。
  3. 跨平台編碼轉換委託 ICU:不要手寫 iconv/WinAPI 邏輯,ICU 經過嚴格測試、支援完整 Unicode 標準。
  4. 新專案評估 Panama / JNA:減少 C++ 程式碼維護成本,提升型別安全與開發效率。

記住:在 JNI 邊界,每一個字串傳遞都是一次編碼協議的協商。文件化協議、編寫測試案例覆蓋邊界字元 (空字串、ASCII、中文、Emoji、補充字元、超長字串),才能構建穩健的混合語言系統。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved