#![expect(
unsafe_code,
reason = "the emulator is C: the four entry points it calls, the five it exports, and the host it lends"
)]
use std::any::Any;
use std::ffi::{CString, c_char, c_int, c_void};
use std::ptr;
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, AtomicPtr, Ordering};
use super::{
Access, AccessKind, Alias, Driver, EmulatorCommand, Error, Finished, Host, InterruptLine,
Memory, Start, Stop, StopReason,
};
use crate::device::Level;
use crate::machine::Board;
use crate::session::Session;
use crate::soc::{Region, RegionKind, Width};
use crate::time::VirtualNanos;
const NO_DEADLINE_NS: u64 = i64::MAX as u64;
const RIGLESS_HOST_ABI_VERSION: u64 = 1;
#[repr(C)]
struct CHost {
abi_version: u64,
opaque: *mut c_void,
read_memory: Option<unsafe extern "C" fn(*mut c_void, u64, u32) -> u64>,
write_memory: Option<unsafe extern "C" fn(*mut c_void, u64, u32, u64)>,
set_interrupt: Option<unsafe extern "C" fn(*mut c_void, u32, u32)>,
request_reset: Option<unsafe extern "C" fn(*mut c_void)>,
}
const _: () = {
assert!(size_of::<CHost>() == 6 * size_of::<u64>());
assert!(std::mem::offset_of!(CHost, abi_version) == 0);
assert!(std::mem::offset_of!(CHost, opaque) == 8);
assert!(std::mem::offset_of!(CHost, read_memory) == 16);
assert!(std::mem::offset_of!(CHost, write_memory) == 24);
assert!(std::mem::offset_of!(CHost, set_interrupt) == 32);
assert!(std::mem::offset_of!(CHost, request_reset) == 40);
};
unsafe extern "C" {
fn qemu_init(argc: c_int, argv: *mut *mut c_char);
fn qemu_main_loop() -> c_int;
fn qemu_cleanup(status: c_int);
fn bql_unlock();
fn replay_mutex_unlock();
}
struct Lent(*const CHost);
impl Lent {
fn host(&self) -> &CHost {
unsafe { &*self.0 }
}
}
impl Memory for Lent {
fn read(&mut self, address: u64, width: Width) -> Result<u64, Error> {
let host = self.host();
let read_memory = host
.read_memory
.expect("checked non-null in rigless_core_new");
Ok(unsafe { read_memory(host.opaque, address, u32::from(width.bits())) })
}
fn write(&mut self, address: u64, width: Width, value: u64) -> Result<(), Error> {
let host = self.host();
let write_memory = host
.write_memory
.expect("checked non-null in rigless_core_new");
unsafe { write_memory(host.opaque, address, u32::from(width.bits()), value) };
Ok(())
}
}
impl Host for Lent {
fn set_interrupt(&mut self, line: InterruptLine, level: Level) {
let host = self.host();
let set_interrupt = host
.set_interrupt
.expect("checked non-null in rigless_core_new");
unsafe {
set_interrupt(
host.opaque,
u32::from(line.index()),
u32::from(level == Level::High),
);
}
}
fn request_reset(&mut self) {
let host = self.host();
let request_reset = host
.request_reset
.expect("checked non-null in rigless_core_new");
unsafe { request_reset(host.opaque) };
}
}
trait Driven: Send {
fn first_deadline(&mut self) -> VirtualNanos;
fn access(&mut self, access: Access, host: &mut dyn Host) -> (u64, VirtualNanos);
fn stop(&mut self, stop: Stop, host: &mut dyn Host) -> Option<VirtualNanos>;
fn into_any(self: Box<Self>) -> Box<dyn Any>;
}
struct Run<D: Board + ?Sized, R: Driver<D>> {
session: Session<D>,
driver: R,
last: Option<Stop>,
}
impl<D: Board + ?Sized + 'static, R: Driver<D> + 'static> Driven for Run<D, R> {
fn first_deadline(&mut self) -> VirtualNanos {
self.session.next_deadline()
}
fn access(&mut self, access: Access, host: &mut dyn Host) -> (u64, VirtualNanos) {
let (value, next) = self.session.access(access, host);
self.driver.on_access(&access, value);
(value, next)
}
fn stop(&mut self, stop: Stop, host: &mut dyn Host) -> Option<VirtualNanos> {
let next = self.session.stop(stop, host);
if !self.session.reached(&stop) {
return Some(next);
}
self.last = Some(stop);
let until = self.driver.on_stop(&mut self.session, stop)?;
Some(
self.session
.resume(until, stop.at, stop.instruction_count, host),
)
}
fn into_any(self: Box<Self>) -> Box<dyn Any> {
self
}
}
struct Core {
driven: Box<dyn Driven>,
host: Lent,
}
static PREPARED: Mutex<Option<Box<dyn Driven>>> = Mutex::new(None);
static ACTIVE: AtomicPtr<Core> = AtomicPtr::new(ptr::null_mut());
static RAN: AtomicBool = AtomicBool::new(false);
static ENTERED: AtomicBool = AtomicBool::new(false);
#[unsafe(no_mangle)]
extern "C" fn rigless_core_new(host: *const CHost, first_deadline_ns: *mut u64) -> *mut c_void {
let abi_version = unsafe { host.cast::<u64>().read() };
assert!(
abi_version == RIGLESS_HOST_ABI_VERSION,
"the fork's struct rigless_host is version {abi_version}, this build wants \
{RIGLESS_HOST_ABI_VERSION}"
);
let host_ref = unsafe { &*host };
assert!(
host_ref.read_memory.is_some()
&& host_ref.write_memory.is_some()
&& host_ref.set_interrupt.is_some()
&& host_ref.request_reset.is_some(),
"struct rigless_host carries a null callback"
);
let Some(mut driven) = PREPARED
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take()
else {
return ptr::null_mut();
};
let first = driven.first_deadline();
unsafe { *first_deadline_ns = first.0 };
let core = Box::into_raw(Box::new(Core {
driven,
host: Lent(host),
}));
ACTIVE.store(core, Ordering::Release);
core.cast()
}
fn width_of(bits: u32) -> Width {
match bits {
8 => Width::W8,
16 => Width::W16,
32 => Width::W32,
64 => Width::W64,
other => panic!("the proxy asked for a {other} bit access"),
}
}
struct NotReentered;
impl NotReentered {
fn enter() -> Self {
assert!(
!ENTERED.swap(true, Ordering::AcqRel),
"the fork called back into the core while already inside a call to it"
);
Self
}
}
impl Drop for NotReentered {
fn drop(&mut self) {
ENTERED.store(false, Ordering::Release);
}
}
#[unsafe(no_mangle)]
extern "C" fn rigless_core_access(
core: *mut c_void,
writing: u32,
address: u64,
width_bits: u32,
value: u64,
now_ns: u64,
instruction_count: u64,
program_counter: u64,
next_deadline_ns: *mut u64,
) -> u64 {
let _guard = NotReentered::enter();
let core = unsafe { &mut *core.cast::<Core>() };
let access = Access {
kind: if writing == 0 {
AccessKind::Read
} else {
AccessKind::Write
},
address,
width: width_of(width_bits),
value,
instruction_count,
virtual_time: VirtualNanos(now_ns),
program_counter,
};
let (answer, next) = core.driven.access(access, &mut core.host);
unsafe { *next_deadline_ns = next.0 };
answer
}
#[unsafe(no_mangle)]
extern "C" fn rigless_core_stop(
core: *mut c_void,
reason: u32,
now_ns: u64,
instruction_count: u64,
next_deadline_ns: *mut u64,
) -> u32 {
let _guard = NotReentered::enter();
let core = unsafe { &mut *core.cast::<Core>() };
let stop = Stop {
at: VirtualNanos(now_ns),
instruction_count,
reason: StopReason::from_code(reason).unwrap_or_else(|| {
panic!("the proxy stopped for a reason {reason} the header does not name")
}),
};
let (next, finished) = match core.driven.stop(stop, &mut core.host) {
Some(next) => (next.0, 0),
None => (NO_DEADLINE_NS, 1),
};
unsafe { *next_deadline_ns = next };
finished
}
#[unsafe(no_mangle)]
extern "C" fn rigless_core_free(_core: *mut c_void) {}
pub fn run<D, R>(session: Session<D>, start: &Start<'_>, driver: R) -> Result<Finished<D, R>, Error>
where
D: Board + ?Sized + 'static,
R: Driver<D> + 'static,
{
if RAN.swap(true, Ordering::AcqRel) {
return Err(Error::AlreadyRan);
}
let mut session = session;
let arguments = {
let target = session.target();
let mut targets = session.board_mut().targets();
let command = targets.swap_remove(target);
let memory = command.mcu.memory();
arguments(&command.emulator, memory, start)?
};
*PREPARED
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(Box::new(Run {
session,
driver,
last: None,
}));
let mut argv: Vec<*mut c_char> = arguments
.iter()
.map(|argument| argument.as_ptr().cast_mut())
.chain(std::iter::once(ptr::null_mut()))
.collect();
let argc = c_int::try_from(arguments.len()).expect("a handful of arguments");
unsafe { qemu_init(argc, argv.as_mut_ptr()) };
let status = unsafe { qemu_main_loop() };
unsafe { qemu_cleanup(status) };
unsafe { bql_unlock() };
unsafe { replay_mutex_unlock() };
let core = ACTIVE.swap(ptr::null_mut(), Ordering::AcqRel);
assert!(
!core.is_null(),
"the emulator ended with status {status} before the machine took the core"
);
let core = unsafe { Box::from_raw(core) };
let run = core
.driven
.into_any()
.downcast::<Run<D, R>>()
.unwrap_or_else(|_| unreachable!("the run that was prepared"));
let Run {
session,
driver,
last,
} = *run;
Ok(Finished {
session,
driver,
stop: last,
})
}
fn arguments(
command: &EmulatorCommand,
memory: &[Region],
start: &Start<'_>,
) -> Result<Vec<CString>, Error> {
let machine = std::iter::once(command.machine.to_owned())
.chain(command.properties.iter().map(ToString::to_string))
.collect::<Vec<_>>()
.join(",");
let mut arguments: Vec<String> = vec![
"rigless".into(),
"-M".into(),
machine,
"-cpu".into(),
command.cpu.into(),
];
for region in memory {
arguments.push("-object".into());
arguments.push(format!(
"memory-backend-ram,id={},size={},share=on",
region.name, region.size
));
arguments.push("-device".into());
arguments.push(memory_device(region, &command.aliases));
}
arguments.extend([
"-kernel".into(),
start.firmware.display().to_string(),
"-icount".into(),
format!("shift={},sleep=off,align=off", command.icount_shift),
"-nographic".into(),
"-display".into(),
"none".into(),
"-serial".into(),
"none".into(),
"-monitor".into(),
"none".into(),
]);
if let Some(address) = start.debugger {
arguments.extend(["-gdb".into(), format!("tcp:{address}")]);
if start.wait_for_debugger {
arguments.push("-S".into());
}
}
arguments
.into_iter()
.map(|argument| CString::new(argument.clone()).map_err(|_nul| Error::Argument(argument)))
.collect()
}
fn memory_device(region: &Region, aliases: &[Alias]) -> String {
let alias = aliases
.iter()
.find(|alias| alias.region == region.name)
.map(|alias| format!(",alias-base={:#x}", alias.at))
.unwrap_or_default();
let read_only = if region.kind == RegionKind::Ram {
""
} else {
",readonly=on"
};
format!(
"rigless-memory,memdev={},base={:#x}{alias}{read_only}",
region.name, region.base
)
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::*;
use crate::emulator::Property;
#[test]
#[should_panic(expected = "while already inside a call to it")]
fn a_call_into_the_core_from_inside_one_is_refused() {
let _outer = NotReentered::enter();
let _inner = NotReentered::enter();
}
#[test]
fn the_forks_header_and_this_mirror_agree_on_the_version() {
let header = include_str!("../../../qemu/include/hw/misc/rigless-core.h");
let version = header
.lines()
.find_map(|line| line.strip_prefix("#define RIGLESS_HOST_ABI_VERSION ((uint64_t)"))
.and_then(|rest| rest.strip_suffix(')'))
.expect("the header defines its version as ((uint64_t)N)");
assert_eq!(version.parse::<u64>(), Ok(RIGLESS_HOST_ABI_VERSION));
}
#[test]
fn the_arguments_are_the_forks_own_with_the_memory_placed_and_the_boot_region_aliased() {
let command = EmulatorCommand {
machine: "test-machine",
cpu: "test-cpu",
properties: vec![Property::new("num-irq", 91u8)],
icount_shift: 2,
aliases: vec![Alias {
region: "flash",
at: 0,
}],
};
let memory = [
Region::flash("flash", 0x0800_0000, 2 << 20),
Region::ram("sram", 0x2000_0000, 192 << 10),
];
let start = Start {
firmware: Path::new("fw.elf"),
debugger: Some("127.0.0.1:3333".parse().unwrap()),
wait_for_debugger: true,
};
let rendered: Vec<String> = arguments(&command, &memory, &start)
.unwrap()
.into_iter()
.map(|argument| argument.into_string().unwrap())
.collect();
assert_eq!(
rendered,
[
"rigless",
"-M",
"test-machine,num-irq=91",
"-cpu",
"test-cpu",
"-object",
"memory-backend-ram,id=flash,size=2097152,share=on",
"-device",
"rigless-memory,memdev=flash,base=0x8000000,alias-base=0x0,readonly=on",
"-object",
"memory-backend-ram,id=sram,size=196608,share=on",
"-device",
"rigless-memory,memdev=sram,base=0x20000000",
"-kernel",
"fw.elf",
"-icount",
"shift=2,sleep=off,align=off",
"-nographic",
"-display",
"none",
"-serial",
"none",
"-monitor",
"none",
"-gdb",
"tcp:127.0.0.1:3333",
"-S",
]
);
}
}