re-export tbbmalloc from RcppParallel.dll on windows - #262
Merged
Conversation
downstream packages on windows link with '-lRcppParallel' alone, and so can only use tbb symbols that RcppParallel.dll exports. the tbb runtime symbols come along implicitly -- RcppParallel references them, so those archive members are linked in and re-exported via their '-export:' directives -- but nothing references the tbbmalloc entry points, so packages using the scalable allocator (e.g. via RcppArmadillo's ARMA_USE_TBB_ALLOC) fail to link with undefined references to scalable_malloc and scalable_free. link tbbmalloc with --whole-archive so its objects are always included and re-exported. tbbmalloc must precede tbb on the link line: both archives bundle an itt_notify object defining the same symbols, and with tbbmalloc's copy already linked, tbb's is never pulled in. older (non-oneTBB) toolchains like rtools42 keep the plain link: there, tbb and tbbmalloc both define DllMain and so cannot be linked wholesale, and their objects carry no '-export:' directives to re-export anyhow.
generates a small package using scalable_malloc and scalable_free via the RcppParallel::CxxFlags() / RcppParallelLibs() flags, installs it, and confirms the allocator can be used. on windows, this exercises the tbbmalloc re-export from RcppParallel.dll; elsewhere, the run-time resolution of tbbmalloc symbols loaded by RcppParallel::.onLoad(). the test builds a package, so it runs only on CI (or with NOT_CRAN), and only when the tbb backend is enabled.
R CMD check sets R_TESTS=startup.Rs when running tests, which breaks R sub-processes run with a different working directory. the Rscript invocations in the test package's Makevars failed at startup because of this, expanding CxxFlags() and RcppParallelLibs() to nothing, so compilation failed with tbb/scalable_allocator.h not found.
kevinushey
added a commit
that referenced
this pull request
Jul 28, 2026
* build the bundled onetbb as a shared library on windows Windows was the only platform where TBB was not a shared library loaded at runtime, and nearly every Windows-specific wart traced back to that. Use the bundled oneTBB there too, built shared, shipped as tbb.dll / tbbmalloc.dll and linked exactly as on other platforms. Rtools ships static libraries only, so adopting its TBB meant linking it into RcppParallel.dll. That made the TBB version -- and ABI -- a property of the user's toolchain (Intel TBB 2017 on Rtools42, oneTBB later), and left downstream packages with no TBB library to link against. The workarounds accumulated: re-exporting the whole tbbmalloc archive so the allocator could be resolved (#262), a tbb.dll stub so packages linking -ltbb could load (#249, #250), building that stub differently for legacy toolchains (#258), offering it from RcppParallelLibs() so rstan could link at all (#269), and a library lookup that searched for archives never installed with the package (#270). Worse, the stub linked the TBB archives itself, so it was a second complete copy of the oneTBB scheduler. Both it and RcppParallel.dll export the same 81 tbb::detail::r1:: entry points, both are loaded in every session, and which one a downstream package binds to is up to the linker. Two schedulers with separate thread pools and separate global state is a correctness problem, not a linking inconvenience. This removes: - the Rtools TBB detection in configure.R, and with it the TBB_NAME derivation from archive names and the tbb12 special case - the --whole-archive tbbmalloc re-export, which only existed because there was no DLL to link - BUILD_SHARED_LIBS=0 for Windows - src/tbb-compat/ and buildTbbStub() entirely, along with the #ifndef _WIN32 guard in observer_proxy.cpp and the __TBB_LEGACY_..._PROVIDED macro that let the stub tell which headers it was compiling against - the Windows branches in .install.libs(), loadTbbLibrary(), tbbLdFlags() and tbbLibraryPath(), and the Windows-first load order in .onLoad() Notes on the pieces that are not just deletions: - mingw CMake would name a shared build libtbb.dll; patch PREFIX to "" so the runtime is tbb.dll, the name binaries built against 5.1.11 and earlier import. The import library keeps its lib prefix, so -ltbb still resolves. - the legacy task_scheduler_observer_v3::observe definition now applies on Windows too. mingw has no .def file, so its export comes from a dllexport directive: the declaration gains TBB_EXPORT, which is empty when consuming the headers. - .onLoad loads tbb before RcppParallel on every platform now, since RcppParallel.dll imports from tbb.dll and the package library directory is not on the DLL search path. - tbbLdFlags() keeps -lRcppParallel on Windows: isProcessForkedChild is compiled into RcppParallel.dll, and Windows has no lazy binding to resolve it at load time. The downstream CI check now asserts the property this is all for: the downstream library's TBB symbols come from exactly one module, that module is tbb.dll, and RcppParallel.dll imports from it rather than carrying its own copy. * fix the shared tbb link on rtools42 and aarch64 windows Two failures from the first CI run, both only reachable now that TBB is linked as a shared library on Windows -- a static build has no link step. Rtools42 (gcc 10): the TBB link failed with undefined references to __stack_chk_fail, __strncpy_chk and __strncat_chk. TBB compiles with -fstack-protector-strong and _FORTIFY_SOURCE, and on mingw those helpers live in libssp rather than libgcc; newer toolchains pull it in implicitly, Rtools42 does not. Pass -lssp via CMAKE_SHARED_LINKER_FLAGS, mirroring the -lssp that configure.R already adds for RcppParallel's own link. aarch64 Windows: 'lld: error: unknown argument: -z'. Rtools45-aarch64 drives clang with lld, and Clang.cmake adds -Wl,-z,relro,-z,now for any non-Apple target. GNU.cmake already guards this with NOT MINGW; give Clang.cmake the equivalent guard. windows-latest (release) passed on the first run, so the shared mingw build, the PREFIX "" naming and the reordered load in .onLoad() all work. * link libssp via TBB_COMMON_LINK_LIBS rather than the shared linker flags TBB compiles with -fstack-protector-strong and _FORTIFY_SOURCE=2, and on mingw the helpers those need (__stack_chk_fail, __stack_chk_guard, __strncpy_chk, ...) live in libssp rather than libgcc. Rtools42 (gcc 10) does not pull it in implicitly, so the link fails with undefined references to them. Passing -lssp through CMAKE_SHARED_LINKER_FLAGS was not enough: those flags are emitted before the objects, and GNU ld only resolves symbols that are already undefined when it reaches a library, so it was discarded. tbb and tbbmalloc happened to survive that because tbb_handle_ipo gives them LTO; tbbmalloc_proxy has no such call and failed to link. Putting ssp in TBB_COMMON_LINK_LIBS routes it through target_link_libraries instead, which lands after the objects, and covers all three targets. * fix the RcppParallel.dll half of the downstream import check Two reasons it was never actually running, both visible in the CI log even though the job passed: - the path was built with paste0("libs", .Platform$r_arch), giving 'libsx64' rather than 'libs/x64'. system.file() returned "" for that, so objdump was handed '/RcppParallel.dll' and reported "No such file". Use archSystemFile(), which the package already has for this, and fail loudly if the library still can't be found. - on aarch64 the runner has an x86_64 objdump from C:/mingw64 ahead of Rtools' own on the PATH, and it cannot read an aarch64 PE, so both halves of the check skipped there. Try the objdump sitting beside the compiler first, and fall back through the remaining candidates. * locate objdump via the compiler R actually builds with The previous attempt looked beside Sys.which("gcc"/"clang"), but on the aarch64 runner those resolve to C:/mingw64 -- the same x86_64 toolchain that leads the PATH -- so both candidates were the one objdump that cannot read an aarch64 PE, and the check still skipped there. They were also not deduplicated, differing only in slash direction. 'R CMD config CC' names the compiler that produced the library, so its objdump can read it by construction. Try that first, and normalize the candidates so one objdump is not tried under two spellings. * don't emit -ltbb when no tbb was installed The Windows branch of tbbLdFlags() named the TBB libraries unconditionally, dropping the file.exists() guard the previous stub-based code had. On a build that fell back to tinythread -- configure.R sets TBB_ENABLED = FALSE only there, when cmake is missing and TBB_LIB is unset -- RcppParallelLibs() then emitted -L<libs> -lRcppParallel -L<lib> -ltbb -ltbbmalloc for libraries that were never built or shipped, so a downstream package would fail to link with "cannot find -ltbb". CxxFlags() correctly reports -DRCPP_PARALLEL_USE_TBB=0 in that configuration, so the package is not even using TBB. Name them only when TBB is enabled and the library actually resolves. Also drop an unverified aside from NEWS: Rtools42 and Rtools45 are confirmed to ship cmake, Rtools40 was not checked.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
RcppParallel 6.0.0 broke linking for downstream Windows packages that use the TBB scalable allocator (e.g. via RcppArmadillo's
ARMA_USE_TBB_ALLOC):Seen in the wild building mr.mashr on r-universe: https://github.com/r-universe/cran/actions/runs/30078654393/job/89436369922
On Windows,
RcppParallelLibs()emits only-lRcppParallel, so downstream packages can only use TBB symbols thatRcppParallel.dllexports. The tbb runtime symbols come along implicitly: RcppParallel references them, so those members of the static Rtoolslibtbb12.aare linked in, and re-exported via the-export:directives embedded in the objects (merged with R'stmp.def). But nothing in RcppParallel references the tbbmalloc entry points, so no member oflibtbbmalloc.ais ever linked in, andscalable_mallocand friends are not exported. (5.1.11 additionally emitted-L$(TBB_LIB) -ltbb -ltbbmallocfor downstream packages, so they got these symbols statically from Rtools; that was dropped in 6.0.0.)Fix
Link tbbmalloc into
RcppParallel.dllwith--whole-archive, so its objects are always included and theirscalable_*API is re-exported. Downstream flags are unchanged.Two details:
itt_notifyobject defining the same 189 strong symbols. With tbbmalloc's copy linked first, tbb's is never pulled in (nothing in libtbb12 requires symbols unique to its own itt object); the reverse order fails withduplicate symbol: __itt_fini_ittlib.DllMain, so tbbmalloc cannot be linked wholesale; its objects also carry no-export:directives, so nothing would be re-exported anyhow. No change in behavior on those toolchains.Note that plain tbbmalloc does not replace the default allocator -- that is
tbbmalloc_proxy's job, which is a separate archive (and not shipped by Rtools at all). Verified against the actual Rtools archives:libtbbmalloc.adefines thescalable_*API and internal helpers only; nomalloc/free/operator newoverrides, and noDllMainon oneTBB toolchains.Validation
Simulated R's exact link (def file + flag order) with clang/lld against the real Rtools43/44/45 archives:
scalable_*symbols, plus the same 70tbb::detail::r1symbols as the shipped 6.0.0 binary