---
title: "FFI Interface IR"
summary: "Export deterministic typed FFI contracts for bindgen, inventory, and compatibility checks"
scope: "core"
kind: "spec"
category: "installation"
aliases:
  - "ffi export"
  - "typed ffi bindgen"
  - "interface ir"
entry_for:
  - "calcit ffi export"
id: core/ffi/interface-ir
parent: core/ffi
related:
  - core/ffi
  - core/wasm/component-boundary
---

# FFI Interface IR

`calcit ffi export` 在不执行应用代码的前提下读取项目 Snapshot，并把 typed
raw-binding boundary 导出为确定性的 Interface IR。native Interface IR v3 与 Component
Interface IR v4 是当前 versioned contract；`calcit-bindgen` 必须先识别对应版本，才能生成
Rust、Calcit、TypeScript 或 WIT。core 不生成业务层
normalization API，也不把尚未完成的 component packaging 描述为可运行产物。

```bash
calcit calcit.cirru ffi export
calcit calcit.cirru ffi export --json
calcit calcit.cirru ffi export --json --ns app.ffi
calcit calcit.cirru ffi export --boundary component
```

默认 `--boundary native` 使用本文所述的 raw-binding Interface IR v3。
`--boundary component` 是独立的 Component Interface IR v4，只提取
`defwasm-import` / `defwasm-export` 并默认输出 Cirru EDN；其方向语义、
strict type closure 与 core/bindgen 职责见
[WASM Component 边界](wasm-component-boundary.md)。两种 boundary 使用不同的
versioned schema，consumer 不应根据字段形状猜测类型。

Component definition 的必填 `invocation` 字段只有 `sync` 与 `async`。未标记的函数导出为
`sync`；函数 schema 中显式的 `:async true` 导出为 `async`。这是 WASI 0.3 native async
lowering 的唯一表层信号，不引入额外的 `Task<T>` 类型，也不把 scheduler、poll、future handle
或 native `async-task-v1` 队列协议暴露到 Calcit 语法。typed `Result<T,E>` 仍是普通返回类型；
取消、future 生命周期和调度由 Canonical ABI adapter 与 runtime consumer 实现。

The JSON command writes one parseable envelope to stdout. Its
`data.interface` value follows
[`schemas/ffi-interface-ir-v3.schema.json`](../../schemas/ffi-interface-ir-v3.schema.json).
The envelope's `interface_schema` field carries the schema identifier.
Definitions and diagnostics are sorted deterministically, and `revision` is a
digest of the interface plus diagnostics. Unordered EDN maps and sets are
canonicalized recursively before `logical_schema`, `lowering.raw`, and the
revision are emitted. Consumers must check `version` before generation.
`package_version` comes from the adjacent `deps.cirru :version`, which is the
project's release-version source of truth. Legacy projects without that field
fall back to the compatibility version retained in the snapshot.

## Boundary selection

Version 3 selects local definitions whose `:ffi` metadata contains at least
one lowering field: `:backend`, `:target`, `:kind`, `:symbol`, `:invoke`, or
`:transport`. Empty `:ffi {}` placeholders and capability-only metadata such
as `:features` do not declare a raw binding. A typed external-object trait with
`:kind :external-object` is also a static host capability contract rather than
a callable lowering entry; inspect it with `calcit query def`, not `ffi export`.
Other callable kinds, incomplete lowering, and malformed non-container metadata
remain visible instead of disappearing. Dependencies are excluded; run the
command in each module that owns a boundary.

V3 会忽略 `:kind :external-object` 的 typed host capability trait：它用于静态
成员检查，不是 callable lowering；请用 `calcit query def` 审计其 schema 与 FFI
metadata。其他 callable kind、字段不完整的 lowering 以及 malformed metadata
仍会进入 inventory 并产生诊断，不能借此静默绕过 bindgen 检查。

Both layers remain visible:

- `logical_schema` preserves the backend-neutral Calcit schema;
- `signature` is the strict generator-safe projection;
- `declarations` contains only transitively reachable local Struct/Enum shapes;
- `lowering` preserves backend selection and symbol/invocation metadata;
- `status` and `diagnostic_codes` prevent generators from treating an
  unsupported definition as usable.

## Lowering contract validation

Interface IR v3 callable definitions have one direction: Calcit imports a raw
binding from the selected host backend. An explicit import/export direction
field is reserved for a future IR version; consumers must not infer a reverse
export from v3 metadata.

Native callables are generator-safe only when all three lowering fields are
present and coherent:

| Invoke                | Transport          | Symbol |
| --------------------- | ------------------ | ------ |
| `sync`                | `edn-buffer-v1`    | portable C base identifier |
| `async`               | `async-task-v1`    | portable C base identifier |
| `blocking-callback`   | `blocking-host-v1` | portable C base identifier |

