Memory and disposables
How OCCT wraps the wasm heap — `using` syntax, Symbol.dispose, leak diagnostics, pool patterns.
Every OCCT object you allocate in JS — gp_Pnt, TopoDS_Shape,
BRepPrimAPI_MakeBox, every handle, every container — owns memory inside the
WebAssembly linear memory. Failing to release it leaks the wasm heap, which
grows up to the 4 GB wasm32 ceiling and then crashes the runtime.
The using rule
Declare every disposable OCCT object with using so the runtime invokes
[Symbol.dispose]() when control leaves the scope.
{
using box = new oc.BRepPrimAPI_MakeBox(10, 10, 10);
using shape = box.Shape(); // also disposable
// ... use box / shape ...
} // ← both disposed here, in reverse declaration orderThe OCJS repo includes an oxlint rule, ocjs-lint/require-using-on-disposable,
that flags any plain const/let declaration of a disposable as an error.
It's wired into the in-repo eslint.config.mjs for the smoke-test suite but
is not auto-installed by pnpm add opencascade.js — the published npm
tarball ships only the wasm, JS loader, .d.ts, manifest, provenance, and
changelogs. If you want the rule in your own project, copy it from the OCJS
repo into your local lint setup.
Pure-data containers
Return-by-value (RBV) containers whose fields are all primitives (no nested
disposables) do not auto-emit [Symbol.dispose]. You can use them without
using:
// gp_Pnt has only doubles — no dispose
const point = transform.TransformedPoint(p0);
console.log(point.X(), point.Y(), point.Z());Coordinate accessors like X / Y / Z are methods, not properties — see
Calling OCCT from JS
for the pattern.
Containers with at least one disposable child auto-emit a [Symbol.dispose]
that walks the children and disposes each. The lint rule above handles the
classification automatically.
When using is not required
using is required for anything that owns wasm memory and surfaces a
[Symbol.dispose]. Four common shapes do not need a using on the call
site — see Return shapes for the full
decision table.
voidmethods with class-only outputs —curve.D0(u, pt)returnsvoid; theusinglives on the inputpt, not the call result.- Primitive / enum-only envelopes —
surface.Bounds(0,0,0,0)returns a plain{ U1, U2, V1, V2 }object with no disposer; bind it withconst. - Native primitive returns —
pnt.X(),list.Size(),face.IsNull()return numbers / booleans, never disposables. - Forwarding ownership out of a helper — when a function passes ownership
to its caller, the helper returns the disposable without
usingso the caller binds it. TheDisposableStack.move()pattern below is the canonical OCCT idiom.
Composing lifetimes with DisposableStack
The TC39 explicit-resource-management proposal also ships a built-in
DisposableStack — a LIFO container that adopts disposables and disposes
each one when the stack itself is disposed. Two OCCT-specific patterns
recur often enough to keep in your toolbox.
stack.use(...) — adopt a multi-handle return value as one resource
Some BRep_Tool overloads return an RBV envelope that owns several embind
handles (e.g. PolygonOnTriangulation in its 2-arg form returns a
{ P, T, L } triple). Binding the whole envelope through using is the
easy path, but if you only want to forward it once and free everything in
one shot — stack.use() adopts the envelope and lets the parent stack
cascade through its [Symbol.dispose].
using stack = new DisposableStack();
const r = stack.use(oc.BRep_Tool.PolygonOnTriangulation(edge, loc));
// ... read r.P / r.T / r.L ...
// stack disposes here, which disposes r, which disposes P/T/L in turn.stack.move() — transfer ownership out of a search loop
The hardest OCCT lifetime puzzle is "iterate through a topology looking
for a match; on hit, hand the owning handles to the caller; on miss, free
everything before the next iteration". using alone can't express this
because a using binding always disposes at scope exit. DisposableStack
solves it: each iteration creates a fresh stack, adopts every interim
handle, and on success move()s the stack into the return value (which
empties the iteration-scoped stack so its scope-exit dispose is a no-op).
function findFirstTriangulatedEdge(shape: TopoDS_Shape):
DisposableStack & { edge: TopoDS_Edge; tri: Poly_Triangulation; loc: TopLoc_Location } {
const oc = getOC();
using explorer = new oc.TopExp_Explorer(shape, oc.TopAbs_ShapeEnum.TopAbs_EDGE, oc.TopAbs_ShapeEnum.TopAbs_SHAPE);
while (explorer.More()) {
using iterStack = new DisposableStack();
using current = explorer.Current();
const edge = iterStack.use(oc.TopoDS.Edge(current));
const loc = iterStack.use(new oc.TopLoc_Location());
// ... walk faces, look up triangulation ...
const tri = iterStack.use(oc.BRep_Tool.Triangulation(face, loc, 0));
if (!tri.isNull()) {
// Match: transfer ownership. `iterStack` is now empty, its scope-exit
// dispose is a no-op, and the returned stack owns edge/tri/loc.
return Object.assign(iterStack.move(), { edge, tri, loc });
}
explorer.Next();
// No match: `iterStack` disposes here, freeing edge/loc/tri this round.
}
throw new Error('no triangulated edge');
}
// Caller side — one `using` collects three handles:
using ctx = findFirstTriangulatedEdge(shape);
// The 3-arg PolygonOnTriangulation overload returns a native Handle smart
// pointer (disposable). For the elision overload that returns the {P, T, L}
// envelope, see the `stack.use()` example above.
using polygon = oc.BRep_Tool.PolygonOnTriangulation(ctx.edge, ctx.tri, ctx.loc);DisposableStack is part of the JS standard library wherever
[Symbol.dispose] is — no OCJS-specific runtime is involved. See
MDN: DisposableStack
for the full surface (defer, adopt, disposed, etc.).
Pool pattern for hot loops
In a render loop you may allocate the same OCCT object hundreds of times per second. Allocate once, reuse, dispose once:
class TransformPool {
private readonly transforms = new Map<string, typeof oc.gp_Trsf>();
get(key: string): typeof oc.gp_Trsf {
let trsf = this.transforms.get(key);
if (!trsf) {
trsf = new oc.gp_Trsf();
this.transforms.set(key, trsf);
}
return trsf;
}
[Symbol.dispose]() {
for (const trsf of this.transforms.values()) trsf[Symbol.dispose]();
this.transforms.clear();
}
}
using pool = new TransformPool();
for (let i = 0; i < 1000; i++) {
const trsf = pool.get(`rot-${i % 8}`);
trsf.SetRotation(axis, i * 0.01);
// ...
}Diagnosing leaks
Wasm heap grows monotonically — to detect a leak, measure
oc.HEAP8.byteLength before and after a representative workload.
const before = oc.HEAP8.byteLength;
runWorkload();
const after = oc.HEAP8.byteLength;
console.log(`heap delta: ${(after - before) / 1024} KB`);For deep audits, build with -sASSERTIONS=2 -fsanitize=leak (debug builds
only) and the runtime prints a leak summary on process.exit().
What [Symbol.dispose] actually does
Calls the wrapped object's C++ destructor through the embind glue. For
handles, this decrements the refcount; for value types, it frees the C++
allocation. The JS wrapper object becomes invalid — subsequent method calls
throw an embind error (text varies, typically along the lines of
BindingError: <T> instance already deleted).
Pitfalls
usinginside anif (cond)branch disposes when control exits the branch, not when the outer block ends. Move the declaration up if you need it across the branch.- Returning a
usingvalue disposes it before the caller sees it. Use a plainconstfor return values and disposal at the call site. - Passing a disposed object as a parameter throws on every method.
Disciplined
usingplacement is the cheapest defence.
Related
- Calling OCCT from JS — overload dispatch, enums,
defaults, and the
TopoDSdowncast bridge. - Return shapes — when a call returns an envelope, a
native value, or an in-place class output, and which of those need
using. - Handles and collections —
isNull/nullifyon smart pointers and theNCollection_*container family.
Calling OCCT from JavaScript
How OCCT C++ surfaces project into JS — suffix-free overloads, val-dispatched calls, string enums, default args, and the TopoDS bridge.
Handles and collections
Smart-pointer surfaces (isNull / nullify) and the NCollection_* container family — what shows up in JS and how to use it.