From 04b08a352333b7f4a2f299d530efd96ffd7403ad Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Tue, 25 Jul 2023 13:52:16 -0400 Subject: [PATCH 1/7] Revert #299 --- inst/include/cpp11/protect.hpp | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/inst/include/cpp11/protect.hpp b/inst/include/cpp11/protect.hpp index acab13d6..2948b494 100644 --- a/inst/include/cpp11/protect.hpp +++ b/inst/include/cpp11/protect.hpp @@ -54,34 +54,20 @@ inline void set_option(SEXP name, SEXP value) { SETCAR(opt, value); } -inline Rboolean* setup_should_unwind_protect() { +inline Rboolean& get_should_unwind_protect() { SEXP should_unwind_protect_sym = Rf_install("cpp11_should_unwind_protect"); SEXP should_unwind_protect_sexp = Rf_GetOption1(should_unwind_protect_sym); - if (should_unwind_protect_sexp == R_NilValue) { - // Allocate and initialize once, then let R manage it. - // That makes this a shared global across all compilation units. should_unwind_protect_sexp = PROTECT(Rf_allocVector(LGLSXP, 1)); - SET_LOGICAL_ELT(should_unwind_protect_sexp, 0, TRUE); detail::set_option(should_unwind_protect_sym, should_unwind_protect_sexp); UNPROTECT(1); } - return reinterpret_cast(LOGICAL(should_unwind_protect_sexp)); -} - -inline Rboolean* access_should_unwind_protect() { - // Setup is run once per compilation unit, but all compilation units - // share the same global option, so each compilation unit's static pointer - // will point to the same object. - static Rboolean* p_should_unwind_protect = setup_should_unwind_protect(); - return p_should_unwind_protect; -} - -inline Rboolean get_should_unwind_protect() { return *access_should_unwind_protect(); } + Rboolean* should_unwind_protect = + reinterpret_cast(LOGICAL(should_unwind_protect_sexp)); + should_unwind_protect[0] = TRUE; -inline void set_should_unwind_protect(Rboolean should_unwind_protect) { - *access_should_unwind_protect() = should_unwind_protect; + return should_unwind_protect[0]; } } // namespace detail @@ -94,11 +80,12 @@ inline void set_should_unwind_protect(Rboolean should_unwind_protect) { template ()()), SEXP>::value>::type> SEXP unwind_protect(Fun&& code) { - if (detail::get_should_unwind_protect() == FALSE) { + static auto should_unwind_protect = detail::get_should_unwind_protect(); + if (should_unwind_protect == FALSE) { return std::forward(code)(); } - detail::set_should_unwind_protect(FALSE); + should_unwind_protect = FALSE; static SEXP token = [] { SEXP res = R_MakeUnwindCont(); @@ -108,7 +95,7 @@ SEXP unwind_protect(Fun&& code) { std::jmp_buf jmpbuf; if (setjmp(jmpbuf)) { - detail::set_should_unwind_protect(TRUE); + should_unwind_protect = TRUE; throw unwind_exception(token); } @@ -133,7 +120,7 @@ SEXP unwind_protect(Fun&& code) { // unset it here before returning the value ourselves. SETCAR(token, R_NilValue); - detail::set_should_unwind_protect(TRUE); + should_unwind_protect = TRUE; return res; } From 293acca57620bfa6c15dd8537f16e81a92f712fd Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Wed, 26 Jul 2023 16:54:46 -0400 Subject: [PATCH 2/7] Remove unwind protect global variable altogether --- NEWS.md | 4 ++++ inst/include/cpp11/protect.hpp | 26 -------------------------- 2 files changed, 4 insertions(+), 26 deletions(-) diff --git a/NEWS.md b/NEWS.md index 0509039a..0df238f5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # cpp11 (development version) +* TODO (something about both the performance optimization being removed, and + the danger of using nested unwind_protect() calls from within the same .Call + entry point) + # cpp11 0.4.5 * On 2023-07-20, cpp11 was temporarily rolled back to 0.4.3 manually by CRAN due diff --git a/inst/include/cpp11/protect.hpp b/inst/include/cpp11/protect.hpp index 2948b494..7ff4a47f 100644 --- a/inst/include/cpp11/protect.hpp +++ b/inst/include/cpp11/protect.hpp @@ -54,22 +54,6 @@ inline void set_option(SEXP name, SEXP value) { SETCAR(opt, value); } -inline Rboolean& get_should_unwind_protect() { - SEXP should_unwind_protect_sym = Rf_install("cpp11_should_unwind_protect"); - SEXP should_unwind_protect_sexp = Rf_GetOption1(should_unwind_protect_sym); - if (should_unwind_protect_sexp == R_NilValue) { - should_unwind_protect_sexp = PROTECT(Rf_allocVector(LGLSXP, 1)); - detail::set_option(should_unwind_protect_sym, should_unwind_protect_sexp); - UNPROTECT(1); - } - - Rboolean* should_unwind_protect = - reinterpret_cast(LOGICAL(should_unwind_protect_sexp)); - should_unwind_protect[0] = TRUE; - - return should_unwind_protect[0]; -} - } // namespace detail #ifdef HAS_UNWIND_PROTECT @@ -80,13 +64,6 @@ inline Rboolean& get_should_unwind_protect() { template ()()), SEXP>::value>::type> SEXP unwind_protect(Fun&& code) { - static auto should_unwind_protect = detail::get_should_unwind_protect(); - if (should_unwind_protect == FALSE) { - return std::forward(code)(); - } - - should_unwind_protect = FALSE; - static SEXP token = [] { SEXP res = R_MakeUnwindCont(); R_PreserveObject(res); @@ -95,7 +72,6 @@ SEXP unwind_protect(Fun&& code) { std::jmp_buf jmpbuf; if (setjmp(jmpbuf)) { - should_unwind_protect = TRUE; throw unwind_exception(token); } @@ -120,8 +96,6 @@ SEXP unwind_protect(Fun&& code) { // unset it here before returning the value ourselves. SETCAR(token, R_NilValue); - should_unwind_protect = TRUE; - return res; } From 7fd15aa009568d3eb090278b2e6b4cff7ef19300 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 27 Jul 2023 15:20:38 -0400 Subject: [PATCH 3/7] Add a descriptive test --- cpp11test/R/cpp11.R | 8 +++ cpp11test/src/cpp11.cpp | 20 ++++++- cpp11test/src/test-protect-nested.cpp | 81 +++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 cpp11test/src/test-protect-nested.cpp diff --git a/cpp11test/R/cpp11.R b/cpp11test/R/cpp11.R index 4b9a1d9a..da84b9ec 100644 --- a/cpp11test/R/cpp11.R +++ b/cpp11test/R/cpp11.R @@ -223,3 +223,11 @@ rcpp_sum_dbl_accumulate_ <- function(x_sxp) { rcpp_grow_ <- function(n_sxp) { .Call(`_cpp11test_rcpp_grow_`, n_sxp) } + +test_destruction_inner <- function() { + invisible(.Call(`_cpp11test_test_destruction_inner`)) +} + +test_destruction_outer <- function() { + invisible(.Call(`_cpp11test_test_destruction_outer`)) +} diff --git a/cpp11test/src/cpp11.cpp b/cpp11test/src/cpp11.cpp index 44472d85..6120c333 100644 --- a/cpp11test/src/cpp11.cpp +++ b/cpp11test/src/cpp11.cpp @@ -422,10 +422,26 @@ extern "C" SEXP _cpp11test_rcpp_grow_(SEXP n_sxp) { return cpp11::as_sexp(rcpp_grow_(cpp11::as_cpp>(n_sxp))); END_CPP11 } +// test-protect-nested.cpp +void test_destruction_inner(); +extern "C" SEXP _cpp11test_test_destruction_inner() { + BEGIN_CPP11 + test_destruction_inner(); + return R_NilValue; + END_CPP11 +} +// test-protect-nested.cpp +void test_destruction_outer(); +extern "C" SEXP _cpp11test_test_destruction_outer() { + BEGIN_CPP11 + test_destruction_outer(); + return R_NilValue; + END_CPP11 +} extern "C" { /* .Call calls */ -extern SEXP run_testthat_tests(SEXP); +extern SEXP run_testthat_tests(void *); static const R_CallMethodDef CallEntries[] = { {"_cpp11test_col_sums", (DL_FUNC) &_cpp11test_col_sums, 1}, @@ -483,6 +499,8 @@ static const R_CallMethodDef CallEntries[] = { {"_cpp11test_sum_int_for2_", (DL_FUNC) &_cpp11test_sum_int_for2_, 1}, {"_cpp11test_sum_int_for_", (DL_FUNC) &_cpp11test_sum_int_for_, 1}, {"_cpp11test_sum_int_foreach_", (DL_FUNC) &_cpp11test_sum_int_foreach_, 1}, + {"_cpp11test_test_destruction_inner", (DL_FUNC) &_cpp11test_test_destruction_inner, 0}, + {"_cpp11test_test_destruction_outer", (DL_FUNC) &_cpp11test_test_destruction_outer, 0}, {"_cpp11test_upper_bound", (DL_FUNC) &_cpp11test_upper_bound, 2}, {"run_testthat_tests", (DL_FUNC) &run_testthat_tests, 1}, {NULL, NULL, 0} diff --git a/cpp11test/src/test-protect-nested.cpp b/cpp11test/src/test-protect-nested.cpp new file mode 100644 index 00000000..2679a5f7 --- /dev/null +++ b/cpp11test/src/test-protect-nested.cpp @@ -0,0 +1,81 @@ +#include "cpp11/function.hpp" +#include "cpp11/protect.hpp" +#include "testthat.h" + +#ifdef HAS_UNWIND_PROTECT + +/* + * See https://github.com/r-lib/cpp11/pull/327 for full details. + * + * - `cpp11::package("cpp11test")["test_destruction_outer"]` uses + * `unwind_protect()` to call R level `test_destruction_outer()` but no entry + * macros are set up. Instead we are going to catch exceptions that get here + * with `expect_error_as()`. + * + * - Call R level `test_destruction_outer()` to set up `BEGIN_CPP11` / + * `END_CPP11` entry macros. + * + * - C++ `test_destruction_outer()` goes through `unwind_protect()` to call + * the R level `test_destruction_inner()`. + * + * - R level `test_destruction_inner()` sets up its own `BEGIN_CPP11` / + * `END_CPP11` entry macros. + * + * - C++ `test_destruction_inner()` goes through `unwind_protect()` to call + * `Rf_error()` (i.e., we are nested within `unwind_protect()`s!). + * + * - `longjmp()` is caught from inner `unwind_protect()`, and an exception + * is thrown which is caught by the inner entry macros, allowing us to run + * the destructor of `x`, then we let R continue the unwind process. + * + * - This `longjmp()`s again and is caught by the outer `unwind_protect()`, an + * exception is thrown which is caught by the outer entry macros, and we let + * R continue the unwind process one more time. + * + * - This `longjmp()` is caught by `cpp11::package()`'s `unwind_protect()`, + * an exception is thrown, and that is caught by `expect_error_as()`. + */ + +// Global variable to detect if the destructor has been run or not +static bool destructed = false; + +class HasDestructor { + public: + ~HasDestructor(); +}; + +HasDestructor::~HasDestructor() { + // Destructor has run! + destructed = true; +} + +[[cpp11::register]] void test_destruction_inner() { + // Expect that `x`'s destructor gets to run on the way out + HasDestructor x{}; + cpp11::stop("oh no!"); +} + +[[cpp11::register]] void test_destruction_outer() { + const auto test_destruction_inner = + cpp11::package("cpp11test")["test_destruction_inner"]; + test_destruction_inner(); +} + +context("unwind_protect-nested-C++") { + test_that( + "nested `unwind_protect()` (with entry macros set up) will run destructors" + "(#327)") { + const auto fn = [&] { + const auto test_destruction_outer = + cpp11::package("cpp11test")["test_destruction_outer"]; + test_destruction_outer(); + }; + + expect_error_as(fn(), cpp11::unwind_exception); + expect_true(destructed); + + destructed = false; + } +} + +#endif From c7c0c6ffa77d8110f459a46a836c6fe9ebb08bde Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 27 Jul 2023 17:04:56 -0400 Subject: [PATCH 4/7] Add FAQ bullets about `unwind_protect()` --- vignettes/FAQ.Rmd | 180 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 179 insertions(+), 1 deletion(-) diff --git a/vignettes/FAQ.Rmd b/vignettes/FAQ.Rmd index 34f81e11..61621ca7 100644 --- a/vignettes/FAQ.Rmd +++ b/vignettes/FAQ.Rmd @@ -3,8 +3,8 @@ title: "FAQ" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{FAQ} - %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} + %\VignetteEngine{knitr::rmarkdown} --- ```{r, include = FALSE} @@ -283,3 +283,181 @@ add_one(x) .Internal(inspect(x)) x ``` + +#### 15. Should I call `cpp11::unwind_protect()` manually? + +`cpp11::unwind_protect()` is cpp11's way of safely calling R's C API. In short, it allows you to run a function that might throw an R error, catch the `longjmp()` of that error, promote it to an exception that is thrown and caught by a try/catch that cpp11 sets up for you at `.Call()` time (which allows destructors to run), and finally tells R to continue unwinding the stack now that the C++ objects have had a chance to destruct as needed. + +Since `cpp11::unwind_protect()` takes an arbitrary function, you may be wondering if you should use it for your own custom needs. In general, we advise against this because this is an extremely advanced feature that is prone to subtle and hard to debug issues. + +##### Destructors + +The following setup for `test_destructor_ok()` with a manual call to `unwind_protect()` would work: + +```{cpp11} +#include + +class A { + public: + ~A(); +}; + +A::~A() { + Rprintf("hi from the destructor!"); +} + +[[cpp11::register]] +void test_destructor_ok() { + A a{}; + cpp11::unwind_protect([&] { + Rf_error("oh no!"); + }); +} + +[[cpp11::register]] +void test_destructor_bad() { + cpp11::unwind_protect([&] { + A a{}; + Rf_error("oh no!"); + }); +} +``` + +```{r, error=TRUE} +test_destructor_ok() +``` + +But if you happen to move `a` into the `unwind_protect()`, then it won't be destructed, and you'll end up with a memory leak at best, and a much more sinister issue if your destructor is important: + +```{r, eval=FALSE} +test_destructor_bad() +#> Error: oh no! +``` + +In general, the only code that can be called within `unwind_protect()` is "pure" C code. If you mix complex C++ objects with R's C API within `unwind_protect()`, then any R errors will result in a jump that prevents your destructors from running. + +##### Nested `unwind_protect()` + +Another issue that can arise has to do with _nested_ calls to `unwind_protect()`. It is very hard (if not impossible) to end up with invalidly nested `unwind_protect()` calls when using the typical cpp11 API, but you can manually create a scenario like the following: + +```{cpp11} +#include + +[[cpp11::register]] +void test_nested() { + cpp11::unwind_protect([&] { + cpp11::unwind_protect([&] { + Rf_error("oh no!"); + }); + }); +} +``` + +If you were to run `test_nested()` from R, it would likely crash or hang your R session due to the following chain of events: + +- `test_nested()` sets up a try/catch to catch unwind exceptions +- The outer `unwind_protect()` is called. It uses the C function `R_UnwindProtect()` to call its lambda function. +- The inner `unwind_protect()` is called. It again uses `R_UnwindProtect()`, this time to call `Rf_error()`. +- `Rf_error()` performs a `longjmp()` which is caught by the inner `unwind_protect()` and promoted to an exception. +- That exception is thrown, but because we are in the outer call to `R_UnwindProtect()` (a C function), we end up throwing that exception _across_ C stack frames. This is _undefined behavior_, which typically results in that exception not being caught anywhere, causing C++ (and ultimately R) to abort. + +You might think that you'd never do this, but the same scenario can also occur with a combination of 1 call to `unwind_protect()` combined with usage of the cpp11 API: + +```{cpp11} +#include + +[[cpp11::register]] +void test_hidden_nested() { + cpp11::unwind_protect([&] { + cpp11::stop("oh no!"); + }); +} +``` + +Because `cpp11::stop()` (and most of the cpp11 API) uses `unwind_protect()` internally, we've indirectly ended up in a nested `unwind_protect()` scenario again. + +In general, if you must use `unwind_protect()` then you must be very careful not to use any of the cpp11 API inside of the `unwind_protect()` call. + +It is worth pointing out that calling out to an R function from cpp11 which then calls back into cpp11 is still safe, i.e. if the registered version of the imaginary `test_outer()` function below was called from R, then that would work: + +```{cpp11, eval = FALSE} +#include + +[[cpp11::register]] +void test_inner() { + cpp11::stop("oh no!") +} + +[[cpp11::register]] +void test_outer() { + auto fn = cpp11::package("mypackage")["test_inner"] + fn(); +} +``` + +This might seem unsafe because `cpp11::package()` uses `unwind_protect()` to call the R function for `test_inner()`, which then goes back into C++ to call `cpp11::stop()`, which itself uses `unwind_protect()`, so it seems like we are in a nested scenario, but this scenario does actually work. It makes more sense if we analyze it one step at a time: + +- Call the R function for `test_outer()` +- A try/catch is set up to catch unwind exceptions +- The C++ function for `test_outer()` is called +- `cpp11::package()` uses `unwind_protect()` to call the R function for `test_inner()` +- Call the R function for `test_inner()` +- A try/catch is set up to catch unwind exceptions (_this is the key!_) +- The C++ function for `test_inner()` is called +- `cpp11::stop("oh no!")` is called, which uses `unwind_protect()` to call `Rf_error()`, causing a `longjmp()`, which is caught by that `unwind_protect()` and promoted to an exception. +- That exception is thrown, but this time it is caught by the try/catch set up by `test_inner()` as we entered it from the R side. This prevents that exception from crossing the C++ -> C boundary. +- The try/catch calls `R_ContinueUnwind()`, which `longjmp()`s again, and now the `unwind_protect()` set up by `cpp11::package()` catches that, and promotes it to an exception. +- That exception is thrown and caught by the try/catch set up by `test_outer()`. +- The try/catch calls `R_ContinueUnwind()`, which `longjmp()`s again, and at this point we can safely let the `longjmp()` proceed to force an R error. + +#### 16. Ok but I really want to call `cpp11::unwind_protect()` manually + +If you've read the above bullet and still feel like you need to call `unwind_protect()`, then you should keep in mind the following when writing the function to unwind-protect: + +- You shouldn't create any C++ objects that have destructors. +- You shouldn't use any parts of the cpp11 API that may call `unwind_protect()`. +- You must be very careful not to call `unwind_protect()` in a nested manner. + +In other words, if you only use plain-old-data types and only use R's C API within the `unwind_protect()`, then you can use it. + +One place you may want to do this is when working with long character vectors. Unfortunately, due to the way cpp11 must protect the individual CHARSXP objects that make up a character vector, it can currently be quite slow to use the cpp11 API for this. Consider this example of extracting out individual elements with `x[i]` vs using the native R API: + +```{cpp11} +#include + +[[cpp11::register]] +cpp11::sexp test_extract_cpp11(cpp11::strings x) { + const R_xlen_t size = x.size(); + + for (R_xlen_t i = 0; i < size; ++i) { + (void) x[i]; + } + + return R_NilValue; +} + +[[cpp11::register]] +cpp11::sexp test_extract_r_api(cpp11::strings x) { + const R_xlen_t size = x.size(); + const SEXP data{x}; + + cpp11::unwind_protect([&] { + for (R_xlen_t i = 0; i < size; ++i) { + (void) STRING_ELT(data, i); + } + }); + + return R_NilValue; +} +``` +```{r} +set.seed(123) +x <- sample(letters, 1e6, replace = TRUE) + +bench::mark( + test_extract_cpp11(x), + test_extract_r_api(x) +) +``` + +We plan to improve on this in the future, but for now this is one of the only places where we feel it is reasonable to call `unwind_protect()` manually. From a26f0a8eb2e338677a958077db357eb37c84af57 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 27 Jul 2023 17:05:11 -0400 Subject: [PATCH 5/7] Tweak internals vignette advice --- vignettes/internals.Rmd | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/vignettes/internals.Rmd b/vignettes/internals.Rmd index 3b55b698..73d7de05 100644 --- a/vignettes/internals.Rmd +++ b/vignettes/internals.Rmd @@ -31,23 +31,22 @@ You can load the package in an interactive R session devtools::load_all() ``` -Or run the tests with +Or run the cpp11 tests with ```r devtools::test() ``` -`test()` will also re-compile the package if needed, so you do not always have to run `load_all()`. +There are more extensive tests in the `cpp11test` directory. Generally when developing the C++ headers I run R with its working directory in the `cpp11test` directory and use `devtools::test()` to run the cpp11tests. -If you change the cpp11 headers you will need to clean and recompile the cpp11test package +If you change the cpp11 headers you will need to install the new version of cpp11 and then clean and recompile the cpp11test package: ```r +# Assuming your working directory is `cpp11test/` devtools::clean_dll() devtools::load_all() ``` -Generally when developing the C++ headers I run R with its working directory in the `cpp11test` directory and use `devtools::test()` to run the cpp11tests. - To calculate code coverage of the cpp11 package run the following from the `cpp11` root directory. ```r @@ -64,8 +63,6 @@ You may need to link the newly installed version 10. To do so, run `brew unlink Alternatively many IDEs support automatically running `clang-format` every time files are written. - - ## Code organization cpp11 is a header only library, so all source code exposed to users lives in [inst/include](https://github.com/r-lib/cpp11/tree/main/inst/include). From 7923a98b14d1d7345caf5345e86f6dd8d9b4df1b Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 27 Jul 2023 17:13:53 -0400 Subject: [PATCH 6/7] NEWS bullet --- NEWS.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/NEWS.md b/NEWS.md index 0df238f5..fe2d9892 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,8 +1,11 @@ # cpp11 (development version) -* TODO (something about both the performance optimization being removed, and - the danger of using nested unwind_protect() calls from within the same .Call - entry point) +* Nested calls to `cpp11::unwind_protect()` are no longer supported or + encouraged. Previously, this was something that could be done for performance + improvements, but ultimately this feature has proven to cause more problems + than it is worth and is very hard to use safely. For more information, see the + new `vignette("FAQ")` section titled "Should I call `cpp11::unwind_protect()` + manually?" (#327). # cpp11 0.4.5 From 6c88ac26e8fd38dfdb5e1ca36e91cfaaa33a785e Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Fri, 28 Jul 2023 10:11:57 -0400 Subject: [PATCH 7/7] Tweaks from code review --- vignettes/FAQ.Rmd | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vignettes/FAQ.Rmd b/vignettes/FAQ.Rmd index 61621ca7..29d99340 100644 --- a/vignettes/FAQ.Rmd +++ b/vignettes/FAQ.Rmd @@ -334,7 +334,7 @@ test_destructor_bad() #> Error: oh no! ``` -In general, the only code that can be called within `unwind_protect()` is "pure" C code. If you mix complex C++ objects with R's C API within `unwind_protect()`, then any R errors will result in a jump that prevents your destructors from running. +In general, the only code that can be called within `unwind_protect()` is "pure" C code or C++ code that only uses POD (plain-old-data) types and no exceptions. If you mix complex C++ objects with R's C API within `unwind_protect()`, then any R errors will result in a jump that prevents your destructors from running. ##### Nested `unwind_protect()` @@ -359,7 +359,7 @@ If you were to run `test_nested()` from R, it would likely crash or hang your R - The outer `unwind_protect()` is called. It uses the C function `R_UnwindProtect()` to call its lambda function. - The inner `unwind_protect()` is called. It again uses `R_UnwindProtect()`, this time to call `Rf_error()`. - `Rf_error()` performs a `longjmp()` which is caught by the inner `unwind_protect()` and promoted to an exception. -- That exception is thrown, but because we are in the outer call to `R_UnwindProtect()` (a C function), we end up throwing that exception _across_ C stack frames. This is _undefined behavior_, which typically results in that exception not being caught anywhere, causing C++ (and ultimately R) to abort. +- That exception is thrown, but because we are in the outer call to `R_UnwindProtect()` (a C function), we end up throwing that exception _across_ C stack frames. This is _undefined behavior_, which is known to have caused R to crash on certain platforms. You might think that you'd never do this, but the same scenario can also occur with a combination of 1 call to `unwind_protect()` combined with usage of the cpp11 API: @@ -418,7 +418,7 @@ If you've read the above bullet and still feel like you need to call `unwind_pro - You shouldn't use any parts of the cpp11 API that may call `unwind_protect()`. - You must be very careful not to call `unwind_protect()` in a nested manner. -In other words, if you only use plain-old-data types and only use R's C API within the `unwind_protect()`, then you can use it. +In other words, if you only use plain-old-data types, are careful to never throw exceptions, and only use R's C API, then you can use `unwind_protect()`. One place you may want to do this is when working with long character vectors. Unfortunately, due to the way cpp11 must protect the individual CHARSXP objects that make up a character vector, it can currently be quite slow to use the cpp11 API for this. Consider this example of extracting out individual elements with `x[i]` vs using the native R API: