見出し画像

RustでLチカ (連載4)

さて、とうとうRustでプログラムを書いてみます。

前回まで、C言語やC++言語でLチカを動かしてみました。
これがRustになるとどういう感じになるのか。。。

有識者の導きの下、やってみます。
C/C++をずっと使ってきたとはいえ、Rust初心者の筆者としては、ちょっとドキドキです、、、

1.RustでLチカその1―RAWなプログラム

最初に、とりあえず動かしてみます。

Rustライブラリを作成するためのワークスペースを作成し、サンプルコードをそのままコピー&ペーストし、ビルドして実行してみます。

サンプル一式は以下URLにあります。

https://github.com/KyotoMicrocomputer/solid-rapi4-examples

これから使用するサンプルコードは以下のものです。

https://github.com/KyotoMicrocomputer/solid-rapi4-examples/tree/main/rust-blinky-raw-rtos

lib.rsはこちら。

https://github.com/KyotoMicrocomputer/solid-rapi4-examples/blob/main/rust-blinky-raw-rtos/rustapp/src/lib.rs

Rawなプログラム、と書きましたが、Rustコード上で直接GPIOレジスタを操作する、というニュアンスです。
筆者もそうですが、従来のC/C++民のイメージでは、Low LevelのLow、のニュアンスの方が近いかもしれません。
が、Rust民からすると、Rawだそうです。
こんなところから、感覚が違うんだなーと思いつつ。。。次に行きます。

1.1 Rustのライブラリをリンクする元のワークスペースを作る

まず、C++で新規ワークスペースを作成します。

1.2 RustのLibraryプロジェクトをワークスペースに追加する

①プロジェクトを作成します

②Rustライブラリを選択し、「ソリューションに追加」を選択します。

「ソリューションに追加」は、以下の赤枠の部分から選択できます。

以下のように作成できました。

③メインプロジェクトのリンカ設定で、 追加のライブラリファイル にライブラリ名を指定します。

④メインプロジェクトのビルド依存関係にRustのライブラリを追加します。

1.3 main.cppとlib.rsにプログラムを書く

①main.cppを空にします。

②lib.rsに以下のように書きます。

#[no_mangle]
pub extern "C" fn slo_main() {
    unsafe { ffi::SOLID_LOG_printf(b"Starting LED blinker\n\0".as_ptr().cast()) };

    // Configure the LED port
    green_led::init();

    loop {
        // Turn on the LED
        green_led::update(true);
        unsafe { ffi::dly_tsk(200_000) };

        // Turn off the LED
        green_led::update(false);
        unsafe { ffi::dly_tsk(200_000) };
    }
}

/// FFI declarations used in this application
#[allow(non_camel_case_types)]
mod ffi {
    use std::os::raw::{c_char, c_int};

    pub type int_t = c_int;

    pub type RELTIM = u32;
    pub type ER = int_t;

    extern "C" {
        pub fn SOLID_LOG_printf(format: *const c_char, ...);
        pub fn dly_tsk(dlytim: RELTIM) -> ER;
    }
}

mod green_led {
    const GPIO_BASE: usize = 0xFE200000;
    const GPIO_NUM: usize = 42;

    pub fn init() {
        unsafe {
            let reg = (GPIO_BASE + (GPIO_NUM / 10) * 4) as *mut u32; // GPFSEL4
            let mode = 1; // output
            reg.write_volatile(
                reg.read_volatile() & !(7 << (GPIO_NUM % 10 * 3)) | (mode << (GPIO_NUM % 10 * 3)),
            );
        }
    }

    pub fn update(new_state: bool) {
        unsafe {
            let reg = (GPIO_BASE
                + (GPIO_NUM / 32) * 4
                + if new_state {
                    0x1c /* GPSET1 */
                } else {
                    0x28 /* GPCLR1 */
                }) as *mut u32; // GPFSEL4
            reg.write_volatile(1 << (GPIO_NUM % 32));
        }
    }
}

1.3 ビルド&実行

