現代日本語書き言葉均衡コーパス(BCCWJ)で漢直(T-Code)を研究する
ゆる言語学ラジオというpodcastを聞いていたところ, 「現代日本語書き言葉均衡コーパス」(BCCWJ)というものが紹介されていました. これは「書籍全般、雑誌全般、新聞、白書、ブログ、ネット掲示板、教科書、法律などのジャンルにまたがって1億430万語のデータを格納しており、各ジャンルについて無作為にサンプルを抽出」したコーパスとのことです.
さて, やはり漢字というのはなるべく変換しない方がいいわけですが, その解決策として漢字直接入力(漢直)というのがあります. たとえば, T-Codeという入力法では2打鍵の組み合わせで, 1つのひらがな・カタカナ・数学・記号・漢字が入力されます.
こうした漢直で問題になるのが, その打鍵の配列の評価です. なるべくホームポジション近くで, よく出現する文字を入力できれば, それは効率のよい配置と言えます. たとえば, T-Codeでは"kd"(QWERTY)で「の」と入力できますが, 「の」の出現率が高ければこれは妥当だということです.
ここでBCCWJの登場です. BCCWJには文字表というものがあり, これには各文字の出現頻度・100万字あたりの頻度が書かれています. これを使えば, ある漢直がどの程度の日本語文章を入力できるのかを評価できるはずです.
では, やってみましょう…とはいえ, ある打鍵の組がどれだけホームポジションに近いのか…は自明ではありません. そこで今回は漢直の練習テキストEELLLを代わりの指標として使います. この練習テキストは, いくつかのレッスンに分かれており, 最初は「の」や「、」から始まり, ひらがな・数字・カタカナ・漢字…と少しずつ新たな文字を導入していきます. レッスンに早く登場する文字はそれだけ重要だと考えられているはずです. つまり, 各文字がEELLLの中で何番目に出現するのかの順位を代替的な指標として使います.
EELLL内の順位(縦)と, BCCWJ文字表(Version 1.1)で100万文字あたりの頻度での順位(横)を比較してプロットします.
理想的にはy=xの直線状になるといいですが, そうはなっていませんね. これを見ると右上と左下はいい感じだが, 中間はびみょうな感じがします. つまり, 400文字ぐらいまでは出現頻度が高い文字が早めのレッスンに出ますし, 逆に頻度が低いものは後のレッスンになっています. その間はちょっとばらばらな感じです. また, 右下にちょこちょこ外れている子たちがいますね.
では, どんな文字がEELLLでの順位とBCCWJでの順位の乖離が大きいのでしょうか?
char | lesson | rank_eelll | freq | rank_bccwj | norm_sq | |
---|---|---|---|---|---|---|
129 | ぢ | 105 | 129 | 8.46803 | 1154 | 1050625 |
133 | ぺ | 106 | 131 | 18.15968 | 1144 | 1026169 |
131 | ぴ | 106 | 131 | 34.50698 | 1100 | 938961 |
132 | ぷ | 106 | 131 | 40.06188 | 1078 | 896809 |
127 | ゅ | 104 | 124 | 57.65840 | 998 | 763876 |
281 | 遇 | 305 | 282 | 25.28123 | 1124 | 708964 |
134 | ぽ | 106 | 131 | 72.06532 | 923 | 627264 |
338 | 即 | 311 | 335 | 40.59946 | 1077 | 550564 |
361 | 巨 | 314 | 362 | 46.38987 | 1050 | 473344 |
434 | 塚 | 323 | 434 | 27.55950 | 1119 | 469225 |
426 | 序 | 322 | 423 | 34.73737 | 1099 | 456976 |
469 | 熟 | 404 | 464 | 29.11078 | 1112 | 419904 |
433 | 浦 | 323 | 434 | 39.51919 | 1081 | 418609 |
342 | 募 | 311 | 335 | 61.56475 | 978 | 413449 |
370 | 華 | 315 | 371 | 56.05592 | 1004 | 400689 |
327 | 岩 | 310 | 326 | 67.49340 | 948 | 386884 |
415 | 羽 | 321 | 414 | 51.53520 | 1031 | 380689 |
458 | 卒 | 403 | 454 | 46.06733 | 1054 | 360000 |
372 | 迎 | 315 | 371 | 63.41297 | 966 | 354025 |
690 | 言 | 505 | 688 | 1363.83967 | 96 | 350464 |
「ぢ」「ぺ」「ぴ」などが上位にあります. これらはひらがななので初期レッスンには出るものの実際のところ, あまり登場しないということです. たしかにね. というか, 告白すると自分も「ぢ」についてはあまりに入力しないのでT-Codeでの入力方法がわかりません. これらのrank_eelllが130ぐらいで, rank_bccwjが1100ぐらい…つまり上のプロットの右下の点はこいつらということです.
漢字でいうと「遇」「即」「巨」があります. これらは…いやーあんまりよくわかりませんね…まあ出現しないわりにレッスンでは早いというところです.
上の表は多くの文字が(EELLL順位) < (BCCWJ順位)というものでした. つまり, レッスンが早すぎるものたちです. 逆にレッスンが遅すぎる - EELLLで過少評価されている文字を見てみましょう.
char | lesson | rank_eelll | freq | rank_bccwj | norm_sq | |
---|---|---|---|---|---|---|
690 | 言 | 505 | 688 | 1363.83967 | 96 | 350464 |
970 | 存 | 654 | 969 | 269.56401 | 412 | 310249 |
842 | 条 | 525 | 842 | 385.98666 | 311 | 281961 |
1034 | 識 | 661 | 1023 | 220.36853 | 492 | 281961 |
1094 | 離 | 670 | 1086 | 183.78294 | 571 | 265225 |
687 | 何 | 505 | 688 | 778.37298 | 174 | 264196 |
839 | 得 | 524 | 837 | 353.41494 | 332 | 255025 |
879 | 域 | 602 | 880 | 295.75143 | 378 | 252004 |
928 | 笑 | 605 | 923 | 253.58021 | 438 | 235225 |
1073 | 療 | 663 | 1059 | 182.12414 | 576 | 233289 |
1102 | 願 | 676 | 1098 | 161.55307 | 616 | 232324 |
1032 | 呼 | 661 | 1023 | 197.30926 | 545 | 228484 |
828 | 認 | 522 | 826 | 324.38607 | 349 | 227529 |
1047 | 君 | 662 | 1042 | 184.32563 | 568 | 224676 |
1025 | 影 | 661 | 1023 | 189.15353 | 557 | 217156 |
678 | 等 | 504 | 673 | 559.53526 | 224 | 201601 |
1003 | 移 | 659 | 1003 | 189.24569 | 556 | 199809 |
759 | 質 | 512 | 758 | 374.09353 | 316 | 195364 |
751 | 確 | 511 | 752 | 381.80896 | 313 | 192721 |
825 | 申 | 522 | 826 | 287.10420 | 389 | 190969 |
「言」「何」「得」などたしかに結構入力しそうな文字がありますが, これらはどれもレッスン全体の後半での出現です. 個人的にはプログラミング系のことを書いていると頻出する「呼」がちゃんとここに入っていて良かったです.
さて, 今回はBCCWJ文字表(Version 1.1)を使ってT-Codeの配列について研究してみました. この文字表を使うことで, 他にもEELLLでどのレッスンまでいけば文章中で何割の文字を入力できるようになるのか? 何文字の打鍵を覚えれば文章の何割を直接入力できるのか?が計測できます. それらは次回になります.
Waylandで日本語入力への道: Return of RVO編
Kernel/VM Advent Calendarの何日目かの記事です.
- 第1回 Waylandで日本語入力への道: 下調べ編 - Gentoo metalog
- 第2回 Waylandで日本語入力への道: vtableを作ろう編 - Gentoo metalog
- 第3回 Waylandで日本語入力への道: コンストラクタに届け編 - Gentoo metalog
- 第4回 Waylandで日本語入力への道: dynamic_castがならなくて編 - Gentoo metalog
- 第5回 Waylandで日本語入力への道: std::string の捏造編 - Gentoo metalog
前回のあらすじ: std::string(C++)のことを調べて, Rustで作れるようになった. でも, 小さい文字列用に最適化された状態にはできない… ちくしょう! アセンブラに相談だ!
DISCLAIMER: waylandで日本語入力したい時にこの記事は役に立たないし, RustでC++のbindingをしたい時にもやめた方がいいと思う.
文字列をやってるとだいぶ日本語入力に近付いてきた気持ちになるこのシリーズいかがおすごしでしょうか.
RVO?なんですか?
さて前回は, std::string(C++)の小さい文字列用フォーマットを作りたかったものの挫折したわけですが…. このへんをいろいろ調べていくと, Return Value Optimization (RVO)による現象なのかなあとわかってきました.
せっかくなので, このへん深掘りしておきましょう. 小さいC++プログラムを作ってやってみましょう. メインはこんな感じ.
#include <string> #include <iostream> extern "C" { std::string f(); } int main() { std::string s = f(); std::cout << s << std::endl; printf("s in main() is at %p\n", &s); printf(" buffer address: %p\n", *(void**)&s); return 0; }
これに対して, C++によるf()の実装とRustによるf()の実装を作ります. C++版はこうなります.
extern "C" std::string f() { std::string foo = ""; printf("foo in f() is at %p\n", &foo); printf(" buffer address: %p\n", *(void**)&foo); return foo; }
RustでSSOしないと, こんな感じ
#[no_mangle] pub unsafe extern "C" fn f() -> CxxString { let buffer: Box<[u8]> = Box::new([0; 32]); let ptr = Box::into_raw(buffer) as *const _; println!("string buffer is at {:?}", ptr); CxxString { ptr: ptr, size: 0, capacity: 32, pad: [0; 8], } }
で, これらのアセンブラを見ます. まずはmain()から.
11b6: 48 89 e5 mov %rsp,%rbp ... std::string s = f(); 11cd: 48 8d 45 c0 lea -0x40(%rbp),%rax 11d1: 48 89 c7 mov %rax,%rdi 11d4: e8 77 fe ff ff call 1050 <f@plt>
ここはf()を呼んでいるところですが, %rdiレジスタに"-0x40(%rbp)"が入っています. ここで%rbp=%rspなので, まあスタック上のアドレス(std::string sのアドレス)が, f()への隠し引数として渡されているわけです.
C++版とRust版では, この引数の扱いが変わってきます. C++版のf()を見ましょう.
22de: 48 89 7d d8 mov %rdi,-0x28(%rbp) # -0x28(%rbp) = %rdi = 隠し引数 = "mainのs" ... 2301: 48 8b 45 d8 mov -0x28(%rbp),%rax 2305: 48 8d 0d f4 0c 00 00 lea 0xcf4(%rip),%rcx # 3000 <_fini+0x928> 230c: 48 89 ce mov %rcx,%rsi # 多分文字列 "" 230f: 48 89 c7 mov %rax,%rdi # callの第一引数: "mainのs" 2312: e8 69 fe ff ff call 2180 <_ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEC1IS3_EEPKcRKS3_@plt> # call されるのはstd::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string<std::allocator<char> >(char const*, std::allocator<char> const&)
ここでcallされているのは, std::stringのコンストラクタです. 結局, main内の"std::string s"に対して, f()の中から直接, 文字列("")を引数にとるコンストラクタを呼んでいます.
では, Rust版ではどうなっているのでしょう.
7847: 48 89 7c 24 08 mov %rdi,0x8(%rsp) ... 78c6: e8 85 f9 ff ff call 7250 <_ZN5alloc5boxed16Box$LT$T$C$A$GT$8into_raw17h9f973d5138f7f2c2E> 78cb: 48 89 44 24 48 mov %rax,0x48(%rsp) # 0x48(%rsp) = Box::into_raw() ... 7951: 48 8b 4c 24 08 mov 0x8(%rsp),%rcx 7956: 48 8b 44 24 10 mov 0x10(%rsp),%rax CxxString { ptr: ptr, # ここから返り値へのコピー 795b: 48 8b 54 24 48 mov 0x48(%rsp),%rdx # %rdx = 0x48(%rsp) = Box::into_raw() size: 0, capacity: 32, pad: [0; 8], 7960: 48 c7 84 24 90 00 00 movq $0x0,0x90(%rsp) 7967: 00 00 00 00 00 CxxString { 796c: 48 89 11 mov %rdx,(%rcx) # ptr = %rdx = 0x48(%rsp) = Box::into_raw() 796f: 48 c7 41 08 00 00 00 movq $0x0,0x8(%rcx) # size = 0 7976: 00 7977: 48 c7 41 10 20 00 00 movq $0x20,0x10(%rcx) # capacity = 32 797e: 00 797f: 48 8b 94 24 90 00 00 mov 0x90(%rsp),%rdx 7986: 00 7987: 48 89 51 18 mov %rdx,0x18(%rcx) # pad
Rust版のf()でも, 返り値となる構造体にBoxで確保したバッファやcapacityなどを直接コピーしています. ここで最適化されて直接構築されるのがある意味で問題で, たとえば一時オブジェクトがポインタで返っていってそれが呼び出し側でコピーコンストクタなり呼ばれなおす…という感じならもしかしてうまくいっていたのかもしれません.
結局のところ, 隠し引数のアドレスをとってきて, そのアドレスを元に適切なデータを構築する必要があります. ということは, このrdiレジスタに来る隠し引数をとる方法がなにかあれば…なにか…なにかないのか……?
asm!()でい!
レジスタには来てるんだから, asmでとればいいじゃないという話ですね. ということで, こんな感じ
let rdi: usize; unsafe { asm!("mov {}, rdi", out(reg) rdi) }; eprintln!("rdi = {:#x}", rdi); CxxString { ptr: (rdi + 16) as *const _, size: 3, capacity: 0x6f6f66, // "foo" pad: [0; 8], }
asm!()でレジスタrdiの値をとって, そこから最適化状態のstd::stringのバッファ位置を計算します. capacityのところに, 文字列になるように値を設定すれば…
rdi = 0x7ffd92b51c40 foo s in main() is at 0x7ffd92b51c40 buffer address: 0x7ffd92b51c50
やったね, 動きました.
ところで, 返り値の書きこみ先がわかったなら, もっと直接的に書く感じのコードでもいいのでは? たとえばこんな
#[repr(C)] struct CxxShortString { ptr: *const u8, size: u64, buffer: [u8; 16], } #[no_mangle] pub unsafe extern "C" fn f() { let rdi: *mut CxxShortString; unsafe { asm!("mov {}, rdi", out(reg) rdi) }; eprintln!("rdi = {:?}", rdi); let s = "foo"; (*rdi).ptr = &(*rdi).buffer as *const _; (*rdi).size = 3; (*rdi).buffer[..s.len()].copy_from_slice(s.as_bytes()); }
これもいい感じに動きました. どっちがRustっぽいでしょうね. asm!()な時点で終わりっちゃ終わりか.
asm!()を使ったりバイナリ形式を仮定したり, ひたすらプラットフォーム依存な感じですが, そういうとこ遊べるのも自作IMEのいいところということでここはひとつ. これで最適化されたstd::string(C++)を作れるようになりました. しかし, これ使いづらいなあ…
ということで, 貯まってたネタがなくなってきたので, 次回はどうだろう…多分
次回: Waylandで日本語入力への道: Preeditを作ろう編
かなあ…
Waylandで日本語入力への道: std::string の捏造編
Kernel/VM Advent Calendarの何日目かの記事です.
- 第1回 Waylandで日本語入力への道: 下調べ編 - Gentoo metalog
- 第2回 Waylandで日本語入力への道: vtableを作ろう編 - Gentoo metalog
- 第3回 Waylandで日本語入力への道: コンストラクタに届け編 - Gentoo metalog
- 第4回 Waylandで日本語入力への道: dynamic_castがならなくて編 - Gentoo metalog
前回のあらすじ: 関数だけ並べたvtableでさぼっていたら, しっかりdynamic_cast が動かなくて怒られた. bindgenもそうしてるんだけどなあ…. しょうがないので, ちゃんと型情報を入れてあげました.
DISCLAIMER: waylandで日本語入力したい時にこの記事は役に立たないし, RustでC++のbindingをしたい時にもやめた方がいいと思う.
そろそろ忘れられているかもしれませんが, このシリーズはWaylandで日本語入力をするために行われています. 今回は std::string のbinary formatを調べて, それをRustで作りあげて C++ (fcitx5)に返します. 日本語を入力するというのは, こういった作業の先にあるもので, かくも大変なものなのですね. 日本語をしっかり入力していきましょう.
std::string の中身ってどうなってるの?
さっそく std::string のbinary formatを調べましょう. ぐぐってるとこういう記事が見つかりました.
これを見ると, std::stringは基本形態では図の左のように3つのフィールド buffer・size・capacityを持つことがわかります. bufferがheap上で文字列を保持するバッファを指し, sizeは文字列の長さ, capacityはbufferのサイズです. (x86_64 linuxでは)
一方, 文字列のサイズが小さい場合には, その管理のために24byteを使うのは無駄があります. そこでSmall String Optimization (SSO)として, 図の右側のように, 構造体の内部に文字列のデータを持っておきます. この時, capacityは32byteで固定ということなのでしょう.
gdbで確認じゃ
せっかくなので実際にコードを書いて確認しておきましょう. 以下のようなコードを書いて, "g++ -O0 -ggdb"でコンパイルしてgdbでcoutの行で止めます.
#include <string> #include <iostream> std::string f() { std::string tmp = "foo"; tmp.reserve(32); return tmp; } int main() { std::string s = f(); std::cout << s << std::endl; return 0; }
さて見てみますと, たしかに最初の8byteが文字列のバッファを指していて, その次に文字列サイズの3, そして次にバッファサイズの0x20=32が入っています.
(gdb) n 11 std::string s = f(); (gdb) 12 std::cout << s << std::endl; (gdb) x/4xg &s 0x7fffffffd7a0: 0x000055555556c2b0 0x0000000000000003 0x7fffffffd7b0: 0x0000000000000020 0x0000000000000000 (gdb) x/s 0x000055555556c2b0 0x55555556c2b0: "foo" (gdb) p sizeof(s) $1 = 32
では, ここで"tmp.reserve()"の行を削除するとどうなるでしょう. Small String Optimizationがきかなくなるので, 文字列"foo"がsの内部に埋め込まれます.
(gdb) n 12 std::cout << s << std::endl; (gdb) x/4xg &s 0x7fffffffd7a0: 0x00007fffffffd7b0 0x0000000000000003 0x7fffffffd7b0: 0x00000000006f6f66 0x0000000000000000 (gdb) x/s 0x7fffffffd7b0 0x7fffffffd7b0: "foo"
Rustでstd::string(C++)を作ろう
これらをふまえて, Rustでstd::string(ただしC++)を作って返しましょう. Rustのなら一発なのに.
こんな感じで標準タイプのstd::string(C++)の形を作って
#[repr(C)] struct CxxString { ptr: *const u8, size: u64, capacity: u64, pad: [u8; 8], }
適当に32byteのバッファを作って CxxStringの構造体に持たせて, capacityを32にしておきます. バッファは(多分)C++がstd::stringが死ぬ時に解放してくれる…はずなのでOK.
let buffer: Box<[u8]> = Box::new([0; 32]); CxxString { ptr: Box::into_raw(buffer) as *const _, size: 0, capacity: 32, pad: [0; 8], }
ということで動かすと, 今度は"sub_mode()"のtodo!()に当たって死にました. こっちは上記の右側の構造でやってみたいところです.
RustでもSSOしよう
として, したかったんですが一度挫折しました. 挫折したコードたち
let x = Box::new(CxxString { ptr: std::ptr::null_mut(), size: 0, capacity: 0, pad: [0; 8], }); let ptr = Box::into_raw(x); let buf = (ptr as u64) + 16; let mut x = Box::from_raw(ptr); x.ptr = buf as *const _; let ptr = Box::into_raw(x); *ptr
let mut x = CxxString { ptr: std::ptr::null_mut(), size: 0, capacity: 0, pad: [0; 8], }; let ptr = &mut x as *mut _; x.ptr = ((ptr as u64) + 16) as *const _; x
なにをどうしてもdouble freeぽくなったりしてます.
たとえば下のコードだと関数の出口で
(gdb) p &x $1 = (*mut tcode::CxxString) 0x7fffffffce80 (gdb) x/4xg $1 0x7fffffffce80: 0x00007fffffffce90 0x0000000000000000 0x7fffffffce90: 0x0000000000000000 0x0000000000000000
こんな感じで, SSOされたバイナリができています. これが返っていって
(gdb) n fcitx::InstancePrivate::showInputMethodInformation (this=this@entry=0x5555555d2ea0, ic=ic@entry=0x555555e7c700) at /dev/shm/portage/app-i18n/fcitx-5.1.5/work/fcitx5-5.1.5/src/lib/fcitx/instance.cpp:391 391 auto subModeLabel = engine->subModeLabel(*entry, *ic); (gdb) p &subMode $2 = (std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > *) 0x7fffffffcf10 (gdb) x/4xg &subMode 0x7fffffffcf10: 0x00007fffffffce90 0x0000000000000000 0x7fffffffcf20: 0x0000000000000000 0x0000000000000000
こう, 本当にそのまま値がコピーされてるんですよね. 結果として, std::string(C++)のデスクトラクタがこれはSSOではないなと思って, 0x00007fffffffce90をfree()しにいく. すると, そこはallocした先頭のアドレスではないのでinvalid pointerで死ぬと見えます.
一方C++だとなんだかmainのstack上で直接いじられているように見える.
なんでだろ〜となった時, 困った時は逆アセンブルですよね. ということで
次回: Waylandで日本語入力への道: Return of RVO編
Waylandで日本語入力への道: dynamic_castがならなくて編
Kernel/VM Advent Calendarの何日目かの記事です. Waylandで日本語入力をしようAdvent Calendarじゃないですよ.
- 第1回 Waylandで日本語入力への道: 下調べ編 - Gentoo metalog
- 第2回 Waylandで日本語入力への道: vtableを作ろう編 - Gentoo metalog
- 第3回 Waylandで日本語入力への道: コンストラクタに届け編 - Gentoo metalog
前回のあらすじ: vtableをごりごり作ってコンストラクタを呼んであげた. これで動くんじゃない? そんなわけないんですけどね.
DISCLAIMER: waylandで日本語入力したい時にこの記事は役に立たないし, RustでC++のbindingをしたい時にもやめた方がいいと思う.
vtableを埋めたし動くかな?
さて前回コンストラクタをちゃんと呼んだことで, fcitx5が自作のアドオンを読んで動くようにはなりました. これで設定画面にも"T-Code"(自作のIMEの名前, いまさら?)がでてきて, IMEの選択ができるようになります. じゃあ, ということでT-Codeに切り替えてみましょう.
I2023-12-10 18:18:39.807909 addonmanager.cpp:193] Loaded addon tcode Program received signal SIGSEGV, Segmentation fault. 0x00007ffff7ab1c7b in __dynamic_cast () from /usr/lib/gcc/x86_64-pc-linux-gnu/13/libstdc++.so.6 (gdb) bt #0 0x00007ffff7ab1c7b in __dynamic_cast () at /usr/lib/gcc/x86_64-pc-linux-gnu/13/libstdc++.so.6 #1 0x00007ffff7f7bcd4 in fcitx::InputMethodEngine::subModeLabel[abi:cxx11](fcitx::InputMethodEntry const&, fcitx::InputContext&) (this=this@entry=0x5555556f1660, entry=..., ic=...) at /dev/shm/portage/app-i18n/fcitx-5.1.5/work/fcitx5-5.1.5/src/lib/fcitx/inputmethodengine.cpp:24 ... (gdb) up #1 0x00007ffff7f7bcd4 in fcitx::InputMethodEngine::subModeLabel[abi:cxx11](fcitx::InputMethodEntry const&, fcitx::InputContext&) ( this=this@entry=0x5555556f1660, entry=..., ic=...) at /dev/shm/portage/app-i18n/fcitx-5.1.5/work/fcitx5-5.1.5/src/lib/fcitx/inputmethodengine.cpp:24 24 if (auto *this2 = dynamic_cast<InputMethodEngineV2 *>(this)) { (gdb) list 19 return overrideIcon(entry); 20 } 21 22 std::string InputMethodEngine::subModeLabel(const InputMethodEntry &entry, 23 InputContext &ic) { 24 if (auto *this2 = dynamic_cast<InputMethodEngineV2 *>(this)) { 25 return this2->subModeLabelImpl(entry, ic); 26 } 27 return {}; 28 }
はい, クラッシュしました. 場所はここで, dynamic_cast しているところですね.
dynamic_cast では対象の変数の型情報を読んで, castできるかを判定します. その型情報へのポインタはvtableの関数が並ぶ1つ前のエントリにあります. ということで, 前回さぼって関数だけ並べたのが無事にクラッシュふませたということです.
型情報を入れよう
ということで型情報を入れて, vtableを完全体にしましょう. まず fcitx5 本体の InputMethodEngineの型情報をリンクしてきます.
extern "C" { #[link_name = "\u{1}_ZTIN5fcitx17InputMethodEngineE"] static TCodeEngine_Type_Info: *const std::os::raw::c_void; }
型情報の型はよくわからない(し, 特に知る必要もない)ので, とりあえずvoid*で受けておきます.
完全なvtableの定義はこんな感じで.
struct EngineVTableFull { offset: u64, type_info: *const *const std::os::raw::c_void, vtable: EngineVTable, }
これらを使って完全なvtableとして vtable_full をtcode_factory_create()の中で作っていきます. 本当は TCODE_ENGINE_VTABLE のように, 完全なvtableも constで作っておきたいところですが, リンクして持ってくる TCodeEngine_Type_Info がstaticでconstから参照できないので, ヒープに確保しておきます.
let engine = Box::new(TCodeEngine { vtable: std::ptr::null_mut(), d_ptr: 0, }); let ptr = Box::into_raw(engine) as *mut _; TCodeEngine_ctor(ptr); let mut engine = Box::from_raw(ptr); let mut vtable_full = Box::new(EngineVTableFull { offset: 0, type_info: &TCodeEngine_Type_Info, vtable: TCODE_ENGINE_VTABLE, }); engine.vtable = &mut vtable_full.vtable; Box::leak(vtable_full); Box::into_raw(engine) as *mut _
vtable・engineが関数の終わりで消えないように, Box::leak・Box::into_raw しておきましょう. 厳密にはメモリリークしていきますが, InputMethodのインスタンスはこのaddonに1つなので, まあいいでしょう.
ということで動かすと…
thread '<unnamed>' panicked at 'not yet implemented: override_icon', fcitx5-tcode/src/lib.rs:275:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace terminate called without an active exception
と…とりあえず, dynamic_cast はできるようになったっぽいです… じゃあ, この override_icon をとりあえず簡単に実装したいわけですが…
virtual std::string overrideIcon(const InputMethodEntry &) { return {}; }
オオ, std::string を返すのか… rustから? どうやって?? ということで
次回: Waylandで日本語入力への道: std::string の捏造編
Waylandで日本語入力への道: コンストラクタに届け編
Kernel/VM Advent Calendarの何日目かの記事です. このカレンダーは delayed allocationで, extent allocationです.
前回のあらすじ: fcitx::AddonFactoryのvtableを作ることに成功し, 無事にfcitxがクラッシュするようになった. 次はfcitx::InputMethodEngineV3を作りたいけど24個もエントリがあるなあ.
DISCLAIMER: waylandで日本語入力したい時にこの記事は役に立たないし, RustでC++のbindingをしたい時にもやめた方がいいと思う.
vtableをうめよう
とりあえず, build.rsをいじって"fcitx::InputMethodEngineV4"のbindingも生成します.
fcitx::InputMethodEngineV4 -> fcitx::InputMethodEngineV3 -> fcitx::InputMethodEngineV2 -> fcitx::InputMethodEngine -> fcitx::AddonInstance という継承関係になっています. これを生成されたとおりに, rustにすると以下のようになります.
let engine = fcitx5_sys::fcitx_InputMethodEngineV4 { _base: fcitx5_sys::fcitx_InputMethodEngineV3 { _base: fcitx5_sys::fcitx_InputMethodEngineV2 { _base: fcitx5_sys::fcitx_InputMethodEngine { _base: fcitx5_sys::fcitx_AddonInstance { vtable_: todo!(), d_ptr: todo!(), }, }, }, }, };
あまりにネストが深くてうるさい感じです. 結局はvtable_とd_ptrしか持っていないので, フラットにして構わないでしょう. ということで, vtableおよびcreate()から返すべきTCodeEngineを実装します. d_ptrはとりあえず0でやっておきます.
#[repr(C)] struct EngineVTable { pub complete_object_dtor: unsafe extern "C" fn(this: *mut TCodeEngine), pub deleting_dtor: unsafe extern "C" fn(this: *mut TCodeEngine), pub reload_config: unsafe extern "C" fn(this: *mut TCodeEngine), ... pub set_config_for_input_method: unsafe extern "C" fn( this: *mut TCodeEngine, entry: *const fcitx5_sys::fcitx_InputMethodEntry, config: *const fcitx5_sys::fcitx_RawConfig, ), } const TCODE_ENGINE_VTABLE: EngineVTable = EngineVTable { complete_object_dtor: dtor, deleting_dtor: dtor, reload_config: reload_config, ... set_config_for_input_method: set_config_for_input_method, }; unsafe extern "C" fn dtor(this: *mut TCodeEngine) { eprintln!("engine dtor called"); } unsafe extern "C" fn reload_config(this: *mut TCodeEngine) { eprint!("reload_config called"); } unsafe extern "C" fn save(this: *mut TCodeEngine) {} ... struct TCodeEngine { vtable: *const EngineVTable, d_ptr: u64, } unsafe extern "C" fn tcode_factory_create( _this: *mut fcitx5_sys::fcitx_AddonFactory, _manager: *mut fcitx5_sys::fcitx_AddonManager, ) -> *mut fcitx5_sys::fcitx_AddonInstance { eprintln!("create called"); let engine = Box::new(TCodeEngine { vtable: &mut TCODE_ENGINE_VTABLE, d_ptr: 0, }); Box::into_raw(engine) as *mut _ }
これで動かすと, 以下のようにfcitx5がクラッシュします.
Thread 1 "fcitx5" received signal SIGSEGV, Segmentation fault. 0x00007ffff7f566dd in fcitx::AddonManagerPrivate::realLoad (this=this@entry=0x5555555d5af0, q_ptr=q_ptr@entry=0x5555555d2f68, addon=...) at /dev/shm/portage/app-i18n/fcitx-5.1.5/work/fcitx5-5.1.5/src/lib/fcitx/addonmanager.cpp:192 192 addon.instance_->d_func()->addonInfo_ = &(addon.info()); (gdb) p *addon.instance_ $2 = {_vptr.AddonInstance = 0x7ffff49fc160, d_ptr = std::unique_ptr<fcitx::AddonInstancePrivate> = { get() = 0x0}}
ここで addon.instance_ == TCodeEngineのインスタンスです. d_ptrとd_func()がなにやら関係のありそうな感じです.
AddonInstance::d_ptrの定義はこれです.
std::unique_ptr<AddonInstancePrivate> d_ptr; FCITX_DECLARE_PRIVATE(AddonInstance);
d_ptrの一行下のFCITX_DECLARE_PRIVATE()が以下のようなマクロになります.
#define FCITX_DECLARE_PRIVATE(Class) \ inline Class##Private *d_func() { \ return static_cast<Class##Private *>(d_ptr.get()); \ } \ inline const Class##Private *d_func() const { \ return static_cast<Class##Private *>(d_ptr.get()); \ } \ friend class Class##Private;
これがSEGVしてたコードのd_func()の部分で, 結局はd_ptrを0でさぼってたからぬるぽをふんでたという話ですね. じゃあ, d_ptrを適切に初期化すればいいわけです. 適切な初期化方法がなにかというと…まあ AddonInstanceのコンストラクタが初期化しているので, これを呼びましょう.
AddonInstance::AddonInstance() : d_ptr(std::make_unique<AddonInstancePrivate>()) {}
コンストラクタを呼ぼう
コンストラクタを呼ぶのは簡単です. 生成されたbinding.rsを見ていると, fcitx5側の関数を呼びたかったらこんな感じでlinkすればいいんだなとわかってきます.
extern "C" { #[link_name = "\u{1}_ZN5fcitx13AddonInstanceC2Ev"] fn TCodeEngine_ctor(this: *const TCodeEngine); }
そして, 呼び出し側をこうやっとくと…コンストラクタが呼ばれ…
let engine = Box::new(TCodeEngine { vtable: &TCODE_ENGINE_VTABLE, d_ptr: 0, }); let ptr = Box::into_raw(engine) as *mut _; TCodeEngine_ctor(ptr); let engine = Box::from_raw(ptr); assert!(engine.vtable == &TCODE_ENGINE_VTABLE); Box::into_raw(engine) as *mut _
assertにひっかかってクラッシュします. ちゃんとこのへん知らなかったんですが, 親クラスのコンストラクタ呼んだ後は, vtableが親クラスのやつになってるみたいです. 学びですね〜
そんならコンストラクタの後からvtableをいれます.
let engine = Box::new(TCodeEngine { vtable: std::ptr::null_mut(), d_ptr: 0, }); let ptr = Box::into_raw(engine) as *mut _; TCodeEngine_ctor(ptr); let mut engine = Box::from_raw(ptr); engine.vtable = &TCODE_ENGINE_VTABLE; Box::into_raw(engine) as *mut _
ということで, ここまでやるとfcitx5がtcodeのアドオンを読んで動くようになります…. まあ実際は"OnDemand=True"にしてるのでちゃんと読んでるわけではなさそうですが…
次回こそ多分 Waylandで日本語入力への道: dynamic_castがならなくて編
Waylandで日本語入力への道: vtableを作ろう編
Kernel/VM Advent Calendarの何日目かの記事です. このカレンダーは delayed allocationなのでサイコーです.
前回のあらすじ: Linuxデスクトップ元年の終わりが近付き, waylandで日本語入力したくなってきた. fcitx5で自作addonをrustで作るために下調べをして, bindgenでbindingを生成するも, 求めていたvtableはそこにはなかった.
DISCLAIMER: waylandで日本語入力したい時にこの記事は役に立たないし, RustでC++のbindingをしたい時にもやめた方がいいと思う.
vtableを作るぞ
ないものは作るしかないですね. vtableを作ります. まずはbuild.rsを調整して, 必要な他のクラスは生成しつつじゃまな fcitx::AddonFactory の分は消します. こんな感じ.
let bindings = builder .header("wrapper.hpp") .allowlist_item("fcitx::AddonManager") .allowlist_item("fcitx::AddonInstance") .blocklist_item("fcitx::AddonFactory") .opaque_type("std::.*") .vtable_generation(true) .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) .generate() .expect("Unable to generate bindings");
vtableのレイアウトを調べるために, "g++ -fdump-lang-class=/dev/stdout factory.hpp $(pkgconf --cflags Fcitx5Core)"を実行します. factory.hppには"#include
Vtable for fcitx::AddonFactory fcitx::AddonFactory::_ZTVN5fcitx12AddonFactoryE: 5 entries 0 (int (*)(...))0 8 (int (*)(...))(& _ZTIN5fcitx12AddonFactoryE) 16 0 24 0 32 (int (*)(...))__cxa_pure_virtual
最初のエントリはtop_offsetというもので, 2つ目のエントリは型情報へのポインタが入っています. 実際にvirtual関数のテーブルになっているのは3つ目からです.
__cxa_pure_virtualなのが純粋仮想関数であるcreate()なのはOKとして…上2つはなんでしょう.
Itanium C++ ABI (Revision: 1.83)に以下のように書かれています.
> The entries for virtual destructors are actually pairs of entries. The first destructor, called the complete object destructor, performs the destruction without calling delete() on the object. The second destructor, called the deleting destructor, calls delete() after destroying the object.
つまり, これは2つはどちらもデストラクタで, 1つ目は complete object destructorでdeleteしないもの. 2つ目は deleting destructorでdeleteするものとのことです.
今回はfcitx5のaddon factoryなのでどうせデストラクタが呼ばれる時はプログラム自体終わるでしょうし, いまは気にしないことにします.
structの中のvtable_は, 最初の2つをとばして関数の並びの先頭を指します. ということで, 以下のようにstruct fcitx_AddonFactory__bindgen_vtableの定義を書きます. create()の部分はvirtual destructorを消して, bindgenを走らせれば作ってもらえます. そこからコピペしてよいでしょう.
ここで本当のvtableの最初の2つのエントリはいらないの?という話になります. dynamic_castなどをしなければ, 必要ないようです. bindgenもこれらなしでvtableを生成してきます.
#[repr(C)] pub struct fcitx_AddonFactory__bindgen_vtable { pub fcitx_AddonFactory_complete_object_destructor: unsafe extern "C" fn(this: *mut fcitx_AddonFactory), pub fcitx_AddonFactory_deleting_destructor: unsafe extern "C" fn(this: *mut fcitx_AddonFactory), pub fcitx_AddonFactory_create: unsafe extern "C" fn( this: *mut fcitx_AddonFactory, manager: *mut fcitx_AddonManager, ) -> *mut fcitx_AddonInstance, } #[doc = " Base class for addon factory."] #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct fcitx_AddonFactory { pub vtable_: *const fcitx_AddonFactory__bindgen_vtable, }
これにあわせてvtableと AddonFactoryに相当するstructを作ります.
const FACTORY_VTABLE: fcitx5_sys::fcitx_AddonFactory__bindgen_vtable = fcitx5_sys::fcitx_AddonFactory__bindgen_vtable { fcitx_AddonFactory_complete_object_destructor: tcode_complete_obj_dtor, fcitx_AddonFactory_deleting_destructor: tcode_deleting_dtor, fcitx_AddonFactory_create: tcode_factory_create, }; const FACTORY: fcitx5_sys::fcitx_AddonFactory = fcitx5_sys::fcitx_AddonFactory { vtable_: &FACTORY_VTABLE, };
関数はとりあえずこんな感じで…todo!()にぶつかってpanicしたら成功です.
unsafe extern "C" fn tcode_factory_create( _this: *mut fcitx5_sys::fcitx_AddonFactory, _manager: *mut fcitx5_sys::fcitx_AddonManager, ) -> *mut fcitx5_sys::fcitx_AddonInstance { eprintln!("create called"); todo!() } unsafe extern "C" fn tcode_complete_obj_dtor(_this: *mut fcitx5_sys::fcitx_AddonFactory) { eprintln!("dtor called"); todo!() } unsafe extern "C" fn tcode_deleting_dtor(_this: *mut fcitx5_sys::fcitx_AddonFactory) { eprintln!("dtor called"); todo!() }
addonのエントリポイント
AddonFactoryのデータができてしまえば, エントリポイントを書くのは簡単です. extern "C"とno_mangleだけやっときゃ大丈夫.
#[no_mangle] pub extern "C" fn fcitx_addon_factory_instance() -> *const fcitx5_sys::fcitx_AddonFactory { &FACTORY }
addonを読ませる
addon・inputmethodは設定ファイルでfcitx5に認識されます. $HOME下でやろうと思うと以下の2つのファイルでやるといいです. iconとしてはfcitx-anthyを使いまわしときます… また, Libraryでshared objectの名前を指定します.
$ cat .local/share/fcitx5/addon/tcode.conf [Addon] Name[ja]=T-Code Name=T-Code Category=InputMethod Version=5.1.2 Library=libtcode Type=SharedLibrary OnDemand=True Configurable=True [Dependencies] 0=core/5.0.6 $ cat .local/share/fcitx5/inputmethod/tcode.conf [InputMethod] Name[ja]=T-Code Name=T-Code Icon=fcitx-anthy LangCode=ja Addon=tcode Configurable=True Label=あ
Libraryで指定されたshared objectは, /usr/lib64/fcitx5の下から探されます. ビルドされたファイルをここにおくといいです. もしくは FCITX_ADDON_DIRS 環境変数でこのサーチパスを変更できます.
ということで, fcitx5を動かすとlibtcode.soが読まれてtodo!()に当たってクラッシュするようになってうれしいですね.
これだけじゃなんにもならないので, create()の実装をまともにしましょう. また, fcitx5-anthyを参考にします.
fcitx::AddonInstance *create(fcitx::AddonManager *manager) override { fcitx::registerDomain("fcitx5-anthy", FCITX_INSTALL_LOCALEDIR); return new AnthyEngine(manager->instance()); }
fcitx::AddonInstanceを返せばいいわけですが, 実際のところは AnthyEngineは, そこからさらに継承した fcitx::InputMethodEngineV3 になっているので, そうしたいところです.
https://github.com/fcitx/fcitx5-anthy/blob/1172f034313fb085e5b1e79382ad4f8fd03704cc/src/engine.h#L31
もうvtableの作り方もわかったし, なんもこわいことがないで……す…ね…
Vtable for fcitx::InputMethodEngineV3 fcitx::InputMethodEngineV3::_ZTVN5fcitx19InputMethodEngineV3E: 24 entries 0 (int (*)(...))0 8 (int (*)(...))(& _ZTIN5fcitx19InputMethodEngineV3E) 16 0 24 0 32 (int (*)(...))fcitx::AddonInstance::reloadConfig 40 (int (*)(...))fcitx::AddonInstance::save 48 (int (*)(...))fcitx::AddonInstance::getConfig 56 (int (*)(...))fcitx::AddonInstance::setConfig 64 (int (*)(...))fcitx::AddonInstance::getSubConfig 72 (int (*)(...))fcitx::AddonInstance::setSubConfig 80 (int (*)(...))fcitx::InputMethodEngine::listInputMethods 88 (int (*)(...))__cxa_pure_virtual 96 (int (*)(...))fcitx::InputMethodEngine::activate 104 (int (*)(...))fcitx::InputMethodEngine::deactivate 112 (int (*)(...))fcitx::InputMethodEngine::reset 120 (int (*)(...))fcitx::InputMethodEngine::filterKey 128 (int (*)(...))fcitx::InputMethodEngine::updateSurroundingText 136 (int (*)(...))fcitx::InputMethodEngine::subMode 144 (int (*)(...))fcitx::InputMethodEngine::overrideIcon 152 (int (*)(...))fcitx::InputMethodEngine::getConfigForInputMethod 160 (int (*)(...))fcitx::InputMethodEngine::setConfigForInputMethod 168 (int (*)(...))fcitx::InputMethodEngineV2::subModeIconImpl 176 (int (*)(...))fcitx::InputMethodEngineV2::subModeLabelImpl 184 (int (*)(...))fcitx::InputMethodEngineV3::invokeActionImpl
アー…結構でかいvtableでだるそー……ということで
次回: Waylandで日本語入力への道: コンストラクタに届け編
Waylandで日本語入力への道: 下調べ編
Kernel/VM Advent Calendarの何日目かの記事です. このカレンダーは btrfsやext4・XFSなどにも採用されている最先端のファイルシステム技術である delayed allocationを採用しています. したがって人々が本当に記事を書くぞ!となった時に, 随時 allocされるため, 非常に効率がよくなると言われています.
さて, Linuxデスクトップ元年と言われた今年2023年もそろそろ終わりそうです. 元年が終わるまでには片付けたいことがあります. そうwaylandへの移行です. 長年のXを片付けて, waylandで新しい気持ちで新年を迎えていきたいものです.
waylandへの移行にあたって問題になるのが日本語入力です. といっても, fcitx5を使えばだいたいいいわけですが…本ブログの読者のみなさまは高い漢字入力の意識をお持ちでしょうから, 自作のIMEが動いてくれないことには困ってしまうよ…ということがあるでしょう. そこで本記事と今後の記事では, fcitx5用にIME addonを実装していきます. Rustで.
Anthyの調査
fcitx5 addonを自作するにあたって, まずは既存のaddonを調査します. fcitx5-anthyを見てみましょう.
ファイル名を見て, このへんかなということで, src/engine.cpp を見ます. アドオンの定義はマクロでやりがちだろうなあと思いなが探すとこんなコードがあります.
fcitx5-anthy/src/engine.cpp at master · fcitx/fcitx5-anthy · GitHub
FCITX_ADDON_FACTORY(AnthyFactory)
これが以下のマクロで展開されて, こうなります.
extern "C" { FCITXCORE_EXPORT ::fcitx::AddonFactory *fcitx_addon_factory_instance() { static AnthyFactory factory; return &factory; } }
'fcitx::AddonFactory*' を返す fcitx_addon_factory_instance() という関数を作っておけば, fcitx5本体がこの関数を呼んでくれるという感じです.
AnthyFactoryはfcitx::AddonFactoryを継承していて, create()がoverrideされています.
class AnthyFactory : public fcitx::AddonFactory { fcitx::AddonInstance *create(fcitx::AddonManager *manager) override { fcitx::registerDomain("fcitx5-anthy", FCITX_INSTALL_LOCALEDIR); return new AnthyEngine(manager->instance()); } };
fcitx::AddonFactoryは, こんな感じの簡単なクラスです.
class FCITXCORE_EXPORT AddonFactory { public: virtual ~AddonFactory(); /** * Create a addon instance for given addon manager. * * This function is called by AddonManager * * @return a created addon instance. * * @see AddonManager */ virtual AddonInstance *create(AddonManager *manager) = 0; };
ということで, まずは fcitx::AddonFactoryを継承したクラスを作って, それを関数から返しましょう. Rustで.
bindgenの栄光と挫折
RustでC/C++とのffiをやるにあたっては, bindgenを使うとbindingを自動生成してくれるのでべんりです. fcitx4のときもこれを使ってRustでaddonを書きました. 今回も fcitx::AddonFactoryの構造をrust側にとりこむためにbindgenを使いましょう. きっとサクッといきますよ.
wrapper.hppにfcitxのヘッダをincludeするコードを書いて, とりあえず以下のようにbuild.rsを書きます.
use std::env; use std::path::PathBuf; use pkg_config; fn main() { println!("cargo:rustc-link-lib=Fcitx5Core"); println!("cargo:rustc-link-lib=Fcitx5Config"); println!("cargo:rustc-link-lib=Fcitx5Utils"); println!("cargo:rerun-if-changed=wrapper.hpp"); let fcitx5 = pkg_config::Config::new() .atleast_version("5.1.5") .probe("Fcitx5Core") .unwrap(); let builder = fcitx5 .include_paths .iter() .fold(bindgen::Builder::default(), |b, p| { b.clang_arg(format!("-I{}", p.to_str().unwrap())) }); let bindings = builder .header("wrapper.hpp") .allowlist_item("fcitx::AddonFactory") .opaque_type("std::.*") .vtable_generation(true) .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) .generate() .expect("Unable to generate bindings"); let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); bindings .write_to_file(out_path.join("bindings.rs")) .expect("Couldn't write bindings!"); }
fcitx5のヘッダは/usr/include/Fcitx5/Coreの下などにあるので, pkg_config でそのパスをとってきて, コンパイラの引数を足しておきます. また, 生成対象を fcitx::AddonFactory にしぼり, "std::*"下の全ては透過的にします. こうすると, たとえば "pub type std_string = [u64; 4usize];" のようにサイズだけあわせるようになり, 中の詳細については生成されません. 逆にこうしないと, bindgenの生成が失敗しがちです.
これで, fcitx::AddonFactory がrust用に以下のように…
#[repr(C)] #[derive(Debug)] pub struct fcitx_AddonFactory { pub vtable_: *const fcitx_AddonFactory__bindgen_vtable, }
あ, はい vtableですね. fcitx::AddonFactory はvirtualな関数を持つので vtableを持ちます. Rustで「継承したクラス」を表現するには, このvtableを適切な関数で埋めてあげればいいわけですね. 幸い bindgenで ".vtable_generation(true)"にしているのでうめるべき関数の入った structが "fcitx_AddonFactory__bindgen_vtable" として生成されているはず…
が, このようなただのvoidになってんですねえ
#[repr(C)] pub struct fcitx_AddonFactory__bindgen_vtable(::std::os::raw::c_void);
なんでだろ〜というわけですが, 以下のコメントにあるように, bindgenは他のクラスを継承しているもの・virtualなデスクトラクタを持つものにはvtableを作らないという話です. まあこのへんプラットフォーム依存で難しいのでしょうがないとこですかね.
じゃあこれどうやって fcitx::AddonFactoryを作るねんとなるわけですが…
次回: Waylandで日本語入力への道: vtableを作ろう編
gentoo.hatenablog.com