Skip to content

Commit 514d22f

Browse files
committed
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.
1 parent de75c55 commit 514d22f

16 files changed

Lines changed: 286 additions & 596 deletions

File tree

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

Lines changed: 67 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -97,24 +97,27 @@ 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") {
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+
tbbImports <- function(dll) {
110108

111109
objdump <- Sys.which("objdump")
112-
output <- if (nzchar(objdump))
113-
suppressWarnings(system2(objdump, c("-p", shQuote(dllName)), stdout = TRUE, stderr = TRUE))
110+
if (!nzchar(objdump)) {
111+
writeLines("** objdump not found; skipping the import table check")
112+
return(NULL)
113+
}
114114

115+
output <- suppressWarnings(
116+
system2(objdump, c("-p", shQuote(dll)), stdout = TRUE, stderr = TRUE)
117+
)
115118
status <- attr(output, "status")
116119

117-
# keep only the import tables. the export table follows them, and lists this
120+
# keep only the import tables. the export table follows them, and lists the
118121
# library's own inlined TBB instantiations -- left in, it would be absorbed
119122
# into the last 'DLL Name:' block and credit that library with TBB symbols
120123
# it never imported
@@ -125,49 +128,71 @@ if (.Platform$OS.type == "windows") {
125128
# each imported library opens a 'DLL Name:' block listing the symbols taken
126129
# from it, and runs until the next such block
127130
starts <- grep("DLL Name:", output, fixed = TRUE)
128-
readable <- length(starts) > 0L && !(is.numeric(status) && status != 0L)
129131

130-
if (!nzchar(objdump)) {
132+
# don't let an objdump that couldn't read the file pass as 'no imports': the
133+
# runner's objdump may be built for another target (an x86_64 one cannot
134+
# read an aarch64 PE, and exits non-zero having printed nothing)
135+
if (!length(starts) || (is.numeric(status) && status != 0L)) {
136+
fmt <- "** '%s' could not read the import table of '%s'; skipping the check"
137+
writeLines(sprintf(fmt, objdump, basename(dll)))
138+
writeLines(output)
139+
return(NULL)
140+
}
131141

132-
writeLines("** objdump not found; skipping the import table check")
142+
imported <- sub(".*DLL Name:[[:space:]]*", "", output[starts])
143+
ends <- c(starts[-1L], length(output) + 1L)
133144

134-
} else if (!readable) {
145+
# attribute the TBB symbols to the module each was taken from
146+
providers <- character()
147+
for (i in seq_along(starts)) {
148+
block <- output[seq(starts[[i]], ends[[i]] - 1L)]
149+
if (any(grepl("_ZN3tbb", block, fixed = TRUE)))
150+
providers <- c(providers, imported[[i]])
151+
}
135152

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)
153+
fmt <- "%s imports: %s [tbb symbols from: %s]"
154+
writeLines(sprintf(
155+
fmt, basename(dll),
156+
paste(imported, collapse = ", "),
157+
if (length(providers)) paste(providers, collapse = ", ") else "none"
158+
))
159+
160+
tolower(providers)
142161

143-
} else {
162+
}
163+
164+
if (.Platform$OS.type == "windows") {
144165

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

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-
}
168+
if (!is.null(providers)) {
155169

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)"))
170+
if (length(providers) != 1L)
171+
stop("expected this library's tbb symbols to come from exactly one ",
172+
"module, but found ", length(providers), " (",
173+
paste(providers, collapse = ", "), "); more than one means more ",
174+
"than one copy of the oneTBB runtime")
159175

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")
176+
if (!identical(providers, "tbb.dll"))
177+
stop("this library's tbb symbols came from '", providers,
178+
"' rather than the shared 'tbb.dll' runtime")
165179

166180
}
167181

