From bb0db65e585aa03fcba2143605fe40dfdec0e8fe Mon Sep 17 00:00:00 2001 From: Sean Cross Date: Tue, 13 Oct 2020 08:32:39 +0800 Subject: [PATCH 01/12] utralib: fix build dependency on svd file Previously, the svd file was treated as an environment variable to watch. Naturally this never changed, so an updated svd file never triggered a rebuild. Signed-off-by: Sean Cross --- utralib/build.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/utralib/build.rs b/utralib/build.rs index a68a58d7d..2c9957334 100644 --- a/utralib/build.rs +++ b/utralib/build.rs @@ -1,9 +1,10 @@ use std::env; fn main() { - let svd_file = env::var("XOUS_SVD_FILE").expect("Set the environment variable `XOUS_SVD_FILE` to point to an SVD file"); + let svd_file = env::var("XOUS_SVD_FILE") + .expect("Set the environment variable `XOUS_SVD_FILE` to point to an SVD file"); println!("cargo:rerun-if-env-changed=XOUS_SVD_FILE"); - println!("cargo:rerun-if-env-changed={}", svd_file); + println!("cargo:rerun-if-changed={}", svd_file); let src_file = std::fs::File::open(svd_file).expect("couldn't open src file"); let mut dest_file = std::fs::File::create("src/generated.rs").expect("couldn't open dest file"); From 53f1b3c416a97f38a94bb70ffe8b8f66b24eec55 Mon Sep 17 00:00:00 2001 From: Sean Cross Date: Tue, 13 Oct 2020 22:54:12 +0800 Subject: [PATCH 02/12] kernel: main: make `utralib` incusion conditional Only include utralib when running on baremetal. Signed-off-by: Sean Cross --- kernel/src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kernel/src/main.rs b/kernel/src/main.rs index d45b6cbf6..757261f43 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -25,6 +25,8 @@ mod syscall; use services::SystemServices; use xous_kernel::*; + +#[cfg(baremetal)] use utralib::generated::*; #[cfg(baremetal)] From 39c7dced01bf4d3ecd7f1411639cc933d8f36a62 Mon Sep 17 00:00:00 2001 From: Sean Cross Date: Tue, 13 Oct 2020 22:54:43 +0800 Subject: [PATCH 03/12] kernel: services: switch address space prior to passing messages Switch address spaces to pass messages to the new process, and switch backafterwards. Signed-off-by: Sean Cross --- kernel/src/services.rs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/kernel/src/services.rs b/kernel/src/services.rs index 15142e013..5a770855b 100644 --- a/kernel/src/services.rs +++ b/kernel/src/services.rs @@ -1499,24 +1499,26 @@ impl SystemServices { pub fn remember_server_message( &mut self, sidx: usize, - pid: PID, - context: TID, + current_pid: PID, + current_thread: TID, message: &Message, client_address: Option, ) -> Result { - // let current_pid = self.current_pid(); - // let server_pid = self - // .server_from_sidx(sidx) - // .ok_or(xous_kernel::Error::ServerNotFound)? - // .pid; - // { - // let server_process = self.get_process(server_pid)?; - // server_process.mapping.activate(); - // } + let server_pid = self + .server_from_sidx(sidx) + .ok_or(xous_kernel::Error::ServerNotFound)? + .pid; + { + let server_process = self.get_process(server_pid)?; + server_process.mapping.activate()?; + } let server = self .server_from_sidx_mut(sidx) .expect("couldn't re-discover server index"); - server.queue_response(pid, context, message, client_address) + let result = server.queue_response(current_pid, current_thread, message, client_address); + let current_process = self.get_process(current_pid).expect("couldn't find old process"); + current_process.mapping.activate().expect("couldn't switch back to previous address space"); + result } /// Get a server index based on a SID From a0a90cc3ab1002dd61c97d461a13ab10c7091450 Mon Sep 17 00:00:00 2001 From: Sean Cross Date: Wed, 14 Oct 2020 10:04:20 +0800 Subject: [PATCH 04/12] emulation: rename `example.svd` to `renode.svd` Signed-off-by: Sean Cross --- emulation/{example.svd => renode.svd} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename emulation/{example.svd => renode.svd} (100%) diff --git a/emulation/example.svd b/emulation/renode.svd similarity index 100% rename from emulation/example.svd rename to emulation/renode.svd From 585e55e8b7d2607df70b9fe9fadc2ff31a3ae441 Mon Sep 17 00:00:00 2001 From: Sean Cross Date: Wed, 14 Oct 2020 10:04:36 +0800 Subject: [PATCH 05/12] kernel: implement `retry_instruction()` This function is used to decrement the program counter to re-run a given instruction. This can be used as a cheap way to implement blocking calls. Signed-off-by: Sean Cross --- kernel/src/arch/hosted/process.rs | 4 ++++ kernel/src/arch/riscv/process.rs | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/kernel/src/arch/hosted/process.rs b/kernel/src/arch/hosted/process.rs index 3ac462e2f..8f381a63e 100644 --- a/kernel/src/arch/hosted/process.rs +++ b/kernel/src/arch/hosted/process.rs @@ -179,6 +179,10 @@ impl Process { process.threads[thread - 1].allocated = true; } + pub fn retry_instruction(&mut self, _tid: TID) -> Result<(), xous_kernel::Error> { + Ok(()) + } + pub fn setup_process(pid: PID, setup: ThreadInit) -> Result<(), xous_kernel::Error> { let mut tmp = Process { pid }; tmp.setup_thread(INITIAL_TID, setup) diff --git a/kernel/src/arch/riscv/process.rs b/kernel/src/arch/riscv/process.rs index 5a5d8e04c..c4f439809 100644 --- a/kernel/src/arch/riscv/process.rs +++ b/kernel/src/arch/riscv/process.rs @@ -235,6 +235,15 @@ impl Process { } } + pub fn retry_instruction(&mut self, tid: TID) -> Result<(), xous_kernel::Error> { + let process = unsafe { &mut *PROCESS }; + let mut thread = &mut process.threads[tid]; + if thread.sepc >= 4 { + thread.sepc -= 4; + } + Ok(()) + } + /// Initialize this process thread with the given entrypoint and stack /// addresses. pub fn setup_process(pid: PID, thread_init: ThreadInit) -> Result<(), xous_kernel::Error> { From 0c344efb607509871cc09218ae07fe1b3953275b Mon Sep 17 00:00:00 2001 From: Sean Cross Date: Wed, 14 Oct 2020 10:05:42 +0800 Subject: [PATCH 06/12] kernel: syscall: move `do_yield()` to its own function This will allow us to call `Yield` for other operations that need to be blocking. Signed-off-by: Sean Cross --- kernel/src/syscall.rs | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/kernel/src/syscall.rs b/kernel/src/syscall.rs index 943ce6c77..a8f9af16c 100644 --- a/kernel/src/syscall.rs +++ b/kernel/src/syscall.rs @@ -10,6 +10,25 @@ use xous_kernel::*; /// This is the context that called SwitchTo static mut SWITCHTO_CALLER: Option<(PID, TID)> = None; +fn do_yield(_pid: PID, tid: TID) -> SysCallResult { + // If we're not running on bare metal, treat this as a no-op. + if !cfg!(baremetal) { + return Ok(xous_kernel::Result::Ok); + } + + let (parent_pid, parent_ctx) = unsafe { + SWITCHTO_CALLER + .take() + .expect("yielded when no parent context was present") + }; + SystemServices::with_mut(|ss| { + // TODO: Advance thread + ss.activate_process_thread(tid, parent_pid, parent_ctx, true) + .map(|_| Ok(xous_kernel::Result::ResumeProcess)) + .unwrap_or(Err(xous_kernel::Error::ProcessNotFound)) + }) +} + fn send_message(pid: PID, thread: TID, cid: CID, message: Message) -> SysCallResult { SystemServices::with_mut(|ss| { let sidx = ss @@ -590,24 +609,7 @@ pub fn handle_inner(pid: PID, tid: TID, call: SysCall) -> SysCallResult { interrupt_claim(no, pid as definitions::PID, callback, arg) .map(|_| xous_kernel::Result::Ok) } - SysCall::Yield => { - // If we're not running on bare metal, treat this as a no-op. - if !cfg!(baremetal) { - return Ok(xous_kernel::Result::Ok); - } - - let (parent_pid, parent_ctx) = unsafe { - SWITCHTO_CALLER - .take() - .expect("yielded when no parent context was present") - }; - SystemServices::with_mut(|ss| { - // TODO: Advance thread - ss.activate_process_thread(tid, parent_pid, parent_ctx, true) - .map(|_| Ok(xous_kernel::Result::ResumeProcess)) - .unwrap_or(Err(xous_kernel::Error::ProcessNotFound)) - }) - } + SysCall::Yield => do_yield(pid, tid), SysCall::ReturnToParentI(_pid, _cpuid) => { unsafe { // let (_current_pid, _current_ctx) = crate::arch::irq::take_isr_return_pair() From e5100bcb337313bd040f15d8d7dfc6e79485079b Mon Sep 17 00:00:00 2001 From: Sean Cross Date: Wed, 14 Oct 2020 10:06:27 +0800 Subject: [PATCH 07/12] kernel: syscall: implement SendMessage SendMessage is a blocking version of TrySendMessage, except this call will block if the server queue is full. Signed-off-by: Sean Cross --- kernel/src/syscall.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/kernel/src/syscall.rs b/kernel/src/syscall.rs index a8f9af16c..7f9a3e9d3 100644 --- a/kernel/src/syscall.rs +++ b/kernel/src/syscall.rs @@ -673,7 +673,17 @@ pub fn handle_inner(pid: PID, tid: TID, call: SysCall) -> SysCallResult { // SysCall::Connect(sid) => { // SystemServices::with_mut(|ss| ss.connect_to_server(sid).map(xous_kernel::Result::ConnectionID)) // } - // SysCall::SendMessage(cid, message) => send_message(pid, tid, cid, message), + SysCall::SendMessage(cid, message) => { + let result = send_message(pid, tid, cid, message); + match result { + Ok(o) => Ok(o), + Err(xous_kernel::Error::ServerQueueFull) => { + arch::process::Process::with_current_mut(|p| p.retry_instruction(tid))?; + do_yield(pid, tid) + }, + Err(e) => Err(e), + } + } _ => panic!("Unhandled Syscall: {:?}", call), //Err(xous_kernel::Error::UnhandledSyscall), } } From 61fdd5be15ab599bfcf4a593870ea6bc3521f083 Mon Sep 17 00:00:00 2001 From: Sean Cross Date: Wed, 14 Oct 2020 10:52:51 +0800 Subject: [PATCH 08/12] kernel: hosted: add current_tid() to Process This allows us to mock the current TID for debugging purposes on hosted environments. Signed-off-by: Sean Cross --- kernel/src/arch/hosted.rs | 4 ++++ kernel/src/arch/hosted/process.rs | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/kernel/src/arch/hosted.rs b/kernel/src/arch/hosted.rs index b9e56b87c..687303163 100644 --- a/kernel/src/arch/hosted.rs +++ b/kernel/src/arch/hosted.rs @@ -68,6 +68,10 @@ fn generate_pid_key() -> [u8; 16] { process_key } +pub fn current_pid() -> PID { + crate::arch::process::current_pid() +} + /// Each client gets its own connection and its own thread, which is handled here. fn handle_connection( conn: TcpStream, diff --git a/kernel/src/arch/hosted/process.rs b/kernel/src/arch/hosted/process.rs index 8f381a63e..738e384f2 100644 --- a/kernel/src/arch/hosted/process.rs +++ b/kernel/src/arch/hosted/process.rs @@ -171,6 +171,19 @@ impl Process { }) } + /// Calls the provided function with the current inner process state. + pub fn with_current_mut(f: F) -> R + where + F: FnOnce(&mut Process) -> R, + { + let mut process = Self::current(); + f(&mut process) + } + + pub fn current_tid(&self) -> TID { + 1 + } + fn setup_thread_inner(thread: TID, process_table: &mut ProcessTable) { let current_pid_idx = process_table.current.get() as usize - 1; let process = &mut process_table.table[current_pid_idx].as_mut().unwrap(); From ecd2d00f47ce69ff20699e4a4e0677edc75d5ef3 Mon Sep 17 00:00:00 2001 From: Sean Cross Date: Wed, 14 Oct 2020 10:54:23 +0800 Subject: [PATCH 09/12] kernel: fix return_scalar2 This call did not work, because it wasn't properly returning control to the parent process. Signed-off-by: Sean Cross --- kernel/src/syscall.rs | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/kernel/src/syscall.rs b/kernel/src/syscall.rs index 7f9a3e9d3..4b506bd29 100644 --- a/kernel/src/syscall.rs +++ b/kernel/src/syscall.rs @@ -345,8 +345,8 @@ fn return_scalar(pid: PID, _tid: TID, sender: MessageSender, arg: usize) -> SysC } fn return_scalar2( - pid: PID, - _tid: TID, + server_pid: PID, + server_tid: TID, sender: MessageSender, arg1: usize, arg2: usize, @@ -360,7 +360,7 @@ fn return_scalar2( let server = ss .server_from_sidx_mut(sidx) .ok_or(xous_kernel::Error::ServerNotFound)?; - if server.pid != pid { + if server.pid != server_pid { return Err(xous_kernel::Error::ServerNotFound); } let result = server.take_waiting_message(sender.idx, None)?; @@ -387,14 +387,23 @@ fn return_scalar2( return Err(xous_kernel::Error::ProcessNotFound); } }; + + // Handle the server's return value + ss.switch_from_thread(server_pid, server_tid)?; + ss.ready_thread(server_pid, server_tid)?; + ss.set_thread_result( + server_pid, + server_tid, + xous_kernel::Result::Ok, + )?; + + // Switch to the client ss.ready_thread(client_pid, client_tid)?; ss.switch_to_thread(client_pid, Some(client_tid))?; - ss.set_thread_result( - client_pid, - client_tid, - xous_kernel::Result::Scalar2(arg1, arg2), - )?; - Ok(xous_kernel::Result::Ok) + #[cfg(not(baremetal))] + ss.set_thread_result(client_pid, client_tid, xous_kernel::Result::Scalar2(arg1, arg2))?; + + Ok(xous_kernel::Result::Scalar2(arg1, arg2)) }) } @@ -457,7 +466,12 @@ pub fn handle(pid: PID, tid: TID, call: SysCall) -> SysCallResult { print!("KERNEL({}:{}): Syscall {:?}", pid, tid, call); let result = handle_inner(pid, tid, call); #[cfg(feature = "debug-print")] - println!(" -> {:?}", result); + println!( + " -> ({}:{}) {:?}", + crate::arch::current_pid(), + crate::arch::process::Process::current().current_tid(), + result + ); result } @@ -680,7 +694,7 @@ pub fn handle_inner(pid: PID, tid: TID, call: SysCall) -> SysCallResult { Err(xous_kernel::Error::ServerQueueFull) => { arch::process::Process::with_current_mut(|p| p.retry_instruction(tid))?; do_yield(pid, tid) - }, + } Err(e) => Err(e), } } From 54a8de27766f5516dbe72e942c411d74114c24f8 Mon Sep 17 00:00:00 2001 From: Sean Cross Date: Wed, 14 Oct 2020 11:16:21 +0800 Subject: [PATCH 10/12] kernel: fix return_scalar and return_scalar2 These didn't correctly set the thread status in the server when returning a value. Instead of marking a server as runnable, it remained in an idle state. This correctly sets the thread status in the server, and resumes the client. This has been tested in Renode as well as using `cargo test`. Signed-off-by: Sean Cross --- kernel/src/syscall.rs | 72 ++++++++++++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 21 deletions(-) diff --git a/kernel/src/syscall.rs b/kernel/src/syscall.rs index 4b506bd29..b791ead88 100644 --- a/kernel/src/syscall.rs +++ b/kernel/src/syscall.rs @@ -298,7 +298,12 @@ fn return_memory(pid: PID, tid: TID, sender: MessageSender, buf: MemoryRange) -> }) } -fn return_scalar(pid: PID, _tid: TID, sender: MessageSender, arg: usize) -> SysCallResult { +fn return_scalar( + server_pid: PID, + server_tid: TID, + sender: MessageSender, + arg: usize, +) -> SysCallResult { SystemServices::with_mut(|ss| { let sender = SenderID::from(sender); @@ -308,7 +313,7 @@ fn return_scalar(pid: PID, _tid: TID, sender: MessageSender, arg: usize) -> SysC let server = ss .server_from_sidx_mut(sidx) .ok_or(xous_kernel::Error::ServerNotFound)?; - if server.pid != pid { + if server.pid != server_pid { return Err(xous_kernel::Error::ServerNotFound); } let result = server.take_waiting_message(sender.idx, None)?; @@ -337,10 +342,27 @@ fn return_scalar(pid: PID, _tid: TID, sender: MessageSender, arg: usize) -> SysC return Err(xous_kernel::Error::ProcessNotFound); } }; - ss.ready_thread(client_pid, client_tid)?; - ss.switch_to_thread(client_pid, Some(client_tid))?; - ss.set_thread_result(client_pid, client_tid, xous_kernel::Result::Scalar1(arg))?; - Ok(xous_kernel::Result::Ok) + + if !cfg!(baremetal) { + // In a hosted environment, `switch_to_thread()` doesn't continue + // execution from the new thread. Instead it continues in the old + // thread. Therefore, we need to instruct the client to resume, and + // return to the server. + // In a baremetal environment, the opposite is true -- we instruct + // the server to resume and return to the client. + ss.set_thread_result(client_pid, client_tid, xous_kernel::Result::Scalar1(arg))?; + Ok(xous_kernel::Result::Ok) + } else { + // Switch away from the server, but leave it as Runnable + ss.switch_from_thread(server_pid, server_tid)?; + ss.ready_thread(server_pid, server_tid)?; + ss.set_thread_result(server_pid, server_tid, xous_kernel::Result::Ok)?; + + // Switch to the client + ss.ready_thread(client_pid, client_tid)?; + ss.switch_to_thread(client_pid, Some(client_tid))?; + Ok(xous_kernel::Result::Scalar1(arg)) + } }) } @@ -388,22 +410,30 @@ fn return_scalar2( } }; - // Handle the server's return value - ss.switch_from_thread(server_pid, server_tid)?; - ss.ready_thread(server_pid, server_tid)?; - ss.set_thread_result( - server_pid, - server_tid, - xous_kernel::Result::Ok, - )?; + if !cfg!(baremetal) { + // In a hosted environment, `switch_to_thread()` doesn't continue + // execution from the new thread. Instead it continues in the old + // thread. Therefore, we need to instruct the client to resume, and + // return to the server. + // In a baremetal environment, the opposite is true -- we instruct + // the server to resume and return to the client. + ss.set_thread_result( + client_pid, + client_tid, + xous_kernel::Result::Scalar2(arg1, arg2), + )?; + Ok(xous_kernel::Result::Ok) + } else { + // Switch away from the server, but leave it as Runnable + ss.switch_from_thread(server_pid, server_tid)?; + ss.ready_thread(server_pid, server_tid)?; + ss.set_thread_result(server_pid, server_tid, xous_kernel::Result::Ok)?; - // Switch to the client - ss.ready_thread(client_pid, client_tid)?; - ss.switch_to_thread(client_pid, Some(client_tid))?; - #[cfg(not(baremetal))] - ss.set_thread_result(client_pid, client_tid, xous_kernel::Result::Scalar2(arg1, arg2))?; - - Ok(xous_kernel::Result::Scalar2(arg1, arg2)) + // Switch to the client + ss.ready_thread(client_pid, client_tid)?; + ss.switch_to_thread(client_pid, Some(client_tid))?; + Ok(xous_kernel::Result::Scalar2(arg1, arg2)) + } }) } From ac321be7e9a2a822c5760b8a89b4726b4bc3a15f Mon Sep 17 00:00:00 2001 From: Sean Cross Date: Wed, 14 Oct 2020 11:18:02 +0800 Subject: [PATCH 11/12] xous: implement `send_message()` This fixes `send_message()` so it works as expected. Signed-off-by: Sean Cross --- xous-rs/src/syscall.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/xous-rs/src/syscall.rs b/xous-rs/src/syscall.rs index c9c657c9d..81914b443 100644 --- a/xous-rs/src/syscall.rs +++ b/xous-rs/src/syscall.rs @@ -981,21 +981,26 @@ pub fn try_send_message(connection: CID, message: Message) -> core::result::Resu } } -/// Send a message to a server, but keep trying if the server queue is full. +/// Send a message to a server. Depending on the mesage type (move or borrow), it +/// will either block (borrow) or return immediately (move). +/// If the message type is `borrow`, then the memory addresses pointed to will be +/// unavailable to this process until this function returns. +/// +/// If the server queue is full, this will block. /// /// # Errors /// /// * **ServerNotFound**: The server does not exist so the connection is now invalid /// * **BadAddress**: The client tried to pass a Memory message using an address it doesn't own /// * **Timeout**: The timeout limit has been reached -pub fn send_message(connection: CID, message: Message) -> core::result::Result<(), Error> { +pub fn send_message(connection: CID, message: Message) -> core::result::Result { let result = rsyscall(SysCall::SendMessage(connection, message)); - if let Ok(Result::Ok) = result { - Ok(()) - } else if let Err(e) = result { - Err(e) - } else { - panic!("Unexpected return value: {:?}", result); + match result { + Ok(Result::Ok) => Ok(Result::Ok), + Ok(Result::Scalar1(a)) => Ok(Result::Scalar1(a)), + Ok(Result::Scalar2(a, b)) => Ok(Result::Scalar2(a, b)), + Err(e) => Err(e), + v => panic!("Unexpected return value: {:?}", v), } } From 3bb9b55bb438177519d773ca19bafa26a94af737 Mon Sep 17 00:00:00 2001 From: Sean Cross Date: Wed, 14 Oct 2020 11:18:31 +0800 Subject: [PATCH 12/12] graphics-server: use `send_message()` instead of `try_send_message()` This function is now implemented, and should give us better performance. Signed-off-by: Sean Cross --- examples/graphics-server/src/lib.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/graphics-server/src/lib.rs b/examples/graphics-server/src/lib.rs index cdcebbdbb..e72b2217b 100644 --- a/examples/graphics-server/src/lib.rs +++ b/examples/graphics-server/src/lib.rs @@ -3,24 +3,24 @@ pub mod api; pub use api::{Point, Color}; -use xous::{try_send_message, CID}; +use xous::{send_message, CID}; pub fn draw_line(cid: CID, start: Point, end: Point) -> Result<(), xous::Error> { - try_send_message(cid, api::Opcode::Line(start, end).into()).map(|_| ()) + send_message(cid, api::Opcode::Line(start, end).into()).map(|_| ()) } pub fn draw_circle(cid: CID, center: Point, radius: u32) -> Result<(), xous::Error> { - try_send_message(cid, api::Opcode::Circle(center, radius).into()).map(|_| ()) + send_message(cid, api::Opcode::Circle(center, radius).into()).map(|_| ()) } pub fn draw_rectangle(cid: CID, start: Point, end: Point) -> Result<(), xous::Error> { - try_send_message(cid, api::Opcode::Rectangle(start, end).into()).map(|_| ()) + send_message(cid, api::Opcode::Rectangle(start, end).into()).map(|_| ()) } pub fn set_style(cid: CID, width: u32, stroke: Color, fill: Color) -> Result<(), xous::Error> { - try_send_message(cid, api::Opcode::Style(width, stroke, fill).into()).map(|_| ()) + send_message(cid, api::Opcode::Style(width, stroke, fill).into()).map(|_| ()) } pub fn flush(cid: CID) -> Result<(), xous::Error> { - try_send_message(cid, api::Opcode::Flush.into()).map(|_| ()) + send_message(cid, api::Opcode::Flush.into()).map(|_| ()) }