見出し画像

Rustの文法について (連載9)

前回までで、Rustでプログラムを作成し、クレートも作成し、一通りRustのプログラムが書けるような雰囲気になってきました。
でも本当にそうでしょうか?

今まで使ってきたプログラムは、サンプルプログラムの抜粋です。
ソースコード一行一行、細かく見てはいません。
なんとなく動きはわかるけど、、、じゃぁ見ずに書いてみなよ、と言われたらちょっと泣きそうになります。

では見ていきましょう。

1. 見ていく対象のプログラム

今までさんざんサンプルとして使わせてもらった、以下のソースコードを見ていきましょう。

use itron::{task::delay, time::duration};
 
#[no_mangle]
pub extern "C" fn slo_main() {
   println!("Starting LED blinker");
 
   // Configure the LED port
   green_led::init();
 
   loop {
      // Turn on the LED
      green_led::update(true);
      delay(duration!(ms: 200)).unwrap();
 
      // Turn off the LED
      green_led::update(false);
      delay(duration!(ms: 200)).unwrap();
   }
}
 
mod green_led {
   use bcm2711_pac::gpio;
   use tock_registers::interfaces::{ReadWriteable, Writeable};
 
   const GPIO_NUM: usize = 42;
 
   fn gpio_regs() -> &'static gpio::Registers {
   // Safety: SOLID for RaPi4B provides an identity mapping in this area, and we don't alter
   // the mapping
      unsafe { &*(gpio::BASE.to_arm_pa().unwrap() as usize as *const gpio::Registers) }
   }
 
   pub fn init() {
   // Configure the GPIO pin for output
      gpio_regs().gpfsel[GPIO_NUM / gpio::GPFSEL::PINS_PER_REGISTER].modify(
         gpio::GPFSEL::pin(GPIO_NUM % gpio::GPFSEL::PINS_PER_REGISTER).val(gpio::GPFSEL::OUTPUT),
      );
   }
 
   pub fn update(new_state: bool) {
      if new_state {
         gpio_regs().gpset[GPIO_NUM / gpio::GPSET::PINS_PER_REGISTER]
            .write(gpio::GPSET::set(GPIO_NUM % gpio::GPSET::PINS_PER_REGISTER));
      } else {
         gpio_regs().gpclr[GPIO_NUM / gpio::GPCLR::PINS_PER_REGISTER].write(gpio::GPCLR::clear(
            GPIO_NUM % gpio::GPCLR::PINS_PER_REGISTER,
         ));
      }
   }
}


1.1  use

1行目:
use itron::{task::delay, time::duration};

itronクレートのtask::delay関数とtime::durationマクロを呼び出して使いたいので、これらをuseを使用してスコープ内にインポートする、というコードです。

22,23行目:
use bcm2711_pac::gpio;
use tock_registers::interfaces::{ReadWriteable, Writeable};

これらもgreen_ledモジュールからgpioモジュールを使う指定です。

useを使わなくても、例えば、
&'static gpio::Registersのところを
&'static bcm2711_pac::gpio::Registers とすれば大丈夫です。ですが記述が長くなりますね。

1.2  5,13,17行目 マクロ

println!やduration!のように、「!」が付いているものはマクロを意味します。

1.3  fn, mod

fnは関数、modはそれらを一つの束にしたモジュールの宣言です。

ここまでは直感的に大丈夫ですね。

問題は次です。

1.4 戻り値のある関数、戻りの型の定義

fn gpio_regs() -> &'static gpio::Registers {
   unsafe { &*(gpio::BASE.to_arm_pa().unwrap() as usize as *const gpio::Registers) }
}

gpio_regs()は戻り値のある関数で、-> (右矢印)は戻り値の型を指定しています。
「&'static gpio::Registers」型の戻り値があるという事です。

ここで、「&'static gpio::Registers」型、とはどういう意味でしょうか?

&をつけることは、「参照」を意味しました。
じゃ、その次についているアポストロフィーは?

Rust公式ドキュメント探しました。
https://doc.rust-lang.org/nomicon/lifetimes.html

「Lifetimes are denoted with an apostrophe: 'a, 'static.」
と書かれてあります。
ライフタイムを示すものだそうです。

で?それにstaticが付いたらどうなるのでしょう。
staticなので、もしかしてずっと生きているのでしょうか。

こちらもRust公式ドキュメントにありました。

https://doc.rust-jp.rs/book-ja/ch10-03-lifetime-syntax.html#静的ライフタイム

「議論する必要のある1種の特殊なライフタイムが、'staticであり、これは、この参照がプログラムの全期間生存できる事を意味します。」

やっとわかりました!
gpio_regs()が返す参照が、このプログラムの全期間生存できる、という事ですね。

これでgpio_regs()の戻り値の型とそのライフタイムがわかりました。
では、この参照先はどこでしょうか。return文がありませんよね?

Rustでは、return文がなくても、最後の式の値が戻り値になります。
すなわち、その、(今まで触れないように避けてきた)unsafeで囲われている部分です。

1.5 unsafeブロック再び

なぜここでunsafeブロックが出てきて、一体ここは何をしているのかを見ていきましょう。

unsafe { &*(gpio::BASE.to_arm_pa().unwrap() as usize as *const gpio::Registers) }

unsafeブロックの先頭で&という操作をしています。
これは” * ”によって()の中身の生ポインタを一旦参照解除し参照先の場所にアクセスするようにします。

()の中身:
gpio::BASE.to_arm_pa().unwrap()はGPIOベースアドレス0xfe20_0000u64を指すが、それをusizeにキャストし、さらに*const gpio::Registers 型にキャストする。

その上で”&”をつけ、その場所への参照を生成する、という操作になっています。

