Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion riscv-macros/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
## v0.4.1 - Unreleased

### Added

- `entry` macro for the `riscv-rt` crate (migrated from `riscv-rt-macros`).
- `post_init` macro for the `riscv-rt` crate (migrated from `riscv-rt-macros`).

## v0.4.0 - 2025-12-19

Expand Down
4 changes: 3 additions & 1 deletion riscv-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ keywords = ["riscv", "register", "peripheral"]
license = "MIT OR Apache-2.0"
name = "riscv-macros"
repository = "https://github.com/rust-embedded/riscv"
version = "0.4.0"
version = "0.4.1"
edition = "2021"

[lib]
Expand All @@ -18,6 +18,8 @@ proc-macro = true
[features]
rt = []
rt-v-trap = ["rt"]
riscv-rt = ["rt", "syn/extra-traits", "syn/full"]
u-boot = []

[dependencies]
proc-macro2 = "1.0"
Expand Down
76 changes: 76 additions & 0 deletions riscv-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ use syn::{parse_macro_input, DeriveInput};

mod riscv;

#[cfg(feature = "riscv-rt")]
mod riscv_rt;

/// Attribute-like macro that implements the traits of the `riscv-types` crate for a given enum.
///
/// As these traits are unsafe, the macro must be called with the `unsafe` keyword followed by the trait name.
Expand Down Expand Up @@ -59,3 +62,76 @@ pub fn pac_enum(attr: TokenStream, item: TokenStream) -> TokenStream {
}
.into()
}

/// Attribute to mark which function will be called before jumping to the entry point.
/// You must enable the `post-init` feature in the `riscv-rt` crate to use this macro.
///
/// In contrast with `__pre_init`, this function is called after the static variables
/// are initialized, so it is safe to access them. It is also safe to run Rust code.
///
/// The function must have the signature of `[unsafe] fn([usize])`, where the argument
/// corresponds to the hart ID of the current hart. This is useful for multi-hart systems
/// to perform hart-specific initialization.
///
/// # IMPORTANT
///
/// This attribute can appear at most *once* in the dependency graph.
///
/// # Examples
///
/// ```
/// #[riscv_macros::post_init]
/// unsafe fn before_main(hart_id: usize) {
/// // do something here
/// }
/// ```
#[cfg(feature = "riscv-rt")]
#[proc_macro_attribute]
pub fn post_init(args: TokenStream, input: TokenStream) -> TokenStream {
riscv_rt::Fn::post_init(args, input)
}

/// Attribute to declare the entry point of the program
///
/// The specified function will be called by the reset handler *after* RAM has been initialized.
/// If present, the FPU will also be enabled before the function is called.
///
/// # Signature
///
/// ## Regular Usage
///
/// The type of the specified function must be `[unsafe] fn([usize[, usize[, usize]]]) -> !` (never ending function).
/// The optional arguments correspond to the values passed in registers `a0`, `a1`, and `a2`.
/// The first argument holds the hart ID of the current hart, which is useful for multi-hart systems.
/// The other two arguments are currently unused and reserved for future use.
///
/// ## With U-Boot
///
/// This runtime supports being booted by U-Boot. In this case, the entry point function
/// must have the signature `[unsafe] fn([c_int[, *const *const c_char]]) -> !`, where the first argument
/// corresponds to the `argc` parameter and the second argument corresponds to the `argv` parameter passed by U-Boot.
///
/// Remember to enable the `u-boot` feature in the `riscv-rt` crate to use this functionality.
///
/// # IMPORTANT
///
/// This attribute can appear at most *once* in the dependency graph.
///
/// The entry point will be called by the reset handler. The program can't reference to the entry
/// point, much less invoke it.
///
/// # Examples
///
/// ``` no_run
/// #[riscv_macros::entry]
/// fn main() -> ! {
/// loop {
/// /* .. */
/// }
/// }
/// ```
#[cfg(feature = "riscv-rt")]
#[proc_macro_attribute]
pub fn entry(args: TokenStream, input: TokenStream) -> TokenStream {
riscv_rt::Fn::entry(args, input)
}
275 changes: 275 additions & 0 deletions riscv-macros/src/riscv_rt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{
parse_macro_input, punctuated::Punctuated, spanned::Spanned, token::Comma, Error, FnArg, Ident,
ItemFn, Result, ReturnType, Type, Visibility,
};

