ファントムシータ「にんぎょひめのうた」: ナイフは本当に刺したのか

ファントムシータの新曲「にんぎょひめのうた」は、レトロホラーアイドルにふさわしい衝撃的なMVであった。



わたしは戸川純のファンでもあるので、この曲はファントムシータ版の「好き好き大好き」ではないかと感じた。戸川純の「好き好き大好き」はカバー曲ではあるが、ファントムシータにとっても、特別な位置を占める一曲であろう。「ハイネ」「Moth to a flame」「フレイム・メドゥーサ」といった単独ライブだけでなく、@JAMや宵祭りなどでも多く披露されてきた。


とはいえ、今回の「ふぁんとむ❤︎らんど」では、「好き好き大好き」は登場しなかった…そもそも全曲オリジナル曲となったツアーだったのだが。ここで「好き好き大好き」を精神的に受け継ぐのが「にんぎょひめのうた」ではないかと思ったのだ。「すき すき すき」という連呼、「いたずらなナイフ」が「殺し」てしまうこと…好きなあまりの愛の暴走として、「好き好き大好き 愛してるって言わなきゃ殺す」と通じるものがある……として、戸川純のライブ映像や歌詞解説本 *1と見比べ…

…と、そこで「にんぎょひめのうた」の歌詞の違和感に気づいた。

「にんぎょひめのうた」では2番で

あなたのことが好き
だから行って
好きと行って*2

と「言って」ではなく「行って」と書かれている。ちなみに1番では

あなたのことがすき
だから言って
すきと言って

と「言って」と書かれている。この違いはどういうことか。
2番で「行って」ということは…王子様に、自分とではなく、別の好きな誰かと一緒に「行って」ほしい…ということなのか?

ここで一度アンデルセンの原作(の翻訳)「人魚の姫」を見てみよう。実は、よく知られた童話の印象と違って、原作にも「ナイフ」が登場している。ここに注目したい。



人魚姫は声を代償に*3、足を生やし、地上で王子と再会する。しかし、王子はそれが自分を助けた娘とは気づかない。そのまま王子は別の王女と婚約してしまう。王子が誰かほかの人と結婚してしまうと、人魚姫は次の朝に死んで海の泡になってしまう…。そこに人魚姫の姉たちが「助け」を持ってくる。

「あたしたち、魔法使いに、髪の毛をやってしまったのよ。あなたが、今夜、死なないですむように、魔法使いの助けをかりに行ったの。そしたら、ナイフをくれたわ。ほら、これよ。ねえ、よく切れそうでしょう。お日さまがのぼらないうちに、あなたは、これで、王子の心臓をつきささなくてはいけないのよ。王子のあたたかい血が、あなたの足にかかると、足がちぢこまって、また、さかなのしっぽが生えるのよ。だから、また、もとの人魚になれるわけ。そうして、水の中へはいって、あたしたちのところへもどってくれば、死んで、塩からい海のあわになるまで、三百年も生きていられるのよ。
 さあ、早く! お日さまののぼらないうちに、王子かあなたか、どちらかひとりが死ななければならないのよ。おばあさまは、あんまり心配なさったものだから、白い髪が、すっかりぬけ落ちてしまったわ。あたしたちの髪の毛が、魔法使いのはさみで切られてしまったのと、そっくりよ。
 王子を殺して、帰ってきなさいね! さあ、いそぐのよ! 空が、うっすらと赤くなってきたじゃないの。もうすぐ、お日さまがのぼるわ。そしたら、あなたは死ななければならないのよ」
 こう言うと、おねえさまたちは、それはそれは悲しそうに、深いため息をついて、波間に沈みました。
 人魚のお姫さまは、テントのむらさき色のたれまくを引きあけました。中では、美しい花嫁が、王子の胸に頭をもたせて眠っています。お姫さまは身をかがめて、王子の美しいひたいにキスをしました。空を見れば、夜あけの空が赤くそまって、だんだん明るくなってきました。お姫さまは、するどいナイフをじっと見つめました。それから、また目を王子にむけました。王子は夢のなかで、花嫁の名前を呼びました。ほかのことは、すっかり忘れて、王子の心は、ただただ花嫁のことでいっぱいだったのです。人魚のお姫さまの手の中で、ナイフがふるえました。――
 しかし、その瞬間、お姫さまは、それを遠くの波間に投げすてました。すると、ナイフの落ちたところが、まっかに光って、まるで血のしたたりが、水の中からふき出たように見えました。お姫さまは、なかばかすんできた目を開いて、もう一度王子を見つめました。と、船から身をおどらせて、海の中へ飛びこみました。自分のからだがとけて、あわになっていくのがわかりました。

つまり、原作でも人魚姫はナイフで王子を「殺しちゃいなよ」とそそのかされている。しかし、この人魚姫は王子を殺さなかった。王子の心が花嫁でいっぱいであることに気づき、ナイフを捨ててしまう。

では、この前提で「いたずらなナイフ」の後の歌詞を見てみよう。

「どうして?」
できなかった
「そうしたってあなたは」
言えなかった?
「これが愛してるってこと…?」

