|
OCCT-Light 0.1
C ABI and C++ veneer for multi-language CAD workflows
|
Two surfaces, two styles. The public C ABI uses idiomatic C conventions (snake_case,
occtl_*prefix). The internal C++ implementation follows OCCT conventions (theParam,aLocal,myField, file separators). Both surfaces are aggressively documented.
Companion docs: ARCHITECTURE.md · ABI_PATTERNS.md · BREPGRAPH_AS_CANONICAL.md · MODULES.md · BINDINGS.md.
Public C is lower_snake_case because that's what C compilers and binding generators expect; internal C++ follows OCCT (thePar / aLocal / myField) so it reads the same way for OCCT contributors. The extern "C" shim is the boundary; the two guides never collide inside the same identifier.
| Element | Pattern | Example |
|---|---|---|
| Function | occtl_<noun>_<verb> | occtl_graph_create, occtl_error_clear |
| Type | occtl_<noun>_t | occtl_graph_t, occtl_status_t |
| Enum value | OCCTL_<DOMAIN>_<VALUE> | OCCTL_OK, OCCTL_KIND_FACE |
| Macro | OCCTL_<NAME> | OCCTL_API, OCCTL_VERSION_MAJOR |
| Parameter | lower_snake_case | out_graph, node_id, info |
| Out parameter | prefixed out_ | out_graph, out_required |
The full convention is in ABI_PATTERNS.md §1.
Constructor verbs are split between _create_<variant> (allocates an out-handle the caller owns) and _make_<entity> (mutates a graph and returns a borrowed node id, mirroring OCCT's BRep*API_Make* family). The two are not interchangeable; see ABI_PATTERNS.md §1.1 for the table and rationale.
Every public function, type, and enum carries a Doxygen block in C-style /** */:
Required tags on every public function:
| Tag | When |
|---|---|
@param[in] / @param[out] / @param[in,out] | Every parameter. Pointer params carry owns it or borrows it plus NULL semantics. Value-typed params (POD structs and scalars passed by value) get @param[in] only — no ownership tag, no NULL clause; the value owns itself. |
@retval | Every status code the function can return. List them all. |
\\par Thread Safety | Yes, No, or a one-line qualifier (e.g. Concurrent reads of distinct graphs are safe). |
@sa | Related functions (_free mate, alternative call, etc.). |
Versioned *_init setters (the runtime initialisers paired with each _INIT static initialiser) carry the full block too — no brief-only one-liners. They take a single caller-allocated struct, write defaults into it, and return void. Required shape:
info is Borrows it because the caller retains ownership of the memory the function writes into — the function does not allocate, retain, or free anything.
Worked example for a value-in / value-out function (no pointers, no status code, so no @retval):
Required tags on every public type:
Required tags on every public enum:
_free mate, the alternative shape (callback vs iterator), the _v2 if applicable.examples/ instead.Headers use no section separator comments and no free comments. The only accepted comments are SPDX, Doxygen blocks/lines, and include-guard end comments. Each declaration or group stands on its own with its Doxygen documentation block.
Forbidden in all headers:
─, ━, ┄, etc.) in comments — use plain ASCII only.//===, // ---, /* ==== */ or any other separator decoration./// @brief / //! @brief on the symbol it documents, or a ///< / //!< trailing brief.The veneer mirrors the C ABI in modern, ergonomic C++. Identifier naming for parameters, locals, and members follows OCCT (the* / a* / my*) — same as the internal C++ — so the C++ identifier rules do not change when you cross from src/ into include/occtl-hpp/. The visible STL-flavour comes from the shape of the API (lowercase namespace, snake_case methods, STL types, exceptions), not from the identifiers inside it.
| Element | Pattern | Example |
|---|---|---|
| Namespace | occtl::<sub> (lowercase, distinct from internal OcctL::*) | occtl::core, occtl::topo |
| Class | PascalCase | Graph, Error, NodeId |
| Public method | snake_case (STL-shape) | graph.face_count(), node.kind() |
| Free function | snake_case | version(), abi_version() |
| Method parameter | the<Name> | theInfo, theEnabled, theStatus |
| Local variable | a<Name> / an<Name> | aMajor, anError, aHandle |
| Class member | my<Name> | myInitialised, myCode |
| Constant | THE_<NAME> (TU-static) or constexpr | THE_DEFAULT_TIMEOUT |
The veneer uses STL freely: std::span, std::string_view, std::optional, exceptions. Headers are .hpp. Doxygen blocks use /// or /** */ consistently per file.
The veneer never re-documents C ABI semantics — it @see-references the C entry point.
Forbidden in veneer headers:
//=== separators or any other ASCII-art section dividers. Veneer headers use no separators (same rule as public C headers).// comment must be /// @brief on the symbol it documents, or ///< trailing on the same line.Why the split. Method names in the veneer are snake_case because that is the shape STL/Boost/range-v3 consumers expect to type. Identifier naming (params/locals/members) is OCCT because the same rule must hold across the entire .hxx / .cxx / .hpp set — there is no value in two parallel naming worlds for the same the* concept.
This is OCCT-aligned territory. Style follows the project's OCCT-oriented implementation conventions.
| Element | Pattern | Example |
|---|---|---|
| Class | PascalCase, no Package_ prefix (we use namespaces instead) | Graph, ErrorState |
| Public method | PascalCase | Initialize(), Shape() |
| Private method | camelCase | cacheLookup() |
| Method parameter | the<Name> | theGraph, theTolerance |
| Local variable | a<Name> / an<Name> | aBuilder, anIndex |
| Class member | my<Name> | myHandle, myInitialised |
| Struct member (POD) | <Name> | Code, Message |
| Global / TU-static | THE_<NAME> | THE_OCCT_VERSION_STRING |
| Namespace | OcctL, OcctL::Core, OcctL::Topo | (PascalCase, single-word segments preferred) |
for, structured bindings, if constexpr, std::optional, std::string_view are encouraged where they read better.auto is only allowed for: lambda types, range-based for over containers whose element type is verbose, and structured bindings. Never for "I don't want to type the type."nullptr, never NULL or 0 for pointers.[[nodiscard]] on factory and query methods that return values the caller would obviously want to inspect.std::vector** and **std::array** at internal C++ boundaries (the implementation never crosses the C ABI). NCollection types are used only when interoperating with OCCT APIs that expect them.NCollection_Sequence — its linked-list shape is a performance trap. Use std::vector or NCollection_Vector.std::string / std::string_view. Convert to TCollection_AsciiString at the OCCT boundary.std::unique_ptr for sole ownership across function boundaries inside the implementation. Handle(...) for OCCT reference-counted types.[[nodiscard]] static factory methods returning std::unique_ptr<T> for non-OCCT objects.const on every value parameter that is not modified, including in the .hxx and .cxx.const on every method that does not modify the object.constexpr for compile-time constants.OCCT-style Doxygen with //! lines:
Conventions:
@param[in] / @param[out] / @param[in,out] on every parameter.@return for non-void returns.//!-attached to a function, variable, type, or @file / @namespace / @name group) are forbidden. If a comment is worth writing, it belongs on the declaration it describes.Each method is preceded by exactly this 100-character separator (// followed by 98 = signs) and an empty line:
Do not use the old-style // purpose: … // function: … block. The empty line after the separator is required.
Forbidden patterns in .cxx / .hxx files:
Three-line section-header blocks with a label sandwiched between separators:
Section labels between separators are not separators — they are orphan comments. Each function definition gets its own standalone //=== line; grouping is implicit.
─, ━, ┄, etc.). ASCII only.// ---, // ———, etc.). Only // + 98 = is valid.= count. Count matters: 98, not 97, not 99. The line is exactly 100 characters: // plus 98 =.Default to none. Add a one-line comment only when:
Never:
Keep comments minimal and focused on non-obvious constraints.
Header includes are grouped: project headers first (""), then OCCT headers, then standard library. One blank line between groups.
Every extern "C" entry point is wrapped in OcctL::Core::Guard:
Guard catches Standard_Failure (root of all OCCT exceptions), std::exception, and ... and populates the thread-local error via TranslateException. No exception ever escapes extern "C".
When a specific OCCT failure needs a cleaner status code than OCCTL_INTERNAL, catch it explicitly inside the lambda — the inner catch runs before Guard's outer catch:
Catch Standard_Failure (not a narrower subclass) to guarantee the catch fires regardless of which OCCT exception type is thrown.
Every module that links OCCT delegates to OCCT — see AGENTS hard rule #2 in the repository root. The pattern is three lines: convert POD → OCCT, call OCCT, convert back.
Conversion helpers live in <module>/*Math.hxx (e.g. src/geom/GeomMath.hxx, src/topo/TopoMath.hxx):
Forbidden (unless no OCCT equivalent): hand-rolled vector / matrix / transformation math, custom curve / surface evaluation, magic tolerance thresholds. Acceptable: field copies in ToGp / FromGp, integer index work, STL container management for non-geometric data.
Tolerances come from OCCT's named constants — Precision::Confusion() (linear), Precision::SquareConfusion() (squared), Precision::Angular() (radians), gp::Resolution() (the threshold OCCT enforces inside gp_Dir(gp_Vec) / gp_Vec::Normalize()). When a shim pre-validates a vector before handing it to OCCT, copy OCCT's exact comparison (aSqMod <= gp::Resolution() * gp::Resolution()) — anything looser silently rejects inputs OCCT would have accepted.
Tests are gtest-based C++ that drives the public C API (or the C++ veneer when testing veneer-specific behavior).
//=== separators in test files. Tests don't need them and they clutter.// Vertex tests, // Edge tests, etc.). Use blank-line separation between groups of related tests.// ---). If you need a visual separator, a single blank line is enough.<Module>Test; test names <Method>_<Scenario>_<Expected> (e.g. Create_NullInfo_ReturnsInvalidArgument).EXPECT_EQ / EXPECT_NE / ASSERT_EQ. EXPECT_NEAR for floating-point with Precision::Confusion() or a documented tolerance.EXPECT_THROW(..., occtl::Error) to assert that the C++ veneer translates a non-OK status code into an exception. The veneer is always built with C++ exceptions enabled.OCCT headers first, blank line, gtest header. Anonymous namespace wraps the tests.
add_library, target_link_libraries).add_* call; no multi-target one-liners.target_* over directory-level *_directories / *_link_libraries.PRIVATE and only widen when consumers genuinely need it.CamelCase for project-local; OCCTL_* SCREAMING_SNAKE for cache options.| Tool | What it enforces |
|---|---|
clang-format | C++ formatting (config in repo root). Public C and internal C++ share one config; the differences are stylistic, not formatting. |
clang-tidy | Modernization checks (modernize-*) and bug-prone checks. Suppression files per directory. |
| Public-header hygiene | No <string>, no Handle.hxx, no TopoDS_*.hxx, no STL or OCCT identifiers in include/occtl/*.h. Fails the build. |
| Public-header style | No free comments or separator banners in include/occtl/*.h or include/occtl-hpp/*.hpp; only SPDX, Doxygen, and include-guard comments. |
| Guard audit | Every exported OCCTL_API occtl_status_t C shim in src/**/*.cxx contains OcctL::Core::Guard. |
| Doxygen build | Generates HTML for both public C and the C++ veneer. Warnings as errors. |
.editorconfig | Indent (2 spaces, no tabs), line endings (LF), trailing newline, UTF-8. |