Windows版アイヌ語IMEをZigとRustで作っていく
Githubに公開してリアルタイム開発中
モチベーション
アイヌ語専用の入力まだできてないの???
Windows上
時間あったら作るか
(Discordサーバーにて)
Twitter をしばらく調べまくっていたが、情報ありませんでした
実はそれこそ数年前から作りたいと思っていたが、システムプログラミングに暗いもんで
C とかで Windows API と戦う機会はなくはなかったけど、さすがに今どき潔癖症で使いたくない
Rust とか Zig で作りたいけど、結構めんどくさいらしいので、暫く手を出せずにいる状態
(Discordサーバーにて)
というより、普通 Windows のような PC 上は、英語やドイツ語、フランス語など、アルファベットを使う言語は、伝統的に変換入力がないので
むしろ流れで入力するのが普通かもしれなくて、予測変換は日本語や中国語など、特殊なケース
でも学習者のためには予測変換があってもいい気がするので、作っていきたいですね
(Discordサーバーにて)
先行研究
アイヌ語入力
辞書登録法
日本語入力に、アイヌ語用カナをユーザー辞書などで搭載する方法。https://note.com/xiupos/n/ncaedc77c00a0?sub_rt=share_h]
言及
要望や言及がそこそこある。
- 「まだそんな段階なのか。」
- 「「イランカラㇷ゚テ」がまともに入出力できないことに問題意識を持たないのはおかしい。」
- horkewでホㇿケウになってくれれば嬉しいのですが。
- Windowsはアイヌ語入力機能が付いてないんか。遅れてるな。
- iOS/macOSのアイヌ語キーボードもスペルチェッカーとかサジェストがないから、もっといいのほしいと思ってる...(Discordサーバー)
- Xで「アイヌ語 入力」を検索
- Xで「アイヌ語 IME」を検索
-
Xで「アイヌ語 キーボード」を検索
https://twitter.com/cicada3301_kig/status/1031433502909063168
アイヌ語変換
入力方式(IME)
Zig と Rust のインターロッパビリティー
Windowsにおける入力方式、IME、即ちText Service FrameworkのAPIを利用したアプリケーションとは、本質的には一個のDLLで、DllGetClassObject
、DllCanUnloadNow
、DllRegisterServer
、DllUnregisterServer
の四つの関数を露出している。
The input method is essentially a dll file which exports four interfaces
EXPORTS
DllGetClassObject PRIVATE
DllCanUnloadNow PRIVATE
DllRegisterServer PRIVATE
DllUnregisterServer PRIVATE
Text Service Framework のアーキテクチャは、アプリ(例えば notepad.txt) ⇄ TSF Manager ⇄ サービス(IME、なんちゃら.dll)
TSFの管理ソフト(TSF Manger)は
システムに登録する方法は管理者権限を持つ(elevated)コマンドラインで
regsvr32 "MyTextService.dll"
Text Serviceが一旦システム登録されたら、Windowsが自動的にDLLをすべてのフォアグラウンドアプリにロードされる。
32bitと64bitのアプリケーション両方に利用できるように、32bit DLLと64bit DLL両方をビルドした方が良さそう?自動的にWindowsが選んでくれるのでターゲットを追加してファイルネームを同じにするだけで済むらしい。
ImmInstallIME()
関数でレジストリキーが作られが、どれか一つのDLLに一回実行させる。
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layouts
ITfInputProcessorProfilesなどで言語情報を登録する必要がある。
言語バー
スペルチェックなどの特殊効果はDisplay Attributesで設定
IMEとは?
IME(Input Method Editor)の定義について
アイヌ語の場合、表音文字の入力であれば、本来だったら英語など、キーボードだけで入力しても良さそうな気がする。ローマ字で入力する場合は、英語キーボードだけでも足りるように、あまり需要はない。問題なのがカタカナ表記の入力で、簡単にアイヌ語カタカナを入力するのが少々難しい。日本語のカタカナと圧倒的に違うところは、日本語のカタカナはほぼ純音節文字(音拍文字)なのに対して、小描きカナによって、音素文字の特徴も見られるような、半音節音素文字のように見える。
ヨーロッパのラテン・ギリシア・キリルなどの、線形アルファベットなら、キーボードだけで足りるので、全く問題なく入力できる。モンゴル文字や満洲文字も線形音素文字で、方向が特殊なのと表示がかなり難しいこと以外は、キーボード入力である程度足りる。他に線形なアブジャドなら、書写順はどうであろうが、キーボード入力で足りないことはないが、アラビア文字やその変種は特殊記号を入れることでアルファベットの要素も交じる、半子音音素文字や完全音素文字にもなっていたりするが、線形ではなくなるけどあくまで表示上の都合になるので、影響がない。ハングルのような音節単位にまとめる音素文字には、表示上の都合と変換上の都合がある(どこまでが一つの音節の境界かを判断する必要がある)が、前後まとめて変換すれば問題なさそう。
日本語のひらがなやカタカナだけやチェロキー文字のような音節文字の場合は、殆どアルファベットと変わらず、ただ子音と母音がペアになって次の入力を待つ必要があるだけ。より複雑なアブギダでデヴァナーガリー、ミャンマー文字やチベット文字など、順番を入れ替えたり音ベースか形ベースかで若干むずかしくなり、特に子音字を小さく書いて付け字とする点では似ている。アイヌ語カタカナはカナダ原住民文字系と似て、音節文字ベースで末子音を小書きすることで対応しているが、あくまで線形なので非線形のものと比べればそれほど難しいというわけではないので、予測変換なしで打つことも全然可能で、子音が連続する所や子音で終わるところがあればそこで切ると解決する。ちなみに、中国語や日本語の難しいところは漢字変換で、中国語ならほぼ一字一音で、まあ漢字の字音ベースと字形ベースのエンコーディングを入れることで対応可能だが、日本語の入力は音訓や語形変化など非常に複雑だが、概ね線形で音コードで入れて、そこから変換候補を作るので、まあ突き詰めていけばそれほど難しくもない
しかし、スマホというものは、パソコン時代のデザインを継承しつつも、UI/UXの分野を全く一新させていて、しかもパソコンのデザインに逆に影響を与えるようになった。IMEも同様で、予測変換という、東アジアの漢字くらい、つまり中国語や日本語以外、入力候補や予測変換というものはそもそも存在しておらず、表音文字であれば何語を打とうが、アルファベットで音声や音声を表して文字の形を打つだけで済んでいるが、携帯電話、PDAやスマホなど小画面の不便さによって、キーボードのように用意に表音文字を打つことが困難ということで、こういった東アジア以外の文字もIMEの特徴が輸入されている。だから、むしろ更に逆入力したほうが便利なのではないかとも思う。
なので、ここで作たいものは、もちろんアイヌ語のキーボードとして、アイヌ語カタカナの入力をする機能が必須だが、設定などで例えばアイヌ語ローマ字入力でも、入力候補や予測変換、スペルチェックなどの高度な機能もオプションとして付ける。あとアイヌ語表記や方言の多様性も鑑みて、豊富な設定選択を与えることで、個々人でカスタマイズ可能のものにしたい。
安全設定
MicrosoftはIMEに対してサインするように要求されている
コードサイニング
CA(Certificate Authority)から証明書を購入しなければならない。値段は十万円/年程度。
// (TODO...)
IME の要件
- 安全要件
- TSFを使わないければならない
- システムトレイとの互換性(どういうこと?ついには理解できなかった…
-
アプリコンテナーで作動すること
- 辞書ファイルなどのデータは、Program FilesやWindowsにある必要がある
- インタネット接続は必ずしも許されないので、辞書更新などは別途デスクトップ・アプリを作る必要がある
- アプリ間のデータ共有はWebサービスで行う(なんで?
- (一旦無視)タッチUI関連
- (無視)Windows 8 検索窓関連
-
UIデザインのガイドライン
- 必要時だけオンデマンドで現れる
- IMEアイコンはモノクロでないといえけない
- 所有されたWindowである必要がある
- 軽く簡単に閉じることができる
- DPIスケーリングの準拠
- インストーラーを提供
- アクセシビリティ
Windows 8 の変更
- モードアイコンは一個になった?
- 互換性のフラグ
TF_INPUTPROCESSORPROFILE
を設置しなければWindows Appでは動かない(恐らくITfInputProcessorProfileMgr
などを使えば自動的にフラグをマークできる) - デジタルサインニングをしなければ、Windows Defenderにブロックされたり、ユーザーにクリティカルなメッセージを表示する
- Windows APPで動く際に同じAPPのコンテナに制約され、辞書ファイル・オンラインアップデータ・同時学習・プロセス情報通信などが影響される
バーチャルマシンでのテスト環境整備
自分のPCでIMEをインストールしてしまうと、コードエディタに影響してしまうため、Hyper-VのVMで動かすとデバッグもやりやすいらしいので、それを行う。ついでにWindows 11をWindows 10で試してみる。
ちなみに最初はVirtualBoxでやろうとしていたが、なぜかある時から起動しなくなってて、今はむしろHyper-Vの方がいいらしい。
なんかカッコいい、起動が結構遅い
きたああー!Windows 11だあ!
綺麗になったね
うおおおおなんかカッコいい
YouTube普通に見れる!なんならこっちのシステムが無駄なものがないから軽い
マイクロソフトのチュートリアルに参照してローカルハードディスクのアクセスを設置する
するとRedirected drivers and foldersが表示され、ホストPCのアクセスができるようになります
DllRegisterServer
がやることは基本的にレジストリーに必要なものを登録して、DLLをWindowsに認識させて、システムトレーにIMEとして提供することらしい。
ひとまずテスト実装でメッセージボックスを表示させてみる。
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
アイコン
システムトレイに表示するためのアイコン(Figmaで手作り、暫定)
サービスアイコン
カナ英字切替
ZigでCOMの実装、例のsqueek502氏
期せずして私が見ないで作ったファイル構造や関数名など偶然一致していてなんか嬉しくなったsqueek502氏のブログやプロジェクト結構面白い
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のレジストリー操作専用のライブラリの存在を知らないので、ひとまず手動で行いたいと思う。
ここのコード(BSD Zero Clause License)をお借りしてきて、レジストリーへの登録を行う。
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;
}
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内部に制限する。
$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からも正しく登録されていることが確認できる。
解除
解除もお忘れなく。
pub fn unregisterServer(comptime guid: UTF16StringLiteral) !void {
const clsid_key = CLSID ++ guid;
try registry.deleteTree(HKEY_CLASSES_ROOT, clsid_key);
}
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;
}
するとちゃんと削除されていることが確認できる。
言語プロフィールの登録
うーん、アイヌ語はそもそもサポートされていないな
ここのマジックナンバーデータベースで検索してみたところ
4096、即ち 0x1000 = LOCALE_UNASSIGNED_LCID
= LOCALE_CUSTOM_UNSPECIFIED
= (winnt.h) (MAKELCID(MAKELANGID(LANG_NEUTRAL,SUBLANG_CUSTOM_UNSPECIFIED),SORT_DEFAULT))
サポートされていない言語、指定されていない言語ということになる。
マイクロソフト側に何か動きがない限り、カスタム言語を設定するしかないかもしれない。さすがに日本語と設定するのは正しくないと思う。
Keymanや他の一部のキーボード/IMEには独自に言語設定を行って、何かしらの方法でシステムに言語を追加しているが、どうやってやっているのか、どの範囲でやっているのかがまだわからない。
一応Windowsではカスタムロケールを設定することができるとのこと、また、ロケールを自作するソフトも公開されている。
こうやって設定していくことができる。
この感動よ……ようやく第一歩を踏み出せた気がした
サーバー、プロフィール、カテゴリの3つを登録しなければ、言語バーには現れない。少なくとも現時点でプロフィールを登録するための(下参照)ITfInputProcessorProfiles
(あるいはITfInputProcessorProfileMgr
)及びITfCategoryManager
はZigのラッパーが用意されていないので、自分で実装しなければならない。実装はsrc/windows/profile.zig
及びsrc/windows/category.zig
を参照されたし。
プロフィール
要はITfInputProcessorProfiles
のRegister()
メソッド及び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,
);
}
}
↑上で自分でインタフェース実装したところ多分無駄骨だった説濃厚
ITfInputProcessorProfiles
やCoCreateInstance
をGithubで検索してもなかったが、普通にあったねんけど……???はあ?たしか午前中もローカルで何度も検索してみた覚えはあるけど、なぜ見つかった記憶無いんだろう…謎すぎる(多分なかなか成果が出ず焦っていたんだろうけど、あまりにも普通に鎮座していて理解できん
まあそれは良いとして、ITfInputProcessorProfiles
は古いインタフェースで、Windows Vista以降はITfInputProcessorProfileMgr
を使った方がいいらしいので、それを使う。
まず、ITfInputProcessorProfiles
とITfInputProcessorProfileMgr
両方が同じ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,
);
}
重要な概念の整理
TSFのサービス開発もWindowsにおけるCOM(Component Object Model)開発の一部であり、COMにおける概念を含んでいる。COMコンポーネントには、GUIDのクラスID(CLSID)が制作者によって割り当てられ、それによってコンポーネントが区別される。コンポーネントは複数のインタフェースを公開し、それぞれのインタフェースにはGUIDのインタフェースID(IID)が割り当てられている。インタフェースはすべてIUnknown
を継承しなければならない。
// TODO:
スレッドマネジャー
クライエント識別子
テキスト・インプット・プロセッサー
ITfTextInputProcessor
インタフェースを実装したクラスはTIPとも略され、テキストサービスの本体。
ドキュメントマネジャー
編集コンテクスト
範囲
プロパティ
コンパートメント
コンパートメント(compartment、区分)とは、COMのアパートメントモデルスレッド間の状態管理の行うための区分である。
コンポジション
DllGetClassObject
の変更は、古いDLLが搭載されたままだと、新たにアプリケーション(例えばnotepad.exe
)を起動しなければ作動しないので、一回試すものを閉じておく必要がある。反応がなかったので、試してみたらいけた、よくわかったな……
ただ、毎回ainuKey
に切り替えると実行されるので、変化していると錯覚しやすいため、ビルドのバージョン番号を手入力で付けてみた
手入力は流石にめんどくさいので、こちらの記事を参考に、ビルド時間を出すようにした。
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として入れるようにした。記事を書いた。
DllGetClassObject
にて、IUnknown.QueryInterface
でポインタを渡して、リファレンス・カウントを実装して、IClassFactory
を実装したなんちゃらClassFactory
を作って、ITfTextInputProcessor
を実装したなんちゃらTextService
を作ったら、最初にCOMが搭載されたTSFアプリケーション、例えばnotepad.exe
を起動すれば、DllGetClassObject
が呼び出される(DllRegisterServer
やDllUnregisterServer
がregsvr32
した瞬間に呼び出されるのとは対照的)。
もしClassFactory
及びTextService
が正しく呼び出せたら、TextService.Activate()
及びTextService.Deactivate()
が呼び出されるはず。その間にテキストの処理を行い、それが終わったら消える。
Deactivated前にちゃんとアイコンになっているのが確認できる。
詳細のコードは以下の通りである。
ここ数日間クラッシュ問題にハマってしまっている。具体的には、
-
ITfTextInputProcessor::Activate()
において、ITfLangBarItemButton
(implements ITfLangBarItem implements ITfUnknown
)及び
ITfKeyEventSink
を登録(Advise
)しようとしたらテキストアプリケーション(notepad.exe
)のプロセスがクラッシュする。 -
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,
);
数日間全く進捗はないが、今日逆にITfLangBarItemButton
のCompartment
の方を試したら、同じアクセス違反で、ただし今回はなんと実行するコードが自身のところに書き込もうとしている。
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]