use alloc::boxed::Box;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use crate::core::device::{Device, DeviceClass, PropertySpec, RealizeCtx, ResetKind};
use crate::core::error::{Error, Result};
use crate::core::props::{Props, ValueKind};
use crate::core::space::{AddressSpace, MemAttrs, RequesterId};
use crate::core::sync::{LockRank, Mutex};
use crate::machine::realize::{BindCtx, Instance};
pub const CLASS_NAME: &str = "riscv.loader";
pub const DEFAULT_ADDR: u64 = 0x8000_0000;
#[derive(Debug, Default)]
struct Binding {
space: Option<Arc<AddressSpace>>,
requester: RequesterId,
error: Option<String>,
loads: u64,
}
#[derive(Debug)]
pub struct Loader {
image: Arc<[u8]>,
addr: u64,
space_name: Option<String>,
binding: Mutex<Binding>,
}
impl Loader {
pub fn new(props: &Props) -> Result<Loader> {
let mut r = props.reader();
let image = r.require_media("image")?.to_bytes();
let addr = r.or_addr("addr", DEFAULT_ADDR)?;
let space = r.optional_str("space")?.map(ToString::to_string);
r.finish()?;
Ok(Loader::from_image(image, addr, space))
}
#[must_use]
pub fn from_image(image: impl Into<Arc<[u8]>>, addr: u64, space: Option<String>) -> Loader {
Loader {
image: image.into(),
addr,
space_name: space,
binding: Mutex::with_rank(LockRank::DEVICE, Binding::default()),
}
}
#[must_use]
pub fn addr(&self) -> u64 {
self.addr
}
#[must_use]
pub fn len(&self) -> u64 {
self.image.len() as u64
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.image.is_empty()
}
#[must_use]
pub fn last_error(&self) -> Option<String> {
self.binding.lock().error.clone()
}
#[must_use]
pub fn loads(&self) -> u64 {
self.binding.lock().loads
}
pub fn load_into(&self, space: &AddressSpace, requester: RequesterId) -> Result<()> {
if self.image.is_empty() {
return Ok(());
}
let attrs = MemAttrs::DEFAULT
.with_requester(requester)
.with_privileged(true);
const CHUNK: usize = 4096;
let mut at = self.addr;
for piece in self.image.chunks(CHUNK) {
space
.write_bytes(at, piece, attrs)
.map_err(|e| Error::Config {
at: CLASS_NAME.to_string(),
message: format!(
"cannot write the image at {at:#x} in space `{}`: {e} — the machine \
needs {} byte(s) of memory from {:#x}",
space.name(),
self.image.len(),
self.addr
),
})?;
at += piece.len() as u64;
}
Ok(())
}
}
pub static CLASS: DeviceClass = DeviceClass {
name: CLASS_NAME,
version: 1,
summary: "writes a media image into guest memory at reset: firmware, a kernel, a ramdisk",
properties: &[
PropertySpec {
name: "image",
kind: ValueKind::Media,
required: true,
summary: "the image, as the name of a media slot (`image = \"firmware\"`)",
},
PropertySpec {
name: "addr",
kind: ValueKind::Addr,
required: false,
summary: "where the first byte lands (default 0x80000000)",
},
PropertySpec {
name: "space",
kind: ValueKind::Str,
required: false,
summary: "which address space to write into, if not the one the object declares",
},
],
construct: |props| Ok(Box::new(Loader::new(props)?)),
};
impl Device for Loader {
fn class(&self) -> &'static DeviceClass {
&CLASS
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
Ok(())
}
fn reset(&self, _kind: ResetKind) {
let (space, requester) = {
let binding = self.binding.lock();
(binding.space.clone(), binding.requester)
};
let Some(space) = space else {
return;
};
let result = self.load_into(&space, requester);
let mut binding = self.binding.lock();
binding.loads += 1;
binding.error = result.err().map(|e| e.to_string());
}
}
impl Instance for Loader {
fn bind(&self, ctx: &BindCtx<'_>) -> Result<()> {
let space = match &self.space_name {
Some(name) => ctx.space_named(name).ok_or_else(|| Error::Config {
at: ctx.path().to_string(),
message: format!("no address space named `{name}`"),
})?,
None => ctx.space().ok_or_else(|| Error::Config {
at: ctx.path().to_string(),
message: String::from(
"a loader needs an address space to write into (`space = mem`)",
),
})?,
};
self.load_into(space, ctx.requester())?;
let mut binding = self.binding.lock();
binding.space = Some(Arc::clone(space));
binding.requester = ctx.requester();
Ok(())
}
}
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
bindings.bind(CLASS_NAME, |props| Ok(Arc::new(Loader::new(props)?)))
}
pub fn register(registry: &mut crate::core::Registry) -> Result<()> {
registry.add(&CLASS)
}
#[must_use]
pub fn schema() -> crate::machine::validate::ClassSchema {
use crate::machine::validate::{ClassSchema, PropSchema};
ClassSchema::new(CLASS_NAME)
.prop(PropSchema::new("image", ValueKind::Media).required())
.prop(PropSchema::new("addr", ValueKind::Addr))
.prop(PropSchema::new("space", ValueKind::Str))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::space::{RamStore, Region};
use crate::core::value::Width;
fn space_with_ram(len: u64) -> Arc<AddressSpace> {
let space = AddressSpace::new("mem", 64);
let ram = Arc::new(RamStore::new(len));
space
.topology()
.map(Region::ram("ram", ram), DEFAULT_ADDR)
.expect("a fresh space");
Arc::new(space)
}
fn peek(space: &AddressSpace, addr: u64) -> u8 {
space
.read(addr, Width::U8, MemAttrs::DEBUG)
.expect("mapped") as u8
}
#[test]
fn the_image_lands_where_it_was_aimed() {
let space = space_with_ram(0x1000);
let loader = Loader::from_image(&b"\x01\x02\x03"[..], DEFAULT_ADDR, None);
loader
.load_into(&space, RequesterId::ANONYMOUS)
.expect("three bytes fit");
assert_eq!(peek(&space, DEFAULT_ADDR), 1);
assert_eq!(peek(&space, DEFAULT_ADDR + 2), 3);
assert_eq!(loader.len(), 3);
}
#[test]
fn an_image_that_does_not_fit_says_where_and_how_big() {
let space = space_with_ram(0x100);
let loader = Loader::from_image(alloc::vec![0xaau8; 0x400], DEFAULT_ADDR, None);
let e = loader
.load_into(&space, RequesterId::ANONYMOUS)
.expect_err("1 KiB into 256 bytes")
.to_string();
assert!(e.contains("1024"), "{e}");
assert!(e.contains("80000000"), "{e}");
}
#[test]
fn an_empty_image_loads_nothing_and_is_not_an_error() {
let space = space_with_ram(0x100);
let loader = Loader::from_image(&[][..], DEFAULT_ADDR, None);
assert!(loader.is_empty());
loader
.load_into(&space, RequesterId::ANONYMOUS)
.expect("nothing to do");
assert_eq!(peek(&space, DEFAULT_ADDR), 0);
}
#[test]
fn a_reset_writes_the_image_in_again() {
let space = space_with_ram(0x1000);
let loader = Loader::from_image(&b"\xde\xad"[..], DEFAULT_ADDR, None);
loader.binding.lock().space = Some(Arc::clone(&space));
space
.write(DEFAULT_ADDR, Width::U8, 0, MemAttrs::DEFAULT)
.unwrap();
assert_eq!(peek(&space, DEFAULT_ADDR), 0);
loader.reset(ResetKind::Cold);
assert_eq!(peek(&space, DEFAULT_ADDR), 0xde);
assert_eq!(loader.loads(), 1);
assert_eq!(loader.last_error(), None);
}
#[test]
fn a_loader_with_no_space_yet_resets_without_complaint() {
let loader = Loader::from_image(&b"x"[..], DEFAULT_ADDR, None);
loader.reset(ResetKind::Cold);
assert_eq!(loader.loads(), 0);
}
#[test]
fn a_media_slot_is_required() {
let e = Loader::new(&Props::new())
.expect_err("no image")
.to_string();
assert!(e.contains("image") && e.contains("media"), "{e}");
}
}