ビルドして実行をすると、LEDが点滅します。

1.4 デバッグ機能

デバッグ機能が普通に動きます。

・ブレークポイント

・ステップイン

2.RustでLチカその2―PACなプログラム

GPIOレジスタの操作にPAC(peripheral access crate)を使用するプログラムです。

また、TOPPERSカーネル関数 dly_tsk を itron パッケージの itron::task::delay 関数を経由して使用します。

先程作成したlib.rsを、以下サンプルコードのものに置き換え、ビルドして実行してみます。

https://github.com/KyotoMicrocomputer/solid-rapi4-examples/blob/main/rust-blinky-pac-rtos/rustapp/src/lib.rs

2.1 プログラムを書きかえる

先程作成したlib.rsの内容を、以下プログラムに変更します。

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,
            ));
        }
    }
}

次に、GitHub上に存在する、BCM2711 SoC向けのperipheral access crateを、このプログラムで使えるようにします。

Cargo.toml の[dependencies]に次の記述を追加してください。

[dependencies]
tock-registers = "0.7.0"
itron = { version = "= 0.1.9", features = ["unstable", "nightly", "solid_fmp3"] }
bcm2711_pac = { git = "https://github.com/KyotoMicrocomputer/solid-rapi4-examples.git" }

・Cargo.toml

追加すると以下のようになります。

2.2 ビルド&実行

ビルドして実行をすると、LEDが点滅します。

2.3 デバッグ機能

デバッグ機能も先程と同様、普通に動きます。

・ブレークポイント

ブレークしたついでに、RTOS Viewerを見てみましょう。

特にタスクは作っていないので、root_taskがRunningですね。

3.その1とその2の違い

ここで、二つのプログラムのうち、代表してinit()関数を見比べてみましょう。

その1:

    const GPIO_BASE: usize = 0xFE200000;
    const GPIO_NUM: usize = 42;
     :
     :
    pub fn init() {
        unsafe {
            let reg = (GPIO_BASE + (GPIO_NUM / 10) * 4) as *mut u32; // GPFSEL4
            let mode = 1; // output
            reg.write_volatile(
                reg.read_volatile() & !(7 << (GPIO_NUM % 10 * 3)) | (mode << (GPIO_NUM % 10 * 3)),
            );
        }
    }

その2:

   const GPIO_NUM: usize = 42;
     :
     :
    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),
        );
    }

その1で作成したinit関数は、全体的にunsafeで囲われています。

unsafeとは、その名の通り、「安全でない」部分です。
これで囲われている部分は、プログラマが自分自身で安全性を確保する必要があります。

例えば、GPIO_BASEという定数の定義を間違っていると、最悪の場合はリセットになったりします。
そうならないために、正しい値を定義してね、自分でちゃんとしてね、です。

百聞は一見に如かず。
その1のRawなプログラムで、GPIOのアドレスを0番地と定義して、アクセス例外を発生させてみる、という事が簡単にできてしまいます。

これをビルドして実行すると、、、

アクセス例外に飛んでいきました。

って、C/C++だと、その感覚はめっちゃ普通。

一方、その2はunsafeを一か所しか使っていません。
GPIOのレジスタ定義は、PAC(peripheral access crate)であらかじめ定義されているので、アドレス定義のミスが起因での例外発生はありません。(0が一つ足らなかった!とか、ない。)

正しいアドレスが定義されてるされていることは前提です。
プログラマはアドレスが正しいか特に意識する必要ないです。

それに、人によって、命名するレジスタ名っていろいろバリエーションありますよね。
GPIO_BASEだったり、
GPIO_regだったり、
RegGPIOだったり。

プログラマが増えるたびにいろんなレジスタ名が命名されてしまうこともない。
(コーディング規則で、「レジスタ名には _Reg をつけること」として、守ってくれない人にキーキー怒ることもなくなる。)

これって、、、やっぱり、便利じゃない?

4.よく言われる安全性、、、とは?

