XFSの2038年問題にオンラインでも勝利したい

tldr;

これの詳しい話. また, オンラインでも変換できることを示す.

XFSのbigtimeとは?

XFSのタイムスタンプは現状では, signed 32bitがUNIXエポックタイム(1970年1月1日 00:00:00 UTC)からの秒数で, 32bitがナノ秒部分となっています. これだと2038年以後の日時を表現できなくなります. そこで, その2つのフィールドをまとめて64bitにして, signed 32bitで表現できていた最小のタイムスタンプ(1901年12月13日 20:45:52 UTC, signedなのでマイナスも表現できてエポックより前のここが最小)からの経過ナノ秒でタイムスタンプを表現することにします. これがbigtimeという機能で, 2486年7月頃までの表現が可能になります.

ようするに, これが

/* Legacy timestamp encoding format. */
struct xfs_legacy_timestamp {
    __be32      t_sec;      /* timestamp seconds */
    __be32      t_nsec;     /* timestamp nanoseconds */
};

これになる

typedef __be64 xfs_timestamp_t;

bigtimeの変換を見てみよう

おおよそ10年以内, 2013年以後に作られたXFSであれば, オフラインで(mountしていない状態で)bigtimeを有効にできます. 実験してみましょう.

まずは, 適当なところにXFSのファイルシステムイメージを作りmountします. もうデフォルトでbigtimeが有効な場合もあるので, "-m bigtime=0"でbigtimeを明示的に無効化しておきます.

$ rm xfs2.img
$ truncate -s 10G xfs2.img
$ mkfs.xfs -m bigtime=0 xfs2.img
meta-data=xfs2.img               isize=512    agcount=4, agsize=655360 blks
         =                       sectsz=512   attr=2, projid32bit=1
         =                       crc=1        finobt=1, sparse=1, rmapbt=1
         =                       reflink=1    bigtime=0 inobtcount=1 nrext64=1
         =                       exchange=0
data     =                       bsize=4096   blocks=2621440, imaxpct=25
         =                       sunit=0      swidth=0 blks
naming   =version 2              bsize=4096   ascii-ci=0, ftype=1, parent=0
log      =internal log           bsize=4096   blocks=16384, version=2
         =                       sectsz=512   sunit=0 blks, lazy-count=1
realtime =none                   extsz=4096   blocks=0, rtextents=0
$ sudo mount xfs2.img /mnt/scratch

"old"というファイルに適当なタイムスタンプをつけて, umountします.

$ sudo touch -t 202410160900 /mnt/scratch/old
$ sudo umount /mnt/scratch

xfs_adminでbigtimeを有効にします. さくっと終わります.

$ xfs_admin -O bigtime=1 xfs2.img
Running xfs_repair to upgrade filesystem.
...
Phase 1 - find and verify superblock...
...
Phase 7 - verify and correct link counts...
done

新しいファイル"new"に同じタイムスタンプをつけて, umountします.

$ sudo mount xfs2.img /mnt/scratch
$ sudo touch -t 202410160900 /mnt/scratch/new
$ sudo umount /mnt/scratch

ここからxfs_dbを使ってinodeをチェックします. まず"info"でスーパーブロックの情報を見ると, "bigtime=1"とフラグがついていることがわかります.

xfs_db> info
meta-data=xfs.img                isize=512    agcount=4, agsize=655360 blks
         =                       sectsz=512   attr=2, projid32bit=1
         =                       crc=1        finobt=1, sparse=1, rmapbt=1
         =                       reflink=1    bigtime=1 inobtcount=1 nrext64=1
         =                       exchange=0
data     =                       bsize=4096   blocks=2621440, imaxpct=25
         =                       sunit=0      swidth=0 blks
naming   =version 2              bsize=4096   ascii-ci=0, ftype=1, parent=0
log      =internal log           bsize=4096   blocks=16384, version=2
         =                       sectsz=512   sunit=0 blks, lazy-count=1
realtime =none                   extsz=4096   blocks=0, rtextents=0

次に2つのファイルのinodeの中の情報を見ます. まずは"ls /"で2つのファイルのinode番号を確認. 131と132です.

$ xfs_db xfs2.img
xfs_db> ls /
/:
8          128                directory      0x0000002e   1 . (good)
10         128                directory      0x0000172e   2 .. (good)
12         131                regular        0x001bf664   3 old (good)
14         132                regular        0x001bb2f7   3 new (good)

