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#
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.
cargo run --example contract_walkthroughlet 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.
cargo run --example semantic_equivalencelet 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.
cargo run --example trust_boundarylet 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.
cargo run --example hermetic_bundlelet 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.
cargo run --example host_value_validationlet 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.
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.
cargo run --example bounded_parsing// 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.