Import casadi and nlopt lazily so that importing cadquery does not crash at exit on Windows (#1911) - #2092
Conversation
…ash at exit on Windows `import cadquery` pulled in casadi (assembly constraint solver) and nlopt (sketch solver) eagerly. On Windows the two wheels carry different C runtimes - nlopt is MSVC/UCRT, casadi is MinGW-w64 against the legacy msvcrt.dll with bundled libstdc++/libwinpthread - and with both loaded the interpreter corrupts its heap during teardown (CadQuery#1911). Each alone is fine, and neither is needed unless constraints are actually solved. solver.py now imports casadi inside the functions that use it (with `from __future__ import annotations` for the ca.MX/ca.Opti annotations) and sketch_solver.py imports nlopt inside SketchConstraintSolver.solve. A fresh-process test asserts that importing cadquery loads neither. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTte2haDWay6HAwN5afmDC
…oes not crash on Windows Lazy imports keep `import cadquery` clean, but a process that actually solves an assembly (casadi) and a sketch (nlopt) still crashed at exit. The mechanism: both are SWIG modules and share SWIG's runtime type table through the swig_runtime_data5 capsule. Its destructor frees every module's type records with the C runtime of the module that created the capsule, and the two wheels are built against different runtimes (nlopt: MSVC/UCRT; casadi: MinGW-w64 on msvcrt), so the heap is corrupted during teardown (0xC0000005 / 0xC0000374). cadquery.occ_impl.swig_runtime registers an atexit handler that clears the capsule's destructor when both modules are loaded; the handful of records it would have freed are leaked at exit instead, which is harmless. tests/test_assembly.py + tests/test_sketch.py now exit 0 on Windows (they exited 0xC0000005 before), and a fresh-process test solves a constraint with both backends loaded and asserts exit 0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTte2haDWay6HAwN5afmDC
|
Independent reproduction on a different setup, plus two findings that may be useful here. Environment: Windows 11, python.org CPython 3.12.10, installed with pip (not uv) into a clean venv: All exit codes below come from a Python 1. casadi 3.8.0 looks like the regression point — 3.7.2 is cleanIdentical wheels and machine, only casadi differs:
This corroborates @gdahlberg55's table in #1564. It bounds the affected range, and gives people a stopgap ( 2. Possible false-green in the commit 2 regression testWhether the process crashes depends on what is still alive at interpreter exit, and the dependence is not monotonic:
So holding a wrapped object from both modules at exit suppresses the crash on its own, with no fix applied. Since 3. Ruled out by testIn case it saves anyone the detour taken in #1911: OCP and VTK are not involved ( |
Fixes the crash-at-exit on Windows reported in #1911 — both the
import cadquerycase and the case where constraints are actually solved. Two independent commits so they can be taken separately.Root cause
Minimal reproduction needs no CadQuery — it is the pair of dependencies:
Reproduced with python.org CPython 3.12.14 and the PyPI wheels
casadi==3.8.0,nlopt==2.11.0, from native PowerShell ($LASTEXITCODE = -1073741819), from cmd.exe (cmd /v:on,!ERRORLEVEL! = -1073741819— a plain%ERRORLEVEL%on the same line expands before the command runs and shows 0, which is why cmd looked clean in the issue), and from Git Bash. Import order does not matter.-X faulthandlershowsWindows fatal exception: access violationwith<no Python frame>: native, during interpreter teardown.Why the pair, precisely:
nlopt/_nlopt.pydandnloptjni.dlllinkMSVCP140.dll,VCRUNTIME140.dll,api-ms-win-crt-*(MSVC / UCRT);casadi/_casadi.pydandlibcasadi.dlllinklibgcc_s_seh-1.dll,libstdc++-6.dll,libwinpthread-1.dll,msvcrt.dll(MinGW-w64, legacy CRT), bundled in the wheel.ctypesdoes not crash; only the two Python extension modules together do. Both are SWIG modules and both embedswig_runtime_data5: they share SWIG's runtime type table through theswig_runtime_data5.type_pointer_capsulecapsule. At exit the capsule's destructor walks the shared table and frees every module's type records with the C runtime of the module that created the capsule — memory the other runtime allocated. Clearing that destructor (PyCapsule_SetDestructor(capsule, NULL)) before exit makes the pair exit 0; that is the whole mechanism.conda builds everything with one toolchain, which is why it does not reproduce there.
Commit 1 — lazy imports (
import cadqueryno longer loads either)occ_impl/solver.py: the module-levelimport casadi as cabecomes an import at the top of the 11 functions/methods that use it;from __future__ import annotationskeeps theca.MX/ca.Optiannotations in the class body and_build_transform's signature from importing at definition time.occ_impl/sketch_solver.py:import nloptmoves intoSketchConstraintSolver.solve, its only user.tests/test_lazy_imports.py::test_import_cadquery_does_not_load_solver_backends: asserts in a fresh process thatimport cadqueryloads neither.Commit 2 — exit-time guard (solving constraints no longer crashes)
occ_impl/swig_runtime.py: anatexithandler that, when bothcasadiandnloptare loaded, clears the SWIG runtime capsule's destructor. The handful of type records it would have freed are leaked at exit instead, which is harmless. Registered fromcadquery/__init__.py.tests/test_lazy_imports.py::test_process_exits_cleanly_with_both_solver_backends_loaded: solves an assembly constraint withnloptalso loaded in a fresh process and asserts exit 0.Tests (Windows 11, Python 3.12.14, wheels as above)
pytest tests/test_assembly.py tests/test_sketch.py: 196 passed, and the pytest process itself now exits 0 — before commit 2 it exited0xC0000005after printing the summary (the same crash, since the suite loads both backends).python -c "import cadquery"exits 0 (was0xC0000005).Nothing here changes behaviour on Linux/macOS beyond deferring two imports.
🤖 Generated with Claude Code
https://claude.ai/code/session_01KTte2haDWay6HAwN5afmDC