OpenCascade.js

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:

  1. Walks every header listed in the active YAML via libclang.
  2. Builds an AST per class, applies the bindgen-filters.yaml exclusions, and classifies each member as constructor / static method / instance method / property.
  3. Resolves typedefs (including the OCCT occ::handle<> family) against a shared cache.
  4. Emits one .hxx per class with EMSCRIPTEN_BINDINGS(...) registrations and one .d.ts.json shard 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.

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.json shards 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

SymptomStageFix
libclang: cannot find <Standard_Real.hxx>1OCCT headers not mounted into the Docker image
undefined symbol: _ZN10TopoDS_...3Remove a bindings: entry whose .o no longer exists, or the class transitively needs another class you trimmed
BindingError: invalid type at runtime3Duplicate EMSCRIPTEN_BINDINGS(<name>) group across additionalBindCode and a generated .hxx
Codegen emits any for a known type1The 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 reference1Same 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 TopoDS downcast 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 stageDocker stagePublished tag
1 — discoverbindgen-base:bindgen-base
2 — emit (TUs + .d.ts)bindgen-base:bindgen-base
3 — compile + linkcompiled-{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.

On this page