NetworkInterface.getDisplayName()で発生する文字化けをチェック

Javajava.net.NetworkInterfaceにある、getDisplayName()メソッドで文字化けするために、IPHLPAPIを使って新たにクラスを作るということを以前していましたが、何故文字化けするのか調査しようと思って、簡単なテストプログラムを作ってみました。
Javajava.net.NetworkInterfaceのロジックとしては、jdk1.6のソースコードからjdk6-src\j2se\src\windows\native\java\net\NetworkInterface.cを参考にして作っています。
テスト用のJavaプログラムとして、
NetworkInfo.java

package charset;

import java.util.Enumeration;

public class NetworkInfo {
    private int             index = 0;
    private String          description_jdk = null;
    private String          description_cvt = null;
    private String          description_loc = null;
    private String          description_bstr = null;

    static {
        System.loadLibrary("CharSet");
        init();
    }

    public NetworkInfo() {
    }
    
    public int getIndex() {
        return index;
    }
    
    public String getDescriptionJDK() {
        return description_jdk;
    }
    
    public String getDescriptionCVT() {
        return description_cvt;
    }
    
    public String getDescriptionLOC() {
        return description_loc;
    }
    
    public String getDescriptionBSTR() {
        return description_bstr;
    }

    public static void main(String[] args) {
        try {
            Enumeration<java.net.NetworkInterface> ifs = 
                         java.net.NetworkInterface.getNetworkInterfaces();
            while (ifs.hasMoreElements()) {
                java.net.NetworkInterface intf = ifs.nextElement();
                System.out.println("Interface : ");
                System.out.println("  Name : "+intf.getName());
                System.out.println("  Description : "+intf.getDisplayName());
            }
        } catch (Exception e) {}
    
    
        NetworkInfo[] nis = NetworkInfo.getInterfaces();
        for (int i = 0; i < nis.length; i++) {
            NetworkInfo ni = nis[i];
            System.out.println("Nic : "+ni.getIndex());
            System.out.println("  DescriptionJDK : "+ni.getDescriptionJDK());
            System.out.println("  DescriptionCVT : "+ni.getDescriptionCVT());
            System.out.println("  DescriptionLOC : "+ni.getDescriptionLOC());
            System.out.println("  DescriptionBSTR : "+ni.getDescriptionBSTR());
        }
    }

    private static native void init();
    public static native NetworkInfo[] getInterfaces();
}

JNIを呼び出すことになるので、いつものようにコンパイルした後にjavahでヘッダファイルを作りました。
いつもだとIncludeや変数情報などはプログラムファイルと別にヘッダファイルを用意するんですが、今回はテスト用に単純なアプリケーションなので、ヘッダ情報等もまとめてC++のJNIプログラム1ファイルで作っちゃいました。

#include <windows.h>
#include <tchar.h>
#include <comdef.h>

#include <iphlpapi.h>
#pragma comment(lib, "iphlpapi.lib")

#include <jni.h>
#include "charset_NetworkInfo.h"

#include <locale>
using std::locale;

jclass      ni_class;               /* NetworkInfo */
jmethodID   ni_ctor;                /* NetworkInfo() */
jfieldID    ni_indexID;             /* NetworkInfo.index */
jfieldID    ni_description_jdkID;   /* NetworkInfo.description_jdk */
jfieldID    ni_description_cvtID;   /* NetworkInfo.description_cvt */
jfieldID    ni_description_locID;   /* NetworkInfo.description_loc */
jfieldID    ni_description_bstrID;  /* NetworkInfo.description_bstr */

JNIEXPORT void JNICALL Java_charset_NetworkInfo_init(JNIEnv *env, jclass cls) {
    ni_class = (jclass)env->NewGlobalRef(cls);
    ni_indexID = env->GetFieldID(ni_class, "index", "I");
    ni_description_jdkID = env->GetFieldID(ni_class, "description_jdk", "Ljava/lang/String;");
    ni_description_cvtID = env->GetFieldID(ni_class, "description_cvt", "Ljava/lang/String;");
    ni_description_locID = env->GetFieldID(ni_class, "description_loc", "Ljava/lang/String;");
    ni_description_bstrID = env->GetFieldID(ni_class, "description_bstr", "Ljava/lang/String;");
    ni_ctor = env->GetMethodID(ni_class, "<init>", "()V");
}

