Skip to content

Commit b9a73ba

Browse files
authored
Merge pull request #789 from lionel-/impl-unwind
Use protect-unwind API and add Rcpp_fast_eval()
2 parents 921e7d6 + 801b7f3 commit b9a73ba

File tree

10 files changed

+253
-9
lines changed

10 files changed

+253
-9
lines changed

ChangeLog

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,48 @@
1+
2+
2017-12-13 Lionel Henry <lionel@rstudio.com>
3+
4+
* inst/include/Rcpp/api/meat/Rcpp_eval.h: Add Rcpp_fast_eval() for safe
5+
and fast evaluation of R code using the new protect-unwind API in R 3.5.
6+
Unlike Rcpp_eval(), this does not evaluate R code within tryCatch() in
7+
order to avoid the catching overhead. R longjumps are now correctly
8+
intercepted and rethrown. Following this change the C++ stack is now
9+
safely unwound when a longjump is detected while calling into R code.
10+
This includes the following cases: caught condition of any class, long
11+
return, restart jump, debugger exit.
12+
13+
Rcpp_eval() also uses the protect-unwind API in order to gain safety.
14+
To maintain compatibility it still catches errors and interrupts in
15+
order to rethrow them as typed C++ exceptions. If you don't need to
16+
catch those, consider using Rcpp_fast_eval() instead to avoid the
17+
overhead.
18+
19+
These improvements are only available for R 3.5.0 and greater. You also
20+
need to explicitly define `RCPP_PROTECTED_EVAL` before including Rcpp.h.
21+
When compiled with old versions of R, Rcpp_fast_eval() always falls back
22+
to Rcpp_eval(). This is in contrast to internal::Rcpp_eval_impl() which
23+
falls back to Rf_eval() and which is used in performance-sensititive
24+
places.
25+
26+
Note that with this change, Rcpp_eval() now behaves like the C function
27+
Rf_eval() whereas it used to behave like the R function base::eval().
28+
This has subtle implications for control flow. For instance evaluating a
29+
return() expression within a frame environment now returns from that
30+
frame rather than from the Rcpp_eval() call. The old semantics were a
31+
consequence of using evalq() internally and were not documented.
32+
33+
* inst/include/Rcpp/exceptions.h: Add LongjumpException and
34+
resumeJump() to support Rcpp_fast_eval().
35+
36+
* inst/include/Rcpp/macros/macros.h: Catch LongjumpException and call
37+
resumeJump(). If resumeJump() doesn't jump (on old R versions), throw an
38+
R error (this normally should not happen).
39+
40+
* inst/include/RcppCommon.h: Add Rcpp_fast_eval() to the public API and
41+
internal::Rcpp_eval_impl() to the private API.
42+
43+
* inst/include/Rcpp/Environment.h: Use safe evaluation
44+
* inst/include/Rcpp/Language.h: idem
45+
146
2017-12-05 Kevin Ushey <kevinushey@gmail.com>
247

348
* inst/include/Rcpp/Environment.h: Use public R APIs

