Skip to content

Commit 4616f1c

Browse files
authored
Remove cpp11_should_unwind_protect altogether (#327)
* Revert #299 * Remove unwind protect global variable altogether * Add a descriptive test * Add FAQ bullets about `unwind_protect()` * Tweak internals vignette advice * NEWS bullet * Tweaks from code review
1 parent 0971415 commit 4616f1c

File tree

7 files changed

+298
-48
lines changed

7 files changed

+298
-48
lines changed

NEWS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# cpp11 (development version)
22

3+
* Nested calls to `cpp11::unwind_protect()` are no longer supported or
4+
encouraged. Previously, this was something that could be done for performance
5+
improvements, but ultimately this feature has proven to cause more problems
6+
than it is worth and is very hard to use safely. For more information, see the
7+
new `vignette("FAQ")` section titled "Should I call `cpp11::unwind_protect()`
8+
manually?" (#327).
9+
310
# cpp11 0.4.5
411

512
* On 2023-07-20, cpp11 was temporarily rolled back to 0.4.3 manually by CRAN due

cpp11test/R/cpp11.R

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,11 @@ rcpp_sum_dbl_accumulate_ <- function(x_sxp) {
223223
rcpp_grow_ <- function(n_sxp) {
224224
.Call(`_cpp11test_rcpp_grow_`, n_sxp)
225225
}
226+
227+
test_destruction_inner <- function() {
228+
invisible(.Call(`_cpp11test_test_destruction_inner`))
229+
}
230+
231+
test_destruction_outer <- function() {
232+
invisible(.Call(`_cpp11test_test_destruction_outer`))
233+
}

cpp11test/src/cpp11.cpp

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,10 +422,26 @@ extern "C" SEXP _cpp11test_rcpp_grow_(SEXP n_sxp) {
422422
return cpp11::as_sexp(rcpp_grow_(cpp11::as_cpp<cpp11::decay_t<SEXP>>(n_sxp)));
423423
END_CPP11
424424
}
425+
// test-protect-nested.cpp
426+
void test_destruction_inner();
427+
extern "C" SEXP _cpp11test_test_destruction_inner() {
428+
BEGIN_CPP11
429+
test_destruction_inner();
430+
return R_NilValue;
431+
END_CPP11
432+
}
433+
// test-protect-nested.cpp
434+
void test_destruction_outer();
435+
extern "C" SEXP _cpp11test_test_destruction_outer() {
436+
BEGIN_CPP11
437+
test_destruction_outer();
438+
return R_NilValue;
439+
END_CPP11
440+
}
425441

426442
extern "C" {
427443
/* .Call calls */
428-
extern SEXP run_testthat_tests(SEXP);
444+
extern SEXP run_testthat_tests(void *);
429445

430446
static const R_CallMethodDef CallEntries[] = {
431447
{"_cpp11test_col_sums", (DL_FUNC) &_cpp11test_col_sums, 1},
@@ -483,6 +499,8 @@ static const R_CallMethodDef CallEntries[] = {
483499
{"_cpp11test_sum_int_for2_", (DL_FUNC) &_cpp11test_sum_int_for2_, 1},
484500
{"_cpp11test_sum_int_for_", (DL_FUNC) &_cpp11test_sum_int_for_, 1},
485501
{"_cpp11test_sum_int_foreach_", (DL_FUNC) &_cpp11test_sum_int_foreach_, 1},
502+
{"_cpp11test_test_destruction_inner", (DL_FUNC) &_cpp11test_test_destruction_inner, 0},
503+
{"_cpp11test_test_destruction_outer", (DL_FUNC) &_cpp11test_test_destruction_outer, 0},
486504
{"_cpp11test_upper_bound", (DL_FUNC) &_cpp11test_upper_bound, 2},
487505
{"run_testthat_tests", (DL_FUNC) &run_testthat_tests, 1},
488506
{NULL, NULL, 0}

cpp11test/src/test-protect-nested.cpp

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#include "cpp11/function.hpp"
2+
#include "cpp11/protect.hpp"
3+
#include "testthat.h"
4+
5+
#ifdef HAS_UNWIND_PROTECT
6+
7+
/*
8+
* See https://github.com/r-lib/cpp11/pull/327 for full details.
9+
*
10+
* - `cpp11::package("cpp11test")["test_destruction_outer"]` uses
11+
* `unwind_protect()` to call R level `test_destruction_outer()` but no entry
12+
* macros are set up. Instead we are going to catch exceptions that get here
13+
* with `expect_error_as()`.
14+
*
15+
* - Call R level `test_destruction_outer()` to set up `BEGIN_CPP11` /
16+
* `END_CPP11` entry macros.
17+
*
18+
* - C++ `test_destruction_outer()` goes through `unwind_protect()` to call
19+
* the R level `test_destruction_inner()`.
20+
*
21+
* - R level `test_destruction_inner()` sets up its own `BEGIN_CPP11` /
22+
* `END_CPP11` entry macros.
23+
*
24+
* - C++ `test_destruction_inner()` goes through `unwind_protect()` to call
25+
* `Rf_error()` (i.e., we are nested within `unwind_protect()`s!).
26+
*
27+
* - `longjmp()` is caught from inner `unwind_protect()`, and an exception
28+
* is thrown which is caught by the inner entry macros, allowing us to run
29+
* the destructor of `x`, then we let R continue the unwind process.
30+
*
31+
* - This `longjmp()`s again and is caught by the outer `unwind_protect()`, an
32+
* exception is thrown which is caught by the outer entry macros, and we let
33+
* R continue the unwind process one more time.
34+
*
35+
* - This `longjmp()` is caught by `cpp11::package()`'s `unwind_protect()`,
36+
* an exception is thrown, and that is caught by `expect_error_as()`.
37+
*/
38+
39+
// Global variable to detect if the destructor has been run or not
40+
static bool destructed = false;
41+
42+
class HasDestructor {
43+
public:
44+
~HasDestructor();
45+
};
46+
47+
HasDestructor::~HasDestructor() {
48+
// Destructor has run!
49+
destructed = true;
50+
}
51+
52+
[[cpp11::register]] void test_destruction_inner() {
53+
// Expect that `x`'s destructor gets to run on the way out
54+
HasDestructor x{};
55+
cpp11::stop("oh no!");
56+
}
57+
58+
[[cpp11::register]] void test_destruction_outer() {
59+
const auto test_destruction_inner =
60+
cpp11::package("cpp11test")["test_destruction_inner"];
61+
test_destruction_inner();
62+
}
63+
64+
context("unwind_protect-nested-C++") {
65+
test_that(
66+
"nested `unwind_protect()` (with entry macros set up) will run destructors"
67+
"(#327)") {
68+
const auto fn = [&] {
69+
const auto test_destruction_outer =
70+
cpp11::package("cpp11test")["test_destruction_outer"];
71+
test_destruction_outer();
72+
};
73+
74+
expect_error_as(fn(), cpp11::unwind_exception);
75+
expect_true(destructed);
76+
77+
destructed = false;
78+
}
79+
}
80+
81+
#endif

inst/include/cpp11/protect.hpp

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -54,36 +54,6 @@ inline void set_option(SEXP name, SEXP value) {
5454
SETCAR(opt, value);
5555
}
5656

57-
inline Rboolean* setup_should_unwind_protect() {
58-
SEXP should_unwind_protect_sym = Rf_install("cpp11_should_unwind_protect");
59-
SEXP should_unwind_protect_sexp = Rf_GetOption1(should_unwind_protect_sym);
60-
61-
if (should_unwind_protect_sexp == R_NilValue) {
62-
// Allocate and initialize once, then let R manage it.
63-
// That makes this a shared global across all compilation units.
64-
should_unwind_protect_sexp = PROTECT(Rf_allocVector(LGLSXP, 1));
65-
SET_LOGICAL_ELT(should_unwind_protect_sexp, 0, TRUE);
66-
detail::set_option(should_unwind_protect_sym, should_unwind_protect_sexp);
67-
UNPROTECT(1);
68-
}
69-
70-
return reinterpret_cast<Rboolean*>(LOGICAL(should_unwind_protect_sexp));
71-
}
72-
73-
inline Rboolean* access_should_unwind_protect() {
74-
// Setup is run once per compilation unit, but all compilation units
75-
// share the same global option, so each compilation unit's static pointer
76-
// will point to the same object.
77-
static Rboolean* p_should_unwind_protect = setup_should_unwind_protect();
78-
return p_should_unwind_protect;
79-
}
80-
81-
inline Rboolean get_should_unwind_protect() { return *access_should_unwind_protect(); }
82-
83-
inline void set_should_unwind_protect(Rboolean should_unwind_protect) {
84-
*access_should_unwind_protect() = should_unwind_protect;
85-
}
86-
8757
} // namespace detail
8858

8959
#ifdef HAS_UNWIND_PROTECT
@@ -94,12 +64,6 @@ inline void set_should_unwind_protect(Rboolean should_unwind_protect) {
9464
template <typename Fun, typename = typename std::enable_if<std::is_same<
9565
decltype(std::declval<Fun&&>()()), SEXP>::value>::type>
9666
SEXP unwind_protect(Fun&& code) {
97-
if (detail::get_should_unwind_protect() == FALSE) {
98-
return std::forward<Fun>(code)();
99-
}
100-
101-
detail::set_should_unwind_protect(FALSE);
102-
10367
static SEXP token = [] {
10468
SEXP res = R_MakeUnwindCont();
10569
R_PreserveObject(res);
@@ -108,7 +72,6 @@ SEXP unwind_protect(Fun&& code) {
10872

10973
std::jmp_buf jmpbuf;
11074
if (setjmp(jmpbuf)) {
111-
detail::set_should_unwind_protect(TRUE);
11275
throw unwind_exception(token);
11376
}
11477

@@ -133,8 +96,6 @@ SEXP unwind_protect(Fun&& code) {
13396
// unset it here before returning the value ourselves.
13497
SETCAR(token, R_NilValue);
13598

136-
detail::set_should_unwind_protect(TRUE);
137-
13899
return res;
139100
}
140101

0 commit comments

Comments
 (0)