inode番号131であるファイル"old"を見ます. "v3.bigtime = 0"とinodeの内部にもフラグがあります.

xfs_db> inode 131
xfs_db> p
...
core.atime.sec = Wed Oct 16 09:00:00 2024
core.atime.nsec = 0
core.mtime.sec = Wed Oct 16 09:00:00 2024
core.mtime.nsec = 0
core.ctime.sec = Wed Oct 16 09:21:57 2024
core.ctime.nsec = 153635926
...
v3.bigtime = 0
...

一方, inode番号132であるファイル"new"を見ます. 今度は"v3.bigtime = 1"です.

xfs_db> inode 132
xfs_db> p
...
core.atime.sec = Wed Oct 16 09:00:00 2024
core.atime.nsec = 0
core.mtime.sec = Wed Oct 16 09:00:00 2024
core.mtime.nsec = 0
core.ctime.sec = Wed Oct 16 09:23:32 2024
core.ctime.nsec = 281349756
...
v3.bigtime = 1
...

すなわち, スーパーブロックとそれぞれのinodeの両方にbigtimeのフラグがあります. それぞれのinodeのbigtimeフラグは, そのinode内のタイムスタンプが新旧どちらの表現であるかを示します. スーパーブロックのフラグが立っていれば, 新しいファイルやファイルの書きかえ時に新しいフォーマットのタイムスタンプに書き変わります.

ダンプを見よう

バイナリダンプを見とかないと終われないので見ましょう.

ファイル"old"はinode番号が131で, inodeのサイズが512バイト(infoのisizeに書いてる)なので, これで見れます.

先頭の"IN"でちゃんとinodeをdumpできていることが確認できます. 周りをあけている, 0x10620からの2行がタイムスタンプ(atime, mtime, ctime)です. dateコマンドでチェックしたところ(big-endianに注意), ちゃんと設定したタイムスタンプが入っています.

$ hexdump -C -s $((131 * 512)) -n 512 xfs2.img
00010600  49 4e 81 a4 03 02 00 00  00 00 00 00 00 00 00 00  |IN..............|
00010610  00 00 00 01 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00010620  67 0f 02 00 00 00 00 00  67 0f 02 00 00 00 00 00  |g.......g.......|
00010630  67 0f 07 25 09 28 4c 56  00 00 00 00 00 00 00 00  |g..%.(LV........|

00010640  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00010650  00 00 18 01 00 00 00 00  00 00 00 00 56 5c 1c 43  |............V\.C|
00010660  ff ff ff ff f4 58 94 32  00 00 00 00 00 00 00 04  |.....X.2........|
00010670  00 00 00 01 00 00 00 04  00 00 00 00 00 00 00 10  |................|
00010680  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00010690  67 0f 07 25 09 28 4c 56  00 00 00 00 00 00 00 83  |g..%.(LV........|
000106a0  2c 82 1d 4d 14 58 4a 73  8c dd 20 29 7f c7 88 dd  |,..M.XJs.. )....|
000106b0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00010770  00 2b 01 00 07 1d 04 73  65 6c 69 6e 75 78 73 74  |.+.....selinuxst|
00010780  61 66 66 5f 75 3a 6f 62  6a 65 63 74 5f 72 3a 75  |aff_u:object_r:u|
00010790  6e 6c 61 62 65 6c 65 64  5f 74 00 00 00 00 00 00  |nlabeled_t......|
000107a0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00010800

$ date --date=@$((0x670f0200))
Wed Oct 16 09:00:00 AM JST 2024

一方, ファイル"new"の方はこうです. 同様にタイムスタンプの周囲をあけています.

$ hexdump -C -s $((132 * 512)) -n 512 xfs2.img
00010800  49 4e 81 a4 03 02 00 00  00 00 00 00 00 00 00 00  |IN..............|
00010810  00 00 00 01 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00010820  35 cc 2a cf 0b 94 00 00  35 cc 2a cf 0b 94 00 00  |5.*.....5.*.....|
00010830  35 cc 2c 17 de 1b 36 7c  00 00 00 00 00 00 00 00  |5.,...6|........|

00010840  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00010850  00 00 18 01 00 00 00 00  00 00 00 00 36 2f 08 59  |............6/.Y|
00010860  ff ff ff ff 38 e7 18 5c  00 00 00 00 00 00 00 04  |....8..\........|
00010870  00 00 00 01 00 00 00 0f  00 00 00 00 00 00 00 18  |................|
00010880  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00010890  35 cc 2c 17 de 1b 36 7c  00 00 00 00 00 00 00 84  |5.,...6|........|
000108a0  2c 82 1d 4d 14 58 4a 73  8c dd 20 29 7f c7 88 dd  |,..M.XJs.. )....|
000108b0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00010970  00 2b 01 00 07 1d 04 73  65 6c 69 6e 75 78 73 74  |.+.....selinuxst|
00010980  61 66 66 5f 75 3a 6f 62  6a 65 63 74 5f 72 3a 75  |aff_u:object_r:u|
00010990  6e 6c 61 62 65 6c 65 64  5f 74 00 00 00 00 00 00  |nlabeled_t......|
000109a0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00010a00