JNIEXPORT jobjectArray JNICALL Java_charset_NetworkInfo_getInterfaces(JNIEnv *env, jclass cls) {
    jobjectArray ni_list = NULL;
    DWORD dwSize = 0;

    GetIfTable(NULL, &dwSize, true);
    PMIB_IFTABLE pIfTable = (MIB_IFTABLE *)calloc(dwSize, 1);
    if (pIfTable != NULL) {
        if (GetIfTable(pIfTable, &dwSize, true) == NO_ERROR) {
            jsize ni_count = (jsize)pIfTable->dwNumEntries;
            if (ni_count > 0) {
                ni_list = env->NewObjectArray(ni_count, ni_class, NULL);
                for (unsigned long i = 0; i < pIfTable->dwNumEntries; i++) {
                    MIB_IFROW pInterface = pIfTable->table[i];
                    jobject netifObj = env->NewObject(ni_class, ni_ctor);
                    env->SetIntField(netifObj, ni_indexID, pInterface.dwIndex);

                    /*
                     * XPでの表示をテストするために、
                     *  jdk6-src\j2se\src\windows\native\java\net\NetworkInterface.c
                     * の582行目付近にある以下のロジックを参照して、Unicodeモードでない方の処理を流用しました。
                     *
                     *  if (ifs->dNameIsUnicode) {
                     *      displayName = (*env)->NewString(env, (PWCHAR)ifs->displayName, wcslen ((PWCHAR)ifs->displayName));
                     *  } else {
                     *      displayName = (*env)->NewStringUTF(env, ifs->displayName);
                     *  }
                     */
                    jstring jdk_string = env->NewStringUTF((const char *)pInterface.bDescr);
                    env->SetObjectField(netifObj, ni_description_jdkID, jdk_string);

                    /*
                     * JavaにUnicodeの文字列を設定するところで、MBCS文字列からUnicodeへの変換処理をcの関数
                     * mbstowcsを利用して変換してみました。
                     */
                    size_t wcs_len = mbstowcs(NULL, (const char *)pInterface.bDescr, strlen((const char *)pInterface.bDescr));
                    wchar_t *cvt_value = (wchar_t *)calloc(wcs_len, sizeof(wchar_t));
                    wcs_len = mbstowcs(cvt_value, (const char *)pInterface.bDescr, strlen((const char *)pInterface.bDescr));
                    jstring cvt_string = env->NewString((const jchar *)((wchar_t *)cvt_value), (jsize)wcs_len);
                    env->SetObjectField(netifObj, ni_description_cvtID, cvt_string);

                    /*
                     * JavaにUnicodeの文字列を設定するところで、MBCS文字列からUnicodeへの変換処理をcの関数
                     * mbstowcsを利用して変換してみました。
                     * 変換時にプログラムのロケールを一時的に"japanese"にしてみました。
                     */
                    locale jpn("japanese");
                    locale old_locale = locale::global(jpn); 
                    wcs_len = mbstowcs(NULL, (const char *)pInterface.bDescr, strlen((const char *)pInterface.bDescr));
                    wchar_t *loc_value = (wchar_t *)calloc(wcs_len, sizeof(wchar_t));
                    wcs_len = mbstowcs(loc_value, (const char *)pInterface.bDescr, strlen((const char *)pInterface.bDescr));
                    jstring loc_string = env->NewString((const jchar *)((wchar_t *)loc_value), (jsize)wcs_len);
                    locale::global(old_locale);
                    env->SetObjectField(netifObj, ni_description_locID, loc_string);

                    /*
                     * 変換処理は_bstr_tに任せてみました。
                     */
                    _bstr_t bstr_value = _bstr_t((char *)pInterface.bDescr);
                    jstring bstr_string = env->NewString((const jchar *)((wchar_t *)bstr_value), (jsize)bstr_value.length());
                    env->SetObjectField(netifObj, ni_description_bstrID, bstr_string);

                    env->SetObjectArrayElement(ni_list, i, netifObj);
                }
            }
        }
        free(pIfTable);
    }

    return ni_list;
}

Javaのプログラムを実行した結果(一部抜粋)は、

・・・
Interface : 
  Name : eth1
  Description : TAP-Win32 Adapter V9 - ?p?P?b?g ?X?P?W
