Windows ARM の IMEについて覚え書き

## Windows ARM で3rdParty IMEは使えるのか
A. バイナリによる。
挙動は以下の通り。

|バイナリ    |動作                  |
|----------|--------------------------|
|**x86**  |**動作可**         |
|**x64**  |**動作可**         |
|**ARM64EC**  |**動作可**         |
|**ARM64**  |**動作不可**         |

確認方法はタスクマネージャーの詳細>アーキテクチャから

ARM64 (x64 互換)はARM64ECです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1280188/f4239350-821e-4ff6-aade-6e49a2d7e863.png)


## 何で動く物と動かない物がある？

A.一言で言えばアセンブラ記述が違うから。

下記記事を読めば分かるが詳しく調べてみる。

https://learn.microsoft.com/ja-jp/windows/arm/arm64ec

次のような要素を持つアプリケーションを作成してみる。

![スクリーンショット 2025-03-24 17.21.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1280188/25e96929-c5d3-4264-b8f0-3d0fac320233.png)

<details><summary>コード</summary>

```c++:main.cpp
#include <windows.h>
#include <msctf.h>
#include <imm.h>
#include <vector>
#include <string>
#include <comdef.h>

#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "imm32.lib")

struct IMEProfile {
    LANGID langID;
    CLSID clsid;
    GUID guidProfile;
    std::wstring description;
};

std::vector<IMEProfile> g_imeProfiles;
ITfInputProcessorProfiles* g_pProfiles = nullptr;

const wchar_t g_szClassName[] = L"TSF_IME_GUI_Window";

// コントロールのID
#define IDC_LISTBOX 101
#define IDC_ACTIVATE_BUTTON 102

// IMEプロファイルの列挙
bool EnumerateIMEProfiles() {
    LANGID currentLang = GetSystemDefaultLangID();
    IEnumTfLanguageProfiles* pEnum = nullptr;
    HRESULT hr = g_pProfiles->EnumLanguageProfiles(currentLang, &pEnum);
    if (FAILED(hr) || !pEnum) {
        return false;
    }

    g_imeProfiles.clear();
    TF_LANGUAGEPROFILE langProfile;
    ULONG fetched = 0;
    while (pEnum->Next(1, &langProfile, &fetched) == S_OK) {
        IMEProfile profile;
        profile.langID = langProfile.langid;
        profile.clsid = langProfile.clsid;
        profile.guidProfile = langProfile.guidProfile;

        BSTR bstrDesc = nullptr;
        hr = g_pProfiles->GetLanguageProfileDescription(langProfile.clsid, langProfile.langid, langProfile.guidProfile, &bstrDesc);
        if (SUCCEEDED(hr) && bstrDesc) {
            profile.description = bstrDesc;
            SysFreeString(bstrDesc);
        }
        else {
            profile.description = L"(説明なし)";
        }
        g_imeProfiles.push_back(profile);
    }
    pEnum->Release();
    return !g_imeProfiles.empty();
}

// エラーメッセージ取得用の関数
std::wstring GetErrorMessage(HRESULT hr) {
    wchar_t* msgBuffer = nullptr;
    FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL,
        hr,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        (LPWSTR)&msgBuffer,
        0,
        NULL);
    std::wstring message;
    if (msgBuffer) {
        message = msgBuffer;
        LocalFree(msgBuffer);
    }
    else {
        message = L"エラーコード: " + std::to_wstring(hr);
    }
    return message;
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
    switch (msg) {
    case WM_CREATE:
    {
        // リストボックスの作成
        HWND hList = CreateWindowW(L"LISTBOX", NULL,
            WS_CHILD | WS_VISIBLE | LBS_NOTIFY | WS_VSCROLL,
            10, 10, 300, 150,
            hwnd, (HMENU)IDC_LISTBOX, GetModuleHandle(NULL), NULL);

        // ボタンの作成
        HWND hButton = CreateWindowW(L"BUTTON", L"IME有効化",
            WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
            320, 10, 100, 30,
            hwnd, (HMENU)IDC_ACTIVATE_BUTTON, GetModuleHandle(NULL), NULL);

        // IMEプロファイルを列挙してリストボックスに追加
        if (!EnumerateIMEProfiles()) {
            MessageBox(hwnd, L"利用可能なIMEプロファイルが見つかりません。", L"エラー", MB_ICONERROR);
        }
        else {
            for (size_t i = 0; i < g_imeProfiles.size(); ++i) {
                SendMessage(hList, LB_ADDSTRING, 0, (LPARAM)g_imeProfiles[i].description.c_str());
            }
        }

        HWND hEdit = CreateWindowW(L"EDIT", L"",
            WS_CHILD | WS_VISIBLE | WS_BORDER | ES_LEFT,
            10, 170, 300, 25,
            hwnd, NULL, GetModuleHandle(NULL), NULL);
    }
    break;
    case WM_COMMAND:
        if (LOWORD(wParam) == IDC_ACTIVATE_BUTTON) {
            HWND hList = GetDlgItem(hwnd, IDC_LISTBOX);
            int sel = (int)SendMessage(hList, LB_GETCURSEL, 0, 0);
            if (sel == LB_ERR) {
                MessageBox(hwnd, L"IMEプロファイルが選択されていません", L"警告", MB_ICONWARNING);
            }
            else if (sel < 0 || sel >= (int)g_imeProfiles.size()) {
                MessageBox(hwnd, L"選択番号が不正です。", L"エラー", MB_ICONERROR);
            }
            else {
                IMEProfile chosenProfile = g_imeProfiles[sel];

                HRESULT hr = g_pProfiles->ChangeCurrentLanguage(chosenProfile.langID);
                if (FAILED(hr)) {
                    std::wstring errMsg = L"現在の言語の変更に失敗。\n理由: " + GetErrorMessage(hr);
                    MessageBox(hwnd, errMsg.c_str(), L"エラー", MB_ICONERROR);
                    break;
                }

                hr = g_pProfiles->ActivateLanguageProfile(chosenProfile.clsid, chosenProfile.langID, chosenProfile.guidProfile);
                if (FAILED(hr)) {
                    std::wstring errMsg = L"IMEプロファイルの有効化に失敗。\n理由: " + GetErrorMessage(hr);
                    MessageBox(hwnd, errMsg.c_str(), L"エラー", MB_ICONERROR);
                }
            }
        }
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
}

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int nCmdShow) {
    HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
    if (FAILED(hr)) {
        MessageBox(NULL, L"COMの初期化に失敗。", L"エラー", MB_ICONERROR);
        return 1;
    }

    hr = CoCreateInstance(CLSID_TF_InputProcessorProfiles, NULL, CLSCTX_INPROC_SERVER,
        IID_ITfInputProcessorProfiles, (void**)&g_pProfiles);
    if (FAILED(hr) || !g_pProfiles) {
        MessageBox(NULL, L"ITfInputProcessorProfilesのインスタンス作成に失敗。", L"エラー", MB_ICONERROR);
        CoUninitialize();
        return 1;
    }

    WNDCLASSW wc = { 0 };
    wc.lpfnWndProc = WndProc;
    wc.hInstance = hInstance;
    wc.lpszClassName = g_szClassName;
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);

    if (!RegisterClassW(&wc)) {
        MessageBox(NULL, L"ウィンドウクラスの登録に失敗", L"エラー", MB_ICONERROR);
        g_pProfiles->Release();
        CoUninitialize();
        return 1;
    }

    HWND hwnd = CreateWindowW(g_szClassName, L"IME切替 GUI",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 450, 250,
        NULL, NULL, hInstance, NULL);
    if (!hwnd) {
        MessageBox(NULL, L"ウィンドウの作成に失敗", L"エラー", MB_ICONERROR);
        g_pProfiles->Release();
        CoUninitialize();
        return 1;
    }

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);
    
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    
    g_pProfiles->Release();
    CoUninitialize();
    return (int)msg.wParam;
}
```

