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);
+}