Skip to content

Commit fdd181e

Browse files
authored
build the bundled onetbb as a shared library on windows (#274)
* 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.
1 parent de75c55 commit fdd181e

20 files changed

Lines changed: 421 additions & 613 deletions

.github/scripts/tbb-downstream-check.R

Lines changed: 117 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -97,77 +97,141 @@ if (status != 0L)
9797

9898
dllName <- paste0("check", .Platform$dynlib.ext)
9999

100-
# Report which module supplied the TBB symbols, and fail if that is more than
101-
# one. On Windows the surface can legitimately come from either RcppParallel.dll
102-
# (which links TBB statically) or the 'tbb.dll' stub that RcppParallelLibs()
103-
# offers after '-lRcppParallel' -- in practice it is the stub, since
104-
# RcppParallel.dll turns out not to re-export the runtime at all, which is why
105-
# '-ltbb' is load-bearing rather than a fallback. What must not happen is a
106-
# library split across both: they are separate copies of the oneTBB runtime, so
107-
# an observer registered with one would never fire for arenas owned by the
108-
# other, and unlike the link error this replaced, that failure is silent.
109-
if (.Platform$OS.type == "windows") {
110-
111-
objdump <- Sys.which("objdump")
112-
output <- if (nzchar(objdump))
113-
suppressWarnings(system2(objdump, c("-p", shQuote(dllName)), stdout = TRUE, stderr = TRUE))
100+
# On Windows, check where the TBB symbols came from. RcppParallel builds oneTBB
101+
# as a shared library and links against it, exactly as on other platforms, so
102+
# there should be a single TBB runtime in the process: this library's TBB
103+
# symbols must all come from 'tbb.dll', and RcppParallel.dll must import from it
104+
# too rather than carrying a copy of its own. Two runtimes would be a silent
105+
# failure -- an observer registered with one would never fire for arenas owned
106+
# by the other -- so it is worth asserting rather than assuming.
107+
108+
# objdump has to match the target architecture, and the first one on the PATH
109+
# may well not: the aarch64 runner leads with an x86_64 objdump from C:/mingw64,
110+
# which exits non-zero having read nothing. Ask R which compiler it builds with
111+
# -- that is the one that produced the PE, so its objdump can always read it --
112+
# and only then fall back to whatever the PATH offers
113+
objdumpCandidates <- function() {
114+
115+
cc <- suppressWarnings(
116+
system2(
117+
file.path(R.home("bin"), "R"),
118+
c("CMD", "config", "CC"),
119+
stdout = TRUE,
120+
stderr = FALSE
121+
)
122+
)
123+
124+
# CC may carry arguments (e.g. 'gcc -std=gnu2x'), and may name the compiler
125+
# rather than spell out its path, in which case the PATH has to resolve it
126+
cc <- strsplit(trimws(paste(cc, collapse = " ")), "[[:space:]]+")[[1L]][[1L]]
127+
cc <- if (file.exists(cc)) cc else Sys.which(cc)
128+
129+
compilers <- c(cc, Sys.which(c("gcc", "clang", "cc")))
130+
beside <- file.path(dirname(compilers[nzchar(compilers)]), "objdump.exe")
131+
132+
candidates <- c(beside, Sys.which("objdump"))
133+
candidates <- candidates[nzchar(candidates) & file.exists(candidates)]
134+
135+
# '/' and '\\' spellings of one path are the same objdump; don't try twice
136+
unique(normalizePath(candidates, winslash = "/", mustWork = FALSE))
114137

115-
status <- attr(output, "status")
138+
}
116139

117-
# keep only the import tables. the export table follows them, and lists this
118-
# library's own inlined TBB instantiations -- left in, it would be absorbed
119-
# into the last 'DLL Name:' block and credit that library with TBB symbols
120-
# it never imported
121-
exports <- grep("export table|The Export Tables", output)
122-
if (length(exports))
123-
output <- output[seq_len(exports[[1L]] - 1L)]
140+
tbbImports <- function(dll) {
124141

125142
# each imported library opens a 'DLL Name:' block listing the symbols taken
126-
# from it, and runs until the next such block
127-
starts <- grep("DLL Name:", output, fixed = TRUE)
128-
readable <- length(starts) > 0L && !(is.numeric(status) && status != 0L)
143+
# from it; finding at least one is how we know objdump really read the file,
144+
# rather than failing in a way that would look like 'imports nothing'
145+
starts <- integer()
146+
output <- character()
147+
148+
for (objdump in objdumpCandidates()) {
149+
150+
output <- suppressWarnings(
151+
system2(objdump, c("-p", shQuote(dll)), stdout = TRUE, stderr = TRUE)
152+
)
153+
status <- attr(output, "status")
154+
155+
# keep only the import tables. the export table follows them, and lists
156+
# the library's own inlined TBB instantiations -- left in, it would be
157+
# absorbed into the last 'DLL Name:' block and credit that library with
158+
# TBB symbols it never imported
159+
exports <- grep("export table|The Export Tables", output)
160+
if (length(exports))
161+
output <- output[seq_len(exports[[1L]] - 1L)]
162+
163+
starts <- grep("DLL Name:", output, fixed = TRUE)
164+
if (length(starts) && !(is.numeric(status) && status != 0L))
165+
break
166+
167+
fmt <- "** '%s' could not read the import table of '%s'"
168+
writeLines(sprintf(fmt, objdump, basename(dll)))
169+
writeLines(output)
129170

130-
if (!nzchar(objdump)) {
171+
starts <- integer()
131172

132-
writeLines("** objdump not found; skipping the import table check")
173+
}
174+
175+
if (!length(starts)) {
176+
fmt <- "** no usable objdump for '%s'; skipping the import table check"
177+
writeLines(sprintf(fmt, basename(dll)))
178+
return(NULL)
179+
}
133180

134-
} else if (!readable) {
181+
imported <- sub(".*DLL Name:[[:space:]]*", "", output[starts])
182+
ends <- c(starts[-1L], length(output) + 1L)
135183

136-
# don't let an objdump that couldn't read the file pass as 'no imports':
137-
# the runner's objdump may be built for another target (an x86_64 one
138-
# cannot read an aarch64 PE, and exits non-zero having printed nothing)
139-
fmt <- "** '%s' could not read the import table of '%s'; skipping the check"
140-
writeLines(sprintf(fmt, objdump, dllName))
141-
writeLines(output)
184+
# attribute the TBB symbols to the module each was taken from
185+
providers <- character()
186+
for (i in seq_along(starts)) {
187+
block <- output[seq(starts[[i]], ends[[i]] - 1L)]
188+
if (any(grepl("_ZN3tbb", block, fixed = TRUE)))
189+
providers <- c(providers, imported[[i]])
190+
}
191+
192+
fmt <- "%s imports: %s [tbb symbols from: %s]"
193+
writeLines(sprintf(
194+
fmt, basename(dll),
195+
paste(imported, collapse = ", "),
196+
if (length(providers)) paste(providers, collapse = ", ") else "none"
197+
))
198+
199+
tolower(providers)
200+
201+
}
142202

143-
} else {
203+
if (.Platform$OS.type == "windows") {
144204

145-
imported <- sub(".*DLL Name:[[:space:]]*", "", output[starts])
146-
ends <- c(starts[-1L], length(output) + 1L)
205+
providers <- tbbImports(dllName)
147206

148-
# attribute the TBB symbols to the module each was taken from
149-
providers <- character()
150-
for (i in seq_along(starts)) {
151-
block <- output[seq(starts[[i]], ends[[i]] - 1L)]
152-
if (any(grepl("_ZN3tbb", block, fixed = TRUE)))
153-
providers <- c(providers, imported[[i]])
154-
}
207+
if (!is.null(providers)) {
155208

156-
writeLines(sprintf("imports: %s", paste(imported, collapse = ", ")))
157-
writeLines(sprintf("tbb symbols from: %s", if (length(providers))
158-
paste(providers, collapse = ", ") else "(none; resolved statically)"))
209+
if (length(providers) != 1L)
210+
stop("expected this library's tbb symbols to come from exactly one ",
211+
"module, but found ", length(providers), " (",
212+
paste(providers, collapse = ", "), "); more than one means more ",
213+
"than one copy of the oneTBB runtime")
159214

160-
if (length(providers) > 1L)
161-
stop("the downstream library takes TBB symbols from more than one ",
162-
"module (", paste(providers, collapse = ", "), "); those are ",
163-
"separate copies of the oneTBB runtime, so its observers and its ",
164-
"arenas would belong to different schedulers")
215+
if (!identical(providers, "tbb.dll"))
216+
stop("this library's tbb symbols came from '", providers,
217+
"' rather than the shared 'tbb.dll' runtime")
165218

166219
}
167220

221+
# and RcppParallel itself must be a client of that same runtime
222+
rcppParallelDll <- RcppParallel:::archSystemFile("libs", "RcppParallel.dll")
223+
if (!file.exists(rcppParallelDll))
224+
stop("could not locate RcppParallel.dll within the installed package")
225+
226+
providers <- tbbImports(rcppParallelDll)
227+
if (!is.null(providers) && !identical(providers, "tbb.dll"))
228+
stop("RcppParallel.dll does not take its tbb symbols from 'tbb.dll' ",
229+
"(got: ", paste(providers, collapse = ", "),
230+
"); it appears to carry its own copy of the runtime")
231+
168232
}
169233

170-
# loading also proves any load-time dependency on the tbb stub resolves
234+
# loading also proves the load-time dependency on the tbb runtime resolves
171235
dll <- dyn.load(dllName)
172236
on.exit(dyn.unload(dll[["path"]]), add = TRUE)
173237

NEWS.md

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,37 @@
88
loaded into the process, and failed with "Library not loaded:
99
@rpath/libtbb.dylib" otherwise. (#209)
1010

11-
* Fixed `RcppParallel::tbbLibraryPath()` returning `NULL` on Windows. It looked
12-
for the static archives `libtbb12.a` / `libtbb.a`, which are never installed
13-
with the package: TBB is linked statically into `RcppParallel.dll` there, and
14-
the only TBB library installed alongside it is the `tbb.dll` stub, which is
15-
what it now finds. Previously it succeeded only when the package happened to
16-
be running on the machine that built it against an Rtools TBB, and even then
17-
answered with a path into Rtools -- a build dependency -- rather than with
18-
anything the installed package uses. Note that `tbbLibraryPath("tbbmalloc")`
19-
returns `NULL` on Windows, as no separate tbbmalloc library is installed
20-
there. (#270)
21-
22-
* `RcppParallel::tbbLibraryPath()` called with no arguments, and the internal
23-
`tbbRoot()`, no longer report the Rtools directory recorded when the package
24-
was configured. Unlike the named-library form, these return their answer
25-
without checking that it exists, so for a pre-built binary (e.g. the CRAN
26-
build) they named a directory on the machine that built the package. On
27-
Windows both now report the package's own library directory. (#270)
11+
* On Windows, RcppParallel now builds the bundled oneTBB as a shared library
12+
and links against it, shipping `tbb.dll` and `tbbmalloc.dll` alongside the
13+
package -- the same arrangement already used on every other platform.
14+
Previously it linked the static TBB provided by Rtools directly into
15+
`RcppParallel.dll`, which meant the TBB version (and ABI) depended on the
16+
user's toolchain, and left downstream packages with no TBB library to link
17+
against. Building it ourselves gives every platform the same oneTBB and makes
18+
the ABI a property of RcppParallel. `TBB_LIB` / `TBB_INC` are still honoured
19+
for anyone supplying their own build. This requires cmake, which Rtools has
20+
shipped since Rtools42; toolchains without one continue to use the tinythread
21+
fallback.
22+
23+
* As a consequence, `RcppParallel::RcppParallelLibs()` now emits `-ltbb` and
24+
`-ltbbmalloc` on Windows, in addition to `-lRcppParallel` (which remains
25+
necessary there for the entry points RcppParallel compiles itself, such as
26+
`isProcessForkedChild`). Packages that previously resolved TBB symbols out of
27+
`RcppParallel.dll`, or the tbbmalloc API via `-lRcppParallel` alone, should
28+
rebuild against these flags.
29+
30+
* The `tbb.dll` compatibility stub is gone. It existed to publish the
31+
pre-oneTBB `task_scheduler_observer` entry point on top of a statically
32+
linked runtime, but because it linked the TBB archives itself it amounted to
33+
a second, independent copy of the oneTBB scheduler living in the same
34+
process. That entry point is now exported by the real `tbb.dll`, as it
35+
already was by the shared libraries on other platforms, so binaries built
36+
against RcppParallel 5.1.11 and earlier continue to resolve it.
37+
38+
* Fixed `RcppParallel::tbbLibraryPath()` returning `NULL` on Windows, and
39+
`tbbRoot()` reporting a directory that need not exist on the machine running
40+
the package -- for a pre-built binary, the Rtools tree of the machine that
41+
built it. Both now describe the installation actually in use. (#270)
2842

2943
* Fixed an issue where compiling code including `tbb/parallel_for_each.h`
3044
could fail with toolchains that accept `-std=c++20` but provide a

R/tbb.R

Lines changed: 24 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,9 @@ tbbLibraryPath <- function(name = NULL) {
2222
return(tbbRoot)
2323

2424
# form library names
25-
#
26-
# on Windows the library we install is the 'tbb.dll' stub -- TBB itself is
27-
# inside RcppParallel.dll -- so look for that first. the static archives are
28-
# still tried afterwards, for a TBB_LIB pointed at an Rtools tree at runtime
2925
tbbLibNames <- list(
3026
"Darwin" = paste0("lib", name, ".dylib"),
31-
"Windows" = c(paste0(name, ".dll"), paste0("lib", name, c("12", ""), ".a")),
27+
"Windows" = paste0(name, ".dll"),
3228
"SunOS" = paste0("lib", name, ".so"),
3329
"Linux" = paste0("lib", name, c(".so.2", ".so"))
3430
)
@@ -89,35 +85,35 @@ tbbCxxFlags <- function() {
8985
# Return the linker flags required for TBB on this platform
9086
tbbLdFlags <- function() {
9187

92-
# on Windows, we statically link to oneTBB
88+
# on Windows every symbol must be resolved at link time -- there is no
89+
# equivalent of lazy binding or '-undefined dynamic_lookup' -- so a
90+
# downstream package needs both the TBB libraries it calls into and
91+
# RcppParallel itself, for the entry points we compile (e.g.
92+
# isProcessForkedChild). elsewhere those resolve from the process at load
93+
# time and only TBB needs naming
9394
if (is_windows()) {
9495

95-
libPath <- archSystemFile("libs")
96-
97-
ldFlags <- sprintf("-L%s -lRcppParallel", asBuildPath(libPath))
98-
99-
# also offer the stub library, which exports the TBB runtime wholesale.
100-
# this is not just a safety net: R CMD SHLIB links RcppParallel.dll
101-
# against an export list generated from our own objects, so the TBB
102-
# entry points pulled in from the static library are not re-exported,
103-
# and a downstream package linking '-lRcppParallel' alone resolves none
104-
# of them (which is what broke rstan). the stub is what makes those
105-
# symbols reachable at all.
106-
#
107-
# it still comes last, so that anything RcppParallel.dll does export
108-
# wins, and so no import of the stub is recorded unless something needs
109-
# it. that matters because the stub carries its own copy of the oneTBB
110-
# runtime: a package split across both would register observers with one
111-
# scheduler while its tasks ran in the other.
112-
# .github/scripts/tbb-downstream-check.R asserts that does not happen
113-
tbbPath <- archSystemFile("lib")
114-
if (file.exists(file.path(tbbPath, "tbb.dll")))
115-
ldFlags <- paste(ldFlags, sprintf("-L%s -ltbb", asBuildPath(tbbPath)))
96+
libsPath <- archSystemFile("libs")
97+
ldFlags <- sprintf("-L%s -lRcppParallel", asBuildPath(libsPath))
98+
99+
# only name the TBB libraries when there are TBB libraries to name: a
100+
# build that fell back to tinythread ships none, and asking the linker
101+
# for them would fail the downstream build outright
102+
if (TBB_ENABLED && !is.null(tbbLibraryPath("tbb"))) {
103+
fmt <- "%s -L%s -l%s -l%s"
104+
ldFlags <- sprintf(
105+
fmt,
106+
ldFlags,
107+
asBuildPath(tbbLibraryPath()),
108+
TBB_NAME,
109+
TBB_MALLOC_NAME
110+
)
111+
}
116112

117113
return(ldFlags)
118114

119115
}
120-
116+
121117
# shortcut if TBB_LIB defined
122118
tbbLib <- Sys.getenv("TBB_LINK_LIB", Sys.getenv("TBB_LIB", unset = TBB_LIB))
123119
if (nzchar(tbbLib)) {
@@ -150,15 +146,6 @@ tbbLdFlags <- function() {
150146

151147
tbbRoot <- function() {
152148

153-
# on Windows, always answer with our own library directory. TBB is linked
154-
# statically into RcppParallel.dll there, and the only TBB library we
155-
# install is the tbb.dll stub, so that directory is the whole story. TBB_LIB
156-
# is worse than useless here: it records the Rtools tree of whichever
157-
# machine ran configure, which for a pre-built binary is a path that need
158-
# not exist on the user's machine at all (#270)
159-
if (is_windows())
160-
return(archSystemFile("lib"))
161-
162149
if (nzchar(TBB_LIB))
163150
return(TBB_LIB)
164151

0 commit comments

Comments
 (0)