Skip to content

Commit 328ce3c

Browse files
committed
Simplify preserve list approach
And finally provide some reasoning backed by the standard about how the preserve list is unique across the compilation units in the package!
1 parent aeb67b3 commit 328ce3c

File tree

13 files changed

+142
-156
lines changed

13 files changed

+142
-156
lines changed

cpp11test/src/protect.cpp

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818

1919
[[cpp11::register]] void protect_one_cpp11_(SEXP x, int n) {
2020
for (R_xlen_t i = 0; i < n; ++i) {
21-
SEXP p = cpp11::preserved.insert(x);
22-
cpp11::preserved.release(p);
21+
SEXP p = cpp11::detail::store_insert(x);
22+
cpp11::detail::store_release(p);
2323
}
2424
}
2525

@@ -32,18 +32,6 @@
3232

3333
// The internal protections here are actually uneeded, but it is a useful way to benchmark
3434
// them
35-
//
36-
// clang-format off
37-
#ifdef __clang__
38-
# pragma clang diagnostic push
39-
# pragma clang diagnostic ignored "-Wunused-variable"
40-
#endif
41-
42-
#ifdef __GNUC__
43-
# pragma GCC diagnostic push
44-
# pragma GCC diagnostic ignored "-Wunused-variable"
45-
#endif
46-
// clang-format on
4735