・・・
Nic : 3
  DescriptionJDK : TAP-Win32 Adapter V9 - ?p?P?b?g ?X?P?W
  DescriptionCVT : TAP-Win32 Adapter V9 - ?p?P?b?g ?X?P?W???[?? ?~?j?|?[?g
  DescriptionLOC : TAP-Win32 Adapter V9 - パケット スケジューラ ミニポート
  DescriptionBSTR : TAP-Win32 Adapter V9 - パケット スケジューラ ミニポート
・・・

となり、内部的な文字コードの変換における問題って感じですね。
内部コードのロケールの問題らしいので、かなり強引ですが文字コードを変換しちゃうメソッドを作ってしまいました。
クラス名はJapaneseとかなり即物的な名前にしました。・・・名前を考えるのが面倒だったので。
Japanese.java

package charset;

public class Japanese {
    public Japanese() {
    }
    public static native String FixCharset(String Original);
}

今までと同様に、これをコンパイルしてヘッダファイルを作ってから、
Japanese.cpp

#include <jni.h>
#include "charset_Japanese.h"

#include <locale>
using std::locale;

JNIEXPORT jstring JNICALL Java_charset_Japanese_FixCharset(JNIEnv *env, jclass cls, jstring original) {
    /*
     *  Java文字列を一旦MBCS文字列に変換する
     */
    wchar_t *words = (wchar_t *)env->GetStringChars(original, NULL);
    size_t mbs_len = wcstombs(NULL, words, wcslen(words));
    char *mbs_value = (char *)calloc(mbs_len+1, sizeof(char));
    mbs_len = wcstombs(mbs_value, words, wcslen(words));
    env->ReleaseStringChars(original, (const jchar *)words);
    /*
     *  MBCS文字列をLocal=japaneseで再度Unicode文字列に変換し、結果をJava文字列に設定する
     */
    locale jpn("japanese");
    locale old_locale = locale::global(jpn); 
    size_t wcs_len = mbstowcs(NULL, (const char *)mbs_value, mbs_len);
    wchar_t *wcs_value = (wchar_t *)calloc(wcs_len, sizeof(wchar_t));
    wcs_len = mbstowcs(wcs_value, (const char *)mbs_value, mbs_len);
    jstring wcs_string = env->NewString((const jchar *)((wchar_t *)wcs_value), (jsize)wcs_len);
    locale::global(old_locale);
    free(mbs_value);
    return wcs_string;
}

を作ってテストしてみました。
テストプログラムを

        NetworkInfo[] nis = NetworkInfo.getInterfaces();
        for (int i = 0; i < nis.length; i++) {
            NetworkInfo ni = nis[i];
            System.out.println("Nic : "+ni.getIndex());
            System.out.println("  DescriptionJDK : "+ni.getDescriptionJDK());
            System.out.println("  DescriptionCVT : "+ni.getDescriptionCVT());
            System.out.println("  DescriptionLOC : "+ni.getDescriptionLOC());
            System.out.println("  DescriptionBSTR : "+ni.getDescriptionBSTR());
            System.out.println("  Converted : "+Japanese.FixCharset(ni.getDescriptionJDK()));
        }

に変更して実行した結果が、

Nic : 3
  DescriptionJDK : TAP-Win32 Adapter V9 - ?p?P?b?g ?X?P?W
  DescriptionCVT : TAP-Win32 Adapter V9 - ?p?P?b?g ?X?P?W???[?? ?~?j?|?[?g
  DescriptionLOC : TAP-Win32 Adapter V9 - パケット スケジューラ ミニポート
  DescriptionBSTR : TAP-Win32 Adapter V9 - パケット スケジューラ ミニポート
  Converted : TAP-Win32 Adapter V9 - パケット スケジ

になりました。
"パケット スケジ"で切れているのは、Java保有している元々の文字列が"?p?P?b?g ?X?P?W"で切れている関係です。

env->NewStringUTF((const char *)pInterface.bDescr);

の内部処理で切れちゃったみたいですね。


あくまでも想像ですが、pInterface.bDescrの値として

offset  0  1  2  3  4  5  6  7   8  9  A  B  C  D  E  F    0123456789ABCDEF
     0  54 41 50 2d 57 69 6e 33  32 20 41 64 61 70 74 65   TAP-Win32 Adapte
    16  72 20 56 39 20 2d 20 83  70 83 50 83 62 83 67 20   r V9 - .p.P.b.g
    32  83 58 83 50 83 57 83 85  81 5b 83 89 20 83 7e 83   .X.P.W...[.. .~.
    48  6a 83 7c 81 5b 83 67 00                            j.|.[.g.

が設定されているので、NewStringUTF関数の中の処理で、offsetが38byteから0x83,0x85と0x80以上の値が連続したために内部の変換処理でエラー等の不具合が発生したのかもしれません。
これに関しては、Javaソースコードを追いかける気にならなかったので、あくまでも想像ベースですけど。