Automatically generate Candid from Rust on the IC

How to auto-generate the Candid declaration from Rust code on the Internet Computer.

Mar 12, 2023

#rust #programming #webdevelopment #candid

Follow my Instagram @karsten.wuerth

Karsten Würth on Unsplash


Update July 22nd, 2023: There is now a better way to automatically generate Candid from Rust. Check out my new blog post about that subject!


The ability to auto-generate the Candid declaration from Rust code on the Internet Computer is expected to become available in the second quarter of 2023.

In the meantime, a workaround can be used to generate these types, which I notably use in my open-source Blockchain-as-a-Service project, Juno.

Here’s how you can implement the workaround yourself.


1. Annotate

Because this solution involves a workaround, the first step is to annotate the public methods that need to be exported to Candid types.

To do this, use the candid\_method macro of the Candid crate, specifying the export type and, if necessary, the query type. The default is update.

use ic_cdk_macros::{query, update}; use ic_cdk::export::candid::{candid_method}; #[candid_method(query)] #[query] fn hello(name: String) -> String { format!("Hello, {}!", name) } #[candid_method] #[update] fn world(name: String) -> String { format!("World, {}!", name) }

2. Collect and generate

To collect the methods that we need to generate their declarations, we use the export\_service macro from the Candid crate. We add a query method and prefix its name with two underscores, as it has no practical purpose for our canister.

use ic_cdk::export::candid::{export_service}; #[query(name = "__get_candid_interface_tmp_hack")] fn export_candid() -> String { export_service!(); __export_service() }

3. Save to file system with a test

Since we need a hook to initiate the generation of the DID files, we use the test runner to execute the necessary steps. This is why we use a test module for this purpose.

#[cfg(test)] mod tests { use super::export_candid; #[test] fn save_candid() { use std::env; use std::fs::write; use std::path::PathBuf; let dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); let dir = dir .parent() .unwrap() .parent() .unwrap() .join("src") .join("demo"); write(dir.join("demo.did"), export_candid()).expect("Write failed."); } }

The above snippet is designed for a canister named demo and a file structure relative to a standard sample architecture root/src/demo/Cargo.toml. Please update the path and name based on your project requirements.

That's all! 😁 Simply run cargo test and a did file should be automatically generated.

service : { hello : (text) -> (text) query; world : (text) -> (text) }

Conclusion

Of course, you can always wait until Q2 or avoid these shenanigans by using Juno (😄). Nonetheless, I hope this short blog post will be useful to you.

To infinity and beyond!
David