![[Rust] UEFI環境でTUIアプリを動かす [Ratatui]](https://devio2024-media.developers.io/image/upload/v1749195045/user-gen-eyecatch/gbqby7bk9pxeoxifjydb.png)
[Rust] UEFI環境でTUIアプリを動かす [Ratatui]
Introduction
UEFIはOSとファームウェアとのインターフェースを定義する仕様です。
BIOSに代わるファームウェアインターフェースで、現在のほとんどのPCで標準的に使用されています。
UEFIを使うとBIOSの操作画面をGUI化できたりします。
BIOS画面。自作するときよく見てた。
UEFIでGUI化したBIOS。最近はこんな感じで使えるみたいです。
このUEFIで、TUIのアプリをRustで作成できるcrateがtui-uefiです。
今回はUEFIで実行できるRustアプリを作成してUEFI(Qemu)で動かしてみます。
Ratatui & tui-uefi
Ratatuiはターミナルユーザーインターフェース(TUI)を作成するためのRustクレートです。
テーブルやグラフなどのUIがターミナルで簡単に実装でき、レイアウトも柔軟に指定可能です。
TUIなのでメモリも省エネ設計です。
// テキストwidget作成
use ratatui::{prelude::*, widgets::*};
let paragraph = Paragraph::new("Hello Ratatui!")
.style(Style::default().fg(Color::Yellow))
.block(Block::default().borders(Borders::ALL));
RustでTUIアプリといえばRatatuiというくらいに使われています。
そして、tui-uefiはUEFI環境でTUIアプリケーションを実行するためのcrateです。
tui-uefiではratatuiのAPIを可能な限り維持しつつUEFIの制約に対応しています。
そして、no_std環境での動作も可能になっています。
BIOS/UEFI レベルでのユーティリティアプリをRustで作成することが可能になります。
Environment
- MacBook Pro (14-inch, M3, 2023)
- OS : MacOS 14.5
- Rust : 1.83.0
Setup
まずはHomebrewでqemuをインストールしてUFEIを起動してみます。
brew install qemu
brew install edk2
qemu-system-x86_64 \
-drive if=pflash,format=raw,readonly=on,file=/opt/homebrew/share/qemu/edk2-x86_64-code.fd \
-m 2048 \
-machine q35 \
-cpu qemu64 \
-accel tcg
qemu起動が確認できればOKです。
Ratatui with UEFI
ではUEFI用ツールチェーンをインストールしてtui-uefiでアプリを作成してみましょう。
tui-uefiリポジトリをcloneしてそこに作るのが楽なので、まずはcloneします。
% git clone https://github.com/reubeno/tui-uefi.git
UEFI用ツールチェーンをインストールします。
% rustup target add x86_64-unknown-uefi
#rustup target add x86_64-unknown-uefi --toolchain nightly
UEFIアプリ用プロジェクトを作成します。
% cd tui-uefi
% cargo new my-uefi-app
Cargo.tomlは下記。
ratatui-uefiとterminput-uefiはパス指定にします。
[dependencies]
anyhow = "1.0.98"
ratatui = { version = "0.29.0", default-features = false }
ratatui-uefi = { path = "../ratatui-uefi" }
terminput = { version = "0.4.3", default-features = false }
terminput-uefi = { path = "../terminput-uefi" }
uefi = { version = "0.34.1", features = ["alloc"] }
main.rsを記述します。
シンプルなカウンターアプリを実装(with Claude)。
#![feature(uefi_std)]
use anyhow::Result;
use ratatui::{
Frame, Terminal,
text::Line,
widgets::{Block, Borders, Paragraph},
};
use uefi::proto::console;
fn setup_uefi_crate() {
let system_table = std::os::uefi::env::system_table();
let image_handle = std::os::uefi::env::image_handle();
unsafe {
uefi::table::set_system_table(system_table.as_ptr().cast());
let ih = uefi::Handle::from_ptr(image_handle.as_ptr().cast()).unwrap();
uefi::boot::set_image_handle(ih);
}
}
fn create_ui() -> Result<(
Terminal<ratatui_uefi::UefiOutputBackend>,
terminput_uefi::UefiInputReader,
)> {
let output_handle = uefi::boot::get_handle_for_protocol::<console::text::Output>()?;
let output = uefi::boot::open_protocol_exclusive::<console::text::Output>(output_handle)?;
let input_handle = uefi::boot::get_handle_for_protocol::<console::text::Input>()?;
let input = uefi::boot::open_protocol_exclusive::<console::text::Input>(input_handle)?;
let output_backend = ratatui_uefi::UefiOutputBackend::new(output);
let terminal = Terminal::new(output_backend)?;
let input_reader = terminput_uefi::UefiInputReader::new(input);
Ok((terminal, input_reader))
}
struct CounterApp(i32);
impl CounterApp {
fn new() -> Self { CounterApp(0) }
fn render(&self, frame: &mut Frame) {
let lines = vec![
Line::from(format!("Count: {}", self.0)),
Line::from("↑/↓: +/-1, q: quit"),
];
let status = Paragraph::new(lines)
.block(Block::default().borders(Borders::ALL).title("Counter"))
.centered();
frame.render_widget(status, frame.area());
}
}
fn run() -> Result<()> {
let (mut terminal, mut input_reader) = create_ui()?;
let mut app = CounterApp::new();
terminal.clear()?;
loop {
terminal.draw(|frame| app.render(frame))?;
if let Some(event) = input_reader.read_event()? {
match event {
terminput::Event::Key(terminput::KeyEvent {
code: terminput::KeyCode::Char('q'),
..
}) => break,
terminput::Event::Key(terminput::KeyEvent {
code: terminput::KeyCode::Up,
..
}) => app.0 += 1,
terminput::Event::Key(terminput::KeyEvent {
code: terminput::KeyCode::Down,
..
}) => app.0 -= 1,
_ => {}
}
}
}
Ok(())
}
fn main() {
setup_panic_handler();
setup_uefi_crate();
if let Err(e) = run() {
println!("error: {:?}", e);
}
}
fn setup_panic_handler() {
std::panic::set_hook(Box::new(|info| {
if let Some(location) = info.location() {
println!("Panic at {}:{}", location.file(), location.line());
} else {
println!("Panic occurred but no location information available.");
}
}));
}
nightlyでビルドします。target指定を忘れずに。
% cargo +nightly build --target=x86_64-unknown-uefi
QEMUでアプリを実行してみます。
% qemu-system-x86_64 \
-drive if=pflash,format=raw,readonly=on,file=/opt/homebrew/share/qemu/edk2-x86_64-code.fd \
-drive format=raw,file=fat:rw:target/x86_64-unknown-uefi/debug \
-nographic
UEFIのシェルが起動したら
fs0:
と入力してビルドしたUEFIアプリがある場所へ移動します。
そこでアプリ(my-uefi-app.efi)を実行します。
UEFI Interactive Shell v2.2
EDK II
UEFI v2.70 (EDK II, 0x00010000)
Mapping table
FS0: Alias(s):HD0a1:;BLK1:
PciRoot(0x0)/Pci(0x1,0x1)/Ata(0x0)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)
BLK0: Alias(s):
PciRoot(0x0)/Pci(0x1,0x1)/Ata(0x0)
BLK2: Alias(s):
PciRoot(0x0)/Pci(0x1,0x1)/Ata(0x0)
Press ESC in 4 seconds to skip startup.nsh or any other key to continue.
Shell > fs0:
Shell > my-uefi-app.efi
UEFIでアプリが起動します。
アプリ終了後はctrl + a → xでqemuを終了します。
Summary
今回はUEFIでRustのTUIアプリを実行してみました。
これ以外にもいろいろな環境でRustは動作しますし、
RatatuiについてはRat in the Wild ChallengeというRatatuiを使用していろいろな環境でアプリ構築するチャレンジもあるようで、おもしろそうです。
(PlayStation1や3DSでも動いているらしい)