Unsafeがよくわからなかったので整理した (Rust)

はじめに

RustのThe Bookを読んでも、Unsafeについて何となくもやっとしていたので書いて整理しました。整理したというのは私が何をわかっていて何をわかっていないかを何となくですが把握したということです。

例えば、The Bookには以下のようなことが書いてあります。

Rust has a second language hidden inside it that doesn’t enforce these memory safety guarantees: it’s called unsafe Rust and works just like regular Rust, but gives us extra superpowers.

Rustの第2の言語とか、superpowersという単語で混乱し、つまりUnsafeってなんなんだともやもやしていました。この記事での説明はだいぶ僕の解釈が入っているので、正確なところは記事の下の方にドキュメントへのリンクを載せたので参照ください。

Unsafe

構文と意味を分けて説明しようと思います。構文の話は単純なのでわかりやすいです。

構文の観点から

Rustの構文のうちのいくつかをUnsafeと呼んでいて、Unsafeとはそれらの構文のことです。いくつかの構文というのは以下のような操作ができる構文です。

  • Raw Pointerの参照を外す
  • Unsafeな関数やメソッドを呼ぶ
  • Mutable Static Variableへのアクセス
  • Unsafe Traitを実装する
  • 共有体のフィールドの値へのアクセス

コンパイラは以上のようなことをチェックしていて、例えばRaw Pointerというポインターの参照を外して値を読み取ろうとするとエラーを出します。読み出すにはunsafeというキーワードをつけたブロックの中で行う必要があります。端的に言うと、上記で挙げたようなUnsafeな構文はunsafeというキーワードをつけたブロック内でみ使えて、ブロック外だとエラーになります。

意味の観点から

じゃあ、一体どういう意味なんだというと、The Bookにはあまり詳しくは書かれていません。おそらく背景の理論が型理論とかそういうやつで、説明すると難しくなるからふわっとした説明になってしまうのでしょうか。だから私はもやもやしていたのでしょうか。

なので、「こういうことなんでしょ」という何となくの私の気持ちを書いておこうと思います。*1

Rustではコンパイラにより安全性が保証されています。これはコンパイラのチェックで定義した状態になることが理論的に証明されているということです。なので例えば参照先はいつでも正常な値を参照しています。コンパイラのチェックが通ったプログラムがセグメンテーションフォルトが起こることはありえないということです。

Rustの制約は厳しいです。例えばイミュータブルな参照とミュータブルな参照を同時に持つことはできません。制約をつけることで安全性の証明がしやすいのでしょう。そのためにマクロで柔軟性をカバーしたりしています。

Unsafeな操作ができるようにしたのは、そうせざるを得なかったからです。ドキュメントには以下のように書いてあります。

Another reason Rust has an unsafe alter ego is that the underlying computer hardware is inherently unsafe. If Rust didn’t let you do unsafe operations, you couldn’t do certain tasks. Rust needs to allow you to do low-level systems programming, such as directly interacting with the operating system or even writing your own operating system. Working with low-level systems programming is one of the goals of the language.

または以下です。

By opting out of having Rust enforce these guarantees, you can give up guaranteed safety in exchange for greater performance or the ability to interface with another language or hardware where Rust’s guarantees don’t apply.

つまり理論的に安全性が保証された部分の機能だけでは、システムプログラミングや他の言語を呼び出したりできないです。なぜならRustで定義していないものを扱うからです。またはパフォーマンスの改善には安全性が保証されていない操作をする必要があります。*2

それではUnsafeな操作をいくつか見ていきます。

Raw Pointerの参照を外す

Raw Pointerは、

  • Borrowing Rulesがない - イミュータブルな参照とミュータブルな参照を同時に持てないとかそんなルールがない
  • 不正な参照な可能性がある
  • Nullポインターも持てる
  • メモリの開放は手動でやる

というようなポインターです。

Raw Pointerにはイミュータブルなものとミュータブルなものがあり、以下がアスタリスクも含めて型の名前です。

  • *const T
  • *mut T

イミュータブルとは、参照を外したときに値を変更できないという意味です。試しに、イミュータブルなRaw Pointerの参照を外して値を書き換えようとするとエラーが出ました。

`r1` is a `*const` pointer, so the data it refers to cannot be written

ポインタの作成はunsafeキーワードをつけたブロック(Unsafeブロックと呼ぶことにします)の外でもできますが、そのポインタの参照を外すにはUnsafeブロックの中で行う必要があります。