Rustは安全な言語、とよく言われますが、具体的に何かどう安全でどう嬉しいのか、を有識者にヒアリングし、その場で思いついたことを羅列してもらいました。

4.1 言語仕様としての安全性

まず、一つ目。
C言語とは違い、デフォルトが「安全側」。

  • 変数定義はデフォルトが定数

  • スコープはデフォルトがローカル ⇒ pub宣言をすればパブリックに

  • 初期化なし、は許されない。初期値が決められない場合は、「決められない」と書かないといけない

  • Switch文のdefaultなしは許されない

等など。。。

そして、危険な因子を排除するためなのか、規則がいろいろあり、書き方と手続きがCに比べると複雑です。例えば、

  • Cでいう「シンボル」にも、命名規則がある

  • 文字列は常にUTF8

  • 配置とアライメントとパック 例えばCでは構造体のメンバは,特に指定がなければメンバの持つアライメント値を整列条件として並べられる。Rustではrepr(C)等明示的に指示する必要がある。

等など。。。

これらのことが間違っているとコンパイルエラーや警告になります。

4.2 メモリアクセス手段としての安全性

ポインタはただ場所をポイントしてくれる便利なもの、と筆者は思っていたのですが、、、Rustでは所有権、参照、借用等という概念があり、きっちりと守られています。

C++ ではスマートポインタというのがあり、所有権や参照カウンタ等でdeleteのタイミングを自動で判断してくれるので、Cの場合よりは安全性が向上しているのですが、Rustは、さらに上を行くイメージです。

  • Rustの借用はポインタ+長さ で規定される⇒空間的にも安全

  • 借用した変数は、メモリの寿命(lifetime)が有効であることを保証

  • ある瞬間において、書き手の借用は一人。読み手の借用は複数あってもよい

上記のことが間違っているとコンパイルエラーになります。

加えて、先程の事例でも紹介しましたが、できるだけunsafeは使うべきではありません。本当に、もう、どうしようもない時以外は。。。

5.Rustを使うときに「???」となること

Rustはプログラマ側で生じる危険因子をできるだけ排除した安全な言語、とはいえ、ハードルが低くはないのも事実です。

  • 見てきたように言語仕様が堅苦しく自由にできない
    →言語仕様としての安全性の裏返し。やっているうちに慣れる。

逆に、コンパイル時にあれだけチェックしてくれていたら、コンパイルが通った暁には期待する動作ができるまでの時間が短縮されるかもしれない。

  • Cargoとパッケージ、という概念が慣れない
    新言語なのである程度は新しい概念があるのは仕方ない

  • .oとソースの関係
    コンパイル単位は、コンパイラが決めているようで、どのオブジェクトにそのソースコードが入っているのか、パッと見てわからない。
    →そのうち慣れる。

まぁ、やってたらそのうち慣れるもんですよね。

6.初めてRustを使ってみた感想

あくまでも筆者の意見ですが、メンテナンス性や安全性にあまりとらわれずに、ささっとPOCを自分一人で作るのであれば、使い慣れたC/C++で、今まで気を付けていたことを忘れないよう注意して、動かせばいいと思います。

しかし、多人数で継続性のあるプロジェクトの場合、自由度を持ったプログラミング言語では、意思疎通の齟齬がどうしてもできてしまいます。

生まれも育ちも、価値観も全く違う人間同士がグループになり、一緒にシステムを作り上げていくことを想像してみると、堅固に規定された同じルール(言語仕様)にのっとって作業をすることが大事になります。

筆者の場合、国際色豊かなチームで開発を行っていた際、コーディング規則を細かく厳しくしていくことによって、それを実現していましたが、正直規則を遵守しているかどうか細かくチェックできず、苦労した経験があります。

Rustの場合は、かなりの部分それが言語仕様として規定され、遵守しないとビルドができない、というチェック機能が働いているのは正直なところ有難いです。

今回はここまでです。

次回はRustの特徴である「メモリ安全性」について、もう少し詳しく調べてみます。

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