SOLID未分類 SOLID for Raspberry Pi 4 (連載5)

SOLID for Raspberry Pi 4 (連載5)

(2022/11/21)

メモリ安全:所有権・借用

Rustではデータに所有権があって、所有権を奪われないためには借用するんだよ。。。

 

と言われても、難しすぎて何の事かわかりません。

しかしここを理解しなければ、何がどうメモリ安全なんだか、うわっつらで言葉をなでているだけにすぎません。
筆者、完全にその状態です。

有識者のさらなる導きの下、勉強していければと思います。

 

 

1.所有権

動的なメモリ領域を確保したとして、誰が責任をもってその変数が確保していた領域を解放するのか。

 

Cの場合、動的メモリ領域はmallocで確保しfreeで解放しています。

プログラマがfreeを書き忘れたら、メモリリークになります。メモリを食いつぶし、システムクラッシュに至ります。

すなわち、
「システムが不安定。」
という、謎現象。

終わりの見えないデバッグを何度した事か。

 

やらかすのはfreeを書き忘れる事だけではありません。
うっかりと、既にfree済の領域に対してアクセスしてしまったりすることもあります。
また慎重になりすぎるあまり、freeしてるのにまたfreeするコードがあったり。。。

C++の場合は、new で確保しdelete で開放します。deleteを忘れるとメモリリークになります。基本同じです。
delete忘れます。そしてやはりシステムクラッシュします。

これに対処するため、C++にはスマートポインタというものが存在します。
スマートポインタとは、メモリを自動で解放してくれる、というものです。

・変数がスコープから外れたときにメモリが開放するstd::unique_ptr。
・参照カウントを持ち、参照カウントがゼロになったときにメモリを解放するstd::shared_ptr。

スマートポインタを使って実装すれば良いですが、スマートポインタを使わなくても実装できるので、ついつい安易な方向に。。。

 

Rustでは、このC++のスマートポインタの概念を一般化させ、普通メモリ(場所)って所有権あるよね、所有権もってる人(スコープ)が使い終わったら(スコープ外)勝手に開放するからね、になりました(と理解しました)。

すなわち、全データ型(※)に所有権が存在し、「スマートポインタを使わなくても。。。」という抜け道がふさがれてしまった一方、解放に責任を持つ必要がなくなりました。

※usize型など、一部、Copyトレイトといって、所有権を自動で複製するような型も存在します。この場合、プログラマは所有権を意識する必要はありません。

 

では実際に、見ていきましょう。

 

1.1 所有権のムーブ

Rustでは、データ(メモリ上に指定されたアドレスに書かれている値)に対し所有権を持つのは、必ず一つのスコープ内からのみ、です。
スコープ外のところにデータが移るのであれば、移った先のスコープに所有権を移します。

 

例えば、


	let numgpio_array: Vec<usize> = vec![42, 5, 6];
	green_led::init(numgpio_array);

mod green_led {
		:
		:
    pub fn init(num: Vec<usize>) {
        // Configure the GPIO pin for output
        gpio_regs().gpfsel[num[0] / gpio::GPFSEL::PINS_PER_REGISTER].modify(
            gpio::GPFSEL::pin(num[0] % gpio::GPFSEL::PINS_PER_REGISTER).val(gpio::GPFSEL::OUTPUT),
        );
		:
		:
}

というコードがあったとします。

numgpio_arrayとしてメモリ内に、42, 5, 6というデータがあります。
これを、init()関数の引数として指定してみます。

Rustではこの場合、numgpio_arrayで指定されているメモリ領域にあるデータは、init関数のスコープに所有権が移ります。
したがって、以下のコードはエラーになります。


