Open22

Windows版アイヌ語IMEをZigとRustで作っていく

mkpolimkpoli

モチベーション

https://x.com/mkpoli/status/1769729099851870426?s=20

アイヌ語専用の入力まだできてないの???
Windows上
時間あったら作るか
(Discordサーバーにて)

Twitter をしばらく調べまくっていたが、情報ありませんでした
実はそれこそ数年前から作りたいと思っていたが、システムプログラミングに暗いもんで
C とかで Windows API と戦う機会はなくはなかったけど、さすがに今どき潔癖症で使いたくない
Rust とか Zig で作りたいけど、結構めんどくさいらしいので、暫く手を出せずにいる状態
(Discordサーバーにて)

というより、普通 Windows のような PC 上は、英語やドイツ語、フランス語など、アルファベットを使う言語は、伝統的に変換入力がないので
むしろ流れで入力するのが普通かもしれなくて、予測変換は日本語や中国語など、特殊なケース
でも学習者のためには予測変換があってもいい気がするので、作っていきたいですね
(Discordサーバーにて)

先行研究

アイヌ語入力

https://ja.wikipedia.org/wiki/ことえり
https://nadroom.dousetsu.com/kotoeri/kotoeri_tips.html
http://www.hokkajda-esp-ligo.jp/jp/JOKO_JE_PDF/171-Rakonto_pri_komputila_programo_por_enmeti_ainlingvajn_literojn.pdf
https://support.apple.com/ja-jp/guide/japanese-input-method/jpim10244/mac
https://support.apple.com/ja-jp/111831
https://id.fnshr.info/2022/11/07/ios-ainu/
https://x.com/mkpoli/status/1706538858236813672?s=20
https://support.apple.com/ja-jp/guide/japanese-input-method/jpimce21c292/mac

辞書登録法

日本語入力に、アイヌ語用カナをユーザー辞書などで搭載する方法。
https://www.google.co.jp/ime/
https://x.com/hk_cantonese/status/1765214500754710832?s=20
https://ja.wiktionary.org/wiki/Wiktionary:アイヌ語のカナ表記
https://wentwayup.tamaliver.jp/e117896.html
https://note.com/xiupos/n/ncaedc77c00a0?sub_rt=share_h]

言及

要望や言及がそこそこある。

アイヌ語変換

https://ha1.seikyou.ne.jp/home/akairingosaita/typing/test2-22.htm
https://aynuitak.at-ninja.jp/WEBhenkan/chiyu.htm
https://en.wiktionary.org/wiki/Module:ain-translit
https://ja.wiktionary.org/wiki/モジュール:ain-kana-conv
https://www.hm.pref.hokkaido.lg.jp/wp-content/uploads/2022/04/bulletin_ACRC_vol7_01_p001_008.pdf
https://ainugo.nam.go.jp/js/common.js?1629435492
https://huling.org/tools/conv-ain
https://x.com/mkpoli/status/1701804054853341191?s=20
https://www.npmjs.com/package/ainconv
https://crates.io/crates/ainconv
https://pypi.org/project/ainconv/
https://zenn.dev/mkpoli/articles/ff01135f91d7ee
https://zenn.dev/mkpoli/articles/6ecddd6c185ef1

入力方式(IME)

https://learn.microsoft.com/ja-jp/windows/win32/tsf/text-services-framework
https://github.com/microsoft/Windows-classic-samples/tree/main/Samples/IME
https://qiita.com/shinjimorimitsu/items/62932c7355688808439d
https://github.com/saschanaz/ime-rs
https://keyman.com/
https://github.com/keymanapp/keyman
https://github.com/google/mozc
https://github.com/dinhngtu/VietType
https://ja.wikipedia.org/wiki/Anthy
https://github.com/chewing/windows-chewing-tsf
https://github.com/EasyIME/libIME2
https://github.com/rime/weasel/
https://github.com/EasyIME/PIME
https://github.com/rime/librime
https://github.com/akaza-im/akaza
https://github.com/osfans/PRIME
https://github.com/aiongg/khiin-rs/tree/master/windows

