Worked examples

The six runnable programs in examples/, what each demonstrates, and the exact command to run it.

The repository ships six small Rust programs under examples/. Each is a single file with a main, each drives the crate through its public API, and together they cover the ideas you need before writing anything of your own: the Contract — candid-core's canonical, validated model of a Candid interface, held as a flat table of type nodes with named declarations and an optional actor — the content-addressed identities computed over it, the resolver that supplies imported sources, host values, and the limits that bound every untrusted read.

Prefer these over any snippet you find elsewhere, including the ones on this site. The Verify workflow builds and lints all six on every pull request and every push to main: cargo test --all-targets --locked and cargo clippy --all-targets --all-features --locked -- -D warnings on Linux, macOS and Windows under current stable, the same test command again at the declared 1.78 minimum supported Rust version, and the whole feature matrix on top. --all-targets includes examples, so one that drifted out of step with the API stops that workflow rather than reaching you.

Running them#

bash
git clone https://github.com/b3hr4d/candid-core
cd candid-core

cargo run --example contract_walkthrough
cargo run --example semantic_equivalence
cargo run --example trust_boundary
cargo run --example hermetic_bundle
cargo run --example host_value_validation
cargo run --example bounded_parsing

No --features flag appears there because everything the examples need is on by default: default = ["compiler", "filesystem-compiler", "host-value"]. Each example still declares its own required-features in Cargo.toml, so under a reduced feature set Cargo reports the example as skipped rather than failing to compile.

Example Needs The idea it carries
contract_walkthrough compiler Source in, a graph you can walk out
semantic_equivalence compiler Meaning and source bytes are identified separately
trust_boundary compiler A document that arrives from elsewhere is revalidated, not believed
hermetic_bundle compiler Imports resolved from data the host holds, with no filesystem
host_value_validation compiler host-value A Candid value checked against a node in the graph, losslessly
bounded_parsing compiler Untrusted bytes decoded under a policy you choose

They are listed above, and explained below, in the order that reads as a learning path.

contract_walkthrough: source in, a graph out#

The shortest path from Candid text to something you can traverse. It compiles one self-contained source string with compile_did, prints the identities, and then walks from the actor to each method's function node. Its input is deliberately recursive — type List = opt record { head: nat; tail: List } — which stays finite in the Contract because tail is an integer index back into the same type table rather than a nested value.

bash
cargo run --example contract_walkthrough
rustexamples/contract_walkthrough.rs
let contract = compilation.contract();
println!("contract identity: {}", contract.contract_id());
println!("interface identity: {:?}", contract.interface_id());
println!("canonical type nodes: {}", contract.types().len());

let Some(Actor::Service { service }) = contract.actor() else {
    return Err("walkthrough expected a service actor".into());
};
let TypeNode::Service { methods } = &contract.types()[*service as usize] else {
    return Err("actor did not reference a service node".into());
};

That is the whole traversal idiom. contract.actor() hands you the actor, whose service field is an index; contract.types()[i] resolves it, and each method's function field is another index into the same table. The program ends by printing the canonical Contract JSON, so you can read the document the rest of the toolchain consumes.

semantic_equivalence: meaning versus source bytes#

Two sources describe the same wire interface while differing in type name, comment, field order and method order. The example compiles both and asserts what the two identities each claim: interface_id covers the type graph reachable from the actor, so it is identical, while source_bundle_id covers raw source files and import edges, so it is not.

bash
cargo run --example semantic_equivalence
rustexamples/semantic_equivalence.rs
let second = compile_did(
    r#"
    // Different name, documentation, field order, and method order.
    type Transfer = record { amount: nat; owner: principal };
    service : {
      a: (Transfer) -> () query;
      z: (Transfer) -> () query;
    };
    "#,
)?;

// … the first source declares `Payload` with the fields and methods swapped …

assert_eq!(
    first.contract().interface_id(),
    second.contract().interface_id()
);
assert_ne!(
    first.source_info().unwrap().source_bundle_id(),
    second.source_info().unwrap().source_bundle_id()
);

This is the practical rule for caching. Key on source_bundle_id when you are asking "have I already compiled these exact bytes"; compare interface_id when you are asking "is this the same interface a caller sees". Content-addressed identities sets out what each one covers and what it deliberately excludes.

trust_boundary: what validation refuses#

Compile a Contract, serialize it, then hand two edited copies back to Contract::from_json — the same validation the candid-core validate subcommand runs. Both edits are rejected, for two different reasons worth knowing.

bash
cargo run --example trust_boundary
rustexamples/trust_boundary.rs
let mut injected: serde_json::Value = serde_json::from_str(&canonical_json)?;
injected["widget"] = serde_json::json!("date-picker");
let rejected = Contract::from_json(&serde_json::to_string(&injected)?).unwrap_err();
println!("unknown core metadata rejected: {rejected}");

