riscv_emulator/platform.rs
1//! Platform abstraction layer — time, keyboard, and sleep.
2//!
3//! The [`Platform`] trait decouples the emulator core from the host OS.
4//! This lets the same [`crate::emulator::Emulator`] run identically on
5//! POSIX systems, Windows, and inside a WebAssembly sandbox — each with its
6//! own implementation of this trait.
7//!
8//! ## Implementations
9//!
10//! | Target | Type | Notes |
11//! |--------|------|-------|
12//! | Linux / macOS | `posix_platform::PosixPlatform` | Uses `libc` for time, terminal raw mode, and stdin |
13//! | Windows | `windows_platform::WindowsPlatform` | Uses `QueryPerformanceCounter` and `_kbhit`/`_getch` |
14//! | WebAssembly | `wasm::WasmPlatform` (private) | Uses `web_sys::Performance`; keyboard fed via JS callbacks |
15//!
16//! ## Adding a new platform
17//!
18//! Implement [`Platform`] for your type and pass it to
19//! [`crate::emulator::Emulator::run`]. No other changes are needed.
20
21/// Host platform interface used by the emulator for time, I/O, and sleep.
22///
23/// Every method has a clear contract so implementors know exactly what is
24/// expected. The emulator will call these frequently — keep them cheap.
25pub trait Platform {
26 /// Current time in microseconds.
27 ///
28 /// The emulator uses this to compute `elapsed_us` between `step` calls,
29 /// which drives the CLINT timer. The value does not need to be an absolute
30 /// epoch; only the *difference* between two calls matters.
31 fn get_time_microseconds(&self) -> u64;
32
33 /// Put the terminal into raw mode (no echo, no line buffering).
34 ///
35 /// Called once at startup so individual keystrokes reach the emulated
36 /// guest without the host OS buffering them.
37 fn capture_keyboard(&mut self);
38
39 /// Restore the terminal to its original (canonical) mode.
40 ///
41 /// Called when the emulator exits, typically via a `Drop` guard.
42 fn reset_keyboard(&mut self);
43
44 /// Sleep briefly to avoid burning CPU while the guest is in WFI.
45 ///
46 /// The duration is intentionally short (< 1 ms) — the goal is to yield
47 /// the host scheduler, not to sleep for a fixed wall-clock period.
48 fn mini_sleep(&self);
49
50 /// Returns `1` if a key is waiting in stdin, `0` if not, `-1` on EOF.
51 fn is_kb_hit(&mut self) -> i32;
52
53 /// Read one byte from stdin.
54 ///
55 /// Only call this after [`is_kb_hit`] returns `1`. Returns the byte as a
56 /// positive `i32`, or `-1` on error/EOF.
57 ///
58 /// [`is_kb_hit`]: Platform::is_kb_hit
59 fn read_kb_byte(&mut self) -> i32;
60}
61
62// ─────────────────────────────────────────────────────────────────────────────
63// POSIX implementation (Linux / macOS)
64// ─────────────────────────────────────────────────────────────────────────────
65
66#[cfg(all(not(target_os = "windows"), not(target_arch = "wasm32")))]
67pub mod posix_platform {
68 use std::sync::atomic::{AtomicBool, Ordering};
69
70 /// Set to `true` when stdin reaches EOF (e.g. the pipe is closed).
71 pub static IS_EOFD: AtomicBool = AtomicBool::new(false);
72 /// Set to `true` when SIGINT is received.
73 pub static SIGINT_FIRED: AtomicBool = AtomicBool::new(false);
74
75 /// POSIX platform — Linux and macOS.
76 pub struct PosixPlatform;
77
78 impl super::Platform for PosixPlatform {
79 fn get_time_microseconds(&self) -> u64 {
80 let mut tv = libc::timeval {
81 tv_sec: 0,
82 tv_usec: 0,
83 };
84 unsafe {
85 libc::gettimeofday(&mut tv, std::ptr::null_mut());
86 }
87 tv.tv_usec as u64 + tv.tv_sec as u64 * 1_000_000
88 }
89
90 fn capture_keyboard(&mut self) {
91 unsafe {
92 let mut term: libc::termios = std::mem::zeroed();
93 libc::tcgetattr(0, &mut term);
94 term.c_lflag &= !(libc::ICANON | libc::ECHO);
95 libc::tcsetattr(0, libc::TCSANOW, &term);
96 libc::signal(
97 libc::SIGINT,
98 sigint_handler as *const () as libc::sighandler_t,
99 );
100 }
101 }
102
103 fn reset_keyboard(&mut self) {
104 unsafe {
105 let mut term: libc::termios = std::mem::zeroed();
106 libc::tcgetattr(0, &mut term);
107 term.c_lflag |= libc::ICANON | libc::ECHO;
108 libc::tcsetattr(0, libc::TCSANOW, &term);
109 }
110 }
111
112 fn mini_sleep(&self) {
113 unsafe {
114 libc::usleep(500);
115 }
116 }
117
118 fn is_kb_hit(&mut self) -> i32 {
119 if IS_EOFD.load(Ordering::SeqCst) {
120 return -1;
121 }
122 let mut n: libc::c_int = 0;
123 unsafe {
124 libc::ioctl(0, libc::FIONREAD, &mut n);
125 }
126 if n == 0 {
127 // A zero-length write is a cheap way to test if the fd is still open.
128 let r = unsafe { libc::write(libc::STDIN_FILENO, std::ptr::null(), 0) };
129 if r != 0 {
130 IS_EOFD.store(true, Ordering::SeqCst);
131 return -1;
132 }
133 }
134 if n != 0 {
135 1
136 } else {
137 0
138 }
139 }
140
141 fn read_kb_byte(&mut self) -> i32 {
142 if IS_EOFD.load(Ordering::SeqCst) {
143 return 0xffff_ffff_u32 as i32;
144 }
145 let mut c: u8 = 0;
146 let r = unsafe {
147 libc::read(
148 libc::STDIN_FILENO,
149 &mut c as *mut u8 as *mut libc::c_void,
150 1,
151 )
152 };
153 if r > 0 {
154 c as i32
155 } else {
156 -1
157 }
158 }
159 }
160
161 extern "C" fn sigint_handler(_sig: libc::c_int) {
162 SIGINT_FIRED.store(true, Ordering::SeqCst);
163 unsafe {
164 libc::exit(0);
165 }
166 }
167}
168
169// ─────────────────────────────────────────────────────────────────────────────
170// Windows implementation
171// ─────────────────────────────────────────────────────────────────────────────
172
173#[cfg(all(target_os = "windows", not(target_arch = "wasm32")))]
174pub mod windows_platform {
175 /// Windows platform using `QueryPerformanceCounter` and CRT console APIs.
176 ///
177 /// Arrow keys arrive from `_getch` as two-byte sequences (`224` + code).
178 /// This type translates them to ANSI escape sequences so the guest sees
179 /// the standard `ESC [ A` / `ESC [ B` / etc. that terminal apps expect.
180 pub struct WindowsPlatform {
181 escape_seq: i32,
182 }
183
184 impl Default for WindowsPlatform {
185 fn default() -> Self {
186 Self::new()
187 }
188 }
189
190 impl WindowsPlatform {
191 pub fn new() -> Self {
192 WindowsPlatform { escape_seq: 0 }
193 }
194 }
195
196 impl super::Platform for WindowsPlatform {
197 fn get_time_microseconds(&self) -> u64 {
198 use windows::Win32::System::Performance::*;
199 let mut freq: i64 = 0;
200 let mut li: i64 = 0;
201 unsafe {
202 QueryPerformanceFrequency(&mut freq).unwrap();
203 QueryPerformanceCounter(&mut li).unwrap();
204 }
205 (li as u64 * 1_000_000) / freq as u64
206 }
207
208 fn capture_keyboard(&mut self) {}
209 fn reset_keyboard(&mut self) {}
210
211 fn mini_sleep(&self) {
212 unsafe {
213 windows::Win32::System::Threading::Sleep(1);
214 }
215 }
216
217 fn is_kb_hit(&mut self) -> i32 {
218 extern "C" {
219 fn _kbhit() -> i32;
220 }
221 unsafe { _kbhit() }
222 }
223
224 fn read_kb_byte(&mut self) -> i32 {
225 extern "C" {
226 fn _getch() -> i32;
227 }
228
229 // Second byte of an arrow-key escape sequence.
230 if self.escape_seq == 1 {
231 self.escape_seq += 1;
232 return '[' as i32;
233 }
234
235 let r = unsafe { _getch() };
236
237 if self.escape_seq != 0 {
238 self.escape_seq = 0;
239 return match r {
240 72 => 'A' as i32, // ↑
241 80 => 'B' as i32, // ↓
242 75 => 'D' as i32, // ←
243 77 => 'C' as i32, // →
244 71 => 'H' as i32, // Home
245 79 => 'F' as i32, // End
246 _ => r,
247 };
248 }
249
250 match r {
251 13 => 10, // CR → LF
252 224 => {
253 self.escape_seq = 1;
254 27
255 } // start of arrow-key sequence
256 _ => r,
257 }
258 }
259 }
260}