The symbol is the unsuffixed logical base such as `read_file`; Calcit derives
the versioned C entry point. Native targets are omitted or `native`. JS targets
are omitted, `browser`, or `node`. Unknown backends, invalid targets, missing
fields, non-portable symbols, unversioned transports, and mismatched
invoke/transport pairs produce path-specific diagnostics before bindgen.

Interface IR v3 的 callable direction 固定为“Calcit 从 host backend import raw
binding”；显式双向 direction 字段留给后续 IR 版本。native callable 必须声明
未带协议后缀的 portable C base symbol，并使用 `sync + edn-buffer-v1`、
`async + async-task-v1` 或 `blocking-callback + blocking-host-v1` 之一。
未知 backend/target、缺失字段、非法 symbol、未版本化 transport 与组合错配都会
在 bindgen 前产生带精确 path 的 diagnostic。

V3 表示 `Unit`、`Bool`、`Number`、`String`、`Buffer`、同质 `List`、明确的
`Option` / `Result`，以及由 namespace-qualified declaration 支撑的本地 Struct/Enum。
此外，V3 将 `Int8`、`UInt8`、`Int16`、`UInt16`、`Int32`、`UInt32`、`Int64`、
`UInt64`、`Float32` 与 `Float64` 分别导出为独立的 `kind`。普通 `Number` 仍导出为
`number`，不得根据值、名称或调用位置猜测整数宽度。数值 refinement 在参数、结果、
Struct field、Enum payload、List、Option 与 Result 中使用同一递归表示，并自然进入 revision。

V3 represents `Unit`, `Bool`, `Number`, `String`, `Buffer`, homogeneous
`List`, explicit `Option` / `Result`, and local Struct/Enum references backed
by namespace-qualified declarations. Declaration fields and variant payloads
may use declared type parameters, while callable signatures remain
monomorphic. It also exports every width-specific integer and floating-point
refinement as its own `kind`; `Number` remains `number` and is never narrowed
by guessing. Only declarations transitively reachable from an FFI signature
enter the document and revision.

Local `defstruct` / `defenum` forms are interpreted statically from the
snapshot; application code is not executed. Missing or ambiguous declarations,
wrong type-argument arity, trait-bounded declarations, `Dynamic`, callbacks,
`Map`, `Set`, `Ref`, resources, host objects, variadic functions, and generic
callable boundaries produce explicit diagnostics. A rejected signature is
`null`; there is no Dynamic fallback or declaration-name guessing.

The core `FfiTask` and `FfiResponse` capabilities are known host-managed types,
not missing application declarations. Interface IR reports them with stable
`E_FFI_IR_HOST_MANAGED_TYPE` diagnostics. Keep those lifecycle boundaries in a
handwritten Calcit adapter and wrap opaque native tokens with `ffi:task` or
`ffi:response`; generators must not copy or synthesize their representation.

Unsupported signatures distinguish two other common handwritten boundaries.
`E_FFI_IR_DYNAMIC_TYPE` identifies an open `Dynamic` position that must become a
concrete raw type or be validated/decoded by an adapter.
`E_FFI_IR_CALLBACK_TYPE` identifies typed runtime function and untyped `DynFn`
callbacks because v3 does not define their ownership, thread-affinity, or
lifetime contract. A nested macro-kind function annotation is not a runtime
callback and continues to use `E_FFI_IR_UNSUPPORTED_TYPE`; a macro raw binding
uses `E_FFI_IR_UNSUPPORTED_SCHEMA`. Other types outside the portable subset
continue to use `E_FFI_IR_UNSUPPORTED_TYPE`.
A local `defunion` is also reported as `E_FFI_IR_UNSUPPORTED_TYPE` (not as a
missing declaration): v3 has no union representation, so match the union in a
handwritten adapter and pass a member type across the raw binding.

Declaration IDs follow the resolved nominal Struct/Enum name, not necessarily
the snapshot binding that stores the base definition. This supports the normal
top-level trait pattern `Foo0 = defstruct Foo ...` plus
`Foo = impl-traits Foo0 FooImpl`. If multiple bindings define the same nominal
ID, export rejects the reference as ambiguous and reports the source bindings
in deterministic order.

当前 v3 只导出带有效 lowering 字段的本地 raw binding，忽略 snapshot 中普通
定义的空 `:ffi {}` 占位。Struct/Enum 使用 namespace-qualified declaration ID，
Option/Result 使用明确类型节点；只纳入从 FFI signature 传递可达的声明。缺失、
歧义、参数数量错误或无法表示的声明会产生结构化错误，不会按名称猜测或静默退化
为动态调用。生成器必须先检查 interface `version`、definition `status` 和顶层
`diagnostics`。