この歌詞のにんぎょひめも、やはり、ナイフを刺すことが「できなかった」のではないか。刺しても殺しても「そうしたってあなたは」他の人が好きで、他の人で心がいっぱいであることは変わらない。王子が幸せであるならばいい…「これが愛してるってこと…?」と気づいてしまう。

そしてにんぎょひめは「いきがつづくまで」「無邪気な愛の歌を」歌う。王子には「好きと行って」ほしい。これは一見「理不尽」で「残酷」なようだが、「あなたが好きだから」「やっぱりお姫さま」であり「ほっとする」。そういうことなのではないか。

だが、この解釈には問題がある。MVの中では、明らかに刺されるシーンが描かれ、みんな殺しあいになり、そしておそらくは最後の「彼」も死んでいる。やはり、ここでナイフは刺されていて、殺してしまっているのが自然ではないか。 このにんぎょひめは、嫉妬して王子様を「独り占め」するためにナイフで刺してしまったのではないか?

「好き好き大好き」の精神を考えてみると、それはファントムシータにとって、おかしくはない。好きなあまりに殺してしまう、「愛してるって言わなきゃ殺す」、「これが愛してるってこと…?」、そういう「やっぱりわたしはお姫さま」…レトロホラーアイドルのファントムシータにはそういう姿もありうる。

ここには2つのストーリーがある。好きなあまりに、独り占めするために、殺してしまうこと。好きなあまりに、自分が死ぬことになっても、相手の幸せを願ってしまうこと。MVを見て聴く限りでは前者のストーリーが見えている。圧倒的なMVで、これしかないようにも見える。でも、歌詞を読むと「行って」によって、もうひとつのストーリーが浮かび上がる。そう、ここにストーリーの分岐があったのだ。

MVの随所に挟まれる恋愛ゲームのモチーフが、2つのストーリーを結びつける。一見すると、恋愛ゲームは、複数の女の子がいて、その間の選び選ばれない嫉妬と争いを印象づける。これはMVでの殺してしまうストーリーの裏付けに見える。

ファントムシータ『にんぎょひめのうた』(OFFICIAL MUSIC VIDEO) https://www.youtube.com/watch?v=Z-N0frXwQE4&t=22s

だが、恋愛ゲームにはもう1つ、マルチエンディングという観点がある。ゲームプレイヤーは選択に迫られる。選択を「間違う」とそれは悪い結果に…時には死ぬようなバッドエンドにつながる。逆に間違わなければ、「本当の結末」に、トゥルーエンドにたどりつくことができる。MVの恋愛ゲームのモチーフは、違う行動によって違う結末を迎えられることを予告していたのではないか。*4

「にんぎょひめのうた」のMVと歌詞、そして原作。これらは、人魚姫・にんぎょひめにとっての複数の結末だ。だが、原作がトゥルーエンドとは限らない。 たしかに人魚姫にとっては、王子の幸せを願うのがトゥルーエンドかもしれない。しかし、ファントムシータのにんぎょひめにとって、そしてわれわれハイネにとっては、一体どの結末がトゥルーエンドなのだろうか。

*1:新装増補版 戸川純全歌詞解説集──疾風怒濤ときどき晴れ | ele-king

*2:https://www.uta-net.com/song/397178/ から引用、以下同じ

*3:それゆえに、「言えなかった」とも見える

*4:Adoはプロデューサーとして本曲について、ドキドキ文芸部!の影響を述べている。 https://x.com/PSP1024daifuku/status/2087920625180713358 同作もまた、一見普通の恋愛ゲームに見えて、実は虚構と現実の境界そのものを揺るがす仕掛けを持つ作品だった。「にんぎょひめのうた」において、MVとは違った歌詞という別のレイヤーを見ることで別の結末が出てくるという構造は、この影響元とも響き合うところがある。

mount namespaceむずすぎるっピ〜になった

clone(CLONE_NEWNS)で新しいmount namespaceでbashとか上げられますね…そこから親も子も同じ/tmpの中身が見えますね…まではいい

ここで子の中でmount -t tmpfs tmpfs /tmpするとどうなるか. 子からは新しく空っぽのtmpfsが見える. 親の方は? 新しいnamespaceなんだから, 親に影響出ないでほしい…が, /tmpはpropagaton=sharedなので子でのmount /tmpが親にも伝わり親の方も空っぽ/tmpになっちゃう.

これをしないためには, 子の方でmount --make-private /tmpして, propagationを切ってやるといい. となった時に, アレこれって親の/tmpのpropagtionも変わる?となるけど変わらない. 結局ここは別のnamespaceなので.

基本, 別のnamespaceなのでmountは互いに無関係なのだけど, propagationによって相手に伝わる…し, manの例は/tmp/fooに新しくmountしたものが相手にも見えますね〜としてて, /tmp自体もpropagationがあることがよくわかってなかった

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だけでできて大変にべんり…という感じだった。…が、いまはやめようかなと思っている。

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

環境が悪いというとそれまでなのだが、nvidiaのGPU(+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を作ろう編

かなあ…