4836
[[cpp11::register]] void protect_many_(int n) {
4937
#ifdef CPP11_BENCH
@@ -63,12 +51,12 @@
6351
[[cpp11::register]] void protect_many_cpp11_(int n) {
6452
std::vector<SEXP> res;
6553
for (R_xlen_t i = 0; i < n; ++i) {
66-
res.push_back(cpp11::preserved.insert(Rf_ScalarInteger(n)));
54+
res.push_back(cpp11::detail::store_insert(Rf_ScalarInteger(n)));
6755
}
6856

6957
for (R_xlen_t i = n - 1; i >= 0; --i) {
7058
SEXP x = res[i];
71-
cpp11::preserved.release(x);
59+
cpp11::detail::store_release(x);
7260
res.pop_back();
7361
}
7462
}

cpp11test/src/test-protect.cpp

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,3 @@
1-
// Avoid unused variable warnings regarding `cpp11::preserved`
2-
// clang-format off
3-
#ifdef __clang__
4-
# pragma clang diagnostic push
5-
# pragma clang diagnostic ignored "-Wunused-variable"
6-
#endif
7-
8-
#ifdef __GNUC__
9-
# pragma GCC diagnostic push
10-
# pragma GCC diagnostic ignored "-Wunused-variable"
11-
#endif
12-
// clang-format on
13-
141
#define CPP11_USE_FMT
152
#include "cpp11/protect.hpp"
163
#include "testthat.h"

inst/include/cpp11/doubles.hpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ template <>
8484
inline r_vector<double>::r_vector(std::initializer_list<named_arg> il)
8585
: cpp11::r_vector<double>(safe[Rf_allocVector](REALSXP, il.size())),
8686
capacity_(il.size()) {
87-
protect_ = preserved.insert(data_);
87+
protect_ = detail::store_insert(data_);
8888
int n_protected = 0;
8989

9090
try {
@@ -100,7 +100,7 @@ inline r_vector<double>::r_vector(std::initializer_list<named_arg> il)
100100
UNPROTECT(n_protected);
101101
});
102102
} catch (const unwind_exception& e) {
103-
preserved.release(protect_);
103+
detail::store_release(protect_);
104104
UNPROTECT(n_protected);
105105
throw e;
106106
}
@@ -111,8 +111,8 @@ inline void r_vector<double>::reserve(R_xlen_t new_capacity) {
111111
data_ = data_ == R_NilValue ? safe[Rf_allocVector](REALSXP, new_capacity)
112112
: safe[Rf_xlengthgets](data_, new_capacity);
113113
SEXP old_protect = protect_;
114-
protect_ = preserved.insert(data_);
115-
preserved.release(old_protect);
114+
protect_ = detail::store_insert(data_);
115+
detail::store_release(old_protect);
116116

117117
data_p_ = REAL(data_);
118118
capacity_ = new_capacity;

inst/include/cpp11/integers.hpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
#include "cpp11/as.hpp" // for as_sexp
1010
#include "cpp11/attribute_proxy.hpp" // for attribute_proxy
1111
#include "cpp11/named_arg.hpp" // for named_arg
12-
#include "cpp11/protect.hpp" // for preserved
12+
#include "cpp11/protect.hpp" // for store_insert, store_release
1313
#include "cpp11/r_vector.hpp" // for r_vector, r_vector<>::proxy
1414
#include "cpp11/sexp.hpp" // for sexp
1515

@@ -87,10 +87,10 @@ inline void r_vector<int>::reserve(R_xlen_t new_capacity) {
8787
SEXP old_protect = protect_;
8888

8989
// Protect the new data
90-
protect_ = preserved.insert(data_);
90+
protect_ = detail::store_insert(data_);
9191

9292
// Release the old protection;
93-
preserved.release(old_protect);
93+
detail::store_release(old_protect);
9494

9595
data_p_ = INTEGER(data_);
9696
capacity_ = new_capacity;
@@ -100,7 +100,7 @@ template <>
100100
inline r_vector<int>::r_vector(std::initializer_list<named_arg> il)
101101
: cpp11::r_vector<int>(safe[Rf_allocVector](INTSXP, il.size())),
102102
capacity_(il.size()) {
103-
protect_ = preserved.insert(data_);
103+
protect_ = detail::store_insert(data_);
104104
int n_protected = 0;
105105

106106
try {
@@ -116,7 +116,7 @@ inline r_vector<int>::r_vector(std::initializer_list<named_arg> il)
116116
UNPROTECT(n_protected);
117117
});
118118
} catch (const unwind_exception& e) {
119-
preserved.release(protect_);
119+
detail::store_release(protect_);
120120
UNPROTECT(n_protected);
121121
throw e;
122122
}

inst/include/cpp11/list.hpp

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
#include "cpp11/R.hpp" // for SEXP, SEXPREC, SET_VECTOR_ELT
66
#include "cpp11/attribute_proxy.hpp" // for attribute_proxy
77
#include "cpp11/named_arg.hpp" // for named_arg
8-
#include "cpp11/protect.hpp" // for preserved
8+
#include "cpp11/protect.hpp" // for store_insert, store_release
99
#include "cpp11/r_string.hpp" // for r_string
1010
#include "cpp11/r_vector.hpp" // for r_vector, r_vector<>::proxy
1111
#include "cpp11/sexp.hpp" // for sexp
@@ -78,7 +78,7 @@ template <>
7878
inline r_vector<SEXP>::r_vector(std::initializer_list<SEXP> il)
7979
: cpp11::r_vector<SEXP>(safe[Rf_allocVector](VECSXP, il.size())),
8080
capacity_(il.size()) {
81-
protect_ = preserved.insert(data_);
81+
protect_ = detail::store_insert(data_);
8282
auto it = il.begin();
8383
for (R_xlen_t i = 0; i < capacity_; ++i, ++it) {
8484
SET_VECTOR_ELT(data_, i, *it);
@@ -89,7 +89,7 @@ template <>
8989
inline r_vector<SEXP>::r_vector(std::initializer_list<named_arg> il)
9090
: cpp11::r_vector<SEXP>(safe[Rf_allocVector](VECSXP, il.size())),
9191
capacity_(il.size()) {
92-
protect_ = preserved.insert(data_);
92+
protect_ = detail::store_insert(data_);
9393
int n_protected = 0;
9494

9595
try {
@@ -105,7 +105,7 @@ inline r_vector<SEXP>::r_vector(std::initializer_list<named_arg> il)
105105
UNPROTECT(n_protected);
106106
});
107107
} catch (const unwind_exception& e) {
108-
preserved.release(protect_);
108+
detail::store_release(protect_);
109109
UNPROTECT(n_protected);
110110
throw e;
111111
}
@@ -117,8 +117,8 @@ inline void r_vector<SEXP>::reserve(R_xlen_t new_capacity) {
117117
: safe[Rf_xlengthgets](data_, new_capacity);
118118

119119
SEXP old_protect = protect_;
120-
protect_ = preserved.insert(data_);
121-
preserved.release(old_protect);
120+
protect_ = detail::store_insert(data_);
121+
detail::store_release(old_protect);
122122

123123
capacity_ = new_capacity;
124124
}

inst/include/cpp11/logicals.hpp

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
#include "cpp11/R.hpp" // for SEXP, SEXPREC, Rf_all...
88
#include "cpp11/attribute_proxy.hpp" // for attribute_proxy
99
#include "cpp11/named_arg.hpp" // for named_arg
10-
#include "cpp11/protect.hpp" // for preserved
10+
#include "cpp11/protect.hpp" // for store_insert, store_release
1111
#include "cpp11/r_bool.hpp" // for r_bool
1212
#include "cpp11/r_vector.hpp" // for r_vector, r_vector<>::proxy
1313
#include "cpp11/sexp.hpp" // for sexp
@@ -80,7 +80,7 @@ inline bool operator==(const r_vector<r_bool>::proxy& lhs, r_bool rhs) {
8080
template <>
8181
inline r_vector<r_bool>::r_vector(std::initializer_list<r_bool> il)
8282
: cpp11::r_vector<r_bool>(Rf_allocVector(LGLSXP, il.size())), capacity_(il.size()) {
83-
protect_ = preserved.insert(data_);
83+
protect_ = detail::store_insert(data_);
8484
auto it = il.begin();
8585
for (R_xlen_t i = 0; i < capacity_; ++i, ++it) {
8686
SET_LOGICAL_ELT(data_, i, *it);
@@ -91,7 +91,7 @@ template <>
9191
inline r_vector<r_bool>::r_vector(std::initializer_list<named_arg> il)
9292
: cpp11::r_vector<r_bool>(safe[Rf_allocVector](LGLSXP, il.size())),
9393
capacity_(il.size()) {
94-
protect_ = preserved.insert(data_);
94+
protect_ = detail::store_insert(data_);
9595
int n_protected = 0;
9696

9797
try {
@@ -107,7 +107,7 @@ inline r_vector<r_bool>::r_vector(std::initializer_list<named_arg> il)
107107
UNPROTECT(n_protected);
108108
});
109109
} catch (const unwind_exception& e) {
110-
preserved.release(protect_);
110+
detail::store_release(protect_);
111111
UNPROTECT(n_protected);
112112
throw e;
113113
}
@@ -118,9 +118,9 @@ inline void r_vector<r_bool>::reserve(R_xlen_t new_capacity) {
118118
data_ = data_ == R_NilValue ? safe[Rf_allocVector](LGLSXP, new_capacity)
119119
: safe[Rf_xlengthgets](data_, new_capacity);
120120
SEXP old_protect = protect_;
121-
protect_ = preserved.insert(data_);
121+
protect_ = detail::store_insert(data_);
122122

123-
preserved.release(old_protect);
123+
detail::store_release(old_protect);
124124

125125
data_p_ = LOGICAL(data_);
126126
capacity_ = new_capacity;

inst/include/cpp11/protect.hpp

Lines changed: 72 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -245,84 +245,94 @@ void warning(const std::string& fmt, Args... args) {
245245
}
246246
#endif
247247

248-
/// A doubly-linked list of preserved objects, allowing O(1) insertion/release of
249-
/// objects compared to O(N preserved) with R_PreserveObject.
250-
static struct {
251-
SEXP insert(SEXP obj) {
252-
if (obj == R_NilValue) {
253-
return R_NilValue;
254-
}
248+
namespace detail {
255249

256-
PROTECT(obj);
250+
// A doubly-linked list of preserved objects, allowing O(1) insertion/release of objects
251+
// compared to O(N preserved) with `R_PreserveObject()` and `R_ReleaseObject()`.
252+
//
253+
// We let R manage the memory of the list itself by calling `R_PreserveObject()` on it.
254+
//
255+
// cpp11 being a header only library makes creating a "global" preserve list a bit tricky.
256+
// The trick we use here is that static local variables in inline extern functions are
257+
// guaranteed by the standard to be unique across the whole program. Inline functions are
258+
// extern by default, but `static inline` functions are not, so do not change these
259+
// functions to `static`. If we did that, we would end up having one preserve list per
260+
// compilation unit instead. As it stands today, we are fairly confident that we have 1
261+
// preserve list per package, which seems to work nicely.
262+
// https://stackoverflow.com/questions/185624/what-happens-to-static-variables-in-inline-functions
263+
// https://stackoverflow.com/questions/51612866/global-variables-in-header-only-library
264+
// https://github.com/r-lib/cpp11/issues/330
265+
//
266+
// > A static local variable in an extern inline function always refers to the
267+
// same object. 7.1.2/4 - C++98/C++14 (n3797)
268+
269+
inline SEXP new_store() {
270+
SEXP out = Rf_cons(R_NilValue, Rf_cons(R_NilValue, R_NilValue));
271+
R_PreserveObject(out);
272+
return out;
273+
}
257274

258-
static SEXP list = get_preserve_list();
275+
inline SEXP store() {
276+
// Note the `static` local variable in the inline extern function here! Guarantees we
277+
// have 1 unique preserve list across all compilation units in the package.
278+
static SEXP out = new_store();
279+
return out;
280+
}
259281

260-
// Get references to the head of the precious list and the next element
261-
// after the head
262-
SEXP head = list;
263-
SEXP next = CDR(list);
282+
inline SEXP store_insert(SEXP x) {
283+
if (x == R_NilValue) {
284+
return R_NilValue;
285+
}
264286

265-
// Add a new cell that points to the current head + next.
266-
SEXP cell = PROTECT(Rf_cons(head, next));
267-
SET_TAG(cell, obj);
287+
PROTECT(x);
268288

269-
// Update the head + next to point at the newly-created cell,
270-
// effectively inserting that cell between the current head + next.
271-
SETCDR(head, cell);
272-
SETCAR(next, cell);
289+
SEXP list = store();
273290

274-
UNPROTECT(2);
291+
// Get references to the head of the preserve list and the next element
292+
// after the head
293+
SEXP head = list;
294+
SEXP next = CDR(list);
275295

276-
return cell;
277-
}
296+
// Add a new cell that points to the current head + next.
297+
SEXP cell = PROTECT(Rf_cons(head, next));
298+
SET_TAG(cell, x);
278299

279-
void print() {
280-
static SEXP list = get_preserve_list();
281-
for (SEXP cell = list; cell != R_NilValue; cell = CDR(cell)) {
282-
REprintf("%p CAR: %p CDR: %p TAG: %p\n", reinterpret_cast<void*>(cell),
283-
reinterpret_cast<void*>(CAR(cell)), reinterpret_cast<void*>(CDR(cell)),
284-
reinterpret_cast<void*>(TAG(cell)));
285-
}
286-
REprintf("---\n");
287-
}
300+
// Update the head + next to point at the newly-created cell,
301+
// effectively inserting that cell between the current head + next.
302+
SETCDR(head, cell);
303+
SETCAR(next, cell);
288304

289-
void release(SEXP cell) {
290-
if (cell == R_NilValue) {
291-
return;
292-
}
305+
UNPROTECT(2);
293306

294-
// Get a reference to the cells before and after the token.
295-
SEXP lhs = CAR(cell);
296-
SEXP rhs = CDR(cell);
307+
return cell;
308+
}
297309

298-
// Remove the cell from the precious list -- effectively, we do this
299-
// by updating the 'lhs' and 'rhs' references to point at each-other,
300-
// effectively removing any references to the cell in the pairlist.
301-
SETCDR(lhs, rhs);
302-
SETCAR(rhs, lhs);
310+
inline void store_release(SEXP cell) {
311+
if (cell == R_NilValue) {
312+
return;
303313
}
304314

305-
private:
306-
// Each compilation unit purposefully gets its own preserve list.
307-
// This avoids issues with sharing preserve list state across compilation units
308-
// and across packages, which has historically caused many issues (#330).
309-
static SEXP get_preserve_list() {
310-
static SEXP out = init_preserve_list();
311-
return out;
312-
}
315+
// Get a reference to the cells before and after the token.
316+
SEXP lhs = CAR(cell);
317+
SEXP rhs = CDR(cell);
313318

314-
static SEXP init_preserve_list() {
315-
// Initialize the list exactly once per compilation unit,
316-
// and let R manage its memory
317-
SEXP out = new_preserve_list();
318-
R_PreserveObject(out);
319-
return out;
320-
}
319+
// Remove the cell from the preserve list -- effectively, we do this
320+
// by updating the 'lhs' and 'rhs' references to point at each-other,
321+
// effectively removing any references to the cell in the pairlist.
322+
SETCDR(lhs, rhs);
323+
SETCAR(rhs, lhs);
324+
}
321325

322-
static SEXP new_preserve_list() {
323-
return Rf_cons(R_NilValue, Rf_cons(R_NilValue, R_NilValue));
326+
inline void store_print() {
327+
SEXP list = store();
328+
for (SEXP cell = list; cell != R_NilValue; cell = CDR(cell)) {
329+
REprintf("%p CAR: %p CDR: %p TAG: %p\n", reinterpret_cast<void*>(cell),
330+
reinterpret_cast<void*>(CAR(cell)), reinterpret_cast<void*>(CDR(cell)),
331+
reinterpret_cast<void*>(TAG(cell)));
324332
}
333+
REprintf("---\n");
334+
}
325335

326-
} preserved;
336+
} // namespace detail
327337

328338
} // namespace cpp11

inst/include/cpp11/r_bool.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
#include "R_ext/Boolean.h" // for Rboolean
88
#include "cpp11/R.hpp" // for SEXP, SEXPREC, ...
99
#include "cpp11/as.hpp" // for as_sexp
10-
#include "cpp11/protect.hpp" // for unwind_protect, preserved
10+
#include "cpp11/protect.hpp" // for unwind_protect
1111
#include "cpp11/r_vector.hpp"
1212
#include "cpp11/sexp.hpp" // for sexp
1313

0 commit comments

Comments
 (0)