Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

Candid Type Generation

This example shows how to automatically generate Rust types from a Candid interface definition (.did file), eliminating the need to manually copy and maintain type definitions from external canisters.

The example deploys a canister that calls the NNS Governance canister using types generated directly from its live Candid interface. The local network is configured with nns: true so that NNS canisters are available at their mainnet IDs during local development and testing.

How it works

candid/nns_governance.did          ← Candid interface fetched from the live canister
        ↓  (build.rs)
$OUT_DIR/nns_governance.rs         ← Rust types generated at build time
        ↓  (include! in declarations/mod.rs)
declarations::nns_governance::*    ← Types available in your canister code

build.rs

The build script uses ic-cdk-bindgen to generate Rust types from the .did file before the main compilation starts:

Config::new("nns_governance", "candid/nns_governance.did")
    .static_callee(Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap())
    .set_type_selector_config("candid/nns_governance.toml")
    .generate();
  • Config::new(name, candid_path)name becomes the output filename (nns_governance.rs)
  • .static_callee(principal) — embeds the target canister ID as a constant in the generated code. Use .dynamic_callee("ENV_VAR_NAME") when the canister ID varies across environments.
  • .set_type_selector_config(path) — TOML file controlling which traits are derived on generated types
  • .generate() — writes to $OUT_DIR/nns_governance.rs (outside of src/, not committed to git)

The cargo:rerun-if-changed directives ensure the build script only re-runs when the .did or .toml files change, keeping incremental rebuilds fast.

Type Selector Configuration

candid/nns_governance.toml controls how Candid types are mapped to Rust:

[rust]
visibility = "pub"
attributes = "#[derive(CandidType, Deserialize, Debug, Clone, serde::Serialize)]"

By default, ic-cdk-bindgen derives CandidType and Deserialize on all types. The attributes field adds Debug, Clone, and serde::Serialize — necessary here because the example uses serde_json::to_string_pretty to display the response.

See the Type Selector specification for the full configuration syntax.

Including the generated code

The generated file lives in Cargo's build output directory and is brought into scope via include!:

// backend/src/declarations/mod.rs
pub mod nns_governance {
    include!(concat!(env!("OUT_DIR"), "/nns_governance.rs"));
}

The include! macro pastes the file contents at compile time, exactly as if you had written the code directly. This is the standard pattern for ic-cdk-bindgen 0.2.0+. The types are then available in your canister:

use declarations::nns_governance::{ListNeurons, ListNeuronsResponse};

Using the generated types

Generated code for a .static_callee canister provides top-level async functions and a CANISTER_ID constant:

// Generated in $OUT_DIR/nns_governance.rs
pub const CANISTER_ID: Principal = Principal::from_slice(&[/* ... */]);

pub async fn list_neurons(arg: &ListNeurons) -> CallResult<ListNeuronsResponse> {
    Ok(Call::bounded_wait(CANISTER_ID, "list_neurons").with_arg(arg).await?.candid()?)
}
// ... one function per canister method

Calling a method is a single typed function call — no raw Principal arguments or string method names:

declarations::nns_governance::list_neurons(&request).await

Fetching and updating Candid definitions

candid/nns_governance.did is already checked in and ready to use. To update it from the live canister:

bash scripts/fetch_candid.sh

This fetches the candid:service metadata from the NNS Governance canister on the IC mainnet and writes it to candid/nns_governance.did.

After refreshing, rebuild to regenerate the Rust types:

icp build backend

If the Candid interface changed in a way that breaks your code (a removed method, a renamed required field), you will get a compile error — not a runtime failure. Note that Candid-compatible changes like adding an optional field also require updating code that constructs request structs, even though the .did file itself is still valid.

Reproducibility note: Always commit the .did file. The generated Rust ($OUT_DIR/nns_governance.rs) is deterministically derived from it at build time, so committing the .did is sufficient to reproduce any historical build exactly. Regenerating from mainnet at an arbitrary time would produce different types if the canister interface had changed, making it impossible to reproduce historical WASM bytes — which matters for any third-party verification.

Possible enhancement: Run a bot to fetch the latest .did file from mainnet on a regular cadence and open a PR if the project still compiles. This keeps types current with zero manual effort.

Advantages over manual type copying

Manual copying ic-cdk-bindgen
Keeping types current Track upstream changes manually Re-run fetch_candid.sh
Missing dependent types Easy to overlook Full interface generated
Interface mismatches Runtime errors Build-time compile errors
Maintenance Ongoing manual work Minimal

Prerequisites

  • Node.js v18+
  • icp-cli: npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm
  • Rust with wasm32-unknown-unknown target: rustup target add wasm32-unknown-unknown

Deploy and test

git clone https://github.com/dfinity/examples
cd examples/rust/candid_type_generation
icp network start -d
icp deploy
bash test.sh
icp network stop

The local network starts with nns: true (configured in icp.yaml), which deploys all NNS canisters at their mainnet canister IDs. This means the inter-canister call to NNS Governance works identically in local development and on the IC mainnet.

Security considerations and best practices

For information about security best practices when developing ICP canisters, see https://docs.internetcomputer.org/guides/security/overview