mkpolimkpoli

Windowsにおける入力方式、IME、即ちText Service FrameworkのAPIを利用したアプリケーションとは、本質的には一個のDLLで、DllGetClassObjectDllCanUnloadNowDllRegisterServerDllUnregisterServerの四つの関数を露出している。

https://github.com/saschanaz/ime-rs/issues/15#issuecomment-1514930752

The input method is essentially a dll file which exports four interfaces

EXPORTS
        DllGetClassObject               PRIVATE
        DllCanUnloadNow                 PRIVATE
        DllRegisterServer               PRIVATE
        DllUnregisterServer             PRIVATE
mkpolimkpoli

Text Service Framework のアーキテクチャは、アプリ(例えば notepad.txt) ⇄ TSF Manager ⇄ サービス(IME、なんちゃら.dll)
https://learn.microsoft.com/en-gb/windows/win32/tsf/architecture

TSFの管理ソフト(TSF Manger)は

システムに登録する方法は管理者権限を持つ(elevated)コマンドラインで

regsvr32 "MyTextService.dll" 

Text Serviceが一旦システム登録されたら、Windowsが自動的にDLLをすべてのフォアグラウンドアプリにロードされる。
https://github.com/aiongg/khiin-rs/tree/master/windows#Development

32bitと64bitのアプリケーション両方に利用できるように、32bit DLLと64bit DLL両方をビルドした方が良さそう?自動的にWindowsが選んでくれるのでターゲットを追加してファイルネームを同じにするだけで済むらしい。
https://learn.microsoft.com/en-gb/windows/win32/tsf/64-bit-platform-considerations

ImmInstallIME()関数でレジストリキーが作られが、どれか一つのDLLに一回実行させる。

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layouts

ITfInputProcessorProfilesなどで言語情報を登録する必要がある。
https://learn.microsoft.com/en-gb/windows/win32/tsf/text-service-registration

言語バー
https://learn.microsoft.com/en-gb/windows/win32/tsf/language-bar

スペルチェックなどの特殊効果はDisplay Attributesで設定
https://learn.microsoft.com/en-gb/windows/win32/tsf/providing-display-attributes

mkpolimkpoli

IMEとは?

IMEInput Method Editor)の定義について

https://ja.wikipedia.org/wiki/インプット_メソッド_エディタ
https://learn.microsoft.com/en-gb/windows/apps/design/input/input-method-editors

アイヌ語の場合、表音文字の入力であれば、本来だったら英語など、キーボードだけで入力しても良さそうな気がする。ローマ字で入力する場合は、英語キーボードだけでも足りるように、あまり需要はない。問題なのがカタカナ表記の入力で、簡単にアイヌ語カタカナを入力するのが少々難しい。日本語のカタカナと圧倒的に違うところは、日本語のカタカナはほぼ純音節文字(音拍文字)なのに対して、小描きカナによって、音素文字の特徴も見られるような、半音節音素文字のように見える。

ヨーロッパのラテン・ギリシア・キリルなどの、線形アルファベットなら、キーボードだけで足りるので、全く問題なく入力できる。モンゴル文字や満洲文字も線形音素文字で、方向が特殊なのと表示がかなり難しいこと以外は、キーボード入力である程度足りる。他に線形なアブジャドなら、書写順はどうであろうが、キーボード入力で足りないことはないが、アラビア文字やその変種は特殊記号を入れることでアルファベットの要素も交じる、半子音音素文字や完全音素文字にもなっていたりするが、線形ではなくなるけどあくまで表示上の都合になるので、影響がない。ハングルのような音節単位にまとめる音素文字には、表示上の都合と変換上の都合がある(どこまでが一つの音節の境界かを判断する必要がある)が、前後まとめて変換すれば問題なさそう。

