現代日本語書き言葉均衡コーパス(BCCWJ)で漢直(T-Code)を研究する

www.youtube.com

ゆる言語学ラジオというpodcastを聞いていたところ, 「現代日本語書き言葉均衡コーパス」(BCCWJ)というものが紹介されていました. これは「書籍全般、雑誌全般、新聞、白書、ブログ、ネット掲示板、教科書、法律などのジャンルにまたがって1億430万語のデータを格納しており、各ジャンルについて無作為にサンプルを抽出」したコーパスとのことです.

clrd.ninjal.ac.jp

さて, やはり漢字というのはなるべく変換しない方がいいわけですが, その解決策として漢字直接入力(漢直)というのがあります. たとえば, 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編

adventar.org

Kernel/VM Advent Calendarの何日目かの記事です.

前回のあらすじ: std::string(C++)のことを調べて, Rustで作れるようになった. でも, 小さい文字列用に最適化された状態にはできない… ちくしょう! アセンブラに相談だ!

DISCLAIMER: waylandで日本語入力したい時にこの記事は役に立たないし, RustでC++のbindingをしたい時にもやめた方がいいと思う.

文字列をやってるとだいぶ日本語入力に近付いてきた気持ちになるこのシリーズいかがおすごしでしょうか.

RVO?なんですか?

さて前回は, std::string(C++)の小さい文字列用フォーマットを作りたかったものの挫折したわけですが…. このへんをいろいろ調べていくと, Return Value Optimization (RVO)による現象なのかなあとわかってきました.

cpprefjp.github.io


せっかくなので, このへん深掘りしておきましょう. 小さい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 の捏造編

adventar.org

Kernel/VM Advent Calendarの何日目かの記事です.

前回のあらすじ: 関数だけ並べたvtableでさぼっていたら, しっかりdynamic_cast が動かなくて怒られた. bindgenもそうしてるんだけどなあ…. しょうがないので, ちゃんと型情報を入れてあげました.

DISCLAIMER: waylandで日本語入力したい時にこの記事は役に立たないし, RustでC++のbindingをしたい時にもやめた方がいいと思う.

そろそろ忘れられているかもしれませんが, このシリーズはWaylandで日本語入力をするために行われています. 今回は std::string のbinary formatを調べて, それをRustで作りあげて C++ (fcitx5)に返します. 日本語を入力するというのは, こういった作業の先にあるもので, かくも大変なものなのですね. 日本語をしっかり入力していきましょう.

std::string の中身ってどうなってるの?

さっそく std::string のbinary formatを調べましょう. ぐぐってるとこういう記事が見つかりました.

tastycode.dev

これを見ると, std::stringは基本形態では図の左のように3つのフィールド buffer・size・capacityを持つことがわかります. bufferがheap上で文字列を保持するバッファを指し, sizeは文字列の長さ, capacityはbufferのサイズです. (x86_64 linuxでは)

一方, 文字列のサイズが小さい場合には, その管理のために24byteを使うのは無駄があります. そこでSmall String Optimization (SSO)として, 図の右側のように, 構造体の内部に文字列のデータを持っておきます. この時, capacityは32byteで固定ということなのでしょう.

C++のstd::stringのバイナリ表現

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編

gentoo.hatenablog.com

Waylandで日本語入力への道: dynamic_castがならなくて編

adventar.org

Kernel/VM Advent Calendarの何日目かの記事です. Waylandで日本語入力をしようAdvent Calendarじゃないですよ.

前回のあらすじ: 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 しているところですね.

fcitx5/src/lib/fcitx/inputmethodengine.cpp at c297bb7900e800e6d08cd59464c62dd6fc8cdd61 · fcitx/fcitx5 · GitHub

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 をとりあえず簡単に実装したいわけですが…

fcitx5/src/lib/fcitx/inputmethodengine.h at c297bb7900e800e6d08cd59464c62dd6fc8cdd61 · fcitx/fcitx5 · GitHub

virtual std::string overrideIcon(const InputMethodEntry &) { return {}; }

オオ, std::string を返すのか… rustから? どうやって?? ということで

次回: Waylandで日本語入力への道: std::string の捏造編

gentoo.hatenablog.com

Waylandで日本語入力への道: コンストラクタに届け編

adventar.org

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の定義はこれです.

https://github.com/fcitx/fcitx5/blob/827561460baf2baba2e16207c6355a2268f12e1e/src/lib/fcitx/addoninstance.h#L126

    std::unique_ptr<AddonInstancePrivate> d_ptr;
    FCITX_DECLARE_PRIVATE(AddonInstance);

d_ptrの一行下のFCITX_DECLARE_PRIVATE()が以下のようなマクロになります.

https://github.com/fcitx/fcitx5/blob/827561460baf2baba2e16207c6355a2268f12e1e/src/lib/fcitx-utils/macros.h#L14

#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を作ろう編

adventar.org


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 "とだけ書いています. 大量に出てきますがその中に fcitx::AddonFactory のvtableのレイアウトも書かれています.

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)に以下のように書かれています.

refspecs.linuxbase.org

> 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を参考にします.

fcitx5-anthy/src/engine.cpp at 1172f034313fb085e5b1e79382ad4f8fd03704cc · fcitx/fcitx5-anthy · GitHub

    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の作り方もわかったし, なんもこわいことがないで……す…ね…

fcitx5/src/lib/fcitx/inputmethodengine.h at 827561460baf2baba2e16207c6355a2268f12e1e · fcitx/fcitx5 · GitHub

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で日本語入力への道: コンストラクタに届け編

gentoo.hatenablog.com

Waylandで日本語入力への道: 下調べ編

adventar.org

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を見てみましょう.

github.com

ファイル名を見て, このへんかなということで, src/engine.cpp を見ます. アドオンの定義はマクロでやりがちだろうなあと思いなが探すとこんなコードがあります.

fcitx5-anthy/src/engine.cpp at master · fcitx/fcitx5-anthy · GitHub

FCITX_ADDON_FACTORY(AnthyFactory)

これが以下のマクロで展開されて, こうなります.

fcitx5/src/lib/fcitx/addoninstance.h at 827561460baf2baba2e16207c6355a2268f12e1e · fcitx/fcitx5 · GitHub

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されています.

fcitx5-anthy/src/engine.cpp at 1172f034313fb085e5b1e79382ad4f8fd03704cc · fcitx/fcitx5-anthy · GitHub

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を作らないという話です. まあこのへんプラットフォーム依存で難しいのでしょうがないとこですかね.

github.com

じゃあこれどうやって fcitx::AddonFactoryを作るねんとなるわけですが…

次回: Waylandで日本語入力への道: vtableを作ろう編
gentoo.hatenablog.com