Rustの設計と実装Tipsを学ぶ

Rustlingsに学ぶ:セルフコンテインドなCLIアプリのためのビルド時データ埋め込み術 Part 1

解析日: 2026/7/15
対象コミット: 9253217
リポジトリ: rust-lang/rustlings
RustプロシージャルマクロCLIビルドシステムデータ埋め込み

Rustlingsに学ぶ:セルフコンテインドなCLIアプリのためのビルド時データ埋め込み術 Part 1

1. 概要

Rustlingsは、Rust言語の学習をインタラクティブに進めるための素晴らしいツールです。小さな演習問題を解きながら、Rustの基本的な概念から高度な機能までを実践的に学ぶことができます。その裏側では、堅牢なCLIアプリケーションとしての設計原則が数多く採用されています。

この記事では、Rustlingsのコードベースから、特にアプリケーションの「セルフコンテインド化」に貢献しているビルド時データ埋め込みのテクニックに焦点を当てます。このパターンは、実行ファイルにすべての必要なアセットを組み込むことで、デプロイを簡素化し、ランタイムの依存関係を減らすための強力な手段となります。

分析対象コミットSHA: 925321705edd96b39995f77edf437f7db6f9b16a 分析日: 2026-07-14T23:04:14.280Z

2. アーキテクチャ

Rustlingsアプリケーションは、メインのCLI実行可能ファイルと、rustlings-macrosというプロシージャルマクロクレートの2つの主要部分で構成されています。

rustlings-macrosクレートは、アプリケーションの学習コンテンツ(演習ファイル、解答ファイル、info.toml設定ファイルなど)をコンパイル時にRustのバイナリ内部に埋め込む役割を担っています。これにより、rustlingsコマンドを実行するだけで、外部のファイルシステムに依存せず、すべてのコンテンツにアクセスできるようになります。

このビルド時埋め込みの仕組みを、以下にMermaid図で示します。

graph TD A["exercises/#quot; & #quot;solutions/"] --> B("rustlings-macros/lib.rs") C["info.toml#quot; & #quot;READMEs"] --> B B --"quote! & include_bytes!"--> D("Generated Rust Code") D --> E("#quot;rustlings#quot; Executable") E --"Runtime Access"--> F["Embedded Exercise Data"] style A fill:#f9f,stroke:#333,stroke-width:2px style C fill:#f9f,stroke:#333,stroke-width:2px style B fill:#bbf,stroke:#333,stroke-width:2px style D fill:#afa,stroke:#333,stroke-width:2px style E fill:#ffc,stroke:#333,stroke-width:2px style F fill:#c9f,stroke:#333,stroke-width:2px

上図のように、rustlings-macrosがコンパイル時に各種ファイルを読み込み、それらをinclude_bytes!マクロを使ってRustコードに変換し、最終的にrustlings実行可能ファイルの一部として組み込んでいます。これにより、ユーザーは単一のバイナリをダウンロードするだけで、すぐに学習を開始できるのです。

3. この記事で学べること

  1. なぜビルド時データ埋め込みが必要なのか: セルフコンテインド化のメリットとデメリット。
  2. include_bytes!マクロの基本: ファイルコンテンツをバイナリに組み込む最も簡単な方法。
  3. プロシージャルマクロによる複雑な埋め込み: 多数のファイルを効率的に管理・埋め込むテクニック。
  4. 埋め込んだデータの効率的な利用: ゼロコピーデシリアライゼーションによるランタイムパフォーマンスの向上。

4. 実践的な実装・コード解説

rustlings-macrosクレートの役割

rustlings-macrosクレートのlib.rsファイルが、データ埋め込みの核心部分です。このクレートは、info.tomlファイルを解析し、exercises/ディレクトリ内のすべての演習ファイルと、対応するsolutions/ディレクトリ内の解答ファイルを検出します。そして、これらのパスを使ってinclude_bytes!マクロを生成し、最終的にアプリケーションのバイナリに含めるRustコードを生成します。

まず、info.toml のデシリアライズに必要な構造体を定義します。

// info.tomlの構造を定義
use serde::Deserialize;

#[derive(Deserialize)]
struct ExerciseInfo<'a> {
    name: &'a str,
    dir: &'a str,
}

#[derive(Deserialize)]
struct InfoFile<'a> {
    exercises: Vec<ExerciseInfo<'a>>,
}

次に、プロシージャルマクロ include_files の実装です。コンパイル時にファイルを読み込み、include_bytes! マクロ呼び出しを自動生成します。

use proc_macro::TokenStream;
use quote::quote;

