macOSでRustコードをクロスコンパイルしてRPi向けのバイナリを作る

この記事では
「Raspberry Pi上で動かすためのプログラムをRustで書いてmacOS (Apple Silicon) 上でビルドする方法」
を解説する。最後におまけとしてLチカもやるよ🦀

RPiに限らず、「Rustのクロスコンパイルをやりたい」あるいは「crossの導入でちょっとハマった」という方の役にも立つかもしれない。

crossとは何か

今回お世話になるのが、Rustのクロスコンパイルを簡単にするcrossというプロジェクトだ。色々なターゲット用にDockerのコンテナとしてビルド環境一式を用意し、お手軽にクロスコンパイルができるのだ。
crossを使わずに cargo build でクロスコンパイルすることも可能なのだが、その場合、何らかの方法でローカルにツールチェーンを用意する必要がある。そのツールチェーンの準備方法がターゲットごとにまちまちであったり、自分の環境に対応していなかったり、情報がなかなか見つからなかったりして、高確率でハマる。心が折れる。なるべくcrossで楽をしよう。

ターゲットはどれを選ぶ?

とりあえず、Raspberry Pi 3以降はarmv7-unknown-linux-gnueabihfを選んでおけば動くかな? もっとCPUの性能を引き出せるものもあるかもしれない。詳しくはRPiのプロセッサ一覧を見て、合ってそうなものを選んでほしい。

準備する

事前に用意するものは

の2つ。Dockerは、crossを動かすために必要となる。

RPiデバイス上で実際に動かすところまでやりたい場合は、sshで入れるようにしておく。

Rustの開発環境とDockerが用意できたら、まず、crossをインストールする。コマンドラインから

cargo install cross

で入れられる。どこでやってもいい。

次に、rustupでツールチェーンを入れる。これはターゲットを追加するたびにやること。これも、どこでやってもいい。

rustup target add armv7-unknown-linux-gnueabihf

プロジェクトを用意する

Rustのプロジェクトを用意してみよう。

cargo init hello_cross

という具合に、好きな名前でプロジェクトを作し、プロジェクトのルートディレクトリに移動しておく。もしくは、既存プロジェクトを別ターゲット用にビルドするのでもいい。

ビルドする

crossは、基本的に「cargoのかわりにcrossと打てば、cargoコマンドでやる色々なことをcrossを使ってできるよ」という風に作られている。ビルドするなら、プロジェクトルートでこれを実行すればいい。

cross build --target armv7-unknown-linux-gnueabihf

問題がなければ、これでうまくいく。
指定したターゲットに対応したイメージを自動で docker pull してくるので、初回はかなり時間がかかる。コーヒーを飲みながら待とう。

もしも面倒なことになった場合は --verbose オプションを追加して実行すると詳しいメッセージが出る。
私はここでけっこうハマったので、次のセクションでいくつかの解決方法を書くので、うまくいかなかった人は参考にしてほしい。

うまくいかなかった場合、こうするといいかも?

ターゲットを指定してDocker Desktopも起動しているのにcrossがその通りに動いてくれないことがある。

  • 指定したターゲットが無視されてしまう
  • Dockerコンテナの導入・起動に失敗する

というパターンがある。
イメージが存在しているかどうかをチェックするには、Docker Desktopの “Images” のリストをチェックしてみる。ghcr.io/cross-rs/{{TARGET}}:{{VERSION}} が存在すれば、イメージの取得は成功している。

Dockerイメージを手動で入れてみる

crossが何らかの原因により自動でイメージを取得できなかった場合は、自分でdocker pullしてみると、以降、crossがそのイメージを適切に起動してビルドに成功するようになったりする。

$ docker pull ghcr.io/cross-rs/armv7-unknown-linux-gnueabihf:0.2.5

このあと普通にcrossコマンドを打つとうまくいったりする。
イメージの取得元は

ghcr.io/cross-rs/{{TARGET}}:{{VERSION}}

だ。{{VERSION}}のところはcrossのバージョンを入れる。バージョンのところは latest と書けばいいらしいが、うまくいかない場合は 0.2.5 のようにバージョンを明記する。crossのバージョンは cross version で調べられる。

Cross.tomlでDockerイメージを指定

プロジェクトルートにCross.tomlというファイルを用意する。crossの設定ファイルだ。このファイルで、使うべきイメージを明示する。たとえばこんな感じ。

[target.armv7-unknown-linux-gnueabihf]
image = "ghcr.io/cross-rs/armv7-unknown-linux-gnueabihf:0.2.5"

詳しくは、crossの設定ファイルについての公式ドキュメントを読むといい。

ちょっと古いバージョンのイメージを入れる

crossのバージョンと完全に一致しているイメージを入れなくても、ちょっと前のバージョンのイメージが動いたりする。最新版がうまくいかなかったら、少し古いものを見てみよう。ghcr.io/cross-rs/armv7-unknown-linux-gnueabihf:0.2.5 がダメだったら ghcr.io/cross-rs/armv7-unknown-linux-gnueabihf:0.2.4 という具合に。

実行

でき上がったバイナリは target/armv7-unknown-linux-gnueabihf/debug/<project_name> にあるので、これを何らかの手段でRPiデバイスに送り込もう。外付けのUSBメモリなどでもいいし、scpなら

scp ./target/armv7-unknown-linux-gnueabihf/release/hello_cross pi@<rpi_host_name>.local:~/

だ。バイナリを置いたら

./hello_cross

のようにコマンドラインから実行。

Hello, world!

と表示されるかな?

おまけ : RPiでRustでLチカ

ハードウェア周りをいじりたいと思ったら rppal というcrateを使うといい。
GPIO・I2C・PWM (sysfs経由)・SPI・UARTに対応している。

まずは、cargo.tomlの依存関係にrppalを追加

[dependencies]
rppal = "0.17"

main.rs のプログラム本体はこんな感じで。ピンは、物理的なピン番号ではなくGPIOの番号で指定する。たとえばGPIO12なら12だ。

use rppal::gpio::Gpio;
use std::{error::Error, thread, time::Duration};

const GPIO_LED: u8 = 12;
const INTERVAL: Duration = Duration::from_millis(500);

fn main() -> Result<(), Box<dyn Error>> {
    let mut led = Gpio::new()?.get(GPIO_LED)?.into_output();

    loop {
        led.set_high();
        thread::sleep(INTERVAL);
        led.set_low();
        thread::sleep(INTERVAL);
    }
}

うまくいくと、指定したピンに接続したLEDが点滅する。