/// Enum representing the supported runtime function attributes
pub enum Fn {
PostInit,
Entry,
}

impl Fn {
/// Convenience method to generate the token stream for the `post_init` attribute
pub fn post_init(args: TokenStream, input: TokenStream) -> TokenStream {
let errors = Self::PostInit.check_args_empty(args).err();
Self::PostInit.quote_fn(input, errors)
}

/// Convenience method to generate the token stream for the `entry` attribute
pub fn entry(args: TokenStream, input: TokenStream) -> TokenStream {
let errors = Self::Entry.check_args_empty(args).err();
Self::Entry.quote_fn(input, errors)
}

/// Generate the token stream for the function with the given attribute
fn quote_fn(&self, item: TokenStream, errors: Option<Error>) -> TokenStream {
let mut func = parse_macro_input!(item as ItemFn);

if let Err(e) = self.check_fn(&func, errors) {
return e.to_compile_error().into();
}

let export_name = self.export_name(&func);
let link_section = self.link_section(&func);

// Append to function name the prefix __riscv_rt_ (to prevent users from calling it directly)
// Note that we do not change the export name, only the internal function name in the Rust code.
func.sig.ident = Ident::new(
&format!("__riscv_rt_{}", func.sig.ident),
func.sig.ident.span(),
);

quote! {
#export_name
#link_section
#func
}
.into()
}

/// Check if the function signature is valid for the given attribute
fn check_fn(&self, f: &ItemFn, mut errors: Option<Error>) -> Result<()> {
// First, check that the function is private
if f.vis != Visibility::Inherited {
combine_err(
&mut errors,
Error::new(f.vis.span(), "function must be private"),
);
}

let sig = &f.sig;
// Next, check common aspects of the signature individually to accumulate errors
if let Some(constness) = sig.constness {
combine_err(
&mut errors,
Error::new(constness.span(), "function must not be const"),
);
}
if let Some(asyncness) = sig.asyncness {
combine_err(
&mut errors,
Error::new(asyncness.span(), "function must not be async"),
);
}
if let Some(abi) = &sig.abi {
combine_err(
&mut errors,
Error::new(abi.span(), "ABI must not be specified"),
);
}
if !sig.generics.params.is_empty() {
// Use to_token_stream to get a span covering the entire <...> block
let span = sig.generics.params.span();
combine_err(&mut errors, Error::new(span, "generics are not allowed"));
}

// Check input parameters...
self.check_inputs(&sig.inputs, &mut errors);
// ... and variadic arguments (they are at the end of input parameters)
if let Some(variadic) = &sig.variadic {
combine_err(
&mut errors,
Error::new(variadic.span(), "variadic arguments are not allowed"),
);
}

// Check output type...
self.check_output(&sig.output, &mut errors);
// ... and where clause (they are after output type)
if let Some(where_clause) = &sig.generics.where_clause {
combine_err(
&mut errors,
Error::new(where_clause.span(), "where clause is not allowed"),
);
}

match errors {
Some(e) => Err(e),
None => Ok(()),
}
}

/// Check if the function has valid input arguments for the given attribute
fn check_inputs(&self, inputs: &Punctuated<FnArg, Comma>, errors: &mut Option<Error>) {
// Use this match to specify expected input arguments for different functions in the future
match self {
Self::PostInit => self.check_fn_args(inputs, &["usize"], errors),
#[cfg(not(feature = "u-boot"))]
Self::Entry => self.check_fn_args(inputs, &["usize", "usize", "usize"], errors),
#[cfg(feature = "u-boot")]
Self::Entry => self.check_fn_args(inputs, &["c_int", "*const *const c_char"], errors),
}
}

/// Check if the function has a valid output type for the given attribute
fn check_output(&self, output: &ReturnType, errors: &mut Option<Error>) {
// Use this match to specify expected output types for different functions in the future
match self {
Self::PostInit => check_output_empty(output, errors),
Self::Entry => check_output_never(output, errors),
}
}

/// The export name for the given attribute
fn export_name(&self, _f: &ItemFn) -> Option<TokenStream2> {
// Use this match to specify export names for different functions in the future
let export_name = match self {
Self::PostInit => Some("__post_init".to_string()),
Self::Entry => Some("main".to_string()),
};

export_name.map(|name| match self {
Self::Entry => quote! {
// to avoid two main symbols when testing on host
#[cfg_attr(any(target_arch = "riscv32", target_arch = "riscv64"), export_name = #name)]
},
_ => quote! {
#[export_name = #name]
},
})
}

/// The link section attribute for the given attribute (if any)
fn link_section(&self, _f: &ItemFn) -> Option<TokenStream2> {
// Use this match to specify section names for different functions in the future
let section_name: Option<String> = match self {
Self::PostInit | Self::Entry => None,
};

section_name.map(|section| quote! {
#[cfg_attr(any(target_arch = "riscv32", target_arch = "riscv64"), link_section = #section)]
})
}

/// Check that no arguments were provided to the macro attribute
fn check_args_empty(&self, args: TokenStream) -> Result<()> {
if args.is_empty() {
Ok(())
} else {
let args: TokenStream2 = args.into();
Err(Error::new(args.span(), "macro arguments are not allowed"))
}
}

/// Iterates through the input arguments and checks that their types match the expected types
fn check_fn_args(
&self,
inputs: &Punctuated<FnArg, Comma>,
expected_types: &[&str],
errors: &mut Option<Error>,
) {
let mut expected_iter = expected_types.iter();
for arg in inputs.iter() {
match expected_iter.next() {
Some(expected) => {
if let Err(e) = check_arg_type(arg, expected) {
combine_err(errors, e);
}
}
None => {
combine_err(errors, Error::new(arg.span(), "too many input arguments"));
}
}
}
}
}

