diff --git a/ceno_emul/tests/data/ceno_rt_io b/ceno_emul/tests/data/ceno_rt_io new file mode 100755 index 000000000..852a85037 Binary files /dev/null and b/ceno_emul/tests/data/ceno_rt_io differ diff --git a/ceno_emul/tests/test_elf.rs b/ceno_emul/tests/test_elf.rs index 7b7b8e13d..74733f89a 100644 --- a/ceno_emul/tests/test_elf.rs +++ b/ceno_emul/tests/test_elf.rs @@ -30,6 +30,52 @@ fn test_ceno_rt_mem() -> Result<()> { Ok(()) } +#[test] +fn test_ceno_rt_io() -> Result<()> { + let program_elf = include_bytes!("./data/ceno_rt_io"); + let mut state = VMState::new_from_elf(CENO_PLATFORM, program_elf)?; + let _steps = run(&mut state)?; + + let all_messages = read_all_messages(&state); + for msg in &all_messages { + print!("{}", String::from_utf8_lossy(msg)); + } + assert_eq!(&all_messages[0], "📜📜📜 Hello, World!\n".as_bytes()); + assert_eq!(&all_messages[1], "🌏🌍🌎\n".as_bytes()); + Ok(()) +} + fn run(state: &mut VMState) -> Result> { state.iter_until_success().collect() } + +const WORD_SIZE: usize = 4; +const INFO_OUT_ADDR: u32 = 0xC000_0000; + +fn read_all_messages(state: &VMState) -> Vec> { + let mut all_messages = Vec::new(); + let mut word_offset = 0; + loop { + let out = read_message(state, word_offset); + if out.is_empty() { + break; + } + word_offset += out.len().div_ceil(WORD_SIZE) as u32 + 1; + all_messages.push(out); + } + all_messages +} + +fn read_message(state: &VMState, word_offset: u32) -> Vec { + let out_addr = ByteAddr(INFO_OUT_ADDR).waddr() + word_offset; + let byte_len = state.peek_memory(out_addr); + let word_len_up = byte_len.div_ceil(4); + + let mut info_out = Vec::with_capacity(WORD_SIZE * word_len_up as usize); + for i in 1..1 + word_len_up { + let value = state.peek_memory(out_addr + i); + info_out.extend_from_slice(&value.to_le_bytes()); + } + info_out.truncate(byte_len as usize); + info_out +} diff --git a/ceno_rt/examples/ceno_rt_io.rs b/ceno_rt/examples/ceno_rt_io.rs new file mode 100644 index 000000000..a25f613d4 --- /dev/null +++ b/ceno_rt/examples/ceno_rt_io.rs @@ -0,0 +1,13 @@ +#![no_main] +#![no_std] + +#[allow(unused_imports)] +use ceno_rt; +use ceno_rt::println; +use core::fmt::Write; + +#[no_mangle] +fn main() { + println!("📜📜📜 Hello, World!"); + println!("🌏🌍🌎"); +} diff --git a/ceno_rt/src/io.rs b/ceno_rt/src/io.rs new file mode 100644 index 000000000..61636e233 --- /dev/null +++ b/ceno_rt/src/io.rs @@ -0,0 +1,74 @@ +use crate::{INFO_OUT_ADDR, WORD_SIZE}; +use core::{cell::Cell, fmt, mem::size_of, slice}; + +static INFO_OUT: IOWriter = IOWriter::new(INFO_OUT_ADDR); + +pub fn info_out() -> &'static IOWriter { + &INFO_OUT +} + +pub struct IOWriter { + cursor: Cell<*mut u32>, +} + +// Safety: Only single-threaded programs are supported. +// TODO: There may be a better way to handle this. +unsafe impl Sync for IOWriter {} + +impl IOWriter { + const fn new(addr: u32) -> Self { + assert!(addr % WORD_SIZE as u32 == 0); + IOWriter { + cursor: Cell::new(addr as *mut u32), + } + } + + pub fn alloc(&self, count: usize) -> &mut [T] { + let byte_len = count * size_of::(); + let word_len = byte_len.div_ceil(WORD_SIZE); + let cursor = self.cursor.get(); + + // Bump the cursor to the next word-aligned address. + self.cursor.set(unsafe { cursor.add(word_len) }); + + // Return a slice of the allocated memory. + unsafe { slice::from_raw_parts_mut(cursor as *mut T, count) } + } + + pub fn write(&self, msg: &[u8]) { + let buf = self.alloc(msg.len()); + buf.copy_from_slice(msg); + } + + pub fn write_frame(&self, msg: &[u8]) { + let word_len = msg.len().div_ceil(WORD_SIZE); + let words: &mut [u32] = self.alloc(1 + word_len); + words[0] = msg.len() as u32; + let bytes = + unsafe { slice::from_raw_parts_mut(words[1..].as_mut_ptr() as *mut u8, msg.len()) }; + bytes.copy_from_slice(msg); + } +} + +impl fmt::Write for &IOWriter { + fn write_str(&mut self, s: &str) -> fmt::Result { + self.write_frame(s.as_bytes()); + Ok(()) + } +} + +mod macros { + #[macro_export] + macro_rules! print { + ($($arg:tt)*) => { + let _ = core::write!($crate::info_out(), $($arg)*); + }; + } + + #[macro_export] + macro_rules! println { + ($($arg:tt)*) => { + let _ = core::writeln!($crate::info_out(), $($arg)*); + }; + } +} diff --git a/ceno_rt/src/lib.rs b/ceno_rt/src/lib.rs index 0ca1c4032..ce79c7f7d 100644 --- a/ceno_rt/src/lib.rs +++ b/ceno_rt/src/lib.rs @@ -1,7 +1,13 @@ -#![no_main] #![no_std] + +mod io; +pub use io::info_out; + use core::arch::{asm, global_asm}; +mod params; +pub use params::*; + #[cfg(not(test))] mod panic_handler { use core::panic::PanicInfo; diff --git a/ceno_rt/src/params.rs b/ceno_rt/src/params.rs new file mode 100644 index 000000000..b8a30b24c --- /dev/null +++ b/ceno_rt/src/params.rs @@ -0,0 +1,3 @@ +pub const WORD_SIZE: usize = 4; + +pub const INFO_OUT_ADDR: u32 = 0xC000_0000; \ No newline at end of file