Currently, all of idols code generation is implemented by "manually"
writing out Rust code as strings. The quote crate provides a macro for
quasi-quoting Rust code inside of Rust code, as a
proc-_macro2::TokenStream. This is generally the preferred way to do
code generation in much of the Rust ecosystem.
Some reasons we might want to use quote include:
Integrating with prettyplease, a crate that does rustfmt-like
formatting as a library dep, so we no longer need to invoke an external
rustfmt as part of the build process in Hubris
When using quote, we can interpolate things like types and
identifiers as their parsed representation from the syn crate. This
allows us to actually parse these fragments from the interface
definition and validate that they're correct --- a Name can be
represented by a syn::Ident, rather than a String, so we know that
it's a valid Rust identifier, and parsing the RON will fail if it
isn't. Similarly, we can parse types this way, allowing us to detect
!Sized types much more accurately.
Code quoted using quote! looks like Rust code, rather than a big
string literal. Therefore, it can benefit from an editor's brace
matching, syntax highlighting, indentation, etc.
This branch rewrites idol's code generation using quote.
Testing
I've manually tested that the code generated is still correct by
comparing the output between this branch and main, using the example
interface definition from the README:
Currently, all of
idol
s code generation is implemented by "manually" writing out Rust code as strings. Thequote
crate provides a macro for quasi-quoting Rust code inside of Rust code, as aproc-_macro2::TokenStream
. This is generally the preferred way to do code generation in much of the Rust ecosystem.Some reasons we might want to use
quote
include:prettyplease
, a crate that does rustfmt-like formatting as a library dep, so we no longer need to invoke an externalrustfmt
as part of the build process in Hubrisquote
, we can interpolate things like types and identifiers as their parsed representation from thesyn
crate. This allows us to actually parse these fragments from the interface definition and validate that they're correct --- aName
can be represented by asyn::Ident
, rather than aString
, so we know that it's a valid Rust identifier, and parsing the RON will fail if it isn't. Similarly, we can parse types this way, allowing us to detect!Sized
types much more accurately.quote!
looks like Rust code, rather than a big string literal. Therefore, it can benefit from an editor's brace matching, syntax highlighting, indentation, etc.This branch rewrites
idol
's code generation usingquote
.Testing
I've manually tested that the code generated is still correct by comparing the output between this branch and
main
, using the example interface definition from the README:Generated client stubs:
### `main`: ```rust #[allow(non_camel_case_types)] #[derive(Copy, Clone, Debug, Eq, PartialEq, userlib::FromPrimitive)] pub enum SpiOperation { exchange = 1, } #[allow(unused_imports)] use userlib::UnwrapLite; #[derive(Clone, Debug)] pub struct Spi { current_id: core::cell::CellGenerated server stubs:
### `main`: ```rust pub const EXCHANGE_MSG_SIZE: usize = 0 + core::mem::size_of::::recv_source(self.1) } fn closed_recv_fail(&mut self) {::closed_recv_fail(self.1) } fn handle( &mut self, op: SpiOperation, incoming: &[u8], rm: &userlib::RecvMessage, ) -> Result<(), idol_runtime::RequestError> {
#[allow(unused_imports)]
use core::convert::TryInto;
use idol_runtime::ClientError;
match op {
SpiOperation::exchange => {
let args = read_exchange_msg(incoming).ok_or_else(|| ClientError::BadMessageContents.fail())?;
let r = self.1.exchange(
rm,
args.device_index,
idol_runtime::Leased::read_only_slice(rm.sender, 0, None).ok_or_else(|| ClientError::BadLease.fail())?,
idol_runtime::Leased::write_only_slice(rm.sender, 1, None).ok_or_else(|| ClientError::BadLease.fail())?,
);
match r {
Ok(val) => {
userlib::sys_reply(rm.sender, 0, zerocopy::AsBytes::as_bytes(&val));
Ok(())
}
Err(val) => {
Err(val.map_runtime(u16::from))
}
}
}
}
}
}
```
### this branch:
```rust
#[allow(unused_imports)]
use userlib::UnwrapLite;
pub const EXCHANGE_MSG_SIZE: usize = 0 + core::mem::size_of::();
pub const EXCHANGE_REPLY_SIZE: usize = core::mem::size_of::<()>();
#[allow(clippy::absurd_extreme_comparisons)]
const fn max_incoming_size() -> usize {
let mut max = 0;
if max < EXCHANGE_MSG_SIZE {
max = EXCHANGE_MSG_SIZE;
}
max
}
pub const INCOMING_SIZE: usize = max_incoming_size();
#[repr(C, packed)]
#[derive(Copy, Clone, zerocopy::FromBytes, zerocopy::Unaligned)]
#[allow(non_camel_case_types)]
pub struct Spi_exchange_ARGS {
pub device_index: u8,
}
pub fn read_exchange_msg(bytes: &[u8]) -> Option<&Spi_exchange_ARGS> {
zerocopy::LayoutVerified::<_, Spi_exchange_ARGS>::new_unaligned(bytes)
.ok()
.into_ref()
}
#[repr(C, packed)]
struct Spi_exchange_REPLY {
value: (),
}
#[allow(dead_code)]
static SPI_EXCHANGE_REPLY: Option<&Spi_exchange_REPLY> = None;
#[allow(non_camel_case_types)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, userlib::FromPrimitive)]
pub enum SpiOperation {
exchange = 1usize,
}
impl idol_runtime::ServerOp for SpiOperation {
fn max_reply_size(self) -> usize {
match self {
Self::exchange => EXCHANGE_REPLY_SIZE,
}
}
fn required_lease_count(self) -> usize {
match self {
Self::exchange => 2usize,
}
}
}
pub trait InOrderSpiImpl {
fn recv_source(&self) -> Option {
None
}
fn closed_recv_fail(&mut self) {
panic!()
}
fn exchange(
&mut self,
msg: &userlib::RecvMessage,
device_index: u8,
source: idol_runtime::Leased,
sink: idol_runtime::Leased,
) -> Result<(), idol_runtime::RequestError>
where
SpiError: idol_runtime::IHaveConsideredServerDeathWithThisErrorType;
}
impl idol_runtime::Server
for (core::marker::PhantomData, &'_ mut S) {
fn recv_source(&self) -> Option {
::recv_source(self.1) } fn closed_recv_fail(&mut self) {::closed_recv_fail(self.1) } fn handle( &mut self, op: SpiOperation, incoming: &[u8], rm: &userlib::RecvMessage, ) -> Result<(), idol_runtime::RequestError> {
#[allow(unused_imports)]
use core::convert::TryInto;
use idol_runtime::ClientError;
match op {
SpiOperation::exchange => {
let args = read_exchange_msg(incoming)
.ok_or_else(|| {
idol_runtime::ClientError::BadMessageContents.fail()
})?;
let r = self
.1
.exchange(
rm,
args.device_index,
idol_runtime::Leased::read_only_slice(rm.sender, 0usize, None)
.ok_or_else(|| ClientError::BadLease.fail())?,
idol_runtime::Leased::write_only_slice(rm.sender, 1usize, None)
.ok_or_else(|| ClientError::BadLease.fail())?,
);
match r {
Ok(val) => {
userlib::sys_reply(
rm.sender,
0,
zerocopy::AsBytes::as_bytes(&val),
);
Ok(())
}
Err(val) => Err(val.map_runtime(u16::from)),
}
}
}
}
}
```
As you can (hopefully) tell, the generated code is the same, with the exception of different formatting.