</details>

実行するとx64,x86,ARM64ECバイナリではMS-IMEでも予測変換が出る。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1280188/828d754a-7162-4f44-98a9-642c40dd8791.png)

しかしARM64バイナリでは下のように予測変換が出てこない。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1280188/ebfed36a-cd2b-41f1-97a9-861e0f1c1289.png)

デバッガの出力を見ると
```
'IME.exe' (Win32): 'C:\Program Files\JustSystems\ATOK32T31_X64\ATOK32TIP.DLL' が読み込まれました。含める/除外するの設定でシンボルの読み込みが無効になっています。
'IME.exe' (Win32): 'C:\Program Files\JustSystems\ATOK32T31_X64\ATOK32TIP.DLL' がアンロードされました
```
とあるので、Text Services Frameworkが利用するText Input Processorが存在しないため入力出来ないと考えられる。

>Arm 上の Windows 11 オペレーティング システム自体は、x64 アプリケーションの実行を可能にするために、Arm64EC の相互運用性に大きく依存しています。 Arm 上の Windows 11 で実行されている x64 アプリによって読み込まれたほとんどのオペレーティング システム コードは Arm64EC としてコンパイルされており、アプリケーションが関知しなくてもそのコードのネイティブ パフォーマンスが有効になります。

上記からx64,ARM64ECバイナリではWindows APIのARM64ECコードが呼ばれるため従来のx64向けIMEが動作するが、ARM64バイナリではWindows APIのARMコードが呼ばれる為、従来のx64向けIMEが動作しない。

## 結局3rdPary製のIMEは使える様になるのか

A.今の所無理。

現実的な解決案？として
- MS-IMEしか使わないようにする。
- ARM64のアプリケーションを使わないようにする。
- ARM64バイナリのIMEが出るのを待つ+Arm64X バイナリで配布されるのを待つ。
- ARM64バイナリのIMEが出るのを待つ+x64向けのIMEを両方利用する。

ぐらいが考えられる。
結局の所、x64と同じ使用感で扱える様にするにはIMEを作成している各社がARM64向けビルドの作成とArm64X バイナリにまとめて配布する必要がある。

## まとめ？
バイナリによって従来のIMEが使えるか変わるためARM64とx64向けのバイナリのみを用意するのでは無く、ARM64EC向けのバイナリもあるとエンドユーザーはうれしいと思う。