タイムスタンプはナノ秒なので…とdateコマンドにスケールだけ変えてつっこむと未来が見えてしまいます. 最初に書いたようにこれは, 昔のタイムスタンプの最小値からのナノ秒です. その値は…"-2147483648" …ってイコール 2の31乗ですね. signed 32bitの最小なのでそれはそう. そのぶん補正すると, ちゃんと同じタイムスタンプがとれます.

$ date --date=@$((0x35cc2acf0b940000/1000000000))
Mon Nov  3 12:14:08 PM JST 2092
$ date --date='Dec 13 20:45:52 UTC 1901' +%s
-2147483648
$ date --date=@$((0x35cc2acf0b940000/1000000000 - 2**31))
Wed Oct 16 09:00:00 AM JST 2024

ここでもう一度"old"を同じタイムスタンプでtouchして, umountして, dumpをとります. するとすると, ちゃんと古いフォーマットからbigtimeのフォーマットに変換されているのが見てとれます.

$ hexdump -C -s $((131 * 512)) -n $((512 * 1)) xfs2.img
00010600  49 4e 81 a4 03 02 00 00  00 00 00 00 00 00 00 00  |IN..............|
00010610  00 00 00 01 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00010620  35 cc 2a cf 0b 94 00 00  35 cc 2a cf 0b 94 00 00  |5.*.....5.*.....|
00010630  35 cc 2d fa 8d a4 e9 c7  00 00 00 00 00 00 00 00  |5.-.............|

00010640  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00010650  00 00 18 01 00 00 00 00  00 00 00 00 56 5c 1c 43  |............V\.C|
...

むりやり online convertしよう!

さて上記まで見たところ, bigtimeを有効にしても既存のファイルのタイムスタンプはただちに変換されていないことがわかります. 各inodeにフラグがあるので, 次の更新までは元のフォーマットのまま放置していてもかまいせん. xfs_adminがやっていることは結局スーパーブロックのbitを1つ立てているにすぎません.

そんなたかだか1bitのために, umountするなんて嫌じゃないですか? bit1つぐらい実行時で立ててもいいじゃん. 理論上できるでしょ. 立ててやりましょう. kernel moduleで.

!!! 以下のコードは絶対に実行しないようにしましょう. ファイルシステムやシステムが破壊される可能性があります. 責任は一切とれません !!!

お使いのエディタから以下のコードを書きました. 今日はnvimにしました.

"/mnt/tmp"にmountされているXFSを確認し, そのfeatureフラグを立てます. そのフラグはメモリ上にしか影響しないので, diskに書かれるsuperblockのフラグも立てて, 後でxfs_repairに怒られないようにします. ただそれだけのkernel moduleです.

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/module.h>
#include <linux/nsproxy.h>
#include <linux/namei.h>

#if 0
#include "xfs/xfs.h"
#include "xfs/libxfs/xfs_types.h"
#include "xfs/libxfs/xfs_format.h"
#include "xfs/libxfs/xfs_shared.h"
#include "xfs/libxfs/xfs_trans_resv.h"
#include "xfs/xfs_mount.h"
#endif

