Kernel/VM Advent Calendarの何日目かの記事です.
- 第1回 Waylandで日本語入力への道: 下調べ編 - Gentoo metalog
- 第2回 Waylandで日本語入力への道: vtableを作ろう編 - Gentoo metalog
- 第3回 Waylandで日本語入力への道: コンストラクタに届け編 - Gentoo metalog
- 第4回 Waylandで日本語入力への道: dynamic_castがならなくて編 - Gentoo metalog
- 第5回 Waylandで日本語入力への道: std::string の捏造編 - Gentoo metalog
前回のあらすじ: std::string(C++)のことを調べて, Rustで作れるようになった. でも, 小さい文字列用に最適化された状態にはできない… ちくしょう! アセンブラに相談だ!
DISCLAIMER: waylandで日本語入力したい時にこの記事は役に立たないし, RustでC++のbindingをしたい時にもやめた方がいいと思う.
文字列をやってるとだいぶ日本語入力に近付いてきた気持ちになるこのシリーズいかがおすごしでしょうか.
RVO?なんですか?
さて前回は, std::string(C++)の小さい文字列用フォーマットを作りたかったものの挫折したわけですが…. このへんをいろいろ調べていくと, Return Value Optimization (RVO)による現象なのかなあとわかってきました.
せっかくなので, このへん深掘りしておきましょう. 小さいC++プログラムを作ってやってみましょう. メインはこんな感じ.
#include <string> #include <iostream> extern "C" { std::string f(); } int main() { std::string s = f(); std::cout << s << std::endl; printf("s in main() is at %p\n", &s); printf(" buffer address: %p\n", *(void**)&s); return 0; }
これに対して, C++によるf()の実装とRustによるf()の実装を作ります. C++版はこうなります.
extern "C" std::string f() { std::string foo = ""; printf("foo in f() is at %p\n", &foo); printf(" buffer address: %p\n", *(void**)&foo); return foo; }
RustでSSOしないと, こんな感じ
#[no_mangle] pub unsafe extern "C" fn f() -> CxxString { let buffer: Box<[u8]> = Box::new([0; 32]); let ptr = Box::into_raw(buffer) as *const _; println!("string buffer is at {:?}", ptr); CxxString { ptr: ptr, size: 0, capacity: 32, pad: [0; 8], } }
で, これらのアセンブラを見ます. まずはmain()から.
11b6: 48 89 e5 mov %rsp,%rbp ... std::string s = f(); 11cd: 48 8d 45 c0 lea -0x40(%rbp),%rax 11d1: 48 89 c7 mov %rax,%rdi 11d4: e8 77 fe ff ff call 1050 <f@plt>
ここはf()を呼んでいるところですが, %rdiレジスタに"-0x40(%rbp)"が入っています. ここで%rbp=%rspなので, まあスタック上のアドレス(std::string sのアドレス)が, f()への隠し引数として渡されているわけです.
C++版とRust版では, この引数の扱いが変わってきます. C++版のf()を見ましょう.
22de: 48 89 7d d8 mov %rdi,-0x28(%rbp) # -0x28(%rbp) = %rdi = 隠し引数 = "mainのs" ... 2301: 48 8b 45 d8 mov -0x28(%rbp),%rax 2305: 48 8d 0d f4 0c 00 00 lea 0xcf4(%rip),%rcx # 3000 <_fini+0x928> 230c: 48 89 ce mov %rcx,%rsi # 多分文字列 "" 230f: 48 89 c7 mov %rax,%rdi # callの第一引数: "mainのs" 2312: e8 69 fe ff ff call 2180 <_ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEC1IS3_EEPKcRKS3_@plt> # call されるのはstd::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string<std::allocator<char> >(char const*, std::allocator<char> const&)
ここでcallされているのは, std::stringのコンストラクタです. 結局, main内の"std::string s"に対して, f()の中から直接, 文字列("")を引数にとるコンストラクタを呼んでいます.
では, Rust版ではどうなっているのでしょう.
7847: 48 89 7c 24 08 mov %rdi,0x8(%rsp) ... 78c6: e8 85 f9 ff ff call 7250 <_ZN5alloc5boxed16Box$LT$T$C$A$GT$8into_raw17h9f973d5138f7f2c2E> 78cb: 48 89 44 24 48 mov %rax,0x48(%rsp) # 0x48(%rsp) = Box::into_raw() ... 7951: 48 8b 4c 24 08 mov 0x8(%rsp),%rcx 7956: 48 8b 44 24 10 mov 0x10(%rsp),%rax CxxString { ptr: ptr, # ここから返り値へのコピー 795b: 48 8b 54 24 48 mov 0x48(%rsp),%rdx # %rdx = 0x48(%rsp) = Box::into_raw() size: 0, capacity: 32, pad: [0; 8], 7960: 48 c7 84 24 90 00 00 movq $0x0,0x90(%rsp) 7967: 00 00 00 00 00 CxxString { 796c: 48 89 11 mov %rdx,(%rcx) # ptr = %rdx = 0x48(%rsp) = Box::into_raw() 796f: 48 c7 41 08 00 00 00 movq $0x0,0x8(%rcx) # size = 0 7976: 00 7977: 48 c7 41 10 20 00 00 movq $0x20,0x10(%rcx) # capacity = 32 797e: 00 797f: 48 8b 94 24 90 00 00 mov 0x90(%rsp),%rdx 7986: 00 7987: 48 89 51 18 mov %rdx,0x18(%rcx) # pad
Rust版のf()でも, 返り値となる構造体にBoxで確保したバッファやcapacityなどを直接コピーしています. ここで最適化されて直接構築されるのがある意味で問題で, たとえば一時オブジェクトがポインタで返っていってそれが呼び出し側でコピーコンストクタなり呼ばれなおす…という感じならもしかしてうまくいっていたのかもしれません.
結局のところ, 隠し引数のアドレスをとってきて, そのアドレスを元に適切なデータを構築する必要があります. ということは, このrdiレジスタに来る隠し引数をとる方法がなにかあれば…なにか…なにかないのか……?
asm!()でい!
レジスタには来てるんだから, asmでとればいいじゃないという話ですね. ということで, こんな感じ
let rdi: usize; unsafe { asm!("mov {}, rdi", out(reg) rdi) }; eprintln!("rdi = {:#x}", rdi); CxxString { ptr: (rdi + 16) as *const _, size: 3, capacity: 0x6f6f66, // "foo" pad: [0; 8], }
asm!()でレジスタrdiの値をとって, そこから最適化状態のstd::stringのバッファ位置を計算します. capacityのところに, 文字列になるように値を設定すれば…
rdi = 0x7ffd92b51c40 foo s in main() is at 0x7ffd92b51c40 buffer address: 0x7ffd92b51c50
やったね, 動きました.
ところで, 返り値の書きこみ先がわかったなら, もっと直接的に書く感じのコードでもいいのでは? たとえばこんな
#[repr(C)] struct CxxShortString { ptr: *const u8, size: u64, buffer: [u8; 16], } #[no_mangle] pub unsafe extern "C" fn f() { let rdi: *mut CxxShortString; unsafe { asm!("mov {}, rdi", out(reg) rdi) }; eprintln!("rdi = {:?}", rdi); let s = "foo"; (*rdi).ptr = &(*rdi).buffer as *const _; (*rdi).size = 3; (*rdi).buffer[..s.len()].copy_from_slice(s.as_bytes()); }
これもいい感じに動きました. どっちがRustっぽいでしょうね. asm!()な時点で終わりっちゃ終わりか.
asm!()を使ったりバイナリ形式を仮定したり, ひたすらプラットフォーム依存な感じですが, そういうとこ遊べるのも自作IMEのいいところということでここはひとつ. これで最適化されたstd::string(C++)を作れるようになりました. しかし, これ使いづらいなあ…
ということで, 貯まってたネタがなくなってきたので, 次回はどうだろう…多分
次回: Waylandで日本語入力への道: Preeditを作ろう編
かなあ…