Rust(ラスト)はプログラミング言語としては新鋭で、2006年に開発が始まり、最初の安定版(バージョン1)は2015年に登場しました。以降、概要から見ていきましょう。
Rustは、Webブラウザソフトウェア「Firefox」を開発しているMozillaが支援するオープンソースのプログラミング言語です。2006年に開発がスタートした当初は、Mozilla所属のグレイドン・ホアレ氏の個人プロジェクトでしたが、2009年からはMozilla自体が支援に加わり、公式プロジェクト化されました。MozillaとRustの関係は、Mozillaが2012年に開発を開始したWebレンダリングエンジン「Servo」がRustを用いていることにあります。
Rustは、度重なる仕様変更を繰り返して、2015年にバージョン1がリリースされました。その後は、後方互換性を重視した6週間隔のリリースサイクルを保持しています。現在は、コミュニティーベースのRust Project Developers主体で開発が進められており、全てのファイルがGitHubで公開されています。
Microsoftは2019年11月から、同社のコアプラットフォームであるWindowsの開発にRustを採用していることを公表しています。そしてGoogleは、2021年4月に同社のコアプラットフォームであるAndroid OSの開発にRustを採用すると発表しています。さらに、Linuxカーネルの開発にRustを使用する動きも始まっています。
OSのコア部分の開発は、C言語やC++言語と相場が決まっていましたが、そこにRustが風穴を開けることになりました。その背景には、OSの抱えるセキュリティ上の問題(脆弱《ぜいじゃく》性)の解決があります。セキュリティ上の脆弱性の多くは、メモリ利用におけるバグに起因するものとされていて、C/C++を用いる限りはこれらの言語の特性から克服は困難とされてきました。
Rustでは、独自の言語設計によりメモリ安全性(メモリ利用における安全性が言語によって保証されていること)を実現しています。MicrosoftやGoogleはメモリ安全性を一番に評価し、Rustの採用を始めているのです。
このようにRustは、C/C++が使われてきた、あるいはこれから使われるであろう場面で、C/C++に替わる有効な選択肢となってきています。
その理由の第一が、Rustがネイティブコンパイラ言語ならではのコンパクトさと高速性を持ち、かつメモリなどのリソースの細やかな取り回しが可能なプログラミング言語である点です。しかし、単に軽量、高速で細やかなリソース操作が可能であるというだけでは、C/C++にとって替わる意味がありせん。
Rustでは、C/C++の弱点といわれる低い並列性、ポインタに代表される危険性を克服しています(※1)。つまり、高い安全性を保ちながら、並列性を担保し、細かなリソース操作も可能にしたプログラミング言語、それがRustなのです。
※1 メモリ安全性を犠牲にして効率を重視したUnsafe Rustという言語仕様もRustには含まれています。これを利用すると、C/C++のようなメモリ操作が可能になりますが、本連載では触れない予定です。
C/C++は設計が古いこともあって(Cに至っては誕生50年になろうとしています)、当時のCPUやメモリの能力を最大限に引き出すために効率を優先、安全性を二の次にしてきたようないきさつがあります。
分かりやすいのがポインタで、ポインタによる効率的なメモリ操作を可能にする一方、初期化されていない、有効なオブジェクトを指さないポインタ「ダングリングポインタ」や、確保したメモリを解放しないまま残り、メモリリソース枯渇の原因となる「メモリリーク」などによるプログラムの欠陥の原因になるなどの問題を生じさせてきました。
ネイティブコンパイラ言語では、動的にポインタの有効性などをチェックするのは性能とのトレードオフの関係にあり、一般的には行われていません。JavaやC#といった中間言語型の言語では動的なチェックを行っていますが、性能的には不利になります。
Rustでは、コンパイル時にメモリ安全性を確保するためのボローチェッカーを備えています。ボローチェッカーとは、メモリなどのリソースの所有者とリソースの生存期間(ライフタイム)の静的解析をする仕組みのことです。Rustでは、リソースと所有者を1対1にするという制約を設けることで、ある変数がオブジェクトを所有するとして、そのオブジェクトは他の変数では所有できないとし、変数の消滅とオブジェクトの破棄というライフタイムを管理できます。
ライフタイムを管理できるのでオブジェクトの破棄タイミングをコンパイラが把握でき、不要になったタイミングですぐに破棄できます。これはGC(ガベージコレクタ)が不要ということであり、GC特有の、「プログラマーからするとオブジェクトの破棄タイミングが分からない、コントロールできない」という問題と無縁になります。
このようにRustはメモリ安全性を備えるプログラミング言語なのですが、同様にメモリ安全性を備えるJavaやC#はどうかというと、これらの言語はインタープリタが必要な中間言語型であり、OSのコア部分の開発といったシステムプログラミングには適しません。システムプログラミングには、ターゲットCPUが直接解釈できる機械語に変換できるネイティブコンパイラ言語であることが求められるのです。
Rustのコンパイラは、Clangと同様にLLVM(※2)と呼ばれる仮想コンパイラプラットフォームに基づいたコンパイル機構に準拠しており、LLVMの中間コードにいったん変換されたあと、最終的にターゲットとなるCPUで動作可能なバイナリを生成します。Javaの仮想マシンと異なるのは、Javaが実行時に中間言語を解釈するのに対し、Rustは実行時には既にCPUが直接実行可能なバイナリになっている点です。
これらから、Rustのバイナリはコンパクトで、Cに匹敵する、あるいは上回る速度で動作し、システムプログラミングに適したプログラミング言語となっています。
※2 以前は、Low Level Virtual Machine(低水準仮想機械)の略とされていましたが、現在は単に(何の頭文字でもない)LLVMとされています。
C/C++は、スレッドセーフでないことがたびたび問題視されます(設計時にはスレッドという概念がなかったためです)。Cの一部の標準ライブラリ関数ではstatic(静的)な変数を使っているため、複数のスレッドから関数を呼び出すことで競合が発生する可能性があります。また変数をロックして競合の問題を回避したとしても、今度はロック解除待ちが相互に発生するデッドロックの問題もあります。
2021年現在はpthreadsといったPOSIX準拠のマルチスレッド対応ライブラリの登場で状況は変わっていますが、外部ライブラリに頼らざるを得ない状況は変わりません。Rustでは、標準ライブラリにマルチスレッドの機能が用意されているので、スレッドセーフが保証されています。
これまでに登場したプログラミング言語とRustについて、主要な項目で比較したのが表1です。
| 主要項目 | Rust | C/C++ | Java/C# |
|---|---|---|---|
| ネイティブコンパイル | ○ | ○ | × |
| メモリ安全性 | ○ | × | ○ |
| スレッドセーフ | ○ | △ | ○ |
| ガベージコレクタ | 不要 | ― | 必要 |
| 表1 RustとC/C++、Java/C#の比較 | |||
大まかに、テストには、関数単位でテストを行う単体テストと、アプリケーション全体をテストする結合テストがあります。Rustでは、標準でこれら双方の自動テスト機能が用意されています。
Javaの場合、テストのための外部ライブラリ「JUnit」が有名ですが、Rustでは別途テストライブラリをインストールする必要がありませんし、手順も統一しやすくなっています。
Rustはモジュールシステムを標準で用意しています。モジュールシステムとは、プロジェクトを階層化、分割して管理する仕組みです。ソースファイル1個のプログラムなら問題ありませんが、ソースファイルが多数にわたり、多くのプログラマーがプロジェクトに参加するようになると、プロジェクトを機能や役割で分割して管理する必要があります。
Rustでは、プロジェクトをパッケージ、クレート、モジュールといった階層で分けて管理できます。言語仕様の中にこれらを利用するための仕組みが用意されているので、特別な外部ツールが必要になりません。Cargoというパッケージマネジャーを使って、これらを適切に管理できます。
この他にも、マルチパラダイムのプログラミング言語であるなどさまざまな特徴があります。
Rustの概要を紹介したところで、今度は手元にRustの動作環境を整えていきましょう。Rustのインストールは非常に簡単です。基本的には、公式サイト記載の手順に従うだけでインストールは完了します。
本稿では、macOSとWindows向けにおすすめのインストール手段を紹介します(LinuxについてはmacOSとほぼ同様の手順になります)。
macOSの場合は、Install Rustのページに表示されているコマンドを、ターミナルで実行します(画面2)。
デフォルトでは、Rustのインストール場所は$HOME/.rustupフォルダ、$HOME/.cargoフォルダになります($HOMEはログインユーザーのホーム。/Users/naoなど)。
$HOME/.rustupフォルダは、後述するRustのアップデート用フォルダで、メタデータやツールチェーンが置かれます。$HOME/.cargoフォルダが後述するRustのパッケージマネジャーのフォルダで、Rustのコマンド類はbinフォルダに配置されます。
$HOME/.cargo/binフォルダの環境変数PATHへの設定は、ターミナルを開き直すか、source $HOME/.cargo/envコマンドを実行します。
インストールするバージョンなどを変更したい場合には「2) Customize installation」を選択しましょう。ここではデフォルトの「1) Proceed with installation(default)」のまま進めました(画面3)。
「Rust is installed now. Great!」と表示されればインストールは完了です(画面4)。
Windowsの場合、インストーラ「rustup-init.exe」を利用します(画面5)。インストーラには32bit版と64bit版があります。Windowsのバージョンに合ったものを選びましょう。ここでは64bit版を選択しました。なお、WSL(Windows Subsystem for Linux)用のインストールコマンドも表示されますが、基本的にはmacOS、Linux用と同じです。
Windowsでインストーラを利用する場合、Microsoft C++ Build toolsのインストールを求められることがあります(画面6)。
Microsoft C++ Build Toolsのインストール方法は2つあります。
前者の場合、Microsoft Visual C++ Build Toolsから「Build Toolsのダウンロード」をクリックします(画面7)。
画面6では、「Windows 10 SDK」と「English language pack」を有効にしてインストールします、と表示されています。具体的には、画面7でダウンロードできるVisual Studio Build Toolsのインストーラ(vs_buildtools__1932109249.1589283028.exe。数字はバージョンで変化する)の画面で、[ワークロード]タブが選択されている状態で「C++によるデスクトップ開発」を選択し、右ペインの[インストールの詳細]から「Windows 10 SDK」を選択します(画面8)。
同じ画面の[言語パック]タブで「英語」も選択します(画面9)。
後者の場合、Visual Studioが不要な場合にはディスク領域とインストール時間を無駄にするので、前者の方法で済ませてしまうのがよいでしょう。筆者も、前者でインストールしました。
「Microsoft Visual C++ Build Tools」のインストールを済ませたら、画面6で「Continue?」に対して「y」を入力して先に進めましょう。
デフォルトでは、Rustのインストール場所は%HOME%\.rustupフォルダ、%HOME%\.cargoフォルダになります($HOMEはログインユーザーのホーム。C:\Users\naoなど)。
%HOME%\.rustupフォルダは、後述するRustのアップデート用フォルダで、メタデータやツールチェーンが置かれます。%HOME%\.cargoフォルダが後述するRustのパッケージマネジャー用フォルダで、Rustのコマンド類はそこのbinフォルダに置かれます。%HOME%\.cargo\binフォルダの環境変数PATHへの設定は、レジストリを書き換えることで自動的に実行されます。
インストールするバージョンなどを変更したい場合には「2) Customize installation」を選択しましょう。ここではデフォルトの「1) Proceed with installation (default)」のまま進めました(画面10)。
「Rust is installed now. Great!」と表示されたらインストールは完了です(画面11)。[Enter]キーを押してコマンドプロンプトを閉じます。
アンインストールは、macOS、Windowsともにrustup self uninstallコマンドを実行します。「Continue? (y/N)」に対してyで応答すれば、$HOME/.rustupと$HOME/.cargo(macOSなど)あるいは%HOME%\.rustupと%HOME%\.cargo(Windows)からプログラムが除去され、環境変数からも取り除かれます。アップデートがあれば、rustup updateコマンドで更新できます。
% rustup update info: syncing channel updates for 'stable-x86_64-apple-darwin' ...中略... stable-x86_64-apple-darwin updated - rustc 1.53.0 (53cb7b09b 2021-06-17) (from rustc 1.52.1 (9bc8c42bb 2021-05-09)) info: cleaning up downloads & tmp directories %
Rustでは、日本語のドキュメントを読むことができます(画面12)。Steve KlabnikとCarol Nicholsの両氏によるドキュメントは、米国のNo Starch Pressよりペーパーバック書籍、電子書籍としても販売されています。
Rustをインストールすると、rustup, rustc, cargoという3つの主要なプログラムが$HOME/.cargo/binに用意されます(以降、macOSを基本にして取り上げます)。それぞれ、以下の役割を持っています。
rustupはRustのツールチェーン(開発に必要なプログラム群)を管理します。既に紹介しましたが、Rustをインストール、アップデートしたりアンインストールしたりするのは、このrustupです。
rustcはRustのコンパイラです。中間言語、実行バイナリ、静的ライブラリ、動的ライブラリをソースコードから生成するなど、Rustの中核をなします。
cargoは、Rustのビルドツールです。プロジェクトのビルド、テスト、外部ライブラリのダウンロード、ドキュメントの作成などを行えます。
コンパイルはrustcで行えますが、将来的にはプロジェクトのビルドという考え方が望ましいため、以降はcargoを中心に取り上げていきます。
Rustでは、アプリケーションを「プロジェクト」という単位で管理します。プロジェクトにおけるパッケージの作成、ビルドといった一連の作業はパッケージマネジャーであるcargoで行います。
なお、本連載では、フォルダ「atmarkit_rust」が任意の場所に作成されているものとして、そこをプロジェクトフォルダとします。ターミナル(macOSの場合)あるいはコマンドプロンプト(Windowsの場合)を開き、そこにcdコマンドで移動しておきましょう。
それでは早速プロジェクト内にパッケージを作成してみましょう。パッケージは、プロジェクトにおける機能の単位です(本連載のプロジェクト管理の回であらためて取り上げます)。今回作成するのはプログラミング言語入門の定番、「Hello, World!」を表示するパッケージです。
パッケージの作成には、cargoのnewサブコマンドを使います。パッケージの名前は「hello_world」です。Rustでは、パッケージ名やファイル名は全て小文字として、複数の単語から構成される場合には間をアンダースコア(_)で区切ることが推奨されています。コマンドは、上記のatmarkit_rustフォルダで実行します。
% pwd
/Users/nao/Documents/atmarkit_rust
% cargo new --bin hello_world
Created binary (application) `hello_world` package
%
実行例はmacOSのターミナル上のものなので、Windowsの場合はプロンプト部分を読み替えてください(%→>など)。ここでは、バイナリ(アプリケーション)hello_worldパッケージが作成されたと表示されます。--binオプションは、バイナリ(実行形式ファイル)を作成することを意味しています(省略時のデフォルト)。今回は触れませんが、--libオプションを与えるとライブラリの作成になります。
これで、cargoコマンドを実行したフォルダに、hello_worldフォルダが作成されます。cdコマンドでそこに移動して、今度はサブコマンドrunを指定してビルド、実行します。
% cd hello_world
% cargo run
Compiling helloworld v0.1.0 (/Users/nao/Documents/atmarkit_rust/hello_world)
Finished dev [unoptimized + debuginfo] target(s) in 2.26s
Running `target/debug/hello_world`
Hello, world!
%
プログラムを1行も書いていないのに、「Hello, world!」と表示されました。そうです、cargo newではパッケージを作成しますが、ソースファイルのひな型が自動的に作成されます。そのソースファイルに、「Hello, world!」を表示するコードが書かれているというわけです。
早速見てみましょう。ソースファイル名は、srcフォルダの中にあるmain.rsです。cargoを用いると、ソースファイルはsrcフォルダに作成されます。
fn main() {
println!("Hello, world!");
}
これだけです。Rustのソースファイルの拡張子は.rsです。mainがある、printlnがある、ブロックは中かっこ({ })の囲みである、文の終わりにはセミコロン(;)があるなど、C/C++の利用経験がある人なら、内容のイメージは容易でしょう。詳細は本連載の2回目以降で順次取り上げていきます。
ちなみに、Rustには望ましいとされる書式があります。リスト01-01のように、ブロックは改行せずに始める、インデントは半角スペース4個分である、などです。もっと詳しく知りたいという場合には、rustfmtというRust用フォーマッタにソースファイルをそのまま表示させてみればいいでしょう(上記には挙げませんでしたが標準でインストールされます)。
バイナリ(実行形式)は、今回の例ではtarget/debugフォルダに作成されます。
% ls target/debug build hello_world incremental deps hello_world.d examples hello_world.dSYM
hello_worldがバイナリです。Windowsでは、hello_world.exeとなります。このファイルを直接指定して実行できます。他のフォルダやファイルについては、この時点では理解する必要はありませんので省略します。
インストールと動作確認が済んだところで、パッケージの作成に伴って作成されるファイルを確認してみましょう(Windowsではdir /ahコマンドを実行してください)。
% ls -a . .git Cargo.lock src .. .gitignore Cargo.toml target %
.gitと.gitignoreは、Gitリポジトリのためのファイルです。つまり、ここからGitHubにパッケージをプッシュできるわけで、Git連携の準備が整っているのです。Cargo.lockとCargo.tomlは、パッケージのための重要なファイルです。このうち、Cargo.tomlについて見てみましょう(リスト01-02)。
[package] name = "hello_world" version = "0.1.0" authors = ["Yamauchi <nao@naosan.jp>"] edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies]
大かっこ([~])で挟まれたセクション、そしてキーと値のエントリで構成される一般的な形式です。[package]セクションでは、文字通りパッケージの情報を保持します。authorsエントリにはパッケージを作成した人の情報がOSから取得されて自動的に入ります。
下の方にある[dependencies]には依存関係にある別パッケージを記載していきますが、今回はないので有効なエントリは何もありません。このCargo.tomlには、ビルドや実行に関する指示も記述していくことができますが、それらについては本連載の後半の回で取り上げていきます。
続けて、Cargo.lockも見てみましょう(リスト02-03)。
# This file is automatically @generated by Cargo. # It is not intended for manual editing. [[package]] name = "hello_world" version = "0.1.0"
ここにもパッケージ情報のようなものが書かれていますが、注釈にある通りcargoコマンドによって自動的に生成されるので触らないように、とあります。ここでは、このような管理目的のファイルがあるのだという程度の認識にしておきましょう。
残ったsrcとtargetは、既に説明した通りソースファイルの置き場所、コンパイル結果の置き場所です。targetには、デフォルトではデバッグターゲットのためのdebugフォルダが作成され、そこに成果物が置かれます。cargo runに--releaseオプションを与えるとリリースターゲットとなり、releaseフォルダが作成されてそこに成果物が置かれます。本連載では--releaseオプションは使用せず、デバッグターゲットにてバイナリを作成していきます。
今回は、プログラミング言語Rustの特徴を紹介し、環境を構築して「Hello, World!」を表示させてみました。また、それを通じて、コマンド、フォルダ構成、ファイルの役割などについても触れました。次回は、Rustの言語仕様、変数やデータ型といった基本を解説していきます。