static int init_xfs_conv(void)
{
    return 0;
    const char *MNT_DIR = "/mnt/tmp";

    pr_info("xfs online convert start");

    struct path path __free(path_put) = {};
    int ret;

    ret = kern_path(MNT_DIR, LOOKUP_FOLLOW, &path);
    if (ret) {
        pr_err("failed to open %s %d", MNT_DIR, ret);
        return ret;
    }

    struct super_block *sb = path.mnt->mnt_sb;
    pr_info("xfs sb %px", sb);
    struct xfs_mount *mp = XFS_M(sb);
    pr_info("xfs magic %x", mp->m_sb.sb_magicnum);
    if (mp->m_sb.sb_magicnum != XFS_SB_MAGIC) {
        pr_err("invalid xfs magic");
        return -EINVAL;
    }

    uint64_t features = READ_ONCE(mp->m_features);
    pr_info("xfs feature %llx", features);
    if (features & XFS_FEAT_BIGTIME) {
        pr_info("already has XFS_FEAT_BIGTIME");
        return 0;
    }
    return 0;

    features |= XFS_FEAT_BIGTIME;
    WRITE_ONCE(mp->m_features, features);

    uint64_t sb_feautres = READ_ONCE(mp->m_sb.sb_features_incompat);
    sb_feautres |= XFS_SB_FEAT_INCOMPAT_BIGTIME;
    WRITE_ONCE(mp->m_sb.sb_features_incompat, sb_feautres);

    pr_info("BIGTIME enabled");

    return 0;
}

static void exit_xfs_conv(void)
{
    pr_info("xfs online convert finish");
}

module_init(init_xfs_conv);
module_exit(exit_xfs_conv);

MODULE_DESCRIPTION("DO NOT USE!!! Online Convert XFS!");
MODULE_LICENSE("GPL");

同様に, XFSをmountして"old"をtouchします. さっきのコードをコンパイルしてinsmodしてrmmodします. なんかいい感じに動いたみたい.

$ (mountとか)
$ sudo touch -t 202410160900 /mnt/tmp/old
$ sudo insmod ./xfs-conv.ko; sudo rmmod xfs-conv; dmesg|grep xfs_conv:
[86968.662739] xfs_conv: xfs online convert start
[86968.662765] xfs_conv: xfs sb ffff8ad326471000
[86968.662768] xfs_conv: xfs magic 58465342
[86968.662769] xfs_conv: xfs feature 49ff6ab
[86968.662771] xfs_conv: BIGTIME enabled
[86968.694864] xfs_conv: xfs online convert finish

できたかな?ということで, 同様に"new"を作ってsyncします. それぞれdumpを見ます.

まずは"old"の方. ちゃんと昔のタイムスタンプ.

$ hexdump -C -s $((131 * 512)) -n $((512 * 1)) xfs.img
00010600  49 4e 81 a4 03 02 00 00  00 00 00 00 00 00 00 00  |IN..............|
00010610  00 00 00 01 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00010620  67 0f 02 00 00 00 00 00  67 0f 02 00 00 00 00 00  |g.......g.......|
00010630  67 0f 46 7e 03 cf 92 12  00 00 00 00 00 00 00 00  |g.F~............|

00010640  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00010650  00 00 18 01 00 00 00 00  00 00 00 00 e2 73 9e fe  |.............s..|
...

そして"new"の方. やったぜ新しいタイムスタンプフォーマットで書かれています.

$ hexdump -C -s $((132 * 512)) -n $((512 * 1)) xfs.img
00010800  49 4e 81 a4 03 02 00 00  00 00 00 00 00 00 00 00  |IN..............|
00010810  00 00 00 01 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00010820  35 cc 2a cf 0b 94 00 00  35 cc 2a cf 0b 94 00 00  |5.*.....5.*.....|
00010830  35 cc 3a cc e7 3c 8c d0  00 00 00 00 00 00 00 00  |5.:..<..........|

00010840  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00010850  00 00 18 01 00 00 00 00  00 00 00 00 41 48 72 11  |............AHr.|
...

"old"の方をもう一度touchしてsyncなどします. dumpを見ると…新しいフォーマットに変換されました.

$ sudo touch  /mnt/tmp/old
$ hexdump -C -s $((131 * 512)) -n $((512 * 1)) xfs.img
00010600  49 4e 81 a4 03 02 00 00  00 00 00 00 00 00 00 00  |IN..............|
00010610  00 00 00 01 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00010620  35 cc 2a cf 0b 94 00 00  35 cc 2a cf 0b 94 00 00  |5.*.....5.*.....|
00010630  35 cc 3a d4 a5 d3 2f 09  00 00 00 00 00 00 00 00  |5.:.../.........|

00010640  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00010650  00 00 18 01 00 00 00 00  00 00 00 00 e2 73 9e fe  |.............s..|
...

