カーネルのRust: 安全でない基盤の上に築く安全な抽象化
前提知識
- ›第1回: アーキテクチャとディレクトリマップ
- ›第2回: ブートシーケンスと初期化
- ›Rustの基礎(所有権、トレイト、no_std、unsafe)
- ›第4回で解説したCの関数ポインタ構造体の理解
カーネルのRust: 安全でない基盤の上に築く安全な抽象化
このシリーズを通じて、ある共通のパターンが繰り返し登場してきました。sched_class、file_operations、inode_operations、io_issue_def といった、関数ポインタで埋まったC構造体です。これらは実行時ポリモーフィズムの vtable として機能しています。動作はしますが、根本的なリスクも抱えています。関数ポインタが NULL のままでもファイルシステムを止める手段はなく、ロックの解放を忘れたドライバを検出する術もなく、use-after-free をコンパイル時に捉えることもできません。Rust-for-Linux イニシアチブは、こうした問題を解消することを目指しています。ただしカーネルを書き直すのではなく、既存のCパターンを安全なRust抽象化でラップし、バグのクラスごと根絶するというアプローチです。
第1回で触れたように、Rustサポートはトップレベルの Kbuild に書かれた obj-$(CONFIG_RUST) += rust/ によって条件付きでコンパイルされます。この記事では、その rust/ ディレクトリに何が含まれているか、CバインディングがどのようにRust APIへと流れ込むか、そして実際のGPUドライバがこれらの抽象化をどう活用しているかを見ていきます。
kernel クレートの構造
kernel クレートは、カーネル内のRustコード全体を支える中心的なライブラリです。lib.rs を読むと、CカーネルのKconfig駆動コンパイルモデルを反映したアーキテクチャが浮かび上がります。
#![no_std]
...
#[cfg(not(CONFIG_RUST))]
compile_error!("Missing kernel configuration for conditional compilation");
extern crate self as kernel;
pub use ffi;
pub mod alloc;
#[cfg(CONFIG_AUXILIARY_BUS)]
pub mod auxiliary;
#[cfg(CONFIG_BLOCK)]
pub mod block;
pub mod clk;
#[cfg(CONFIG_CONFIGFS_FS)]
pub mod configfs;
pub mod device;
pub mod driver;
#[cfg(CONFIG_DRM = "y")]
pub mod drm;
pub mod error;
pub mod fs;
#[cfg(CONFIG_I2C = "y")]
pub mod i2c;
pub mod init;
pub mod irq;
#[cfg(CONFIG_NET)]
pub mod net;
#[cfg(CONFIG_PCI)]
pub mod pci;
pub mod platform;
pub mod prelude;
pub mod sync;
pub mod task;
...
graph TD
subgraph "kernel crate (rust/kernel/)"
lib["lib.rs<br/>#![no_std]"]
lib --> prelude["prelude.rs"]
lib --> alloc["alloc/"]
lib --> device["device.rs"]
lib --> driver["driver.rs"]
lib --> drm["drm/<br/>#[cfg(CONFIG_DRM)]"]
lib --> auxiliary["auxiliary.rs<br/>#[cfg(CONFIG_AUXILIARY_BUS)]"]
lib --> pci["pci.rs<br/>#[cfg(CONFIG_PCI)]"]
lib --> init["init.rs"]
lib --> sync["sync/"]
lib --> fs["fs.rs"]
lib --> net["net/<br/>#[cfg(CONFIG_NET)]"]
end
#[cfg(CONFIG_*)] 属性は、Cの #ifdef CONFIG_* に相当するRust版です。KconfigシンボルはRustコンパイラへ --cfg フラグとして渡されるため、#[cfg(CONFIG_DRM = "y")] は DRM がビルトイン(ローダブルモジュールとしてではない)の場合にのみ DRM モジュールをビルドに含めます。この密な統合により、Rustコードも3,000万行のCコードベース全体を管理する条件付きコンパイルシステムに参加できます。
14行目の #![no_std] が示す通り、標準ライブラリは使用しません。カーネルは独自のアロケータ、独自の文字列型、独自の同期プリミティブを持っています。カーネル内のRustはフリースタンディング環境で動作します。
Prelude と共通抽象化
すべてのRustカーネルモジュールは use kernel::prelude::* から始まります。
pub use crate::alloc::{flags::*, Box, KBox, KVBox, KVVec, KVec, VBox, VVec, Vec};
pub use macros::{export, fmt, kunit_tests, module, vtable};
pub use pin_init::{init, pin_data, pin_init, pinned_drop, InPlaceWrite, Init, PinInit, Zeroable};
pub use super::error::{code::*, Error, Result};
pub use super::{str::CStrExt as _, ThisModule};
pub use super::{pr_alert, pr_crit, pr_debug, pr_emerg, pr_err, pr_info, pr_notice, pr_warn};
主要な抽象化は以下の通りです。
KVec、KBox— 標準ライブラリのアロケータではなくGFP_KERNELフラグを使うカーネル対応アロケータError/Result— カーネルのエラーコード(-ENOMEM、-EINVAL)をRustの型システムでラップしたものThisModule— 現在のカーネルモジュールへの参照(所有権追跡のために使用)pr_info!など — フォーマットチェック付きのprintk()ラッパーpin_data、pin_init— ピン留め(移動不可)型を安全に初期化するマクロvtable— Rustトレイトと Cの関数ポインタ構造体を橋渡しするマクロ
ヒント: 新しいRustカーネルモジュールを書くなら、まず
use kernel::prelude::*とmodule!マクロから始めましょう。この2つだけで、基本的なモジュールライフサイクル管理に必要なものはすべて揃います。
C → Rust バインディングパイプライン
RustカーネルコードはCの関数を直接呼び出しません。代わりに、階層化されたパイプラインを経由します。
flowchart TD
A["C Headers<br/>(include/linux/*.h)"] -->|bindgen| B["bindings_generated.rs<br/>(raw unsafe FFI)"]
B --> C["rust/bindings/lib.rs<br/>(raw module, re-exported)"]
C --> D["rust/kernel/*.rs<br/>(safe Rust abstractions)"]
D --> E["Kernel Modules<br/>(drivers/gpu/drm/nova/, etc.)"]
style A fill:#f9f,stroke:#333
style B fill:#fbb,stroke:#333
style C fill:#fbb,stroke:#333
style D fill:#bfb,stroke:#333
style E fill:#bfb,stroke:#333
レイヤー1: Raw バインディング — bindgen がCヘッダを処理し、生の extern "C" 関数宣言と型定義を含む bindings_generated.rs を生成します。これらは unsafe であり、使い勝手も良くありません。
rust/bindings/lib.rs は、生成されたコードを適切な #![allow] 属性でラップします。
//! This crate may not be directly used. If you need a kernel C API that is
//! not ported or wrapped in the `kernel` crate, then do so first instead of
//! using this crate.
#![no_std]
#![allow(clippy::all, missing_docs, non_camel_case_types, ...)]
mod bindings_raw {
...
include!(concat!(env!("OBJTREE"), "/rust/bindings/bindings_generated.rs"));
}
このコメントはポリシーの表明です。ドライバは bindings を直接使用してはなりません。生のバインディングはあくまで中間表現です。
レイヤー2: 安全な抽象化 — rust/kernel/ 内のモジュールが、生のバインディングを安全なRust型でラップします。生の struct device * は RAII クリーンアップ付きの Device になり、生のスピンロックは型安全なロックを持つ SpinLock<T> になります。Rustの所有権システムがもっとも価値を発揮するのはここです。ライフタイムとロックのルールがコンパイル時に強制されます。
レイヤー3: ドライバ — 実際のカーネルドライバは安全な抽象化のみを使います。生のCポインタではなく、Rustのトレイトと型だけを扱います。
rust/Makefile がこのパイプライン全体を統制しています。
obj-$(CONFIG_RUST) += core.o compiler_builtins.o ffi.o
always-$(CONFIG_RUST) += bindings/bindings_generated.rs bindings/bindings_helpers_generated.rs
obj-$(CONFIG_RUST) += bindings.o pin_init.o kernel.o
#[vtable] とピン初期化パターン
#[vtable] マクロ
第4回では、Cが struct file_operations と関数ポインタを使ってポリモーフィズムを実現する様子を見ました。Rustにおける答えが #[vtable] 属性マクロです。トレイト実装にこれを付けると、対応するCの操作構造体が自動生成されます。実装されていないオプション操作には自動的に NULL が埋められ、has_* フラグも適切に設定されます。
次のように書くと:
#[vtable]
impl drm::Driver for NovaDriver {
type Data = NovaData;
type File = File;
type Object = gem::Object<NovaObject>;
const INFO: drm::DriverInfo = INFO;
...
}
#[vtable] マクロは、Rustのトレイトメソッドにコールバックする関数ポインタを持つ C互換の struct drm_driver を生成し、C-Rust間のFFI境界も自動的に処理します。実装していない必須メソッドはコンパイルエラーになります。実装していないオプションメソッドは NULL 関数ポインタになり、対応する HAS_* 定数が false に設定されます。
ピン初期化
カーネルのデータ構造には、自己参照(リストヘッドが構造体自身を指すなど)だったり、固定されたメモリアドレスに置かれなければならない(ハードウェアに登録される構造体など)ものが頻繁にあります。Cではその場で初期化して一切移動しません。しかしRustのムーブセマンティクスはこれを困難にします。
#[pin_data] と try_pin_init! マクロがこの問題を解決します。
#[pin_data]
pub(crate) struct NovaData {
pub(crate) adev: ARef<auxiliary::Device>,
}
#[pin_data] 属性は、構造的にピン留めされている(移動してはならない)フィールドを指定します。try_pin_init! はインプレース初期化を提供し、値を最終的なメモリ位置に直接構築するため、一度も移動しません。これは、構築中にCサブシステムへ自分自身を登録するカーネルオブジェクトにとって不可欠な仕組みです。
classDiagram
class RustTrait["trait drm::Driver"] {
+type Data
+type File
+const INFO: DriverInfo
}
class VtableMacro["#[vtable] generates"] {
+C struct drm_driver
+fn pointers → Rust methods
+NULL for optional ops
}
class PinInit["#[pin_data] + try_pin_init!"] {
+In-place construction
+No move after init
+Self-referential safe
}
RustTrait --> VtableMacro : "attribute macro"
PinInit --> RustTrait : "initializes pinned data"
Nova DRMドライバの解説
Nova は NVIDIA GPU 向けの実在のRustカーネルドライバです。おもちゃのサンプルではなく、Rustドライバモデル全体を体現するツリー内ドライバとして存在しています。drivers/gpu/drm/nova/ に置かれています。
モジュールのルート:
mod driver;
mod file;
mod gem;
use crate::driver::NovaDriver;
kernel::module_auxiliary_driver! {
type: NovaDriver,
name: "Nova",
authors: ["Danilo Krummrich"],
description: "Nova GPU driver",
license: "GPL v2",
}
module_auxiliary_driver! マクロは、Cの module_init() / module_exit() に相当するRust版です。第2回で説明した initcall、モジュールのメタデータ、登録・登録解除コードを生成します。type: NovaDriver によって、ドライバを実装している型がマクロに伝わります。
ドライバの実装:
drivers/gpu/drm/nova/driver.rs#L1-L79
pub(crate) struct NovaDriver {
drm: ARef<drm::Device<Self>>,
}
impl auxiliary::Driver for NovaDriver {
type IdInfo = ();
const ID_TABLE: auxiliary::IdTable<Self::IdInfo> = &AUX_TABLE;
fn probe(adev: &auxiliary::Device<Core>, _info: &Self::IdInfo) -> impl PinInit<Self, Error> {
let data = try_pin_init!(NovaData { adev: adev.into() });
let drm = drm::Device::<Self>::new(adev.as_ref(), data)?;
drm::Registration::new_foreign_owned(&drm, adev.as_ref(), 0)?;
Ok(Self { drm })
}
}
#[vtable]
impl drm::Driver for NovaDriver {
type Data = NovaData;
type File = File;
type Object = gem::Object<NovaObject>;
const INFO: drm::DriverInfo = INFO;
kernel::declare_drm_ioctls! {
(NOVA_GETPARAM, drm_nova_getparam, ioctl::RENDER_ALLOW, File::get_param),
(NOVA_GEM_CREATE, drm_nova_gem_create, ioctl::AUTH | ioctl::RENDER_ALLOW, File::gem_create),
(NOVA_GEM_INFO, drm_nova_gem_info, ioctl::AUTH | ioctl::RENDER_ALLOW, File::gem_info),
}
}
sequenceDiagram
participant K as Kernel (C)
participant R as Rust Abstractions
participant N as Nova Driver
K->>R: auxiliary bus probe callback
R->>N: NovaDriver::probe(adev, info)
N->>N: try_pin_init!(NovaData { ... })
N->>R: drm::Device::new(adev, data)
R->>K: C drm_dev_alloc() via FFI
K-->>R: struct drm_device*
R-->>N: ARef<drm::Device>
N->>R: drm::Registration::new_foreign_owned()
R->>K: C drm_dev_register() via FFI
K-->>R: Success
R-->>N: Ok(Self { drm })
各パターンを順に追ってみましょう。
impl auxiliary::Driver— Cのstruct auxiliary_driverをラップしたRustトレイト。カーネルがこのドライバをデバイスにマッチングするとprobe関数が呼ばれます。try_pin_init!— 上で述べた通り、NovaDataをインプレースで初期化します。drm::Device::<Self>::new()— DRMデバイスを作成します。Cのdrm_dev_alloc()を、自動クリーンアップ付きの型安全なRustラッパーでくるんでいます。drm::Registration::new_foreign_owned()— DRMサブシステムにデバイスを登録します(Cのdrm_dev_register()に対応)。#[vtable] impl drm::Driver— DRMドライバの操作を宣言します。declare_drm_ioctls!マクロが ioctl ディスパッチテーブルを生成します。
このコードの美しさは、存在しないものにあります。手動のエラークリーンアップパス、失敗時の登録解除忘れ、NULL 関数ポインタのリスク——これらがすべて消えています。Rustの型システムが Drop を通じてリソースのクリーンアップを担い、? 演算子が自動アンワインドを伴いながらエラーを伝播します。
Kbuild 統合と今後の展望
統合ポイントはトップレベル Kbuild のたった1行です。
obj-$(CONFIG_RUST) += rust/
CONFIG_RUST=y の場合、ビルドシステムは次の順序で動作します。
core、compiler_builtins、ffiクレートをコンパイルするbindgenを実行してbindings_generated.rsを生成するkernelクレートをコンパイルする- 設定で有効化されたRustドライバをコンパイルする
現在のカーネル(lib.rs の19〜59行目)は、arbitrary_self_types、derive_coerce_pointee などいくつかの unstable なRust機能を必要とします。一部はすでに新しいバージョンのRustで安定化されており、リリースのたびにリストは縮小しています。長期的な目標は stable なRustのみで動作させることです。標準の rustc をインストールすれば誰でもカーネルのRust開発に参加できる環境を目指しています。
Rust-for-Linux イニシアチブはカーネルを書き直そうとしているのではありません。新しいコード、とりわけドライバ——コードベースの60%を占め、バグの主な発生源でもある部分——に向けた並行の道を作ろうとしています。デバイスモデル、DRM、ファイルシステム、ネットワークへと抽象化のカバレッジが広がるにつれ、より多くの新しいドライバをRustで書けるようになります。メモリ安全性の保証を享受しながら、このシリーズ全体で見てきた既存のCサブシステムとシームレスに連携できます。
ヒント: Rustサポート付きでカーネルをビルドするには、
rustc、bindgen、rustfmtのインストールが必要です。make LLVM=1 rustavailableを実行してツールチェーンが対応しているか確認した上で、.configでCONFIG_RUST=yを有効にしてください。
シリーズのまとめ
6本の記事を通じて、Linuxカーネルを3,000万行のディレクトリ構造から出発し、ブート初期化、プロセススケジューリング、syscallエントリ、非同期I/O、そしてRustの安全性レイヤーまで順に追いかけてきました。パターンは繰り返します。分散登録のためのリンカセクションのトリック、ポリモーフィズムのためのC関数ポインタ構造体、パフォーマンスのためのキャッシュライン意識のデータレイアウト、オーバーヘッドを排除するための共有メモリ。これらは孤立したテクニックではなく、世界で最も広く使われているOSのアーキテクチャの語彙です。これらを理解することで、カーネルソースのどの部分を読むときにも通じる確かな基礎が身につきます。