Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement system calls with attribute proc macro #1105

Merged
merged 12 commits into from
Mar 23, 2024
Merged
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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ jobs:
run: cargo test --lib
env:
RUSTFLAGS: -Awarnings
- name: Macro unit tests
run: cargo test --package hermit-macro
- name: Download loader
run: gh release download --repo hermit-os/loader --pattern hermit-loader-x86_64
- run: rustup target add x86_64-unknown-none
Expand Down
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ semihosting = ["dep:semihosting"]
shell = ["simple-shell"]

[dependencies]
hermit-macro = { path = "hermit-macro" }
ahash = { version = "0.8", default-features = false }
align-address = "0.1"
bit_field = "0.10"
Expand Down Expand Up @@ -152,6 +153,7 @@ llvm-tools = "0.1"

[workspace]
members = [
"hermit-macro",
"xtask",
]
exclude = [
Expand Down
12 changes: 12 additions & 0 deletions hermit-macro/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "hermit-macro"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true

[dependencies]
proc-macro2 = "1"
quote = "1"
syn = { version = "2", features = ["full"] }
22 changes: 22 additions & 0 deletions hermit-macro/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use proc_macro::TokenStream;
use quote::ToTokens;
use syn::parse::Nothing;
use syn::parse_macro_input;

macro_rules! bail {
($span:expr, $($tt:tt)*) => {
return Err(syn::Error::new_spanned($span, format!($($tt)*)))
};
}

mod system;

// The structure of this implementation is inspired by Amanieu's excellent naked-function crate.
#[proc_macro_attribute]
pub fn system(attr: TokenStream, item: TokenStream) -> TokenStream {
parse_macro_input!(attr as Nothing);
match system::system_attribute(parse_macro_input!(item)) {
Ok(item) => item.into_token_stream().into(),
Err(e) => e.to_compile_error().into(),
}
}
195 changes: 195 additions & 0 deletions hermit-macro/src/system.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
use proc_macro2::{Ident, Span};
use quote::quote;
use syn::{Abi, Attribute, Item, ItemFn, Pat, Result, Signature, Visibility};

fn validate_vis(vis: &Visibility) -> Result<()> {
if !matches!(vis, Visibility::Public(_)) {
bail!(vis, "#[system] functions must be public");
}

Ok(())
}

struct ParsedSig {
args: Vec<Ident>,
}

fn parse_sig(sig: &Signature) -> Result<ParsedSig> {
if let Some(constness) = sig.constness {
bail!(constness, "#[system] is not supported on const functions");
}
if let Some(asyncness) = sig.asyncness {
bail!(asyncness, "#[system] is not supported on async functions");
}
match &sig.abi {
Some(Abi {
extern_token: _,
name: Some(name),
}) if matches!(&*name.value(), "C" | "C-unwind") => {}
_ => bail!(
&sig.abi,
"#[system] functions must be `extern \"C\"` or `extern \"C-unwind\"`"
),
}
if !sig.generics.params.is_empty() {
bail!(
&sig.generics,
"#[system] cannot be used with generic functions"
);
}
if !sig.ident.to_string().starts_with("sys_") {
bail!(&sig.ident, "#[system] functions must start with `sys_`");
}

let mut args = vec![];

for arg in &sig.inputs {
let pat = match arg {
syn::FnArg::Receiver(_) => bail!(arg, "#[system] functions cannot take `self`"),
syn::FnArg::Typed(pat) => pat,
};
if let Pat::Ident(pat) = &*pat.pat {
args.push(pat.ident.clone());
} else {
bail!(pat, "unsupported pattern in #[system] function argument");
}
}

Ok(ParsedSig { args })
}

fn validate_attrs(attrs: &[Attribute]) -> Result<()> {
for attr in attrs {
if !attr.path().is_ident("cfg") && !attr.path().is_ident("doc") {
bail!(
attr,
"#[system] functions may only have `#[doc]` and `#[cfg]` attributes"
);
}
}

Ok(())
}

fn emit_func(mut func: ItemFn, sig: &ParsedSig) -> Result<ItemFn> {
let args = &sig.args;
let attrs = func.attrs.clone();
let vis = func.vis.clone();
let sig = func.sig.clone();

let ident = Ident::new(&format!("__{}", func.sig.ident), Span::call_site());
func.sig.ident = ident.clone();
func.vis = Visibility::Inherited;
func.attrs.clear();

let func_call = quote! {
kernel_function!(#ident(#(#args),*))
};

let func_call = if func.sig.unsafety.is_some() {
quote! {
unsafe { #func_call }
}
} else {
func_call
};

let func = syn::parse2(quote! {
#(#attrs)*
#[no_mangle]
#vis #sig {
#func

#func_call
}
})?;

Ok(func)
}

pub fn system_attribute(func: ItemFn) -> Result<Item> {
validate_vis(&func.vis)?;
let sig = parse_sig(&func.sig)?;
validate_attrs(&func.attrs)?;
let func = emit_func(func, &sig)?;
Ok(Item::Fn(func))
}

#[cfg(test)]
mod tests {
use quote::ToTokens;

use super::*;

#[test]
fn test_safe() -> Result<()> {
let input = syn::parse2(quote! {
/// Adds two numbers together.
///
/// This is very important.
#[cfg(target_os = "none")]
pub extern "C" fn sys_test(a: i8, b: i16) -> i32 {
let c = i16::from(a) + b;
i32::from(c)
}
})?;

let expected = quote! {
/// Adds two numbers together.
///
/// This is very important.
#[cfg(target_os = "none")]
#[no_mangle]
pub extern "C" fn sys_test(a: i8, b: i16) -> i32 {
extern "C" fn __sys_test(a: i8, b: i16) -> i32 {
let c = i16::from(a) + b;
i32::from(c)
}

kernel_function!(__sys_test(a, b))
}
};

let result = system_attribute(input)?.into_token_stream();

assert_eq!(expected.to_string(), result.to_string());

Ok(())
}

#[test]
fn test_unsafe() -> Result<()> {
let input = syn::parse2(quote! {
/// Adds two numbers together.
///
/// This is very important.
#[cfg(target_os = "none")]
pub unsafe extern "C" fn sys_test(a: i8, b: i16) -> i32 {
let c = i16::from(a) + b;
i32::from(c)
}
})?;

let expected = quote! {
/// Adds two numbers together.
///
/// This is very important.
#[cfg(target_os = "none")]
#[no_mangle]
pub unsafe extern "C" fn sys_test(a: i8, b: i16) -> i32 {
unsafe extern "C" fn __sys_test(a: i8, b: i16) -> i32 {
let c = i16::from(a) + b;
i32::from(c)
}

unsafe { kernel_function!(__sys_test(a, b)) }
}
};

let result = system_attribute(input)?.into_token_stream();

assert_eq!(expected.to_string(), result.to_string());

Ok(())
}
}
2 changes: 1 addition & 1 deletion src/arch/aarch64/kernel/scheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ extern "C" fn task_start(_f: extern "C" fn(usize), _arg: usize) -> ! {
}

impl TaskFrame for Task {
fn create_stack_frame(&mut self, func: extern "C" fn(usize), arg: usize) {
fn create_stack_frame(&mut self, func: unsafe extern "C" fn(usize), arg: usize) {
// Check if TLS is allocated already and if the task uses thread-local storage.
if self.tls.is_none() {
self.tls = TaskTLS::from_environment();
Expand Down
4 changes: 2 additions & 2 deletions src/arch/aarch64/kernel/switch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ macro_rules! kernel_function_impl {
($kernel_function:ident($($arg:ident: $A:ident),*) { $($operands:tt)* }) => {
/// Executes `f` on the kernel stack.
#[allow(dead_code)]
pub fn $kernel_function<R, $($A),*>(f: unsafe extern "C" fn($($A),*) -> R, $($arg: $A),*) -> R {
pub unsafe fn $kernel_function<R, $($A),*>(f: unsafe extern "C" fn($($A),*) -> R, $($arg: $A),*) -> R {
unsafe {
assert!(mem::size_of::<R>() <= mem::size_of::<usize>());

Expand All @@ -14,7 +14,7 @@ macro_rules! kernel_function_impl {
let $arg = {
let mut reg = 0_usize;
// SAFETY: $A is smaller than usize and directly fits in a register
// Since f takes $A as argument via C calling convention, any opper bytes do not matter.
// Since f takes $A as argument via C calling convention, any upper bytes do not matter.
ptr::write(ptr::from_mut(&mut reg) as _, $arg);
reg
};
Expand Down
2 changes: 1 addition & 1 deletion src/arch/riscv64/kernel/scheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ extern "C" fn task_entry(func: extern "C" fn(usize), arg: usize) {
}

impl TaskFrame for Task {
fn create_stack_frame(&mut self, func: extern "C" fn(usize), arg: usize) {
fn create_stack_frame(&mut self, func: unsafe extern "C" fn(usize), arg: usize) {
// Check if the task (process or thread) uses Thread-Local-Storage.
// check is TLS is already allocated
if self.tls.is_none() {
Expand Down
2 changes: 1 addition & 1 deletion src/arch/x86_64/kernel/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -846,7 +846,7 @@ pub fn configure() {
#[cfg(feature = "fsgsbase")]
if !supports_fsgs() {
error!("FSGSBASE support is enabled, but the processor doesn't support it!");
crate::__sys_shutdown(1);
crate::shutdown(1);
}

debug!("Set CR4 to {:#x}", cr4);
Expand Down
2 changes: 1 addition & 1 deletion src/arch/x86_64/kernel/scheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ extern "C" fn task_entry(func: extern "C" fn(usize), arg: usize) -> ! {
}

impl TaskFrame for Task {
fn create_stack_frame(&mut self, func: extern "C" fn(usize), arg: usize) {
fn create_stack_frame(&mut self, func: unsafe extern "C" fn(usize), arg: usize) {
// Check if TLS is allocated already and if the task uses thread-local storage.
#[cfg(not(feature = "common-os"))]
if self.tls.is_none() {
Expand Down
4 changes: 2 additions & 2 deletions src/arch/x86_64/kernel/switch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ macro_rules! kernel_function_impl {
($kernel_function:ident($($arg:ident: $A:ident),*) { $($operands:tt)* }) => {
/// Executes `f` on the kernel stack.
#[allow(dead_code)]
pub fn $kernel_function<R, $($A),*>(f: unsafe extern "C" fn($($A),*) -> R, $($arg: $A),*) -> R {
pub unsafe fn $kernel_function<R, $($A),*>(f: unsafe extern "C" fn($($A),*) -> R, $($arg: $A),*) -> R {
unsafe {
assert!(mem::size_of::<R>() <= mem::size_of::<usize>());

Expand All @@ -229,7 +229,7 @@ macro_rules! kernel_function_impl {
let $arg = {
let mut reg = 0_usize;
// SAFETY: $A is smaller than usize and directly fits in a register
// Since f takes $A as argument via C calling convention, any opper bytes do not matter.
// Since f takes $A as argument via C calling convention, any upper bytes do not matter.
ptr::write(ptr::from_mut(&mut reg) as _, $arg);
reg
};
Expand Down
Loading
Loading