はじめに
本書はAtCoderのコンテストにRustで参加するためのガイドブックです。
AtCoderとは?
AtCoderは、オンラインで参加できるプログラミングコンテスト(競技プログラミング)のサイトです。リアルタイムのコンテストで競い合ったり、約3000問のコンテストの過去問にいつでも挑戦することが出来ます。 (AtCoderのトップページより引用)
Note: 競技プログラミングはプログラミングで解決できるような問題をなるべく早く正確に解く競技です。競技プログラミングについてよく知らないが興味があるという方は、インターネット上に初心者向けの詳しい情報がたくさんありますので検索してみてください。AtCoderで開催されているものでは、大きく分けて二種類あります。
- 与えられる問題に対して、その解を出力するようなプログラムを書く競技 (アルゴリズム系)
- 定期開催のもの: AtCoder Beginner Contest (ABC), AtCoder Regular Contest (ARC), AtCoder Grand Contest (AGC) があり、難易度は通常 ABC < ARC < AGC です。
- 定期開催の他、企業によって開かれるコンテストもあります。そういったコンテストで上位成績をとると、その企業への就職、インターン、アルバイトなどで多少優遇されることがあります (コンテストによります) 。
- 例: 「整数
N
が与えられます。N
以下の正整数から等確率に1つを選ぶとき、それが奇数になる確率を求めなさい」 (AtCoder Beginner Contest 142 A問題)- 例: 「
N
人の身長が与えられます。K
cm以上の人の人数を出力してください」 (AtCoder Beginner Contest 142 B問題)- 与えられる問題に対して、少しでも良い解を出力するようなプログラムを書く競技 (マラソン系)
- 定期開催のものはまだありません。企業が自社の取り組みやそこでの課題をテーマに出題することが多いようです。
- 例: 「ある観測データが与えられるので、可能な限り圧縮するプログラムとそれを解凍するプログラムを書いてください」 (Wethernews Programming Competition)
なぜRustなのか?
AtCoderで使える言語は非常にたくさんあります。どの言語を使ってもよいですし、問題によって使い分けても構いません。その中でなぜRustを使うのか、そのメリットとデメリットをまとめてみました。できるだけ一般論で比較するよう心がけますが、競技プログラミングにおけるC++人口がそれなりに多いことと、Rustはその特性上C++と比較されることが多いので、具体的にC++との比較になっている部分も多くあります。
メリット
高速である
AtCoder含め、競技プログラミングでは「実行時間制限」とよばれるものがあります。この時間内にプログラムの実行が終わらないと「TLE (Time Limit Exceeded)」という判定が付いて誤答扱いとなります。多くの場合は想定されている解法であれば多少の余裕をもって解けるように設定されていますが、非常にたくさんの言語が使える都合上全ての言語で公平になるようにはできません。遅い言語に合わせて設定すると速い言語では強引な解法でゴリ押しできてしまうことがありますし、速い言語に合わせると遅い言語では想定されている解法でも通せないということになります。いずれにせよ、基本的には速い言語であるほうが計算時間的には有利です。 (もちろん遅い言語と言われるものにも、例えば書き易さであったり、ライブラリが充実していたり、なにかしらのメリットがあるはずです。どちらかが絶対的に有利ということではありません。)
Rustは最速と言われるC/C++並みに速いとされていますので、(少なくともAtCoderでは) 速度面で不利になることはないと言えるでしょう。
信頼性が高い
信頼性は、ここではRustの公式トップページに倣いメモリ安全性、スレッド安全性、バグの起こしにくさであるとします。競技プログラミングで特に大事になってくるのはメモリ安全性とバグの起こしにくさです。
Reliability Rust’s rich type system and ownership model guarantee memory-safety and thread-safety — and enable you to eliminate many classes of bugs at compile-time.
これは様々なプログラミング言語が様々なアイデアで対処している部分です。たとえばメモリの確保と解放を正しく行うために、C++であればスマートポインタやコンテナを用意したり、他の言語ではガベージコレクタという実行時の機構を用意したりしています。RustでもC++のスマートポインタやコンテナと同様なものを用意し、自分でメモリの確保と解放を行わなくてよいようにできています。ガベージコレクタを利用すると確かに安全でメモリ管理に関してほとんど何も考えなくてよいものの、そのためにほとんどのデータをヒープに置いて参照経由で扱わなければならず、実行速度にも多少影響します。かといってC++のスマートポインタやコンテナは間違った使い方が簡単にできてしまいます。特にイテレータはコレクションに対する操作を行うための標準的なツールにもかかわらず実質的に単なるポインタと同様なので、しばしば無効なイテレータが発生します。例えば (少々意図的な例ですが) 次のように簡単に問題を起こせてしまいます。
#include <iostream>
#include <vector>
#include <string>
using namespace std::string_literals;
int main() {
std::vector<std::string> v = {"hello"s, "world"s};
v.shrink_to_fit();
for (auto const &i: v) {
if (i == "hello") v.push_back("c++"s); // イテレータを無効化してしまう
// 未定義動作なので、プログラム全体が何を起こすか分からない
std::cout << i << std::endl;
}
}
このようなことはRustではコンパイルエラーとしてコンパイル時に検出されます。
let mut v = vec!["hello", "world"];
for &i in &v {
if i == "hello" {
v.push("rust");
// E0502: cannot borrow `v` as mutable because it is also borrowed as immutable
// 4 | for &i in &v {
// | --
// | immutable borrow occurs here
// | immutable borrow later used here
// 5 | if i == "hello" {
// 6 | v.push("rust");
// | ^^^^^^^^^^^^^^ mutable borrow occurs here
}
println!("{}", i);
}
また、複雑なアルゴリズムにバグはつきものです。例えば、添字計算をしていてちょっとした書き間違いで配列のサイズを超えたところを参照してしまったといったことは度々起こりえます。こういうとき、例えばC/C++では配列外参照をしてしまったプログラムがどのように動作するかの保証がなく、segmentation fault
とだけ表示されて異常終了したり、たまたまうまくいってしまったり、手元で正解するケースがサーバーでは誤答となったりし得ます。こういったよく分からない動作が起きてしまうとバグの原因特定が難しくなったり無関係なところを原因と思い込んだりしてしまい、デバッグにかなりの時間を費やしてしまうこともしばしばあります。この配列外参照の例ではRustは必ずエラーを起こしますし、どこで起こしたかも表示してくれます。それが自分のコードでない場合でも (-g
オプション付きでビルドされたかCargoでデバッグビルドされたバイナリであれば) バックトレースを表示させることで呼出元となる自分のコードを特定できます。
#![allow(unused)] fn main() { let x = vec![1, 2, 3]; let y = 3; let _z = x[y]; // 配列外参照! }
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 3', ...
stack backtrace:
(...中略...)
16: alloc::vec::{{impl}}::index<i32,usize>
at ...
17: test::main
at .\test.rs:4
(...中略...)
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
多くの問題をコンパイル時に発見できる
RustはC/C++並みの速度を確保するため、実行時にやらなければいけないことをなるべく減らす方針の言語です。たとえば先ほど触れたように、多くの言語にあるガベージコレクタがありません。それだけならばC++と変わりありませんが、速度と安全性を両立させるためにRustではできるだけ多くのことをコンパイル時に確認する仕組みになっています。C++が受け入れてしまうような危険なコードもコンパイルエラーにします。
それ以外にも、例えば整数型同士の暗黙の型変換がない (例えu8
からu32
であっても) こともある種のミスを防ぐのに役立ちます。たとえばC++で総和をとるためにaccumulate()
関数を利用する際、気をつけなければオーバーフローしないはずのところでオーバーフローを起こしてしまいます (もし符号付き整数型であったなら未定義動作にもなってしまいます) 。
#include <iostream>
#include <limits>
#include <numeric>
#include <vector>
int main() {
// long long で表せる最大値を`large`とする。
long long large = std::numeric_limits<long long>::max();
std::vector<long long> s = {large};
// sには`large`しか入っていないので、総和は普通に`large`になるはず。ところが
// 総和の型をint型だと思って計算してしまいオーバーフローを起こしてしまう。特
// に何のエラーもなし。
std::cout << std::accumulate(s.begin(), s.end(), 0) << std::endl;
// 正しくは以下の通り。
std::cout << std::accumulate(s.begin(), s.end(), 0ll) << std::endl;
}
Rustではこのようなことは最初から型の不一致によりコンパイルエラーとなります。
let s = vec![std::i64::MAX];
let x: i32 = s.into_iter().sum();
// E0277: the trait bound `i32: std::iter::Sum<i64>` is not satisfied
// 3 | let x: i32 = s.into_iter().sum();
// | ^^^ the trait `std::iter::Sum<i64>` is not implemented for `i32`
ジェネリクスとトレイトの仕組みも強力です。たとえばジェネリックな関数が型変数T
をもつとき、このT
のとりうる型を特定のトレイト (=機能一覧) を実装しているものだけに制限することができます。逆にT
に対してできることはその特定のトレイトが定める機能のみです。従って、一度コンパイルが通った関数はその制約を満たす限りのどのようなT
を与えても関数の内部でコンパイルエラーとなることはありません。特にライブラリを整備するにあたってはこれはとてもありがたいことです。実際に使ってみなくても、コンパイルさえ通れば、将来的に作られうるどんなユーザー定義型を与えようともその関数が正しく呼び出せることが保証されます。C++のテンプレートなどでは実際に具体的な型を与えて始めて様々な検証をするので、使う段階になってからでないとエラーが発見できません。このことは、後述するコンパイルエラーの分かりやすさにも繋がっています。
コンパイルエラーが分かりやすい
これは少々主観的な話になるのかもしれませんが、Rustのコンパイルエラーは読みやすく分かりやすいという評判があります。実際にコンパイルエラーが発生したとき、まずエラーが起きた場所はもちろんとして、エラーが関連する他の場所 (例えば以前に借用された場所など) などをアスキーアート的な手法で視覚的に分かりやすく表示してくれます。さらに、なぜそれが間違っているのか/それをどのように修正することができるかのヒントが提示されることもあります。例えば、先ほどのエラー全体は次のようになっていました。
error[E0277]: the trait bound `i32: std::iter::Sum<i64>` is not satisfied
--> (filename):3:32
|
3 | let x: i32 = s.into_iter().sum();
| ^^^ the trait `std::iter::Sum<i64>` is not implemented for `i32`
|
= help: the following implementations were found:
<i32 as std::iter::Sum<&'a i32>>
<i32 as std::iter::Sum>
error: aborting due to previous error
For more information about this error, try `rustc --explain E0277`.
先ほど少し触れたように、トレイト/ジェネリクスの仕組みによりエラーの所在が明確化されています。関数が呼び出せないのは引数に関数が設定した制約を満たさないものを与えているからだと分かります。動的型付け言語やC++のテンプレートでは、仕組み上実際に実行または実体化してみなければエラーの存在が分かりませんので、与えられた引数が関数の求める条件を満たしていなかったのか関数の方に根本的な書き間違いがあるのかがインタプリタ/コンパイラには分かりません。必然的に実際にエラーを起こしたライブラリの内部の実装部分を指し示しながら、ここで呼び出されていて、ここで呼び出されていて、...と辿っていくしかなく、しかもそのうちのどれが悪いのかが分からないので関係がありそうなところを全て表示していくしかありません。
一部のエラーには詳細な説明が用意されており、rustc --explain (エラーコード)
とすることにより、そのエラーが何を言っているのか/どういうコードでそのエラーが発生するのかなどをもりこんだ詳細な解説を読むこともできます。たとえば上のエラーのE0277
にも用意されています。
$ rustc --explain E0277
You tried to use a type which doesn't implement some trait in a place which
expected that trait. Erroneous code example:
```
// here we declare the Foo trait with a bar method
trait Foo {
fn bar(&self);
}
// we now declare a function which takes an object implementing the Foo trait
fn some_func<T: Foo>(foo: T) {
foo.bar();
}
fn main() {
// we now call the method with the i32 type, which doesn't implement
// the Foo trait
some_func(5i32); // error: the trait bound `i32 : Foo` is not satisfied
}
```
(以下略)
抽象化のための機能を数多く備えている
例えば次のようなさまざまな機能があります。Rustは後発の言語ですので、他のプログラミング言語に備わっている優れた機能も参考にして多数の機能が導入されています。
-
強力な型推論
型推論アルゴリズムとしては非常に有名なHindley/Milnerのアルゴリズムをベースにした賢い型推論器を持ちます。言語設計的に型推論を制限している箇所 (例: 関数の引数) や、型推論だけでは決定できず型を明示する必要があることもあります (例:
iter.collect::<Result<_, _>>()?
) 。それでもほとんどのケースでは推論され、型の明示は最低限で済みます。 -
Rustの列挙型 (
enum
)は各バリアントに値を持てます。いわゆる直和型や代数的データ型と呼ばれるものです。タグ付き共用体 (tagged union) と呼ばれることもあります。例えば一方向連結リストの構造は次のように表せます。#![allow(unused)] fn main() { enum List<T> { /// 先頭要素とそれ以降の要素によるリストを持つ。 Cons(T, Box<List<T>>), /// リストが空であることを示す。 Nil, } }
-
JavaやC#のインターフェイスと似ていますが、より柔軟に利用することができます。Rustではいわゆる「オブジェクト指向言語」が持つ継承の仕組みをもたない代わりに強力な列挙型とトレイトの仕組みを活用します。例えば単純な継承関係であれば次のような方法で置き換えられます。多くの場合、1.と2.を用いれば十分対応できることでしょう (参考) 。
-
実装クラスが数個かつパラメータも多くはない
列挙型 (直和型)を使います。 むしろこのような場合は最初から継承よりも直和型が適切である場合が多いです。 実際、KotlinやScalaではJVM上で抽象クラスとそのサブクラスを直和型のように扱うためにsealed classという機能を備えています。
-
実装の共有
トレイトのデフォルト実装を使います。 フィールドの共通化は、代わりにgetterメソッドをトレイトに記述しておきます。 むしろこれにより各フィールドやメソッドの役割が明確になり、無意味に複雑なコードが生まれにくくなります。
ただし、getterを書く際に「1つのオブジェクトに対する可変参照は高々1つしか存在できない」という原則が障害となることがごく稀にあります。
-
特定のメソッドだけ必要
trait object (
Box<_>
)でdynamic dispatchを行ないます。
なお、
Deref
とDerefMut
トレイトを実装すると、メソッド呼出時に自動的に対象の型への型変換が行われ、簡単に対象の型のメソッドを呼び出すことができるようになります。これを継承における「親クラスからのメソッドの受け継ぎ」に転用することは可能ですが、これはアンチパターンとされることも多いです (参考) 。その他また、Rustのトレイトは型クラスのように考えることもできます (参考) 。
-
-
パターンマッチングは、端的に言うならばタプルや構造体・列挙型などを (構造に合わせて分岐しながら) 分解し、中身を取り出す機能です。特に先に触れた列挙型を扱うにあたっては、パターンマッチは非常に強力な機能です。最近でも少しずつ改善が行なわれており、 Rust 1.26でslice patternが追加されたり1.39で
if
の制限の取り払われたりしました。これらの改善もあり、特にML系言語に馴染みのある方は近い感覚で扱えるのではないでしょうか。 -
衛生的とは識別子が衝突する心配がないことです。マクロ内で宣言されたローカル変数がマクロ外から参照できてしまうことも、その逆にマクロ内で参照する変数がたまたま展開先のローカル変数を参照してしまうこともありません。また、マクロの各引数が何を受け取るのかを指定することができるため、例えば式を受け取ると指定すればその引数はひとまとまりの式として扱われます。この式の値を利用するときに、前後の関係で式としての解釈が壊れることもありません (例えば
$x = 1 + 2
のとき2 * $x
は2 * 1 + 2
ではなく2 * (1 + 2)
に相当します) 。このように、Rustのマクロは積極的に利用しても比較的安全です。特にRustでは多くなりがちなボイラープレートを短くまとめるなど役に立つ機会も多いでしょう。またマクロの入力はパターンマッチで行われるため受け付ける文法が比較的自由であり、この後に述べるRustのデメリットのうち特に冗長性に関わることは可読性を保ったままマクロで解決できることも多いです。
-
block expression
block expression とはブロックを式として扱える仕組みのことです。ブロックの式としての値はそのブロックの末尾にある式の値になります。これがあると、例えば別の変数の初期化にしか使わない一時変数のスコープを絞ることができます。地味ではありますが、あると便利に感じる機能です。
-
一般的にシャドーイングはすべきでないとされる傾向がありますが、Rustではむしろシャドーイングが推奨されています。 これにより、たとえば処理の途中で一度変数に格納するときにもわざわざprefixやsuffixが付いた別の変数を宣言しなくて良くなりますし、あるいは再束縛することで不要になった
mut
を消すこともできます。#![allow(unused)] fn main() { // この配列を数値にしてかつソートしたい let list = vec!["2", "4", "1", "5", "3"]; let list = list.into_iter().map(|x| x.parse().unwrap()); // ソートしたいので mut とする let mut list: Vec<i32> = list.collect(); list.sort(); let list = list; // ソートしたので mut は不要、消す // list.push(4); // => 既に mut ではないので変更できない assert_eq!(list, [1, 2, 3, 4, 5]); }
一般にシャドーイングするべきではないとされる理由は様々にありますが、一番はやはり混同しやすくなるからと考えられます。その点Rustは強い静的型付け言語ですので、仮に混同したとしてもどこかのタイミングで型エラーになるという期待ができます。
また、変数の個数を抑止するという効果もあります。block expression等と適宜組み合わせることで変数の数やスコープはさらに小さく保つことができ、多少関数の実装が長くなっても見通しが悪くなりにくいと言えます。競技プログラミングではmain関数が長くなりがちなので一層嬉しいのではないでしょうか。
ゼロコスト抽象化を追求している
Rustの言語デザインやライブラリは、一定の使いやすさを実現しつつも、使いやすさのために実行時の高速性を犠牲にはしないというゼロコスト抽象化 (zero-cost abstraction)を追求しています。
他の言語では「イテレータではなくfor文で書いた方が速い」、「async/awaitを使うと遅い」といったことは珍しくありません。これはその言語の選択で、動作が遅くなるとしてもコードを簡潔に簡単に書けることを優先したと言えます。一方でRustでは抽象化によってパフォーマンスが犠牲になるのなら、その抽象化はできるだけ採用しません。最適化の余地 (自由度) を残すためには関数群は基礎的なものにとどめる必要があり、特定の機能を実現するために冗長なコードを書く必要があります。結果的にコードは長く面倒になり手間もかかりますが、そうしてでもパフォーマンスをとることができるように設計されています。
このことは「簡潔な構文や関数によってその機能が必要とするコストを隠してしまう」ことを避けているとも言えます。つまり、本当にパフォーマンスが必要なときに最適化を検討するべき「コスト」の部分が明確化されているということでもあります。
デメリット
現れる概念が比較的難しい
先に見たように、Rustでは、いままで他の言語ではコンパイラが検証していなかったようなことをコンパイル時に検証します。そのためにRustでは所有権や借用をはじめとする独特の概念が導入されており、それらの概念の理解そのものが難しいとされることも多いようです。これらの概念が課す多数のルールがなぜ存在するのかを理解することは、仕組みをある程度理解していなければ難しいものです。
たとえば要素への参照をもったままVec
本体の可変参照をとることはできませんが、これは可変参照と共有参照は同時に存在できないというルールからです。ではなぜこのようなルールがあるのでしょうか。たとえばVec
に要素を追加するとキャパシティが足りないときにメモリの再確保とデータの移動が行われるので、無効な参照が生み出せてしまうからです。しかしこういった事情を知らないと、単にRustがよく分からない制約を課してくるだけの書きにくい言語だと感じてしまうかもしれません。一方でこの難しさを隠せている言語もあります。たとえばJavaやC#などオブジェクトを参照で扱うような言語では、要素への参照を得ても単にそのオブジェクトへの参照が一つ増えるだけで、可変長配列の領域そのものへの参照を得るわけではないということもあるかもしれません。そのような言語では、要素そのものは可変長配列が管理するメモリ上にあるわけではないので、要素への参照をもったまま可変長配列を伸ばしても問題は起こりません。そのかわり、おそらく別の部分の犠牲 (アクセスに必ず参照を介することのコストであったり、参照型と値型の挙動の違いによる難しさであったり) があります。
Rustが課すルールにも理由がありますので、そういった事情について意識的に考えることは他の言語や競技プログラミング以外の文脈でも活きる有意義なものではあると思います。単にAtCoderである程度の競技プログラミングをするだけであれば、C#やJavaといった言語でもほぼ正解できるよう調整されているようなので、どちらを取るかは好みといっていいかもしれません。
素早く書くことにはあまり向かない仕様
Rustの安全指向や標準ライブラリの設計方針などは、時間をかけて大規模なプログラムを書くときや堅牢なプログラムを書くときには非常に役に立ちます。一方で競技プログラミングでは、一般のプログラミングと異なり、次のような特徴があります。
- 早くプログラムを完成させることが重要
- 入力のフォーマットや扱う値の範囲・個数などが定まっている
- スレッドを起動してやりとりするようなことは通常ない
- 後日提出したプログラムを見直したり保守することは通常ない (ライブラリを除く)
したがって、Rustの様々な設計は、必要以上に煩雑に感じることがあります。例えば、Rustでの競技プログラミングを始めようとした方で、標準入力をとるのがとても面倒で挫折し (かけ) た、という方も度々見かけます。例えば、空白区切り二つの整数を読み取ってその和を計算するプログラムは、工夫をしなければ次のようになります。
fn main() { let (a, b) = { let mut s = String::new(); std::io::stdin().read_line(&mut s).unwrap(); let mut iter = s.split_whitespace().map(|i| i.parse::<i32>().unwrap()); (iter.next().unwrap(), iter.next().unwrap()) }; println!("{}", a + b); }
「二つの空白区切りの数字を読むだけでこれほど多くのコードが必要なのか」と思われるかもしれませんが、こうなっているのには次のような設計があります。
- 空白区切りの入力を任意の型の値として読み込むことができない。
std
には整数を読む機能などはなく、かならず一行単位 (read_line()
) または全体 (read_to_string()
) を文字列として読んでから処理する必要があります。なおread_to_string()
を使うときは、use std::io::Read;
が必要であることと、手元でテストするときにEOF (Ctrl + D
(macOSやLinux) またはCtrl + Z
(Windows)) を入力するまで入力が終了しないこと、に注意が必要です。 - 入力はバッファをとって、そこに書き込む。
read_line(&mut s)
の部分にあたります。入力をString
に入れて返す関数よりも、バッファを受けとって書き込む方式の方が、必要に応じてバッファを事前にアロケートしておける分、パフォーマンス的には柔軟なのです。とはいえ競技プログラミングでは高々O(log n)回のリアロケーション (この挙動はドキュメント化されていないため変わる可能性もなくはありませんが) にかかるコストを気にする必要はないと思われます。 - イテレータを上手に扱う必要がある。
受け取った文字列を空白区切りにするためには
split_whitespace()
という関数を使いますが、これはイテレータを返します。その各要素を整数に変換するためにmap()
とparse()
関数を使いますが、変換先の型を指定するためにparse::<i32>()
などの書き方を使う必要があります。itertools
を使えばイテレータを直接分解するような書き方ができますが 、std
に限れば一つずつnext()
で要素を取り出すしかありません。 std
を含む各ライブラリは大抵明示的なエラーハンドリングを要求する。- 何かの理由で標準入力が読み込めない状態になるかもしれません。整数ではない入力を整数にしようとするかもしれません。イテレータの要素が足りないかもしれません。こういったものをRustでは
Option
やResult
で表現します。?
を使えば視認性を損ねることはない (むしろコードを俯瞰するときのの助けになる) ですが競技プログラミングではunwrap()
を使うことになるでしょう。これはエラーが起きたならパニック (i.e.RE
) するという乱暴なものですが、入力の形式が決まっている以上エラーになるのは読み間違えたか書き間違えたときのみでしょう。 - もちろん逆に、明示的なエラーハンドリングは要求しないかわりに失敗時は内部でパニックするような関数もあります。
例えば
Index
で境界外アクセスしたときやメモリが足りずにアロケーションに失敗したとき、println!
が失敗したときにパニックします。 このように「失敗するとパニックする」ものはその条件をドキュメントに# Panics
という形で書いています。
- 何かの理由で標準入力が読み込めない状態になるかもしれません。整数ではない入力を整数にしようとするかもしれません。イテレータの要素が足りないかもしれません。こういったものをRustでは
Note: 入力をとる方法については、2020年言語アップデートで外部クレートとして
proconio
やtext_io
,whiteread
などのクレートが導入されたため、かなり改善されました。リンク先はそれぞれのクレートのドキュメントになっていますので、詳しくはそちらをご覧ください。
他にも、先程少し触れましたが例えば数値型の四則演算や比較を行なうときには基本的に両辺の型が等しくなければいけません。
i32
と&'_ i32
を両辺に持って来ることくらいは許されていますがi32
とi64
をそのまま足したり比較したりはできません。
片方を明示的に変換する必要があります。
これは型システムの都合等ではなく数値型に対しては意図的に制限されています。
さらにスライスのインデックスはusize
及びusize
の範囲でなくてはなりません。
isize
では駄目です。
競技プログラミングでは、非負整数として入力される値であっても、計算途中では符号付き整数の方が扱いやすいので符号付き整数として扱うことが多々あります。その場合arr[(添字の計算式) as usize]
のようにusize
に戻す必要があります。
実際AtCoderの提出のうち、"as usize"
という部分文字列を持つRustのコードは結構な数が存在します。
地味なところですが、動的計画法など添字を多用するところでは面倒に感じるかもしれません。
一般的には危ないのでコンパイラが許してくれないものの、使い方の制限に照らせば問題ない動作をしたいということもありえます。たとえばこちらでは二次元配列 (Vec<Vec<T>>
) における要素の交換を実装しようとしていますが、同じ要素に対する二つの可変参照を持てないというルールによって普通に実装することができません。競技中に「あ、これがやりたい」と思ったことが必ずしもスムーズに実装できない場合がありえます。
Rustで参加する競技プログラマーの中には、こうした煩雑さを改善するためのマクロやヘルパ関数 (もっと便利に標準入力がとれるようにするなど) を定義し、テンプレート (ひな型) として用意している方もいます。インターネット上で公開されている方もいらっしゃいますし、過去のコンテストでの上位Rust参加者の提出などをのぞいてみると、いろいろと参考になるかもしれません。
標準ライブラリが小さい
Rustは比較的新しい言語ですので、インターネット接続環境を前提にしたパッケージ管理システムCargoを標準で持ちます。このため、言語の成長とクレートやRustエコシステムの成長を分離することを目的に、Rustは標準ライブラリを最低限の抽象化とインターフェースとして位置付け、できるだけ小さく保ち続けてきました。かつて標準ライブラリの一部だったり本体にバンドルされていたライブラリ (num
, rand
, regex
など) を積極的に分離することさえしています。ユーザーはCargoを使えば、使いたいパッケージをcrates.ioからいつでも自由にダウンロードできます。
しかし、逆に言えば標準ライブラリだけでは使える機能が非常に制限されるということにもなります。そのため、2020年言語アップデートでいくつかの著名な外部クレートが導入されて利用できるようになりました。2020年4月6日現在、利用可能なクレートの一覧とそれぞれについての簡単な説明がこちらにまとめられています。
コンパイル時間が長くなりがち
様々な解析をコンパイル時に行う都合上、コンパイル時間が長めにかかる傾向があります。Rustではコンパイル速度を速くすることはあまり重要視されていません。特に手元で提出をテストする際、外部クレートを利用するならその外部クレートのビルドも実行することになります。二回目以降のビルドではビルドキャッシュを利用するためコンパイルする必要はありませんが、初回の実行では利用する外部クレートによっては数分単位の時間をとられる可能性があります。つまり、素早く書き上げたコードを手元で軽く実行してみることにすら時間をとられてしまい、提出時刻が数分遅れてしまうということがあり得ます。結果的に手元でコンパイルが通るかどうかをチェックする時間すら惜しいとなってしまうと本末転倒です。なお、外部ライブラリを含むパッケージをコンテスト開始前に一回ビルドしておいて、競技時はそのフォルダをコピーして編集するというふうにすれば回避できます。
AtCoderの環境について
現在 AtCoder で利用できる最新の環境は 2020 年言語アップデートによって更新された環境です。
- Rust 1.42.0
- 外部クレートあり
コンパイルオプションや利用できる外部クレートの一覧と簡単な解説やコード例など、この環境についてのより詳しい情報は 2020 Update のページにまとまっています。ご一読ください。
開発環境の準備
TODO このページは書きかけです。
この章では、手元のパソコンに Rust の開発環境と、テキストエディタとして Visual Studio Code をインストールする方法について説明します。
環境を手元に用意するメリットは普段使い慣れたエディタや環境でコーディングできるようになることです。開発環境を用意して Language Server Protocol に対応するエディタ (Visual Studio Code など) を利用すれば、入力補完やリアルタイムのエラーチェックなど様々な機能の恩恵を受けることができます。逆に言うと、自分の使い慣れたエディタや環境を使いたいと思わずむしろ環境を整える方が面倒だと感じられる方は AtCoder のコードテスト (TODO: リンクが完成次第挿入) を利用するのが最も簡単な方法です。外部ライブラリにまつわる互換性の問題もありません。
既に Rust の開発環境がインストールされており、普段使い慣れたエディタや編集環境もあるという方には前半は全く不要な話となります。cargo-generate
のインストールへお進みください。それもインストールされている方はこの章で説明することはありませんので、実際に参加方法の説明へお進みください。
Rust ツールチェインのインストール
TODO このページは書きかけです。
Note: インストール方法は適宜変わる可能性があります。情報が古いまたは不足している場合は、公式の book にある解説を適宜参照ください (英語) 。
まずは Rust 本体をインストールする必要があります。
ステップ 1: Rust に必要になる依存関係を追加する
OS によっては Rust を実行するのに追加のプログラムが必要になりますので、まずはそれをインストールしましょう。
-
Windows
(TODO: Visual Studio Build Tools をインストール)
-
macOS
(TODO: Cコンパイラ (に付属するリンカー) のインストール)
-
Linux (Windows Subsystem for Linux を含む)
(TODO: Cコンパイラ (に付属するリンカー) のインストール)
ステップ 2: Rustup のインストールスクリプトを実行する
OS によっては標準のパッケージ管理に Rust のパッケージが存在することもありますが、ここでは Rustup という Rust のバージョン管理プログラムを通してインストールすることを前提とします。このプログラムを使うと、最新版へのバージョンアップや特定のバージョンのインストールがコマンド一つで実行できます。また、そのようにしてインストールされた複数のバージョンの Rust を簡単に使い分けることができます。
https://rustup.rs/ にアクセスすると、お使いのプラットフォームに合わせてインストールスクリプトを実行する方法が表示されます。大きくは Windows と macOS/Linux で分かれます。
-
Windows
rustup-init.exe
のような実行ファイルのダウンロードリンクが示されていると思いますので、それをダウンロードして実行します。 -
macOS / Linux (Windows Subsystem for Linux 含む)
curl
を用いてスクリプトをダウンロード・実行する方法が示されていると思います。書かれているコマンドをターミナルへ入力してください。
(TODO: 以下、スクリプトの指示に従ってインストールする方法とインストールを確認する方法を書く)
ステップ 3: インストールされたことを確認する
インストール終了後、 Windows の方はコマンド プロンプト、 macOS または Linux の方は端末を開き、次のように入力してみましょう。次のようにバージョン情報が表示されればインストールは完了です。もしコマンドが見つからない、のようなエラーメッセージが表示される場合は、環境変数がまだ反映されていない可能性があります。一度再起動をして、もう一度試してみてください。
$ rustc --version
rustc 1.42.0 (b8cedc004 2020-03-09)
ソースコードエディタの準備
Rust プログラムのソースコードは単純なテキストファイルです。編集にはテキストエディタというプログラムを使います。単純にテキストを読み書きするという機能に限ればテキストエディタは OS にも付属していますが、よりプログラミングに特化した機能を多く揃えるテキストエディタが無料で簡単に使える時代ですので、ぜひ活用するべきです。それらのエディタが持つ機能には例えば次のようなものがあります。 (一例です)
- コードハイライト
文法構造に合わせてソースコードを色付けして表示する機能です。たとえば型名や関数名などに色がつきます。この機能はコードを視覚的に把握するのに役立ちます。 - 入力補完
変数名や関数名の最初の何文字かを入力するだけでそれから始まる要素を列挙して表示してくれます。目当てのものがあればキー一つで残りの部分を補完してくれます。 - エラーハイライト
入力するのと同時にリアルタイムに内容をチェックし、エラーがあればそれをエディタ上に赤線を引くなどの形で表示してくれます。コンパイラを直接実行する手間もエラーメッセージを読み解く手間もなくエラーに気づくことができるメリットがあります。 - ドキュメント (説明書き) の表示
久しぶりに使う関数の引数の数や順番がわからなくなることはよくあることですが、こういうときにマウスカーソルを関数に合わせるだけでその関数のシグネチャやドキュメントを表示してくれます。 - 定義へのジャンプ
その関数や型が定義されている場所を開いてくれます。構造体の中身を少し忘れてしまったり、関数の処理の中身を確認したりといったときに簡単にソース上を移動できます。
こういった機能は Language Server Protocol という共通の仕組みの上に実装されているので、これをサポートするエディタであればこれらの機能を享受できます。 Language Server Client を実装しているエディタは無数にあり、どれを使うかは完全に好みです。
本説明はできるだけエディタによらないように進めていくつもりですが、エディタに特有の内容が現れるときは Visual Studio Code を想定して説明します。それ以外のエディタを使われる方は適宜読み替えてお読みください。次節で Rust のコーディング支援機能を導入する方法を説明します。
(TODO: Visual Studio Code のインストールと Rust (rls) 拡張機能のインストールを説明する)
Rust Language Server のインストール
Rust Language Server (RLS) は、ソースコード編集中の様々な支援機能を提供するためのプログラムです。例えば、プログラムを編集しながらリアルタイムでエラー箇所をハイライトしたり、コード補完機能を提供したり、定義にジャンプしたり、簡単なドキュメントを表示したりといった様々な機能があります。使い慣れたエディタとともにこうした支援機能が使えることが、ローカルに Rust の環境を作る大きなメリットです。
なお、こうしたコーディング支援機能が不要だと考える方はインストールをする必要はありません。
Note: Visual Studio Code を利用している方は、拡張機能が自動的にインストールしてくれるため、ここで直接インストールする必要はありません。
さて、 RLS は Rust 本体と同時に配布されているため、 RLS のインストールをする前に対応するバージョンの Rust をインストールしなければなりません。現在 AtCoder で使える Rust のバージョンは 1.42.0 ですので、コマンド プロンプトあるいは端末を開いて次のようにコマンドを実行します。
$ rustup install 1.42.0
info: syncing channel updates for '1.42.0-x86_64-pc-windows-msvc'
info: latest update on 2020-03-12, rust version 1.42.0 (b8cedc004 2020-03-09)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'rust-docs'
12.0 MiB / 12.0 MiB (100 %) 5.9 MiB/s in 1s ETA: 0s
info: downloading component 'rust-std'
info: downloading component 'rustc'
35.6 MiB / 35.6 MiB (100 %) 28.2 MiB/s in 1s ETA: 0s
info: downloading component 'rustfmt'
info: installing component 'cargo'
info: installing component 'clippy'
info: installing component 'rust-docs'
12.0 MiB / 12.0 MiB (100 %) 2.4 MiB/s in 4s ETA: 0s
info: installing component 'rust-std'
info: installing component 'rustc'
35.6 MiB / 35.6 MiB (100 %) 10.7 MiB/s in 3s ETA: 0s
info: installing component 'rustfmt'
1.42.0-x86_64-pc-windows-msvc installed - rustc 1.42.0 (b8cedc004 2020-03-09)
info: checking for self-updates
続けて RLS をインストールします。なお必要なコンポーネントは変わるかもしれませんので、適宜 RLS のリポジトリも参照してください。
$ rustup component add --toolchain 1.42.0 rls rust-analysis rust-src
info: downloading component 'rls'
info: installing component 'rls'
info: downloading component 'rust-analysis'
info: installing component 'rust-analysis'
info: downloading component 'rust-src'
info: installing component 'rust-src'
両方の実行が完了すれば、 RLS のインストールは終了です。
cargo-generate
のインストール
AtCoder で利用できる Rust のバージョンやライブラリを指定したプロジェクトを簡単に生成するために、 atcoder-rust-base の ja ブランチにプロジェクトの雛形を用意しています。この雛形はcargo generate
というコマンドを使って展開するため、先にこのコマンドをインストールする必要があります。
cargo generate
サブコマンドを利用できるようにするには cargo-generate
が必要です。端末 (Windows ユーザーの方はコマンド プロンプト) を開いて cargo install cargo-generate
コマンドを実行してください。これだけでインストールは完了です。
$ cargo install cargo-generate
Updating crates.io index
Downloaded cargo-generate v0.5.0
(...中略...)
Compiling cargo-generate v0.5.0
Finished release [optimized] target(s) in 5m 46s
Installing ...
Installed package `cargo-generate v0.5.0` (executable `...`)
実際に cargo generate --help
として実行できるかどうかを確認しましょう。
$ cargo generate --help
cargo-generate 0.5.0
Ashley Williams <ashley666ashley@gmail.com>
cargo, make me a project
USAGE:
cargo generate [FLAGS] [OPTIONS] --git <git>
FLAGS:
-f, --force Enforce to create a new project without case conversion of project name
-h, --help Prints help information
-V, --version Prints version information
-v, --verbose
OPTIONS:
--branch <branch>
--git <git>
-n, --name <name>
コンテストの参加方法
TODO この章は書きかけです。
本章では、手元にAtCoderのジャッジサーバーの環境と同等のプロジェクトを用意してコンテストに参加する方法について説明します。
ここでは、現在AtCoderで提供されている環境と同じ環境を使って手元でもコーディングできるようにするための最低限の汎用的な方法をなるべく標準的な方法で解説しています。実は、より競プロやAtCoderへの参加に特化したツールというものもあり、インターネットを検索するといくつも見つけることができます。これらのツールには、テストケースをAtCoderから自動でダウンロードしてテストしたり、自作ライブラリを自動で埋め込んで提出できる形に整形したり、そのツールから直接提出できたりするようなものも存在します。ここではそういったツールのご紹介はしませんが、競プロに慣れ、本格的に取り組んでみようと考え始めるようになれば一度検討してみるのもよいかもしれません。もちろん既存のツールを使わずに、自分にとって最も参加しやすいスタイルを新しく確立するのも面白いことです。
コンテスト開始前の事前準備
コンテストの開始前にプロジェクトを準備して一度ビルドしておくと、コンテスト開始後の動きがスムーズになります。特に初回ビルド時は依存クレートのコンパイルに時間がかかります。お使いのパソコンのスペックにもよりますが、もしすべてのクレートを有効にした場合は完了まで数分程度かかるとお考えください。
- 作業するためのプロジェクトを作成します。
- 使いそうなクレートを有効化し、一度プロジェクトをビルドしておきます。
- プロジェクトを問題数分だけコピーしておきます。
- 好みに応じて一つのプロジェクトを使い回すのでもかまいません。
プロジェクトを生成する
TODO このページは書きかけです。
まずは AtCoder サーバーと同じ環境のプロジェクトを手元に準備します。
まずは、今から参加するコンテストのソースコードを置くフォルダを準備してください。
(TODO: フォルダを作成してそこに cd する方法を説明する)
では、実際にプロジェクトを生成していきます。
cargo-generate
をインストールしている場合
cargo generate
コマンドをインストールしている場合は、次のようにコマンドを実行してプロジェクト名を入力すれば AtCoder での環境と同様の環境のプロジェクトが生成できます。
$ cargo generate --git https://github.com/rust-lang-ja/atcoder-rust-base --branch ja
Project Name: abc000
Creating project called `abc000`...
Done! New project created /path/to/project/abc000
これで表示されているパスにプロジェクトが生成されました。
Note: ここで生成されるプロジェクトはサンプルとして AtCoder Beginner Contest 086 C - Traveling の解答例とサンプルケースのテスト方法例が書かれてます。テンプレートを自分向けにカスタマイズしたい場合は、リポジトリをフォークするなどして編集し、それを URL に指定して生成させることもできます。
cargo-generate
をインストールしていない場合
まずは空のプロジェクトを作成します。
$ cargo init abc000
Created binary (application) package
そして abc000/Cargo.toml
の [dependencies]
以下に AtCoder で利用できるライブラリを追加してください。 atcoder-rust-base の ja ブランチ に含まれている Cargo.toml の雛形を参考にできます。ただしこれは cargo-generate
用の雛形のためプロジェクト名など一部の項目がプレースホルダになっており、そのまま使えるものではないことに注意が必要です。もし完全な形の Cargo.toml をお探しなら、 ja-all-enabled ブランチ に有効な形の Cargo.toml が含まれていますので、こちらを利用することもできます。
また、 Rustup を利用している場合、プロジェクトディレクトリに rust-toolchain
というファイルを置いて中身に 1.42.0
とだけ書いておくと、 cargo
などのツールを呼び出したときに自動的に Rust 1.42.0 のツールチェインを利用してくれます。もしそのツールチェインがインストールされていなければ自動でダウンロードが始まり、インストールされます。この先しばらくして Rust のバージョンアップが進み、再び AtCoder 側のバージョンとの乖離が大きくなってきたときは、間違って 1.43 以降にしか存在しない機能を利用してしまって Compilation Error を受けないためにも強制的に 1.42.0 を利用させるのが便利です。
利用するクレートの選択とコンパイル
TODO このページは書きかけです。
先にも軽く触れた通り、依存クレートのコンパイルにはそれなりの時間がかかります。コンテストが始まってから最初の問題の提出前に実行するとなるとそれなりのタイムロスになります。そこで次の二つのことをコンテスト開始前に行ってしまい、コンテスト開始後になるべくスムーズに行動できるよう準備しておきましょう。
利用するクレートの選択
これは事前準備時間を短くする、またはビルドディレクトリのディスク容量を減らすことに効果があります。
すべての依存クレートを有効化すると、スペックや環境にもよりますが、初回のコンパイルに数分かかるようになり、ビルドキャッシュも数百 MB (メガバイト) になります。ただし二回目以降のコンパイル時間やコンパイル後のバイナリの実行速度には影響しません。すなわち、競プロとして AtCoder 上で不利になるようなことはありません。したがって、最初にすべてのクレートを有効にしてしまったほうがコンテスト中の自由度は高くなります。このあたりのバランスはご自分のパソコンのリソースと相談して選択してください。
事前コンパイル
利用するクレートの選択を終えたら、次は事前コンパイルを行います。
(TODO: コマンド プロンプトまたは端末を開き、プロジェクトのあるフォルダまで移動する方法を説明する)
端末を開いてプロジェクトのあるフォルダまで移動したら、次のようにビルドを行います。
$ cargo build
Updating crates.io index
Compiling proc-macro2 v0.4.30
Compiling unicode-xid v0.1.0
Compiling syn v0.15.39
Compiling lazy_static v1.3.0
Compiling quote v0.6.12
:
:
Finished dev [unoptimized + debuginfo] target(s) in 20.27s
Finished
の文字が見えたら、依存クレートのコンパイルは完了となります。
Note: ここでは
cargo build
としてデバッグモードでのビルドを行いました。実際の AtCoder では--release
オプションをつけて実行速度を最適化するのですが、これをつけるとさらにビルド時間が伸びてしまうことや、デバッガによるデバッグがしづらくなることなどのデメリットがあります。加えて、手元の環境ですることはサンプルケースレベルの比較的小さいサイズの入力を試す程度であることが多いため、速度を最適化する必要性もさほどないことが多いのではないでしょうか。一方、手元でもリリースビルドでテストすることがあるという方は、この段階でcargo build --release
も加えて実行しておくことをおすすめします。そうでないとコンテスト中にリリースビルド用の依存クレートのコンパイルが行われ、多くの時間を取られてしまいます。
(オプション) Rust Language Server の事前準備
Note: この手順は Rust Language Server (RLS) を利用しない方は必要ありません。
RLS は cargo build
とは別にキャッシュを持っており、初回起動時にキャッシュを作成します。したがって今の状態のプロジェクトは cargo build
については十分高速に実行できますが、プロジェクトを開いてから補完などが機能するまでには時間がかかってしまうということです。したがって、このキャッシュについても今のうちに作っておくと、開いた直後から快適な編集ができます。
Note: 最近は RLS の後継 (RLS 2) として開発されている Rust Analyzer という Language Server もあります。 GitHub のリポジトリによればまだ開発段階とのことですが、以前に比べ機能も揃ってきています。不具合や課題もまだまだあるとはいえ、特にレスポンス面では Rust Language Server を超えている部分も多く見られます。この説明では正式版の RLS を採用しますが、導入方法もだんだんと整備されて簡単になっていますので、興味のある方はぜひお試しください。Visual Studio Code であれば拡張機能を一つインストールするだけです。
実際にキャッシュを作る方法は、単に今作成したプロジェクトを開くだけです。以下では Visual Studio Code の場合に説明しますが、他のエディタを利用する場合はそれに合わせて行ってください。
-
プロジェクトを開きます。
もし対応するバージョンの RLS がインストールされていない場合は、次のようなダイアログが右下に表示されます。ここで Yes を選択すれば自動的に RLS がインストールされます。
-
ステータスバー左側の様子をチェックします。
開いた当初は、くるくる回るインジケーターとともに Starting と表示されています。
しばらくするとくるくる回りながらビルドが始まります。右側には現在ビルドしているクレートが表示されます。
それが終わると、くるくる回るインジケーターが消えて RLS とだけ表示される状態になります。
この状態になれば RLS の準備は完了です。
プロジェクトのコピー
TODO このページは書きかけです。
競プロで書くコードは書き捨てにする、という方は先程作ったプロジェクトをすべての問題で使いまわしても問題ありませんが、コードをずっと残しておきたいとか、あるいは詰まったから一旦保存しておいて別の問題に行きたいことがよくある、といった事情で、プロジェクトを予め問題数分用意しておきたい方もいるかと思われます。この作業も少々手間ですので、このようにされたい方はコンテスト前に準備しておくとよいでしょう。
もっとも簡単で確実なのは、プロジェクトをそのまままるごとコピーすることです。容量に不安のない方はそれで構いません。しかし数百 MB に及ぶプロジェクトをそのままコピーすることは時間も負担もかかりますし、リソース的に難しいという方もいると思います。多くの容量を占めるのは target/
以下であり、しかもほとんどは全く同一のビルドキャッシュになります。そこでこの target
フォルダをシンボリックリンクにしてしまい、実体は一つにしてしまうという手があります。 (TODO: Linux と macOS での動作確認をする)
方法は、コピーするときに target
フォルダだけ除外してコピーしておき、コピーされた各プロジェクトの中に、先に除外した target/
フォルダを指すようなシンボリックリンク (Windows の場合、ジャンクションでも構いません) を作成します。
-
Windows
(TODO: ジャンクション (
mklink /J target /path/to/original/target
) または (必要ならローカルセキュリティポリシーまたはレジストリエディタを使って SeCreateSymbolicLinkPrivilege 権限を有効にして) シンボリックリンク (mklink /D target /path/to/original/target
)) -
macOS または Linux
(TODO:
ln -s /path/to/original/target target
)
ただし、このときはメインバイナリ target/debug/<project-name>
すらも共有になってしまうため、別のプロジェクトを cargo run
する前に cargo clean -p <project-name>
としてバイナリを削除する必要があります。 cargo
はおそらくソースファイルとバイナリの更新日時を比較してリビルドの要不要を判断しているため、状態によっては前のプロジェクトの成果物をそのまま実行してしまうことがあります。
コンテスト開始後の流れ
- 問題を開き、テストケースを記述します。
- プログラムを書きます。
- テストします。
- プログラムを提出します。
テストケースの作成
Rust には組み込みでテストをするための機構が備わっています。これらの様式に則ってテストを書くと、 cargo test
コマンドを実行するだけでテストを実行することができます。これを競プロでもサンプルケースが通るかどうかを確かめるのに利用することができます。この機能の使い方の例として、生成したテンプレートの tests/sample_inputs.rs
には AtCoder Beginner Contest 086 C - Traveling のサンプルケースが書かれています。
Note: 直接手と目で確認する場合や、別のコンテスト支援ツールを利用する場合などはこの手順は必要ありません。また
cargo test
を利用しない場合、サンプルのテストファイルは削除しても良いですし、しなくても特に問題はありません。
なんとなく見れば分かるように、 output_with_stdin()
の引数にサンプル入力を渡し assert_eq!(output.stdout_str(), <expected output>);
で期待する出力と一致するかどうかを確かめます。一致しない場合、テストは失敗します。
use cli_test_dir::*;
const BIN: &'static str = "./main";
#[test]
fn sample1() {
let testdir = TestDir::new(BIN, "");
let output = testdir
.cmd()
.output_with_stdin(r#"2
3 1 2
6 1 1
"#)
.tee_output()
.expect_success();
assert_eq!(output.stdout_str(), "Yes\n");
assert!(output.stderr_str().is_empty());
}
#[test]
fn sample2() {
let testdir = TestDir::new(BIN, "");
let output = testdir
.cmd()
.output_with_stdin(r#"1
2 100 100
"#)
.tee_output()
.expect_success();
assert_eq!(output.stdout_str(), "No\n");
assert!(output.stderr_str().is_empty());
}
#[test]
fn sample3() {
let testdir = TestDir::new(BIN, "");
let output = testdir
.cmd()
.output_with_stdin(r#"2
5 1 1
100 1 1
"#)
.tee_output()
.expect_success();
assert_eq!(output.stdout_str(), "No\n");
assert!(output.stderr_str().is_empty());
}
標準入力を書く場合、通常のダブルクオートでも改行を含めることはできますが、特別な文字 (\
や "
など) を含む場合に備えて、サンプルのように r#"........"#
のような形式を使うとよいかもしれません。
Note:
r"..."
は基本的には普通の文字列ですが、文字列内の\
をエスケープしません。また"
そのものを含めたいときに備えてr####"...."####
のように#
を任意個挟むことができます。
なお、この方法では出力を単純に文字列同士の比較によって判定しています。浮動小数点数のように出力に誤差を認める形式や「グラフを一つ出力せよ」のように解が複数あり得る問題のテストにはそのままでは利用できません。それでもテストには任意のコードがかけるわけですから (実際にそこまでするかどうかは別としても) 浮動小数点数やグラフをパースしてチェックするというような使い方も可能です。
プログラムの作成
TODO このページは書きかけです。
テストが用意できたらプログラムを作成します。エディタを開いて思う通りにコードを書きましょう。
ちなみに、 AtCoder のコンテストでは、一部の例外的なコンテストを除いて、例え類題検索のようなことであっても、コンテスト中に自由にインターネットを検索・閲覧してよいことになっています。
Note: このことは厳密には各コンテストごとのルール (例えば AtCoder Beginner Contest 160 であればこちら) に記されています。
したがって、コンテスト中に標準ライブラリのドキュメントやその他の外部クレートのドキュメント、使い方やサンプルコードなどを検索することは問題ありません。たとえば itertools
にこういうメソッドなかったかなと思うことがあれば、遠慮なく Google で "itertools rust" などと検索してみましょう。大抵はそれでドキュメントが見つかるはずです。
(TODO: コンテスト中に特に有用なページがあればまとめたい)
プログラムのテスト
一通り書き終わったと思ったら、提出前にまずは一度サンプルケースで答えが合うかどうかを確認してみましょう。コマンド プロンプトまたは端末を開いてプロジェクトフォルダに移動して、次のようなコマンドを実行します。
$ cargo test
Compiling abc000 v0.1.0 (C:\Users\dicen\workspace\daily\2020\0406\abc000)
Finished dev [unoptimized + debuginfo] target(s) in 0.52s
Running target\debug\deps\main-1ecbd097f851d76e.exe
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Running target\debug\deps\sample_inputs-05a0de7d755e5cc1.exe
running 3 tests
No
test sample3 ... ok
Yes
test sample1 ... ok
No
test sample2 ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
すると、このようにテスト結果が表示されます。このように最後に test result: ok.
となっていれば、すべてのテストに成功しているということになります。一方で、もし成功しないテストがあれば、次のようになります。
$ cargo test
Compiling abc000 v0.1.0 (C:\Users\dicen\workspace\daily\2020\0406\abc000)
Finished dev [unoptimized + debuginfo] target(s) in 0.68s
Running target\debug\deps\main-1ecbd097f851d76e.exe
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Running target\debug\deps\sample_inputs-05a0de7d755e5cc1.exe
running 3 tests
No
test sample2 ... ok
Yes
No
test sample1 ... FAILED
test sample3 ... ok
failures:
---- sample1 stdout ----
thread 'sample1' panicked at 'assertion failed: `(left == right)`
left: `"Yes\n"`,
right: `"No\n"`', tests\sample_inputs.rs:16:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
failures:
sample1
test result: FAILED. 2 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
error: test failed, to rerun pass '--test sample_inputs'
最後の test result: FAILED.
により失敗したことが示されています。その横には成功と失敗の個数が示されています。そして上には失敗したときのコマンドの出力も示されています。 assertion failed
以降から、 No
と出力すべきところを Yes
と出力してしまったことがわかります。
Note: メッセージに示される
left:
とright:
はマクロassert_eq!(left, right);
に渡される引数の左右です。用意したテストコードでは実際の出力を左、期待される出力を右としてassert_eq!(output.stdout_str(), "No\n");
としていたことから、本来はNo
と出力すべきところをYes
と表示した、ということがわかります。
プログラムの提出
TODO このページは書きかけです。
テストも終わって問題ないとなれば、最後は提出です。書いたコードをコピーして、コンテストページから提出し、結果を確認してください。 AC
ならば正解で、 WA
ならば不正解です。サンプルが通るのに WA
になった場合は、考慮漏れや他のバグがないか確認してみましょう。
Tips(小技集)
TODO このページは書きかけです。
クレートの使用例
外部クレートの使用例については 2020 Update のページに具体的なソースコードと一緒にまとまっています。
AtCoder運営者向けの情報
本章では以下の内容について説明します。
- Rustツールチェインやクレート(外部ライブラリ)をジャッジサーバへインストールする手順
- 提出されたプログラムのコンパイルおよび実行手順
Rustツールチェインやクレートの内容など
- Rust 1.39.0
- インストールするクレート:このページを参照
内容についてのお問い合わせなど
内容について質問などがありましたら、GitHub Issueなどでご連絡ください。
なお本章のMarkdownソースファイルはGitHub rust-lang-ja/atcoder-rust-resourcesにあります。
素のRust環境について
AtCoder 2019/7 Language Updateのスプレッドシートでは、従来のようにクレートがインストールされていない「素のRust環境」の併設を望む声もあるようです。
そのような環境を望む理由については、正直な話、よくわかりません。 本章の手順に従ってセットアップした環境が問題なく動作するなら、わざわざ別の環境を用意する必要はないからです。 もしユーザプログラム(選手が提出したプログラム)がクレートを使わないなら、本章の手順でインストールしたクレートはないものと同じになります。コンパイルにかかる時間やコンパイルによって生成される実行ファイルには影響を与えません1。
あるいは本章でセットアップした環境が何らかの条件下ではうまく動かないものであって、それに当たった際に一切コンパイルできなくなるなどの障害が起こることを心配しているのかもしれません。とはいえジャッジサーバの環境は固定されていますので、そういうことは起こりにくいと考えられます。
もしそれでも欲しいという声があるなら、素のRust環境を別途用意することも技術的には可能です。
このことは他の言語におけるライブラリとまったく同様かと思います。例えばC++にboostがインストールされていても、それらをincludeしたり利用したりしなければコンパイル時間や成果物の性能にほぼ影響を与えないのと同じです。
方法1:環境は1つのまま、コンパイルコマンドを2種類用意する
1つ目の方法は、環境自体は本章の手順で1つだけセットアップして、コンパイルコマンドを切り替えることです。
本章ではクレート入りの環境を利用する方法として、公式のパッケージマネージャCargoを使う方法と、rustc
に必要なオプションを追加して呼び出す方法の二通りを提案します。しかし、これはRustコンパイラを呼び出す方法やコンパイルオプションが違うだけであって、ツールチェインを改変するものではありません。rustc
を今まで通りに外部クレートのパスを指定せずに呼び出せば、そのまま素の環境ということになります。
たとえばクレート入り環境を利用する方法として「rustc
に必要なオプションを付ける方法」を選んだとします。このときのコンパイルコマンドは以下のように直接外部クレートのパスを指定するオプションを付けてコンパイラを実行します2。
$ RUST_LIB=$RUST_HOME/lib
$ rustc --edition=2018 -C opt-level=3 \
$(rustc-dep-option-generator $RUST_LIB/Cargo.toml $RUST_LIB/target/release/deps) \
main.rs
この環境を素の環境、つまりクレートがインストールされていない環境と同じにするには、外部クレート関連のコンパイルオプションをつけずにコンパイラを実行します。
$ rustc --edition=2018 -C opt-level=3 main.rs
例示のコマンドラインではクレート検索パス生成ツール(rustc-dep-option-generator
コマンド)を利用しています。このコマンドの出力は各外部クレートのパスを指定するコンパイルオプション群です。なおこのツールを利用することは必須ではありませんし、事前にこのツールを使うか手動でコンパイルオプションを生成してベタ書きするのでも構いません。
方法2:文字通り2つの環境を用意する
2つ目の方法は、本章の環境に加えて、素のRust環境も用意することです。
シェルの環境変数RUSTUP_HOME
とCARGO_HOME
を本章のものとは違う値に設定して(またはunset
して)rustup
をインストールすれば、本章の環境とは別の場所にRustツールチェインがインストールでされます。
コンパイルの際も環境変数の値を変えることで、本章でセットアップする環境と、素のRust環境を切り替えます。
環境変数 | 本章でセットアップする環境(クレートを使用できる環境)を使用する場合 | 素のRust環境を使用する場合 |
---|---|---|
RUSTUP_HOME | /usr/local/lib/rust/rustup | rustup をデフォルトの場所にインストールしたのならunset RUSTUP_HOME |
CARGO_HOME | /usr/local/lib/rust/cargo | Cargoをデフォルトの場所にインストールしたのならunset CARGO_HOME |
本章で想定している環境
用語
まずは用語を整理します。本章で用いる用語には以下のものがあります。
- ユーザプログラム
- 選手が提出したプログラムのこと
- ジャッジサーバ
- オンラインジャッジを行うサーバのこと。ジャッジサーバはユーザプログラムをコンパイルし、実行する
- 実行ファイル(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
程度です。
インストール
本節では以下の内容について説明します。
- Rustツールチェインのインストール
- クレートのコンパイルとインストール
- (必要なら) クレートの検索パス生成ツールのインストール
Rustツールチェインのインストール
このページではAtCoderのジャッジサーバにRustツールチェインをインストールする手順を説明します。
Rustバージョン
今回の言語アップデートでは2019年11月7日にリリースされた1.39.0をインストールします。
Rustの安定版(stable版)は6週間ごとにリリースされますので、既にバージョン1.40.0が12月19日にリリースされています。ただ、安定性のためには世に出てからある程度の時間が経っているバージョンを選ぶ方が望ましいと考えられますので、ここでは1つ前のバージョンをインストールします。様子を見る期間については、現在のところはおよそ次期バージョンがリリースされるまでを1つの目安としています。
ツールチェインのインストールにはrustup
というRustプロジェクト公式のコマンドラインツールを使います。
これにより特定のバージョンのRustをインストールすることが可能になります。
ツールチェインの内容とインストール先
今回インストールするRustツールチェインには以下のものが含まれています。
- Rustコンパイラである
rustc
コマンド - Rustのビルドツール兼パッケージマネージャである
cargo
コマンド - Rustの標準ライブラリとAPIドキュメント
実際のコンパイルにもCargoを利用する場合はもちろんですが、運用時にはrustc
に必要なオプションを付けてコンパイルする方法をとる場合でも、依存ライブラリをまとめてコンパイルするためにCargoを利用します。具体的にはこのページに続く数ページをご覧ください。
Rustツールチェインはデフォルトではrustup
を実行したLinuxユーザのホームディレクトリ配下(~/.rustup/toolchains
)にインストールされます。
しかし今回はジャッジサーバ上の全てのLinuxユーザから使えるよう、/usr/local/lib/rust
配下にインストールします。
このディレクトリは一般のLinuxユーザからは書き込みができないように設定し、ジャッジの際にツールチェインがユーザプログラムによって変更されないよう保護します。
依存ソフトウェアのインストール
Rustツールチェインをインストールする前に、Rustプログラムのリンクやクレートのビルドに必要なソフトウェアをインストールしましょう。 必要なソフトウェアは以下のとおりです。
- Rustツールチェインやクレートのダウンロードに必要なツール:curl, git-core
- ユーザプログラムのリンク時に必要なツール:gcc, binutils
- 一部のRustクレートのビルドに必要なツール:make, pkg-config
以下のコマンドを実行します。
$ sudo apt update
$ sudo apt install -y curl git-core gcc binutils make pkg-config
Rustツールチェインのインストール
rustup
とRustツールチェインをインストールしましょう。
以下のコマンドを実行します。
$ sudo -i
# whoami
root
# RUST_TOOLCHAIN=1.39.0
# export RUST_HOME=/usr/local/lib/rust
# export RUSTUP_HOME=${RUST_HOME}/rustup
# export CARGO_HOME=${RUST_HOME}/cargo
# mkdir -p $RUST_HOME
# chmod 0755 $RUST_HOME
## rustupをインストールし、同時に指定したバージョンのRustツールチェインをインストールする
# curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
sh -s -- -y --default-toolchain "${RUST_TOOLCHAIN}" --no-modify-path
インストールに成功すると以下のように表示されます。
info: downloading installer
info: syncing channel updates for '1.39.0-x86_64-unknown-linux-gnu'
info: latest update on 2019-11-07, rust version 1.39.0 (4560ea788 2019-11-04)
info: downloading component 'rustc'
info: downloading component 'rust-std'
info: downloading component 'cargo'
...(中略)...
1.39.0 installed - rustc 1.39.0 (4560ea788 2019-11-04)
Rust is installed now. Great!
To get started you need Cargo's bin directory (/usr/local/lib/rust/cargo/bin)
in your PATH environment variable.
To configure your current shell run source /usr/local/lib/rust/cargo/env
環境変数の設定
rootユーザと一般のLinuxユーザがRustツールチェインを使えるように設定しましょう。
それぞれのユーザのシェルの初期設定ファイルに以下のコマンドを追加します。
(bash
の例です)
# .bashrcファイルに以下のコマンドを追加する
export RUST_HOME=/usr/local/lib/rust
export RUSTUP_HOME=${RUST_HOME}/rustup
export CARGO_HOME=${RUST_HOME}/cargo
source ${CARGO_HOME}/env
最後のsource
コマンドは、コマンド検索パス(PATH
)に/usr/local/lib/rust/cargo/bin
を追加します。
インストール後の動作確認
Rustツールチェインが正しくインストールできたか確認しましょう。 一般のLinuxユーザで以下のコマンドを実行します。
## 一般ユーザで実行
$ source ~/.bashrc
$ echo $RUSTUP_HOME
/usr/local/lib/rust/rustup
$ which rustc
/usr/local/lib/rust/cargo/bin/rustc
## バージョンなどを確認
$ rustc -V
rustc 1.39.0 (4560ea788 2019-11-04)
$ cargo -V
cargo 1.39.0 (1c6ec66d5 2019-09-30)
$ rustup -V
rustup 1.21.1 (7832b2ebe 2019-12-20)
$ rustup show
Default host: x86_64-unknown-linux-gnu
1.39.0-x86_64-unknown-linux-gnu (default)
rustc 1.39.0 (4560ea788 2019-11-04)
## Rustプログラムのビルドと実行ができることを確認
$ cd /tmp
$ cargo new hello && cd $_
$ cargo run
Hello, world! # このように表示されればOK
$ cd
$ rm -rf /tmp/hello
クレートのコンパイルとインストール
このページではAtCoderのジャッジサーバにRustの外部ライブラリであるクレートをインストールする手順を説明します。
インストールするクレートについて
今回インストールするクレートは、SlackのRust日本語コミュニティ「rust-jp」のメンバーがAtCoderでぜひ使いたいと考えているものです。 大半は既存のクレートですが、この機会に新たに開発したものもあります。
これらのクレートは主に以下の種類に分類されます。
- Rust の標準ライブラリにないが、他のいくつかの言語では標準の機能であって、かつ競プロ以外でも広く使われているもの
- 例:C++の
lower_bound
やbitset
に相当する機能
- 例:C++の
- 競技プログラミング特化型だが、競プロの面白さを損なわない範囲(ズルにならない範囲)で便利になるもの
- 例:Rustでは入力関連の記述が煩雑になりがちなので、それらを簡潔に記述できるマクロ集
- セキュリティなどの理由から高速性が犠牲になっている機能を置き換え、特にマラソンにおいて威力を発揮するもの
- 例:標準ライブラリのハッシュ関数はDoS攻撃を避けるために暗号強度があり、計算量が多い。 競技プログラミングでは暗号強度は求められず、より計算量の少ないハッシュ関数で十分
AtCoder運営者様へのお願い
対象クレートの一覧はこのページにあります。 ジャッジサーバで使用して問題ない内容か、インストール前にレビューをお願いいたします。
質問や要望などがありましたら、GitHub Issueなどでご連絡ください。
ファイルレイアウトとクレートの事前コンパイル
インストール作業に入る前に、背景を説明します。
一般的なCargoを使用した開発では、Cargo.toml
という設定ファイルに依存クレートの情報を記述します。
cargo build
コマンドを実行すると、Cargoはそれらのクレートのソースコードをダウンロードし、適切なオプションと共にrustc
を実行することでクレートをコンパイルします。
クレートのソースコードは$CARGO_HOME/registory/src
にダウンロードされ、コンパイル済みのクレート(rlib
ファイル)はCargo.toml
が置かれたディレクトリを起点とする相対ディレクトリ./target/release/deps
配下に出力されます。
一般的な開発時のファイルレイアウト
$HOME # ユーザのホームディレクトリ
|-- $CARGO_HOME (~/.cargo)
| └-- registory
| └-- src
| └-- クレートのソースコード
|
└-- my-package
|-- Cargo.toml # 設定ファイル。依存クレートの情報が書かれている
|-- src
| └-- main.rs # Rustプログラムのソースコード
└-- target
└-- release
|-- deps
| |-- X.rlib # コンパイル済みのクレート
| └-- Y.rlib
└-- main # コンパイル、リンク済みの実行ファイル
ジャッジサーバーでは様々な条件が通常の環境と異なるため、最初に説明した通りいくつか工夫をする必要があります。
まず、今回は各ファイルを以下のように配置し、クレートのコンパイルは導入時に済ませておくことにします。
AtCoderジャッジサーバでのファイルレイアウト(導入時に配置するもの)
$RUST_HOME (/usr/local/lib/rust)
|-- $CARGO_HOME (/usr/local/lib/rust/cargo)
| └-- registory
| └-- src
| └-- クレートのソースコード
└-- lib
|-- Cargo.toml # 設定ファイル。依存クレートの情報が書かれている
└-- target
└-- release
└-- deps
|-- X.rlib # コンパイル済みのクレート
└-- Y.rlib
そしてジャッジの際には以前説明したCargoを利用する方法とrustc
を利用する方法のいずれかを利用してコンパイルします。
[cargo] AtCoderジャッジサーバでのファイルレイアウト(ジャッジの際に作成するもの)
$HOME # ユーザのホームディレクトリ
└-- WORKAREA # ジャッジ用の一時ディレクトリ
└-- lib # 全部入りプロジェクトをまるまるコピーしたもの
|-- main.rs # ユーザプログラム(提出されたプログラム)のソースコードに置き換える
|-- Cargo.toml
└-- target
└-- release
|-- deps # (外部クレートのコンパイルキャッシュ)
└-- atcoder-rust-base # コンパイル、リンク済みの実行ファイル
[rustc] AtCoderジャッジサーバでのファイルレイアウト(ジャッジの際に作成するもの)
Cargoを使用せず、rustc
に適切なオプション(ライブラリ検索パスなど)を与えて実行することで、事前にコンパイルしておいたrlib
ファイルとリンクさせます。
$HOME # ユーザのホームディレクトリ
└-- WORKAREA # ジャッジ用の一時ディレクトリ
|-- main.rs # ユーザプログラム(提出されたプログラム)のソースコード
└-- main # コンパイル、リンク済みの実行ファイル
クレートのコンパイルに使用するCargoパッケージのダウンロード
それではインストール作業に入りましょう。実際のコンパイルにCargoを使う場合もrustc
を使う場合もここは同様です。
クレートの事前コンパイルに使用するCargoパッケージは、GitHub rust-lang-ja/atcoder-rust-base(ja-all-enabledブランチ)に用意されています。
このパッケージにはCargo.toml
ファイルなどが含まれており、インストール対象のクレートがすでに設定されています。
([dependencies]
セクションに書かれています)
このリポジトリをgit clone
し、/usr/local/lib/rust/lib
に配置します。以下のコマンドを実行します。
## rootユーザで作業する
$ sudo -i
# whoami
root
# echo $RUST_HOME
/usr/local/lib/rust
## Cargoパッケージをgit cloneする
# git clone https://github.com/rust-lang-ja/atcoder-rust-base.git \
--branch ja-all-enabled --single-branch \
${RUST_HOME}/lib
クレートを削除する
検討の結果、一部のクレートは残念ながら導入すべきでないクレートだと判断されることもあるかと思います。そういった場合はGitHub Issueを通してご連絡頂ければ当該クレートとそれに関連するテストの削除等の対応をさせていただきますが、一応、その方法も説明しておこうと思います。
例えば、競プロ入出力補助のproconio
が相応しくないので削除したいとなったとします。まずはこのクレートを依存から削除する必要があります。git clone
したディレクトリ内のCargo.toml
ファイルを開き、[dependencies]
セクションを見つけてください。すると例えば次のようにクレートが並んでいるかと思います。
[dependencies]
# AtCoder 2019年言語アップデート以降に使用できるクレート
# 競技プログラミングの入出力サポート
proconio = { version = "=0.3.4", features = ["derive"] }
# f64のOrd/Eq実装
ordered-float = "=1.0.2"
(...以下略...)
クレートによってオプションが付せられていることもありますが、{削除したいクレート名} = ...
となっている行をコメントアウトまたは削除してください。
続いてテストを削除します。src/main.rs
ファイルを開くと、ずらっとテスト関数が並んでいます。これらの関数からrun_{削除したいクレート名}
とtest_{削除したいクレート名}
という関数を丸ごと削除してください。test_{削除したいクレート名}
関数は、その前の行に#[test]
アトリビュートがついているかと思いますので、それごと削除してください。
そしてmain()
関数内にあるrun_{削除したいクレート名}
関数を呼び出す文を削除してください。
基本的にはこれで削除は完了です。この後クレートのテストを行いますが、ここでunresolved import
系のエラーが出るようなら適宜削除してください。
削除するべきコードが無い場合、テストが別のファイルに分かれている場合があります。例えばjemallocator
系を削除したいならばtests/test_jemallocator.rs
というファイルを削除してください。
またjemallocator
系を削除する場合、Cargo.toml
内の以下の部分も削除してください。
[features]
jemalloc = ["jemalloc-ctl", "jemallocator"]
default = ["jemalloc"]
# 代替ヒープアロケータ。条件によってはシステムアロケータより速いことも
[target.'cfg(not(windows))'.dependencies]
jemallocator = { version = "=0.3.2", optional = true }
jemalloc-ctl = { version = "=0.3.3", optional = true }
[[test]]
name = "jemallocator"
path = "tests/test_jemallocator.rs"
required-features = ["jemalloc"]
クレートのテスト
一度、導入したクレートが正しく動作するのかを確認しましょう。
テストコードは$RUST_HOME/lib/src/
及び$RUST_HOME/lib/tests
にあります。先に述べたように使わないクレートに関わるコードは削除してください。実行するには、cargo test
コマンドを入力します。
# cd $RUST_HOME/lib
$ cargo test --release
Compiling atcoder-rust-base v0.1.0 (...)
Finished release [optimized] target(s) in 7.31s
Running target\release\deps\...
running 11 tests
test test_ascii ... okYes
Yes
test test_bitset_fixed ... ok
test test_modtype ... ok
test test_ordered_float ... ok
test test_permutohedron ... ok
test test_itertools ... ok
test test_rand_family ... ok
test test_proconio ... ok
test test_regex ... ok
test test_rustc_hash ... ok
test test_superslice ... ok
test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
多少表示されるテストの個数は異なるかもしれませんが、一番下の行がtest result: ok.
で0 failed;
となっていれば問題ありません。
また今回、使用するクレート自体のテストを実行するためのツール、dep-tests
を作成しました。
dep-tests
はcargo commandのエイリアスとして、cargo dep-tests
で起動できます。
ただしalias
は任意のスクリプトを実行できないのでcwdが$RUST_HOME/lib
である必要があります。
以下、cwdを$RUST_HOME/lib
とします。
参考: cargo で npm-scripts 的なことをする
[alias]
dep-tests = ["run", "--manifest-path", "./dep-tests/Cargo.toml", "--"]
$ cargo dep-tests --help
Finished dev [unoptimized + debuginfo] target(s) in 0.07s
Running `dep-tests/target/debug/dep-tests --help`
dep-tests 0.0.0
Run all of the tests in the dependency graph.
USAGE:
dep-tests [FLAGS] [OPTIONS] <dir>
FLAGS:
--all-features Activate all available features
--no-default-features Do not activate the `default` feature
--frozen Require Cargo.lock and cache are up to date
--locked Require Cargo.lock is up to date
--offline Run without accessing the network
-h, --help Prints help information
-V, --version Prints version information
OPTIONS:
-p, --package <SPEC>... Package to run test for
--features <FEATURES>... Space-separated list of features to activate
--color <WHEN> Coloring: auto, always, never
-d, --depth <N> How deep in the dependency chain to search
ARGS:
<dir> Directory to run tests [default: /tmp/atcoder-rust-base-dep-tests]
$ cargo dep-tests --all-features -d 1
いくつかのクレートはdep-tests
で動かないのでテストしないように除外されています。
そのリストと理由は$RUST_HOME/lib/dep-tests.toml
に記述されています。
dep-tests
の動作の説明をしておきます。
実はcargo
にはdependency graph上にあるクレートのテストをそのまま実行する機能があります。
$ cargo test -p maplit -p num
しかしこれには致命的な欠点があり、[dev-dependencies]
が一つでもあると問答無用で拒否されます。
$ cargo test -p regex
error: package `regex` cannot be tested because it requires dev-dependencies and is not a member of the workspace
このエラーはrust-lang/cargo#6192
で追加されました。
禁止されている理由はこのPRで言及されている通り、dev-dependencyがある場合ワースクペース上には無い新たなクレートが必要になることがあるためです。
dep-tests
は『拡張』したワークスペースを新たに作成してその上でテストを実行します。
例えばitertools v0.8.1
, regex v1.3.1
を対象にした場合、以下のようなCargo.toml
が生成されます。
[package]
name = "atcoder-rust-base-dep-tests"
version = "0.0.0"
edition = "2018"
publish = false
[workspace]
members = ["./itertools-0.8.1", "./regex-1.3.1"]
[patch.crates-io]
itertools = {path = "./itertools-0.8.1"}
regex = {path = "./regex-1.3.1"}
[dependencies]
_0 = {package = "aho-corasick",version = "=0.7.6",default-features = false,features = ["default", "std"]}
_1 = {package = "alga",version = "=0.9.2",default-features = false,features = ["default", "std"]}
# 略
_25 = {package = "itertools",path = "./itertools-0.8.1",default-features = false,features = ["default", "use_std"]}
# 略
_84 = {package = "regex",path = "./regex-1.3.1",default-features = false,features = ["aho-corasick", "default", "memchr", "perf", "perf-cache", "perf-dfa", "perf-inline", "perf-literal", "std", "thread_local", "unicode", "unicode-age", "unicode-bool", "unicode-case", "unicode-gencat", "unicode-perl", "unicode-script", "unicode-segment"]}
# 略
_107 = {package = "version_check",version = "=0.9.1",default-features = false,features = []}
_108 = {package = "whiteread",version = "=0.4.4",default-features = false,features = []}
具体的な動作は以下の通りです。
- 現在のdependency graph上の
atcoder-rust-base
から現在のプラットフォームに適合する、normal-dependencyのみで繋がれた部分グラフを求める。 そしてその節点からatcoder-rust-base
を除いたクレートを得る。 またコマンドラインオプションで-d
,--depth
が指定されている場合さらにそのうちの一部に絞る。-d 1
を指定したならばCargo.toml
の[dependencies]
にあるものだけになる。 - コマンドラインオプションの
<dir>
で指定された場所に1.のうちdev-dependencyを含むもののためのワークスペースを一つ、以下の手順で作成する。- 対象のクレートについて、
$CARGO_HOME/registory/src
に展開されている.crate
ファイルの中身をコピーする。 - このコピーしたクレートをworkspace membersとして
Cargo.toml
を作成する。 このときダミーのdependencies
として元のnormal-dependencyとdev-dependencyをバージョンとフィーチャを指定する。 Cargo.lock
をatcoder-rust-base
のもので上書きする。これで大体は元のバージョンが再現できる。
- 対象のクレートについて、
- 1.のうちdev-dependencyを含まないものは
atcoder-rust-base
上で直接テストを実行する。 - 2.で作ったワークスペース上でテストを実行する。
ワークスペースを一つにまとめることには以下の問題があり、ワークスペースを分割することで軽減できそうですがどうせ厳密なバージョン, フィーチャの保存は無理だしビルド時間と消費するディスク容量を激増させてまで分割するべきではない、と考えたため一つにまとめてしまいました。
- 同一の名前のpackageはworkspace memberとしては共存できない
- 既存のpackageのminor, patch versionの増加
- オフになっていたフィーチャの有効化
クレートのコンパイル
クレートをコンパイルしましょう。 以下のコマンドを実行します。
# cd $RUST_HOME/lib
# cargo build --release
コンパイルに成功すると以下のように表示されます。
# cargo build --release
...(中略)...
Finished release [optimized] target(s) in 1m 39s
$
*.rlib
ファイルが作られていることを確認します。
以下のコマンドを実行します。
$ find target/release/deps/ -type f | egrep -c '\.(rlib|so)$'
66
# ↑ 上の数字を確認
(オプション)クレート検索パス生成ツールのインストール
Note: この作業は実際のビルドに
rustc
を利用する場合で、クレート検索パスを指定するコンパイルオプションをツールで生成する場合に必要となります。実際のビルドでcargo
を利用する場合とcargo build -v
などの出力を元にしてクレート検索パスを指定するコンパイルオプションを手書きする場合などはツールは不要です。
このページではrustc
のコマンドライン・オプションを生成するツールであるrustc-dep-option-generator
のインストール手順を説明します。
このツールはRustで書かれています。
依存ソフトウェアのインストール
ツールが依存するシステムライブラリをインストールします。
$ sudo apt update
$ sudo apt install -y libssl-dev
なおlibssl-dev
はrustc-dep-option-generator
がcargo
をライブラリとして用いているために必要ですが、このツールではその機能は使いません。ツール実行時のネットワーク・アクセスは不要です。
ツールのインストール
cargo install
コマンドでインストールします。
以下のコマンドを実行すると、GitHubのリポジトリからツールのソースコードがダウンロードされ、ビルドが実行されます。
ビルドに成功すると、生成された実行ファイルが$CARGO_HOME/bin
配下にインストールされます。
$ sudo -i
# whoami
root
# cargo install --git https://github.com/rust-lang-ja/atcoder-rustc-dep-option-generator.git
インストールの成功時は以下のように表示されます。
# cargo install --git https://github.com/rust-lang-ja/atcoder-rustc-dep-option-generator.git
Updating git repository `https://github.com/rust-lang-ja/atcoder-rustc-dep-option-generator.git`
Installing rustc-dep-option-generator v0.2.0 (https://github.com/rust-lang-ja/atcoder-rustc-dep-option-generator.git#...)
Updating crates.io index
Downloaded failure v0.1.5
...(中略)...
Compiling cargo v0.35.0
Compiling rustc-dep-option-generator v0.2.0 (https://github.com/rust-lang-ja/atcoder-rustc-dep-option-generator.git#...)
Finished release [optimized] target(s) in 4m 28s
Installing /usr/local/lib/rust/cargo/bin/rustc-dep-option-generator
インストール後の動作確認
ツールが動作することを確認しましょう。 以下のコマンドを実行します。
## 一般ユーザで実行
$ which rustc-dep-option-generator
/usr/local/lib/rust/cargo/bin/rustc-dep-option-generator
$ echo $RUST_HOME
/usr/local/lib/rust
$ rustc-dep-option-generator --help
rustc-dep-option-generator 0.2.0
rust-lang-ja Developers
A program to generate Rust compiler options to locate prebuilt crates.
USAGE:
rustc-dep-option-generator [OPTIONS]
FLAGS:
-h, --help Prints help information
-V, --version Prints version information
OPTIONS:
--manifest-path <PATH> Path to Cargo.toml
--format <FORMAT> Output format [default: shell] [possible values: shell, json]
$ RUST_LIB=${RUST_HOME}/lib
$ rustc-dep-option-generator
--extern aho_corasick=/usr/local/lib/rust/lib/target/release/deps/libaho_corasick-aa47a24abbe125fb.rlib
--extern alga=/usr/local/lib/rust/lib/target/release/deps/libalga-8879e1bae3df17da.rlib
...(中略)...
--extern jemallocator=/usr/local/lib/rust/lib/target/release/deps/libjemallocator-f83210f5bd62bec7.rlib
-L dependency=/usr/local/lib/rust/lib/target/release/deps
## ↑ errorの文字が表示されなければOK
##
## エラーの例
## error: failed to find appropriate path for aho-corasick
$ echo $?
0 # 0ならOK
これでインストール作業は終了です。
コンパイルコマンドと実行コマンド
このページではジャッジサーバでユーザプログラムをコンパイルし、実行するためのコマンドを説明します。
[rustc] ファイルレイアウトとコンパイルの流れ
選手がRustプログラムを提出すると、その内容がジャッジサーバ上のファイルシステムに書き出されます。
ジャッジサーバがどのような設計になっているかわからないので、ここでは仮にLinuxユーザのホームディレクトリ配下にWORKAREA
という作業用の一時ディレクトリができるものとします。
RustプログラムはWORKAREA
のmain.rs
に書き出すようにジャッジサーバを設定してください。
$HOME # ユーザのホームディレクトリ
└-- WORKAREA # ジャッジ用の一時ディレクトリ
└-- main.rs # ユーザプログラム(提出されたプログラム)のソースコード
WORKAREA
にcd
で移動して、後述のコンパイルコマンドを実行します。
すると以下のことが行われmain
という名の実行ファイルが生成されます。
- プログラムがアセンブリコードに変換されオブジェクトファイルが作られる
- オブジェクトファイルがRustの標準ライブラリや
${RUST_MOME}/lib/target/release/deps
配下に作成しておいたrlib
とリンクされる
$HOME
└-- WORKAREA
|-- main.rs
└-- main # コンパイルとリンクの成果物。実行ファイル
コンパイルコマンド
ツールチェインのインストールのページで説明したように、コンパイルを実行するシェルに以下の環境変数が設定されている必要があります。
$ echo $RUST_HOME
/usr/local/lib/rust
$ echo $RUSTUP_HOME
/usr/local/lib/rust/rustup
$ echo $CARGO_HOME
/usr/local/lib/rust/cargo
$ echo $PATH
... /usr/local/lib/rust/cargo/bin; ...
コンパイルコマンドは以下のとおりです。
$ cd $HOME/WORKAREA
$ RUST_LIB=$RUST_HOME/lib
$ rustc --edition=2018 -C opt-level=3 \
$(rustc-dep-option-generator $RUST_LIB/Cargo.toml $RUST_LIB/target/release/deps) \
main.rs
なおrustc-dep-option-generator
の出力は実行環境やソースには依らず、単に依存クレートの位置のみに依ります。従ってインストール時に先にrustc-dep-option-generator
を単体で実行し、その出力を直接コンパイルオプションとして置き換えても問題ありません。こうすると毎回コンパイルのたびにこのツールを実行する必要がありません。
これにより$HOME/WORKAREA
ディレクトリにmain
という名の実行ファイルが生成されます。
コンパイルオプションについて
rustc
コマンドに渡すコンパイルオプションは、Cargoでデフォルトのreleaseプロファイルを使用した時に、cargo build --release
で生成される、ごく一般的なものを使用します。
たとえばバイナリの実行速度を最適化するためのオプションとしては-C opt-level=3
だけを指定しています。
もし実行速度を追求するなら-C target-cpu=native
のようなオプションを追加することもできます。
しかしAtCoder運営者様から以下のようなお話があったため、今回の言語アップデートでは使用しないことにします。
https://twitter.com/chokudai/status/1138677406984691712
もうすぐジャッジアップデートの話を出すけれども、ジャッジの目的は「正解と不正解を分けること」であって、「最速を目指すこと」ではないので、十分に早い言語(C++, Rustなど)では、過剰な最適化とかを含めた提案はRejectすることがあるかも。
https://twitter.com/chokudai/status/1138677959059943424
といいつつ、言語間の差を減らすために「C++はO2とか付けない!」みたいな滅茶苦茶なことを言い出すつもりは全くなくて、「AtCoder環境がちょっと変わると動かなくなる」みたいな最適化は受け入れない、くらいの話です。
通常形式のコンテスト(アルゴリズム系)では最速を目指すことには意味がありません。
一方、マラソン系のコンテストでは、たとえ数パーセントでも速度が向上するなら嬉しいことも多くあります。 AtCoderでは将来マラソン系のコンテストもRatedにする可能性があり、その際にコンパイルオプションを追加するか検討できるかもしれません(参考)
コンパイルコマンドが動作することの確認
コンパイルコマンドが動作することを確認しましょう。 クレートのインストールに使用したCargoプロジェクトには、それらのクレートを使用する簡単なサンプルコードが含まれています。
以下の方法でコンパイルできます。
## 一般ユーザで実行
## 作業用のディレクトリを作成する
$ mkdir $HOME/WORKAREA
$ cd $HOME/WORKAREA
## サンプルコードの入ったソースファイル(2つある)をコピーする
$ RUST_LIB=$RUST_HOME/lib
$ cp -p $RUST_LIB/src/main.rs .
$ cp -p $RUST_LIB/tests/test_jemallocator.rs .
## サンプルコードをコンパイル、リンクする
$ rustc --edition=2018 -C opt-level=3 $(rustc-dep-option-generator) main.rs
$ rustc --edition=2018 -C opt-level=3 $(rustc-dep-option-generator) test_jemallocator.rs
コンパイルとリンクに成功すると実行ファイルが作られます。
$ file main
main: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0,
BuildID[sha1]=50373af3038c60f2a3e7a162b310e5226b08c8a9,
with debug_info, not stripped
$ file test_jemallocator
test_jemallocator: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0,
BuildID[sha1]=31b718908ebf067f4b1161b1dc5cf679f012ad7f,
with debug_info, not stripped
実行コマンド
実行ファイルを実行するコマンドは以下のとおりです。
$ cd $HOME/WORKAREA
$ ./main
実行コマンドが動作することの確認
実行コマンドが動作することを確認しましょう。 先ほど作成した実行ファイルを実行します。
## 一般ユーザで実行
$ cd $HOME/WORKAREA
$ ./main
...
## いろいろなメッセージが表示されるが、テスト用の出力なので内容は確認しなくてよい
## 終了コード0で終了したことだけを確認すればOK
$ echo $?
0
$ ./test_jemallocator
...
## こちらも終了コード0で終了したことだけを確認すればOK
$ echo $?
0
AtCoder運営者向けの情報は以上となります。 内容について質問などがありましたら、GitHub Issueなどでご連絡ください。
[cargo] ファイルレイアウトとコンパイルの流れ
選手がRustプログラムを提出すると、その内容がジャッジサーバ上のファイルシステムに書き出されます。
ジャッジサーバがどのような設計になっているかわからないので、ここでは仮にLinuxユーザのホームディレクトリ配下にWORKAREA
という作業用の一時ディレクトリができるものとします。
まずは先ほど依存クレートをコンパイルするのに利用したプロジェクトのディレクトリを、そっくりWORKAREA
内に丸ごとコピーしてください。依存クレートをコンパイルするのに利用したプロジェクトは、本書の通りにセットアップした場合/usr/local/lib/rust/lib
となっているはずです。
$HOME # ユーザのホームディレクトリ
└-- WORKAREA # ジャッジ用の一時ディレクトリ
└-- lib # 全部入りプロジェクトをまるまるコピーしたもの
|-- main.rs # ユーザプログラム(提出されたプログラム)のソースコードに置き換える
|-- Cargo.toml
└-- target
└-- release
|-- deps # (外部クレートのコンパイルキャッシュ)
└-- atcoder-rust-base # コンパイル、リンク済みの実行ファイル
そして、このWORKAREA/lib/main.rs
をユーザーが入力したファイルにそっくりそのまま置き換えます。
以上でCargoプロジェクトが整いました。
コンパイルコマンド
ツールチェインのインストールのページで説明したように、コンパイルを実行するシェルに以下の環境変数が設定されている必要があります。
$ echo $RUST_HOME
/usr/local/lib/rust
$ echo $RUSTUP_HOME
/usr/local/lib/rust/rustup
$ echo $CARGO_HOME
/usr/local/lib/rust/cargo
$ echo $PATH
... /usr/local/lib/rust/cargo/bin; ...
コンパイルコマンドは以下の通りです。
$ cd $HOME/WORKAREA/lib
$ cargo build --release --offline
すると$HOME/WORKAREA/lib/target/release
ディレクトリにatcoder-rust-base
という名前の実行ファイルが生成されます。
コンパイルオプションについて
Cargoを利用する場合、ネットワーク接続がない環境でも安定して動作させるため、--offline
オプションを利用します。それ以外は、通常のRustプロジェクトのビルドと同様です。
コンパイルコマンドが動作することの確認
実際のコンテストではWORKAREA/lib/main.rs
ファイルをユーザーの入力でそっくり置き換えて実行するのですが、置き換える前のWORKAREA/lib/main.rs
には各依存クレートに対応する簡単なテストコードが含まれています。クレートをインストールした際に確かめたのと同様の方法でこのファイルがコンパイルできることを確かめましょう。
まずは動作の確認です。
$ cargo test --release
Compiling atcoder-rust-base v0.1.0 (...)
Finished release [optimized] target(s) in 7.31s
Running target\release\deps\...
running 11 tests
test test_ascii ... okYes
Yes
test test_bitset_fixed ... ok
test test_modtype ... ok
test test_ordered_float ... ok
test test_permutohedron ... ok
test test_itertools ... ok
test test_rand_family ... ok
test test_proconio ... ok
test test_regex ... ok
test test_rustc_hash ... ok
test test_superslice ... ok
test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
多少表示されるテストの個数は異なるかもしれませんが、一番下の行がtest result: ok.
で0 failed;
となっていれば問題ありません。
続いてコンパイルができることを確認します。
$ cargo build --release --offline
(...略...)
Finished release [optimized] target(s) in 0.86s
上のようにFinished
が出力されれば、実行可能バイナリが$HOME/WORKAREA/lib/target/release/atcoder-rust-base
に生成されているはずです。
実行コマンドが動作することの確認
実行コマンドが動作することを確認しましょう。 先ほど作成した実行ファイルを実行します。
## 一般ユーザで実行
$ cd $HOME/WORKAREA
$ target/release/atcoder-rust-base
...
## いろいろなメッセージが表示されるが、テスト用の出力なので内容は確認しなくてよい
## 終了コード0で終了したことだけを確認すればOK
$ echo $?
0
AtCoder運営者向けの情報は以上となります。 内容について質問などがありましたら、GitHub Issueなどでご連絡ください。
コントリビュータ
TODO このページは書きかけです。