Pitfalls
What went wrong while building NetOcc, and why: traps in exposing OCCT to C#, most of them whatever the wrapper technology (C++/CLI or SWIG and P/Invoke). The rules that avoid them are in the CLAUDE.md files of netocc-core (Gotchas) and netocc-generator (Mapping rules); the decisions they led to are in Design decisions, what stays out in Skipped members.
Memory and identity
- Handle lifetime.
Standard_Transientobjects carry their own reference count. The C# proxy increments it when it wraps the object and decrements it inDispose/the finalizer. Value classes are copied instead. - Borrowed references. A
const T&return points into its parent; once the parent is collected, the proxy dangles. Return owned copies, or keep a parent reference in the child proxy. The reverse also bites: C++ keeping a raw pointer to a C#-owned object.new BRepGraph_FaceIterator(graph).Count()crashed on x86: nothing usedgraphafter the constructor, so the GC finalized it mid-enumeration. - Identity. Two proxies can wrap one native object, so
ReferenceEqualsmeans nothing. OverrideEquals/GetHashCode: native pointer for transients,IsEqual+std::hash<TopoDS_Shape>for shapes. - Downcasting. A C# cast between proxy types never reaches the C++ object. Generate
DownCasthelpers (Geom_Line.DownCast(curve)). - Finalizer thread. Finalizers run on the GC thread, in any order. Dispose viewer and GL-backed objects deterministically (
using). OCAF needs more than that, see OCAF. - What an object keeps must outlive its finalizer. Holding a kept argument in the proxy's field isn't enough: when both become garbage in one collection, the argument's finalizer may run first, and the object's destructor then uses freed memory (
BRepGraph_MutGuard's clears its item in the graph). Hold it in a table until a long weak reference (tracking resurrection) says the proxy is collected. Only for arguments the object can't own: a member's argument may be borrowed from the object, and a strong table would keep both forever. - OCCT's doc comments. libclang attaches
//!comments to the next declaration (Cursor.ParsedComment), trailing//!<to the previous one, and parses the Doxygen commands (@param,@return,@code,\c). C# parameter names are the generator's safe form of OCCT's (SWIG keeps them), which pins overloads better than types. The text is LGPL: generate it at pack time, ship the license with it, never commit it. - Enums without a fixed type differ per ABI. MSVC makes them
int, the Itanium ABI (Linux, macOS)unsigned intwhen no value is negative. Parsing on Linux gavecsbase uintand a different skip log (the golden test failed there only). Only a written type counts (enum X : T,enum class): libclang has noisFixed, the declaration's tokens tell (a:before{). - Linking OCCT's reference manual. Doxygen names pages after the C++ name: a capital becomes
_and its lower letter,_becomes__,::becomes_1_1(class_b_rep_prim_a_p_i___make_box.html); structs arestruct...html, namespacesnamespace...html; enums live on their header's page (gp___trsf_form_8hxx.html) or their scope's (a class, struct or namespace:namespace_math_opt.html). Template instances and alias-named ones have no page: link the template, and for a non-public nested template the class around it. The versioned URLs redirect (301) to occt3d.com, which answers 404 for missing pages: sample links withcurl -L. GCinsidenamespace OCC.Coreis OCCT's packageOCC.Core.GC, notSystem.GC: runtime code there writesglobal::System.GC.- Hosting the OCCT view. A child window of the app's own class (
CS_OWNDC, as OpenGL needs) gets the mouse directly; astaticcontrol passes clicks to its parent (HTTRANSPARENT). Avalonia'sNativeControlHostcreates the native control only after the window shows (and again when it moves), so work that needs the view waits for it.Aspect_VKeyMouseandAspect_VKeyFlagsare anonymous enums: C# repeats their values. - SWIG's overload check isn't C#'s. SWIG ranks overloads by their
typechecktypemaps; a type without one ranks as a pointer, and two pointer-ranked parameters differ only by C++ type (a handle isn't its class).char16_tagainstconst char16_t*was ambiguous to SWIG (Warning 516, one dropped silently) though C# tells a char from a string. Comparing by C# signatures instead is too coarse the other way: it tookStart()for a shadow of staticStart(handle).
Value types (structs)
- Zero-initialized structs are invalid.
default(gp_Dir),new gp_Trsf[n]and unassigned fields are all-zero: a zero direction, a zero scale. They also differ from OCCT's defaults:default(Quantity_Color)is black, whilenew Quantity_Color()is OCCT's default color, yellow. A parameterless constructor can't prevent that, so always construct explicitly. - Mutating a copy.
SetX/Transformon a copy (a property result, aList<T>indexer, aforeachvariable) changes only the copy. Prefer the-edvariants (Transformed,Translated) that return a new value. - Layout drift. The C# layout must match C++ on every compiler and OCCT version. Private members rule out
offsetof, sostatic_assertsize and alignment in the shim and run a layout-probe test per RID. - Equality. The default
ValueType.Equalsuses reflection and boxes, so generateIEquatable<T>. OCCT's tolerantIsEqual(other, tol)is notEquals. - Ported math is LGPL. Rewriting OCCT's inline implementations in C# makes that code an OCCT derivative. Keep managed code to trivial math written from scratch, and let the rest call native.
Calls and data
- Chatty calls. Every interop call costs, so walking triangulation nodes one by one is slow. Add bulk helpers that copy a whole mesh into pinned arrays in one call; with struct types it's a single memcpy.
- Out-params. C#
outcan't carry an input value, and some OCCT refs are in/out. Map C++&toref, neverout. - Type widths differ per platform:
- C
longis 32-bit on Windows and 64-bit on Linux/macOS;wchar_tis 16 vs 32 bits. size_tis 32-bit on x86. SWIG maps it to a 32-bituintby default, which is wrong on 64-bit.- One managed assembly serves every RID, so the interop layer may use only fixed-width or pointer-sized types.
- C
- Strings. Default P/Invoke
char*marshaling is ANSI on Windows, while OCCT reads paths as UTF-8.C:\Daten\Übersicht\Teil.stepbreaks unless strings are marshaled as UTF-8 explicitly.TCollection_ExtendedStringis UTF-16 (char16_t): pass it asLPWStr, which is UTF-16 on every .NET platform, unlikewchar_t. - SWIG typemap fallback. For a typemap method missing on
const T&, SWIG falls back to theT&typemap. Anargoutwritten forHandle(T)&out-params got applied to everyconst Handle(T)¶meter and broke unrelated modules. Give every in/out typemap an emptyconst T&twin. - C# signature collisions. Overloads that map to one C# signature don't compile:
const char*andconst TCollection_AsciiString&both becomestring. Wrap one of them. - Module headers. OCCT headers often only forward-declare the classes in their signatures, but the handle casts in the wrapper need complete types. The aggregate
_module.hxxmust include the header of every class used in wrapped signatures, not just the package's own headers.
Errors
- Exceptions. Catch
Standard_Failure,std::exceptionand...in the native shim. A C++ exception that escapes through P/Invoke becomes an opaqueSEHExceptionon Windows and aborts the process on Linux/macOS. - Release OCCT skips its own checks. OCCT's CMake defaults to
BUILD_RELEASE_DISABLE_EXCEPTIONS=ON, which definesNo_Exceptionand compiles everyStandard_*_Raise_ifprecondition out of the TK DLLs. Bad input then crashes instead of throwing. Inline header code compiled into the shim still checks, so the same call can behave differently depending on where it's compiled. Build OCCT with the optionOFF(a per-port block in the vcpkg triplet does it). - Null shapes. OCCT's inline accessors don't check
myTShape:ShapeType()on a nullTopoDS_Shapeis an access violation (0xC0000005) that ends the .NET process, as it crashes C++ (Handle::operator->doesn't check). No guard: where C++ crashes, NetOcc may too. Exploring a null shape,Location()andNbChildren()are safe. The docs warn. - Signals. Don't call
OSD::SetSignal()in a .NET process. It installs signal/SEH handlers that fight the runtime's own; .NET uses SIGSEGV forNullReferenceExceptionon Linux/macOS. A native access violation stays fatal, as in C++, so run crash-prone batch work in a worker process.
OCAF
The tree dies under live attributes.
~TDF_Datafrees the label nodes, but it never detaches attributes that something else still references. A surviving attribute keeps a dangling label and its whole sibling chain (myNext). Releasing it later reads freed memory:TNaming_NamedShape's destructor callsClear()→Label().Root(). C++ gets away with it through scope order, but .NET finalizers run in any order, long afterClose: aTNaming_Builderfinalizer crashed the test host with an AccessViolation.- Fix: label, attribute and
TNaming_Builderproxies each hold one native reference on theirTDF_Data, and drop it after releasing their own object. The tree then outlives every proxy that points into it. - Hidden holders: a class that keeps attribute handles in private members needs the same treatment. The generator can't see private members, so these go on a config list.
- Fix: label, attribute and
Close through the application.
TDocStd_Ownerkeeps a raw pointer to its document. OnlyTDocStd_Application::Closeclears it; the document's destructor doesn't. On a kept-alive tree whose document died unclosed,TDocStd_Document::Get(label)returns a dangling document.Undo limit 0 means no transactions. With the default limit 0,
OpenCommandopens no TDF transaction andAbortCommandrolls nothing back. CallSetUndoLimit(n > 0)first.Documents live until closed.
NewDocumentandOpenregister the document with the application, so dropping the proxy doesn't free it. ForXCAFApp_Application, a process-wide singleton, that means the whole process lifetime.Status codes, not exceptions.
Open/SaveAsreport failures asPCDM_ReaderStatus/PCDM_StoreStatus:- a missing file gives
PCDM_RS_UnknownDocument; - an unwritable path gives
PCDM_SS_Failure, with the reason inSaveAs's status-message overload.
A format exists only after
BinDrivers::DefineFormat(app)/XmlDrivers::DefineFormat(app).- a missing file gives
GUIDs. OCCT 8's
Standard_UUID(uint32, 2× uint16, 8 bytes) hasSystem.Guid's memory layout, soStandard_GUIDmaps toGuidwithout byte shuffling. Older OCCT declaredunsigned long Data1, which is 64-bit on Linux, so check the layout per OCCT version.
32-bit (x86)
- AnyCPU often means 32-bit. .NET Framework AnyCPU exes with Prefer32Bit, or with an inferred x86 RID, run as 32-bit processes. With x64-only natives they get a
BadImageFormatException. Ship win-x86 and pick the native flavor at runtime (IntPtr.Size). - Struct returns on x86. A
__stdcallexport that returns a struct by value is decorated_Name@N, whereNcounts the hidden return-buffer pointer. .NET computesNfrom the managed signature, which lacks that pointer, so the call fails withEntryPointNotFoundException, on x86 only. Fix: never return structs by value across the C ABI. Copy the result into a thread-local buffer, return its address, and read it in C# after the pending-exception check. - Calling convention. x86 is the only target with two conventions (cdecl/stdcall). A mismatch between an export and its
DllImportcorrupts the stack on x86 only, while every x64/arm64 test passes. Use SWIG'sSWIGSTDCALLfor our own thunks too, and test x86 in CI. - Address space. A 32-bit process gets 2–4 GB. Large STEP assemblies or fine meshes run out of memory, so x86 is for compatibility and x64 for heavy work.
Build and deployment
- vcpkg needs system autotools off Windows. On Linux and macOS, OCCT pulls in fontconfig, whose
gperfdependency builds with autotools. It needsautoconf-archivebesidesautoconf/automake/libtoolfrom apt or brew. In Docker, a mirror caught mid-sync failsapt-get updatewith "Hash Sum mismatch": fetch only the components you need, and retry. - Old frameworks and current tooling. Traps when adding them:
.NET 6: VSTest 18 and NUnit3TestAdapter 6 ship only net462 + net8.0 builds. net6.0 tests need Microsoft.NET.Test.Sdk 17.13.0 and NUnit3TestAdapter 5.2.0, the last releases with a netcoreapp3.1 build, pinned per target framework.
.NET Framework 4.6.2: it has no
System.ValueTuple, and the package is notorious for binding-redirect trouble on 4.6.x. It also has noRuntimeInformation, which arrived in 4.7.1. Keep tuple syntax out of library code, and branch the loader with#if NETFRAMEWORK.dotnet test --archsets a RID, which before net8.0 implies self-contained (warning NETSDK1201). SetSelfContained=falseso the tests run on the installed runtimes..NET Framework 3.5 is CLR 2, and the modern C# features we use all work there: module initializers,
inparameters, theunmanagedconstraint. The gaps are APIs:Environment.Is64BitProcess→IntPtr.Size;Path.Combinewith more than two parts → fold two-part calls;AppContext.BaseDirectory→AppDomain.CurrentDomain.BaseDirectory;Encoding.GetString(byte*, int)→new string(sbyte*, int, int, Encoding).
NUnit 4 and VSTest can't host CLR 2, so a plain net35 console program tests it. The machine needs the .NET Framework 3.5 Windows feature.
.NET 6 on macOS: install the runtime with
dotnet-install.sh --runtime dotnet --skip-non-versioned-filesinto the SDK's dotnet root. Homebrew'sdotnet@6is a separate keg: the net6.0 test host doesn't find it, and it takes over/opt/homebrew/bin/dotnet. Microsoft's .NET 6 SDK pkg replaces thedotnethost with the 6.0 one; newer SDKs still run through their own hostfxr.Testing another library build:
SetTargetFrameworkon a ProjectReference skips the SDK's framework negotiation. That negotiation is also what removes the RID from library references, so withoutGlobalPropertiesToRemove="RuntimeIdentifier;SelfContained"the test gets a RID-specific build instead of the AnyCPU one.
- PyPI's SWIG wheel doesn't install on MSYS2 Python. In Git Bash,
C:\msys64\ucrt64\bin\pythoncan shadow CPython (platformmingw_x86_64_ucrt): nowin_amd64wheels, and venvs get abin/layout.build.py toolstakes SWIG from conda-forge through micromamba instead, which works under any interpreter. - Size and build time. Wrap per package; a full wrapper is large (NetOcc.dll is about 12.5 MB per framework) and its shims take long to compile and link. Building OCCT itself takes a long time too, so use vcpkg binary caching.
- Exact OCCT match. Shims are ABI-bound to the OCCT build they were compiled against. Ship the TK libs with the wrapper and move versions in lockstep, as
find_package(OpenCASCADE <version> EXACT)does. On Windows, an older OCCT onPATH(e.g. installed by another CAD tool) can win the DLL search and crash the app. - .NET Framework and native NuGet assets. net4x projects don't reliably pick up
runtimes/<rid>/native. Ship abuildTransitive/net4x/*.targetsthat copies the natives (intox86/+x64/for AnyCPU). - No dots in native library names. In
DllImport("NetOcc.Native"),LoadLibrarytreats.Nativeas the extension and won't append.dll. .NET Core's probing also tries<name>.dll, but relying on each runtime's name variations is fragile. HenceNetOccFoundationClasses& co. - A Windows DLL exports at most 65,535 functions. One wrapper library for five OCCT modules reached 71,092 exports and failed with LNK1189. Build one library per OCCT module (
-dllimportper SWIG module); the linker names the count, so watch it as wrapping grows. - Native dependency lookup. The OS loader, not .NET, resolves the shim's TK dependencies:
- Put them next to the shim.
- Load from subfolders with
LoadLibraryEx(..., LOAD_WITH_ALTERED_SEARCH_PATH). - Set RPATH
$ORIGIN(Linux) /@loader_path(macOS). - Test from a clean consumer project on every RID.
Packaging and CI
- Symlink chains in native packages. A Linux/macOS install has
libX.so -> libX.so.8.0 -> libX.so.8.0.1. NuGet (a zip) and MSBuild copies store every name as a full file, which triples the size. Keep one file per library, named as the loader asks for it: the SONAME (readelf -d) or the install name (otool -D). - Runtime packages need
lib/placeholders. A package with onlyruntimes/andbuild/net35/is treated as .NET Framework-only (NU1701 on netstandard2.0, failing restores on net6.0+). Add emptylib/net35/_._andlib/netstandard2.0/_._; don't add only thelib/net35/_._that NU5127 suggests. - Backslashes in
PackagePath. Packed on Linux or macOS, a trailing backslash (runtimes\<rid>\native\) gives zip entries likeruntimes/<rid>/native//libX.so. NuGet normalizes them on extraction, so restores still work. Forward slashes pack cleanly on every OS,/for the package root too. dotnetcommand-line properties.-p:Name=a;band-p:Name=a,bboth split into separate properties (MSB1006). An escaped%3Bsurvived, but broke restore's sources. Pass lists through an environment variable, which MSBuild reads as a property.- Restore sources. Right after two
dotnet packruns,dotnet restore --source <folder> --source https://api.nuget.org/...took nuget.org for a path relative to the project (NU1301). Run by hand it worked. A generatednuget.configpassed with--configfilealways works. - CLR 2 tests. VSTest can't host CLR 2 and NUnit 4 needs 4.6.2. A net35 test target works as an exe on NUnit 3.14 that runs itself through NUnitLite (
new AutoRun().Execute(args)). Test code then compiles for net35:- no tuples;
- no
Marshal.SizeOf<T>; - two-part
Path.Combineonly; - no
Environment.Is64BitProcess; - a static extension (C# 14) for
Assert.EnterMultipleScope.
- vcpkg ABI and triplets. Removing comments from a triplet didn't change the package ABI: a fresh install restored OCCT from the binary cache. A new vcpkg tool checkout or compiler, as in a rebuilt Docker image, does change it.
- vcpkg binary cache in actions/cache. vcpkg's package ABIs also hash the runner image's tools. The windows-2025 image update from 20260907.229 to 20260922.246 changed every ABI, even with the same MSVC 14.51.36231.
- A key over the repo's vcpkg files, saved only on a miss, then restores useless archives, rebuilds OCCT (about 1 h) and saves nothing, in every run.
- What works: save under a new key whenever the archive set changed, and restore the newest by prefix.
- Prune the archives no install uses (
Abi:in<install root>/vcpkg/status); the cache then stays at one set (Windows about 200 MB).
- Keeping Actions caches alive. GitHub evicts caches unused for 7 days. Scheduled runs can start late or be dropped at load peaks, so a weekly cron races the eviction: run twice a week, off the full hour. Public repos disable schedules after 60 days without activity.-
dotnet teston a solution builds only the test projects and what they reference (SDK 10): the demos job'sdotnet test NetOcc.Demos.slnxnever compiled the WPF and Avalonia viewers.dotnet buildthe solution, thendotnet test --no-build. - A multi-target pack runs in the outer build, which has no default items:
<None Update="README.md" Pack="true" />finds nothing there and pack fails with NU5039.Includeit (the inner builds then list it twice, harmlessly). - nuget.org READMEs. nuget.org renders no mermaid and resolves no relative links: a repository README makes a poor package README. Keep a separate one for users, with absolute links, and test that its example is a tested one.
- License expressions with OCCT's terms.
MIT AND LGPL-2.1-only WITH OCCT-exception-1.0packs without NU5124: NuGet's SPDX list has the Open CASCADE exception. - nuget.org Trusted Publishing. The policy takes the workflow file name only (
build.yml), not its path. The temporary API key lives one hour, so log in right before the push. Only the publish job needsid-token: write. git addfails on a path that matches nothing. create-pull-request'sadd-pathslistednetocc-core/changes, which git doesn't have while no entries wait (it keeps no empty folders), so the first release PR, made from[Unreleased]without entries, failed there. List the folder only whengit ls-filesfinds something in it.
Generating from headers
Scaling to all packages. What broke going from 30 to 344 packages:
- libclang stops at a fatal error. A missing include (
GeomBndLib_InfiniteHelpers.pxx, not installed) ends the parse silently, and the headers after it look fine. Retry without the header that led there. - Header bugs hide in function bodies.
MathLin_Jacobi.hxxsets a memberEigenResultdoesn't have. WithSkipFunctionBodies, clang doesn't see it; MSVC does, in every wrapper that includes it. - Destructors mangle differently. For MSVC,
clang_Cursor_getManglingnames a destructor??_D(vbase destructor), while DLLs export??1.clang_Cursor_getCXXManglingslists all variants. %nodefaultdtorplus a constructor crashes. The owning proxy'sDisposethrowsMethodAccessExceptionon the finalizer thread, which kills the process.- C++ ambiguity counts private overloads.
BOPAlgo_ParallelAlgohas a publicPerform()and a privatePerform(const Message_ProgressRange& = {}); overload resolution comes before the access check. - Out-of-line nested classes (
class BRepGraph::ShapesView { ... };at file scope) look like top-level classes unless the semantic parent is checked. Visit them with their class instead. - SWIG constructors with
ref+pre/posttypemaps call aSwigConstructhelper withoutref, unless the typemap hascshin="ref $csinput". - Namespace-function defaults (
= THE_ZERO_TOL) only resolve inside the namespace; put the shim struct there. - Collection aliases live outside the packages. OCCT 8 moved them to
src/Deprecated/NCollectionAliases/(972 headers), so a package's own parse never seesTColgp_Array1OfPnt. Parse each package's alias headers with it, typedefs only.TColgp,TColGeomandTColGeom2daren't packages anymore. - Typemaps are positional. A module's own collection macros after its classes left
SWIGTYPE_p_*types in that module only, since the others import it first, and itsconst&returns got no owned-copy typemap. Emit them before the classes;build.py generatenow fails on anySWIGTYPE_p_*. - Vtables of classes the DLLs don't export. An inline constructor of a polymorphic class makes MSVC emit the vtable in the shim, and the vtable names every virtual function. One that isn't exported fails to link (LNK2001,
ShapeAnalysis_BoxBndTreeSelector::Reject). An exported constructor sets OCCT's own vtable. - Unchecked NCollection accessors.
NCollection_Array2::Valuechecks only the flat position: a column past the end reads the next row.Array1::First/Lastread index 0 of an empty array. Check both in%extend. - Import cycles reach the module itself. Four Step modules imported themselves through a package cycle. Keep the module out of its own import closure.
- Template arguments are canonical. libclang spells every argument of a specialization, defaults included:
NCollection_Map<TDF_Label, NCollection_DefaultHasher<TDF_Label>>. Spell the%templatethe same way, and skip platform-spelled builtins (size_tisunsigned long longon Windows,unsigned longon Linux). - Commas in SWIG macros. A macro argument ends at a comma, template brackets or not. Pass
NCollection_DataMap< K, V, H >on to another macro as%arg(...), and don't instantiate collections whose arguments hold commas. - Streams without callbacks. A
std::streambufthat calls into C# per write breaks on managed exceptions (fatal across native frames off Windows) and onseekp/tellg, which binary writers and readers use. Buffer in native memory, seekable, and copy the bytes before or after the call. - References into an object need its owner. A proxy of a
T&return points into the owner; without a managed reference to the owner's proxy, the GC finalizes the owner and the borrowed proxy writes into freed memory. Give every proxy an owner field and set it in thecsoutofSWIGTYPE&, members only. A number or structT&is a C#refreturn (ref *(T*)ptr), which also runs on CLR 2. Never for value-type thunks:thisis a C# struct the GC may move. - Implicit constructors aren't declarations. With a global
%nodefaultctor, a class that declares no constructor (BRep_Builder) had none in C#. Emit the implicitT(), checked like an inline one. - Generated on Windows, built everywhere. Headers that compile only on Windows reach the output:
OSD_WNT.hxxincludeswindows.h, and the Linux build stops there. Leave out OCCT's WNT headers; portable ones guardwindows.hwith_WIN32, so an include-based rule would dropStandard_Mutex.hxx. - Wrapping more exposes header bugs. Once
HLRTopoBRep_DSFiller::Insertmapped (a map parameter), its module header includedContap_Contour.hxx, which usesAdaptor2d_Curve2dwithout including it. Apreludeon the declaring package fixes every user. - Flat names can be taken. Nested and namespace types get flat names (
Geom_Curve::ResD1asGeom_Curve_ResD1), butBVH::RadixSortersits next to the class templateBVH_RadixSorterat file scope. Check every flat name against the file-scope names of the translation unit. - A C# import loses to a sibling namespace. The class
BRepGraph(package BRepGraph) is used fromOCC.Core.BRepGraphInc. Withusing OCC.Core.BRepGraph;above a file-scoped namespace,BRepGraphresolves to the namespaceOCC.Core.BRepGraphfirst (CS0118). Usings inside the namespace are searched before the enclosing namespaces, which is where SWIG puts them. - libclang sign-extends enumerators.
InitValofBits_Reserved = 0xF000inenum BitLayout : uint16_tis -4096; readUnsignedInitValfor unsigned underlying types, and give the C# enum that base (%typemap(csbase)). - One layout for every target. libclang computes layouts for win-x64, where
size_tisunsigned long long. A plain-data struct holding one (MathUtils_PolyResult) failed its layout guard on x86. The canonical type can't tell; walk the typedef sugar forsize_t,ptrdiff_t& co., and refuselong,wchar_tandlong double. - Parsers in public headers. StepFile installs its bison parser and flex scanner (
step.tab.hxx,namespace step), whose members don't all link. Exclude such namespaces in config. - Uninstantiated templates have no members. A class template instance exists in the AST with its members only once something instantiates it, and libclang can't.
using BRepGraph_FaceIterator = BRepGraph_Iterator<...>;alone gives an empty declaration. A second parse withstatic_assert(sizeof(::Alias) != 0)after the includes instantiates the class, not its member definitions. - Generated files outlive their types. A struct or nested header the generator no longer writes stays in the output and still compiles into the build. Delete the generator's own outputs it didn't write in a run.
- Out typemaps reach constructors. SWIG's
new_Twrapper returnsT*through the out typemap ofT*. One that adds a reference for returned transients, as a handle return does, counted twice with thereffeature: new objects started at 2 and leaked. Put such typemaps on a spelling constructors never use (T* const). - Pointers to width typedefs. libclang gives canonical types, so
intptr_t*islong long*on win-x64 butint*on x86, anduint64_t*isunsigned long*on Linux. By value that converts implicitly; a pointer doesn't, and the x86 build failed. Spell pointers to typedefs as written, and give SWIG no typedefs for them: it writes the resolved type into the wrappers (NetOcc_Address<const unsigned long long*>), which broke Linux. Typemaps can name the typedefs instead. - Declared here, defined there. A translation unit may only forward-declare a class another package defines (
GeomEval_RepCurveDesc::Basein Geom). It is still that package's proxy. Only a class no header defines (TNaming_Node,Message_ProgressScope::NullString) is opaque: a pointer to it is an address. - Pinned arrays dangle when kept. A C# array is pinned for the call only, and constructors and setters keep pointers (VrmlData nodes,
BRepGProp_UFunction's coefficients). Such pointers are addresses the caller allocates natively. - MSVC:
reinterpret_castfromvoid**toconst int**counts as casting away constness ("conversion loses qualifiers"). Go throughvoid*withstatic_cast. $1_ltypedrops qualifiers. For aconst char*¶meter SWIG declareschar** arg; a typemap that assigns aconst char**needs a cast.- SWIG preprocesses
%extendbodies. Its preprocessor doesn't define_WIN32, so#if defined(_WIN32)in an%extendbody always took the X11 branch (Windows then failed onXw_Window). Platform dispatch goes into a%{ %}helper the body calls. - An empty value class needs its copies too.
%occt_valueclasswas only emitted for classes with constructors or instance members;Graphic3d_ValidatedCubeMapOrderhas neither, so a static member'sconst&return of it took the borrowingcsout, whoseret.netoccOwner = thisdoesn't compile in a static method. SWIGTYPE_T, not justSWIGTYPE_p_T. A by-value type without typemaps becomesSWIGTYPE_T; the build's guard looked forSWIGTYPE_p_only and let one through (a kept-argument typemap overriding an address).- A class-scope typedef's spelling needs its class. Naming an instance through a typedef in another class (
OpenGl_Context::OpenGl_ResourcesMap) makes the alias need that class's header; the includes of the template and arguments don't bring it. A package parsed whole while its config lists a few classes leaked such names: filter at the parse. - Typedefs that differ per platform. Resolving
Aspect_Drawable(void*on Windows,unsigned longon X11) on the Windows parse writes a type no other platform converts: keep the name, map it pointer-sized. - SWIG's overloads count the object. An instance member and a static overload taking the object first and the same arguments collide in SWIG's wrappers (Warning 516), and SWIG keeps the static one: skip the instance member, logged.
- Bit fields have no portable layout.
MeshVS_TwoColorspacks six bit fields into 8 bytes on MSVC; libclang's field offsets said 9. A record with bit fields isn't plain data. - A delegating constructor imports the vtable. MSVC emits a polymorphic class's vtable in the shim for an ordinary inline constructor, but for an inline delegating one it references the vtable the DLL should export (the class's virtual functions are exported). OCCT doesn't export
Select3D_SensitiveCircle's vtable: LNK2019. The export tables tell; a first rule that treated every inline constructor this way wrongly dropped 443. - A constructor's
pre/postrun in a static helper. SWIG converts a constructor's arguments inSwigConstruct<Class>, before the proxy exists: nothis. Hand values to the constructor body (csconstruct) through a thread-static, asNativeKeepdoes. %apply/%clearwork inside a class body. Around one declaration they scope a type-and-name typemap to it alone (NETOCC_KEEP).- CLR 2 writes
bool[]back as four-byte BOOLs.[In, Out]withMarshalAs(LPArray, ArraySubType = U1)round-trips on .NET Framework 4 and .NET, but not on CLR 2: copy through a pinnedbyte[]. - SWIG's argument checks return early. A null class reference (or a failed
incheck) returns from the wrapper before the later parameters'intypemaps run. C#postcode must not trust what those would have written: a sentinel a laterinparks in arefslot never gets there. Compare the slot with what C# passed instead. - A C#
chargoes through a code page. SWIG maps C++charto C#char, which P/Invoke converts with the ANSI code page: a UTF-8 lead byte comes back asÃ.%apply unsigned char { char }passes the byte. - Typemap code runs outside
%exception's try. Anintypemap that must fail (a Clongout of range) sets the pending exception and returns$null; a C++ throw there would leave theextern "C"wrapper. - C++17 elides a copy into
new.new T(f())constructs a non-copyable, non-movableTthatfreturns by value in place; an%extendaccessor so gives C# a proxy SWIG's by-value return (which copies) can't. - A typemap's locals belong to one type. In
%typemap(in) const A&, A& (A temp)the local isA&'s only, and theconst A&wrapper doesn't declaretemp: give each type its list,const A& ($*1_ltype temp), A& ($*1_ltype temp). - C# member variables use
csvarout, notcsout. Astd::pairwithfirst/secondas members would miss the value-type, string and bitset return typemaps;%extendaccessors returning by value reuse them. %arg()normalizes spaces. A type passed through%arg(NCollection_Map< K, H >::Iterator)comes out asNCollection_Map< K,H >::Iteratorin the wrapper: harmless, but a byte-for-byte diff of SWIG's C++ shows it. Macros work inside a template's%extend, which is expanded before parsing.%csattributes Classmarks the constructor. The class name also names its constructor, and the feature lands there; a class attribute takes%typemap(csattributes) Class. A member's feature matches by parameters andconst, and reaches the default-argument overloads only when it spells the defaults: write it as the declaration.- Template names aren't the text before
<.NCollection_DynamicArray<T>::DynamicIterator<true>is an instance of the nested template, not ofNCollection_DynamicArray: cut the last argument list only. Cutting at the first<gave the iterator the array's name, and the wrappers didn't compile. - Member bodies of an instance aren't compiled until used.
IntPolyh_Array<T>::DumpcallsT::Dump(), which someTlack; libclang never instantiates it, the wrapper does. An explicit instantiation (template class X<T>;) in a check parse instantiates every member: lay each error at the member whose body holds it or its instantiation note. The note at the explicit instantiation's line tells which instance. - Their vtables too. A polymorphic instance the DLLs don't export gets its vtable in the shim once a constructor is wrapped, and an instantiated virtual member may call an unexported function (
ShapePersistent_Poly::pPolygon2D::Import, LNK2019). Uninstantiated, the virtual's body names no callee; check the vtable in the check parse. const char*string overloads read Latin-1.TCollection_ExtendedString(const char*, bool isMultiByte = false)copies each byte to a character, so a UTF-8 C# string through aconst char*overload of an ExtendedString API comes out as mojibake. Where overloads collide in C#, wrap the UTF-16 one (TCollection_ExtendedString,const char16_t*).- Const twins aren't interchangeable for classes. The const one returns an owned copy, the non-const one a borrowed proxy; a copy of a view (
BRepGraph::ShapesView) holds a raw pointer, a borrowed iterator item dangles once the list goes. Prefer onlyrefreturns of values. - Copies that point at their owner. A value class whose fields point at other objects (
BRepGraph_TopoViewholds its graph), returned by value orconst&, is a copy the proxy owns, and it dangles once the owner is collected: the tests neededGC.KeepAlive(graph). Mark such returns so the copy keeps the member's object (NETOCC_VIEW), the moved result of a move-only class too. SWIG looks up a return typemap by type and function name, so%apply ... { T Member }around the one declaration scopes it. - A default a macro spells has no tokens where it is.
DEFINE_STANDARD_EXCEPTIONwritesC1(const char* theMessage = ""); tokenizing the default's extent gave nothing (the macro in another header) or garbage from the macro body to the expansion (the same header). Spelling and file locations differ there: evaluate the constant instead (clang_Cursor_Evaluate), and only for parameters a literal fits, since shims compile the default. - Default arguments of instance members stay uninstantiated.
DefaultArgis null,UninstantiatedDefaultArgisn't: without it,IntPolyh_Array(int = 256)andIntPolyh_Array(int, int = 256)looked unambiguous, and the call wasn't. - SWIG writes
enum Xin%extendcasts. A template instance over a nested enum (a flat alias) gets(T< enum X > const *)selffor%extendmembers, and C++ declares a new local enum there; template arguments ofNetOcc_Address< enum X * >too. Keep%extendout of templates that may hold enums, and spell pointers to nested enums by C++'s name. SwigValueWrappermoves only for C++11. It picks its move operators by__cplusplus, which MSVC reports as 199711L without/Zc:__cplusplus: a move-only result (a guard) didn't compile. OCCT's headers use_MSVC_LANG, so the flag changes only SWIG's code.- Instances of protected templates. C++ can't name
ShapePersistent_Geom::geometryBase<Geom_Curve>outside its class, but OCCT's public typedefShapePersistent_Geom::Curvecan; alias through it. Its flat name may be a real class's (ShapePersistent_Geom_Curve): check file-scope names. - Names past the path limit. A flattened instance name reached 250 characters (
Extrema_GGExtPC<...>), and SWIG couldn't write its.csfile. Prefer OCCT's alias (Extrema_ExtPC), and hash the arguments past a length.
- libclang stops at a fatal error. A missing include (
Declared, never defined. OCCT declares a few members
Standard_EXPORTand never defines them:XCAFDoc_GeomTolerance(const Handle(XCAFDoc_GeomTolerance)&)andBRepGProp_VinertGK::GetAbsolutError. The headers can't tell; the shim compiles and then fails to link. Check every out-of-line member against the TK DLLs' export tables by its mangled name. libclang's MSVC mangling matches the DLLs.Headers that don't compile alone.
XCAFDoc_AssemblyIterator.hxxholds aTDF_Labelby value without including it. In_module.hxx, include the dependency headers before the package's own; parse such a package with the missing header first.usingre-exposes protected bases.BRepAlgoAPI_AlgoinheritsBOPAlgo_Optionsprotected and republishesHasErrors& co. withusing. A parser that reads only a class's own method declarations loses them; follow the using-shadow declarations (ClangSharp:UsingShadowDecl.UnderlyingDecl).Placement-only allocation. NCollection-allocated nodes (
Poly_CoherentTriPtr) declareoperator new(size_t, const Handle(NCollection_BaseAllocator)&)and a matchingoperator delete. Those hide the global forms, so the shim'snew Tanddelete pdon't compile. Detect classes whose own operators lack the usual forms; give them no constructors,%nodefaultdtor, and no by-value copies.Hand-written parts next to generated ones. SWIG 4.5 applies
%extend(including%proxycode) and%csmethodmodifierswritten before the class declaration. One companion include placed before the generated classes is enough.%templatemay come before a class of the same module that it holds, since SWIG resolves class names after parsing; typemaps can't, since they apply only to later declarations.ClangSharp traps:
IsImplicitis libclang's cursor flag, not "declared by the compiler"; useIsDefaulted && !IsExplicitlyDefaulted.CXXRecordDecl.Methodsleaves out the constructors; readCtors.SpecializedTemplatethrows on partial specializations (std internals); take the template name fromQualifiedName.- The MSVC STL's
std::array<T, N>is a struct around_Elems; flattened member by member it givesRoots__Elems_0. Treat it as theT[N]it holds. - The NuGet libclang (
libclang.runtime.linux-x64) comes without clang's own headers, and on Linux the C library has nostddef.h: every<cstddef>fails there. On Windows the SDK has one. A fatal error in a system header is the toolchain's, not the header's that reached it: blamed on OCCT's headers one by one, it left out nearly every header, and the docs run documented 281 of 6,946 types in 22 minutes (on Windows 6,220, in about a minute).
SWIG imports
- Warning 401 from import order. SWIG drops a base class it meets after the derived class. With nested
%imports (each imported module importing its own dependencies) and package uses that form cycles, classes of imported modules keep meeting their bases late: 9622 warnings. Harmless for imported classes (no code), but a module class deriving from such a class loses the chain: missingnewmodifiers. Fix: one flat list per module (#ifndef SWIGIMPORTEDaround the imports), bases' packages first. Left over: imported packages deriving from the module's own classes, which come after the imports. - Checking SWIG output changes. SWIG rewrites every file; compare with a copy before regenerating, and give identical
.cxxfiles their old timestamps back, or ninja rebuilds all natives for nothing.
OCCT 8 API changes
Standard_Failurederives fromstd::exception, no longer fromStandard_Transient. The type name comes fromExceptionType(); there is noDynamicType()on it anymore.- Collection aliases are deprecated.
TopTools_ListOfShape,TDF_LabelSequence& co. live inDeprecated/NCollectionAliases, and signatures useNCollection_List<TopoDS_Shape>/NCollection_Sequence<TDF_Label>directly. Wrap the template; keeping the old name for the C# class is fine. Only one instantiation has two aliases, so the alias's prefix is a stable home for the C# class. - Signatures use
occ::handle<T>,double,boolinstead ofHandle(T),Standard_Real,Standard_Boolean.occ::handleis an alias ofopencascade::handle, so either spelling works in.ifiles. - Namespace functions.
TopoDS::Face()& co. are namespace functions now; wrap them through a static shim class.
Documentation (DocFX)
- Examples as test regions.
[!code-csharp[](../samples/X.cs#name)]takes a#region nameand dedents it; a file outside the docfx content globs is fine. Tests keep the examples honest, and asserts after the region stay out of the page. - Nested toc, flat files. Sections in
articles/toc.yml(items:) without moving files keep every URL and relative link. - Mermaid. The modern template renders
```mermaidblocks.mmdc(@mermaid-js/mermaid-cli) validates them; withPUPPETEER_SKIP_DOWNLOAD=1and a puppeteer config whoseexecutablePathnames a local Chromium (EdgeCore'smsedge.exeworks) nothing is downloaded. Rendered PNGs show what parsing doesn't: dagre routes a tree's edges through sibling boxes when the children's order is unlucky; reorder them. Class diagrams draw empty compartments: trees read better asflowchart LRwith---edges.
Code style
- clang-format's C# mode can't take modern C#. With OCCT's
.clang-formatit split raw string literals ("""), changing the program, and mangled switch expressions and property patterns. The C# style comes from.editorconfiginstead, applied by ReSharper's formatter (python format.py). - CSharpier re-sorts usings. It merges the commented using groups into one sorted list, own namespaces before third-party ones, with no option to keep them.
- ReSharper's cleanupcode formats only solution files. Given a project file, it formats the project file itself. Given a solution, it skips files outside the solution's folder and projects that don't load: the samples and package tests need the packed NetOcc. A temporary project over every file, in a solution at the root, needs no restore of the real ones. It needs the libraries' targets too: a branch no target defines (
#if NETFRAMEWORKunder net10.0 alone) stays unformatted. - OCCT's wrapping reads badly in C#. One argument per line (
BinPackArguments: false), and names aligned in columns acrossvar,using varand modifiers, put wide gaps and tall lists into ordinary code; packed lines and single spaces read better. ReSharper also adds blank lines by default after a file header comment (splitting a using group from its comment), between using groups, around fields and around regions, which the documentation's snippets show.
Licensing
- OCCT. LGPL-2.1 with the Open CASCADE exception. Object code that incorporates OCCT header material may be distributed "under terms of your choice", given a prominent notice that it uses OCCT. The TK libs themselves stay LGPL: keep them separate and replaceable, ship the license texts, and publish the exact source, including vcpkg's patches.
- Doc comments. Doc text copied from OCCT headers is OCCT's text. Keep it out of MIT sources and ship it only in the XML docs, with attribution.
- Other OCCT bindings. Their generators and interface files are GPL-3.0 or LGPL-3.0. Use them as reference for rules at most; never copy their code or exclusion lists, or the generator becomes GPL.