diff --git a/.Rbuildignore b/.Rbuildignore index 64aa8b0a..cef39675 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -24,3 +24,4 @@ script.R ^revdep$ ^talk$ \.cache$ +^MAINTENANCE\.md$ diff --git a/.github/workflows/format.yaml b/.github/workflows/format.yaml index a9670588..db1af2b4 100644 --- a/.github/workflows/format.yaml +++ b/.github/workflows/format.yaml @@ -1,9 +1,9 @@ on: push: - branches: master + branches: main pull_request: branches: - - master + - main name: format_check @@ -21,4 +21,3 @@ jobs: - name: Check for a non-empty diff run: git diff-files -U --exit-code - diff --git a/DESCRIPTION b/DESCRIPTION index 81fb617f..11118f83 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,15 +1,15 @@ Package: cpp11 Title: A C++11 Interface for R's C Interface -Version: 0.4.0.9000 +Version: 0.4.2.9000 Authors@R: c(person(given = "Jim", family = "Hester", - role = c("aut", "cre"), - email = "jim.hester@rstudio.com", + role = "aut", comment = c(ORCID = "0000-0002-2739-7082")), person(given = "Romain", family = "François", - role = "ctb"), + role = c("aut", "cre"), + email = "romain@rstudio.com"), person(given = "Benjamin", family = "Kietzman", role = "ctb"), @@ -58,5 +58,5 @@ Config/Needs/cpp11/cpp_register: vctrs Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.1.1 +RoxygenNote: 7.1.2 SystemRequirements: C++11 diff --git a/MAINTENANCE.md b/MAINTENANCE.md new file mode 100644 index 00000000..c561332f --- /dev/null +++ b/MAINTENANCE.md @@ -0,0 +1,49 @@ +## Current state + +The state of cpp11 is pretty stable, it seems to have the features we need for most of our projects using C++. + +## Known outstanding issues + +### Running the cpp11test tests + +Most of the test suite is in a sub-package, cpp11test. +Probably the best way to run these tests is to install the development version of cpp11 and then run `devtools::test()` to run the cpp11test test suite. + +If tests failures occur the output from Catch isn't always easy to interpret. +I have a branch of testthat https://github.com/jimhester/testthat/tree/catch-detailed-output that should make things easier to understand. +I contributed those changes to the main testthat, but something changed after merging the more detailed output was lost, I unfortunately never had the time to track down the cause and fix it. + +In addition getting a debugger to catch when errors happen can be fiddly when running the cpp11test tests, something about the way that Catch redirects stderr / stdout interacts with the debugger. + +The GitHub Actions workflow has some additional logic to handle running the cpp11 tests https://github.com/r-lib/cpp11/blob/fd8ef97d006db847f7f17166cf52e1e0383b2d35/.github/workflows/R-CMD-check.yaml#L95-L102, https://github.com/r-lib/cpp11/blob/fd8ef97d006db847f7f17166cf52e1e0383b2d35/.github/workflows/R-CMD-check.yaml#L117-L124. + +### False positive URL checks for git repositories in the vignettes + +If you run `urlchecker::url_check()` on the repo you will see the following false positives. + +``` +! Warning: vignettes/motivations.Rmd:363:11 Moved +git clone https://github.com/r-lib/cpp11.git + ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + https://github.com/r-lib/cpp11 +! Warning: vignettes/motivations.Rmd:354:11 Moved +git clone https://github.com/RcppCore/Rcpp.git + ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + https://github.com/RcppCore/Rcpp +> +``` + +These only happen with the urlchecker package, they can be safely ignored and the real CRAN checks will not show them. + + +### Ensure you use `Sys.setenv("CPP11_EVAL" = "true"); devtools::submit_cran()` when submitting. + +If you forget to set `CPP_EVAL = "true"` then the vignette chunks will not run properly and the vignettes will not be rendered properly. + +## Future directions + +Some work could be spent in smoothing out the `cpp_source()` / knitr chunk experience. +Our main focus and use cases were in R packages, so that usage is more tested. +Because we don't typically use cpp11 in non package contexts those use cases may not be as nice. + +For similar reasons the matrix support might be somewhat lacking, as the majority of our use cases do not deal with numeric matrices. diff --git a/NEWS.md b/NEWS.md index 4d0cf384..e4c4281e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,15 @@ # cpp11 (development version) +* `cpp_source()` errors on non-existent file (#261). + +# cpp11 0.4.2 + +* Romain François is now the maintainer. + +# cpp11 0.4.1 + +* Fix crash related to unwind protect optimization (#244) + # cpp11 0.4.0 ## New Features diff --git a/R/source.R b/R/source.R index 0632228d..d2820ccd 100644 --- a/R/source.R +++ b/R/source.R @@ -67,6 +67,9 @@ #' @export cpp_source <- function(file, code = NULL, env = parent.frame(), clean = TRUE, quiet = TRUE, cxx_std = Sys.getenv("CXX_STD", "CXX11"), dir = tempfile()) { stop_unless_installed(c("brio", "callr", "cli", "decor", "desc", "glue", "tibble", "vctrs")) + if (!missing(file) && !file.exists(file)) { + stop("Can't find `file` at this path:\n", file, "\n", call. = FALSE) + } dir.create(dir, showWarnings = FALSE, recursive = TRUE) dir.create(file.path(dir, "R"), showWarnings = FALSE) diff --git a/R/vendor.R b/R/vendor.R index ed445c55..5fe10fe2 100644 --- a/R/vendor.R +++ b/R/vendor.R @@ -13,7 +13,7 @@ #' #' **Note**: vendoring places the responsibility of updating the code on #' **you**. Bugfixes and new features in cpp11 will not be available for your -#' code until you run `vector_cpp11()` again. +#' code until you run `cpp_vendor()` again. #' #' @inheritParams cpp_register #' @return The file path to the vendored code (invisibly). diff --git a/README.md b/README.md index 56c44401..3f090a4e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![R-CMD-check](https://github.com/r-lib/cpp11/workflows/R-CMD-check/badge.svg)](https://github.com/r-lib/cpp11/actions) -[![codecov](https://codecov.io/gh/r-lib/cpp11/branch/master/graph/badge.svg?token=EEWYoCYxQ2)](https://codecov.io/gh/r-lib/cpp11) +[![codecov](https://app.codecov.io/gh/r-lib/cpp11/branch/main/graph/badge.svg?token=EEWYoCYxQ2)](https://app.codecov.io/gh/r-lib/cpp11) [![Lifecycle: stable](https://img.shields.io/badge/lifecycle-stable-brightgreen.svg)](https://lifecycle.r-lib.org/articles/stages.html#stable) [![CRAN status](https://www.r-pkg.org/badges/version/cpp11)](https://CRAN.R-project.org/package=cpp11) diff --git a/cpp11test/src/test-complexes.cpp b/cpp11test/src/test-complexes.cpp new file mode 100644 index 00000000..f7c69145 --- /dev/null +++ b/cpp11test/src/test-complexes.cpp @@ -0,0 +1,516 @@ +#include "cpp11/complexes.hpp" +#include "cpp11/strings.hpp" + +#include + +// To simplify comparison testing +inline bool operator==(const Rcomplex& a, const Rcomplex& b) { + return a.r == b.r && a.i == b.i; +} + +context("complexes-C++") { + test_that("complexes::r_vector(SEXP)") { + cpp11::complexes x(Rf_allocVector(CPLXSXP, 2)); + expect_true(x.size() == 2); + + expect_error(cpp11::complexes(Rf_allocVector(INTSXP, 2))); + } + + test_that("complexes::r_vector::const_iterator()") { + cpp11::complexes x(Rf_allocVector(CPLXSXP, 100)); + COMPLEX(x)[0] = Rcomplex{1, 1}; + COMPLEX(x)[1] = Rcomplex{2, 2}; + COMPLEX(x)[2] = Rcomplex{3, 3}; + COMPLEX(x)[3] = Rcomplex{4, 4}; + COMPLEX(x)[4] = Rcomplex{5, 5}; + COMPLEX(x)[97] = Rcomplex{98, 98}; + COMPLEX(x)[98] = Rcomplex{99, 99}; + COMPLEX(x)[99] = Rcomplex{100, 100}; + expect_true(x.size() == 100); + + auto it = x.begin(); + auto it2 = x.begin(); + expect_true(it == it2); + + ++it; + expect_true(!(it == it2)); + expect_true(it != it2); + + Rcomplex two = Rcomplex{2, 2}; + Rcomplex three = Rcomplex{3, 3}; + Rcomplex ninety_nine = Rcomplex{99, 99}; + Rcomplex ninety_eight = Rcomplex{98, 98}; + Rcomplex one_hundred = Rcomplex{100, 100}; + + ++it; + expect_true(*it == three); + --it; + expect_true(*it == two); + --it; + + it += 99; + expect_true(*it == one_hundred); + --it; + expect_true(*it == ninety_nine); + --it; + expect_true(*it == ninety_eight); + it -= 95; + expect_true(*it == three); + } + + test_that("complexes.push_back()") { + Rcomplex one = Rcomplex{1, 1}; + Rcomplex two = Rcomplex{2, 2}; + + cpp11::writable::complexes x; + x.push_back(one); + x.push_back(two); + + expect_true(x.size() == 2); + expect_true(x[0] == one); + expect_true(x[1] == two); + } + test_that("complexes.resize()") { + Rcomplex one = Rcomplex{1, 1}; + Rcomplex two = Rcomplex{2, 2}; + + cpp11::writable::complexes x; + x.resize(2); + x[0] = one; + x[1] = two; + + expect_true(x.size() == 2); + expect_true(x[0] == one); + expect_true(x[1] == two); + } + test_that("complexes.at()") { + cpp11::writable::complexes x; + + expect_error(x.at(-1)); + + expect_error(x.at(0)); + + Rcomplex one = Rcomplex{1, 1}; + + x.push_back(one); + expect_true(x.at(0) == one); + expect_error(x.at(1)); + } + test_that("complexes.pop_back()") { + Rcomplex one = Rcomplex{1, 1}; + Rcomplex two = Rcomplex{2, 2}; + + cpp11::writable::complexes x; + + x.push_back(one); + x.push_back(two); + x.pop_back(); + + expect_true(x.size() == 1); + expect_true(x[0] == one); + + expect_error(x.at(1)); + } + test_that("complexes.insert()") { + Rcomplex one = Rcomplex{1, 1}; + Rcomplex two = Rcomplex{2, 2}; + Rcomplex three = Rcomplex{3, 3}; + + cpp11::writable::complexes x; + + x.insert(0, one); + x.insert(0, two); + x.insert(1, three); + expect_true(x.size() == 3); + + expect_true(x[0] == two); + expect_true(x[1] == three); + expect_true(x[2] == one); + } + test_that("complexes.erase()") { + Rcomplex one = Rcomplex{1, 1}; + Rcomplex two = Rcomplex{2, 2}; + Rcomplex three = Rcomplex{3, 3}; + Rcomplex four = Rcomplex{4, 4}; + Rcomplex five = Rcomplex{5, 5}; + + cpp11::writable::complexes x; + + x.push_back(one); + x.push_back(two); + x.push_back(three); + x.push_back(four); + x.push_back(five); + + expect_true(x.size() == 5); + + x.erase(0); + + expect_true(x.size() == 4); + expect_true(x[0] == two); + expect_true(x[1] == three); + expect_true(x[2] == four); + expect_true(x[3] == five); + + x.erase(2); + + expect_true(x.size() == 3); + expect_true(x[0] == two); + expect_true(x[1] == three); + expect_true(x[2] == five); + } + test_that("complexes.iterator* = ") { + Rcomplex one = Rcomplex{1, 1}; + Rcomplex two = Rcomplex{2, 2}; + Rcomplex three = Rcomplex{3, 3}; + Rcomplex four = Rcomplex{4, 4}; + + cpp11::writable::complexes x; + x.push_back(one); + x.push_back(two); + x.push_back(three); + auto it = x.begin() + 1; + *it = three; + ++it; + *it = four; + + expect_true(x.size() == 3); + expect_true(x[0] == one); + expect_true(x[1] == three); + expect_true(x[2] == four); + } + + test_that("writable::complexes(SEXP)") { + Rcomplex one = Rcomplex{1, 1}; + Rcomplex two = Rcomplex{2, 2}; + Rcomplex three = Rcomplex{3, 3}; + Rcomplex four = Rcomplex{4, 4}; + Rcomplex five = Rcomplex{5, 5}; + Rcomplex six = Rcomplex{6, 6}; + Rcomplex seven = Rcomplex{7, 7}; + + SEXP x = PROTECT(Rf_allocVector(CPLXSXP, 5)); + + COMPLEX(x)[0] = one; + COMPLEX(x)[1] = two; + COMPLEX(x)[2] = three; + COMPLEX(x)[3] = four; + COMPLEX(x)[4] = five; + + cpp11::writable::complexes y(x); + y[0] = six; + + expect_true(x != y.data()); + + expect_true(COMPLEX(x)[0] == one); + expect_true(y[0] == six); + + cpp11::writable::complexes z(y); + z[0] = seven; + + expect_true(z.data() != y.data()); + + expect_true(COMPLEX(x)[0] == one); + expect_true(y[0] == six); + expect_true(z[0] == seven); + + UNPROTECT(1); + } + test_that("writable::complexes(SEXP, bool)") { + Rcomplex five = Rcomplex{5, 5}; + SEXP x = PROTECT(Rf_ScalarComplex(five)); + cpp11::writable::complexes y(x, false); + + expect_true(COMPLEX(y)[0] == five); + UNPROTECT(1); + } + + test_that("writable::complexes(SEXP) assignment") { + Rcomplex zero = Rcomplex{0, 0}; + Rcomplex one = Rcomplex{1, 1}; + Rcomplex two = Rcomplex{2, 2}; + Rcomplex three = Rcomplex{3, 3}; + + cpp11::writable::complexes x({one, two, three}); + cpp11::writable::complexes y({zero}); + y = x; + expect_true(y.size() == 3); + expect_true(y.data() != x.data()); + expect_true(y.is_altrep() == x.is_altrep()); + } + + test_that("writable::complexes(SEXP) move assignment") { + Rcomplex zero = Rcomplex{0, 0}; + Rcomplex one = Rcomplex{1, 1}; + Rcomplex two = Rcomplex{2, 2}; + Rcomplex three = Rcomplex{3, 3}; + + cpp11::writable::complexes x({one, two, three}); + cpp11::writable::complexes y({zero}); + auto x_data = x.data(); + + y = std::move(x); + expect_true(y.size() == 3); + expect_true(y.data() == x_data); + expect_true(y.is_altrep() == false); + } + + test_that("complexes::names(empty)") { + cpp11::complexes x; + auto nms = x.names(); + expect_true(nms.size() == 0); + } + + test_that("complexes::names") { + Rcomplex one = Rcomplex{1, 1}; + Rcomplex two = Rcomplex{2, 2}; + + using namespace cpp11::literals; + cpp11::writable::complexes x({"a"_nm = one, "b"_nm = two}); + expect_true(x[0] == one); + expect_true(x[1] == two); + + expect_true(x.contains("a")); + expect_true(!x.contains("c")); + + expect_true(x["a"] == one); + expect_true(x["b"] == two); + + cpp11::sexp nms(x.names()); + expect_true(Rf_xlength(nms) == 2); + auto nms0 = CHAR(STRING_ELT(nms, 0)); + auto nms1 = CHAR(STRING_ELT(nms, 1)); + expect_true(strcmp(nms0, "a") == 0); + expect_true(strcmp(nms1, "b") == 0); + } + + test_that("complexes::attr") { + cpp11::complexes x(PROTECT(Rf_allocVector(CPLXSXP, 2))); + COMPLEX(x)[0] = Rcomplex{1, 1}; + COMPLEX(x)[1] = Rcomplex{2, 2}; + + SEXP foo = Rf_install("foo"); + Rf_setAttrib(x, foo, Rf_mkString("bar")); + + // This doesn't compile by design + // x.attr("foo") = "bar"; + + // But this will + cpp11::writable::complexes y(x); + y.attr("foo") = "baz"; + + expect_true(strcmp(CHAR(STRING_ELT(x.attr("foo"), 0)), "bar") == 0); + expect_true(strcmp(CHAR(STRING_ELT(y.attr("foo"), 0)), "baz") == 0); + + UNPROTECT(1); + } + + test_that("writable::complexes(std::vector::iterator)") { + Rcomplex one = Rcomplex{1, 1}; + Rcomplex two = Rcomplex{2, 2}; + Rcomplex three = Rcomplex{3, 3}; + Rcomplex four = Rcomplex{4, 4}; + Rcomplex five = Rcomplex{5, 5}; + + std::vector x({one, two, three, four, five}); + cpp11::writable::complexes y(x.begin(), x.end()); + + expect_true(y.size() == 5); + expect_true(y[0] == one); + expect_true(y[4] == five); + } + + test_that("writable::complexes(std::vector)") { + Rcomplex one = Rcomplex{1, 1}; + Rcomplex two = Rcomplex{2, 2}; + Rcomplex three = Rcomplex{3, 3}; + Rcomplex four = Rcomplex{4, 4}; + Rcomplex five = Rcomplex{5, 5}; + + std::vector x({one, two, three, four, five}); + cpp11::writable::complexes y(x); + + expect_true(y.size() == 5); + expect_true(y[0] == one); + expect_true(y[4] == five); + } + + test_that("writable::complexes attributes are kept when converted to complexes") { + Rcomplex one = Rcomplex{1, 1}; + Rcomplex two = Rcomplex{2, 2}; + + cpp11::writable::complexes x({one, two}); + x.names() = {"a", "b"}; + cpp11::strings x_nms(x.names()); + expect_true(x_nms[0] == "a"); + expect_true(x_nms[1] == "b"); + + cpp11::complexes y(x); + cpp11::strings y_nms(y.names()); + expect_true(y_nms[0] == "a"); + expect_true(y_nms[1] == "b"); + } + + test_that("comparison operator works") { + Rcomplex one = Rcomplex{1, 1}; + Rcomplex two = Rcomplex{2, 2}; + Rcomplex three = Rcomplex{3, 3}; + + using namespace cpp11; + cpp11::complexes base(Rf_allocVector(CPLXSXP, 2)); + cpp11::complexes same_values(Rf_allocVector(CPLXSXP, 2)); + cpp11::complexes diff_length(Rf_allocVector(CPLXSXP, 1)); + cpp11::complexes diff_values(Rf_allocVector(CPLXSXP, 2)); + + COMPLEX(base)[0] = one; + COMPLEX(base)[1] = two; + + COMPLEX(same_values)[0] = one; + COMPLEX(same_values)[1] = two; + + COMPLEX(diff_length)[0] = one; + + COMPLEX(diff_values)[0] = one; + COMPLEX(diff_values)[1] = three; + + expect_true(base == base); + expect_true(base == same_values); + expect_true(!(base == diff_length)); + expect_true(!(base == diff_values)); + + expect_true(!(base != base)); + expect_true(!(base != same_values)); + expect_true(base != diff_length); + expect_true(base != diff_values); + } + test_that("na()") { + Rcomplex x = cpp11::na(); + // Not `ISNA()`, checking specifically for `NA_REAL` + expect_true(R_IsNA(x.r)); + expect_true(R_IsNA(x.i)); + } + test_that("is_na(Rcomplex)") { + Rcomplex x{1, 2}; + expect_true(!cpp11::is_na(x)); + + Rcomplex na_na{NA_REAL, NA_REAL}; + Rcomplex na_real{NA_REAL, 1}; + Rcomplex real_na{1, NA_REAL}; + + expect_true(cpp11::is_na(na_na)); + expect_true(cpp11::is_na(na_real)); + expect_true(cpp11::is_na(real_na)); + } + + test_that("complexes operator[] and at") { + Rcomplex one{1, 1}; + Rcomplex two{2, 2}; + + cpp11::complexes x(Rf_allocVector(CPLXSXP, 2)); + COMPLEX(x)[0] = one; + COMPLEX(x)[1] = two; + + int i0 = 0; + R_xlen_t x0 = 0; + size_t s0 = 0; + + expect_true(x[i0] == one); + expect_true(x[x0] == one); + expect_true(x[s0] == one); + + expect_true(x.at(i0) == one); + expect_true(x.at(x0) == one); + expect_true(x.at(s0) == one); + } + + test_that("writable::complexes operator[] and at") { + Rcomplex one{1, 1}; + Rcomplex two{2, 2}; + + cpp11::writable::complexes x(Rf_allocVector(CPLXSXP, 2)); + COMPLEX(x)[0] = one; + COMPLEX(x)[1] = two; + + int i0 = 0; + R_xlen_t x0 = 0; + size_t s0 = 0; + + expect_true(x[i0] == one); + expect_true(x[x0] == one); + expect_true(x[s0] == one); + + expect_true(x.at(i0) == one); + expect_true(x.at(x0) == one); + expect_true(x.at(s0) == one); + } + + test_that("operator[] and at with names") { + Rcomplex one{1, 1}; + Rcomplex two{2, 2}; + + using namespace cpp11::literals; + cpp11::writable::complexes x({"a"_nm = one, "b"_nm = two}); + cpp11::complexes y(x); + + expect_true(x["a"] == one); + expect_true(x["b"] == two); + expect_error(x["c"] == two); + + expect_true(y["a"] == one); + expect_true(y["b"] == two); + expect_error(y["c"] == two); + } + + test_that("complexes::find") { + Rcomplex one{1, 1}; + Rcomplex two{2, 2}; + + using namespace cpp11::literals; + cpp11::writable::complexes x({"a"_nm = one, "b"_nm = two}); + cpp11::complexes y(x); + + expect_true(x.find("a") == x.begin()); + expect_true(x.find("b") == x.begin() + 1); + expect_true(x.find("c") == x.end()); + + expect_true(y.find("a") == y.begin()); + expect_true(y.find("b") == y.begin() + 1); + expect_true(y.find("c") == y.end()); + } + + test_that("writable::complexes compound assignments") { + Rcomplex one{1, 1}; + Rcomplex two{2, 2}; + + cpp11::writable::complexes x(Rf_allocVector(CPLXSXP, 1)); + COMPLEX(x)[0] = one; + + auto x0 = x[0]; + expect_true(x0 == one); + + // Arithmetic is not defined on Rcomplex, + // so using it on a proxy also fails and is not defined + // expect_error(x0 += two); + // expect_error(x0 -= two); + // expect_error(x0 *= two); + // expect_error(x0 /= two); + // expect_error(x0--); + // expect_error(x0++); + // expect_error(++x0); + // expect_error(--x0); + } + + test_that("writable::doubles convert to doubles with correct size (#128)") { + Rcomplex one{1, 1}; + Rcomplex two{2, 2}; + Rcomplex three{3, 3}; + + cpp11::writable::complexes foo; + foo.push_back(one); + foo.push_back(two); + foo.push_back(three); + + cpp11::complexes bar(foo); + expect_true(Rf_xlength(bar) == 3); + } +} diff --git a/cran-comments.md b/cran-comments.md index d9619dc6..e69de29b 100644 --- a/cran-comments.md +++ b/cran-comments.md @@ -1,16 +0,0 @@ -## Test environments - -* GitHub Actions (ubuntu-18.04): devel, release, oldrel-1, oldrel-2, oldrel-3, oldrel-4 -* GitHub Actions (windows): release, 3.6 -* Github Actions (macOS): release - -## R CMD check results - -0 errors | 0 warnings | 0 note - -## revdepcheck results - -We checked 27 reverse dependencies (26 from CRAN + 1 from Bioconductor), comparing R CMD check results across CRAN and dev versions of this package. - - * We saw 0 new problems - * We failed to check 0 packages diff --git a/inst/include/cpp11.hpp b/inst/include/cpp11.hpp index 71e1cf1d..c6b79be1 100644 --- a/inst/include/cpp11.hpp +++ b/inst/include/cpp11.hpp @@ -4,6 +4,7 @@ #include "cpp11/altrep.hpp" #include "cpp11/as.hpp" #include "cpp11/attribute_proxy.hpp" +#include "cpp11/complexes.hpp" #include "cpp11/data_frame.hpp" #include "cpp11/doubles.hpp" #include "cpp11/environment.hpp" diff --git a/inst/include/cpp11/as.hpp b/inst/include/cpp11/as.hpp index 6b4eb20f..39aadfb1 100644 --- a/inst/include/cpp11/as.hpp +++ b/inst/include/cpp11/as.hpp @@ -62,6 +62,9 @@ using enable_if_enum = enable_if_t::value, R>; template using enable_if_bool = enable_if_t::value, R>; +template +using enable_if_rcomplex = enable_if_t::value, R>; + template using enable_if_char = enable_if_t::value, R>; @@ -140,6 +143,17 @@ enable_if_bool as_cpp(SEXP from) { throw std::length_error("Expected single logical value"); } +template +enable_if_rcomplex as_cpp(SEXP from) { + if (Rf_isComplex(from)) { + if (Rf_xlength(from) == 1) { + return COMPLEX_ELT(from, 0); + } + } + + throw std::length_error("Expected single complex value"); +} + template enable_if_floating_point as_cpp(SEXP from) { if (Rf_isReal(from)) { @@ -218,6 +232,11 @@ enable_if_bool as_sexp(T from) { return safe[Rf_ScalarLogical](from); } +template +enable_if_rcomplex as_sexp(T from) { + return safe[Rf_ScalarComplex](from); +} + template enable_if_c_string as_sexp(T from) { return unwind_protect([&] { return Rf_ScalarString(Rf_mkCharCE(from, CE_UTF8)); }); @@ -282,6 +301,25 @@ inline SEXP as_sexp(std::initializer_list from) { return as_sexp>(from); } +template > +enable_if_rcomplex as_sexp(const Container& from) { + R_xlen_t size = from.size(); + SEXP data = safe[Rf_allocVector](CPLXSXP, size); + + auto it = from.begin(); + Rcomplex* data_p = COMPLEX(data); + for (R_xlen_t i = 0; i < size; ++i, ++it) { + data_p[i] = *it; + } + return data; +} + +inline SEXP as_sexp(std::initializer_list from) { + return as_sexp>(from); +} + + namespace detail { template SEXP as_sexp_strings(const Container& from, AsCstring&& c_str) { diff --git a/inst/include/cpp11/complexes.hpp b/inst/include/cpp11/complexes.hpp new file mode 100644 index 00000000..a608459e --- /dev/null +++ b/inst/include/cpp11/complexes.hpp @@ -0,0 +1,153 @@ +#pragma once + +#include // for min +#include // for array +#include // for initializer_list + +#include "R_ext/Arith.h" // for NA_REAL +#include "cpp11/R.hpp" // for SEXP, SEXPREC, Rf_allocVector +#include "cpp11/as.hpp" // for as_sexp +#include "cpp11/attribute_proxy.hpp" // for attribute_proxy +#include "cpp11/named_arg.hpp" // for named_arg +#include "cpp11/protect.hpp" // for preserved +#include "cpp11/r_vector.hpp" // for r_vector, r_vector<>::proxy +#include "cpp11/sexp.hpp" // for sexp + +// Specializations for complex + +namespace cpp11 { + +template <> +inline SEXP r_vector::valid_type(SEXP data) { + if (data == nullptr) { + throw type_error(CPLXSXP, NILSXP); + } + if (TYPEOF(data) != CPLXSXP) { + throw type_error(CPLXSXP, TYPEOF(data)); + } + return data; +} + +template <> +inline Rcomplex r_vector::operator[](const R_xlen_t pos) const { + // NOPROTECT: likely too costly to unwind protect every elt + return is_altrep_ ? COMPLEX_ELT(data_, pos) : data_p_[pos]; +} + +template <> +inline Rcomplex* r_vector::get_p(bool is_altrep, SEXP data) { + if (is_altrep) { + return nullptr; + } else { + return COMPLEX(data); + } +} + +template <> +inline void r_vector::const_iterator::fill_buf(R_xlen_t pos) { + length_ = std::min(64_xl, data_->size() - pos); + COMPLEX_GET_REGION(data_->data_, pos, length_, buf_.data()); + block_start_ = pos; +} + +typedef r_vector complexes; + +namespace writable { + +template <> +inline typename r_vector::proxy& r_vector::proxy::operator=( + const Rcomplex& rhs) { + if (is_altrep_) { + // NOPROTECT: likely too costly to unwind protect every set elt + SET_COMPLEX_ELT(data_, index_, rhs); + } else { + *p_ = rhs; + } + return *this; +} + +template <> +inline r_vector::proxy::operator Rcomplex() const { + if (p_ == nullptr) { + // NOPROTECT: likely too costly to unwind protect every elt + return COMPLEX_ELT(data_, index_); + } else { + return *p_; + } +} + +template <> +inline r_vector::r_vector(std::initializer_list il) + : cpp11::r_vector(as_sexp(il)), capacity_(il.size()) {} + +template <> +inline void r_vector::reserve(R_xlen_t new_capacity) { + data_ = data_ == R_NilValue ? safe[Rf_allocVector](CPLXSXP, new_capacity) + : safe[Rf_xlengthgets](data_, new_capacity); + SEXP old_protect = protect_; + + // Protect the new data + protect_ = preserved.insert(data_); + + // Release the old protection; + preserved.release(old_protect); + + data_p_ = COMPLEX(data_); + capacity_ = new_capacity; +} + +template <> +inline r_vector::r_vector(std::initializer_list il) + : cpp11::r_vector(safe[Rf_allocVector](CPLXSXP, il.size())), + capacity_(il.size()) { + protect_ = preserved.insert(data_); + int n_protected = 0; + + try { + unwind_protect([&] { + Rf_setAttrib(data_, R_NamesSymbol, Rf_allocVector(STRSXP, capacity_)); + SEXP names = PROTECT(Rf_getAttrib(data_, R_NamesSymbol)); + ++n_protected; + auto it = il.begin(); + for (R_xlen_t i = 0; i < capacity_; ++i, ++it) { + data_p_[i] = COMPLEX_ELT(it->value(), 0); + SET_STRING_ELT(names, i, Rf_mkCharCE(it->name(), CE_UTF8)); + } + UNPROTECT(n_protected); + }); + } catch (const unwind_exception& e) { + preserved.release(protect_); + UNPROTECT(n_protected); + throw e; + } +} + +template <> +inline void r_vector::push_back(Rcomplex value) { + while (length_ >= capacity_) { + reserve(capacity_ == 0 ? 1 : capacity_ *= 2); + } + if (is_altrep_) { + // NOPROTECT: likely too costly to unwind protect every elt + SET_COMPLEX_ELT(data_, length_, value); + } else { + data_p_[length_] = value; + } + ++length_; +} + +typedef r_vector complexes; + +} // namespace writable + +template <> +inline Rcomplex na() { + return Rcomplex{NA_REAL, NA_REAL}; +} + +template <> +inline bool is_na(const Rcomplex& x) { + return ISNA(x.r) || ISNA(x.i); +} + +} // namespace cpp11 diff --git a/inst/include/cpp11/protect.hpp b/inst/include/cpp11/protect.hpp index 4d63942d..ee332939 100644 --- a/inst/include/cpp11/protect.hpp +++ b/inst/include/cpp11/protect.hpp @@ -70,8 +70,6 @@ inline Rboolean& get_should_unwind_protect() { return should_unwind_protect[0]; } -static Rboolean& should_unwind_protect = get_should_unwind_protect(); - } // namespace detail #ifdef HAS_UNWIND_PROTECT @@ -82,11 +80,12 @@ static Rboolean& should_unwind_protect = get_should_unwind_protect(); template ()()), SEXP>::value>::type> SEXP unwind_protect(Fun&& code) { - if (detail::should_unwind_protect == FALSE) { + static auto should_unwind_protect = detail::get_should_unwind_protect(); + if (should_unwind_protect == FALSE) { return std::forward(code)(); } - detail::should_unwind_protect = FALSE; + should_unwind_protect = FALSE; static SEXP token = [] { SEXP res = R_MakeUnwindCont(); @@ -96,7 +95,7 @@ SEXP unwind_protect(Fun&& code) { std::jmp_buf jmpbuf; if (setjmp(jmpbuf)) { - detail::should_unwind_protect = TRUE; + should_unwind_protect = TRUE; throw unwind_exception(token); } @@ -121,7 +120,7 @@ SEXP unwind_protect(Fun&& code) { // unset it here before returning the value ourselves. SETCAR(token, R_NilValue); - detail::should_unwind_protect = TRUE; + should_unwind_protect = TRUE; return res; } diff --git a/inst/include/cpp11/r_complex.hpp b/inst/include/cpp11/r_complex.hpp new file mode 100644 index 00000000..20709058 --- /dev/null +++ b/inst/include/cpp11/r_complex.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include +#include // for complex +#include // for is_convertible, enable_if + +#include "R_ext/Arith.h" // for NA_REAL +#include "R_ext/Complex.h" // for Rcomplex +#include "cpp11/R.hpp" // for SEXP, SEXPREC, ... +#include "cpp11/as.hpp" // for as_sexp +#include "cpp11/protect.hpp" // for unwind_protect, preserved +#include "cpp11/sexp.hpp" // for sexp + +namespace cpp11 { + +class r_complex { + public: + r_complex() = default; + + r_complex(SEXP data) { + if (Rf_isComplex(data)) { + if (Rf_xlength(data) == 1) { + Rcomplex data_ = COMPLEX_ELT(data, 0); + real_ = data_.r; + imag_ = data_.i; + } + } + throw std::invalid_argument("Invalid r_complex value"); + } + + r_complex(Rcomplex value) : + real_(value.r), imag_(value.i) {} + r_complex(std::complex value) : + real_(value.real()), imag_(value.imag()) {} + r_complex(double real, double imag) : + real_(real), imag_(imag) {} + + operator Rcomplex() const { + return Rcomplex{real_, imag_}; + } + operator std::complex() const { + return std::complex(real_, imag_); + } + + bool operator==(r_complex rhs) const { + return (real_ == rhs.real_) && (imag_ == rhs.imag_); + } + bool operator==(Rcomplex rhs) const { return operator==(r_complex(rhs)); } + bool operator==(std::complex rhs) const { return operator==(r_complex(rhs)); } + + double real() const { return real_; } + double imag() const { return imag_; } + private: + double real_ = NA_REAL; + double imag_ = NA_REAL; +}; + +inline std::ostream& operator<<(std::ostream& os, const r_complex& value) { + os << value.real() << "+" << value.imag() << "i" ; + return os; +} + +template +using enable_if_r_complex = enable_if_t::value, R>; + +template +enable_if_r_complex as_sexp(T from) { + sexp res = Rf_allocVector(CPLXSXP, 1); + unwind_protect([&] { SET_COMPLEX_ELT(res.data(), 0, from); }); + return res; +} + +template <> +inline r_complex na() { + return r_complex{NA_REAL, NA_REAL}; +} + +} // namespace cpp11 diff --git a/man/cpp11-package.Rd b/man/cpp11-package.Rd index 4a4f147d..ed817fff 100644 --- a/man/cpp11-package.Rd +++ b/man/cpp11-package.Rd @@ -6,11 +6,7 @@ \alias{cpp11-package} \title{cpp11: A C++11 Interface for R's C Interface} \description{ -Provides a header only, C++11 interface to R's C - interface. Compared to other approaches 'cpp11' strives to be safe - against long jumps from the C API as well as C++ exceptions, conform - to normal R function semantics and supports interaction with 'ALTREP' - vectors. +Provides a header only, C++11 interface to R's C interface. Compared to other approaches 'cpp11' strives to be safe against long jumps from the C API as well as C++ exceptions, conform to normal R function semantics and supports interaction with 'ALTREP' vectors. } \seealso{ Useful links: @@ -22,11 +18,15 @@ Useful links: } \author{ -\strong{Maintainer}: Jim Hester \email{jim.hester@rstudio.com} (\href{https://orcid.org/0000-0002-2739-7082}{ORCID}) +\strong{Maintainer}: Romain François \email{romain@rstudio.com} + +Authors: +\itemize{ + \item Jim Hester (\href{https://orcid.org/0000-0002-2739-7082}{ORCID}) +} Other contributors: \itemize{ - \item Romain François [contributor] \item Benjamin Kietzman [contributor] \item RStudio [copyright holder, funder] } diff --git a/man/cpp_vendor.Rd b/man/cpp_vendor.Rd index abf7dd56..857e49cf 100644 --- a/man/cpp_vendor.Rd +++ b/man/cpp_vendor.Rd @@ -26,7 +26,7 @@ If you choose to vendor the headers you should \emph{remove} \code{LinkingTo: cp \strong{Note}: vendoring places the responsibility of updating the code on \strong{you}. Bugfixes and new features in cpp11 will not be available for your -code until you run \code{vector_cpp11()} again. +code until you run \code{cpp_vendor()} again. } \examples{ # create a new directory diff --git a/tests/testthat/_snaps/source.md b/tests/testthat/_snaps/source.md new file mode 100644 index 00000000..15935031 --- /dev/null +++ b/tests/testthat/_snaps/source.md @@ -0,0 +1,8 @@ +# cpp_source fails informatively for nonexistent file + + Code + cpp_source(i_do_not_exist) + Error + Can't find `file` at this path: + {NON_EXISTENT_FILEPATH} + diff --git a/tests/testthat/test-source.R b/tests/testthat/test-source.R index 8f813d0e..2945d51d 100644 --- a/tests/testthat/test-source.R +++ b/tests/testthat/test-source.R @@ -217,3 +217,13 @@ test_that("cpp_source(d) functions work after sourcing file more than once", { cpp11::cpp_source(test_path("single.cpp"), clean = TRUE) expect_equal(foo(), 1) }) + +test_that("cpp_source fails informatively for nonexistent file", { + i_do_not_exist <- tempfile(pattern = "nope-", fileext = ".cpp") + expect_false(file.exists(i_do_not_exist)) + expect_snapshot( + error = TRUE, + cpp_source(i_do_not_exist), + transform = ~ sub("^.+[.]cpp$", "{NON_EXISTENT_FILEPATH}", .x) + ) +}) diff --git a/vignettes/cpp11.Rmd b/vignettes/cpp11.Rmd index 6a15f081..3e827a5b 100644 --- a/vignettes/cpp11.Rmd +++ b/vignettes/cpp11.Rmd @@ -226,7 +226,7 @@ The C++ version is similar, but: Similar in-place operators are `-=`, `*=`, and `/=`. This is a good example of where C++ is much more efficient than R. -As shown by the following microbenchmark, `sumC()` is competitive with the built-in (and highly optimised) `sum()`, while `sumR()` is several orders of magnitude slower. +As shown by the following microbenchmark, `sum_cpp()` is competitive with the built-in (and highly optimised) `sum()`, while `sum_r()` is several orders of magnitude slower. ```{r sum-bench} x <- runif(1e3) diff --git a/vignettes/internals.Rmd b/vignettes/internals.Rmd index 25d8a295..3b55b698 100644 --- a/vignettes/internals.Rmd +++ b/vignettes/internals.Rmd @@ -68,12 +68,12 @@ Alternatively many IDEs support automatically running `clang-format` every time ## 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/master/inst/include). -R code used to register functions and for `cpp11::cpp_source()` is in [R/](https://github.com/r-lib/cpp11/tree/master/R). -Tests for _only_ the code in `R/` is in [tests/testthat/](https://github.com/r-lib/cpp11/tree/master/tests/testthat) -The rest of the code is in a separate [cpp11test/](https://github.com/r-lib/cpp11/tree/master/cpp11test) package included in the source tree. -Inside [cpp11test/src](https://github.com/r-lib/cpp11/tree/master/cpp11test/src) the files that start with `test-` are C++ tests using the [Catch](https://testthat.r-lib.org/reference/use_catch.html) support in testthat. -In addition there are some regular R tests in [cpp11test/tests/testthat/](https://github.com/r-lib/cpp11/tree/master/cpp11test/tests/testthat). +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). +R code used to register functions and for `cpp11::cpp_source()` is in [R/](https://github.com/r-lib/cpp11/tree/main/R). +Tests for _only_ the code in `R/` is in [tests/testthat/](https://github.com/r-lib/cpp11/tree/main/tests/testthat) +The rest of the code is in a separate [cpp11test/](https://github.com/r-lib/cpp11/tree/main/cpp11test) package included in the source tree. +Inside [cpp11test/src](https://github.com/r-lib/cpp11/tree/main/cpp11test/src) the files that start with `test-` are C++ tests using the [Catch](https://testthat.r-lib.org/reference/use_catch.html) support in testthat. +In addition there are some regular R tests in [cpp11test/tests/testthat/](https://github.com/r-lib/cpp11/tree/main/cpp11test/tests/testthat). ## Naming conventions @@ -86,12 +86,12 @@ In addition there are some regular R tests in [cpp11test/tests/testthat/](https: ## Vector classes -All of the basic r_vector classes are class templates, the base template is defined in [cpp11/r_vector.hpp](https://github.com/r-lib/cpp11/blob/master/inst/include/cpp11/r_vector.hpp) +All of the basic r_vector classes are class templates, the base template is defined in [cpp11/r_vector.hpp](https://github.com/r-lib/cpp11/blob/main/inst/include/cpp11/r_vector.hpp) The template parameter is the type of **value** the particular R vector stores, e.g. `double` for `cpp11::doubles`. This differs from Rcpp, whose first template parameter is the R vector type, e.g. `REALSXP`. The file first has the class declarations, then function definitions further down in the file. -Specializations for the various types are in separate files, e.g. [cpp11/doubles.hpp](https://github.com/r-lib/cpp11/blob/master/inst/include/cpp11/doubles.hpp), [cpp11/integers.hpp](https://github.com/r-lib/cpp11/blob/master/inst/include/cpp11/integers.hpp) +Specializations for the various types are in separate files, e.g. [cpp11/doubles.hpp](https://github.com/r-lib/cpp11/blob/main/inst/include/cpp11/doubles.hpp), [cpp11/integers.hpp](https://github.com/r-lib/cpp11/blob/main/inst/include/cpp11/integers.hpp) ## Coercion functions @@ -100,7 +100,7 @@ There are two different coercion functions `as_sexp()` takes a C++ object and coerces it to a SEXP object, so it can be used in R. `as_cpp<>()` is a template function that takes a SEXP and creates a C++ object from it -The various methods for both functions are defined in [cpp11/as.hpp](https://github.com/r-lib/cpp11/blob/master/inst/include/cpp11/as.hpp) +The various methods for both functions are defined in [cpp11/as.hpp](https://github.com/r-lib/cpp11/blob/main/inst/include/cpp11/as.hpp) This is definitely the most complex part of the cpp11 code, with extensive use of [template metaprogramming](https://en.wikipedia.org/wiki/Template_metaprogramming). In particular the [substitution failure is not an error (SFINAE)](https://en.wikipedia.org/wiki/Substitution_failure_is_not_an_error) technique is used to control overloading of the functions. @@ -126,14 +126,14 @@ Calling `preserved.release()` on this returned token will release the protection This scheme scales in O(1) time to release or insert an object vs O(N) or worse time with `R_PreserveObject()` / `R_ReleaseObject()`. -These functions are defined in [protect.hpp](https://github.com/r-lib/cpp11/blob/master/inst/include/cpp11/protect.hpp) +These functions are defined in [protect.hpp](https://github.com/r-lib/cpp11/blob/main/inst/include/cpp11/protect.hpp) ### Unwind Protect In R 3.5+ cpp11 uses `R_UnwindProtect` to protect (most) calls to the R API that could fail. These are usually those that allocate memory, though in truth most R API functions could error along some paths. If an error happends under `R_UnwindProtect` cpp11 will throw a C++ exception. -This exception is caught by the try catch block defined in the `BEGIN_CPP11` macro in [cpp11/declarations.hpp](https://github.com/r-lib/cpp11/blob/master/inst/include/cpp11/declarations.hpp). +This exception is caught by the try catch block defined in the `BEGIN_CPP11` macro in [cpp11/declarations.hpp](https://github.com/r-lib/cpp11/blob/main/inst/include/cpp11/declarations.hpp). The exception will cause any C++ destructors to run, freeing any resources held by C++ objects. After the try catch block exits the R error unwinding is then continued by `R_ContinueUnwind()` and a normal R error results. diff --git a/vignettes/motivations.Rmd b/vignettes/motivations.Rmd index efadef4e..446c3534 100644 --- a/vignettes/motivations.Rmd +++ b/vignettes/motivations.Rmd @@ -312,7 +312,7 @@ saveRDS(b_sum, "sum.Rds", version = 2) knitr::kable(readRDS("sum.Rds")) ``` -[cpp11test/src/sum.cpp](https://github.com/r-lib/cpp11/blob/master/cpp11test/src/sum.cpp) contains the code ran in these benchmarks. +[cpp11test/src/sum.cpp](https://github.com/r-lib/cpp11/blob/main/cpp11test/src/sum.cpp) contains the code ran in these benchmarks. ## UTF-8 everywhere