core 的 `FfiTask` 与 `FfiResponse` 是已知的宿主管理 capability，并非应用声明
遗漏。Interface IR 使用稳定的 `E_FFI_IR_HOST_MANAGED_TYPE` 诊断区分这类边界；
应在手写 Calcit adapter 中用 `ffi:task` / `ffi:response` 包装不透明 native token，
生成器不得复制或猜测其底层表示。

unsupported signature 还会区分另外两类常见手写边界：
`E_FFI_IR_DYNAMIC_TYPE` 表示开放的 `Dynamic` 位置，必须改成具体 raw type，或在
adapter 中验证/解码；`E_FFI_IR_CALLBACK_TYPE` 表示 typed runtime function 与
untyped `DynFn` callback，因为 v3 尚未定义 ownership、线程亲和性与生命周期契约。
嵌套的 macro-kind function annotation 不是 runtime callback，仍使用
`E_FFI_IR_UNSUPPORTED_TYPE`；macro raw binding 使用
`E_FFI_IR_UNSUPPORTED_SCHEMA`。其他超出 portable subset 的类型仍使用
`E_FFI_IR_UNSUPPORTED_TYPE`。
本地 `defunion` 同样报 `E_FFI_IR_UNSUPPORTED_TYPE`，而不是“声明缺失”：v3 还没有
union 表示，应在手写 adapter 里 match union，再把成员类型交给 raw binding。

Declaration ID 以解析出的 nominal Struct/Enum 名称为准，不强制等于保存 base
definition 的 Snapshot binding 名称，因此支持顶层 `Foo0 = defstruct Foo ...` 与
`Foo = impl-traits Foo0 FooImpl` 模式。多个 binding 声明同一 nominal ID 时会以
稳定顺序报告歧义，不会随机选择一个形状。

`package_version` 读取相邻 `deps.cirru` 的 `:version`，与当前项目发版流程保持
同一事实来源；尚未迁移版本字段的旧项目才回退到 snapshot 兼容值。

## 版本策略

当前导出直接使用 native Interface IR v3 与 Component Interface IR v4，不提供并行的
v2/v1 降级输出。consumer 必须先检查 `version` 再生成；遇到未知版本时应明确拒绝并升级，
不能忽略新的数值 `kind` 或将其退化为 `number`。仓库保留早期 schema 仅供审计已经生成的
contract，不为尚未形成实际生态的 preview Component contract 维护读取或写入兼容层。

Component Interface IR v4 增加单一、非泛化的 `readable-byte-stream` kind，对应 WIT
`stream<u8>`。它只允许作为 `async defwasm-export` 的直接参数；import、同步调用、嵌套容器和
返回位置都会明确失败。该类型只表达 adapter 拥有的作用域化消费能力，不表示可复制、可存储的
通用 Calcit stream，也不暴露 Canonical ABI readable-end handle。

This phase defines an inventory and generator input. It validates fixed
function arity and the published native invocation/transport pairs, but does
not yet validate callback positions, ownership, cancellation, or resource
lifecycle fields nested in lowering metadata. Those structured checks and
generated adapters belong to the next bindgen phase.

## Standalone production bindgen

Production generation is maintained by
[`calcit-lang/calcit-bindgen`](https://github.com/calcit-lang/calcit-bindgen).
This repository owns only Interface IR extraction, versioned schemas, exporter
semantics, and conformance tests; it does not ship generator backends, manifests,
goldens, WIT tooling, or stale-artifact policy.

```bash
calcit /path/to/calcit.std/calcit.cirru ffi export --json --ns calcit.std.hash > /tmp/calcit-std-ffi.json
calcit-bindgen validate /tmp/calcit-std-ffi.json
calcit-bindgen generate /tmp/calcit-std-ffi.json --out /tmp/calcit-std-bindings
calcit-bindgen check /tmp/calcit-std-ffi.json --out /tmp/calcit-std-bindings
```

The standalone tool owns deterministic Rust, method-oriented Calcit,
namespace-qualified TypeScript, and strict-subset WIT generation. Its manifest
records the enabled backend set and every managed artifact. Unknown versions,
unsupported definitions, Dynamic/resource/callback boundaries, and types outside
a selected backend's capability matrix fail explicitly without generated
fallbacks. Consult its README and release for the current matrix and commands.

production generator 由独立仓库
[`calcit-lang/calcit-bindgen`](https://github.com/calcit-lang/calcit-bindgen)
维护。本仓库只负责 Interface IR 提取、版本化 schema、exporter 语义与最小 conformance；
不再保存 generator backend、manifest、golden、WIT tooling 或 stale-artifact policy。

独立工具负责确定性的 Rust、方法化 Calcit、保留 namespace identity 的 TypeScript 与严格
WIT 子集生成。manifest 记录启用的 backend 和全部托管产物；未知版本、unsupported
definition、Dynamic/resource/callback 边界与超出 capability matrix 的类型都会明确失败，
不会生成 fallback。具体能力矩阵、命令和版本以独立仓库 README/release 为准。
