Skip to content

Commit 222d02e

Browse files
authored
build the bundled onetbb on windows when rtools has no onetbb (#269)
* link downstream packages against the tbb stub on windows tbbLdFlags() returned only '-L<libs> -lRcppParallel' on Windows, leaving downstream packages to resolve TBB symbols against whichever objects happened to be pulled out of the static TBB when RcppParallel.dll was linked. That export surface is incidental rather than declared, and packages taking all of their PKG_LIBS from RcppParallelLibs() (e.g. rstan) have no other way to ask for TBB. Offer the stub library as well, after '-lRcppParallel' so the package's own exports still take precedence and no dependency on the stub is recorded unless something actually needs it. RcppParallel 5.1.11 and earlier appended '-ltbb -ltbbmalloc' here for the same reason. * build the bundled onetbb on windows when rtools has no onetbb Rtools42 ships Intel TBB 2017, and configure adopted it purely on the presence of a libtbb* archive. Downstream packages then compiled against those headers, which is not viable: StanHeaders uses tbb::this_task_arena::isolate unconditionally, TBB 2017 still gates it behind TBB_PREVIEW_TASK_ISOLATION, and its library doesn't export isolate_within_arena, so forcing the declaration into view wouldn't link either. rstan 2.32.7 consequently stopped building on R 4.2 / Windows. Only adopt the Rtools TBB when it is oneTBB, and otherwise build the bundled copy from sources, as we already do elsewhere. Rtools42 provides cmake 3.24.1 in the same directory as gcc, so this needs no new tooling. It is built as a static library so that RcppParallel.dll takes exactly the shape it does with an Rtools oneTBB -- oneTBB marks its entry points __declspec(dllexport) on mingw, so the whole-archive re-export of tbbmalloc carries over unchanged. Toolchains that can build neither (e.g. Rtools40, which has no TBB and no cmake) still fall back to tinythread: a missing or too-old cmake is only an error off Windows, where the bundled build is the sole option. Also add CI covering this. R CMD check gains an R 4.2 Windows job, and a new 'downstream' job compiles and loads a translation unit against the flags RcppParallel advertises, exercising both this_task_arena::isolate and task_scheduler_observer. R CMD check alone would not have caught the rstan breakage: RcppParallel installed perfectly well throughout. * add news entries for the windows tbb changes * run the downstream ci job under bash The default shell on Windows runners is PowerShell, where 'r' is an alias for Invoke-History; 'R CMD INSTALL' was parsed as 'Invoke-History CMD INSTALL' and failed before the job did anything useful. * patch onetbb's assembler probe for native windows builds oneTBB probes the GNU assembler version by compiling /dev/null, which does not exist when a native Windows cmake drives mingw gcc. The compiler fails with 'no input files', the version regex doesn't match, and math() then chokes on the error text -- taking the whole configure down: CMake Error at cmake/compilers/GNU.cmake:65 (math): math cannot parse the expression: "g++.exe: error: /dev/null: No such file or directory ... * 1000 + ..." MXE doesn't hit this because it cross-builds oneTBB from a POSIX host. Use NUL on Windows, and treat an unparseable probe as an assembler too old for waitpkg rather than a fatal error -- it only guards an optimization, and Rtools42's GCC 10.4 is below the GCC 11 floor for -mwaitpkg regardless. * don't pass -fstack-clash-protection to mingw when building onetbb GCC 10.4 aborts compiling oneTBB's task_dispatcher.h with the flag on: internal compiler error: in seh_emit_stackalloc, at config/i386/winnt.c:1056 370 | do_throw_noexcept([] { throw; }); a known GCC defect on mingw's SEH targets. It is a hardening flag rather than a correctness one, so leave it off there. Also fold the earlier /dev/null fix into a single patches/mingw_gnu_cmake.diff, as both make oneTBB's GNU.cmake usable for a native mingw build. * don't redeclare the legacy observer classes in the tbb stub The bundled oneTBB carries a local 'provided for backwards compatibility' block declaring tbb::internal::task_scheduler_observer_v3 and tbb::interface6::task_scheduler_observer (added in c276b03; not upstream). tbb-compat.cpp declares them too, which was fine while the stub was only ever built against the pristine headers Rtools ships -- but building the bundled oneTBB on Windows makes the two collide: tbb-compat.cpp:25:7: error: redefinition of 'class tbb::internal::task_scheduler_observer_v3' Have the bundled header advertise what it provides, and skip the stub's own declarations when it does. Either way the stub still exports the out-of-line observe(), which is the whole reason it exists. Also spell that definition __TBB_EXPORTED_METHOD, matching its declaration here and in the old TBB ABI; the two attributes are both no-ops on the Windows targets we build for, so this is consistency rather than a fix. * keep the legacy observe definition out of the bundled tbb on windows The bundled oneTBB carries a local (non-upstream) definition of tbb::internal::task_scheduler_observer_v3::observe in observer_proxy.cpp, alongside the declarations in task_scheduler_observer.h. Once the bundled copy is actually built on Windows, libtbb12.a and tbb-compat.o both define it and the stub fails to link: libtbb12.a(observer_proxy.cpp.obj): multiple definition of 'tbb::internal::task_scheduler_observer_v3::observe(bool)' The stub has to own that definition on Windows, because R CMD SHLIB builds the DLL's export list from the objects it compiles -- a definition sitting in the archive would never make it into tmp.def. So leave it out of the library there; other platforms keep it, where it serves binaries built against RcppParallel 5.x. Also stop trusting R CMD SHLIB's exit code: it reported success through the link failure above, so the install completed and shipped a package with no tbb.dll -- precisely the failure mode that packages linking '-ltbb' only hit at load time. Check for the artifact, and for the copy, instead. * capture the legacy tbb abi modifications as a patch The bundled oneTBB carries local declarations and a definition of the old ABI's task_scheduler_observer, added in c276b03 so that binaries built against RcppParallel 5.x keep working. Neither had a file in patches/, and tools/tbb/update-tbb.R wipes src/tbb wholesale -- so the next oneTBB bump would have dropped them silently, breaking the Windows tbb.dll stub in a way that is hard to trace back. Verified by applying the patch to a pristine oneTBB 2022.0.0 tree and confirming the result matches what we ship. * add a manually-triggered rstan build workflow The 'downstream' job stands in for rstan in the normal CI run: it compiles a translation unit using the two TBB APIs StanHeaders needs, in seconds rather than the best part of an hour. This runs the real thing, on demand, for when that's worth waiting for -- before a release, or after touching the Windows TBB setup. workflow_dispatch inputs choose the R version (4.2 by default, the one whose Rtools has no oneTBB), the runner, and whether to additionally compile and fit a tiny Stan model. The last exercises the TBB runtime at run time, not just the headers and link line, so it's opt-in. * correct rstan's dependency list and pin the package under test Drop V8 and withr: rstan 2.32 replaced V8 with QuickJSR, and V8 failing to install on an old toolchain would have failed the job for reasons that have nothing to do with RcppParallel. The list now mirrors rstan's Imports plus LinkingTo. Install StanHeaders and rstan with dependencies = FALSE, and assert afterwards that RcppParallel is still the .9000 build from this branch -- otherwise dependency resolution could quietly replace it with CRAN's, and the run would report on the released package instead of this one. * capture the functional oneTBB modifications as patches Comparing src/tbb against a pristine oneTBB 2022.0.0 turns up 41 modified files. Most are workarounds for over-zealous CRAN checks -- commented-out '#pragma warning' / '#pragma GCC diagnostic' directives and the like -- and are not worth carrying. Seven are not: - src/tbb/def/{lin32,lin64,mac64}-tbb.def add tbb::internal::task_scheduler_observer_v3::observe to the export list, under a comment reading 'Needed by rstan', along with std:: exception typeinfo and destructors for backwards compatibility. These are the Linux/macOS counterpart of the Windows tbb.dll stub, so losing them would break rstan on exactly the platforms where it currently works. - The four CMakeLists.txt files drop VERSION / SOVERSION, so the build emits unversioned libraries. install.libs.R's versionBundledTbbLibraries() then re-creates the 'libtbb.so.2' layout (#260); the two have to stay in step. Both patches verified by applying them to a pristine tree and confirming every touched file matches what we ship. * note the tbb stub install failure fix in news
1 parent 39afcb3 commit 222d02e

15 files changed

Lines changed: 873 additions & 65 deletions

File tree

.github/scripts/rstan-fit-check.R

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Compile and fit a trivial Stan model. Building rstan proves the headers and
2+
# link line are right; actually running a model additionally exercises the TBB
3+
# runtime that RcppParallel loaded -- including the reduce_sum / task isolation
4+
# path that the old Rtools42 TBB could not support.
5+
6+
library(rstan)
7+
8+
code <- "
9+
data {
10+
int<lower=0> N;
11+
vector[N] y;
12+
}
13+
parameters {
14+
real mu;
15+
}
16+
model {
17+
y ~ normal(mu, 1);
18+
}
19+
"
20+
21+
set.seed(42)
22+
data <- list(N = 20L, y = rnorm(20, mean = 3))
23+
24+
fit <- stan(
25+
model_code = code,
26+
data = data,
27+
chains = 1L,
28+
iter = 200L,
29+
refresh = 0L
30+
)
31+
32+
estimate <- mean(rstan::extract(fit, "mu")[["mu"]])
33+
writeLines(sprintf("posterior mean of mu: %.3f (data mean %.3f)", estimate, mean(data$y)))
34+
35+
# a wildly wrong answer would mean the model ran but the sampler is broken;
36+
# with 20 observations and unit scale this is a very loose bound
37+
if (!is.finite(estimate) || abs(estimate - mean(data$y)) > 1)
38+
stop("posterior mean is implausible; the fit did not work correctly")
39+
40+
writeLines("** rstan fit check passed")
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Build and load a translation unit exercising the parts of the TBB API that
2+
# downstream packages depend on, using only the flags RcppParallel advertises.
3+
#
4+
# StanHeaders -- and so rstan -- needs both of the pieces used here:
5+
# tbb::this_task_arena::isolate, to keep its thread-local AD tape from being
6+
# modified by a task from another arena, and tbb::task_scheduler_observer, to
7+
# install that tape on each worker. Rtools42's TBB 2017 provides neither in a
8+
# usable form, which is what broke rstan on R 4.2 (isolate is gated behind
9+
# TBB_PREVIEW_TASK_ISOLATION, and isolate_within_arena isn't in the library at
10+
# all), so this stands in for a full rstan build.
11+
12+
code <- '
13+
#include <tbb/blocked_range.h>
14+
#include <tbb/parallel_for.h>
15+
#include <tbb/task_arena.h>
16+
#include <tbb/task_scheduler_observer.h>
17+
18+
#include <atomic>
19+
#include <cstddef>
20+
21+
// R.h remaps names like \'length\' onto Rf_ equivalents by default, which
22+
// collides with the standard library; keep it last, and unremapped
23+
#define R_NO_REMAP
24+
#include <R.h>
25+
#include <Rinternals.h>
26+
27+
namespace {
28+
29+
// mirrors StanHeaders\' ad_tape_observer, which is what pulls the
30+
// task_scheduler_observer entry points into the link
31+
struct observer : public tbb::task_scheduler_observer {
32+
observer() : tbb::task_scheduler_observer() { observe(true); }
33+
void on_scheduler_entry(bool) override {}
34+
void on_scheduler_exit(bool) override {}
35+
};
36+
37+
} // end anonymous namespace
38+
39+
extern "C" SEXP tbb_downstream_check(void) {
40+
41+
observer obs;
42+
std::atomic<int> total(0);
43+
44+
// mirrors StanHeaders\' use of task isolation in reduce_sum / map_rect
45+
tbb::this_task_arena::isolate([&] {
46+
tbb::parallel_for(
47+
tbb::blocked_range<std::size_t>(0, 1024),
48+
[&](const tbb::blocked_range<std::size_t>& range) {
49+
total += static_cast<int>(range.end() - range.begin());
50+
}
51+
);
52+
});
53+
54+
return Rf_ScalarInteger(total.load());
55+
56+
}
57+
'
58+
59+
# report how RcppParallel is configured, so a failure here can be read
60+
# against the provenance logged during installation
61+
writeLines(c(
62+
sprintf("TBB_ENABLED : %s", RcppParallel:::TBB_ENABLED),
63+
sprintf("TBB_LIB : '%s'", RcppParallel:::TBB_LIB),
64+
sprintf("TBB_INC : '%s'", RcppParallel:::TBB_INC),
65+
sprintf("CxxFlags() : %s", RcppParallel:::tbbCxxFlags()),
66+
sprintf("LdFlags() : %s", RcppParallel:::tbbLdFlags())
67+
))
68+
69+
if (!RcppParallel:::TBB_ENABLED)
70+
stop("RcppParallel was installed without a tbb backend")
71+
72+
# mirror rstan's src/Makevars.win, which takes all of its TBB configuration
73+
# from RcppParallel: its compiler flags via CxxFlags(), its linker flags via
74+
# RcppParallelLibs(), and the include path itself via 'LinkingTo'
75+
makevars <- c(
76+
"CXX_STD = CXX17",
77+
sprintf("PKG_CPPFLAGS = -I\"%s\"", system.file("include", package = "RcppParallel")),
78+
"PKG_CPPFLAGS += $(shell \"${R_HOME}/bin/Rscript\" -e \"RcppParallel::CxxFlags()\" | tail -n 1)",
79+
"PKG_LIBS += $(shell \"${R_HOME}/bin${R_ARCH_BIN}/Rscript\" -e \"RcppParallel::RcppParallelLibs()\" | tail -n 1)"
80+
)
81+
82+
dir <- tempfile("tbb-downstream-")
83+
dir.create(dir, recursive = TRUE)
84+
owd <- setwd(dir)
85+
on.exit(setwd(owd), add = TRUE)
86+
87+
writeLines(code, "check.cpp")
88+
89+
# deliberately not named 'Makevars': R CMD SHLIB reads a Makevars in the
90+
# working directory as well as R_MAKEVARS_USER, and would apply both. going
91+
# through R_MAKEVARS_USER also keeps any personal ~/.R/Makevars out of the way
92+
writeLines(makevars, "makevars-downstream")
93+
Sys.setenv(R_MAKEVARS_USER = file.path(dir, "makevars-downstream"))
94+
status <- system(paste(shQuote(file.path(R.home("bin"), "R")), "CMD SHLIB check.cpp"))
95+
if (status != 0L)
96+
stop("downstream translation unit failed to build")
97+
98+
# loading also proves any load-time dependency on the tbb stub resolves
99+
dll <- dyn.load(paste0("check", .Platform$dynlib.ext))
100+
on.exit(dyn.unload(dll[["path"]]), add = TRUE)
101+
102+
result <- .Call("tbb_downstream_check")
103+
if (!identical(result, 1024L))
104+
stop("downstream check returned ", result, "; expected 1024")
105+
106+
writeLines("** downstream TBB check passed")

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ jobs:
2323
- {os: ubuntu-24.04-arm, r: 'release'}
2424
- {os: windows-latest, r: 'release'}
2525
- {os: windows-11-arm, r: 'release'}
26+
# Rtools42 provides only a pre-oneTBB copy of TBB, so this is the
27+
# one configuration that builds the bundled oneTBB on Windows
28+
- {os: windows-latest, r: '4.2'}
2629

2730
env:
2831
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
@@ -32,6 +35,7 @@ jobs:
3235

3336
- uses: r-lib/actions/setup-r@v2
3437
with:
38+
r-version: ${{ matrix.config.r }}
3539
use-public-rspm: true
3640

3741
- uses: r-lib/actions/setup-r-dependencies@v2
@@ -40,3 +44,41 @@ jobs:
4044
needs: check
4145

4246
- uses: r-lib/actions/check-r-package@v2
47+
48+
# Verify that a package can still compile and link against the TBB API that
49+
# RcppParallel advertises. This is the contract rstan (via StanHeaders)
50+
# depends on, and it is not covered by R CMD check: RcppParallel can install
51+
# perfectly well while leaving downstream packages unable to build.
52+
downstream:
53+
runs-on: windows-latest
54+
55+
name: downstream (${{ matrix.r }})
56+
57+
strategy:
58+
fail-fast: false
59+
matrix:
60+
r: ['4.2', 'release']
61+
62+
env:
63+
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
64+
VERBOSE: 1
65+
66+
# the default shell on Windows runners is PowerShell, which aliases 'r'
67+
# to Invoke-History and so mangles 'R CMD INSTALL'
68+
defaults:
69+
run:
70+
shell: bash
71+
72+
steps:
73+
- uses: actions/checkout@v3
74+
75+
- uses: r-lib/actions/setup-r@v2
76+
with:
77+
r-version: ${{ matrix.r }}
78+
use-public-rspm: true
79+
80+
- name: install RcppParallel
81+
run: R CMD INSTALL --preclean .
82+
83+
- name: build against RcppParallel
84+
run: Rscript .github/scripts/tbb-downstream-check.R

.github/workflows/rstan.yaml

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Build rstan from source against this branch's RcppParallel.
2+
#
3+
# The 'downstream' job in R-CMD-check.yaml is a stand-in for this: it compiles a
4+
# small translation unit using the two TBB APIs StanHeaders needs. That catches
5+
# the breakage this workflow exists to rule out, in seconds rather than the best
6+
# part of an hour -- so this is deliberately manual, for when the real thing is
7+
# worth waiting for (before a release, or after touching the Windows TBB setup).
8+
on:
9+
workflow_dispatch:
10+
inputs:
11+
r-version:
12+
description: "R version (4.2 uses Rtools42, which has no oneTBB)"
13+
required: true
14+
default: "4.2"
15+
os:
16+
description: "Runner to build on"
17+
required: true
18+
default: "windows-latest"
19+
type: choice
20+
options:
21+
- windows-latest
22+
- macOS-latest
23+
- ubuntu-latest
24+
fit-model:
25+
description: "Also compile and fit a tiny Stan model (slow)"
26+
required: false
27+
default: false
28+
type: boolean
29+
30+
name: rstan
31+
32+
jobs:
33+
rstan:
34+
runs-on: ${{ inputs.os }}
35+
36+
name: rstan (${{ inputs.os }}, R ${{ inputs.r-version }})
37+
38+
# building rstan from source is slow, and slower still on Windows
39+
timeout-minutes: 120
40+
41+
env:
42+
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
43+
VERBOSE: 1
44+
# rstan's own guidance for building it from source
45+
MAKEFLAGS: -j2
46+
47+
defaults:
48+
run:
49+
shell: bash
50+
51+
steps:
52+
- uses: actions/checkout@v3
53+
54+
- uses: r-lib/actions/setup-r@v2
55+
with:
56+
r-version: ${{ inputs.r-version }}
57+
use-public-rspm: true
58+
59+
- name: install RcppParallel from this branch
60+
run: R CMD INSTALL --preclean .
61+
62+
- name: report how RcppParallel resolved TBB
63+
run: |
64+
Rscript -e 'writeLines(c(
65+
paste("TBB_ENABLED", RcppParallel:::TBB_ENABLED),
66+
paste("TBB_LIB ", RcppParallel:::TBB_LIB),
67+
paste("CxxFlags ", RcppParallel:::tbbCxxFlags()),
68+
paste("LdFlags ", RcppParallel:::tbbLdFlags())
69+
))'
70+
71+
# binaries are fine for everything except rstan and StanHeaders, which are
72+
# the packages that actually consume RcppParallel's flags. this is
73+
# rstan's Imports plus its LinkingTo, minus RcppParallel (installed above)
74+
# and StanHeaders (built from source below)
75+
- name: install rstan's dependencies
76+
run: |
77+
Rscript -e 'install.packages(c("Rcpp","RcppEigen","BH","inline","gridExtra","loo","pkgbuild","QuickJSR","ggplot2"))'
78+
79+
# dependencies = FALSE so that resolving StanHeaders/rstan can't quietly
80+
# pull CRAN's RcppParallel over the one we just built -- that would turn
81+
# this into a test of the released package instead
82+
- name: install StanHeaders and rstan from source
83+
run: |
84+
Rscript -e 'install.packages(c("StanHeaders","rstan"), type = "source", dependencies = FALSE, INSTALL_opts = "--no-multiarch")'
85+
86+
- name: check RcppParallel is still the one we built
87+
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")'
89+
90+
- name: load rstan
91+
run: |
92+
Rscript -e 'library(rstan); print(rstan::stan_version())'
93+
94+
- name: compile and fit a tiny model
95+
if: ${{ inputs.fit-model }}
96+
run: Rscript .github/scripts/rstan-fit-check.R

