Skip to content

Commit 69dac53

Browse files
authored
address review findings on the windows onetbb build (#272)
* address review findings on the windows onetbb build - configure: derive TBB_NAME / TBB_MALLOC_NAME whenever an Rtools tree is adopted, not only when it is oneTBB. gating both on 'oneapi' broke an explicitly configured legacy TBB_LIB, which linked -ltbb against a directory holding libtbb_static.a - configure: don't let the cmake probe throw. it now runs on Windows, where an unrunnable CMAKE or unparseable --version output has to degrade to tinythread rather than fail the configure - configure: correct the required cmake version in the error message (3.5, matching the check and DESCRIPTION), and rename useBundledTbb to buildBundledTbb so it no longer collides with the install.libs.R function - install.libs.R: report the Windows static build explicitly instead of logging 'no tbb runtime libraries found', which reads like a failure - tbb.R: note that the stub carries its own oneTBB runtime, so resolving against it means a second scheduler rather than a free fallback - downstream check: assert the built library imports nothing from the tbb.dll stub, which is the property that comment now relies on - GNU.cmake: probe with an empty file rather than the null device, which drops the WIN32 special case and removes the unverified 'gcc -c NUL' - add windows-11-arm to the downstream matrix, restore the trailing newline in observer_proxy.cpp, pin rstan.yaml against DESCRIPTION, checkout@v4 - regenerate patches/legacy_tbb_abi.diff and patches/mingw_gnu_cmake.diff * assert one tbb runtime rather than none from the stub CI disproved the premise of the import assertion added in the previous commit. On both windows-latest configurations -- the Rtools45 oneTBB and the bundled build -- the downstream library takes its entire TBB surface (13 tbb::detail::r1:: entry points, including isolate_within_arena and observe) from tbb.dll, and imports nothing at all from RcppParallel.dll. R CMD SHLIB links RcppParallel.dll against an export list generated from the package's own objects, so the TBB entry points pulled out of the static library are never re-exported; '-ltbb' is therefore load-bearing, not a fallback. So assert the invariant that does hold and does matter: the TBB surface must come from exactly one module. A library split across RcppParallel.dll and the stub would register observers with one scheduler while its tasks ran in the other, which is silent, unlike the link error this replaced. Two further defects in the check itself: - an objdump that cannot read the file exited non-zero having printed no import table, which parsed as 'no imports' and passed vacuously. This is what made the windows-11-arm job green: Sys.which() finds the runner's x86_64 objdump, which cannot read an aarch64 PE. - the export table follows the import tables and lists the library's own inlined tbb::detail::d1:: instantiations. Left in, it was absorbed into the last 'DLL Name:' block and credited that library with TBB imports it never had -- harmless only because tbb.dll happened to be listed last. Also correct the tbbLdFlags() comment, which described RcppParallel.dll as re-exporting an incidental part of the runtime.
1 parent a0b8876 commit 69dac53

11 files changed

Lines changed: 214 additions & 80 deletions

File tree

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

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ if (!RcppParallel:::TBB_ENABLED)
7575
makevars <- c(
7676
"CXX_STD = CXX17",
7777
sprintf("PKG_CPPFLAGS = -I\"%s\"", system.file("include", package = "RcppParallel")),
78-
"PKG_CPPFLAGS += $(shell \"${R_HOME}/bin/Rscript\" -e \"RcppParallel::CxxFlags()\" | tail -n 1)",
78+
"PKG_CPPFLAGS += $(shell \"${R_HOME}/bin${R_ARCH_BIN}/Rscript\" -e \"RcppParallel::CxxFlags()\" | tail -n 1)",
7979
"PKG_LIBS += $(shell \"${R_HOME}/bin${R_ARCH_BIN}/Rscript\" -e \"RcppParallel::RcppParallelLibs()\" | tail -n 1)"
8080
)
8181

@@ -95,8 +95,80 @@ status <- system(paste(shQuote(file.path(R.home("bin"), "R")), "CMD SHLIB check.
9595
if (status != 0L)
9696
stop("downstream translation unit failed to build")
9797

98+
dllName <- paste0("check", .Platform$dynlib.ext)
99+
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))
114+
115+
status <- attr(output, "status")
116+
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)]
124+
125+
# 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)
129+
130+
if (!nzchar(objdump)) {
131+
132+
writeLines("** objdump not found; skipping the import table check")
133+
134+
} else if (!readable) {
135+
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)
142+
143+
} else {
144+
145+
imported <- sub(".*DLL Name:[[:space:]]*", "", output[starts])
146+
ends <- c(starts[-1L], length(output) + 1L)
147+
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+
}
155+
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)"))
159+
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")
165+
166+
}
167+
168+
}
169+
98170
# loading also proves any load-time dependency on the tbb stub resolves
99-
dll <- dyn.load(paste0("check", .Platform$dynlib.ext))
171+
dll <- dyn.load(dllName)
100172
on.exit(dyn.unload(dll[["path"]]), add = TRUE)
101173

