Install and feature surfaces

One package, four build surfaces. Which one you need, and what each pulls into your dependency graph.

candid-core is one crate on crates.io. It reads Candid .did interface source and projects it into a Contract: a validated, canonically ordered type graph with named declarations, an optional actor, and content-addressed identities computed over its canonical bytes. What that crate builds is split into an always-present base plus three Cargo features, so a consumer that only wants the data model never compiles a Candid parser and never links a filesystem capability crate.

Everything on this page is pre-1.0. The current release is 0.1.0-beta.3, and any release before 1.0 may change the public API, the serialized shapes, the canonical bytes, and therefore every identity computed over them. Pin an exact version.

Adding the dependency#

Every published version of candid-core is a prerelease. Cargo only selects a prerelease when the version requirement itself names one, so the usual caret spelling resolves to nothing: candid-core = "0.1" does not match 0.1.0-beta.3, and cargo install candid-core fails with a "could not find … with version *" message. Write the exact requirement.

tomlCargo.toml
[dependencies]
# The full surface: compiler, filesystem-compiler and host-value are all
# enabled by default, so no feature selection is needed.
candid-core = "=0.1.0-beta.3"

The same thing from the command line:

bash
cargo add candid-core@=0.1.0-beta.3

Pinning with = is worth doing for a second reason beyond prerelease selection. With both the API and the wire format unstable before 1.0, an automatic move to the next prerelease is a change you should opt into deliberately rather than inherit from a cargo update.

Upgrade rather than pin an earlier beta

0.1.0-beta.1 and 0.1.0-beta.2 walk a declaration graph as a tree, so a small .did file with shared type aliases can drive work that doubles per level — and no limit refused it, because the walk returned Ok. 0.1.0-beta.3 fixes that and adds max_type_preflight_work so the work is charged. If you compile .did source you did not write, do not pin either earlier version.

The four build surfaces#

default = ["compiler", "filesystem-compiler", "host-value"]. The base is what remains when default-features = false removes all three.

Surface What it adds What it pulls into your graph
(base) Contract, ContractDraft, RawContract, ContractEnvelope, validation, canonicalization, the semantic Contract identities, artifact_id_with_limits / ArtifactKind, Limits / RuntimeContext / CancellationToken, Diagnostic serde, serde_json, sha2, hex
host-value HostValue, HostFieldValue, validate_host_value, ContractTypeRef / ContractMethodRef ic_principal
compiler compile_did and its option/context variants, compile_with_resolver, Compilation, CompileOptions, CompileError, SourceId / SourceResolver / ResolvedSource / MemoryResolver, the SourceInfo provenance sidecar candid, candid_parser
filesystem-compiler (implies compiler) WorkspaceResolver, compile_did_file and its variants, source materialization for candid_parser::check_file, the candid-core binary cap-std

Base — the pure Contract model#

The base surface can build a Contract from a graph you already hold (ContractDraft), validate one that arrived as JSON (Contract::from_json_with_context), canonicalize it, read its contract_id and interface_id, and hash a serialized document into a detached artifact identity. It cannot parse .did text, because no Candid engine is in the graph at all.

Choose it if your process receives Contract documents rather than Candid source — a registry that stores them, a service that validates uploads, a build step downstream of someone else's compile. What that removes is a dependency-graph size you can measure: resolved for x86_64-unknown-linux-gnu, the base graph is 22 packages where the default graph is 126. The figures move with the target, because cap-std brings different platform dependencies on Linux, macOS and Windows, so verify_feature_graph.py resolves and prints every configuration for the host you run it on.

host-value — validating values against a graph#

Adds HostValue, a lossless self-describing JSON encoding for Candid values, and validate_host_value, which checks a value tree against a type node in a Contract's graph. It takes ic_principal directly rather than through candid, which is what keeps the parser stack out of a value-only build.

Choose it if you accept user-entered or wire-received values and want the interface itself to decide whether they are well-formed. See Validating host values.

compiler — Candid source, no filesystem#

Adds the parsing and type-checking path. Parsing and semantic checking are delegated to the official candid_parser crate rather than reimplemented, so this feature is what brings candid and candid_parser into the graph. It covers self-contained sources through compile_did and multi-file bundles with imports through compile_with_resolver, where the host supplies every source synchronously as data.

Choose it if you compile Candid source in a place with no filesystem, or with a filesystem you do not want the library to touch: a browser, a sandboxed worker, an editor that already has the buffers in memory, a server accepting pasted source.

filesystem-compiler — native files#

Adds WorkspaceResolver, which holds an open cap-std directory capability for one authorized root and opens every path relative to it; compile_did_file, which roots a resolver at the file's own parent directory; and the candid-core binary. It implies compiler, so it is a superset.

