|
OCCT-Light 0.1
C ABI and C++ veneer for multi-language CAD workflows
|
The rulebook. Every public symbol, every header, every signature in OCCT-Light follows the conventions in this document. Deviations require an explicit, documented exception.
Companion docs: ARCHITECTURE.md · BREPGRAPH_AS_CANONICAL.md · MODULES.md · BINDINGS.md.
Naming tables for the public C surface, the C++ veneer, and the internal C++ implementation live in `CODING_STYLE.md §2.1 / §3 / §4.1`. The short form: public functions are occtl_<noun>_<verb>, public types occtl_<noun>_t, public enums OCCTL_<DOMAIN>_<VALUE>, public macros OCCTL_<NAME>. The occtl prefix is reserved across every public-facing identifier — no aliases.
Module suffix is added when the noun isn't already module-scoped: occtl_io_step_read, but occtl_graph_create stays as is regardless of which module owns it.
Functions that construct or produce results are assigned to a verb family based on their domain role:
| Verb family | Use for | Result shape |
|---|---|---|
create | Geometry representations and standalone opaque/service handles. | out_rep, out_handle |
make | Direct model construction: topology elements, primitives, face builders, patterns. | usually graph + out_node, or out_graph + out_root. |
| Result/action verbs | Derived-copy operations and feature edits: transformed, mirrored, defeature, draft_faces. | usually out_graph + out_root. |
| Bare CAD verbs | Well-known operations whose verb is the domain term: fuse, cut, common, section, split. | operation-specific. |
| Query verbs | is_*, has_*, *_kind, *_count, *_view, *_nodes, *_ids, *_by_*, *_from_*. | read-only or two-call buffers. |
| Repair verbs | In-place topology repair: sew, recompute_same_parameter, check. | mutates or validates existing graph. |
Prefixes encode ownership domain:
| Prefix | Owns |
|---|---|
occtl_graph_ | Graph lifecycle, clone/compact/cache, graph-wide counts, UID tables, metadata layers, tags, units, history, graph-level iteration. |
occtl_topo_ | Topological entities, topology relations, topology repair/checking, topology construction, occurrence/product topology, topology feature operations. |
occtl_curve_ / occtl_curve2d_ / occtl_surface_ | Geometry representation creation, interrogation, extraction, evaluation, approximation/intersection. |
All int32_t boolean out-parameters use out_is_* or out_has_* predicate names:
This convention makes the direction and boolean semantics immediately visible at the call site. (The int32_t type for booleans is already mandated by §13.)
Use *_count for all count fields and out-parameters. The *_nb_ form is not part of the public ABI.
Output parameters precede input parameters (§7). Within the input group:
This keeps call sites predictable across the entire surface.
All public enums in the occtl_io_step and occtl_io_iges modules use the occtl_io_step_ / occtl_io_iges_ prefix:
A single macro controls visibility:
Build flags: -fvisibility=hidden -fvisibility-inlines-hidden on Unix; /Zc:__cplusplus /utf-8 on MSVC. Only OCCTL_API-marked symbols leak.
Every public function declaration is OCCTL_API occtl_status_t OCCTL_CALL occtl_xxx(...);. The OCCTL_CALL matters for Win32-32-bit P/Invoke; we set it explicitly to __cdecl and document it.
A public header may include only:
<stdint.h>, <stddef.h>occtl_*.h headers from this projectIt must not include <string>, <vector>, <memory>, <Handle.hxx>, <TopoDS_*.hxx>, <NCollection_*.hxx>, or anything else from C++/STL/OCCT.
A CI check enforces this:
The C++ veneer (occtl-hpp) may use STL freely; it's an opt-in #include.
Each header guards its content with both an include guard and extern "C":
Heap-allocated objects are exposed as opaque types:
The struct definition lives in internal headers; consumers only ever see the typedef and pass occtl_graph_t*.
Lifetime contract is documented in the docstring of every function returning a pointer:
occtl_<noun>_free. Free functions are NULL-tolerant and idempotent.Examples:
Free functions never return a status; they cannot fail in a way the caller can act on. They log internally if something goes wrong.
Identity types are passed by value, not allocated:
Wrapped as a struct (not a bare uint64_t) so that the type system catches "vertex id passed where a face id was expected" at the C++ veneer level and so that bindings see a distinct nominal type. The bits field is layout-stable but the internal bit layout (kind in [63:56], reserved in [55:48], payload in [47:0]) is a private implementation detail confined to src/topo/TopoMath.hxx. No static inline helpers expose it in public headers — to query the kind of an ID, call the graph function (e.g. occtl_graph_node_kind) which internally unpacks to the OCCT entity type.
See BREPGRAPH_AS_CANONICAL.md §3 for the full ID model.
OCCTL_OK == 0. The idiom if (occtl_xxx(...) != OCCTL_OK) goto fail; works everywhere. The convenience macros OCCTL_FAILED(s) / OCCTL_SUCCEEDED(s) are provided.
Reserved for: an extractor (occtl_*_as_<kind>, occtl_*_bspline_*) is called on an opaque handle for which the data the function unpacks is not present. This covers two cases:
occtl_curve_as_circle on a line, occtl_curve_bspline_poles on an analytic conic.occtl_curve_bspline_weights on a B-spline whose IsRational() is false (no weight array exists to extract).Do not use OCCTL_WRONG_KIND for:
OCCTL_GEOMETRY_INVALID or OCCTL_INVALID_ARGUMENT.curve_t with a surface_t API). Those are OCCTL_INVALID_ARGUMENT.OCCTL_GEOMETRY_INVALID.The contract: a caller that has already inspected kind() (and, for sub-state-gated extractors, the matching is_rational / is_periodic flag) and dispatched on it must never see OCCTL_WRONG_KIND. It is a programmer-error code, not a data-driven one.
Extended subcodes use a 32-bit value — primary code in the low byte, refinement in the upper bits:
Primary codes are stable forever; extended codes evolve. Bindings filter on the primary; debug tools surface the extended.
occtl_error_last() returns a pointer to thread-local storage — it never returns NULL, but status == OCCTL_OK means "no error pending". Calling another OCCTL function may overwrite it on this thread; copy out anything you need before the next call.
Every internal entry point is wrapped:
The guard helper catches Standard_Failure, std::exception, and ..., populates the thread-local error, and returns OCCTL_INTERNAL. No exception ever crosses the C boundary.
Rule: ordinary fallible ABI functions return occtl_status_t. Outputs are written through pointer parameters. When there is one primary output, it goes first. When there are several outputs, primary first then secondary, all before inputs.
The only direct-return exceptions are genuinely infallible pure-value helpers in core and POD math helpers in geom, such as status/runtime/version accessors, occtl_uid_equal, and value-to-value point/vector/direction/transform functions. Graph queries, data-exchange helpers, ownership-transfer functions, and anything that validates a handle or user pointer use status + out-parameter.
Inputs are const-qualified pointers. Output pointers must not be NULL when the function is called — passing NULL where the API expects an output pointer is OCCTL_INVALID_ARGUMENT.
Within the input group, see §1.5 for the read-path / write-path ordering convention.
Convention: name parameters out_<thing> for clarity in headers.
Every constructor takes a const <name>_create_info_t* rather than a long parameter list. This is the pattern that survives evolution:
Every options struct has:
uint32_t struct_version as its first field, set to a *_VERSION_<N> macro.const void* p_next as its second field, reserved for extension chains. Always NULL until an extension is shipped.*_INIT) for C use.*_init) for binding consumers that can't use C macros.When fields are added in a future version, a new *_VERSION_2 macro and a new struct layout are introduced; the implementation reads struct_version and uses the right layout. Old binaries continue to work.
All strings are UTF-8. Always.
Length-counted variants are preferred for any user-supplied content; NUL-terminated is fine for paths and small identifiers from the program itself.
Caller calls once with buf == NULL to learn the size, allocates, calls again. Returns OCCTL_BUFFER_TOO_SMALL if buf_size was insufficient (with *out_required set), OCCTL_OK otherwise. The output is always NUL-terminated when OCCTL_OK is returned.
When the library can hand back a pointer to its own storage (format names, layer names, error messages), the function returns const char* directly with documented lifetime:
Used only when the two-call pattern is awkward (e.g., complex variable-length tree dumps):
Prefer the two-call pattern. occtl_string_t exists for the cases where it's genuinely better.
Pick from a fixed menu, per use case:
Same shape as the string variant. Returns OCCTL_BUFFER_TOO_SMALL if capacity is too small. Count out-parameters use *_count (§1.4); *_nb_* names are not part of the public ABI.
Steer: for sparse-iteration sources (e.g. BRepGraph iterators that omit removed nodes), prefer §10.3 — the two-call buffer would otherwise walk the structure twice (once to size, once to fill) for data the caller usually iterates once and discards. Use §10.1 for stable snapshots whose length is genuinely unknown until the call has run, not for pull-style enumeration.
The view points into library-owned memory. Lifetime is documented per call — typically "valid until the graph is mutated or freed". This is what lets NumPy, Span<T>, and TypedArray consumers wrap the data zero-copy.
The canonical shape is one opaque type, many factories, one shared _next / _free pair when every factory yields the same value type. The shipped topo iterators use this exact shape (one occtl_node_iter_t, 19 *_iter_create factories, shared occtl_node_iter_next / _free):
Why one type rather than per-kind types: the alternative (occtl_face_iter_t, occtl_edge_iter_t, …) multiplies the binding footprint by the number of factories. With a uniform output type (occtl_node_id_t) only the source varies; collapsing to one type halves binding-side stub code and keeps _next / _free on the cache line for every iterator.
End-of-iteration sentinel: _next returns OCCTL_OK on each step; OCCTL_NOT_FOUND once exhausted, with OCCTL_NODE_ID_INVALID written to the out-param. Subsequent calls remain OCCTL_NOT_FOUND (idempotent end). When a different value type is needed (e.g. an iterator that yields (NodeId, Transform) for child-explorer traversal), introduce a new opaque type for that family rather than overloading the existing one.
occtl_error_last() on the end-of-iteration path: do not populate it. Iterator exhaustion is the contract's success terminus, not an error. Reserve occtl_error_last() for argument-level failures (NULL inputs, uninitialised handles) so clients that poll the error state every call do not see a phantom "iterator exhausted" message at the natural end of every loop.
Lifetime / invalidation: documented per factory. Topo iterators borrow from the source graph; adding nodes or freeing the graph while an iterator is live is undefined; removing a not-yet-visited node is well-defined and omitted (matching OCCT's IsRemoved semantics).
Distinct types are correct when the yield differs: an iterator that yields strings, or (node, score), or transformed shapes from a child-explorer, gets its own opaque struct and its own _next / _free.
Both shapes coexist for the same data when feasible, because callbacks across FFI are awkward in some languages (notably WASM) and iterators are awkward in others.
When an opaque entity has five or more correlated fields that callers tend to read together — most often a curve / surface / face introspection — expose a single function that fills a caller-allocated POD struct in one call, with borrowed pointers into library-owned storage for any variable-length parts.
This is the stable OCCT-Light shape for aggregate reads:
OCCT-Light combines them: caller-allocated outer + struct_version + p_next + borrowed inner spans whose lifetime tracks the parent entity. No allocation by the library; no paired free; one FFI call replaces ten.
| Element | Pattern | Example |
|---|---|---|
| View type | occtl_<entity>_<kind>_t (no _view suffix; see below) | occtl_curve_bspline_t |
| Extractor | occtl_<entity>_as_<kind>(entity, &out) | occtl_curve_as_bspline |
| Version macro | OCCTL_<ENTITY>_<KIND>_VERSION_<N> | OCCTL_CURVE_BSPLINE_VERSION_1 |
| Static initializer | OCCTL_<ENTITY>_<KIND>_INIT | OCCTL_CURVE_BSPLINE_INIT |
| Init function | occtl_<entity>_<kind>_init(POD*) | occtl_curve_bspline_init |
The output type does not carry a _view suffix: whether internal pointers are borrowed (lifetime = parent) or absent (fixed-shape POD) is documented per function. Callers learn the contract from the docstring, not the name. This keeps the existing _as_circle / _as_line / _as_ellipse family intact — they are already aggregate-view reads with no borrowed pointers, and the name shape is unchanged.
Caller use, C:
Caller use, binding (cffi):
Borrowed pointers in the filled struct are valid until the parent entity is freed or mutated. Concretely for a curve view: the pointers are valid until the next call that takes the same occtl_curve_t* non-const (none today; occtl_curve_free invalidates).
Document the contract on every aggregate-view function. The docstring sentence is:
Pointers in
outborrow fromcurveand are valid untilcurveis freed.
struct_version is input: the caller declares which version of the layout they understand. The library:
OCCTL_VERSION_MISMATCH if struct_version is unrecognised (older library, newer caller).struct_version indicates.New fields are appended; the layout up to a given _VERSION_<N> is frozen forever. p_next is reserved for future extension chains and must be NULL today.
occtl_curve_as_bspline on a line) — return OCCTL_WRONG_KIND per §6.1. No fields are written.IsRational() is false) — return OCCTL_OK and set the optional pointer (weights) to NULL. Do not error on optional fields; bindings rely on the NULL-means-absent sentinel.OCCTL_GEOMETRY_INVALID.Atomized accessors (occtl_curve_bspline_degree, _pole_count, _poles, ...) remain when an aggregate-view function exists. They are current API, not a removal path. The aggregate-view is purely additive:
§10.2 covers the case where the only output is a single span (e.g. mesh triangulation). §10.5 generalises to many fields, some of them spans. When the natural answer is "one span and that's it," prefer §10.2 — fewer types, less boilerplate. When the entity has scalars and spans, use §10.5.
_snapshot, paired with _snapshot_release); not adopted in this revision.Per view type, three pieces, mirroring §8:
_VERSION_<N> integer macro per shipped version of the struct._INIT static-initializer literal that zero-initialises everything except struct_version._init(POD*) runtime function for binding consumers that cannot use C macros.We do not provide generic OCCTL_VIEW_INIT(type, var) magic, _Generic dispatch, or x-macros. Each view type is hand-written; the cookie-cutter is small and explicit. Binding generators read the headers directly and don't need preprocessor introspection.
Not exposed. Underlying maps (layer attributes, name registries) are surfaced via either a paired-array snapshot or an opaque iterator. Bindings reconstruct native dicts.
Rules:
void* user_data is always the last parameter. No exceptions.noexcept. C++ callers must catch their own; the wrapper does not.occtl_status_t. OCCTL_CANCELLED short-circuits the host operation.typedef occtl_status_t (OCCTL_CALL *occtl_progress_callback_t)(...).Bindings with garbage collectors must keep a strong reference to the wrapper holding user_data for the lifetime of the registration.
Use int32_t (0 = false, non-zero = true), not bool, not _Bool. This is the form that crosses every binding without size or marshalling surprises (notably P/Invoke on .NET Framework, Win32-32-bit, and some WASM toolchains). The C++ veneer takes bool and converts.
OCCTL_<ENUM>_RESERVED_FUTURE = 0x7fffffff so the underlying storage is forced to 32-bit signed.OCCTL_UNSUPPORTED.int32_t and switch on known values; unknown values map to a "future / unrecognized" sentinel.Geometry reps (occtl_curve_t*, occtl_surface_t*, and their occtl_rep_id_t identifiers) are graph-owned implementation objects. Public callers never own an OCCT geometry object directly. A successful geometry create function stores the rep in the target graph and returns the rep ID that later topology builders can reference.
The create verb is therefore intentional for geometry reps: it creates graph state, not a standalone heap object that the caller must free. Functions that turn reps into topology (occtl_topo_curves_to_wire, occtl_prim_make_face_from_surface, and related helpers) borrow those rep IDs from the graph and do not transfer rep ownership.
Bindings should expose rep IDs as strong nominal values tied to the owning graph. They should not provide a public "free curve" or "free surface" API for graph-owned reps; releasing the graph releases its reps.
The three dimensions (library SemVer, OCCTL_ABI_VERSION, per-struct struct_version) are spelled out in ARCHITECTURE.md §7. What's specific to this doc:
occtl_point3_t, occtl_geom_circle_t, occtl_axis2_placement_t, occtl_oriented_node_t are value types — passed by value, embedded, stack-allocated. If their shape has to change, ship a new type (occtl_geom_circle2_t); don't retrofit a struct_version field. Versioning is for bags, not for values._as_<kind> extractors follow §10.5: occtl_curve_as_circle outputs a math POD and stays as is; occtl_curve_as_bspline outputs an inspection bag and follows §10.5._v2; old symbol is preserved bit-for-bit until a major version bump.A header-only C++ wrapper layered over the C ABI. No additional library; nothing links against it; it just makes C++ users productive.
Design rules:
occtl::Graph) that holds the occtl_graph_t* and frees on destruction. Move-only by default; copy is explicit (Graph::clone()).occtl::Error, which carries code, message, source (UID), and extended.Graph Graph::create(const CreateInfo&)).std::string_view; output as std::string for owned strings, std::string_view for borrowed strings (with documented lifetime).std::span<const T> where C++20 is available; otherwise (const T* data, size_t size) accessors plus .to_vector() materializers.What the veneer does not do:
| Language | Approach |
|---|---|
| Python | cffi (ABI mode) + thin Pythonic facade. NumPy interop via §10.2 spans. |
| C# | P/Invoke from a Roslyn source generator over the C headers. Span<T> for view types. |
| JS/TS native | Node N-API addon + TS types generated from the C headers. |
| JS/TS web | Emscripten compile of the C surface; thin Embind glue. |
| Rust | bindgen for the unsafe occtl-sys crate + hand-written safe occtl crate mirroring occtl-hpp. |
| Go | cgo + hand-written safe layer. |
| Java | JNA raw layer generated from build/abi.json + thin idiomatic facade and parity runner. |
| Swift | Direct C-header import; thin Swift facade. |
Per-language facades live in bindings/<lang>/; they evolve faster than the C surface and are not part of the ABI.
Full plan — the uniform binding contract every facade implements, per-language layouts, packaging, test parity, and the checklist for adding a new language — is in BINDINGS.md. This table is the index; that doc is the body.
Every public function header in include/occtl/ carries Doxygen-style comments:
Each @param says owns it / borrows it explicitly. Each function lists every status code it can return. Generated HTML is published alongside binary releases.
bool or _Bool in public signatures.Handle(...) typedefs, even as void*. Wrap in an opaque struct.static thread_local char buf[256]) for arbitrary string output. Use the two-call pattern.extern "C". Ever.