日本語のひらがなやカタカナだけやチェロキー文字のような音節文字の場合は、殆どアルファベットと変わらず、ただ子音と母音がペアになって次の入力を待つ必要があるだけ。より複雑なアブギダでデヴァナーガリー、ミャンマー文字やチベット文字など、順番を入れ替えたり音ベースか形ベースかで若干むずかしくなり、特に子音字を小さく書いて付け字とする点では似ている。アイヌ語カタカナはカナダ原住民文字系と似て、音節文字ベースで末子音を小書きすることで対応しているが、あくまで線形なので非線形のものと比べればそれほど難しいというわけではないので、予測変換なしで打つことも全然可能で、子音が連続する所や子音で終わるところがあればそこで切ると解決する。ちなみに、中国語や日本語の難しいところは漢字変換で、中国語ならほぼ一字一音で、まあ漢字の字音ベースと字形ベースのエンコーディングを入れることで対応可能だが、日本語の入力は音訓や語形変化など非常に複雑だが、概ね線形で音コードで入れて、そこから変換候補を作るので、まあ突き詰めていけばそれほど難しくもない

しかし、スマホというものは、パソコン時代のデザインを継承しつつも、UI/UXの分野を全く一新させていて、しかもパソコンのデザインに逆に影響を与えるようになった。IMEも同様で、予測変換という、東アジアの漢字くらい、つまり中国語や日本語以外、入力候補や予測変換というものはそもそも存在しておらず、表音文字であれば何語を打とうが、アルファベットで音声や音声を表して文字の形を打つだけで済んでいるが、携帯電話、PDAやスマホなど小画面の不便さによって、キーボードのように用意に表音文字を打つことが困難ということで、こういった東アジア以外の文字もIMEの特徴が輸入されている。だから、むしろ更に逆入力したほうが便利なのではないかとも思う。
https://note.com/ritar/n/nb2dc879b92bb

なので、ここで作たいものは、もちろんアイヌ語のキーボードとして、アイヌ語カタカナの入力をする機能が必須だが、設定などで例えばアイヌ語ローマ字入力でも、入力候補や予測変換、スペルチェックなどの高度な機能もオプションとして付ける。あとアイヌ語表記や方言の多様性も鑑みて、豊富な設定選択を与えることで、個々人でカスタマイズ可能のものにしたい。

mkpolimkpoli

IME の要件

https://learn.microsoft.com/en-gb/windows/apps/design/input/input-method-editor-requirements

Windows 8 の変更

https://learn.microsoft.com/en-us/windows/win32/w8cookbook/third-party-input-method-editors#manifestation

  • モードアイコンは一個になった?
  • 互換性のフラグTF_INPUTPROCESSORPROFILE を設置しなければWindows Appでは動かない(恐らくITfInputProcessorProfileMgrなどを使えば自動的にフラグをマークできる)
  • デジタルサインニングをしなければ、Windows Defenderにブロックされたり、ユーザーにクリティカルなメッセージを表示する
  • Windows APPで動く際に同じAPPのコンテナに制約され、辞書ファイル・オンラインアップデータ・同時学習・プロセス情報通信などが影響される
mkpolimkpoli

バーチャルマシンでのテスト環境整備

自分のPCでIMEをインストールしてしまうと、コードエディタに影響してしまうため、Hyper-VのVMで動かすとデバッグもやりやすいらしいので、それを行う。ついでにWindows 11をWindows 10で試してみる。

ちなみに最初はVirtualBoxでやろうとしていたが、なぜかある時から起動しなくなってて、今はむしろHyper-Vの方がいいらしい。

https://qiita.com/shinjimorimitsu/items/62932c7355688808439d
https://note.com/nerone1024/n/ncfffc882cf0f

なんかカッコいい、起動が結構遅い

きたああー!Windows 11だあ!

綺麗になったね

うおおおおなんかカッコいい

YouTube普通に見れる!なんならこっちのシステムが無駄なものがないから軽い

マイクロソフトのチュートリアルに参照してローカルハードディスクのアクセスを設置する
https://learn.microsoft.com/en-us/windows-server/virtualization/hyper-v/learn-more/use-local-resources-on-hyper-v-virtual-machine-with-vmconnect