Choose it if you are a native tool that reads .did files off disk, or you want the CLI. See The candid-core binary.

What feature selection does, and does not, do#

Two caveats are worth stating plainly, because both are easy to over-read.

Cargo unifies features across a build. Feature splitting bounds what a dependency graph must contain — it cannot subtract from a graph that already asked for more. If anything else in your build depends on candid-core with defaults, the whole surface is compiled once for every consumer in that build, including yours. Your default-features = false line is still meaningful: it states what your crate needs, and it is what makes a lean graph possible when nothing else contradicts it. It is not a guarantee about the compiled output of a mixed workspace.

Feature selection does not shrink the download. Every published source file ships in the .crate archive regardless of features, so a base-only consumer still downloads the compiler sources it will never build. What the archive contains is governed separately, by a positive include allowlist in the manifest — production source, runnable examples, the public docs/ set, and the three root documents. Test suites, fixtures, benchmarks, the fuzz crate and CI assets are not published.

One thing features do not change: ProducerInfo::current reports the same name, version, candid_version and candid_parser_version in every configuration, because it reads the pinned versions out of the manifest at compile time rather than from a linked crate.

Two configurations worth copying#

A browser or WASM host that compiles source it already holds#

tomlCargo.toml
[dependencies]
candid-core = { version = "=0.1.0-beta.3", default-features = false, features = ["compiler"] }

This keeps cap-std out of the graph entirely — and so does the default configuration on that target, because cap-std is declared under cfg(not(target_os = "unknown")) as well as behind the feature. A compiler-only graph resolved for wasm32-unknown-unknown is 107 packages. Compile a bundle the host supplies:

rustexamples/hermetic_bundle.rs
use candid_core::{compile_with_resolver, CompileOptions, MemoryResolver, RuntimeContext};

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(),
)?;
println!("contract: {}", compilation.contract().contract_id());

A pure-model consumer that never sees Candid source#

tomlCargo.toml
[dependencies]
candid-core = { version = "=0.1.0-beta.3", default-features = false }
rust
use candid_core::{Contract, ContractJsonError, Limits, RuntimeContext};

fn accept(document: &[u8]) -> Result<Contract, ContractJsonError> {
    // The byte gate runs before serde_json is invoked, so an oversized
    // document is rejected without being decoded; the gate, the decode and
    // validation then share one budget.
    let context = RuntimeContext::new(Limits::default().with_max_input_bytes(256 * 1024));
    let contract = Contract::from_slice_with_context(document, &context)?;

    println!("{} type nodes", contract.types().len());
    println!("contract identity {}", contract.contract_id());
    Ok(contract)
}

Add features = ["host-value"] to that line if the same process also validates Candid values against the graph.

A missing item is a compile error that names the fix

Items outside the enabled set are absent at compile time rather than present as stubs that fail at run time. If you call compile_did from a base-only build, the error names the missing item and the fix is to turn on the feature it belongs to.

Toolchain, targets and licence#

Minimum supported Rust version
1.78, declared as rust-version in the manifest. CI runs the locked dependency graph against 1.78.0, so a direct or transitive update that breaks it fails before merge.
Edition
2021, across every crate in the repository.
Licence
Apache-2.0.
Native targets exercised in CI
Linux, macOS and Windows on the rolling stable channel, plus Linux on 1.78.0 for MSRV.
WebAssembly
wasm32-unknown-unknown. CI checks the library there in four configurations — base, host-value, compiler and defaults — and lints the compiler one with warnings denied. The browser runtime suite then compiles source and an imported bundle inside headless Chrome and pins the resulting identities, so running there is a runtime claim rather than only a build claim.

The crate refuses to compile on targets whose pointer width is neither 32 nor 64 bits, with a message explaining why: portable wire values are fixed-width u64 widened from usize, and the default limit values do not fit a 16-bit usize.

Two things bare wasm32-unknown-unknown cannot do#

The compiler surface needs no filesystem and both builds and runs on that target. Two limitations are stated rather than papered over.

  • There is no directory to open. WorkspaceResolver and compile_did_file still compile for bare WASM, but WorkspaceResolver::new fails there with did_workspace_root_error, and a load would fail with did_file_read_error. Use MemoryResolver or your own SourceResolver instead.
  • There is no clock. Cancellation and every quantitative limit behave exactly as they do natively. An explicit Limits::with_deadline_unix_ms cannot be measured, so it fails closed with operation_deadline_exceeded rather than calling an unsupported std::time function and aborting the module; Limits::deadline_exceeded reports the same. No deadline configured stays unbounded, as it is everywhere else. The crate takes no web-time, js-sys or wasm-bindgen production dependency to change that.

Next#