つまり, kernel moduleを書いてbitを立てればいいだけなので, XFSのbigtimeはonline変換できて安心だよということになります.

…というのは言いすぎにしろ, このように(trivialなcaseだけど)実際にonline変換はできるのであるから, そのうちちゃんと実装されるかもしれませんね.

convertできない場合

XFSのbigtime有効化について, offlineでの変換・onlineでの変換を見てきました. しかし, このような変換ができない場合もあります. 現在のXFSのフォーマットはv5ですが, v4以前のフォーマットのものは, このような変換はできません. v4からv5へはバックアップ・リストア以外に方法はないです. v5が出たのは2013年なので, その頃より前のファイルシステムではv4であるかもしれません.

v5では, メタデータCRCがついたり, 全てのメタデータがself describing metadata (metadata冒頭に"IN"など, マジックが入る)となるなどの変更が入っています. この変更はあまりに大規模なので, バックアップ・リストアになるしかないのでしょう.

ということでみなさん快適な2038年をお過ごしください.

P.S.

こういう話が好きな人は11月9日に富山に行くといいようですよ.

kernelvm.connpass.com

WezTermをはじめてWezTermをやめようかと思うまで

ターミナルでほとんどを過ごす都合がある。しばらく前まではkittyを使っていたのが、このところはWezTermを使っていた。

WezTermは最近はやりのGPU-acceleratedなターミナルエミュレータで機能が豊富である。

wezfurlong.org

たとえば、タブを作れたり、リーダーキー(screenとかのCtrl-a的な感じで他のショートカットのprefixになる)を設定できたり、workspaceを複数作れたり、builtinなsshでリモート接続したり、multiplexできたりする。

wezfurlong.org

これらリッチな機能のおかげで、端末+tmux的な機能と操作感をWezTermだけでできて大変にべんり…という感じだった。…が、いまはやめようかなと思っている。

やめようかなとなっている理由

環境が悪いというとそれまでなのだが、nvidiaGPU(+Linux)で動かしているせいか、たまにバグる。ひどいときはGUIプロセスが死ぬので、セッション全体的に死ぬ。tmuxにしてれば復帰できたのにね。sleep復帰ごとに毎回死ぬようになってとてもつらい。

また、リモートにsshしたりmultiplexしたり……もできるのだが、versionが一致しないとうまくいかない(厳密にはプロトコルバージョンがあるようだけど)。プロトコルバージョンがそれなりに更新されてる+remoteと手元が別ディストリですとなると動く確率が低下してやってられない。

あと、手元でも操作して、remoteからも同じmultiplexにattachしたい時には、multiplexの設定がtrickyなのもあまりよくない。

結局、multiplexは別ソフトでやる方がいいという結論になる。

やめてどうするか

WezTermでtmux的機能をするのをやめるのも一案だが、それならよりkittyに戻っていいかも。ただkittyはIMEを受けつけないのでそこはよくない。alacrittyを試してみるのもいいかもしれない。

alacritty.org

ついでに、tmuxの代わりにもう少しちゃんとzellijを試してみたい。

github.com

app-office/libreoffice-24.2.3.2-r1でsandbox violation, なぜ?

ひさしぶりのGentooの話. Xvfbをいれていると, libreofficeのconfigure phaseで, sandbox violationが出て進みません. 実際のところ, 下のようにbugzあるのでそれ見りゃいいんですけど.

933257 – app-office/libreoffice-24.2.3.2-r1: If x11-misc/xvfb-run in installed, sandbox violation: /dev/udmabuf

sandbox violationはこんな感じ. Xvfbが/dev/udmabufをさわりにいく.

>>> Source configured.
 * ----------------------- SANDBOX ACCESS VIOLATION SUMMARY -----------------------
 * LOG FILE: "/dev/shm/portage/app-office/libreoffice-24.2.3.2-r1/temp/sandbox.log"
 *
VERSION 1.0
FORMAT: F - Function called
FORMAT: S - Access Status
FORMAT: P - Path as passed to function
FORMAT: A - Absolute Path (not canonical)
FORMAT: R - Canonical Path
FORMAT: C - Command Line

