Skip to content

Commit

Permalink
Merge pull request #232 from bitfinity-network/maxim/export_candid
Browse files Browse the repository at this point in the history
feature: Add `export_candid` attribute macro
  • Loading branch information
ufoscout authored Sep 9, 2024
2 parents 010dab7 + 7bd48c9 commit ea99e9f
Show file tree
Hide file tree
Showing 17 changed files with 181 additions and 2 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ syn = "2.0"
tempfile = "3.6"
thiserror = "1.0"
tokio = "1.0"
trybuild = "1.0"

# IC dependencies
candid = "0.10"
Expand Down
3 changes: 3 additions & 0 deletions ic-canister/ic-canister-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@ serde = { workspace = true }
serde_tokenstream = { workspace = true }
syn = { workspace = true, features = ["extra-traits"] }

[dev-dependencies]
trybuild = { workspace = true }

[package.metadata.cargo-udeps.ignore]
ic-exports = { path = "../../ic-exports" }
63 changes: 63 additions & 0 deletions ic-canister/ic-canister-macros/src/export_candid.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use serde::Deserialize;
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
use syn::{parse_macro_input, ItemFn, ReturnType};

#[derive(Default, Deserialize, Debug)]
struct ExportCandidAttr {}

impl Parse for ExportCandidAttr {
fn parse(input: ParseStream) -> syn::Result<Self> {
if input.is_empty() {
Ok(Self {})
} else {
Err(syn::Error::new(
input.span(),
"unexpected attribute argument",
))
}
}
}

pub(crate) fn export_candid(attr: TokenStream, input: TokenStream) -> TokenStream {
let _ = parse_macro_input!(attr as ExportCandidAttr);

let input_fn = parse_macro_input!(input as ItemFn);
let input_fn_name = input_fn.sig.ident.clone();

match input_fn.sig.output {
ReturnType::Default => {
return syn::Error::new(
input_fn.sig.span(),
"`#[export_candid]` function must return `String`",
)
.to_compile_error()
.into();
}
ReturnType::Type(_, ref ty) => {
if ty.to_token_stream().to_string() != "String" {
return syn::Error::new(
ty.span(),
"`#[export_candid]` function must return `String`",
)
.to_compile_error()
.into();
}
}
};

let result = quote! {
#input_fn

#[no_mangle]
pub fn get_candid_pointer() -> *mut std::os::raw::c_char {
let candid_string: String = #input_fn_name();
let c_string = std::ffi::CString::new(candid_string).unwrap();
c_string.into_raw()
}
};

result.into()
}
17 changes: 17 additions & 0 deletions ic-canister/ic-canister-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use proc_macro::TokenStream;
mod api;
mod canister_call;
mod derive;
mod export_candid;

/// Makes an inter-canister call. This macro takes two inputs: the canister method invocation,
/// and the expected return type. The result type of invocation is `async CallResult`:
Expand Down Expand Up @@ -160,6 +161,22 @@ pub fn generate_exports(input: TokenStream) -> TokenStream {
api::generate_exports(input)
}

/// Allows `candid-extractor` tool to get Candid definition of the canister
///
/// This attribute macro can be used on any function returning a `String` value, and will return
/// this value when called by `candid-extractor` on your canister wasm. This means that you can
/// provide the Candid definition by hand, or generate it by the canister code.
///
/// # Details
///
/// This macro creates a function named `get_candid_pointer` in the scope of the function that uses
/// the attribute, and exports it for dynamic linking. Then `candid-extractor` runs the `wasm` code
/// and calls the method to get the candid definition.
#[proc_macro_attribute]
pub fn export_candid(attr: TokenStream, item: TokenStream) -> TokenStream {
export_candid::export_candid(attr, item)
}

/// Derives [Canister] trait for a struct.
#[proc_macro_derive(Canister, attributes(id, state, canister_no_upgrade_methods))]
pub fn derive_canister(input: TokenStream) -> TokenStream {
Expand Down
11 changes: 11 additions & 0 deletions ic-canister/ic-canister-macros/tests/export_candid.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#[test]
fn not_expended() {
let tests = trybuild::TestCases::new();
tests.compile_fail("tests/export_candid/not_expended/*.rs");
}

#[test]
fn expended() {
let tests = trybuild::TestCases::new();
tests.pass("tests/export_candid/expended/*.rs");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
fn main() {}

#[ic_canister_macros::export_candid()]
fn did() -> String {
panic!()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
fn main() {}

#[ic_canister_macros::export_candid]
fn did() -> String {
panic!()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
fn main() {}

#[ic_canister_macros::export_candid(attribute)]
fn did() -> String {
panic!()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: unexpected attribute argument
--> tests/export_candid/not_expended/invalid_attr_arguments.rs:3:37
|
3 | #[ic_canister_macros::export_candid(attribute)]
| ^^^^^^^^^
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
fn main() {}

#[ic_canister_macros::export_candid]
struct Did {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: expected `fn`
--> tests/export_candid/not_expended/invalid_item_type.rs:4:1
|
4 | struct Did {}
| ^^^^^^
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
fn main() {}

#[ic_canister_macros::export_candid]
fn did() -> () {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: `#[export_candid]` function must return `String`
--> tests/export_candid/not_expended/invalid_return_type.rs:4:13
|
4 | fn did() -> () {}
| ^^
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
fn main() {}

#[ic_canister_macros::export_candid]
fn did() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: `#[export_candid]` function must return `String`
--> tests/export_candid/not_expended/no_return_type.rs:4:1
|
4 | fn did() {}
| ^^
1 change: 1 addition & 0 deletions ic-canister/ic-canister/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ ic-canister-macros = { path = "../ic-canister-macros" }
ic-exports = { path = "../../ic-exports" }

[dev-dependencies]
candid = { workspace = true }
serde = { workspace = true }
37 changes: 35 additions & 2 deletions ic-canister/ic-canister/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -511,8 +511,41 @@
//!
//! # Generating idl
//!
//! You can generate IDL (Candid) definition for your canister using [generate_idl] macro and then compile it via `candid::bindings::candid::compile()`.

//! You can generate IDL (Candid) definition for your canister using [generate_idl] macro and then
//! compile it via `candid::bindings::candid::compile()`.
//!
//! To use `candid-extractor` tool to get the idl from canister `.wasm` file, add a
//! [`#[export_candid]`](export_candid) method in your `lib.rs`.
//!
//! ```
//! use candid::Principal;
//! use ic_canister::{Canister, PreUpdate, query, generate_idl, export_candid, Idl};
//!
//! #[derive(Clone, Canister)]
//! struct MyCanister {
//! #[id]
//! principal: Principal,
//! }
//!
//! impl MyCanister {
//! #[query]
//! fn hello(&self) -> String {
//! "Hello IC".into()
//! }
//!
//! fn idl() -> Idl {
//! generate_idl!()
//! }
//! }
//!
//! impl PreUpdate for MyCanister {}
//!
//! #[export_candid]
//! fn export_candid() -> String {
//! let idl = MyCanister::idl();
//! candid::pretty::candid::compile(&idl.env.env, &Some(idl.actor))
//! }
//! ```
use std::cell::RefCell;
use std::collections::HashMap;
use std::future::Future;
Expand Down

0 comments on commit ea99e9f

Please sign in to comment.