Skip to main content

Integration with other cddl-codegen libraries

This guide is written in general for integrating with other libraries generated by cddl-codegen, but in particular references CML (cardano-multiplatform-lib) for examples. Most things referencing CML will be relevant to other common cddl-codegen generated libraries used as dependencies.

Common cddl-codegen traits

When generating a library that has as a dependency another cddl-codegen-generated library you can share the common cddl-codegen types/traits like Deserialize, RawBytesEncoding, etc. Remember to pass in --common-import-override tag. For CML we pass in --common-import-override=cml_core. This is where all the common cddl-codegen traits are located so we can avoid having duplicate incompatible traits in other libraries.

CML macros

In CML we have macros for implementing WASM conversions and JSON/bytes. We pass in --wasm-cbor-json-api-macro=cml_core_wasm::impl_wasm_cbor_json_api and --wasm-conversions-macro=cml_core_wasm::impl_wasm_conversions which are both located in cml_core_wasm. This drastically reduces WASM wrapper boilerplate.

For list wrappers there is additionally --wasm-list-macro=cml_core_wasm::impl_wasm_list, which collapses each Vec<T>-backed list wrapper (the struct + new/len/get/add accessor block + conversion traits) into a single impl_wasm_list!(rust_elem, wasm_elem, WasmName, needs_into, is_copy); invocation. It supersedes --wasm-conversions-macro for list wrappers (the list macro emits the conversions itself). Maps are unaffected.

Externally defined types

When a library generated by cddl-codegen depends on another one, the consumer must reference the dependency's types rather than regenerate them. There are two ways to tell cddl-codegen about a dependency's type surface:

  1. --extern-import (the primary path) — point the consumer at the dependency's machine-generated extern-interface export. The export is emitted by the dependency's own regeneration and describes its real, final type surface, so there is nothing to hand-maintain and nothing to drift.
  2. A hand-written stub (the escape hatch) — for a dependency you cannot regenerate (external, separately released), hand-write a representation-faithful stub of its types under _CDDL_CODEGEN_EXTERN_DEPS_DIR_/<dep>/. Kept fully working, but you own its fidelity.

Consuming a dependency's export (--extern-import) — the primary path