	let numgpio_array: Vec<usize> = vec![42, 5, 6];
	green_led::init(numgpio_array);
	println!("{}", numgpio_array[0]);

init関数の次の行は、もうnumgpio_arrayの所有権がなくなっているからです。

実際にビルドしてみます。
エラーになりました。

・numgpio_arrayの型Vec<usize>はCopyトレイトついてないよ
・値(の所有権)はinit関数に移されたよ
・値(の所有権)が移されたのにprintlnマクロで参照しちゃってるよ

と表示されています。わかりやすいエラーメッセージですね。

ソースコード内にもワーニングやエラーが表示されています。
緑下波線:warning
赤波線:error

同じメッセージが出ていますね。

 

1. 2 所有権の借用

この場合、init関数内では引数を参照しているだけで、編集は必要ありません。
なので、所有権は移動せず、見るだけ、という事ができます。

「借用」です。

&をつけ、借用であることを明示的に伝えます。


	let numgpio_array: Vec<usize> = vec![42, 5, 6];
	green_led::init(&numgpio_array);
	println!("{}", numgpio_array[0]);

mod green_led {
		:
		:
    pub fn init(num: &Vec<usize>) {
        // Configure the GPIO pin for output
        gpio_regs().gpfsel[num[0] / gpio::GPFSEL::PINS_PER_REGISTER].modify(
            gpio::GPFSEL::pin(num[0] % gpio::GPFSEL::PINS_PER_REGISTER).val(gpio::GPFSEL::OUTPUT),
        );
		:
		:
}

ビルドすると、エラーは解消されます。

 

 

2.ライフタイム

借用して得られる参照 (上の例の場合 &Vec<usize>) の有効期間をライフタイム (寿命) と呼びます。

みていきましょう。

2.1 ライフタイム期間外になってしまうケース

ソースコードを一部変更します。


    let numgpio_array: Vec<usize> = vec![42, 5, 6];
    let r: &usize = &numgpio_array[0];
    green_led::init(numgpio_array);
    println!("{}", r);

 

このスコープでは、numgpio_arrayは、init関数のスコープに所有権が移ります。
init関数が終了するとnumgpio_arrayは解放されているので、printlnマクロの時点では寿命がなくなっています。
rは、numgpio_arrayを参照しており、もう寿命がなくなっているものを参照しているので無効となり、コンパイル時にエラーが報告されます。

 

2.2 ライフタイム期間外にならないケース

では、以下のようにしてみましょう。


    let numgpio_array: Vec<usize> = vec![42, 5, 6];
    let r: &usize = &numgpio_array[0];
    green_led::init(&numgpio_array);
    println!("{}", r);

こちらの場合、init()関数はnumgpio_arrayを借用しているだけであり、所有権は移動していません。
なので、printlnマクロ実行時に、まだnumgpio_arrayは生きているので、rの参照先も生きています。このためエラーにはなりません。

 

 

3.おまけ:命名規則

numgpio_arrayという名前ですが、筆者は最初にnumGpioArrayとしていました。

そうすると、、、

Warningが出てしまいました。

大文字は入れないようにした方がよい、という命名規則に抵触してしまいました。

 

 

4.Cと比べてみて

人間であれば、mallocを書いた後、freeするのを忘れたことがない人はいないのではないかと思います。

注意していてもどうしても忘れてしまうのは仕方ないのですが、いざシステムがうまく動かない時に原因がここにあると気が付くまで、非常に労力を使うし時間も使いますよね。

C#の方向の進化では、ガーベージコレクションという、newしたオブジェクトが使われなくなったら、どこかのタイミングで自動的に開放される仕組みがあります。

筆者は、WindowsアプリくらいしかC#を使わないのですが、newしてdeleteしないのに慣れてしまってC++を書くときにdelete忘れてしまったことがあり、せっかく言語仕様が進化していっているのに自分がついっていってない、、、

 

ケアレスミスを注意して防ごう、というのではなく、誰が書いてもケアレスミスをしないような言語仕様になっていく流れは素晴らしい反面、ちゃんとついていかなければと思った次第です。

 

今回はここまで。

次回は「言語仕様の安全性」に触れてみたいと思います。