let mut tampered: serde_json::Value = serde_json::from_str(&canonical_json)?;
tampered["identities"]["contract"] = serde_json::json!(
    "candid-core:contract:v1:sha256:0000000000000000000000000000000000000000000000000000000000000000"
);
let rejected = Contract::from_json(&serde_json::to_string(&tampered)?).unwrap_err();

An unknown top-level key is refused rather than ignored, so nobody can smuggle side-band data into the canonical document — that is what the envelope's extensions map exists for. And a well-formed but wrong contract_id is refused because validation recomputes the identity from the payload instead of believing the one written in the file. Neither failure is a panic, and the two arrive by different routes: the unknown key fails at decode, as ContractJsonError::MalformedJson, while the wrong identity gets far enough to fail validation, as ContractJsonError::InvalidContract carrying path-addressed violations. The binary's two failure keys, diagnostics and violations, are that same split seen from outside.

hermetic_bundle: imports without a filesystem#

A two-file bundle with an import edge between them, compiled without touching a disk. The host holds every source itself and hands the compiler one immutable logical bundle through a MemoryResolver; a resolver is the capability the compiler uses to see any source beyond the one you passed in, and it is the only authority in play.

bash
cargo run --example hermetic_bundle
rustexamples/hermetic_bundle.rs
let mut resolver = MemoryResolver::new();
resolver.insert(
    "api/root.did",
    r#"import "types.did"; service : { read: () -> (Item) query };"#,
)?;
resolver.insert(
    "api/types.did",
    "type Item = record { id: nat64; label: text };",
)?;

let compilation = compile_with_resolver(
    "api/root.did",
    &resolver,
    CompileOptions::default(),
    &RuntimeContext::default(),
)?;

The bare names are normalized to logical source IDs — memory:/api/root.did and memory:/api/types.did — and the import "types.did" resolves relative to the importing source's directory, which is why both files live under api/. Because nothing is read from or written to disk, this example needs only the compiler feature, and it is the same code path a browser build runs. Sources, imports and provenance covers resolvers in full.

host_value_validation: a value against the graph#

A host value is candid-core's lossless, self-describing representation of a Candid value for host languages. This example builds one and checks it against a type node in the Contract graph, choosing two cases that most encodings damage: a nat above the u128 range and a NaN with a payload.

bash
cargo run --example host_value_validation
rustexamples/host_value_validation.rs
let selector = contract.bind_type(measurement.ty)?;
let value = HostValue::record(
    vec![
        HostFieldValue::new(
            candid_parser::candid::idl_hash("count"),
            HostValue::nat("340282366920938463463374607431768211456")?,
        ),
        HostFieldValue::new(
            candid_parser::candid::idl_hash("reading"),
            // A NaN payload preserved exactly as IEEE-754 bits.
            HostValue::float64("7ff8000000000001")?,
        ),
    ],
    &Limits::default(),
)?;

validate_host_value(contract, &selector, &value, &Limits::default())?;

A nat travels as a decimal string and a float64 as its exact IEEE-754 hex bits, so both values survive a round trip unchanged. Fields are addressed by their Candid label hash, not by name, because that hash is what the wire format actually carries. contract.bind_type(…) turns a bare index into a selector that carries the contract identity with it, so a value cannot be validated against a type from a different Contract by accident.

One import in this example is not candid-core's

candid_parser::candid::idl_hash computes the label ids. That resolves inside this repository because candid_parser is a dev-dependency and examples link dev-dependencies. candid-core keeps its own name-hash module private and exports no hashing function, so copying this file into your own crate means taking candid_parser as your own dependency.

bounded_parsing: untrusted bytes under a policy#

The last one is the one to read before you accept a document you did not produce. It decodes a Contract and a Compilation through the bounded entry points, first under a ceiling too small to admit them and then under one that is large enough.

bash
cargo run --example bounded_parsing
rustexamples/bounded_parsing.rs
// The byte gate runs before serde_json is invoked, so an oversized document
// is rejected without being decoded. This bounds peak allocation against
// the chosen ceiling; it does not reject element-by-element during decode.
let context = RuntimeContext::new(Limits::default().with_max_input_bytes(64));
let rejected =
    Contract::from_slice_with_context(contract_json.as_bytes(), &context).unwrap_err();
println!("oversized Contract rejected: {}", resource_limit(&rejected));

// … raised limits admit both documents …

let render = raised.clone().with_max_canonicalization_work(16);
let starved_render = contract.to_json_pretty_with_limits(&render).unwrap_err();

Two things come out of this. The byte gate fires before the JSON parser is reached, so a refusal arrives as a resource_limit_exceeded violation carrying {resource, limit, observed} rather than as an out-of-memory abort. And the second half is the non-obvious part: raising max_input_bytes far enough to parse a document does not mean you can render it back out, because serialization charges the emitted byte length against a separate max_canonicalization_work budget. Two budgets, two decisions. See Limits, budgets and diagnostics.

Next#

If you have run all six, the natural next steps are Compiling Candid sources for the full set of entry points, Working with limits for the profiles and builders behind bounded_parsing, and the candid-core binary if what you actually want is a JSON document on stdout rather than a Rust API.