するとRedirected drivers and foldersが表示され、ホストPCのアクセスができるようになります

mkpolimkpoli

DllRegisterServerがやることは基本的にレジストリーに必要なものを登録して、DLLをWindowsに認識させて、システムトレーにIMEとして提供することらしい。
https://github.com/aiongg/khiin-rs/tree/master/windows

ひとまずテスト実装でメッセージボックスを表示させてみる。

const std = @import("std");
const testing = std.testing;

const win = std.os.windows;

const WINAPI = win.WINAPI;
const HINSTANCE = win.HINSTANCE;
const DWORD = win.DWORD;
const LPVOID = win.LPVOID;
const BOOL = win.BOOL;
const HWND = win.HWND;
const LPCSTR = win.LPCSTR;
const UINT = win.UINT;
const STDAPI = win.HRESULT;

extern "user32" fn MessageBoxA(hWnd: ?HWND, lpText: LPCSTR, lpCaption: LPCSTR, uType: UINT) callconv(WINAPI) i32;

fn messageBox(
    text: [*:0]const u8,
    caption: [*:0]const u8,
) void {
    _ = MessageBoxA(null, text, caption, 0);
}

export fn DllCanUnloadNow() STDAPI {
    messageBox("DllCanUnloadNow", "Zig");
    return 0;
}

export fn DllGetClassObject() STDAPI {
    messageBox("DllGetClassObject", "Zig");
    return 0;
}

export fn DllRegisterServer() STDAPI {
    messageBox("DllRegisterServer", "Zig");
    return 0;
}

export fn DllUnregisterServer() STDAPI {
    messageBox("DllUnregisterServer", "Zig");
    return 0;
}

そして、regsvr32で登録してみると、ちゃんとDllRegisterServerが表示されます。

regsvr32.exe .\ainuKey.dll


登録取り消ししたら、ちゃんとDllUnregisterServerが表示されます。

regsvr32.exe .\ainuKey.dll


mkpolimkpoli

アイコン

システムトレイに表示するためのアイコン(Figmaで手作り、暫定)

サービスアイコン

カナ英字切替

mkpolimkpoli

https://zenn.dev/link/comments/bda919c75b2b2c の続き

レジストリーの登録

IME類のTextServiceは、COMの登録をし、Windowsのレジストリーにサーバー、プロフィール、カテゴリ情報を登録しなければならない。

サーバーの登録

登録

具体的に以下のキー及びサブキーを作る必要がある({00000000-0000-0000-0000-000000000000}は任意のGUIDに設定する)。

HKEY_CLASSES_ROOT\CLSID\{00000000-0000-0000-0000-000000000000}
HKEY_CLASSES_ROOT\CLSID\{00000000-0000-0000-0000-000000000000}\InprocServer32
HKEY_CLASSES_ROOT\CLSID\{00000000-0000-0000-0000-000000000000}\InprocServer32\ThreadingModel

まだWindowsのレジストリー操作専用のライブラリの存在を知らないので、ひとまず手動で行いたいと思う。
https://github.com/squeek502/watchedoverlay/blob/master/src/registry.zig

ここのコード(BSD Zero Clause License)をお借りしてきて、レジストリーへの登録を行う。

src/dllroot.zig
export fn DllRegisterServer() STDAPI {
    registry.registerServer(NAME, dll_file_name_w, GUID) catch |err| switch (err) {
        error.AccessDenied => return E_ACCESSDENIED,
        error.Unexpected => return E_UNEXPECTED,
    };
    
    messageBox("Success!", "DllRegisterServer");
    return 0;
}
src/registry.zig
pub fn registerServer(comptime service_name: UTF16StringLiteral, dll_path: UTF16String, comptime guid: UTF16StringLiteral) !void {
    const clsid_key = CLSID ++ guid;
    const inproc_key = CLSID ++ guid ++ InprocServer32;
    try registry.createAndSetStringValue(HKEY_CLASSES_ROOT, clsid_key, null, service_name);
    try registry.createAndSetStringValue(HKEY_CLASSES_ROOT, inproc_key, null, dll_path);
    const threading_model_name = std.unicode.utf8ToUtf16LeStringLiteral("ThreadingModel");
    const threading_model_value = std.unicode.utf8ToUtf16LeStringLiteral("Apartment");
    try registry.createAndSetStringValue(HKEY_CLASSES_ROOT, inproc_key, threading_model_name, threading_model_value);
}