inst/NEWS.Rd

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,21 @@
1111
set an initial format string (Dirk in \ghpr{777} fixing \ghit{776}).
1212
\item The 'new' Date and Datetime vectors now have \code{is_na} methods
1313
too. (Dirk in \ghpr{783} fixing \ghit{781}).
14+
\item Evaluation of R code is now safer when compiled against R
15+
3.5 (you also need to explicitly define \code{RCPP_PROTECTED_EVAL}
16+
before including \code{Rcpp.h}). Longjumps of all kinds (condition
17+
catching, returns, restarts, debugger exit) are appropriately
18+
detected and handled, e.g. the C++ stack unwinds correctly.
19+
\item The new function \code{Rcpp_fast_eval()} can be used for
20+
performance-sensitive evaluation of R code. Unlike
21+
\code{Rcpp_eval()}, it does not try to catch errors with
22+
\code{tryEval} in order to avoid the catching overhead. While this
23+
is safe thanks to the stack unwinding protection, this also means
24+
that R errors are not transformed to an \code{Rcpp::exception}. If
25+
you are relying on error rethrowing, you have to use the slower
26+
\code{Rcpp_eval()}. On old R versions \code{Rcpp_fast_eval()}
27+
falls back to \code{Rcpp_eval()} so it is safe to use against any
28+
versions of R.
1429
}
1530
\item Changes in Rcpp Attributes:
1631
\itemize{

inst/include/Rcpp/Environment.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ namespace Rcpp{
109109

110110
/* We need to evaluate if it is a promise */
111111
if( TYPEOF(res) == PROMSXP){
112-
res = Rf_eval( res, env ) ;
112+
res = internal::Rcpp_eval_impl( res, env ) ;
113113
}
114114
return res ;
115115
}
@@ -129,7 +129,7 @@ namespace Rcpp{
129129

130130
/* We need to evaluate if it is a promise */
131131
if( TYPEOF(res) == PROMSXP){
132-
res = Rf_eval( res, env ) ;
132+
res = internal::Rcpp_eval_impl( res, env ) ;
133133
}
134134
return res ;
135135
}
@@ -151,7 +151,7 @@ namespace Rcpp{
151151

152152
/* We need to evaluate if it is a promise */
153153
if( TYPEOF(res) == PROMSXP){
154-
res = Rf_eval( res, env ) ;
154+
res = internal::Rcpp_eval_impl( res, env ) ;
155155
}
156156
return res ;
157157
}
@@ -174,7 +174,7 @@ namespace Rcpp{
174174

175175
/* We need to evaluate if it is a promise */
176176
if( TYPEOF(res) == PROMSXP){
177-
res = Rf_eval( res, env ) ;
177+
res = internal::Rcpp_eval_impl( res, env ) ;
178178
}
179179
return res ;
180180
}

inst/include/Rcpp/Language.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,10 @@ namespace Rcpp{
145145
}
146146

147147
SEXP fast_eval() const {
148-
return Rf_eval( Storage::get__(), R_GlobalEnv) ;
148+
return internal::Rcpp_eval_impl( Storage::get__(), R_GlobalEnv) ;
149149
}
150150
SEXP fast_eval(SEXP env ) const {
151-
return Rf_eval( Storage::get__(), env) ;
151+
return internal::Rcpp_eval_impl( Storage::get__(), env) ;
152152
}
153153

154154
void update( SEXP x){

inst/include/Rcpp/api/meat/Rcpp_eval.h

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,78 @@
1919
#define Rcpp_api_meat_Rcpp_eval_h
2020

2121
#include <Rcpp/Interrupt.h>
22+
#include <Rversion.h>
23+
24+
#if (defined(RCPP_PROTECTED_EVAL) && defined(R_VERSION) && R_VERSION >= R_Version(3, 5, 0))
25+
#define RCPP_USE_PROTECT_UNWIND
26+
#endif
27+
2228

2329
namespace Rcpp {
30+
namespace internal {
31+
32+
#ifdef RCPP_USE_PROTECT_UNWIND
33+
34+
struct EvalData {
35+
SEXP expr;
36+
SEXP env;
37+
EvalData(SEXP expr_, SEXP env_) : expr(expr_), env(env_) { }
38+
};
39+
40+
inline void Rcpp_maybe_throw(void* data, Rboolean jump) {
41+
if (jump) {
42+
SEXP token = static_cast<SEXP>(data);
43+
44+
// Keep the token protected while unwinding because R code might run
45+
// in C++ destructors. Can't use PROTECT() for this because
46+
// UNPROTECT() might be called in a destructor, for instance if a
47+
// Shield<SEXP> is on the stack.
48+
::R_PreserveObject(token);
49+
50+
throw LongjumpException(token);
51+
}
52+
}
53+
54+
inline SEXP Rcpp_protected_eval(void* eval_data) {
55+
EvalData* data = static_cast<EvalData*>(eval_data);
56+
return ::Rf_eval(data->expr, data->env);
57+
}
58+
59+
// This is used internally instead of Rf_eval() to make evaluation safer
60+
inline SEXP Rcpp_eval_impl(SEXP expr, SEXP env) {
61+
return Rcpp_fast_eval(expr, env);
62+
}
63+
64+
#else // R < 3.5.0
65+
66+
// Fall back to Rf_eval() when the protect-unwind API is unavailable
67+
inline SEXP Rcpp_eval_impl(SEXP expr, SEXP env) {
68+
return ::Rf_eval(expr, env);
69+
}
70+
71+
#endif
72+
73+
} // namespace internal
74+
75+
76+
#ifdef RCPP_USE_PROTECT_UNWIND
77+
78+
inline SEXP Rcpp_fast_eval(SEXP expr, SEXP env) {
79+
internal::EvalData data(expr, env);
80+
Shield<SEXP> token(::R_MakeUnwindCont());
81+
return ::R_UnwindProtect(internal::Rcpp_protected_eval, &data,
82+
internal::Rcpp_maybe_throw, token,
83+
token);
84+
}
85+
86+
#else
87+
88+
inline SEXP Rcpp_fast_eval(SEXP expr, SEXP env) {
89+
return Rcpp_eval(expr, env);
90+
}
91+
92+
#endif
93+
2494

2595
inline SEXP Rcpp_eval(SEXP expr, SEXP env) {
2696

@@ -39,8 +109,7 @@ inline SEXP Rcpp_eval(SEXP expr, SEXP env) {
39109
SET_TAG(CDDR(call), ::Rf_install("error"));
40110
SET_TAG(CDDR(CDR(call)), ::Rf_install("interrupt"));
41111

42-
// execute the call
43-
Shield<SEXP> res(::Rf_eval(call, R_GlobalEnv));
112+
Shield<SEXP> res(internal::Rcpp_eval_impl(call, R_GlobalEnv));
44113

45114
// check for condition results (errors, interrupts)
46115
if (Rf_inherits(res, "condition")) {
@@ -49,7 +118,7 @@ inline SEXP Rcpp_eval(SEXP expr, SEXP env) {
49118

50119
Shield<SEXP> conditionMessageCall(::Rf_lang2(::Rf_install("conditionMessage"), res));
51120

52-
Shield<SEXP> conditionMessage(::Rf_eval(conditionMessageCall, R_GlobalEnv));
121+
Shield<SEXP> conditionMessage(internal::Rcpp_eval_impl(conditionMessageCall, R_GlobalEnv));
53122
throw eval_error(CHAR(STRING_ELT(conditionMessage, 0)));
54123
}
55124

inst/include/Rcpp/exceptions.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
#ifndef Rcpp__exceptions__h
2323
#define Rcpp__exceptions__h
2424

25+
#include <Rversion.h>
26+
27+
2528
#define GET_STACKTRACE() stack_trace( __FILE__, __LINE__ )
2629

2730
namespace Rcpp {
@@ -108,6 +111,22 @@ namespace Rcpp {
108111
throw Rcpp::exception(message.c_str());
109112
} // #nocov end
110113

114+
namespace internal {
115+
116+
struct LongjumpException {
117+
SEXP token;
118+
LongjumpException(SEXP token_) : token(token_) { }
119+
};
120+
121+
inline void resumeJump(SEXP token) {
122+
::R_ReleaseObject(token);
123+
#if (defined(RCPP_PROTECTED_EVAL) && defined(R_VERSION) && R_VERSION >= R_Version(3, 5, 0))
124+
::R_ContinueUnwind(token);
125+
#endif
126+
}
127+
128+
} // namespace internal
129+
111130
} // namespace Rcpp
112131

113132

inst/include/Rcpp/macros/macros.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@
4141
catch( Rcpp::internal::InterruptedException &__ex__) { \
4242
rcpp_output_type = 1 ; \
4343
} \
44+
catch(Rcpp::internal::LongjumpException& __ex__) { \
45+
Rcpp::internal::resumeJump(__ex__.token); \
46+
rcpp_output_type = 2 ; \
47+
rcpp_output_condition = PROTECT(string_to_try_error("Unexpected LongjumpException")) ; \
48+
} \
4449
catch(Rcpp::exception& __ex__) { \
4550
rcpp_output_type = 2 ; \
4651
rcpp_output_condition = PROTECT(rcpp_exception_to_r_condition(__ex__)) ; \
@@ -73,6 +78,10 @@
7378
catch (Rcpp::internal::InterruptedException &__ex__) { \
7479
return Rcpp::internal::interruptedError(); \
7580
} \
81+
catch (Rcpp::internal::LongjumpException& __ex__) { \
82+
Rcpp::internal::resumeJump(__ex__.token); \
83+
return string_to_try_error("Unexpected LongjumpException") ; \
84+
} \
7685
catch (std::exception &__ex__) { \
7786
return exception_to_try_error(__ex__); \
7887
} \

inst/include/RcppCommon.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,13 @@ namespace Rcpp {
7474

7575
namespace Rcpp {
7676

77+
SEXP Rcpp_fast_eval(SEXP expr_, SEXP env = R_GlobalEnv);
7778
SEXP Rcpp_eval(SEXP expr_, SEXP env = R_GlobalEnv);
79+
80+
namespace internal {
81+
SEXP Rcpp_eval_impl(SEXP expr, SEXP env = R_GlobalEnv);
82+
}
83+
7884
class Module;
7985

8086
namespace traits {

inst/unitTests/cpp/misc.cpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
// You should have received a copy of the GNU General Public License
2020
// along with Rcpp. If not, see <http://www.gnu.org/licenses/>.
2121

22+
#define RCPP_PROTECTED_EVAL
23+
2224
#include <Rcpp.h>
2325
using namespace Rcpp;
2426
using namespace std;
@@ -224,3 +226,31 @@ String testNullableString(Rcpp::Nullable<Rcpp::String> param = R_NilValue) {
224226
else
225227
return String("");
226228
}
229+
230+
// Class that indicates to R caller whether C++ stack was unwound
231+
struct unwindIndicator {
232+
unwindIndicator(LogicalVector indicator_) {
233+
// Reset the indicator to FALSE
234+
indicator = indicator_;
235+
*LOGICAL(indicator) = 0;
236+
}
237+
238+
// Set indicator to TRUE when stack unwinds
239+
~unwindIndicator() {
240+
*LOGICAL(indicator) = 1;
241+
}
242+
243+
LogicalVector indicator;
244+
};
245+
246+
// [[Rcpp::export]]
247+
SEXP testEvalUnwindImpl(RObject expr, Environment env, LogicalVector indicator) {
248+
unwindIndicator my_data(indicator);
249+
return Rcpp::Rcpp_fast_eval(expr, env);
250+
}
251+
252+
// [[Rcpp::export]]
253+
SEXP testSendInterrupt() {
254+
Rf_onintr();
255+
return R_NilValue;
256+
}

inst/unitTests/runit.misc.R

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,4 +214,55 @@ if (.runThisTest) {
214214
checkTrue(nchar(Rcpp:::bib()) > 0, msg="bib file")
215215
}
216216

217+
test.stackUnwinds <- function() {
218+
# On old versions of R, Rcpp_fast_eval() falls back to Rcpp_eval() and
219+
# leaks on longjumps
220+
hasUnwind <- getRversion() >= "3.5.0"
221+
checkUnwound <- if (hasUnwind) checkTrue else function(x) checkTrue(!x)
222+
testEvalUnwind <- function(expr, indicator) {
223+
testEvalUnwindImpl(expr, parent.frame(), indicator)
224+
}
225+
226+
# On errors - Always unwound
227+
unwound <- FALSE
228+
out <- tryCatch(testEvalUnwind(quote(stop("err")), unwound), error = identity)
229+
checkTrue(unwound)
230+
msg <- if (hasUnwind) "err" else "Evaluation error: err."
231+
checkIdentical(out$message, msg)
232+
233+
# On interrupts - Always unwound
234+
unwound <- FALSE
235+
expr <- quote({
236+
repeat testSendInterrupt()
237+
"returned"
238+
})
239+
out <- tryCatch(testEvalUnwind(expr, unwound), interrupt = function(c) "onintr")
240+
checkTrue(unwound)
241+
checkIdentical(out, "onintr")
242+
243+
# On caught conditions
244+
unwound <- FALSE
245+
expr <- quote(signalCondition(simpleCondition("cnd")))
246+
cnd <- tryCatch(testEvalUnwind(expr, unwound), condition = identity)
247+
checkTrue(inherits(cnd, "simpleCondition"))
248+
checkUnwound(unwound)
249+
250+
# On restart jumps
251+
unwound <- FALSE
252+
expr <- quote(invokeRestart("rst"))
253+
out <- withRestarts(testEvalUnwind(expr, unwound), rst = function(...) "restarted")
254+
checkIdentical(out, "restarted")
255+
checkUnwound(unwound)
256+
257+
# On returns
258+
unwound <- FALSE
259+
expr <- quote(signalCondition(simpleCondition(NULL)))
260+
out <- callCC(function(k)
261+
withCallingHandlers(testEvalUnwind(expr, unwound),
262+
simpleCondition = function(e) k("jumped")
263+
)
264+
)
265+
checkIdentical(out, "jumped")
266+
checkUnwound(unwound)
267+
}
217268
}

0 commit comments

Comments
 (0)