NEWS.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,25 @@
66
'random_access_iterator' in namespace 'std'". This affected CRAN's macOS
77
x86_64 machines, which pair Apple clang 14 with the macOS 11.3 SDK.
88

9+
* On Windows, RcppParallel now only uses the copy of TBB provided by Rtools
10+
when that copy is oneTBB; otherwise, the bundled oneTBB is built from
11+
sources, as it already is on other platforms. Rtools42 provides Intel TBB
12+
2017, whose headers downstream packages cannot build against: StanHeaders
13+
uses `tbb::this_task_arena::isolate`, which that release still gates behind
14+
`TBB_PREVIEW_TASK_ISOLATION` and does not export from its library. As a
15+
result, rstan could no longer be built on R 4.2 for Windows.
16+
17+
* On Windows, `RcppParallel::RcppParallelLibs()` once again offers the TBB
18+
stub library, so that packages taking all of their linker flags from it
19+
(e.g. rstan) can resolve TBB symbols which `RcppParallel.dll` does not
20+
itself re-export.
21+
22+
* Fixed an issue on Windows where a failure to build the TBB stub library
23+
could go unreported, since `R CMD SHLIB` has been seen to exit successfully
24+
even when the link failed. Installation completed, but shipped a package
25+
with no `tbb.dll` -- which packages linking `-ltbb` then discovered only
26+
when they failed to load. Installation now fails instead.
27+
928
* Fixed an issue where building the bundled oneTBB could fail when `CXX`
1029
(or `CC`) was configured with a leading compiler launcher such as `ccache`
1130
(e.g. `CXX = "ccache g++"`). The launcher is now forwarded to cmake via

R/tbb.R

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,25 @@ tbbLdFlags <- function() {
8787

8888
# on Windows, we statically link to oneTBB
8989
if (is_windows()) {
90-
90+
9191
libPath <- archSystemFile("libs")
9292

9393
ldFlags <- sprintf("-L%s -lRcppParallel", asBuildPath(libPath))
94+
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
103+
tbbPath <- archSystemFile("lib")
104+
if (file.exists(file.path(tbbPath, "tbb.dll")))
105+
ldFlags <- paste(ldFlags, sprintf("-L%s -ltbb", asBuildPath(tbbPath)))
106+
94107
return(ldFlags)
95-
108+
96109
}
97110

98111
# shortcut if TBB_LIB defined

0 commit comments

Comments
 (0)