注意すべきは、管理者権限でregsvr32.dllを実行しなければレジストリーに書き込むことが失敗してしまい、ACCESSDENIEDのコードが出てくる場合ある。

また、一旦DLLが搭載されてしまうと、他のアプリにすぐ注入してしまうので、DLLファイル自体を書き込めなくなるようにロックされるので、再コンパイルができなため、簡単なPowerShellスクリプトを書いて、ファイルのロックをVM内部に制限する。

cp.ps1
$dir = "C:\Users\User\Desktop\ainuKey\"
if (!(Test-Path $dir)) {
    New-Item -ItemType directory -Path $dir
}
Copy-Item -Path $PSScriptRoot\zig-out\lib\ainuKey.dll -Destination $dir

# Print the path of the copied file
Write-Host "Copied ainuKey.dll to $dir\ainuKey.dll"

管理者権限のターミナルで開くとちゃんとSuccessと出ている。

Registry Editorからも正しく登録されていることが確認できる。

解除

解除もお忘れなく。

src/registry.zig
pub fn unregisterServer(comptime guid: UTF16StringLiteral) !void {
    const clsid_key = CLSID ++ guid;
    try registry.deleteTree(HKEY_CLASSES_ROOT, clsid_key);
}
src/dllroot.zig
export fn DllUnregisterServer() STDAPI {
    messageBox("DllUnregisterServer", "Zig");

    registry.unregisterServer(GUID) catch |err| switch (err) {
        error.AccessDenied => return E_ACCESSDENIED,
        error.Unexpected => return E_UNEXPECTED,
    };

    messageBox("Success!", "DllUnregisterServer");
    return 0;
}

するとちゃんと削除されていることが確認できる。

mkpolimkpoli

言語プロフィールの登録

うーん、アイヌ語はそもそもサポートされていないな
https://learn.microsoft.com/ja-jp/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c

ここのマジックナンバーデータベースで検索してみたところ
https://www.magnumdb.com/search?q=LOCALE_CUSTOM_UNSPECIFIED+

4096、即ち 0x1000 = LOCALE_UNASSIGNED_LCID = LOCALE_CUSTOM_UNSPECIFIED = (winnt.h) (MAKELCID(MAKELANGID(LANG_NEUTRAL,SUBLANG_CUSTOM_UNSPECIFIED),SORT_DEFAULT))

サポートされていない言語、指定されていない言語ということになる。

https://github.com/Alexpux/mingw-w64/blob/master/mingw-w64-tools/widl/include/winnt.h

マイクロソフト側に何か動きがない限り、カスタム言語を設定するしかないかもしれない。さすがに日本語と設定するのは正しくないと思う。

Keymanや他の一部のキーボード/IMEには独自に言語設定を行って、何かしらの方法でシステムに言語を追加しているが、どうやってやっているのか、どの範囲でやっているのかがまだわからない。

一応Windowsではカスタムロケールを設定することができるとのこと、また、ロケールを自作するソフトも公開されている。

https://learn.microsoft.com/en-us/windows/win32/intl/custom-locales
https://www.microsoft.com/en-us/download/details.aspx?id=41158

こうやって設定していくことができる。

mkpolimkpoli

サーバー、プロフィール、カテゴリの3つを登録しなければ、言語バーには現れない。少なくとも現時点でプロフィールを登録するためのITfInputProcessorProfiles(あるいはITfInputProcessorProfileMgr)及びITfCategoryManagerはZigのラッパーが用意されていないので、自分で実装しなければならない。実装はsrc/windows/profile.zig及びsrc/windows/category.zigを参照されたし。(下参照)

