はじめに
この記事は「Rustのstrをコピーなしで連結する」の続きです。
前回の記事でstrを連結する関数を提供するクレートとしてstr-concatを紹介しました。
しかしこのstr-concatは2020/02/07にv0.2.0をリリースした上で、過去のバージョンを全て削除しました。これは依存ライブラリが全てコンパイルできなくなるような大変更です。
この記事では、変更の内容とその理由について説明します。
変更点
元々str-concatはsafeな関数として提供されていました。
なぜsafeにstrを連結できる(と考えられていた)かは前回の記事をご覧下さい。
これがv0.2.0ではunsafeとなり、safeに使うためにユーザが満たすべき条件が明示されました。
その条件とは「連結する2つのstrが一度に割り当てられたメモリ領域に属すること」です。
何がまずいか
別々に割り当てられたstrがたまたま隣接するケースを考えます。
let buf1 = [0; 16];
let buf2 = [0; 16];
if let Ok(combined) = concat_slice(&buf1, &buf2) {
let x = combined[20];
}
このbuf1
とbuf2
はスタックに隣接して積まれるのでstr-concatで連結できます。しかし生成されたstrへのアクセスcombined[20]
は未定義動作を踏んでいます。
combined[20]
はcombined
の先頭アドレスに対しadd(20)
を呼ぶことで行われます。このadd
はunsafeな関数であり、ドキュメントには「以下の条件に違反すると未定義動作となる」と書かれています。
-
Both the starting and resulting pointer must be either in bounds or one byte past the end of the same allocated object. Note that in Rust, every (stack-allocated) variable is considered a separate allocated object.
-
The computed offset, in bytes, cannot overflow an isize.
-
The offset being in bounds cannot rely on "wrapping around" the address space. That is, the infinite-precision sum must fit in a usize.
ここで違反しているのは1つ目の「addの元アドレスと加算後のアドレスが同じ割り当てオブジェクトに属している」です。
実際にLLVMの最適化によってbuf2
が初期化されずx
に不定な値が入る例が報告されています。
正しい使い方
str-concatの関数はunsafeになっただけで使い方は変わりません。
unsafe関数を呼ぶ人が「連結対象の2つが一度に割り当てられたメモリ領域に属すること」を保証する必要がある、ということです。
fn main() {
// コンパイルは通るがNG
let buf1 = [0; 16];
let buf2 = [0; 16];
if let Ok(combined) = unsafe { str_concat::concat_slice(&buf1, &buf2) } {
let x = combined[20];
}
// buf1とbuf2はどちらもbufに属するのでOK
let buf = [0; 32];
let buf1 = &buf[0..16];
let buf2 = &buf[16..32];
if let Ok(combined) = unsafe { str_concat::concat_slice(&buf1, &buf2) } {
let x = combined[20];
}
}