本章で想定している環境
用語
まずは用語を整理します。本章で用いる用語には以下のものがあります。
- ユーザプログラム
- 選手が提出したプログラムのこと
- ジャッジサーバ
- オンラインジャッジを行うサーバのこと。ジャッジサーバはユーザプログラムをコンパイルし、実行する
- 実行ファイル(executable file)
- マシンコードを含み、メモリに読み込んで実行できる形式のファイルのこと。バイナリファイルとも呼ばれる。プログラムのソースコードをコンパイル、リンクすることで生成される
- Rustコンパイラ(
rustc
)rustc
コマンドのこと。Rustプログラムのコンパイルとリンクを行う- なお
rustc
にはリンカの機能は含まれていないため、rustc
はリンク時に外部ツールを呼び出すようになっている。ターゲットがLinux Gnu ABIの場合はgcc
経由でld
を呼び出す
- Cargo(
cargo
)- Rustのビルドツール兼パッケージマネージャの
cargo
コマンドのこと - なおカーゴは貨車の意味
- Rustのビルドツール兼パッケージマネージャの
- Rustツールチェイン
- Rustコンパイラ、Cargo、Rustの標準ライブラリなどをバージョンごとにまとめたもの
- Rustup(
rustup
)- Rustツールチェインの管理ツールである
rustup
コマンドのこと - 指定したバージョンのRustツールチェインを簡単にインストールできるだけでなく、複数バージョンのツールチェインも管理できる
rustc
やCargoと同様、Rustプロジェクトチームにより公式にサポートされている
- Rustツールチェインの管理ツールである
- クレート(crate)
- クレートは貨物などを入れる木箱の意味
- Rustにはlib crateとbin crateがある
- ライブラリクレート(lib crate)はRustで書かれたライブラリのこと。コンパイルするとrlibファイルが作られる
- バイナリクレート(bin crate)はRustで書かれたアプリケーションのこと。コンパイル、リンクすると実行ファイルが作られる
- 世界中のRustユーザが作成した無数のクレートが、セントラルリポジトリのcrates.ioで公開されている
- 一般にクレートと呼ぶ場合は文脈によりバイナリクレートかライブラリクレートかを区別する
- 今回の例では、外部クレートは全てlib crateで、外部クレートをまとめてコンパイルするため・ユーザープログラムをコンパイルするために使うクレートはbin crate
- Cargoパッケージ
- Cargoの1単位。Rustのソースコードや
Cargo.toml
などの設定ファイルで構成される - 1つのCargoパッケージには単一または複数のクレートが含まれる
- Cargoの1単位。Rustのソースコードや
- Cargoプロジェクト
- 基本的にCargoパッケージと同じもの
想定する環境
本章ではAtCoderのジャッジサーバ環境として、以下のものを想定しています。
- Ubuntu 18.04 LTS x86_64
- ユーザプログラムは何らかのコンテナ内で実行される
- インターネットなどのネットワークアクセスは不可
- 物理メモリ、ディスクファイルサイズ、プロセス数などの制限がある
- ユーザプログラムが出力したファイルは、テストケースを1つ実行するごとに削除される
- プログラミング言語ごとにファイルシステムが分かれているわけではない
- たとえば提出言語としてBashやPythonを選択したときに、他の言語のコンパイラを実行するようなスクリプトを提出、実行できる
- (このようなプログラムを提出することは推奨はされないだろうが、現状は可能となっている)
- ユーザプログラムをコンパイル、実行するLinuxユーザは、単一のユーザなのか、複数のユーザなのかは不明
一般的なRustプログラム開発環境との違い
一般的なRustプログラムの開発ではrustup
をデフォルト設定で使うことで、いまログインしているLinuxユーザ専用の場所にツールチェインをインストールします。またユーザプログラムのビルドにはCargoを使い、そのプログラムが依存しているクレートのソースファイルを自動的にダウンロードし、ユーザプログラムと共にコンパイルします。
ジャッジサーバではこの方法はあまりうまくいきません。
- Linuxユーザを複数使用している場合に、ユーザごとにツールチェインをインストールすることになる。
- ツールチェインのファイルパーミッションがそのままだと、悪意のあるユーザプログラムがツールチェインを改変してしまうかもしれない。
- クレートのソースファイルのダウンロードをCargoに任せると
- ダウンロードに時間と回線負荷がかかる。
- crates.ioの障害の際や依存クレートのいずれかが取り消された際などにダウンロードできずエラーとなる可能性がある。
- 不正防止のためにユーザプログラムの実行時にネットワークを制限する場合、ビルドと実行の間でネットワークから切断する処理を挿入する必要がある。
- ジャッジの際に毎回クレートがコンパイルされることになり非効率。
- マシンスペックにもよりますが、手元の環境 (i5-8250U) では CPU全コア100%で並列実行して分単位の時間 がかかります。
そこで本章では以下のようにします。
- Rustツールチェインを、ジャッジサーバ上の全Linuxユーザがread-onlyでアクセスできる場所にインストールする。
- 選手が使用できるクレートを事前に選定し、それらを全て設定したプロジェクトをコンパイルしておく。
- コンパイルしたプロジェクトはジャッジサーバ上の全Linuxユーザがread-onlyでアクセスできる場所に保存する。
- ユーザプログラムをコンパイルする際は以下のいずれかの方法をとる。
- 事前にコンパイルしたCargoプロジェクトをコピーした上で
main.rs
をユーザプログラムに置き換えてCargoを用いる。 - 所定のオプションを簡単なツールにより生成して
rustc
を直接実行する。
- 事前にコンパイルしたCargoプロジェクトをコピーした上で
最後のユーザプログラムのコンパイルについて二つの方法1がありますが、それぞれについて補足します。
これらはお互いに「他方の短所を解決するかわりに他方の長所を持たない」という関係があります。
Cargoを用いる場合
通常のRustプロジェクトと同様ですが、通常なら意識する必要のない「インターネットからの依存クレートの自動ダウンロード」と「依存クレートの自動再コンパイル」を避ける必要があります。自動ダウンロードを避けるには--offline
オプションをつける必要があります2。依存クレートの自動再コンパイルを正式に抑制する方法はありません。ただし、ツールチェインもクレートもバージョンアップしない固定された環境ではまず起きないだろうとは思われます3。
これはRust 1.36から導入された、ネット接続環境のない場合でもローカルキャッシュを活用してできるだけコンパイル不能になることを避けるオプションです。詳しくはこちらもご参照ください。
これはCargoのソースを見たわけ ではなく 、多少試したところは大丈夫そうだという推測でしかありませんし、仮に今は大丈夫でも将来的に変更が加わる可能性もあります。
考えられる長所と短所を述べますが、要約すると、長所は公式であることです。逆に短所はディレクトリ構成が縛られること、想定されていない環境下でも確実に動作するのか分からないことです。
長所
- 公式のツールなので他の準備が不要。
- 公式のツールなのでバグにも見舞われにくく、処理の信頼性が高い。
短所
- 全部入りのCargoプロジェクト全体をコピーして実行環境に展開する必要がある。
- 所定のディレクトリ構成にしなければならないためです。
Cargo.toml
があり、src
ディレクトリがあり、...。
target
ディレクトリ以下に生成されている依存クレートのコンパイルキャッシュを利用するためです。- Cargoを動作させる だけ であれば
Cargo.toml
をコピーしてsrc
にmain.rs
を入れるだけOKです。問題なくコンパイル・実行できます。ただしそれでは依存クレートを含めてゼロからコンパイルしますので、マシンパワーにもよりますが通常 並列処理の高負荷と分単位の時間 がかかります。従って既に一度ビルドされたプロジェクトを丸ごとコピーしてくる必要があります。 - (参考) この
target
ディレクトリのサイズは手元環境では163MBでした。コンパイルの度にこれだけのディレクトリをコピーする必要があります。
- Cargoを動作させる だけ であれば
- 実行環境の改竄から守るためです。
- もしCargoプロジェクトを使い回すことにすると、例えば次のようなことが可能になります。
- 一回目の実行で、ライブラリを差し替える、または、必要なファイルに偽装して計算結果を保存し、二回目の実行で不正な動作をさせる。
.cargo/config
ファイルに設定を追加する(例:コンパイルオプションを変更するなど)。
- プロジェクトに属するファイル・ディレクトリを読取専用にすれば緩和されると思いますが、この場合
cargo
自体が処理の途中に何らかの形で書き込みを行う(ログやキャッシュなど)ことがあれば正常に動作しない可能性がゼロではありません。
- もしCargoプロジェクトを使い回すことにすると、例えば次のようなことが可能になります。
- 所定のディレクトリ構成にしなければならないためです。
- 依然としていつ依存クレートの再コンパイルが行なわれるかについて保証を得られない。
- 再コンパイルが起こると通常 並列処理の高負荷と分単位の時間 がかかります。
rustcを直接実行する場合
Cargoは高機能ですが、実際のコンパイル処理は適切なオプションをつけてrustc
を呼び出しているにすぎません。Cargoと同じコンパイルオプションを生成することができればCargoに頼らなくても実行ファイルを生成できます。要するに、現在のRustと同様に単体ファイルをrustc
でコンパイルする形ですが、外部クレートのパスなどを指定するコンパイルオプションを追加するということです4。
イメージとしてはgcc
等でいう-lsome_library
のようなものになります。
考えられる長所と短所を述べますが、要約すると、長所はCargoを用いる場合の短所が解決されていることです。逆に短所は公式ではないことで、Cargoを使っていれば起きないような齟齬が起きる可能性があることです。
長所
- 具体的に
rustc
を呼び出すため、実際に為される処理が明確にコンパイルとリンクのみであり、Cargoのようにブラックボックスではない。- 依存クレートの自動ダウンロードも自動再コンパイルも
rustc
の機能ではないので大丈夫です。
- 依存クレートの自動ダウンロードも自動再コンパイルも
- ディレクトリ構成を気にする必要はない。
- 今まで通り、適当なディレクトリにユーザプログラムを好きな名前で配置するだけでOKです。
- 全部入りのCargoプロジェクトをコピーする必要はない。
- read-onlyのシステム領域に依存関係のコンパイル済みクレートを用意しておけば、参照する依存クレートのパスとしてその場所を指定するだけで事足ります。
- 依存クレートを読み取り専用にすることができる。
- Cargoの場合は再コンパイルにより依存クレートのファイルを更新する可能性があるためread-onlyとしたときにどう動作するか確信できませんが、
rustc
を直接実行する場合は依存クレートのファイルを参照するだけですので、read-onlyでも問題ないと思われます。
- Cargoの場合は再コンパイルにより依存クレートのファイルを更新する可能性があるためread-onlyとしたときにどう動作するか確信できませんが、
短所
- コンパイルオプションを準備する必要がある。
- 手でやる場合は
cargo build --release -v
の出力を参考にして必要なオプションを選ぶ必要があります。 - コンパイルオプションを生成するためのツールを作成しましたので、それを利用することもできます。このツールを利用する場合はそのツールのコンパイルと実行が必要です5。
- 手でやる場合は
- オプションが正しいかどうか、欠けていないかどうかを気にする必要がある。
- 普通は
cargo build
で全て終わるためrustc
を直接実行することはなく、どのクレートにも手動でリンクする場合のオプションに関する情報はありません。 - ただし一度テストして問題がなかったのなら、固定されている環境で後々問題を起こすことは考えづらいです。
- 普通は
とはいえツールはRustで書かれているためgit clone
とcargo build
程度です。