プロフィール

要はITfInputProcessorProfilesRegister()メソッド及びAddLanguageProfile()メソッドを呼び出し、IMEのText ServiceのGUID、プロフィールのGUID(別で新しく作る)、ロケールID(Windowsに予め定められた数字)、説明文字(右下の言語バーに表示されるもの)、アイコンの場所(dllの中に埋め込み)、アイコンの番号(RCファイルの番号)を渡して登録する。登録解除も同様。

pub fn registerProfile(
    comptime language: UTF16StringLiteral,
    dll_path: UTF16String,
    comptime description: UTF16StringLiteral,
    comptime guid: Guid,
    comptime guid_profile: Guid,
) !void {
    const locale_id = LocaleNameToLCID(language, 0);
    const profiles = profile.createProfileManager() orelse {
        messageBox("Failed to create profile manager", "registerProfile", .Error);
        unreachable;
    };
    const icon_path = dll_path;
    _ = ITfInputProcessorProfiles.ITfInputProcessorProfiles_Register(
        profiles,
        &guid,
    );
    const locale_id_u16: u16 = @intCast(locale_id);
    _ = ITfInputProcessorProfiles.ITfInputProcessorProfiles_AddLanguageProfile(
        profiles,
        &guid,
        locale_id_u16,
        &guid_profile,
        @ptrCast(description.ptr),
        @intCast(description.len),
        @ptrCast(icon_path.ptr),
        @intCast(icon_path.len),
        0,
    );
}

カテゴリ

同様。

pub const GUID_TFCAT_DISPLAYATTRIBUTEPROVIDER: Guid = Guid.initString("046b8c80-1647-40f7-9b21-b93b81aabc1b");
pub const GUID_TFCAT_TIPCAP_COMLESS: Guid = Guid.initString("364215d9-75bc-11d7-a6ef-00065b84435c");
pub const GUID_TFCAT_TIPCAP_INPUTMODECOMPARTMENT: Guid = Guid.initString("ccf05dd7-4a87-11d7-a6e2-00065b84435c");
pub const GUID_TFCAT_TIPCAP_UIELEMENTENABLED: Guid = Guid.initString("49d2f9cf-1f5e-11d7-a6d3-00065b84435c");
pub const GUID_TFCAT_TIP_KEYBOARD: Guid = Guid.initString("34745c63-b2f0-4784-8b67-5e12c8701a31");
pub const GUID_TFCAT_TIPCAP_IMMERSIVESUPPORT: Guid = Guid.initString("13a016df-560b-46cd-947a-4c3af1e0e35d");
pub const GUID_TFCAT_TIPCAP_SYSTRAYSUPPORT: Guid = Guid.initString("25504fb4-7bab-4bc1-9c69-cf81890f0ef5");
pub fn registerCategories(
    comptime guid: Guid,
) !void {
    const category_manager: *ITfCategoryMgr = category.createCategoryManager() orelse {
        messageBox("Failed to create category manager", "registerCategories", .Error);
        unreachable;
    };
    for (SUPPORTED_CATEGORIES) |guid_cat| {
        _ = ITfCategoryMgr.ITfCategoryMgr_RegisterCategory(
            category_manager,
            &guid,
            &guid_cat,
            &guid,
        );
    }
}
mkpolimkpoli

↑上で自分でインタフェース実装したところ多分無駄骨だった説濃厚

