Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
equation314 committed Jul 11, 2024
0 parents commit cd65392
Show file tree
Hide file tree
Showing 6 changed files with 337 additions and 0 deletions.
55 changes: 55 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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 '<meta http-equiv="refresh" content="0;url=%s/index.html">' $(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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/target
/.vscode
.DS_Store
Cargo.lock
19 changes: 19 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "crate_interface"
version = "0.1.1"
edition = "2021"
authors = ["Yuekai Jia <[email protected]>"]
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
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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!"
);
```
187 changes: 187 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<TokenStream, String> {
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::<Vec<_>>().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::<TokenStream>()
.or(Err("expect a correct argument list"))?)
}
34 changes: 34 additions & 0 deletions tests/test_crate_interface.rs
Original file line number Diff line number Diff line change
@@ -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);
}

0 comments on commit cd65392

Please sign in to comment.