F: open_wr
S: deny
P: /dev/udmabuf
A: /dev/udmabuf
R: /dev/udmabuf
C: Xvfb :99 -screen 0 1280x1024x24 -nolisten tcp -auth /dev/shm/portage/app-office/libreoffice-24.2.3.2-r1/temp/xvfb-run.ec0CjX/Xautho
rity
 * --------------------------------------------------------------------------------

sandbox violationが起きているのは, configure.acでいうとこのへん. AT-SPI2のテストを走らせるのに, gtk3・xvfb-run・dbus-launchが必要なのでそれらのチェックをしている. なのでUSE=gtkが入ってなければ多分 enable_atspi_tests=no になって, $XVFB_RUNの行にもいかず, sandbox violationも起きないでしょう. 多分.

11884   │ # AT-SPI2 tests require gtk3, xvfb-run, dbus-launch and atspi-2
11885   │ if ! test "$ENABLE_GTK3" = TRUE; then
11886   │     if test "$enable_atspi_tests" = yes; then
11887   │         AC_MSG_ERROR([--enable-atspi-tests requires --enable-gtk3])
11888   │     fi
11889   │     enable_atspi_tests=no
11890   │ fi
11891   │ if ! test "$enable_atspi_tests" = no; then
11892   │     AC_PATH_PROGS([XVFB_RUN], [xvfb-run], no)
11893   │     if ! test "$XVFB_RUN" = no; then
11894   │         dnl make sure the found xvfb-run actually works
11895   │         AC_MSG_CHECKING([whether $XVFB_RUN works...])
11896   │         if $XVFB_RUN --auto-servernum true >&AS_MESSAGE_LOG_FD 2>&AS_MESSAGE_LOG_FD; then
11897   │             AC_MSG_RESULT([yes])
11898   │         else
11899   │             AC_MSG_RESULT([no])
11900   │             XVFB_RUN=no
11901   │         fi
11902   │     fi
11903   │     if test "$XVFB_RUN" = no; then
11904   │         if test "$enable_atspi_tests" = yes; then
11905   │             AC_MSG_ERROR([xvfb-run required by --enable-atspi-tests not found])
11906   │         fi
11907   │         enable_atspi_tests=no
11908   │     fi
11909   │ fi

まあ, gtkをこれのためにdisableするのは困るって話なんですが.

ところで, 結局これでどうなるかというと, 変数名がしめすように, ATSPIのunittestが走るだけなんですよね.

vcl/Module_vcl.mk

  86   │ ifneq ($(ENABLE_ATSPI_TESTS),)
  87   │ $(eval $(call gb_Module_add_check_targets,vcl,\
  88   │     CppunitTest_vcl_gtk3_a11y \
  89   │ ))
  90   │ endif

一方でebuildの方を見てみると, 全般的にcheckは落とされている様子. まあビルドしたいだけなので. ということは, 上の判定コード自体全部消してもいいんじゃないかなあ?

app-office/libreoffice/libreoffice-24.2.3.2-r1.ebuild

 378   │     # sed in the tests
 379   │     sed -i \
 380   │         -e "s#all : build unitcheck#all : build#g" \
 381   │         solenv/gbuild/Module.mk || die
 382   │     sed -i \
 383   │         -e "s#check: dev-install subsequentcheck#check: unitcheck slowcheck dev-install subsequentcheck#g" \
 384   │         -e "s#Makefile.gbuild all slowcheck#Makefile.gbuild all#g" \
 385   │         Makefile.in || die

実際のところ, 同じくsandbox violationが起きるからと, kf5-configのところも消されているのだ.

 369   │     # sandbox violations on many systems, we don't need it. Bug #646406
 370   │     sed -i \
 371   │         -e "/KF5_CONFIG/s/kf5-config/no/" \
 372   │         configure.ac || die "Failed to disable kf5-config"

それだけでなく, ebuildを見ると, libreofficeはビルド中に結構いろんなとこをさわりにくるのがわかる. OpenGLがどのぐらい使えるのかを見てるのかなあ? configureの後で? それか, ビルドした実行バイナリを実際動かして, データの後処理的なことをしているのかも. emacsのbuildみたいに.

 602   │     # more and more LO stuff tries to use OpenGL, including tests during build
 603   │     # bug 501508, bug 540624, bug 545974 and probably more
 604   │     addpredict /dev/dri
 605   │     addpredict /dev/ati
 606   │     addpredict /dev/nvidiactl

まとめ: 巨大ソフトのビルドはめんどい

現代日本語書き言葉均衡コーパス(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