[Rust] UEFI環境でTUIアプリを動かす [Ratatui]

[Rust] UEFI環境でTUIアプリを動かす [Ratatui]

Clock Icon2025.06.06

Introduction

UEFIはOSとファームウェアとのインターフェースを定義する仕様です。
BIOSに代わるファームウェアインターフェースで、現在のほとんどのPCで標準的に使用されています。
UEFIを使うとBIOSの操作画面をGUI化できたりします。

BIOS画面。自作するときよく見てた。
bios.png

UEFIでGUI化したBIOS。最近はこんな感じで使えるみたいです。
uefi.png

このUEFIで、TUIのアプリをRustで作成できるcrateがtui-uefiです。
今回はUEFIで実行できるRustアプリを作成してUEFI(Qemu)で動かしてみます。

Ratatui & tui-uefi

ratatui.png

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-1.png

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でアプリが起動します。

qemu-2.png

アプリ終了後はctrl + a → xでqemuを終了します。

Summary

今回はUEFIでRustのTUIアプリを実行してみました。
これ以外にもいろいろな環境でRustは動作しますし、
RatatuiについてはRat in the Wild ChallengeというRatatuiを使用していろいろな環境でアプリ構築するチャレンジもあるようで、おもしろそうです。
(PlayStation1や3DSでも動いているらしい)

References

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.