Skip to main content

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}