Skip to content

Commit c075ab7

Browse files
committed
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 05db507 commit c075ab7

2 files changed

Lines changed: 67 additions & 33 deletions

File tree

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

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -97,37 +97,72 @@ if (status != 0L)
9797

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

100-
# Check where the TBB symbols actually came from. RcppParallelLibs() offers
101-
# the 'tbb.dll' stub after '-lRcppParallel', so anything RcppParallel.dll
102-
# failed to re-export still links -- but the stub carries its own copy of the
103-
# oneTBB runtime, so binding to it means running against a second, independent
104-
# scheduler. That is a silent failure, unlike the link error it replaced, so
105-
# assert here that the whole TBB surface resolved from RcppParallel.dll.
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.
106109
if (.Platform$OS.type == "windows") {
107110

108111
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+
109130
if (!nzchar(objdump)) {
110131

111132
writeLines("** objdump not found; skipping the import table check")
112133

113-
} else {
134+
} else if (!readable) {
114135

115-
output <- system2(objdump, c("-p", shQuote(dllName)), stdout = TRUE, stderr = TRUE)
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)
116142

117-
# each imported library opens a 'DLL Name:' block listing the symbols
118-
# taken from it, and runs until the next such block
119-
starts <- grep("DLL Name:", output, fixed = TRUE)
120-
imported <- sub(".*DLL Name:[[:space:]]*", "", output[starts])
121-
writeLines(sprintf("imports: %s", paste(imported, collapse = ", ")))
143+
} else {
122144

123-
index <- which(tolower(imported) == "tbb.dll")
124-
if (length(index)) {
125-
ends <- c(starts[-1L], length(output) + 1L)
126-
writeLines(output[seq(starts[[index]], ends[[index]] - 1L)])
127-
stop("the downstream library imports the above from the tbb.dll stub; ",
128-
"these should have resolved from RcppParallel.dll instead")
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]])
129154
}
130155

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+
131166
}
132167

133168
}

R/tbb.R

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -92,21 +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 import of 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.
103102
#
104-
# that ordering matters for more than tidiness. the stub links its own
105-
# copy of the oneTBB runtime, so a package resolving some symbols here
106-
# and the rest against RcppParallel.dll would straddle two independent
107-
# schedulers -- an observer registered with one would never fire for
108-
# arenas owned by the other. .github/scripts/tbb-downstream-check.R
109-
# asserts that nothing actually lands on the stub
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
110109
tbbPath <- archSystemFile("lib")
111110
if (file.exists(file.path(tbbPath, "tbb.dll")))
112111
ldFlags <- paste(ldFlags, sprintf("-L%s -ltbb", asBuildPath(tbbPath)))

0 commit comments

Comments
 (0)