From 02e618ae3ee85ae15cb7462d8d6b85030ee006c1 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 2 Apr 2026 15:12:19 -0400 Subject: [PATCH 1/5] Don't rely on transitive include of `"fmt/core.h"` We got it through `cpp11/protect.hpp`, but we should not rely on that --- inst/include/cpp11/function.hpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/inst/include/cpp11/function.hpp b/inst/include/cpp11/function.hpp index cc5af071..5940b796 100644 --- a/inst/include/cpp11/function.hpp +++ b/inst/include/cpp11/function.hpp @@ -11,6 +11,11 @@ #include "cpp11/protect.hpp" // for protect, protect::function, safe #include "cpp11/sexp.hpp" // for sexp +#ifdef CPP11_USE_FMT +#define FMT_HEADER_ONLY +#include "fmt/core.h" +#endif + namespace cpp11 { class function { From b4482bad0e1b100a8115c2b334647e3332aef39b Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 2 Apr 2026 15:21:48 -0400 Subject: [PATCH 2/5] Correctly use `fmt::runtime()` on runtime strings passed to `fmt::format()` In fmt, there is a `FMT_CONSTEVAL` macro that resolves to `consteval` on "new enough" C++ (otherwise it doesn't do anything). For R 4.6+, the default C++ used is finally "new enough" (`__cplusplus > 201703L`). This causes all `fmt::format()` calls to require a constant expression for `const char*` and `std::string&` input, which we are not currently doing via `fmt_arg`. We have a runtime provided string, which must now be wrapped in `fmt::runtime()`, which is what we should have been doing all along. --- NEWS.md | 2 ++ inst/include/cpp11/function.hpp | 4 ++-- inst/include/cpp11/protect.hpp | 8 ++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/NEWS.md b/NEWS.md index 90148254..ae5aab50 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # cpp11 (development version) +* Fixed a bug with `CPP11_USE_FMT` where the input was not being correctly wrapped in `fmt::runtime()`. + # cpp11 0.5.3 * Removed non-API usage of `ATTRIB()` (#481). diff --git a/inst/include/cpp11/function.hpp b/inst/include/cpp11/function.hpp index 5940b796..8b802453 100644 --- a/inst/include/cpp11/function.hpp +++ b/inst/include/cpp11/function.hpp @@ -111,7 +111,7 @@ inline void r_message(const char* x) { inline void message(const char* fmt_arg) { #ifdef CPP11_USE_FMT - std::string msg = fmt::format(fmt_arg); + std::string msg = fmt::format(fmt::runtime(fmt_arg)); safe[detail::r_message](msg.c_str()); #else char buff[1024]; @@ -126,7 +126,7 @@ inline void message(const char* fmt_arg) { template void message(const char* fmt_arg, Args... args) { #ifdef CPP11_USE_FMT - std::string msg = fmt::format(fmt_arg, args...); + std::string msg = fmt::format(fmt::runtime(fmt_arg), args...); safe[detail::r_message](msg.c_str()); #else char buff[1024]; diff --git a/inst/include/cpp11/protect.hpp b/inst/include/cpp11/protect.hpp index 9cb4876a..3058ecba 100644 --- a/inst/include/cpp11/protect.hpp +++ b/inst/include/cpp11/protect.hpp @@ -191,25 +191,25 @@ inline void check_user_interrupt() { safe[R_CheckUserInterrupt](); } #ifdef CPP11_USE_FMT template void stop [[noreturn]] (const char* fmt_arg, Args&&... args) { - std::string msg = fmt::format(fmt_arg, std::forward(args)...); + std::string msg = fmt::format(fmt::runtime(fmt_arg), std::forward(args)...); safe.noreturn(Rf_errorcall)(R_NilValue, "%s", msg.c_str()); } template void stop [[noreturn]] (const std::string& fmt_arg, Args&&... args) { - std::string msg = fmt::format(fmt_arg, std::forward(args)...); + std::string msg = fmt::format(fmt::runtime(fmt_arg), std::forward(args)...); safe.noreturn(Rf_errorcall)(R_NilValue, "%s", msg.c_str()); } template void warning(const char* fmt_arg, Args&&... args) { - std::string msg = fmt::format(fmt_arg, std::forward(args)...); + std::string msg = fmt::format(fmt::runtime(fmt_arg), std::forward(args)...); safe[Rf_warningcall](R_NilValue, "%s", msg.c_str()); } template void warning(const std::string& fmt_arg, Args&&... args) { - std::string msg = fmt::format(fmt_arg, std::forward(args)...); + std::string msg = fmt::format(fmt::runtime(fmt_arg), std::forward(args)...); safe[Rf_warningcall](R_NilValue, "%s", msg.c_str()); } #else From 6744a815bb9bf1954ae3c88350cc67591ae09716 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 2 Apr 2026 15:37:24 -0400 Subject: [PATCH 3/5] Add `r_ns_env()` and use in `get_namespace()` Throwing an informative (and tested!) error when we can't find the package namespace --- NEWS.md | 2 ++ inst/include/cpp11/R.hpp | 23 +++++++++++++++++++++++ inst/include/cpp11/function.hpp | 9 ++++++--- tests/testthat/_snaps/source.md | 8 ++++++++ tests/testthat/test-source.R | 19 +++++++++++++++++++ 5 files changed, 58 insertions(+), 3 deletions(-) diff --git a/NEWS.md b/NEWS.md index ae5aab50..810a4795 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # cpp11 (development version) +* Removed non-API usage of `R_NamespaceRegistry`. + * Fixed a bug with `CPP11_USE_FMT` where the input was not being correctly wrapped in `fmt::runtime()`. # cpp11 0.5.3 diff --git a/inst/include/cpp11/R.hpp b/inst/include/cpp11/R.hpp index c10c7630..30808d01 100644 --- a/inst/include/cpp11/R.hpp +++ b/inst/include/cpp11/R.hpp @@ -113,6 +113,29 @@ inline bool r_env_has(SEXP env, SEXP sym) { #endif } +/// Get a namespace from the namespace registry +/// +/// Returns `R_NilValue` if the namespace is not in the registry, i.e. if the package has +/// not been loaded yet. +/// +/// Unlike `R_FindNamespace()`, does not attempt to load the package, does not error when +/// the namespace can't be found, and does not go through R. +/// +/// SAFETY: Keep as a pure C function. Call like an R API function, i.e. wrap in `safe[]` +/// as required. +inline SEXP r_ns_env(const char* name) { +#if R_VERSION >= R_Version(4, 6, 0) + return R_getRegisteredNamespace(name); +#else + SEXP sym = Rf_install(name); + SEXP out = Rf_findVarInFrame3(R_NamespaceRegistry, sym, TRUE); + if (out == R_UnboundValue) { + out = R_NilValue; + } + return out; +#endif +} + } // namespace detail template diff --git a/inst/include/cpp11/function.hpp b/inst/include/cpp11/function.hpp index 8b802453..23820ea2 100644 --- a/inst/include/cpp11/function.hpp +++ b/inst/include/cpp11/function.hpp @@ -8,7 +8,7 @@ #include "cpp11/R.hpp" // for SEXP, SEXPREC, CDR, Rf_install, SETCAR #include "cpp11/as.hpp" // for as_sexp #include "cpp11/named_arg.hpp" // for named_arg -#include "cpp11/protect.hpp" // for protect, protect::function, safe +#include "cpp11/protect.hpp" // for protect, protect::function, safe, stop #include "cpp11/sexp.hpp" // for sexp #ifdef CPP11_USE_FMT @@ -71,8 +71,11 @@ class package { if (strcmp(name, "base") == 0) { return R_BaseEnv; } - sexp name_sexp = safe[Rf_install](name); - return safe[detail::r_env_get](R_NamespaceRegistry, name_sexp); + SEXP env = safe[detail::r_ns_env](name); + if (env == R_NilValue) { + stop("Can't find namespace: '%s'.", name); + } + return env; } // Either base env or in namespace registry, so no protection needed diff --git a/tests/testthat/_snaps/source.md b/tests/testthat/_snaps/source.md index ad9152aa..3435da17 100644 --- a/tests/testthat/_snaps/source.md +++ b/tests/testthat/_snaps/source.md @@ -7,3 +7,11 @@ ! Can't find `file` at this path: {NON_EXISTENT_FILEPATH} +# `cpp11::package` throws expected error on unknown packages + + Code + test() + Condition + Error: + ! Can't find namespace: 'definitely_not_a_package'. + diff --git a/tests/testthat/test-source.R b/tests/testthat/test-source.R index 2945d51d..0c7fd0fa 100644 --- a/tests/testthat/test-source.R +++ b/tests/testthat/test-source.R @@ -227,3 +227,22 @@ test_that("cpp_source fails informatively for nonexistent file", { transform = ~ sub("^.+[.]cpp$", "{NON_EXISTENT_FILEPATH}", .x) ) }) + +test_that("`cpp11::package` throws expected error on unknown packages", { + skip_on_os("solaris") + dll_info <- cpp_source( + code = ' + #include "cpp11/function.hpp" + + [[cpp11::register]] + SEXP test() { + auto pkg = cpp11::package("definitely_not_a_package"); + return R_NilValue; + } + ', clean = TRUE) + on.exit(dyn.unload(dll_info[["path"]])) + + expect_snapshot(error = TRUE, { + test() + }) +}) From 64694cf020c74aa9831d4484e62d2fa5efcab4f9 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 2 Apr 2026 15:52:15 -0400 Subject: [PATCH 4/5] Define `RCPP_NO_R_HEADERS_CHECK` before all `#include ` usage `#include ` sets everything up the right way, and otherwise we get a warning from Rcpp which doesn't seem to end up being relevant for this use case --- R/register.R | 8 +++++++- cpp11test/src/cpp11.cpp | 1 + cpp11test/src/find-intervals.cpp | 1 + cpp11test/src/matrix.cpp | 1 + cpp11test/src/protect.cpp | 3 ++- cpp11test/src/release.cpp | 3 ++- cpp11test/src/test-as.cpp | 3 ++- 7 files changed, 16 insertions(+), 4 deletions(-) diff --git a/R/register.R b/R/register.R index 3ea10d52..c35cc71a 100644 --- a/R/register.R +++ b/R/register.R @@ -89,7 +89,13 @@ cpp_register <- function(path = ".", quiet = !is_interactive(), extension = c(". extra_includes <- character() if (pkg_links_to_rcpp(path)) { - extra_includes <- c(extra_includes, "#include ", "#include ", "using namespace Rcpp;") + extra_includes <- c( + extra_includes, + "#include ", + "#define RCPP_NO_R_HEADERS_CHECK", + "#include ", + "using namespace Rcpp;" + ) } pkg_types <- c( diff --git a/cpp11test/src/cpp11.cpp b/cpp11test/src/cpp11.cpp index 421de637..0eb9809c 100644 --- a/cpp11test/src/cpp11.cpp +++ b/cpp11test/src/cpp11.cpp @@ -2,6 +2,7 @@ // clang-format off #include +#define RCPP_NO_R_HEADERS_CHECK #include using namespace Rcpp; #include "cpp11/declarations.hpp" diff --git a/cpp11test/src/find-intervals.cpp b/cpp11test/src/find-intervals.cpp index 0e7933e2..0c9ebcaa 100644 --- a/cpp11test/src/find-intervals.cpp +++ b/cpp11test/src/find-intervals.cpp @@ -2,6 +2,7 @@ #include "cpp11.hpp" using namespace cpp11; +#define RCPP_NO_R_HEADERS_CHECK #include #include using namespace Rcpp; diff --git a/cpp11test/src/matrix.cpp b/cpp11test/src/matrix.cpp index 10348945..82583cda 100644 --- a/cpp11test/src/matrix.cpp +++ b/cpp11test/src/matrix.cpp @@ -32,6 +32,7 @@ using namespace cpp11; return mat; } +#define RCPP_NO_R_HEADERS_CHECK #include using namespace Rcpp; diff --git a/cpp11test/src/protect.cpp b/cpp11test/src/protect.cpp index bb4ef934..f1ece491 100644 --- a/cpp11test/src/protect.cpp +++ b/cpp11test/src/protect.cpp @@ -1,7 +1,8 @@ #include #include -#include "Rcpp.h" +#define RCPP_NO_R_HEADERS_CHECK +#include [[cpp11::register]] void protect_one_(SEXP x, int n) { for (R_xlen_t i = 0; i < n; ++i) { diff --git a/cpp11test/src/release.cpp b/cpp11test/src/release.cpp index 7b321e45..03d158de 100644 --- a/cpp11test/src/release.cpp +++ b/cpp11test/src/release.cpp @@ -1,7 +1,8 @@ #include #include "cpp11/sexp.hpp" -#include "Rcpp.h" +#define RCPP_NO_R_HEADERS_CHECK +#include [[cpp11::register]] void cpp11_release_(int n) { std::vector x; diff --git a/cpp11test/src/test-as.cpp b/cpp11test/src/test-as.cpp index 798596a2..6fef6473 100644 --- a/cpp11test/src/test-as.cpp +++ b/cpp11test/src/test-as.cpp @@ -7,7 +7,8 @@ #include -#include "Rcpp.h" +#define RCPP_NO_R_HEADERS_CHECK +#include context("as_cpp-C++") { test_that("as_cpp(INTSEXP)") { From 7a82cffa9a691178910b162e633a053287018271 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Fri, 3 Apr 2026 10:12:41 -0400 Subject: [PATCH 5/5] Use `r_env_has()` + `r_env_get()` To avoid triggering a NOTE about usage of `Rf_findVarInFrame3()` on R 4.5, where technically we had the tools to avoid that --- inst/include/cpp11/R.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/inst/include/cpp11/R.hpp b/inst/include/cpp11/R.hpp index 30808d01..f6a398d3 100644 --- a/inst/include/cpp11/R.hpp +++ b/inst/include/cpp11/R.hpp @@ -128,11 +128,11 @@ inline SEXP r_ns_env(const char* name) { return R_getRegisteredNamespace(name); #else SEXP sym = Rf_install(name); - SEXP out = Rf_findVarInFrame3(R_NamespaceRegistry, sym, TRUE); - if (out == R_UnboundValue) { - out = R_NilValue; + if (r_env_has(R_NamespaceRegistry, sym)) { + return r_env_get(R_NamespaceRegistry, sym); + } else { + return R_NilValue; } - return out; #endif }