diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..531ddd1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,55 @@ +name: CI + +on: [push, pull_request] + +jobs: + ci: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + rust-toolchain: [nightly] + targets: [x86_64-unknown-linux-gnu, x86_64-unknown-none, riscv64gc-unknown-none-elf, aarch64-unknown-none-softfloat] + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@nightly + with: + toolchain: ${{ matrix.rust-toolchain }} + components: rust-src, clippy, rustfmt + targets: ${{ matrix.targets }} + - name: Check rust version + run: rustc --version --verbose + - name: Check code format + run: cargo fmt --all -- --check + - name: Clippy + run: cargo clippy --target ${{ matrix.targets }} --all-features -- -A clippy::new_without_default + - name: Build + run: cargo build --target ${{ matrix.targets }} --all-features + - name: Unit test + if: ${{ matrix.targets == 'x86_64-unknown-linux-gnu' }} + run: cargo test --target ${{ matrix.targets }} -- --nocapture + + doc: + runs-on: ubuntu-latest + strategy: + fail-fast: false + permissions: + contents: write + env: + default-branch: ${{ format('refs/heads/{0}', github.event.repository.default_branch) }} + RUSTDOCFLAGS: -D rustdoc::broken_intra_doc_links -D missing-docs + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@nightly + - name: Build docs + continue-on-error: ${{ github.ref != env.default-branch && github.event_name != 'pull_request' }} + run: | + cargo doc --no-deps --all-features + printf '' $(cargo tree | head -1 | cut -d' ' -f1) > target/doc/index.html + - name: Deploy to Github Pages + if: ${{ github.ref == env.default-branch }} + uses: JamesIves/github-pages-deploy-action@v4 + with: + single-commit: true + branch: gh-pages + folder: target/doc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ff78c42 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +/.vscode +.DS_Store +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6ba8697 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "crate_interface" +version = "0.1.1" +edition = "2021" +authors = ["Yuekai Jia "] +description = "Provides a way to define an interface (trait) in a crate, but can implement or use it in any crate." +license = "GPL-3.0-or-later OR Apache-2.0 OR MulanPSL-2.0" +homepage = "https://github.com/arceos-org/arceos" +repository = "https://github.com/arceos-org/crate_interface" +keywords = ["arceos", "api", "macro"] +categories = ["development-tools::procedural-macro-helpers", "no-std"] + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "2.0", features = ["full"] } + +[lib] +proc-macro = true diff --git a/README.md b/README.md new file mode 100644 index 0000000..ca910ca --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# crate_interface + +[![Crates.io](https://img.shields.io/crates/v/crate_interface)](https://crates.io/crates/crate_interface) + +Provides a way to **define** an interface (trait) in a crate, but can +**implement** or **use** it in any crate. It 's usually used to solve +the problem of *circular dependencies* between crates. + +## Example + +```rust +// Define the interface +#[crate_interface::def_interface] +pub trait HelloIf { + fn hello(&self, name: &str, id: usize) -> String; +} + +// Implement the interface in any crate +struct HelloIfImpl; + +#[crate_interface::impl_interface] +impl HelloIf for HelloIfImpl { + fn hello(&self, name: &str, id: usize) -> String { + format!("Hello, {} {}!", name, id) + } +} + +// Call `HelloIfImpl::hello` in any crate +use crate_interface::call_interface; +assert_eq!( + call_interface!(HelloIf::hello("world", 123)), + "Hello, world 123!" +); +assert_eq!( + call_interface!(HelloIf::hello, "rust", 456), // another calling style + "Hello, rust 456!" +); +``` diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..adc2621 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,187 @@ +#![doc = include_str!("../README.md")] +#![feature(iter_next_chunk)] + +use proc_macro::TokenStream; +use proc_macro2::Span; +use quote::{format_ident, quote}; +use syn::{Error, FnArg, ImplItem, ImplItemFn, ItemImpl, ItemTrait, TraitItem, Type}; + +fn compiler_error(err: Error) -> TokenStream { + err.to_compile_error().into() +} + +/// Define an interface. +/// +/// This attribute should be added above the definition of a trait. All traits +/// that use the attribute cannot have the same name. +/// +/// It is not necessary to define it in the same crate as the implementation, +/// but it is required that these crates are linked together. +/// +/// See the [crate-level documentation](crate) for more details. +#[proc_macro_attribute] +pub fn def_interface(attr: TokenStream, item: TokenStream) -> TokenStream { + if !attr.is_empty() { + return compiler_error(Error::new( + Span::call_site(), + "expect an empty attribute: `#[crate_interface_def]`", + )); + } + + let ast = syn::parse_macro_input!(item as ItemTrait); + let trait_name = &ast.ident; + + let mut extern_fn_list = vec![]; + for item in &ast.items { + if let TraitItem::Fn(method) = item { + let mut sig = method.sig.clone(); + let fn_name = &sig.ident; + sig.ident = format_ident!("__{}_{}", trait_name, fn_name); + sig.inputs = syn::punctuated::Punctuated::new(); + + for arg in &method.sig.inputs { + if let FnArg::Typed(_) = arg { + sig.inputs.push(arg.clone()); + } + } + + let extern_fn = quote! { + #sig; + }; + extern_fn_list.push(extern_fn); + } + } + + quote! { + #ast + extern "Rust" { + #(#extern_fn_list)* + } + } + .into() +} + +/// Implement the interface for a struct. +/// +/// This attribute should be added above the implementation of a trait for a +/// struct, and the trait must be defined with +/// [`#[def_interface]`](macro@crate::def_interface). +/// +/// It is not necessary to implement it in the same crate as the definition, but +/// it is required that these crates are linked together. +/// +/// See the [crate-level documentation](crate) for more details. +#[proc_macro_attribute] +pub fn impl_interface(attr: TokenStream, item: TokenStream) -> TokenStream { + if !attr.is_empty() { + return compiler_error(Error::new( + Span::call_site(), + "expect an empty attribute: `#[crate_interface_impl]`", + )); + } + + let mut ast = syn::parse_macro_input!(item as ItemImpl); + let trait_name = if let Some((_, path, _)) = &ast.trait_ { + &path.segments.last().unwrap().ident + } else { + return compiler_error(Error::new_spanned(ast, "expect a trait implementation")); + }; + let impl_name = if let Type::Path(path) = &ast.self_ty.as_ref() { + path.path.get_ident().unwrap() + } else { + return compiler_error(Error::new_spanned(ast, "expect a trait implementation")); + }; + + for item in &mut ast.items { + if let ImplItem::Fn(method) = item { + let (attrs, vis, sig, stmts) = + (&method.attrs, &method.vis, &method.sig, &method.block.stmts); + let fn_name = &sig.ident; + let extern_fn_name = format_ident!("__{}_{}", trait_name, fn_name).to_string(); + + let mut new_sig = sig.clone(); + new_sig.ident = format_ident!("{}", extern_fn_name); + new_sig.inputs = syn::punctuated::Punctuated::new(); + + let mut args = vec![]; + let mut has_self = false; + for arg in &sig.inputs { + match arg { + FnArg::Receiver(_) => has_self = true, + FnArg::Typed(ty) => { + args.push(ty.pat.clone()); + new_sig.inputs.push(arg.clone()); + } + } + } + + let call_impl = if has_self { + quote! { + let IMPL: #impl_name = #impl_name; + IMPL.#fn_name( #(#args),* ) + } + } else { + quote! { #impl_name::#fn_name( #(#args),* ) } + }; + + let item = quote! { + #(#attrs)* + #vis + #sig + { + { + #[export_name = #extern_fn_name] + extern "Rust" #new_sig { + #call_impl + } + } + #(#stmts)* + } + } + .into(); + *method = syn::parse_macro_input!(item as ImplItemFn); + } + } + + quote! { #ast }.into() +} + +/// Call a function in the interface. +/// +/// It is not necessary to call it in the same crate as the implementation, but +/// it is required that these crates are linked together. +/// +/// See the [crate-level documentation](crate) for more details. +#[proc_macro] +pub fn call_interface(item: TokenStream) -> TokenStream { + parse_call_interface(item) + .unwrap_or_else(|msg| compiler_error(Error::new(Span::call_site(), msg))) +} + +fn parse_call_interface(item: TokenStream) -> Result { + let mut iter = item.into_iter(); + let tt = iter + .next_chunk::<4>() + .or(Err("expect `Trait::func`"))? + .map(|t| t.to_string()); + + let trait_name = &tt[0]; + if tt[1] != ":" || tt[2] != ":" { + return Err("missing `::`".into()); + } + let fn_name = &tt[3]; + let extern_fn_name = format!("__{}_{}", trait_name, fn_name); + + let mut args = iter.map(|x| x.to_string()).collect::>().join(""); + if args.starts_with(',') { + args.remove(0); + } else if args.starts_with('(') && args.ends_with(')') { + args.remove(0); + args.pop(); + } + + let call = format!("unsafe {{ {}( {} ) }}", extern_fn_name, args); + Ok(call + .parse::() + .or(Err("expect a correct argument list"))?) +} diff --git a/tests/test_crate_interface.rs b/tests/test_crate_interface.rs new file mode 100644 index 0000000..2c616ea --- /dev/null +++ b/tests/test_crate_interface.rs @@ -0,0 +1,34 @@ +use crate_interface::*; + +#[def_interface] +trait SimpleIf { + fn foo() -> u32 { + 123 + } + + /// Test comments + fn bar(&self, a: u16, b: &[u8], c: &str); +} + +struct SimpleIfImpl; + +#[impl_interface] +impl SimpleIf for SimpleIfImpl { + #[inline] + fn foo() -> u32 { + 456 + } + + /// Test comments2 + fn bar(&self, a: u16, b: &[u8], c: &str) { + println!("{} {:?} {}", a, b, c); + assert_eq!(b[1], 3); + } +} + +#[test] +fn test_crate_interface_call() { + call_interface!(SimpleIf::bar, 123, &[2, 3, 5, 7, 11], "test"); + call_interface!(SimpleIf::bar(123, &[2, 3, 5, 7, 11], "test")); + assert_eq!(call_interface!(SimpleIf::foo), 456); +}