Bindgen pipeline
How libclang → embind → wasm-ld combine to produce the bound TypeScript surface.
Maintainer track. Read this if you rebuild OCJS from source or extend it
with custom C++. If you pnpm add opencascade.js and call the published
wasm from JS, you can skip this page — the consumer-facing rules live in
Calling OCCT from JS and
Return shapes.
The OCJS bindings generator is a Python orchestrator over libclang and emscripten's embind. This page maps the stages and the artifacts they emit.
Stage diagram
Stage 1 — bindgen
scripts/build-wasm.sh bindings invokes the Python bindgen which:
- Walks every header listed in the active YAML via libclang.
- Builds an AST per class, applies the
bindgen-filters.yamlexclusions, and classifies each member as constructor / static method / instance method / property. - Resolves typedefs (including the OCCT
occ::handle<>family) against a shared cache. - Emits one
.hxxper class withEMSCRIPTEN_BINDINGS(...)registrations and one.d.ts.jsonshard per class describing the TypeScript shape.
The bindgen is deterministic: same headers + same filter YAML = identical output bytes.
Stage 2 — compile
em++ compiles each .hxx to a .o. The cache key is the .hxx content
hash plus the compile-time flag fingerprint. Cached .o files survive across
consumer builds — that's the speedup that makes a custom-trimmed build take
60 seconds instead of 30 minutes.
Stage 3 — link
emcc links the selected .o files (per the consumer YAML's bindings:
list) against the pre-compiled OCCT library archives in dist/libs/. The
output:
<name>.wasm— the wasm binary.<name>.js— the emscripten loader glue.<name>.d.ts— the merged TypeScript declarations (from.d.ts.jsonshards filtered to the bound symbol set).<name>.build-manifest.json— symbol coverage report (requested vs compiled, wasm bytes, validation flags).
How custom C++ enters the pipeline
additionalCppCode + additionalCppFiles get concatenated into one TU which
flows through a smaller variant of stages 1+2 — bindgen discovers Handle/NCollection
references, em++ compiles, the result links into the final wasm alongside the
auto-generated bindings.
additionalBindCode skips bindgen entirely — it's a literal .cpp snippet
that gets compiled with <emscripten/bind.h> already included.
When the pipeline fails
| Symptom | Stage | Fix |
|---|---|---|
libclang: cannot find <Standard_Real.hxx> | 1 | OCCT headers not mounted into the Docker image |
undefined symbol: _ZN10TopoDS_... | 3 | Remove a bindings: entry whose .o no longer exists, or the class transitively needs another class you trimmed |
BindingError: invalid type at runtime | 3 | Duplicate EMSCRIPTEN_BINDINGS(<name>) group across additionalBindCode and a generated .hxx |
Codegen emits any for a known type | 1 | The link step always prints a triage summary to stderr when this happens. The build still proceeds by default; set OCJS_STRICT_TYPES=1 in CI to fail the build instead of shipping a poisoned .d.ts. File an issue with the printed triage summary. |
Codegen emits unknown / surfaces an unbound reference | 1 | Same gate as above. Warning printed by default; OCJS_STRICT_TYPES=1 escalates to a hard failure for CI consumers. |
What the pipeline produces — JS-side contract
The artifacts above are an implementation detail. The contract those artifacts
expose to JS consumers — overload-dispatched calls, in-place class outputs,
returnValue envelopes, Handle elision — lives in two consumer-facing
concept pages:
- Calling OCCT from JS — overload dispatch, enums,
defaults, and the
TopoDSdowncast bridge. - Return shapes — class outputs, envelopes,
returnValue, and Handle elision.
Docker stage mapping
The pipeline stages map directly to the published Docker stages described in Docker image:
| Pipeline stage | Docker stage | Published tag |
|---|---|---|
| 1 — discover | bindgen-base | :bindgen-base |
| 2 — emit (TUs + .d.ts) | bindgen-base | :bindgen-base |
| 3 — compile + link | compiled-{threading} + final-{threading} | :single-threaded, :multi-threaded |
:bindgen-base is both a build stage and a published image — it carries the
patched OCCT tree, the PCH, and the .d.ts.json index but not the
pre-compiled .o files. Custom-bindings consumers pull :bindgen-base,
re-run generate against their own YAML, and compile from there.
Related
- Extend with C++ — how to inject custom C++ into the pipeline.
- Trim symbols — controlling which classes survive into stage 3.
- YAML schema — every YAML key the pipeline consumes.