Unsafe Rustとは
Rustは、メモリ安全を特徴とするプログラミング言語です。
メモリの確保や破棄、メモリ領域の利用について厳格なルールが課され、安全でないコードはコンパイル時に指摘されエラーとすることで、ルールに沿ったコーディングを強制されます。窮屈さは否めませんが、見返りとしてメモリに起因する不具合を最小限に抑えたコーディングが可能です。
しかしながら、この制約の中では実現できないことが、特にシステムプログラミングのカテゴリには多数存在します。
システムプログラミングは、微妙なメモリ操作やI/O操作、OSとの連携など、低レベルの処理を要求されることが多いため、必然的にこれらを可能にするプログラミング言語が使われてきました。その代表が、C/C++です。
Rustには、こういった用途に対応するためのUnsafe Rustがあります。Unsafe Rustの世界では、通常のRustのようなメモリ安全を保証する機構は強制されません。それに加えて、以下の操作が可能になります。
- 生ポインタの参照外し
- Unsafeな関数やメソッドの呼び出し
- ミュータブルな静的変数へのアクセスや変更
- Unsafeなトレイトの実装
Unsafe Rustのコードは、以下のように関数定義を含めたunsafeブロックの中に記述します。unsafeブロックの中にだけ安全でないコードを記述し、その他の部分では通常のRustのコードを記述することで、安全でない可能性のある部分を限定できます。
もちろん、unsafeブロックの外では、Rustのチェック機能が通常通り働きます。
fn main() { unsafe { // Unsafeなブロック ...Unsafeなコード... } } unsafe fn func() { // Unsafeな関数 }
以降、Rust 2024におけるUnsafe周りの変更を見ていきます。
Unsafeな関数内におけるUnsafeなコードの扱い
Rust 2024では、unsafe_op_in_unsafe_fnリントが既定となり、Unsafeな関数内でのUnsafeな操作に警告が発生するようになりました。
従来は、unsafeとマークされている関数内であれば、Unsafeなコードを特にブロックで囲む必要はありませんでした。Rust 2024では、たとえUnsafeな関数内でも、Unsafeなコードには明示的なマークが必要となりました。
これに沿わない以下のようなコードでは、unsafe_op_in_unsafe_fnリントにより警告が発生します。get_uncheckedメソッドは、スライスの有効範囲をチェックしないUnsafeな関数であるためです。
unsafe fn get_unchecked<T>(x: &[T], i: usize) -> &T { x.get_unchecked(i) // warning: call to unsafe function }
この警告を回避するには、関数内でのget_uncheckedメソッドの呼び出しを、以下のようにunsafeブロックで明示的に囲むか、unsafe_op_in_unsafe_fnリントを無効化します。
unsafe fn get_unchecked<T>(x: &[T], i: usize) -> &T { unsafe { x.get_unchecked(i) } }
この変更の目的は、Unsafeな関数内で安全でないコードが不用意に記述されてしまうのを防ぐことです。
関数に付与するunsafeキーワードは、その関数の呼び出しにUnsafeなコンテキストが必要であることを示し、その内部で安全でないコードの記述を許可するためものです。後者については、unsafeキーワードの効力に対するリスクが大きいと判断されて、今回の変更に至ったようです。
Unsafeとなったexternブロック
Rust 2024では、externブロックにunsafeキーワードが必須となりました。
ここで言うexternブロックとは、他言語の関数に対するインタフェース(FFI; Foreign Function Interface)のためのブロックです。C++言語におけるextern宣言と同様に、主にC言語の関数に対して使います。
[NOTE]extern crate
externキーワードには、外部クレートを参照するためのextern crateとしての使い方もあります。ただし、Rust 2018でextern crateは不要になり、modキーワードによるモジュール宣言だけで外部クレートのモジュール参照が可能になりました。
externキーワードは、以下のリストのように使われます。キーワードに続く"C"は、C言語のためのインタフェースであることを示しています。
#[link(name = "ext_lib")] (1) extern "C" { fn ext_c_func(x: i32) -> i32; (2) }
(1)のlink属性は、外部ライブラリext_lib(Unix系OSではext_lib.so、Windowsではext_lib.dll)の指定、(2)がライブラリ内の関数のシグネチャです。この場合、C言語側では「int _cdecl ext_c_func(int x);」のように関数が宣言されています。
また、externキーワードは、以下のリストのようにRustの関数をC言語などから利用できるようにするためにも使われます。
#[no_mangle] (1) pub extern "C" fn ext_rust_func(x: i32) -> i32 { (2) x % 2 }
(1)のno_mangle属性は、エクスポートするシンボル名にマングリングを適用しないようにするものです。基本的には宣言した名前がそのままエクスポートされるので、C言語などからその名前を探すことは容易です。
(2)がエクスポートする関数の定義であり、pubキーワードにより公開可能にするほか、extern "C"キーワードに続けて関数をいつも通りに定義します。
[NOTE]マングリング
マングリングとは名前修飾ともいい、C++やRustといったコンパイラ型プログラミング言語で名前の衝突を避けるための仕組みです。これらの言語では、名前空間のサポートで同一名称のシンボルを使えますが、これらのサポートのないコンパイラやリンカ、デバッガがこれらのシンボルを識別できるように、グローバルに一意になるように名前を修飾(マングル)します。
前置きが長くなりましたが、Rust 2024で導入されたexternキーワードに対するunsafeマークは、前者の用法に対するものです。外部で(主にC言語で)作成された関数は、安全である保証はありません。このため、unsafeでマークすることで、externブロック内の関数のシグネチャが正しいものなのか、注意を促します。
前者のリストは、以下のようになります。加えて、個々のシグネチャにsafe、unsafeをマークできるようになり、Unsafeなブロック内でも安全な関数を明示する、といった指定が可能になっています。
#[link(name = "ext_lib")] unsafe extern "C" { fn ext_c_func(x: i32) -> i32; unsafe fn strlen(p: *const std::ffi::c_char) -> usize; safe fn sqrt(x: f64) -> f64; }
この変更によって、externブロックの利用において、安全に注意しなければならないことがより明確になったと言えます。