Quickstart: Rust

Compile a Candid interface into a validated Contract graph and read its identities.

This page takes a Candid .did interface and turns it into a Contract: the validated, canonical type graph this project is built around, carrying content-addressed identities that let you tell two interfaces apart — or prove they are the same — without diffing text. Cargo is the only tool you need.

Parsing and type checking are not reimplemented here. The crate delegates both to the official candid_parser, then projects the checked result into its own arena of type nodes, sorts and canonicalizes it, and hashes the canonical bytes. Everything downstream — the TypeScript generator, the schema runtime, the CLI — consumes that graph rather than the source text.

Pre-1.0, so pin the exact version

candid-core is at 0.1.0. Until 1.0, any release may change the public API, the serialized shapes, the canonical bytes, and therefore every identity computed over them, and a caret requirement such as candid-core = "0.1" would take such a release with it. Every example below pins the exact version.

From .did source to a Contract#

  1. Add the dependency#

    tomlCargo.toml
    [dependencies]
    candid-core = "=0.1.0"

    Or from the command line, cargo add candid-core@=0.1.0. The MSRV is Rust 1.78 and the licence is Apache-2.0.

    That line gives you the whole surface, because all three Cargo features are on by default: compiler (the candid_parser-backed source compiler), filesystem-compiler (reading .did files, and the candid-core binary) and host-value (the tagged JSON value ABI). A consumer that only reads Contracts someone else produced can switch all three off with default-features = false and keep the model, the validation and the identities; Install and features lays out the four surfaces and what each one pulls into the dependency graph.

  2. Compile an inline source and read its identities#

    compile_did takes Candid source as a &str and returns a Compilation: the Contract itself plus an optional provenance sidecar. It needs no filesystem, which is what makes it the entry point a browser or WASM host uses.

    rust
    use candid_core::compile_did;
    use std::error::Error;
    
    fn main() -> Result<(), Box<dyn Error>> {
        let compilation = compile_did(
            r#"
            type Account = record { owner: principal; balance: nat };
    
            service : {
              balance_of: (Account) -> (nat) query;
            };
            "#,
        )?;
    
        let contract = compilation.contract();
        println!("contract:  {}", contract.contract_id());
        println!("interface: {}", contract.interface_id().unwrap_or("(none)"));
        Ok(())
    }
    text
    contract:  candid-core:contract:v1:sha256:<64 lowercase hex digits>
    interface: candid-core:interface:v1:sha256:<64 lowercase hex digits>

    The digests are elided above because the exact bytes depend on the release you build against — that is the point of the beta warning, not a coyness about the values. The shape is fixed and is checked on every Contract that loads: a domain prefix, then sha256:, then exactly 64 lowercase hex characters.

    The two identities answer different questions. contract_id covers the complete canonical Contract payload: the format markers, both profile markers, every retained type node, the declarations and their names, and the actor when there is one. interface_id covers only the type graph reachable from the actor plus the profile markers, so declaration names and actor-unreachable declarations do not move it — rename Account to Acct and the interface identity holds while the contract identity changes. It is Option<&str> because a declaration-only Contract with no service block has no interface to identify.

    Neither one covers producer, source text, comments or formatting. What identifies raw source bytes is a third value, source_bundle_id, which lives on the provenance sidecar; what commits to the exact octets of a serialized document is a fourth, artifact_id, computed on demand and never written into the document. Content-addressed identities is the page that pulls the four apart.

    From the same Compilation you can walk the graph itself — contract.types() is the arena, contract.declarations() the named entries, contract.actor() the root — and compilation.source_info() gives the provenance sidecar with the field labels, documentation comments and source list. The runnable walkthrough does exactly that:

    bash
    cargo run --example contract_walkthrough
  3. Compile a file instead#

    compile_did_file is the same pipeline over a path, and it is the one entry point that resolves import statements: it roots a WorkspaceResolver at the entry file's parent directory and reads the bundle through it, so nothing outside that directory is reachable.

    rust
    use candid_core::compile_did_file;
    use std::error::Error;
    
    fn main() -> Result<(), Box<dyn Error>> {
        let compilation = compile_did_file("./service.did")?;
        let contract = compilation.contract();
    
        println!("{} type node(s)", contract.types().len());
        println!("{} declaration(s)", contract.declarations().len());
        println!("{}", contract.to_json_pretty()?);
        Ok(())
    }

    This needs the filesystem-compiler feature, which is on by default and pulls in cap-std. Turn default features off and the item is absent at compile time — a build error naming compile_did_file, not a runtime stub.

    compile_did refuses imports by design

    Hand compile_did a source containing import and it fails with a did_import_requires_file diagnostic listing each import as a note, rather than resolving anything. That is the boundary being explicit: an in-memory entry point never touches a filesystem. Supply the bundle yourself through a SourceResolver and call compile_with_resolver, or read it from disk with compile_did_file. See Sources, imports and provenance.

  4. Tighten the limits and read the diagnostic#

    Every entry point runs under a resource budget, so .did source you did not write cannot hang the process or allocate without bound. Limits::default() is the frozen interactive_v1 profile — 27 values, including max_source_bytes at 1 MiB and max_input_bytes at 4 MiB. Fields are private; you start from a profile and override with with_* builders, then wrap the result in a RuntimeContext and pass it to a _with_context entry point.

    rust
    use candid_core::{compile_did_with_context, CompileOptions, Limits, RuntimeContext};
    
    fn main() {
        // 38 bytes of source, and a 16-byte ceiling.
        let source = "service : { ping: () -> (nat) query };";
        let context = RuntimeContext::new(Limits::default().with_max_source_bytes(16));
    
        let error = compile_did_with_context(source, CompileOptions::default(), &context)
            .expect_err("the source is larger than the limit");
    
        for diagnostic in &error.diagnostics {
            println!("code:    {}", diagnostic.code);
            println!("phase:   {:?}", diagnostic.phase);
            println!("message: {}", diagnostic.message);
            if let Some(limit) = &diagnostic.resource_limit {
                println!(
                    "budget:  resource={} limit={} observed={}",
                    limit.resource, limit.limit, limit.observed
                );
            }
        }
    }
    text
    code:    resource_limit_exceeded
    phase:   Some(Load)
    message: source "memory:/inline.did" uses 38 bytes; limit is 16
    budget:  resource=source_bytes limit=16 observed=38

    That is the whole failure contract. A CompileError is a struct with one public field, diagnostics: Vec<Diagnostic>, and a Diagnostic carries a stable code, a message, and optionally a phase (parse, type_check, load or lower), a severity, a path, a span naming the logical source, ordered related locations, notes, and the {resource, limit, observed} triple above. The codes, the paths and the resource metadata are the machine surface and are stable; the message text is not. limit and observed are fixed-width u64, so the serialized triple means the same thing on a 32-bit and a 64-bit target.

    Raising a limit does not raise the others: the budgets are charged separately, so a caller who raised max_input_bytes enough to parse a document may still fail to render it against max_canonicalization_work. cargo run --example bounded_parsing demonstrates exactly that, and the limits reference lists all 27 values with their defaults.

  5. Skip the Rust and use the binary#

    The crate ships one binary, also called candid-core, gated on the same filesystem-compiler feature. It accepts exactly this grammar and nothing else.

    text
    candid-core compile <path> [--no-source-info | --envelope]
    candid-core validate <path>
    bash
    cargo install candid-core --version 0.1.0 --locked
    
    candid-core compile ./service.did              # contract + provenance sidecar
    candid-core compile ./service.did --envelope   # one self-describing document
    candid-core validate ./contract.json           # re-check a document you were given

    compile prints {"contract": …, "ok": true, "source_info": …}. The Contract inside has nine top-level keys when the interface has an actor, in this order. Digests and arena indices are elided below — the indices a real run assigns depend on the canonical ordering of your own type graph:

    json
    {
      "actor": { "kind": "service", "service": N },
      "canonicalization_profile": "candid-core-canon-1",
      "declarations": [{ "name": "Account", "type": N }],
      "format": "candid-core",
      "format_version": 1,
      "identities": {
        "contract": "candid-core:contract:v1:sha256:…",
        "interface": "candid-core:interface:v1:sha256:…"
      },
      "producer": {
        "candid_parser_version": "0.4.0",
        "candid_version": "0.10.30",
        "name": "candid-core",
        "version": "0.1.0"
      },
      "semantics_profile": "candid-1",
      "types": [ "…the arena, one node per entry…" ]
    }

    A declaration-only interface omits the "actor" key entirely, leaving eight, and the "interface" identity is likewise absent. "actor": null is not part of the v1 wire format at all: serialization never emits it, the identity payload never hashes it, and decoding rejects it. producer is unverified provenance held outside both semantic identities: rewriting it leaves contract_id and interface_id byte-identical, which is why a version bump does not move them.

    --envelope is the flag to reach for when the consumer is TypeScript: it prints a ContractEnvelope — {"contract": …, "extensions": {…}} — whose org.candid-core.field-names/v1 extension carries the field-name table, because a semantic Contract stores label ids and not text. That document is exactly what schemaFromContract in @candid-core/schema consumes whole. Extensions live outside the canonical identities, so carrying them never moves a contract_id.

    Failures print to stdout, not stderr

    Every non-usage outcome is one pretty-printed JSON document on stdout with an empty stderr — success and failure alike. Branch on the exit status or on the ok key, never on which stream produced the output. Success exits 0; a read, parse, validation or resource-limit failure exits 1; a usage error writes nothing to stdout, prints the usage text on stderr, and exits 64. Flags go after the path, and a token beginning with - is always an option in the path position — spell a dash-leading file as ./-service.did.

    The candid-core binary has the full output contract, including the two different failure keys validate uses depending on how far the read got.

Next#

You now have a Contract and know what its identities do and do not cover. The two directions from here are downward, into the model itself, and outward, into the TypeScript that consumes it.