102174
result <- .Call("tbb_downstream_check")

.github/workflows/R-CMD-check.yaml

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
3232
R_KEEP_PKG_SOURCE: yes
3333
steps:
34-
- uses: actions/checkout@v3
34+
- uses: actions/checkout@v4
3535

3636
- uses: r-lib/actions/setup-r@v2
3737
with:
@@ -50,14 +50,21 @@ jobs:
5050
# depends on, and it is not covered by R CMD check: RcppParallel can install
5151
# perfectly well while leaving downstream packages unable to build.
5252
downstream:
53-
runs-on: windows-latest
53+
runs-on: ${{ matrix.config.os }}
5454

55-
name: downstream (${{ matrix.r }})
55+
name: downstream (${{ matrix.config.os }}, ${{ matrix.config.r }})
5656

5757
strategy:
5858
fail-fast: false
5959
matrix:
60-
r: ['4.2', 'release']
60+
config:
61+
# Rtools42 has no oneTBB, so this one builds the bundled copy
62+
- {os: windows-latest, r: '4.2'}
63+
- {os: windows-latest, r: 'release'}
64+
# RcppParallelLibs() changed for every Windows platform, and arm64
65+
# has both its own vendored patch and itt_notify compiled out, so
66+
# its export surface is the one most likely to differ
67+
- {os: windows-11-arm, r: 'release'}
6168

6269
env:
6370
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
@@ -70,11 +77,11 @@ jobs:
7077
shell: bash
7178

7279
steps:
73-
- uses: actions/checkout@v3
80+
- uses: actions/checkout@v4
7481

7582
- uses: r-lib/actions/setup-r@v2
7683
with:
77-
r-version: ${{ matrix.r }}
84+
r-version: ${{ matrix.config.r }}
7885
use-public-rspm: true
7986

8087
- name: install RcppParallel

.github/workflows/rstan.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949
shell: bash
5050

5151
steps:
52-
- uses: actions/checkout@v3
52+
- uses: actions/checkout@v4
5353

5454
- uses: r-lib/actions/setup-r@v2
5555
with:
@@ -83,9 +83,11 @@ jobs:
8383
run: |
8484
Rscript -e 'install.packages(c("StanHeaders","rstan"), type = "source", dependencies = FALSE, INSTALL_opts = "--no-multiarch")'
8585
86+
# compare against this checkout's DESCRIPTION rather than looking for a
87+
# '.9000' suffix, so this keeps working across development version bumps
8688
- name: check RcppParallel is still the one we built
8789
run: |
88-
Rscript -e 'v <- as.character(packageVersion("RcppParallel")); writeLines(paste("RcppParallel", v)); if (!grepl("[.]9000$", v)) stop("RcppParallel was replaced by a released build; the rstan build did not test this branch")'
90+
Rscript -e 'installed <- packageVersion("RcppParallel"); expected <- package_version(read.dcf("DESCRIPTION")[1L, "Version"]); writeLines(sprintf("RcppParallel %s (expected %s)", installed, expected)); if (installed != expected) stop("RcppParallel was replaced by a different build; the rstan build did not test this branch")'
8991
9092
- name: load rstan
9193
run: |

