Skip to content

Commit c3dd7ab

Browse files
committed
Use new protect-unwind API for evaluation of R code
1 parent 921e7d6 commit c3dd7ab

File tree

6 files changed

+152
-3
lines changed

6 files changed

+152
-3
lines changed

ChangeLog

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,44 @@
1+
2+
2017-12-13 Lionel Henry <[email protected]>
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. When
20+
compiled with old versions of R, Rcpp_fast_eval() falls back to
21+
Rcpp_eval(). This is in contrast to internal::Rcpp_eval_impl() which
22+
falls back to Rf_eval() and which is used in performance-sensititive
23+
places.
24+
25+
Note that with this change, Rcpp_eval() now behaves like the C function
26+
Rf_eval() whereas it used to behave like the R function base::eval().
27+
This has subtle implications for control flow. For instance evaluating a
28+
return() expression within a frame environment now returns from that
29+
frame rather than from the Rcpp_eval() call. The old semantics were a
30+
consequence of using evalq() internally and were not documented.
31+
32+
* inst/include/Rcpp/exceptions.h: Add LongjumpException and
33+
resumeJump() to support Rcpp_fast_eval().
34+
35+
* inst/include/Rcpp/macros/macros.h: Catch LongjumpException and call
36+
resumeJump(). If resumeJump() doesn't jump (on old R versions), throw an
37+
R error (this normally should not happen).
38+
39+
* inst/include/RcppCommon.h: Add Rcpp_fast_eval() to the public API and
40+
internal::Rcpp_eval_impl() to the private API.
41+
142
2017-12-05 Kevin Ushey <[email protected]>
243

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

inst/NEWS.Rd

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,20 @@
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+
15+
\item Evaluation of R code is now safer when compiled against R
16+
3.5. Longjumps of all kinds (condition catching, returns,
17+
restarts, debugger exit) are appropriately detected and handled,
18+
e.g. the C++ stack unwinds correctly. The new function
19+
\code{Rcpp_fast_eval()} can be used for performance-sensitive
20+
evaluation of R code. Unlike \code{Rcpp_eval()}, it does not try
21+
to catch errors with \code{tryEval} in order to avoid the catching
22+
overhead. While this is safe thanks to the stack unwinding
23+
protection, this also means that R errors are not transformed to
24+
an \code{Rcpp::exception}. If you are relying on error rethrowing,
25+
you have to use the slower \code{Rcpp_eval()}. On old R versions
26+
\code{Rcpp_fast_eval()} falls back to \code{Rcpp_eval()} so it is
27+
safe to use against any versions of R.
1428
}
1529
\item Changes in Rcpp Attributes:
1630
\itemize{

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

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

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

2329
namespace Rcpp {
30+
namespace internal {
31+
32+
#ifdef R_HAS_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+
throw LongjumpException(static_cast<SEXP>(data));
43+
}
44+
}
45+
46+
inline SEXP Rcpp_protected_eval(void* eval_data) {
47+
EvalData* data = static_cast<EvalData*>(eval_data);
48+
return ::Rf_eval(data->expr, data->env);
49+
}
50+
51+
// This is used internally instead of Rf_eval() to make evaluation safer
52+
inline SEXP Rcpp_eval_impl(SEXP expr, SEXP env) {
53+
return Rcpp_fast_eval(expr, env);
54+
}
55+
56+
#else // R < 3.5.0
57+
58+
// Fall back to Rf_eval() when the protect-unwind API is unavailable
59+
inline SEXP Rcpp_eval_impl(SEXP expr, SEXP env) {
60+
return ::Rf_eval(expr, env);
61+
}
62+
63+
#endif
64+
65+
} // namespace internal
66+
67+
68+
#ifdef R_HAS_UNWIND
69+
70+
inline SEXP Rcpp_fast_eval(SEXP expr, SEXP env) {
71+
internal::EvalData data(expr, env);
72+
Shield<SEXP> token(::R_MakeUnwindCont());
73+
return ::R_UnwindProtect(internal::Rcpp_protected_eval, &data,
74+
internal::Rcpp_maybe_throw, token,
75+
token);
76+
}
77+
78+
#else
79+
80+
inline SEXP Rcpp_fast_eval(SEXP expr, SEXP env) {
81+
return Rcpp_eval(expr, env);
82+
}
83+
84+
#endif
85+
2486

2587
inline SEXP Rcpp_eval(SEXP expr, SEXP env) {
2688

@@ -39,8 +101,7 @@ inline SEXP Rcpp_eval(SEXP expr, SEXP env) {
39101
SET_TAG(CDDR(call), ::Rf_install("error"));
40102
SET_TAG(CDDR(CDR(call)), ::Rf_install("interrupt"));
41103

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

45106
// check for condition results (errors, interrupts)
46107
if (Rf_inherits(res, "condition")) {
@@ -49,7 +110,7 @@ inline SEXP Rcpp_eval(SEXP expr, SEXP env) {
49110

50111
Shield<SEXP> conditionMessageCall(::Rf_lang2(::Rf_install("conditionMessage"), res));
51112

52-
Shield<SEXP> conditionMessage(::Rf_eval(conditionMessageCall, R_GlobalEnv));
113+
Shield<SEXP> conditionMessage(internal::Rcpp_eval_impl(conditionMessageCall, R_GlobalEnv));
53114
throw eval_error(CHAR(STRING_ELT(conditionMessage, 0)));
54115
}
55116

inst/include/Rcpp/exceptions.h

Lines changed: 18 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,21 @@ 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+
#if (defined(R_VERSION) && R_VERSION >= R_Version(3, 5, 0))
123+
::R_ContinueUnwind(token);
124+
#endif
125+
}
126+
127+
} // namespace internal
128+
111129
} // namespace Rcpp
112130

113131

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 {

0 commit comments

Comments
 (0)