こうすることで、生ポインタ (*const _) を通常の参照 (&’static _) に変換した後に、safeコードからgpio::Registersの操作ができるようになります。
(生ポインタのままでできることはほとんどありません。)

生ポインタ (raw pointer, *const , mut _)に対して "&" 操作を行う主な目的は、生ポインタを通常の参照 (&, &mut _)に変換することです。

公式ドキュメントでもこのようなパターンが紹介されています。

A first attempt in Rust - The Embedded Rust Book (rust-lang.org)

このような、生ポインタの参照解除は、unsafe操作として扱われます。というのは、プログラマの不注意によってこの操作を誤った入力で行うと未定義動作を発生させることが可能なためです。

ところで今回のこのunsafe操作は、メモリ安全性を損なわないのでしょうか。
それについても答えが出ており、

・gpio::BASE.to_arm_pa().unwrap()がBCM2711 GPIOレジスタブロックを指す仮想アドレスを正しく示していること
(to_arm_paが返すのは物理アドレスであり、これを仮想アドレスとして扱えるのは、ここに論物一致マッピングが存在するから、です)
・gpio::Registers 型はBCM2711 GPIOレジスタブロックのアクセス用に、I/Oに適した方法 (volatile read/write)でメモリアクセスが行われるように実装されていること

これらのことが分かっているため、メモリ安全性を損なう可能性はありません。

今回のようにunsafe操作を行う場合には、メモリ安全性を損なう可能性はないことを、責任をもって一つ一つ注意深く検証する必要があります。注意を促すために、unsafe操作をunsafeブロックの外で行うとコンパイルエラーになります。

ちなみに、gpio::Registers でどのような操作が可能かという事を知るためには、bcm2711_pacのコードを見てみる必要があります。
以下URLにありますので、興味のある方は見てみてください。
gpset操作、gpclr操作、等記載されています。

https://github.com/KyotoMicrocomputer/solid-rapi4-examples/blob/main/common/bcm2711_pac/src/gpio.rs

自動生成されたAPIドキュメントが以下にあります。こちらもご参照ください。

https://kyotomicrocomputer.github.io/solid-rapi4-examples/rustdoc/bcm2711_pac/gpio/struct.Registers.html

例えば、以下の部分。

ReadWriteをクリックしてみると、これがtock_registersの枠組みを利用して定義されていることがわかります。

さて、以降のunsafeブロックがある行以降のコードについては、あとはそういった操作を行っているだけですね。

サンプルとして使わせてもらったソースコードに記載されている内容は、これでだいたいわかりました。

2. 条件文も少し見てみる

条件文についても少し見てみましょう。

C言語を知っている人なら、まったく問題なく理解できます。

コードは、公式ドキュメントからの抜粋です。

2.1 for文とif文

for n in 1..101 {
   if n % 15 == 0 {
      println!("fizzbuzz");
   } else if n % 3 == 0 {
      println!("fizz");
   } else if n % 5 == 0 {
      println!("buzz");
   } else {
      println!("{}", n);
   }
}

2.2 ループ文、while文

loop {
   count += 1;
 
   if count == 3 {
      println!("three");
 
      // Skip the rest of this iteration
      // 残りの処理をスキップ
      continue;
   }
 
   println!("{}", count);
 
   if count == 5 {
      println!("OK, that's enough");
 
      // Exit this loop
      // ループを抜ける。
      break;
   }
}
while n < 101 {
   if n % 15 == 0 {
      println!("fizzbuzz");
   } else if n % 3 == 0 {
      println!("fizz");
   } else if n % 5 == 0 {
      println!("buzz");
   } else {
      println!("{}", n);
   }
 
   // Increment counter
   // カウンタに1を追加
   n += 1;
}

2.3 Match文(C言語のSwitch文)

match number {
   1 => println!("One!"),
   2 | 3 | 5 | 7 | 11 => println!("This is a prime"),
   13..=19 => println!("A teen"),
   // その他の場合の処理
   _ => println!("Ain't special"),
}

あまり恐れることはなさそうです。

3. ここまで見てきて

この連載を始める前は、Rustコードが呪文にしか見えなかった筆者が、ここまで見てきて思うところがあるので勝手ながら感想を書かせていただきます。

[ネガティブポイント] 
・条件文や宣言文の違いは言語の違いとしてすぐに慣れると思うが、参照、所有権、やっぱりむつかしい。自分が使いこなせる日が来る気がしない。

[ポジティブポイント]😊
・命名規則が厳しくRFC430準拠必要。誰が書いても命名規則が統一されるため読みやすい。
・謎なエラーが発生しないかドキドキしなくてすむ。
(謎なエラー:なぜかまれにリセットする、等。だいたいメモリアクセス起因。)
・文字列操作がしやすくなっている感じがする。
・ビルドが通りさえすれば動きそう。(プログラムが論理的に間違っていなければ)

なんと、ネガティブポイントより、ポジティブポイントの方が多くなりました。

連載前までは、

「そうはいっても、やっぱり使い慣れたC言語でないとコーディングが怖いわ」

と思っていたのですが、なんと!

ここにきて、今まで普通にあると思っていた悩みがなくなるのであれば使ってみたいと思うようになってしまっていました。

特に謎なリセット発生をデバッグするのって大変。原因は大抵不注意。(あ、しまった、程度のやつ。)

メモリアクセス起因の未定義例外の発生を抑えられるという事が、どれほど開発期間を短縮してくれるか、という点、元来適当な性分である筆者にはかなり響いております。

今回はここまで。

次回は「Rustを取り巻く世の中の取り組み」を見てみようと思います。

この記事が気に入ったらサポートをしてみませんか?