NEWS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
2017, whose headers downstream packages cannot build against: StanHeaders
2121
uses `tbb::this_task_arena::isolate`, which that release still gates behind
2222
`TBB_PREVIEW_TASK_ISOLATION` and does not export from its library. As a
23-
result, rstan could no longer be built on R 4.2 for Windows.
23+
result, rstan could no longer be built on R 4.2 for Windows. A TBB
24+
configured explicitly via `TBB_LIB` is still used as-is, whatever its
25+
vintage.
2426

2527
* On Windows, `RcppParallel::RcppParallelLibs()` once again offers the TBB
2628
stub library, so that packages taking all of their linker flags from it

R/tbb.R

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,20 @@ tbbLdFlags <- function() {
9292

9393
ldFlags <- sprintf("-L%s -lRcppParallel", asBuildPath(libPath))
9494

95-
# also offer the stub library. RcppParallel.dll re-exports whichever TBB
96-
# objects happened to be pulled out of the static library when it was
97-
# linked, which is an incidental export surface rather than a declared
98-
# one; the stub exports the runtime wholesale, so anything missing from
99-
# RcppParallel.dll can still be resolved. this comes last deliberately:
100-
# the linker satisfies symbols in order, so RcppParallel's own exports
101-
# still win, and no dependency on the stub is recorded unless something
102-
# actually needs it
95+
# also offer the stub library, which exports the TBB runtime wholesale.
96+
# this is not just a safety net: R CMD SHLIB links RcppParallel.dll
97+
# against an export list generated from our own objects, so the TBB
98+
# entry points pulled in from the static library are not re-exported,
99+
# and a downstream package linking '-lRcppParallel' alone resolves none
100+
# of them (which is what broke rstan). the stub is what makes those
101+
# symbols reachable at all.
102+
#
103+
# it still comes last, so that anything RcppParallel.dll does export
104+
# wins, and so no import of the stub is recorded unless something needs
105+
# it. that matters because the stub carries its own copy of the oneTBB
106+
# runtime: a package split across both would register observers with one
107+
# scheduler while its tasks ran in the other.
108+
# .github/scripts/tbb-downstream-check.R asserts that does not happen
103109
tbbPath <- archSystemFile("lib")
104110
if (file.exists(file.path(tbbPath, "tbb.dll")))
105111
ldFlags <- paste(ldFlags, sprintf("-L%s -ltbb", asBuildPath(tbbPath)))

patches/legacy_tbb_abi.diff

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,6 @@
170170
+} // namespace tbb
171171
+
172172
+#endif /* _WIN32 */
173-
\ No newline at end of file
174173
--- a/src/tbb/src/tbb/def/lin32-tbb.def
175174
+++ b/src/tbb/src/tbb/def/lin32-tbb.def
176175
@@ -17,6 +17,26 @@

