Kernel/VM Advent Calendarの何日目かの記事です.
- 第1回 Waylandで日本語入力への道: 下調べ編 - Gentoo metalog
- 第2回 Waylandで日本語入力への道: vtableを作ろう編 - Gentoo metalog
- 第3回 Waylandで日本語入力への道: コンストラクタに届け編 - Gentoo metalog
- 第4回 Waylandで日本語入力への道: dynamic_castがならなくて編 - Gentoo metalog
前回のあらすじ: 関数だけ並べたvtableでさぼっていたら, しっかりdynamic_cast が動かなくて怒られた. bindgenもそうしてるんだけどなあ…. しょうがないので, ちゃんと型情報を入れてあげました.
DISCLAIMER: waylandで日本語入力したい時にこの記事は役に立たないし, RustでC++のbindingをしたい時にもやめた方がいいと思う.
そろそろ忘れられているかもしれませんが, このシリーズはWaylandで日本語入力をするために行われています. 今回は std::string のbinary formatを調べて, それをRustで作りあげて C++ (fcitx5)に返します. 日本語を入力するというのは, こういった作業の先にあるもので, かくも大変なものなのですね. 日本語をしっかり入力していきましょう.
std::string の中身ってどうなってるの?
さっそく std::string のbinary formatを調べましょう. ぐぐってるとこういう記事が見つかりました.
これを見ると, std::stringは基本形態では図の左のように3つのフィールド buffer・size・capacityを持つことがわかります. bufferがheap上で文字列を保持するバッファを指し, sizeは文字列の長さ, capacityはbufferのサイズです. (x86_64 linuxでは)
一方, 文字列のサイズが小さい場合には, その管理のために24byteを使うのは無駄があります. そこでSmall String Optimization (SSO)として, 図の右側のように, 構造体の内部に文字列のデータを持っておきます. この時, capacityは32byteで固定ということなのでしょう.
gdbで確認じゃ
せっかくなので実際にコードを書いて確認しておきましょう. 以下のようなコードを書いて, "g++ -O0 -ggdb"でコンパイルしてgdbでcoutの行で止めます.
#include <string> #include <iostream> std::string f() { std::string tmp = "foo"; tmp.reserve(32); return tmp; } int main() { std::string s = f(); std::cout << s << std::endl; return 0; }
さて見てみますと, たしかに最初の8byteが文字列のバッファを指していて, その次に文字列サイズの3, そして次にバッファサイズの0x20=32が入っています.
(gdb) n 11 std::string s = f(); (gdb) 12 std::cout << s << std::endl; (gdb) x/4xg &s 0x7fffffffd7a0: 0x000055555556c2b0 0x0000000000000003 0x7fffffffd7b0: 0x0000000000000020 0x0000000000000000 (gdb) x/s 0x000055555556c2b0 0x55555556c2b0: "foo" (gdb) p sizeof(s) $1 = 32
では, ここで"tmp.reserve()"の行を削除するとどうなるでしょう. Small String Optimizationがきかなくなるので, 文字列"foo"がsの内部に埋め込まれます.
(gdb) n 12 std::cout << s << std::endl; (gdb) x/4xg &s 0x7fffffffd7a0: 0x00007fffffffd7b0 0x0000000000000003 0x7fffffffd7b0: 0x00000000006f6f66 0x0000000000000000 (gdb) x/s 0x7fffffffd7b0 0x7fffffffd7b0: "foo"
Rustでstd::string(C++)を作ろう
これらをふまえて, Rustでstd::string(ただしC++)を作って返しましょう. Rustのなら一発なのに.
こんな感じで標準タイプのstd::string(C++)の形を作って
#[repr(C)] struct CxxString { ptr: *const u8, size: u64, capacity: u64, pad: [u8; 8], }
適当に32byteのバッファを作って CxxStringの構造体に持たせて, capacityを32にしておきます. バッファは(多分)C++がstd::stringが死ぬ時に解放してくれる…はずなのでOK.
let buffer: Box<[u8]> = Box::new([0; 32]); CxxString { ptr: Box::into_raw(buffer) as *const _, size: 0, capacity: 32, pad: [0; 8], }
ということで動かすと, 今度は"sub_mode()"のtodo!()に当たって死にました. こっちは上記の右側の構造でやってみたいところです.
RustでもSSOしよう
として, したかったんですが一度挫折しました. 挫折したコードたち
let x = Box::new(CxxString { ptr: std::ptr::null_mut(), size: 0, capacity: 0, pad: [0; 8], }); let ptr = Box::into_raw(x); let buf = (ptr as u64) + 16; let mut x = Box::from_raw(ptr); x.ptr = buf as *const _; let ptr = Box::into_raw(x); *ptr
let mut x = CxxString { ptr: std::ptr::null_mut(), size: 0, capacity: 0, pad: [0; 8], }; let ptr = &mut x as *mut _; x.ptr = ((ptr as u64) + 16) as *const _; x
なにをどうしてもdouble freeぽくなったりしてます.
たとえば下のコードだと関数の出口で
(gdb) p &x $1 = (*mut tcode::CxxString) 0x7fffffffce80 (gdb) x/4xg $1 0x7fffffffce80: 0x00007fffffffce90 0x0000000000000000 0x7fffffffce90: 0x0000000000000000 0x0000000000000000
こんな感じで, SSOされたバイナリができています. これが返っていって
(gdb) n fcitx::InstancePrivate::showInputMethodInformation (this=this@entry=0x5555555d2ea0, ic=ic@entry=0x555555e7c700) at /dev/shm/portage/app-i18n/fcitx-5.1.5/work/fcitx5-5.1.5/src/lib/fcitx/instance.cpp:391 391 auto subModeLabel = engine->subModeLabel(*entry, *ic); (gdb) p &subMode $2 = (std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > *) 0x7fffffffcf10 (gdb) x/4xg &subMode 0x7fffffffcf10: 0x00007fffffffce90 0x0000000000000000 0x7fffffffcf20: 0x0000000000000000 0x0000000000000000
こう, 本当にそのまま値がコピーされてるんですよね. 結果として, std::string(C++)のデスクトラクタがこれはSSOではないなと思って, 0x00007fffffffce90をfree()しにいく. すると, そこはallocした先頭のアドレスではないのでinvalid pointerで死ぬと見えます.
一方C++だとなんだかmainのstack上で直接いじられているように見える.
なんでだろ〜となった時, 困った時は逆アセンブルですよね. ということで
次回: Waylandで日本語入力への道: Return of RVO編