ITfInputProcessorProfilesCoCreateInstanceをGithubで検索してもなかったが、普通にあったねんけど……???はあ?たしか午前中もローカルで何度も検索してみた覚えはあるけど、なぜ見つかった記憶無いんだろう…謎すぎる(多分なかなか成果が出ず焦っていたんだろうけど、あまりにも普通に鎮座していて理解できん

まあそれは良いとして、ITfInputProcessorProfilesは古いインタフェースで、Windows Vista以降はITfInputProcessorProfileMgrを使った方がいいらしいので、それを使う。

まず、ITfInputProcessorProfilesITfInputProcessorProfileMgr両方が同じCLSID_TF_InputProcessorProfiles = GUID{33c53a50-f456-4884-b049-85fd643ecfed}というCoCreateInstanceのrclsid(クラスID)に渡すものがあるが、それぞれ違うriid(インタフェースID)を持っているので、恐らくそれでWindowsによって判別されているであろう。

以下は修正版コード、詳細はレポジトリの内容をご参照ください。

pub fn registerProfile(
    dll_path: UTF16String,
    comptime description: UTF16StringLiteral,
    comptime guid: Guid,
    comptime guid_profile: Guid,
    comptime locale_id: u16,
) !void {
    const profile_manager = profile.createProfileManager() orelse {
        messageBox("Failed to create profile manager", "registerProfile", .Error);
        unreachable;
    };

    const icon_path = dll_path;

    _ = ITfInputProcessorProfileMgr.ITfInputProcessorProfileMgr_RegisterProfile(
        profile_manager,
        &guid,
        locale_id,
        &guid_profile,
        @ptrCast(description.ptr),
        @intCast(description.len),
        @ptrCast(icon_path.ptr),
        @intCast(icon_path.len),
        0,
        std.mem.zeroes(?win32.ui.text_services.HKL),
        0,
        @intFromBool(true),
        0,
    );
}
mkpolimkpoli

重要な概念の整理

TSFのサービス開発もWindowsにおけるCOM(Component Object Model)開発の一部であり、COMにおける概念を含んでいる。COMコンポーネントには、GUIDのクラスID(CLSID)が制作者によって割り当てられ、それによってコンポーネントが区別される。コンポーネントは複数のインタフェースを公開し、それぞれのインタフェースにはGUIDのインタフェースID(IID)が割り当てられている。インタフェースはすべてIUnknownを継承しなければならない。
// TODO:

スレッドマネジャー

クライエント識別子

テキスト・インプット・プロセッサー

ITfTextInputProcessorインタフェースを実装したクラスはTIPとも略され、テキストサービスの本体。

ドキュメントマネジャー

編集コンテクスト

範囲

プロパティ

コンパートメント

コンパートメント(compartment、区分)とは、COMのアパートメントモデルスレッド間の状態管理の行うための区分である。

コンポジション

mkpolimkpoli

DllGetClassObjectの変更は、古いDLLが搭載されたままだと、新たにアプリケーション(例えばnotepad.exe)を起動しなければ作動しないので、一回試すものを閉じておく必要がある。反応がなかったので、試してみたらいけた、よくわかったな……

ただ、毎回ainuKeyに切り替えると実行されるので、変化していると錯覚しやすいため、ビルドのバージョン番号を手入力で付けてみた

手入力は流石にめんどくさいので、こちらの記事を参考に、ビルド時間を出すようにした。
https://zenn.dev/catallaxy_dev/articles/ziglang-how-to-get-date

build.zig
const std = @import("std");
const fs = std.fs;
const fmt = std.fmt;
pub fn build(b: *Build) !void {
    const now = std.time.epoch.EpochSeconds{ .secs = @intCast(std.time.timestamp()) };
    const month_day = now.getEpochDay().calculateYearDay().calculateMonthDay();
    const day_seconds = now.getDaySeconds();
    const month = month_day.month.numeric();
    const day = month_day.day_index + 1;
    const hour = day_seconds.getHoursIntoDay();
    const minute = day_seconds.getMinutesIntoHour();

    var version_file = try fs.cwd().createFile("VERSION", .{});
    defer version_file.close();

    const version = try fmt.allocPrint(std.heap.page_allocator, "{d:0>2}/{d:0>2} {d:0>2}:{d:0>2}\n", .{ month, day, hour, minute });
    try version_file.writeAll(version);

    // ...
}

いやファイルはまだめんどくさいので、ビルドステップで直接importとして入れるようにした。記事を書いた。
https://zenn.dev/mkpoli/articles/620223f8054b03

mkpolimkpoli

DllGetClassObjectにて、IUnknown.QueryInterfaceでポインタを渡して、リファレンス・カウントを実装して、IClassFactoryを実装したなんちゃらClassFactoryを作って、ITfTextInputProcessorを実装したなんちゃらTextServiceを作ったら、最初にCOMが搭載されたTSFアプリケーション、例えばnotepad.exeを起動すれば、DllGetClassObjectが呼び出される(DllRegisterServerDllUnregisterServerregsvr32した瞬間に呼び出されるのとは対照的)。

もしClassFactory及びTextServiceが正しく呼び出せたら、TextService.Activate()及びTextService.Deactivate()が呼び出されるはず。その間にテキストの処理を行い、それが終わったら消える。


Deactivated前にちゃんとアイコンになっているのが確認できる。

詳細のコードは以下の通りである。

https://github.com/mkpoli/ainuKey/blob/master/src/factory.zig

https://github.com/mkpoli/ainuKey/blob/master/src/service.zig

mkpolimkpoli

ここ数日間クラッシュ問題にハマってしまっている。具体的には、

  1. ITfTextInputProcessor::Activate()において、ITfLangBarItemButton(implements ITfLangBarItem implements ITfUnknown)及び
    ITfKeyEventSinkを登録(Advise)しようとしたらテキストアプリケーション(notepad.exe)のプロセスがクラッシュする。
  2. Activate()の後に即Deactivate()される。

という問題にぶつかっている。2の原理はわからないが、少なくともDllCanUnloadNow()とは無関係で、1が解決したらもしかしたら自動的に2が解決されるかもしれない。1は本当に意味不明で、デバッガーとかをかける必要があるかもしれない。有識者求む。


判明しているのは、ITfKeystrokeMgr::AdviseKeyEventSink()ITfSource::AdviseSink()を実行したあと、マウスのカーサーがビジーになって、しばらくしたら反応がない。この時クリックしたらnotepad.exeがクラッシュするが、クリックしなければ静かに失敗する。しかも、リザルトが返されることさえされずそのまま内部で例外が発生したような気がする。

問題箇所

_ = keystroke_manager.ITfKeystrokeMgr_AdviseKeyEventSink(
    self.client_id,
    @ptrCast(self),
    TRUE,
);

パラメータも特に違和感がないんよな……


WinDbgでアクセス違反と出ている、ということはやはりメモリー管理が失敗している?


まって、この42って、クライエントIDじゃん、42は絶対メモリーじゃないから、client_idとポインタを取り違えたということか!!!


client_id がメモリアドレスとして受け取られていることは間違いないようだ。

        const result = ITfKeystrokeMgr.ITfKeystrokeMgr_AdviseKeyEventSink(
            keystroke_manager,
            0xdeadbeef,
            // self.client_id,
            @ptrCast(self),
            TRUE,
        );


数日間全く進捗はないが、今日逆にITfLangBarItemButtonCompartmentの方を試したら、同じアクセス違反で、ただし今回はなんと実行するコードが自身のところに書き込もうとしている。

MSCTF!TF_SetShowFloatingStatus+0x5f078:
00007ffe`d05fbe98 706d            jo      MSCTF!TF_SetShowFloatingStatus+0x5f0e7 (00007ffe`d05fbf07) [br=0]
 *** An Access Violation occurred in notepad:

The instruction at 00007FFED05FBE98 tried to write to an invalid address, 00007FFED05FBE98

 *** enter .exr 000000649C7DDDB0 for the exception record
 ***  enter .cxr 000000649C7DD8C0 for the context
 *** then kb to get the faulting stack
07 00000000`54956d14     : 00000064`9c7de1c8 00000064`9c7de1c8 0000016b`a1f5c9b8 00000000`00000000 : MSCTF!TF_SetShowFloatingStatus+0x5f078
08 00000000`5494a47c     : 0000016b`a1f25590 00000000`00000000 00000000`00000009 00007ffe`bfcd0002 : ainuKey!advise+0xe4 [C:\Users\mk\Desktop\ainuKey\src\component\compartment.zig @ 150]