patches/mingw_gnu_cmake.diff

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,32 @@
11
diff --git a/src/tbb/cmake/compilers/GNU.cmake b/src/tbb/cmake/compilers/GNU.cmake
2-
index cf6d8bdb..bc86acc1 100644
2+
index cf6d8bdb..8f9f0b59 100644
33
--- a/src/tbb/cmake/compilers/GNU.cmake
44
+++ b/src/tbb/cmake/compilers/GNU.cmake
5-
@@ -47,19 +47,42 @@ endif()
5+
@@ -47,19 +47,44 @@ endif()
66
# >=2.31.1). Capturing the output in CMake can be done like below. The version
77
# information is written to either stdout or stderr. To not make any
88
# assumptions, both are captured.
99
+
10-
+# The null device is spelled differently on Windows. A mingw build driven by
11-
+# a native Windows CMake (rather than cross-compiled from a POSIX host) has no
12-
+# '/dev/null', and the compiler would fail with "no input files".
13-
+if (WIN32)
14-
+ set(_tbb_null_device "NUL")
15-
+else()
16-
+ set(_tbb_null_device "/dev/null")
17-
+endif()
10+
+# Probe with a real (empty) source file rather than the null device. A mingw
11+
+# build driven by a native Windows CMake (rather than cross-compiled from a
12+
+# POSIX host) has no '/dev/null', and the compiler would fail with "no input
13+
+# files"; an actual file avoids having to spell the null device per platform.
14+
+set(_tbb_asm_probe_source ${CMAKE_BINARY_DIR}/CMakeFiles/tbb_asm_probe.c)
15+
+set(_tbb_asm_probe_object ${CMAKE_BINARY_DIR}/CMakeFiles/tbb_asm_probe.o)
16+
+file(WRITE ${_tbb_asm_probe_source} "")
1817
+
1918
execute_process(
2019
- COMMAND ${CMAKE_COMMAND} -E env "LANG=C" ${CMAKE_CXX_COMPILER} -xc -c /dev/null -Wa,-v -o/dev/null
21-
+ COMMAND ${CMAKE_COMMAND} -E env "LANG=C" ${CMAKE_CXX_COMPILER} -xc -c ${_tbb_null_device} -Wa,-v -o${_tbb_null_device}
20+
+ COMMAND ${CMAKE_COMMAND} -E env "LANG=C" ${CMAKE_CXX_COMPILER} -xc -c ${_tbb_asm_probe_source} -Wa,-v -o ${_tbb_asm_probe_object}
2221
OUTPUT_VARIABLE ASSEMBLER_VERSION_LINE_OUT
2322
ERROR_VARIABLE ASSEMBLER_VERSION_LINE_ERR
2423
OUTPUT_STRIP_TRAILING_WHITESPACE
2524
ERROR_STRIP_TRAILING_WHITESPACE
2625
)
27-
+unset(_tbb_null_device)
26+
+
27+
+file(REMOVE ${_tbb_asm_probe_source} ${_tbb_asm_probe_object})
28+
+unset(_tbb_asm_probe_source)
29+
+unset(_tbb_asm_probe_object)
2830
set(ASSEMBLER_VERSION_LINE ${ASSEMBLER_VERSION_LINE_OUT}${ASSEMBLER_VERSION_LINE_ERR})
2931
string(REGEX REPLACE ".*GNU assembler version ([0-9]+)\\.([0-9]+).*" "\\1" _tbb_gnu_asm_major_version "${ASSEMBLER_VERSION_LINE}")
3032
string(REGEX REPLACE ".*GNU assembler version ([0-9]+)\\.([0-9]+).*" "\\2" _tbb_gnu_asm_minor_version "${ASSEMBLER_VERSION_LINE}")
@@ -46,7 +48,7 @@ index cf6d8bdb..bc86acc1 100644
4648
message(TRACE "Extracted GNU assembler version: major=${_tbb_gnu_asm_major_version} minor=${_tbb_gnu_asm_minor_version}")
4749

4850
math(EXPR _tbb_gnu_asm_version_number "${_tbb_gnu_asm_major_version} * 1000 + ${_tbb_gnu_asm_minor_version}")
49-
@@ -81,7 +104,14 @@ endif()
51+
@@ -81,7 +106,14 @@ endif()
5052
if (NOT ${CMAKE_CXX_COMPILER_ID} STREQUAL Intel)
5153
# gcc 6.0 and later have -flifetime-dse option that controls elimination of stores done outside the object lifetime
5254
set(TBB_DSE_FLAG $<$<NOT:$<VERSION_LESS:${CMAKE_CXX_COMPILER_VERSION},6.0>>:-flifetime-dse=1>)

src/install.libs.R

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,34 @@
3939

4040
# using bundled TBB
4141
tbbSrc <- "tbb/build/lib_release"
42-
tbbLibs <- list.files(
43-
path = tbbSrc,
44-
pattern = shlibPattern,
45-
full.names = TRUE
46-
)
4742

