From dd767071033932a92026710ed3131228a35eee5d Mon Sep 17 00:00:00 2001 From: DLuminary <86183013+DLuminary@users.noreply.github.com> Date: Mon, 24 Aug 2026 09:03:33 +0300 Subject: [PATCH 1/2] Terminate TBB deterministically before MATLAB unloads the MEX file MATLAB crashes on exit after a nonlinear optimization when GTSAM is built with TBB, on macOS. See borglab/gtsam#2489. The crash does not depend on TBB_NUM_THREADS, is unaffected by explicitly deleting wrapped objects, and disappears with -DGTSAM_WITH_TBB=OFF -- so it is not object lifetime. This matches oneTBB issue #977: a library links TBB and uses a parallel construct, the host loads it dynamically, and the process crashes at exit. Reported there on Linux and macOS but not Windows, and noted as racy. The oneTBB maintainer's guidance is that dynamic library unload is a difficult moment for oneTBB, because worker threads are still finishing their routine and may touch addresses in the unmapped image; the recommendation is task_scheduler_handle with finalize() before the unload. _deleteAllObjects is registered with mexAtExit and runs just before MATLAB clears the MEX file, which is the right point to wind the scheduler down. finalize() blocks until the workers have exited. The call is placed after the collector delete loop so that any destructor which itself uses TBB has already run. Version guard ------------- task_scheduler_handle, tbb::attach and finalize() are oneTBB APIs; the attach tag in particular is not present in early oneTBB. GTSAM's HandleTBB.cmake accepts TBB from 4.4 onwards, so the block is guarded on TBB_INTERFACE_VERSION >= 12060 (oneTBB 2021.6) and compiles out entirely on older TBB, leaving those builds byte-identical. Builds without TBB are unaffected via GTSAM_USE_TBB. Verified in isolation, not against MATLAB: - finalize() is idempotent; calling it repeatedly is harmless - the std::nothrow overload returns false rather than throwing if the scheduler is still referenced elsewhere, so it cannot disturb another holder -- relevant because MATLAB ships its own TBB - TBB re-initializes transparently if the module is used again after a clear, so this does not affect the clear/recompile/reload workflow that mexLock() would break - the guarded block compiles both with the guard active and with it forced inactive --- gtwrap/matlab_wrapper/templates.py | 20 ++++++++++++++++++++ gtwrap/matlab_wrapper/wrapper.py | 2 ++ 2 files changed, 22 insertions(+) diff --git a/gtwrap/matlab_wrapper/templates.py b/gtwrap/matlab_wrapper/templates.py index bff1650..31de924 100644 --- a/gtwrap/matlab_wrapper/templates.py +++ b/gtwrap/matlab_wrapper/templates.py @@ -26,6 +26,16 @@ class WrapperTemplate: '''), prefix=' ') + tbb_headers = textwrap.dedent(""" + #ifdef GTSAM_USE_TBB + #include + #if TBB_INTERFACE_VERSION >= 12060 + #include + #define GTSAM_WRAP_TBB_FINALIZE 1 + #endif + #endif + """) + delete_all_objects = textwrap.dedent(''' void _deleteAllObjects() {{ @@ -39,6 +49,16 @@ class WrapperTemplate: "calling destructors, call \'clear all\' again if you plan to now recompile a wrap\\n" "module, so that your recompiled module is used instead of the old one." << endl; std::cout.rdbuf(outbuf); + #ifdef GTSAM_WRAP_TBB_FINALIZE + // Terminate TBB worker threads deterministically before MATLAB unloads + // this MEX file. Otherwise the workers may still be winding down when + // the image is unmapped and touch freed code, which crashes on exit. + // See oneTBB issue #977. finalize() is idempotent, returns false rather + // than throwing if the scheduler is still referenced elsewhere, and TBB + // re-initializes transparently if the module is used again. + static tbb::task_scheduler_handle tbbHandle{{tbb::attach{{}}}}; + tbb::finalize(tbbHandle, std::nothrow); + #endif }} ''') diff --git a/gtwrap/matlab_wrapper/wrapper.py b/gtwrap/matlab_wrapper/wrapper.py index c5dd26b..324b994 100755 --- a/gtwrap/matlab_wrapper/wrapper.py +++ b/gtwrap/matlab_wrapper/wrapper.py @@ -1952,9 +1952,11 @@ def generate_wrapper(self, namespace): includes = textwrap.dedent("""\ {wrapper_file_headers} {boost_headers} + {tbb_headers} {includes_list} """).format(wrapper_file_headers=self.wrapper_file_headers.strip(), boost_headers=boost_headers, + tbb_headers=WrapperTemplate.tbb_headers, includes_list='\n'.join(map(str, includes_list))) preamble = self.generate_preamble() From 0a5b9633ce2023d510a4b86a8d77fec93adbc794 Mon Sep 17 00:00:00 2001 From: DLuminary <86183013+DLuminary@users.noreply.github.com> Date: Tue, 25 Aug 2026 07:43:34 +0300 Subject: [PATCH 2/2] Regenerate expected MATLAB fixtures for the TBB finalize block --- tests/expected/matlab/class_wrapper.cpp | 19 +++++++++++++++++++ tests/expected/matlab/enum_wrapper.cpp | 19 +++++++++++++++++++ tests/expected/matlab/functions_wrapper.cpp | 19 +++++++++++++++++++ tests/expected/matlab/geometry_wrapper.cpp | 19 +++++++++++++++++++ tests/expected/matlab/inheritance_wrapper.cpp | 19 +++++++++++++++++++ .../matlab/multiple_files_wrapper.cpp | 19 +++++++++++++++++++ tests/expected/matlab/namespaces_wrapper.cpp | 19 +++++++++++++++++++ .../expected/matlab/special_cases_wrapper.cpp | 19 +++++++++++++++++++ tests/expected/matlab/template_wrapper.cpp | 19 +++++++++++++++++++ 9 files changed, 171 insertions(+) diff --git a/tests/expected/matlab/class_wrapper.cpp b/tests/expected/matlab/class_wrapper.cpp index 29d7156..d6b5327 100644 --- a/tests/expected/matlab/class_wrapper.cpp +++ b/tests/expected/matlab/class_wrapper.cpp @@ -1,6 +1,15 @@ #include #include + +#ifdef GTSAM_USE_TBB +#include +#if TBB_INTERFACE_VERSION >= 12060 +#include +#define GTSAM_WRAP_TBB_FINALIZE 1 +#endif +#endif + #include typedef Fun FunDouble; @@ -139,6 +148,16 @@ void _deleteAllObjects() "calling destructors, call 'clear all' again if you plan to now recompile a wrap\n" "module, so that your recompiled module is used instead of the old one." << endl; std::cout.rdbuf(outbuf); +#ifdef GTSAM_WRAP_TBB_FINALIZE + // Terminate TBB worker threads deterministically before MATLAB unloads + // this MEX file. Otherwise the workers may still be winding down when + // the image is unmapped and touch freed code, which crashes on exit. + // See oneTBB issue #977. finalize() is idempotent, returns false rather + // than throwing if the scheduler is still referenced elsewhere, and TBB + // re-initializes transparently if the module is used again. + static tbb::task_scheduler_handle tbbHandle{tbb::attach{}}; + tbb::finalize(tbbHandle, std::nothrow); +#endif } void _class_RTTIRegister() { diff --git a/tests/expected/matlab/enum_wrapper.cpp b/tests/expected/matlab/enum_wrapper.cpp index 71e8075..d92c77a 100644 --- a/tests/expected/matlab/enum_wrapper.cpp +++ b/tests/expected/matlab/enum_wrapper.cpp @@ -2,6 +2,15 @@ #include +#ifdef GTSAM_USE_TBB +#include +#if TBB_INTERFACE_VERSION >= 12060 +#include +#define GTSAM_WRAP_TBB_FINALIZE 1 +#endif +#endif + + typedef gtsam::Optimizer OptimizerGaussNewtonParams; @@ -44,6 +53,16 @@ void _deleteAllObjects() "calling destructors, call 'clear all' again if you plan to now recompile a wrap\n" "module, so that your recompiled module is used instead of the old one." << endl; std::cout.rdbuf(outbuf); +#ifdef GTSAM_WRAP_TBB_FINALIZE + // Terminate TBB worker threads deterministically before MATLAB unloads + // this MEX file. Otherwise the workers may still be winding down when + // the image is unmapped and touch freed code, which crashes on exit. + // See oneTBB issue #977. finalize() is idempotent, returns false rather + // than throwing if the scheduler is still referenced elsewhere, and TBB + // re-initializes transparently if the module is used again. + static tbb::task_scheduler_handle tbbHandle{tbb::attach{}}; + tbb::finalize(tbbHandle, std::nothrow); +#endif } void _enum_RTTIRegister() { diff --git a/tests/expected/matlab/functions_wrapper.cpp b/tests/expected/matlab/functions_wrapper.cpp index e076237..eeca4bd 100644 --- a/tests/expected/matlab/functions_wrapper.cpp +++ b/tests/expected/matlab/functions_wrapper.cpp @@ -2,6 +2,15 @@ #include +#ifdef GTSAM_USE_TBB +#include +#if TBB_INTERFACE_VERSION >= 12060 +#include +#define GTSAM_WRAP_TBB_FINALIZE 1 +#endif +#endif + + @@ -20,6 +29,16 @@ void _deleteAllObjects() "calling destructors, call 'clear all' again if you plan to now recompile a wrap\n" "module, so that your recompiled module is used instead of the old one." << endl; std::cout.rdbuf(outbuf); +#ifdef GTSAM_WRAP_TBB_FINALIZE + // Terminate TBB worker threads deterministically before MATLAB unloads + // this MEX file. Otherwise the workers may still be winding down when + // the image is unmapped and touch freed code, which crashes on exit. + // See oneTBB issue #977. finalize() is idempotent, returns false rather + // than throwing if the scheduler is still referenced elsewhere, and TBB + // re-initializes transparently if the module is used again. + static tbb::task_scheduler_handle tbbHandle{tbb::attach{}}; + tbb::finalize(tbbHandle, std::nothrow); +#endif } void _functions_RTTIRegister() { diff --git a/tests/expected/matlab/geometry_wrapper.cpp b/tests/expected/matlab/geometry_wrapper.cpp index 691fddb..5585f34 100644 --- a/tests/expected/matlab/geometry_wrapper.cpp +++ b/tests/expected/matlab/geometry_wrapper.cpp @@ -5,6 +5,15 @@ #include #include + +#ifdef GTSAM_USE_TBB +#include +#if TBB_INTERFACE_VERSION >= 12060 +#include +#define GTSAM_WRAP_TBB_FINALIZE 1 +#endif +#endif + #include #include @@ -43,6 +52,16 @@ void _deleteAllObjects() "calling destructors, call 'clear all' again if you plan to now recompile a wrap\n" "module, so that your recompiled module is used instead of the old one." << endl; std::cout.rdbuf(outbuf); +#ifdef GTSAM_WRAP_TBB_FINALIZE + // Terminate TBB worker threads deterministically before MATLAB unloads + // this MEX file. Otherwise the workers may still be winding down when + // the image is unmapped and touch freed code, which crashes on exit. + // See oneTBB issue #977. finalize() is idempotent, returns false rather + // than throwing if the scheduler is still referenced elsewhere, and TBB + // re-initializes transparently if the module is used again. + static tbb::task_scheduler_handle tbbHandle{tbb::attach{}}; + tbb::finalize(tbbHandle, std::nothrow); +#endif } void _geometry_RTTIRegister() { diff --git a/tests/expected/matlab/inheritance_wrapper.cpp b/tests/expected/matlab/inheritance_wrapper.cpp index cc82eda..1fae915 100644 --- a/tests/expected/matlab/inheritance_wrapper.cpp +++ b/tests/expected/matlab/inheritance_wrapper.cpp @@ -2,6 +2,15 @@ #include +#ifdef GTSAM_USE_TBB +#include +#if TBB_INTERFACE_VERSION >= 12060 +#include +#define GTSAM_WRAP_TBB_FINALIZE 1 +#endif +#endif + + typedef MyTemplate MyTemplatePoint2; typedef MyTemplate MyTemplateMatrix; @@ -87,6 +96,16 @@ void _deleteAllObjects() "calling destructors, call 'clear all' again if you plan to now recompile a wrap\n" "module, so that your recompiled module is used instead of the old one." << endl; std::cout.rdbuf(outbuf); +#ifdef GTSAM_WRAP_TBB_FINALIZE + // Terminate TBB worker threads deterministically before MATLAB unloads + // this MEX file. Otherwise the workers may still be winding down when + // the image is unmapped and touch freed code, which crashes on exit. + // See oneTBB issue #977. finalize() is idempotent, returns false rather + // than throwing if the scheduler is still referenced elsewhere, and TBB + // re-initializes transparently if the module is used again. + static tbb::task_scheduler_handle tbbHandle{tbb::attach{}}; + tbb::finalize(tbbHandle, std::nothrow); +#endif } void _inheritance_RTTIRegister() { diff --git a/tests/expected/matlab/multiple_files_wrapper.cpp b/tests/expected/matlab/multiple_files_wrapper.cpp index 00e141a..cb22e2d 100644 --- a/tests/expected/matlab/multiple_files_wrapper.cpp +++ b/tests/expected/matlab/multiple_files_wrapper.cpp @@ -2,6 +2,15 @@ #include +#ifdef GTSAM_USE_TBB +#include +#if TBB_INTERFACE_VERSION >= 12060 +#include +#define GTSAM_WRAP_TBB_FINALIZE 1 +#endif +#endif + + @@ -44,6 +53,16 @@ void _deleteAllObjects() "calling destructors, call 'clear all' again if you plan to now recompile a wrap\n" "module, so that your recompiled module is used instead of the old one." << endl; std::cout.rdbuf(outbuf); +#ifdef GTSAM_WRAP_TBB_FINALIZE + // Terminate TBB worker threads deterministically before MATLAB unloads + // this MEX file. Otherwise the workers may still be winding down when + // the image is unmapped and touch freed code, which crashes on exit. + // See oneTBB issue #977. finalize() is idempotent, returns false rather + // than throwing if the scheduler is still referenced elsewhere, and TBB + // re-initializes transparently if the module is used again. + static tbb::task_scheduler_handle tbbHandle{tbb::attach{}}; + tbb::finalize(tbbHandle, std::nothrow); +#endif } void _multiple_files_RTTIRegister() { diff --git a/tests/expected/matlab/namespaces_wrapper.cpp b/tests/expected/matlab/namespaces_wrapper.cpp index 2e327ec..c8992af 100644 --- a/tests/expected/matlab/namespaces_wrapper.cpp +++ b/tests/expected/matlab/namespaces_wrapper.cpp @@ -1,6 +1,15 @@ #include #include + +#ifdef GTSAM_USE_TBB +#include +#if TBB_INTERFACE_VERSION >= 12060 +#include +#define GTSAM_WRAP_TBB_FINALIZE 1 +#endif +#endif + #include #include #include @@ -81,6 +90,16 @@ void _deleteAllObjects() "calling destructors, call 'clear all' again if you plan to now recompile a wrap\n" "module, so that your recompiled module is used instead of the old one." << endl; std::cout.rdbuf(outbuf); +#ifdef GTSAM_WRAP_TBB_FINALIZE + // Terminate TBB worker threads deterministically before MATLAB unloads + // this MEX file. Otherwise the workers may still be winding down when + // the image is unmapped and touch freed code, which crashes on exit. + // See oneTBB issue #977. finalize() is idempotent, returns false rather + // than throwing if the scheduler is still referenced elsewhere, and TBB + // re-initializes transparently if the module is used again. + static tbb::task_scheduler_handle tbbHandle{tbb::attach{}}; + tbb::finalize(tbbHandle, std::nothrow); +#endif } void _namespaces_RTTIRegister() { diff --git a/tests/expected/matlab/special_cases_wrapper.cpp b/tests/expected/matlab/special_cases_wrapper.cpp index 1cc8579..47ab073 100644 --- a/tests/expected/matlab/special_cases_wrapper.cpp +++ b/tests/expected/matlab/special_cases_wrapper.cpp @@ -1,6 +1,15 @@ #include #include + +#ifdef GTSAM_USE_TBB +#include +#if TBB_INTERFACE_VERSION >= 12060 +#include +#define GTSAM_WRAP_TBB_FINALIZE 1 +#endif +#endif + #include typedef gtsam::PinholeCamera PinholeCameraCal3Bundler; @@ -53,6 +62,16 @@ void _deleteAllObjects() "calling destructors, call 'clear all' again if you plan to now recompile a wrap\n" "module, so that your recompiled module is used instead of the old one." << endl; std::cout.rdbuf(outbuf); +#ifdef GTSAM_WRAP_TBB_FINALIZE + // Terminate TBB worker threads deterministically before MATLAB unloads + // this MEX file. Otherwise the workers may still be winding down when + // the image is unmapped and touch freed code, which crashes on exit. + // See oneTBB issue #977. finalize() is idempotent, returns false rather + // than throwing if the scheduler is still referenced elsewhere, and TBB + // re-initializes transparently if the module is used again. + static tbb::task_scheduler_handle tbbHandle{tbb::attach{}}; + tbb::finalize(tbbHandle, std::nothrow); +#endif } void _special_cases_RTTIRegister() { diff --git a/tests/expected/matlab/template_wrapper.cpp b/tests/expected/matlab/template_wrapper.cpp index 057e26f..60394b6 100644 --- a/tests/expected/matlab/template_wrapper.cpp +++ b/tests/expected/matlab/template_wrapper.cpp @@ -2,6 +2,15 @@ #include +#ifdef GTSAM_USE_TBB +#include +#if TBB_INTERFACE_VERSION >= 12060 +#include +#define GTSAM_WRAP_TBB_FINALIZE 1 +#endif +#endif + + typedef ScopedTemplate ScopedTemplateResult; @@ -36,6 +45,16 @@ void _deleteAllObjects() "calling destructors, call 'clear all' again if you plan to now recompile a wrap\n" "module, so that your recompiled module is used instead of the old one." << endl; std::cout.rdbuf(outbuf); +#ifdef GTSAM_WRAP_TBB_FINALIZE + // Terminate TBB worker threads deterministically before MATLAB unloads + // this MEX file. Otherwise the workers may still be winding down when + // the image is unmapped and touch freed code, which crashes on exit. + // See oneTBB issue #977. finalize() is idempotent, returns false rather + // than throwing if the scheduler is still referenced elsewhere, and TBB + // re-initializes transparently if the module is used again. + static tbb::task_scheduler_handle tbbHandle{tbb::attach{}}; + tbb::finalize(tbbHandle, std::nothrow); +#endif } void _template_RTTIRegister() {