libcascade

Two-channel config model

Compile-time OCJS_* env vars vs link-time settings — what each channel controls and when to reach for it.

Maintainer track. Skip this page if you consume the published npm package — every shipped build already pins the right channel-1 flags. The channels matter when you rebuild libcascade from source; a custom build configures channel 2 only.

libcascade exposes two configuration channels with different lifecycles and different scope. Treat them as orthogonal — confusion between the two is the most common cause of build errors.

Channel 1 — compile-time OCJS_* env vars

Set at the bindgen + C++ compile stage. Bake into every .o file the final wasm is linked from.

VariableEffect
OCJS_EXCEPTIONS=1Compile with -fwasm-exceptions everywhere
OCJS_SIMD=1Compile with -msimd128
OCJS_RELAXED_SIMD=1Additionally emit -mrelaxed-simd (Chrome/Firefox only)
OCJS_STRICT_TYPES=1Escalate missing-typedef warning to a build failure (default 0: warn-only — the triage summary is always printed to stderr)

These flags pin into a build-flags manifest alongside each cached .o. Mixing builds with mismatched flags fails-loud at link time — never silent ABI breakage.

Set per-consumer-build in libcascade.config.ts. Apply only to the final emcc link step.

settings: {
  MODULARIZE: true,
  EXPORT_ES6: true,
  ALLOW_MEMORY_GROWTH: true,
  WASM_BIGINT: true,
  EVAL_CTORS: 2,
},
compilerFlags: { optimize: 'O3' },

These flags control loader shape (ESM vs CommonJS), memory limits, and environment detection. They cannot retroactively change the exception model or SIMD configuration the .o files were compiled with.

Why the split?

The bindgen produces one .o file per generated binding in the maintainer Docker pipeline. Caching them across consumer builds turns a 30-minute full build into a 60-second link. The cache key must be deterministic — that's what the compile-time channel fingerprint guards.

If consumers could change compile-time flags from their own config, the cache invariant would break: a downstream -fexceptions request would silently rebuild every class, defeating the cache. Splitting the channels makes the contract explicit.

When you actually need to change channel 1

Almost never as a consumer. The published build is the named single-threaded preset from build-configs/configurations.json, which pins:

  • OCJS_EXCEPTIONS=1 + OCJS_EH_MODE=wasm (native wasm exceptions)
  • OCJS_SIMD=1 (baseline SIMD, Safari-compatible)
  • OCJS_CLOSURE=true + OCJS_CONVERGE=true
  • THREADING=single-threaded

The matching config carries WASM_BIGINT: true and EVAL_CTORS: 2 at link time. The threaded variant retains BigInt but removes eval-ctors with EVAL_CTORS: null.

Presets are addressed by name, not by raw env vars — see Named compile-time configurations for the full list (single-threaded, single-threaded-smallest, multi-threaded, debug).

The combinations not yet pre-built are exotic — OCJS_RELAXED_SIMD=1 for Chrome-only deploys, custom allocator pairings, non-default WASM-opt budgets. To get one, fork the Docker image build pipeline and rebuild from source.

Diagnostic checklist

If a build acts strangely:

  1. Confirm channel-1 fingerprint is what you expect. The full flag set is recorded in the in-repo build manifest (regenerated by every bindings stage):
    cat build/build-flags.json
    For the published tarball, the consumer-facing equivalent is dist/opencascade_single.provenance.json, which captures the active preset, compile flags, and commit SHA the wasm was built from.
  2. Confirm channel-2 settings actually made it into the wasm (libcascade build --render-only prints the rendered flag list first):
    strings dist/my-build.wasm | grep -E 'STACK_SIZE|MAXIMUM_MEMORY'
  3. Recompare against a known-good cached build — drift here points at channel-1 cache poisoning (rebuild the deps layer).

On this page