Every cddl-codegen regeneration emits an extern-interface/<dep>/** directory (a committed, tool-owned sibling of rust/ and wasm/ — see Output format). Point a consumer at the dependency's copy with the repeatable --extern-import <dep>=<path> flag:

cddl-codegen --input=cml_chain.cddl --output=export --lib-name=cml-chain \
--extern-import=cml_core=../cml-core/extern-interface/cml_core

The imported rules land in the same non-exported <dep> scope a physical stub tree would, after which the entire extern-deps pathway is unchanged. Three properties are worth knowing:

  • Version-header strictness. A flag-fed file must begin with the versioned seam header ; _CDDL_CODEGEN_EXTERN_INTERFACE_ v1 and carry only recognized @-annotations. A missing/unknown header, or an unknown @-token, is a hard error naming the file — the strict seam refuses to silently misread an export written by a newer (or corrupted) tool. (Hand-stubs carry no header and stay lenient.)
  • @rust_name pins are read, not re-derived. The export records the dependency's final Rust names, so the consumer reads them instead of re-deriving from the CDDL idents with its own (possibly different) codegen version — eliminating the cross-version naming-skew class. It also spells transparent types (primitive aliases, c-style enums, named collections) truthfully, so the old "declare a uint alias then delete it afterwards" workflow is gone: the export already carries the right shape.
  • ; unexported: records. A rule the dependency could not project faithfully (e.g. a transparent alias carrying @custom_serialize) is excluded from the export and recorded as a ; unexported: <ident> — <reason> comment; anything referencing it is transitively excluded too. You only ever hit one when your consumer spec references such an ident — generation then fails with an undefined-reference error, augmented with the declared dependencies and their export paths. Every remedy is on the dependency's side, because a dependency you import is already declared and cannot be supplemented rule by rule: regenerate the dependency (the ident may have appeared since its last regen), and if the export's ; unexported: records name it, fix the cause in the dependency's own spec — a type it hand-owns travels once the dependency itself declares it as _CDDL_CODEGEN_EXTERN_TYPE_ — or report the projection limitation.

Regeneration order. Regenerate the dependency before the consumer — the export is committed generated output that must reflect the dependency's current spec when the consumer reads it. This is the same discipline --extern-wrapper-index already demands, and multi-crate workspaces already regenerate in dependency order via a driver script.

collections module-name collision

A pre-existing hazard: the wasm collection-wrapper deferral uses a synthetic <dep>::collections scope (_CDDL_CODEGEN_EXTERN_DEPS_DIR_/<dep>/collections). A dependency whose own spec has a module file literally named collections would produce a <dep>::collections scope that collides with it. Avoid naming a module collections in a spec that is consumed as a dependency.

_CDDL_CODEGEN_EXTERN_TYPE_ vs _CDDL_CODEGEN_RAW_BYTES_TYPE_

Whichever path you use, each extern-visible type is described as one of two kinds, and it is important to choose the appropriate one. If the type was defined originally as _CDDL_CODEGEN_RAW_BYTES_TYPE_ in CML (or whatever library) then it is important to define it using this so it will be encoded correctly. If the type was either defined using _CDDL_CODEGEN_EXTERN_TYPE_ (hand-written) or was explicitly defined normally in the dependency lib (e.g. CML) then use _CDDL_CODEGEN_EXTERN_TYPE_. An export minted by --extern-import already classifies each type this way for you; a hand-stub is where you choose it.

Hand-written stubs (the escape hatch)

If your input directory includes a /_CDDL_CODEGEN_EXTERN_DEPS_DIR_/ directory, everything inside will be treated as an external dependency. This lets you specify the import tree of any dependency CDDL structures without an export to point at: a hand-written crate, one you cannot regenerate, one generated by a deliberately separate pass. Declaring the same <dep> both as a physical stub directory and via --extern-import is a hard error (ambiguous double declaration); pick one per dependency. That is why a stub is a whole-dependency declaration rather than a way to top up an export that is missing a rule — for that, the fix belongs in the dependency (see the ; unexported: records above).

You define these types as _CDDL_CODEGEN_EXTERN_TYPE_ if entirely self-contained or _CDDL_CODEGEN_RAW_BYTES_TYPE_ if CBOR bytes, and — this is the fidelity burden the export removes — you must spell transparent types truthfully: a primitive alias as its definition (coin = uint), a c-style value enum as its choices (fe = 0 / 1 / 2), never an @no_alias rule as an opaque extern. An unfaithful stub surfaces later as a wrapper the dependency has no class for (the stub-fidelity contract, documented under --wrapper-requests). For an example see the _CDDL_CODEGEN_EXTERN_DEPS_DIR_ directory inside of specs/multiera. Each folder within the directory is a separate dependency, and nothing inside is generated. You will still need to add the dependency to the Cargo.toml afterwards — a one-time edit: regeneration merges into the existing manifest rather than overwriting it, so hand-added dependencies survive (see Output format).

Wasm bindings across crates

When generating wasm bindings (--wasm=true) and a dependency's wasm-bindgen wrappers live in a separate crate (the split <dep> / <dep>-wasm layout cddl-codegen itself generates), map it with --extern-wasm-crate <dep>=<wasm_crate> so the wasm pass imports and qualifies that dependency's boundary types through the wasm crate. Without the mapping the dependency is assumed to follow the single-crate convention (its rust types are themselves #[wasm_bindgen]-annotated). Either way the generated wasm Cargo.toml needs the appropriate dependency entries added by hand (both crates under the split layout). This is independent of how <dep> was declared — --extern-import and hand-stubs key the dependency identically.

When the consumer's spec uses list/map shapes over a dependency's extern types ([* dep_foo], {* uint => dep_foo}), it would otherwise re-mint the dependency's own collection wrappers — two #[wasm_bindgen] classes of the same name fail to link a single wasm cdylib (duplicate symbol: __wbg_<wrapper>_free). Point --extern-wrapper-index <dep>=<path/to/collections.rs> at the dependency's committed wrapper index (every wasm crate emits wasm/src/generated/collections.rs; see Output format) so the consumer defers to the dependency's wrappers instead of re-minting them. Regenerate the dependency before the consumer — the index is committed generated output that must reflect the dependency's current wrapper inventory.

Migrating CML from hand-stubs to exports

The migration runs in forward dependency order, and each step is verifiable because a faithful stub's derived names already match the export's pins, so the output is byte-identical:

  1. Upgrade the tool and regenerate cml-core — its extern-interface export is minted.
  2. Point cml-chain at that export with --extern-import=cml_core=<...>, delete chain's hand-stub for core, and regenerate — assert the generated output is byte-identical to before. Chain's own export is now minted.
  3. Repeat for cml-multiera, which consumes both chain and core: point one --extern-import at each, delete the stubs, regenerate, assert byte-identity.

Each crate's export describes only its own surface — a dependency's own deps never travel through its export — so a consumer that needs cml-core types while depending on cml-multiera declares cml-core as its own direct --extern-import; the dependency depth never exceeds one.