/// Combine a new error into an optional accumulator
fn combine_err(acc: &mut Option<Error>, err: Error) {
match acc {
Some(e) => e.combine(err),
None => *acc = Some(err),
}
}

/// Check if a function argument matches the expected type
fn check_arg_type(arg: &FnArg, expected: &str) -> Result<()> {
match arg {
FnArg::Typed(argument) => {
if !is_correct_type(&argument.ty, expected) {
Err(Error::new(
argument.ty.span(),
format!("argument type must be `{expected}`"),
))
} else {
Ok(())
}
}
FnArg::Receiver(_) => Err(Error::new(arg.span(), "invalid argument")),
}
}

/// Check if a type matches the expected type name
fn is_correct_type(ty: &Type, expected: &str) -> bool {
let correct: Type = syn::parse_str(expected).unwrap();
if let Some(ty) = strip_type_path(ty) {
ty == correct
} else {
false
}
}

/// Strip the path of a type, returning only the last segment (e.g., `core::usize` -> `usize`)
fn strip_type_path(ty: &Type) -> Option<Type> {
match ty {
Type::Ptr(ty) => {
let mut ty = ty.clone();
*ty.elem = strip_type_path(&ty.elem)?;
Some(Type::Ptr(ty))
}
Type::Path(ty) => {
let mut ty = ty.clone();
let last_segment = ty.path.segments.last().unwrap().clone();
ty.path.segments = Punctuated::new();
ty.path.segments.push_value(last_segment);
Some(Type::Path(ty))
}
_ => None,
}
}

/// Make sure the output type is either `()` or absent
fn check_output_empty(output: &ReturnType, errors: &mut Option<Error>) {
match output {
ReturnType::Default => {}
ReturnType::Type(_, ty) => match **ty {
Type::Tuple(ref tuple) => {
if !tuple.elems.is_empty() {
combine_err(errors, Error::new(tuple.span(), "return type must be ()"));
}
}
_ => combine_err(errors, Error::new(ty.span(), "return type must be ()")),
},
}
}

/// Make sure the output type is `!` (never)
fn check_output_never(output: &ReturnType, errors: &mut Option<Error>) {
if !matches!(output, ReturnType::Type(_, ty) if matches!(**ty, Type::Never(_))) {
combine_err(errors, Error::new(output.span(), "return type must be !"));
}
}
Loading