48-
logTbbLibraries(tbbLibs, tbbSrc)
49-
for (tbbLib in tbbLibs) {
50-
system2("cp", c("-P", shQuote(tbbLib), shQuote(tbbDest)))
51-
}
43+
# on Windows the bundled TBB was built as a static library, and is
44+
# already linked into (and re-exported from) RcppParallel.dll; say so
45+
# explicitly, rather than reporting that no runtime libraries were
46+
# found -- there are none to find, and that reads like a failure
47+
if (.Platform$OS.type == "windows") {
48+
49+
writeLines("** tbb is statically linked into RcppParallel.dll; no runtime libraries to install")
50+
51+
} else {
5252

53-
# restore the versioned library layout shipped by RcppParallel 5.1.11
54-
# and earlier (a real 'libtbb.so.2' plus a 'libtbb.so' symlink)
55-
if (Sys.info()[["sysname"]] == "Linux")
56-
versionBundledTbbLibraries(tbbDest)
53+
tbbLibs <- list.files(
54+
path = tbbSrc,
55+
pattern = shlibPattern,
56+
full.names = TRUE
57+
)
58+
59+
logTbbLibraries(tbbLibs, tbbSrc)
60+
for (tbbLib in tbbLibs) {
61+
system2("cp", c("-P", shQuote(tbbLib), shQuote(tbbDest)))
62+
}
63+
64+
# restore the versioned library layout shipped by RcppParallel 5.1.11
65+
# and earlier (a real 'libtbb.so.2' plus a 'libtbb.so' symlink)
66+
if (Sys.info()[["sysname"]] == "Linux")
67+
versionBundledTbbLibraries(tbbDest)
68+
69+
}
5770

5871
} else {
5972

src/tbb/cmake/compilers/GNU.cmake

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,23 +48,25 @@ endif()
4848
# information is written to either stdout or stderr. To not make any
4949
# assumptions, both are captured.
5050

51-
# The null device is spelled differently on Windows. A mingw build driven by
52-
# a native Windows CMake (rather than cross-compiled from a POSIX host) has no
53-
# '/dev/null', and the compiler would fail with "no input files".
54-
if (WIN32)
55-
set(_tbb_null_device "NUL")
56-
else()
57-
set(_tbb_null_device "/dev/null")
58-
endif()
51+
# Probe with a real (empty) source file rather than the null device. A mingw
52+
# build driven by a native Windows CMake (rather than cross-compiled from a
53+
# POSIX host) has no '/dev/null', and the compiler would fail with "no input
54+
# files"; an actual file avoids having to spell the null device per platform.
55+
set(_tbb_asm_probe_source ${CMAKE_BINARY_DIR}/CMakeFiles/tbb_asm_probe.c)
56+
set(_tbb_asm_probe_object ${CMAKE_BINARY_DIR}/CMakeFiles/tbb_asm_probe.o)
57+
file(WRITE ${_tbb_asm_probe_source} "")
5958

6059
execute_process(
61-
COMMAND ${CMAKE_COMMAND} -E env "LANG=C" ${CMAKE_CXX_COMPILER} -xc -c ${_tbb_null_device} -Wa,-v -o${_tbb_null_device}
60+
COMMAND ${CMAKE_COMMAND} -E env "LANG=C" ${CMAKE_CXX_COMPILER} -xc -c ${_tbb_asm_probe_source} -Wa,-v -o ${_tbb_asm_probe_object}
6261
OUTPUT_VARIABLE ASSEMBLER_VERSION_LINE_OUT
6362
ERROR_VARIABLE ASSEMBLER_VERSION_LINE_ERR
6463
OUTPUT_STRIP_TRAILING_WHITESPACE
6564
ERROR_STRIP_TRAILING_WHITESPACE
6665
)
67-
unset(_tbb_null_device)
66+
67+
file(REMOVE ${_tbb_asm_probe_source} ${_tbb_asm_probe_object})
68+
unset(_tbb_asm_probe_source)
69+
unset(_tbb_asm_probe_object)
6870
set(ASSEMBLER_VERSION_LINE ${ASSEMBLER_VERSION_LINE_OUT}${ASSEMBLER_VERSION_LINE_ERR})
6971
string(REGEX REPLACE ".*GNU assembler version ([0-9]+)\\.([0-9]+).*" "\\1" _tbb_gnu_asm_major_version "${ASSEMBLER_VERSION_LINE}")
7072
string(REGEX REPLACE ".*GNU assembler version ([0-9]+)\\.([0-9]+).*" "\\2" _tbb_gnu_asm_minor_version "${ASSEMBLER_VERSION_LINE}")

src/tbb/src/tbb/observer_proxy.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,4 +336,4 @@ void __TBB_EXPORTED_FUNC task_scheduler_observer_v3::observe( bool enable ) {
336336
} // namespace internal
337337
} // namespace tbb
338338

339-
#endif /* _WIN32 */
339+
#endif /* _WIN32 */

0 commit comments

Comments
 (0)