-
Notifications
You must be signed in to change notification settings - Fork 8
Distributor Guide Writing Compilation Passes
This page explains how to write a new compilation pass that can be plugged into an RTICX distribution.
A compilation pass is a self-contained crate that implements a subset of RTICX functionality. It transforms user application syntax from a higher to a lower level syntax representation, usually expanding the input so that the next pass or the core pass can understand it.
For example, the software-tasks pass transforms #[sw_task] and spawn() calls into hardware tasks and dispatcher interrupts. The deadline pass converts deadline = D into priority = N. The auto-assign pass infers core = N from shared resource usage.
Every pass implements RticPass from rticx-core:
use proc_macro2::TokenStream as TokenStream2;
use syn::ItemMod;
use rticx_core::{InfoBus, MainInjectionPoint, RticPass};
pub trait RticPass {
fn subscribe(&mut self, info_bus: InfoBus);
fn run_pass(
&self,
args: TokenStream2,
app_mod: ItemMod,
) -> syn::Result<(TokenStream2, ItemMod)>;
fn pass_name(&self) -> &str;
fn main_injection(&self, point: &MainInjectionPoint, core: u32) -> Option<TokenStream2> {
None
}
}-
args: the token stream of the#[<distro>::app(...)]attribute arguments. -
app_mod: the annotated module. - The return value is the transformed
(args, app_mod). -
pass_nameis used in error messages: errors are wrapped asin `<pass_name>` compilation pass: <original error>, pointing at the user's span. -
subscribeis the only place a pass receives a (clonable) handle to the sharedInfoBus(see Using the InfoBus). -
main_injectionlets a pass inject tokens into specific spots of the generated entry functions (EntryStart,BeforeInit,BeforePostInit,BeforeIdle). It is called once per injection point per core (coreis the target core index), so passes can target individual cores in multicore apps. The core pass calls it after parsing/analysis.
Passes are registered as pre-core passes. The RticMacroBuilder subscribes the core backend first, then each pre-core pass in insertion order, before invoking that pass's run_pass:
use rticx_core::RticMacroBuilder;
let mut builder = RticMacroBuilder::new(my_backend);
builder.bind_pre_core_pass(MyPass);
let tokens = builder.build_rtic_macro(args, input);- Pre-core passes run before
rticx-coreparses the module. Use them to expand high-level syntax into core RTICX syntax. - There is no
bind_post_core_passanymore. If a pass needs to react after the core codegen, register it as the last pre-core pass and read therticx_core::App/rticx_core::Analysisentries from theInfoBusonce they have been published.
A pass that consumes an #[app(...)] argument (e.g. dispatchers) must remove it from the args token stream before returning from run_pass. Leftover arguments are reported by the core pass as warnings (see Parsing attributes with RticAttr), so consuming a key and leaving it behind produces a misleading warning.
Use RticAttr::args_tokens to reconstruct the stripped arguments:
use quote::format_ident;
use rticx_core::parse_utils::RticAttr;
fn run_pass(&self, args: TokenStream2, app_mod: ItemMod) -> syn::Result<(TokenStream2, ItemMod)> {
// ...parse and transform...
let mut attr = RticAttr::parse_from_tokens(args.clone(), format_ident!("app"))?;
attr.elements.remove("dispatchers");
let args = attr.args_tokens();
Ok((args, code))
}The same rule applies to task-level attributes your pass re-emits: strip the keys only your pass understands (e.g. spawn_by, capacity on #[sw_task]) before renaming the attribute to #[task(...)], because the core pass rejects unknown task arguments with an error.
If a pass needs target-specific information, it can define its own backend trait. The distribution implements this trait and passes the implementation to the pass constructor.
For example, rticx-sw-pass defines SwPassBackend:
pub trait SwPassBackend {
fn queue_path(&self) -> syn::Path;
fn generate_local_pend_fn(&self, core: u32, empty_body_fn: ItemFn) -> ItemFn;
fn generate_cross_pend_fn(&self, core: u32, empty_body_fn: ItemFn) -> Option<ItemFn>;
fn custom_interrupt_path(&self, core: u32) -> Option<syn::Path> { None }
fn subscribe(&mut self, _info_bus: rticx_core::InfoBus) {}
}-
generate_local_pend_fnfills the body of the core-local interrupt-pending function used byspawn. -
generate_cross_pend_fnfills the cross-core interrupt-pending function used byspawn_from. ReturnsNoneon single-core targets. -
custom_interrupt_pathoptionally overrides the default PAC interrupt path. -
subscribeis the default no-op; theSoftwarePasswrapper forwards theInfoBusto its backend, so this is where aSwPassBackendimplementation can grab the bus.
rticx_core::parse_utils::RticAttr is the standard tool for parsing #[name(key = value, ...)]-style attribute arguments: the #[app] attribute and any task/struct attribute:
use rticx_core::parse_utils::RticAttr;
use quote::format_ident;
// from a syn::Attribute
let attr = RticAttr::parse_from_attr(&attribute)?;
// from a syn::Meta (e.g. `attr.meta` of an ItemFn/ItemStruct)
let attr = RticAttr::from_meta(&meta)?;
// from a bare token stream of arguments, e.g. the `args` given to run_pass
let attr = RticAttr::parse_from_tokens(tokens, format_ident!("app"))?;-
attr.name: the attribute name (e.g.taskfor#[task(...)]). -
attr.elements:HashMap<String, syn::Expr>of thekey = valuepairs.
Rather than matching on syn::Expr by hand, use the typed accessors. Each removes the key and parses it, keeping the original spans and producing precise errors:
| Method | Parses | Error if |
|---|---|---|
take_ident("key") |
single identifier (e.g. binds = UART) |
not an identifier |
take_path("key") |
path (e.g. device = my_pac) |
not a path |
take_u16("key") / take_u32("key")
|
unsigned integer literal | not an integer literal |
take_ident_array("key") |
array of identifiers (e.g. shared = [a, b]) |
not an identifier array |
take_expr("key") |
any expression | never |
get_expr("key") |
borrows without removing | never |
let priority = attr.take_u16("priority")?.unwrap_or(DEFAULT_TASK_PRIORITY);
let shared = attr.take_ident_array("shared")?.unwrap_or_default();RticAttr implements ToTokens (re-emits #[name(k = v, ...)]), which is handy for renaming attributes: the software/async passes rename #[sw_task] to #[task] this way:
let mut attr = RticAttr::parse_from_attr(&task_attr)?;
attr.name = format_ident!("task");
attr.elements.remove("spawn_by"); // pass-only key
quote! { #attr }attr.args_tokens() emits only the bare k = v, ... arguments (without the #[name(...)] wrapper): use it when returning stripped #[app] arguments from run_pass.
A typical pass crate contains:
- A
Cargo.tomlwithrticx-coreas a dependency. - A public type implementing
RticPass. - Optionally, a public backend trait for target-specific hooks.
- A public constructor that accepts the backend trait implementation.
use proc_macro2::TokenStream as TokenStream2;
use syn::ItemMod;
use rticx_core::{InfoBus, RticPass};
pub struct MyPass;
impl RticPass for MyPass {
fn subscribe(&mut self, _info_bus: InfoBus) {}
fn run_pass(
&self,
args: TokenStream2,
mut app_mod: ItemMod,
) -> syn::Result<(TokenStream2, ItemMod)> {
// Inspect and transform app_mod and args here
Ok((args, app_mod))
}
fn pass_name(&self) -> &str {
"my-pass"
}
}The InfoBus is the shared, typed information bus that lets passes and backends exchange data during a single macro expansion. An InfoBus is created by RticMacroBuilder and clones are handed out to the core backend and each compilation pass via their subscribe methods before any run_pass is invoked.
Conventions:
- Entry keys are namespaced by the publishing crate and the type name:
crate_name::TypeName. The core pass publishesrticx_core::Appandrticx_core::Analysisafter parsing and analysis. The software-tasks pass publishesrticx_sw_pass::Appandrticx_sw_pass::Analysis(exported as the constantsINFO_APP/INFO_ANALYSIS). - Entries are write-once: a second
publishto an existing key is an error. Pick a key namespace you own to avoid colliding with other passes. -
publishandgeterrors are descriptive:EntryOccupied,EntryNotFound, andInvalidTargetType. The latter includes the expected and stored type names viastd::any::type_name. - Subscribe ordering matters: the core backend receives the bus first, then each pre-core pass in insertion order, in each case before its
run_passruns. A later pass can thereforegetentries published by an earlier pass.
A pass that wants to publish its own parsed/analyzed data typically stashes the InfoBus clone in subscribe and uses it in run_pass:
pub struct MyPass {
info_bus: Option<rticx_core::InfoBus>,
}
impl RticPass for MyPass {
fn subscribe(&mut self, info_bus: rticx_core::InfoBus) {
self.info_bus = Some(info_bus);
}
fn run_pass(
&self,
args: TokenStream2,
app_mod: ItemMod,
) -> syn::Result<(TokenStream2, ItemMod)> {
// ...parse/transform...
if let Some(bus) = &self.info_bus {
bus.publish("my_pass::App", my_app)
.expect("no other crate should publish my_pass::App");
}
Ok((args, app_mod))
}
fn pass_name(&self) -> &str { "my-pass" }
}Because passes are pure syntax transformers, you can test them by feeding them a parsed ItemMod and asserting on the output. For passes that change the module items, you can also write trybuild-style compilation tests as distribution examples.
For interactive debugging, use rticx-expand to snapshot the module after
every pipeline stage. See Debugging and Inspection fore more details.
- Writing Distributions: plug your pass into a distribution.
- Distributor Guide Architecture: understand the full pipeline.