let mut num = 5;

let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;

unsafe {
    println!("r1 is: {}", *r1);
    println!("r2 is: {}", *r2);
}

このUnsafeブロックにより、Raw Pointerの参照を外すというUnsafeな構文を扱えます。そしてBorrowing Rulesがない状態でポインタを扱えます。これはどういうことかというと、Unsafeブロックの中で起こることはコンパイラに安全性が保証されていないということです。Unsafeブロックの中ではプログラマがポインタの参照先などをしっかり管理する必要があります。Unsafeな操作をするとプログラムは全体的に安全性が保証されていない状態になります。ただし例えばもしセグメンテーションフォルトなどが起こったとしたらUnsafeブロックの中を見ると良いということです。なぜならそれ以外の部分ではコンパイラが安全性を保証しているからです。

Unsafeブロックという存在自体は本質的ではなく、 Unsafeな操作ができるのはUnsafeブロックの中 というのが大事ということですね。別にそんなブロックを作らなくても良かったわけですが*3、そのブロックによりUnsafeな操作をしている範囲というのを絞れるようになります*4。つまりプログラマとしては、Unsafeなブロックをあちこちに作ったり、Unsafeなブロック内で余計なことをせずに、 Unsafeブロックは必要最小限 にというのが鉄則ですね。

Unsafeな関数やメソッドを呼ぶ

unsafeキーワードがつけられた関数やメソッドではコンパイラは安全性を保証しません。そのような関数やメソッドはやはりUnsafeブロックから呼ぶ必要があります。

unsafe fn dangerous() {}

unsafe {
    dangerous();
}

それらの関数やメソッド内ではUnsafeな構文を扱えますし、外部の言語の関数を呼び出すなどができます。Rustで定義されていない状態を扱うのでUnsafeブロック内でのみUnsafeな関数やメソッドを呼べるということですね。

Mutable Static Variableへのアクセス

Rustのグローバル変数は以下のようにstaticをつけて宣言します。

static HELLO_WORLD: &str = "Hello, world!";

fn main() {
    println!("name is: {}", HELLO_WORLD);
}

constキーワードをつけたものは定数であるのでmutをつけて可変にすることはできません。staticキーワードをつけたグローバル変数mutをつけると可変にすることができます。そうすると同時にアクセスされた時にデータの競合が起こりますので、Unsafeブロックの中でプログラマがデータの競合に気をつけて扱います。

ちなみに以下のように定義してHELLO_WORLDを扱った場合、staticと似ています。違いはstaticは常に同じ参照であるが、constはそうではないということだそうです。staticにはグローバル変数といういう意味合いがあるからでしょうか。

const HELLO_WORLD: &str = "Hello, world!";

その他

以下はあまり理解していません。

  • Unsafe Traitを実装する
    • あまりよくわかっていませんが、似たような話だと思いますw
    • unsafeキーワードを付けて実装する必要があるようです。呼ぶ時はUnsafeブロック内でしょうか。
  • 共有体のフィールドの値へのアクセス
    • 共有体のフィールドへのアクセスもUnsafeブロック内で行う必要があるようです。理由はデータの競合に似た感じでしょうか。

参考

終わりに

Unsafeがsuperpowersというのは実はちょっと違うのではないでしょうか?むしろスーパーパワーなのは安全性が保証されているSafeなRustの方な気がしています。Unsafeはtechniquesとかhacksの方が私はイメージが近いです。だからもやもやしていたのかもしれません。

Techniques or Hacks(Unsafe Rust)の扱いで、Super Power(Safe Rust)が活きるかどうか決まると...。

f:id:gkuga:20200214131334p:plain

*1:願わくば詳しい人に教えてほしい。型理論とかプログラム意味論?(そもそも背景の理論に何があるのかわかっていない)を勉強せねばという気持ちです。

*2:OSをRustで書けば、ハードウェアとのインターフェースやパフォーマンスの改善部分以外では安全性が保証されていることになります。Rust製のOSを作るモチベーションはそのようなところでしょうか?そうなるとそのOS上でRust以外の非安全な言語を使ってコードを書くことは、想像すると面白いですね。

*3:いや、ブロックを作らない場合どうすればいいかな..コンパイラの実装大変になるか。

*4:SafeとUnsafeの境界がはっきりして、コンパイラの実装も楽になるのかも。知らないけど。