#[proc_macro]
pub fn include_files(_: TokenStream) -> TokenStream {
    let info_file_bytes = include_bytes!("../info.toml");
    let info_file: InfoFile = toml::from_slice(info_file_bytes).unwrap();

    let mut exercise_files_quotes = Vec::new();
    let mut soluti

    for exercise in &info_file.exercises {
        let exercise_path = format!("../exercises/{}/{}.rs", exercise.dir, exercise.name);
        let soluti exercise.dir, exercise.name);

        exercise_files_quotes.push(quote! { include_bytes!(#exercise_path) });
        solution_files_quotes.push(quote! { include_bytes!(#solution_path) });
    }

    quote! {
        EmbeddedFiles {
            info_file: include_bytes!("../info.toml"),
            exercise_files: &[#(ExerciseFiles { exercise: #exercise_files_quotes, solution: #solution_files_quotes }),*],
        }
    }
    .into()
}

このマクロは、rustlingsアプリケーションのビルド時に実行され、次のような構造体とデータを含むRustコードを生成します。

// 生成されるイメージ (簡略化)
struct EmbeddedFiles<'a> {
    info_file: &'a [u8],
    exercise_files: &'a [ExerciseFiles<'a>],
    // ...
}

struct ExerciseFiles<'a> {
    exercise: &'a [u8],
    solution: &'a [u8],
    dir_ind: usize,
}

const EMBEDDED_DATA: EmbeddedFiles = EmbeddedFiles {
    info_file: include_bytes!("path/to/info.toml"),
    exercise_files: &[
        ExerciseFiles { exercise: include_bytes!("path/to/exercise1.rs"), solution: include_bytes!("path/to/solution1.rs"), dir_ind: 0 },
        ExerciseFiles { exercise: include_bytes!("path/to/exercise2.rs"), solution: include_bytes!("path/to/solution2.rs"), dir_ind: 0 },
        // ...
    ],
    // ...
};

メインアプリケーションからは、このEMBEDDED_DATA定数を参照することで、すべての学習コンテンツに直接アクセスできるようになります。これにより、ランタイムでのファイルI/Oが不要になり、起動時間の短縮やデプロイの簡素化が実現されます。

5. 実務に持ち帰れるTips

  1. シンプルなアセット埋め込みにはinclude_bytes! / include_str!: 設定ファイル、Webアセット(HTML/CSS/JS)、画像など、静的な小さなファイルをRustバイナリに含めるには、include_bytes! (バイト配列) や include_str! (文字列) マクロが最も手軽です。これにより、外部ファイルへの依存をなくし、デプロイを簡素化できます。

    // 例: 設定ファイルを埋め込む
    const CONFIG_BYTES: &[u8] = include_bytes!("config.toml");
    
    // 例: HTMLテンプレートを埋め込む
    const HTML_TEMPLATE: &str = include_str!("template.html");
    
  2. 多数の、または動的なファイル構造の埋め込みにはプロシージャルマクロ: Rustlingsのように、ファイル数が多く、特定のパターン(例: exercises/<dir>/<name>.rs)に従ってファイルパスを構築する必要がある場合、手動でinclude_bytes!を記述するのは非現実的です。このような場合に、proc_macroクレートとquoteクレートを組み合わせることで、ファイルシステムをスキャンし、include_bytes!呼び出しを自動生成する強力なツールを作成できます。

  3. 埋め込んだデータへのゼロコピーアクセス: info.tomlのように、埋め込んだ設定ファイルをパースする場合、serdeと#[serde(borrow)]アトリビュートを組み合わせることで、パース時に追加のメモリ確保やコピーをせずに、埋め込んだバイトスライス&[u8]から直接構造体にデシリアライズできます。これにより、ランタイムのパフォーマンスが向上します。

    // #[serde(borrow)]と&'a strの使用例
    #[derive(Deserialize)]
    struct ExerciseInfo<'a> {
        #[serde(borrow)] // 埋め込んだバイト配列から直接&strを借用
        name: &'a str,
        dir: &'a str,
    }
    
  4. セルフコンテインドなバイナリで配布を容易にする: 特にCLIツールや組み込みシステム向けのアプリケーションでは、単一の実行可能ファイルを配布できることは大きなメリットです。ユーザーは単にファイルをコピーするだけでよく、依存関係の管理やインストール手順が大幅に簡素化されます。

6. トレードオフと注意点

7. まとめ

Rustlingsは、その学習体験を支えるために、コンテンツをバイナリ内部に埋め込むという賢明な設計を採用しています。include_bytes!マクロのシンプルさと、プロシージャルマクロによる複雑なファイル構造の自動埋め込みを組み合わせることで、デプロイが容易で、高速に起動するセルフコンテインドなアプリケーションを実現しています。

皆さんのRustプロジェクトで、設定ファイル、テンプレート、静的アセットなどをバイナリに含めたいと考える際には、これらのテクニックをぜひ活用してみてください。特に、単一バイナリでの配布が求められるCLIツールにおいては、非常に効果的なパターンとなるでしょう。

この連載の次のパートでは、anyhowやclapを使った堅牢なエラーハンドリングとCLI設計について深掘りする予定です。お楽しみに!