182+
# and RcppParallel itself must be a client of that same runtime
183+
rcppParallelDll <- file.path(
184+
system.file(paste0("libs", .Platform$r_arch), package = "RcppParallel"),
185+
"RcppParallel.dll"
186+
)
187+
providers <- tbbImports(rcppParallelDll)
188+
if (!is.null(providers) && !identical(providers, "tbb.dll"))
189+
stop("RcppParallel.dll does not take its tbb symbols from 'tbb.dll' ",
190+
"(got: ", paste(providers, collapse = ", "),
191+
"); it appears to carry its own copy of the runtime")
192+
168193
}
169194

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

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+
provided since Rtools42; toolchains without it (e.g. Rtools40) continue to
21+
use the tinythread 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: 19 additions & 39 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,28 @@ 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)))
116-
117-
return(ldFlags)
96+
libsPath <- archSystemFile("libs")
97+
tbbPath <- tbbLibraryPath()
98+
99+
fmt <- "-L%s -lRcppParallel -L%s -l%s -l%s"
100+
return(sprintf(
101+
fmt,
102+
asBuildPath(libsPath),
103+
asBuildPath(tbbPath),
104+
TBB_NAME,
105+
TBB_MALLOC_NAME
106+
))
118107

119108
}
120-
109+
121110
# shortcut if TBB_LIB defined
122111
tbbLib <- Sys.getenv("TBB_LINK_LIB", Sys.getenv("TBB_LIB", unset = TBB_LIB))
123112
if (nzchar(tbbLib)) {
@@ -150,15 +139,6 @@ tbbLdFlags <- function() {
150139

151140
tbbRoot <- function() {
152141

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-
162142
if (nzchar(TBB_LIB))
163143
return(TBB_LIB)
164144

R/zzz.R

Lines changed: 9 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,6 @@
1212

1313
loadTbbLibrary <- function(name) {
1414

15-
# On Windows, TBB is statically linked into RcppParallel.dll, but we
16-
# still ship a stub tbb.dll for compatibility with packages linking via
17-
# '-ltbb' (e.g. through StanHeaders). Such packages record a load-time
18-
# dependency on 'tbb.dll', which the Windows loader can only resolve if
19-
# we've already loaded it -- the library directory itself is not on the
20-
# DLL search path.
21-
if (is_windows()) {
22-
23-
# NOTE: resolve against the package's own library directory, where
24-
# install.libs.R places the stub. this is what tbbRoot() reports on
25-
# Windows too; spell it out here so that loading during .onLoad does
26-
# not depend on the configured TBB_LIB in any way
27-
path <- archSystemFile("lib", paste0(name, ".dll"))
28-
if (!file.exists(path)) {
29-
verboseMessage("tbb library '%s' not found in package 'lib' folder; skipping", name)
30-
return(NULL)
31-
}
32-
33-
verboseMessage("loading tbb library '%s'", path)
34-
return(dyn.load(path, local = FALSE, now = TRUE))
35-
36-
}
37-
3815
path <- tbbLibraryPath(name)
3916
if (is.null(path)) {
4017
verboseMessage("tbb library '%s' could not be resolved; skipping", name)
@@ -52,26 +29,21 @@ loadTbbLibrary <- function(name) {
5229
}
5330

5431
.onLoad <- function(libname, pkgname) {
55-
56-
# on Windows, load RcppParallel first
57-
if (.Platform$OS.type == "windows") {
58-
.dllInfo <<- library.dynam("RcppParallel", pkgname, libname)
59-
}
60-
61-
# load tbb, tbbmalloc
32+
33+
# load tbb, tbbmalloc first: RcppParallel links against them, and on Windows
34+
# in particular the loader can only satisfy that dependency once they are in
35+
# the process -- the package's library directory is not on the DLL search
36+
# path, so it cannot find them by itself
6237
.tbbDllInfo <<- loadTbbLibrary("tbb")
6338
.tbbMallocDllInfo <<- loadTbbLibrary("tbbmalloc")
64-
39+
6540
# load tbbmalloc_proxy, but only if requested
6641
useTbbMallocProxy <- Sys.getenv("RCPP_PARALLEL_USE_TBBMALLOC_PROXY", unset = "FALSE")
6742
if (useTbbMallocProxy %in% c("TRUE", "True", "true", "1"))
6843
.tbbMallocProxyDllInfo <<- loadTbbLibrary("tbbmalloc_proxy")
69-
70-
# load RcppParallel library if available
71-
if (.Platform$OS.type != "windows") {
72-
.dllInfo <<- library.dynam("RcppParallel", pkgname, libname, local = FALSE)
73-
}
74-
44+
45+
.dllInfo <<- library.dynam("RcppParallel", pkgname, libname, local = FALSE)
46+
7547
}
7648

7749
.onUnload <- function(libpath) {

0 commit comments

Comments
 (0)