Skip to content

re-export tbbmalloc from RcppParallel.dll on windows#262

Merged
kevinushey merged 3 commits into
masterfrom
fix-windows-tbbmalloc-exports
Jul 24, 2026
Merged

re-export tbbmalloc from RcppParallel.dll on windows#262
kevinushey merged 3 commits into
masterfrom
fix-windows-tbbmalloc-exports

Conversation

@kevinushey

Copy link
Copy Markdown
Contributor

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):

ld.exe: RcppExports.o: undefined reference to `scalable_free'
ld.exe: RcppExports.o: undefined reference to `scalable_malloc'

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 that RcppParallel.dll exports. The tbb runtime symbols come along implicitly: RcppParallel references them, so those members of the static Rtools libtbb12.a are linked in, and re-exported via the -export: directives embedded in the objects (merged with R's tmp.def). But nothing in RcppParallel references the tbbmalloc entry points, so no member of libtbbmalloc.a is ever linked in, and scalable_malloc and friends are not exported. (5.1.11 additionally emitted -L$(TBB_LIB) -ltbb -ltbbmalloc for downstream packages, so they got these symbols statically from Rtools; that was dropped in 6.0.0.)

Fix

Link tbbmalloc into RcppParallel.dll with --whole-archive, so its objects are always included and their scalable_* API is re-exported. Downstream flags are unchanged.

Two details:

  • tbbmalloc must precede tbb on the link line. Both Rtools archives bundle an itt_notify object 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 with duplicate symbol: __itt_fini_ittlib.
  • Older (non-oneTBB) toolchains, e.g. Rtools42, keep the plain link. There, tbb and tbbmalloc both define 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.a defines the scalable_* API and internal helpers only; no malloc/free/operator new overrides, and no DllMain on oneTBB toolchains.

Validation

Simulated R's exact link (def file + flag order) with clang/lld against the real Rtools43/44/45 archives:

  • test DLL links cleanly with the new flags on all three toolchains
  • exports all 11 scalable_* symbols, plus the same 70 tbb::detail::r1 symbols as the shipped 6.0.0 binary
  • negative control (tbb before tbbmalloc) reproduces the duplicate-symbol failure
  • configure output on Linux is byte-identical before/after; all four branch combinations (windows/unix, with/without system TBB) emit the expected flags

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
kevinushey merged commit 7f1fb9f into master Jul 24, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant