├── .cargo └── config.toml ├── .github ├── .gitignore ├── dependabot.yml └── workflows │ ├── R-CMD-check.yaml │ ├── check-non-api.yaml │ ├── docs.yml │ ├── generate_pkg.yml │ ├── msrv.yaml │ ├── release.yml │ └── wasm.yaml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE.md ├── R-package ├── .Rbuildignore ├── DESCRIPTION ├── LICENSE ├── NAMESPACE ├── R │ ├── 000-wrappers.R │ └── savvy-package.R ├── bootstrap.R ├── cleanup ├── cleanup.win ├── configure ├── configure.win ├── man │ ├── FooEnum.Rd │ ├── Person.Rd │ ├── add_suffix.Rd │ ├── flip_logical.Rd │ ├── or_logical.Rd │ ├── print_list.Rd │ ├── reverse_bit_scalar.Rd │ ├── reverse_bits.Rd │ ├── times_any_int.Rd │ ├── times_any_real.Rd │ ├── times_two_int.Rd │ ├── times_two_real.Rd │ └── to_upper.Rd ├── savvyExamples.Rproj ├── src │ ├── Makevars.in │ ├── Makevars.win.in │ ├── init.c │ ├── rust │ │ ├── .cargo │ │ │ └── config.toml │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ ├── api.h │ │ └── src │ │ │ ├── altrep.rs │ │ │ ├── attributes.rs │ │ │ ├── complex.rs │ │ │ ├── consuming_type.rs │ │ │ ├── convert_from_rust_types.rs │ │ │ ├── enum_support.rs │ │ │ ├── environment.rs │ │ │ ├── error_handling.rs │ │ │ ├── escape.rs │ │ │ ├── function.rs │ │ │ ├── init_vectors.rs │ │ │ ├── lib.rs │ │ │ ├── log.rs │ │ │ ├── missing_values.rs │ │ │ ├── mod1 │ │ │ ├── mod.rs │ │ │ └── mod1_1 │ │ │ │ ├── mod.rs │ │ │ │ └── mod_1_1_foo.rs │ │ │ ├── mod2_ignored │ │ │ └── mod.rs │ │ │ ├── mod3_ignored │ │ │ ├── multiple_defs.rs │ │ │ ├── numeric.rs │ │ │ ├── optional_arg.rs │ │ │ ├── separate_impl_definition.rs │ │ │ └── try_from_iter.rs │ └── savvyExamples-win.def └── tests │ ├── testthat.R │ └── testthat │ ├── _snaps │ ├── consuming_types.md │ └── error_handling.md │ ├── test-altrep.R │ ├── test-attributes.R │ ├── test-complex.R │ ├── test-consuming_types.R │ ├── test-enum.R │ ├── test-environment.R │ ├── test-error_handling.R │ ├── test-from-rust-types.R │ ├── test-function.R │ ├── test-impls_over_multiple_files.R │ ├── test-init_vector.R │ ├── test-invalid_pointer.R │ ├── test-missing_values.R │ ├── test-numeric.R │ ├── test-optional_args.R │ ├── test-panic.R │ ├── test-parse-nested.R │ ├── test-try_from_iter.R │ ├── test-unittest.R │ └── test-unraw.R ├── README.md ├── README.qmd ├── book ├── .gitignore ├── book.toml ├── custom.css └── src │ ├── SUMMARY.md │ ├── advanced_topics.md │ ├── altrep.md │ ├── atomic_types.md │ ├── attributes.md │ ├── data_frames.md │ ├── enum.md │ ├── error.md │ ├── extendr.md │ ├── factor.md │ ├── get_started.md │ ├── initialization_routine.md │ ├── input.md │ ├── intro.md │ ├── key_ideas.md │ ├── linkage.md │ ├── list.md │ ├── matrix.md │ ├── optional_arg.md │ ├── output.md │ ├── savvy_macro.md │ ├── scalar.md │ ├── struct.md │ ├── test.md │ └── type-overview.md ├── build.rs ├── dist-workspace.toml ├── savvy-bindgen ├── Cargo.toml ├── README.md └── src │ ├── gen │ ├── c.rs │ ├── mod.rs │ ├── r.rs │ ├── rust.rs │ ├── static_files.rs │ └── templates │ │ ├── Cargo_toml │ │ ├── Makevars.in │ │ ├── Makevars.win.in │ │ ├── cleanup │ │ ├── cleanup.win │ │ ├── config_toml │ │ ├── configure │ │ ├── configure.win │ │ ├── dllname-win.def │ │ ├── gitignore │ │ └── lib_rs │ ├── ir │ ├── mod.rs │ ├── savvy_enum.rs │ ├── savvy_fn.rs │ ├── savvy_impl.rs │ └── savvy_struct.rs │ ├── lib.rs │ ├── parse_file.rs │ └── utils.rs ├── savvy-cli ├── Cargo.toml ├── README.md └── src │ ├── main.rs │ ├── parse_manifest.rs │ └── utils.rs ├── savvy-ffi ├── Cargo.toml ├── README.md └── src │ ├── altrep.rs │ └── lib.rs ├── savvy-macro ├── Cargo.toml ├── README.md ├── src │ ├── lib.rs │ └── snapshots │ │ ├── savvy_macro__tests__assert_snapshot_ffi-2.snap │ │ ├── savvy_macro__tests__assert_snapshot_ffi-3.snap │ │ ├── savvy_macro__tests__assert_snapshot_ffi.snap │ │ ├── savvy_macro__tests__assert_snapshot_ffi_impl-2.snap │ │ ├── savvy_macro__tests__assert_snapshot_ffi_impl-3.snap │ │ ├── savvy_macro__tests__assert_snapshot_ffi_impl-4.snap │ │ ├── savvy_macro__tests__assert_snapshot_ffi_impl.snap │ │ ├── savvy_macro__tests__assert_snapshot_inner-2.snap │ │ ├── savvy_macro__tests__assert_snapshot_inner-3.snap │ │ ├── savvy_macro__tests__assert_snapshot_inner-4.snap │ │ └── savvy_macro__tests__assert_snapshot_inner.snap └── tests │ ├── cases │ ├── simple_cases.rs │ └── simple_cases.stderr │ └── trybuild.rs ├── src ├── altrep │ ├── altinteger.rs │ ├── altlist.rs │ ├── altlogical.rs │ ├── altraw.rs │ ├── altreal.rs │ ├── altstring.rs │ └── mod.rs ├── error.rs ├── eval.rs ├── ffi.rs ├── io.rs ├── lib.rs ├── log.rs ├── panic_hook.rs ├── protect.rs ├── sexp │ ├── complex.rs │ ├── environment.rs │ ├── external_pointer.rs │ ├── function.rs │ ├── integer.rs │ ├── list.rs │ ├── logical.rs │ ├── mod.rs │ ├── na.rs │ ├── null.rs │ ├── numeric.rs │ ├── raw.rs │ ├── real.rs │ ├── scalar.rs │ ├── string.rs │ └── utils.rs ├── unwind_protect.rs └── unwind_protect_wrapper.c ├── wrapper.h └── xtask ├── Cargo.toml └── src └── main.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | xtask = "run --package xtask --" 3 | r-test = "run --manifest-path ./savvy-cli/Cargo.toml -- test" 4 | 5 | # On Windows, link.exe fails when the artifact contains unresolved symbols 6 | # (i.e., R's API, which cannot be used without a real R session). This option 7 | # makes the linker ignore these problems. 8 | # 9 | # This setting is needed only when you run `cargo test`, not when `R CMD check` 10 | # etc. The `.cargo` directory need to be excluded on building the package (i.e. 11 | # add `^src/rust/\.cargo$` to `.Rbuildignore`) because otherwise you'll get the 12 | # "hidden files and directories" NOTE. 13 | [target.x86_64-pc-windows-msvc] 14 | rustflags = ["-C", "link-arg=/FORCE:UNRESOLVED"] 15 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | workflow_dispatch: 9 | 10 | name: R-CMD-check 11 | 12 | jobs: 13 | R-CMD-check: 14 | runs-on: ${{ matrix.config.os }} 15 | defaults: 16 | run: 17 | shell: bash 18 | 19 | name: "${{ matrix.config.os }} (R: ${{ matrix.config.r }}, rust: ${{ matrix.config.rust }})" 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | config: 25 | # prettier-ignore 26 | - { os: "macos-latest", r: "release", rust: "stable" } 27 | - { os: "macos-15-intel", r: "release", rust: "stable" } 28 | - { os: "windows-latest", r: "release", rust: "stable" } 29 | - { os: "ubuntu-latest", r: "release", rust: "stable" } 30 | - { os: "ubuntu-latest", r: "devel", rust: "stable", http-user-agent: "release" } 31 | - { os: "ubuntu-latest", r: "release", rust: "nightly" } 32 | - { os: "ubuntu-24.04-arm", r: "release", rust: "stable", rspm: "false"} 33 | 34 | env: 35 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 36 | R_KEEP_PKG_SOURCE: yes 37 | 38 | steps: 39 | - uses: actions/checkout@v4 40 | 41 | - uses: dtolnay/rust-toolchain@nightly 42 | if: matrix.config.rust == 'nightly' 43 | 44 | - uses: r-lib/actions/setup-r@v2 45 | with: 46 | r-version: ${{ matrix.config.r }} 47 | http-user-agent: ${{ matrix.config.http-user-agent }} 48 | use-public-rspm: ${{ matrix.config.rspm || 'true' }} 49 | 50 | - uses: r-lib/actions/setup-r-dependencies@v2 51 | with: 52 | extra-packages: | 53 | any::rcmdcheck 54 | github::yutannihilation/savvy-helper-R-package 55 | needs: check 56 | working-directory: R-package 57 | 58 | - name: Run cargo test 59 | run: | 60 | cargo test --manifest-path=./savvy-macro/Cargo.toml 61 | cargo test --manifest-path=./savvy-bindgen/Cargo.toml 62 | cargo test --manifest-path=./savvy-cli/Cargo.toml 63 | 64 | # run `savvy-cli test` to test savvy itself 65 | cargo r-test --features complex 66 | 67 | # run `savvy-cli test` on R-package to test `savvy-cli test` 68 | cargo r-test ./R-package/src/rust/ 69 | env: 70 | SAVVY_PROFILE: dev 71 | 72 | - uses: r-lib/actions/check-r-package@v2 73 | with: 74 | args: 'c("--no-manual")' # no --as-cran 75 | upload-snapshots: true 76 | working-directory: R-package 77 | env: 78 | MAKEFLAGS: -j2 # cf. https://github.com/yutannihilation/savvy/issues/355#issuecomment-2740005471 79 | SAVVY_PROFILE: dev 80 | -------------------------------------------------------------------------------- /.github/workflows/check-non-api.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [main] 4 | pull_request: 5 | branches: [main] 6 | 7 | name: Check so-called "non-API" APIs 8 | 9 | jobs: 10 | check-non-api: 11 | runs-on: ubuntu-latest 12 | 13 | env: 14 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - uses: r-lib/actions/setup-r@v2 20 | 21 | - name: Check non-API 22 | run: | 23 | cat >tmp.R <<'EOF' 24 | e <- new.env() 25 | source("https://raw.githubusercontent.com/r-devel/r-svn/master/src/library/tools/R/sotools.R", local = e) 26 | cat(e$nonAPI, sep = "|") 27 | EOF 28 | 29 | REGEX=$(Rscript tmp.R) 30 | 31 | NON_API=$(grep -R -w -E "${REGEX}" ./savvy-ffi/src/ || true) 32 | 33 | echo "Check result:" 34 | echo 35 | echo $NON_API 36 | echo 37 | 38 | if [ -n "${NON_API}" ]; then 39 | echo 'Found what they call "non-API"!' 40 | exit 1 41 | else 42 | echo "OK!" 43 | fi 44 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: [main] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | 35 | - uses: dtolnay/rust-toolchain@nightly 36 | 37 | - name: Build API Reference 38 | run: cargo doc --no-deps --features complex,altrep 39 | env: 40 | RUSTDOCFLAGS: "--enable-index-page -Zunstable-options" 41 | 42 | - name: Install latest mdbook 43 | run: | 44 | tag=$(curl 'https://api.github.com/repos/rust-lang/mdbook/releases/latest' | jq -r '.tag_name') 45 | url="https://github.com/rust-lang/mdbook/releases/download/${tag}/mdbook-${tag}-x86_64-unknown-linux-gnu.tar.gz" 46 | mkdir bin 47 | curl -sSL $url | tar -xz --directory=bin 48 | echo "$(pwd)/bin" >> $GITHUB_PATH 49 | 50 | - name: Build user guide 51 | run: | 52 | cd book 53 | mdbook build 54 | 55 | - name: Tweak 56 | run: | 57 | # As of v1.0.9, upload-pages-artifact action rejects files with incorrect permissions. 58 | # In Rust doc's case, .lock is such a file. 59 | # 60 | # cf. https://github.com/actions/deploy-pages/issues/188#issuecomment-1597651901 61 | rm -f ./target/doc/.lock 62 | 63 | mkdir _docs 64 | mv ./book/book ./_docs/guide 65 | mv ./target/doc ./_docs/reference 66 | 67 | - name: Setup Pages 68 | uses: actions/configure-pages@v4 69 | 70 | - name: Upload artifact 71 | uses: actions/upload-pages-artifact@v3 72 | with: 73 | path: "./_docs/" 74 | 75 | - name: Deploy to GitHub Pages 76 | id: deployment 77 | uses: actions/deploy-pages@v4 78 | -------------------------------------------------------------------------------- /.github/workflows/generate_pkg.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [main] 4 | pull_request: 5 | branches: [main] 6 | 7 | name: Generate R package using savvy 8 | 9 | jobs: 10 | generate_pkg: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - uses: dtolnay/rust-toolchain@nightly 17 | 18 | - name: check wrapper file generation 19 | run: | 20 | cargo run --manifest-path ./savvy-cli/Cargo.toml -- update ./R-package/ 21 | 22 | # If there's any change, exit with non-zero status 23 | # (the code is derived from https://stackoverflow.com/a/3879077) 24 | git add ./R-package/ 25 | git update-index --refresh 26 | if ! git diff-index --quiet HEAD -- ./R-package/; then 27 | echo 28 | echo "some change on wrapper file generation was detected!" 29 | echo 30 | git diff ./R-package/ 31 | exit 1 32 | fi 33 | 34 | - uses: r-lib/actions/setup-r@v2 35 | with: 36 | use-public-rspm: true 37 | 38 | - uses: r-lib/actions/setup-r-dependencies@v2 39 | with: 40 | extra-packages: | 41 | any::rcmdcheck 42 | any::devtools 43 | any::usethis 44 | working-directory: R-package # this isn't the actual R package, but this is needed to avoid an error 45 | 46 | - name: create tempdir 47 | run: echo "TEMP_DIR=$(mktemp -d)" >> ${GITHUB_ENV} 48 | 49 | - name: create package 50 | run: | 51 | Rscript -e "usethis::create_package('${{ runner.temp }}/sawy')" 52 | cargo run --manifest-path ./savvy-cli/Cargo.toml -- init ${{ runner.temp }}/sawy 53 | cd ${{ runner.temp }}/sawy 54 | sed -i '/savvy/ s|".*"|{ path = "${{ github.workspace }}" }|' src/rust/Cargo.toml 55 | Rscript -e "usethis::use_mit_license('foo')" 56 | Rscript -e "devtools::document()" 57 | 58 | - uses: r-lib/actions/check-r-package@v2 59 | with: 60 | args: 'c("--no-manual")' 61 | working-directory: ${{ runner.temp }}/sawy 62 | env: 63 | MAKEFLAGS: -j2 # cf. https://github.com/yutannihilation/savvy/issues/355#issuecomment-2740005471 64 | SAVVY_PROFILE: dev 65 | 66 | - name: Run Rust tests 67 | run: | 68 | cargo test --manifest-path ${{ runner.temp }}/sawy/src/rust/Cargo.toml 69 | cargo r-test ${{ runner.temp }}/sawy/src/rust 70 | -------------------------------------------------------------------------------- /.github/workflows/msrv.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [main] 4 | pull_request: 5 | branches: [main] 6 | 7 | name: check MSRV 8 | 9 | jobs: 10 | # check at least it can build 11 | check-msrv: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - uses: dtolnay/rust-toolchain@1.70.0 17 | 18 | - name: Build 19 | run: cargo build 20 | -------------------------------------------------------------------------------- /.github/workflows/wasm.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [main] 4 | pull_request: 5 | branches: [main] 6 | workflow_dispatch: 7 | 8 | name: Check WASM build 9 | 10 | jobs: 11 | build-wasm: 12 | runs-on: ubuntu-latest 13 | 14 | env: 15 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Set up R 21 | uses: r-lib/actions/setup-r@v2 22 | with: 23 | use-public-rspm: true 24 | 25 | - name: Tweak 26 | run: | 27 | cd R-package && Rscript bootstrap.R 28 | 29 | - name: Build wasm packages 30 | uses: r-wasm/actions/build-rwasm@main 31 | with: 32 | packages: "local::./R-package" 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .Rdata 4 | .httr-oauth 5 | .DS_Store 6 | 7 | *.o 8 | *.so 9 | *.dll 10 | target 11 | .vscode 12 | 13 | /Cargo.lock 14 | /R-package/src/rust/Cargo.lock 15 | /R-package/src/Makevars 16 | /R-package/src/Makevars.win -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to savvy 2 | ===================== 3 | 4 | (Still work in progress...) 5 | 6 | ## Testing 7 | 8 | ### savvy crate 9 | 10 | As savvy framework requires a real R session to work, `cargo test` doesn't work. 11 | Instead, please use `savvy-cli test`. This extracts test code and creates a 12 | temporary R package on-the-fly to run these tests. 13 | 14 | ```sh 15 | savvy-cli test . 16 | ``` 17 | 18 | if you want to use the dev version of savvy-cli, you can run `cargo r-test`, 19 | which is an alias of `cargo run --manifest-path ./savvy-cli/Cargo.toml -- test`. 20 | 21 | The binary of `savvy-cli` is available from the [GitHub Releases][release]. You 22 | can also install it via `cargo install`. 23 | 24 | [release]: https://github.com/yutannihilation/savvy/releases 25 | 26 | Currently, it also requires 27 | 28 | * `R` is on PATH 29 | * [pkgbuild R package][pkgbuild] is installed 30 | 31 | [pkgbuild]: https://pkgbuild.r-lib.org/ 32 | 33 | #### R package for testing 34 | 35 | `R-package/` directory contains the R package for testing. You can run 36 | `devtools::check()` on the directory. 37 | 38 | ### savvy-macro crate 39 | 40 | savvy-macro uses [insta](https://insta.rs/) for snapshot testing. Please install 41 | `cargo-insta` first. The installation guide can be found on [the official 42 | Getting Started][insta-install]. 43 | 44 | [insta-install]: https://insta.rs/docs/quickstart/ 45 | 46 | If you create a new snapshot or modify an existing snapshot, you can review and 47 | accept the changes with: 48 | 49 | ```sh 50 | cargo insta review 51 | ``` 52 | 53 | ### savvy-bindgen crate 54 | 55 | You can just run `cargo test`. 56 | 57 | ```sh 58 | cargo test --manifest-path=./savvy-bindgen/Cargo.toml 59 | ``` 60 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "savvy" 3 | description = "A simple R extension interface" 4 | version.workspace = true 5 | edition.workspace = true 6 | authors.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | readme = "README.md" 10 | exclude = ["/book", "/R-package", "README.qmd"] 11 | 12 | rust-version = "1.70.0" 13 | 14 | [dependencies] 15 | savvy-ffi = { version = "0.8.14", path = "./savvy-ffi" } 16 | savvy-macro = { version = "0.8.14", path = "./savvy-macro" } 17 | num-complex = { version = "0.4.5", optional = true } 18 | 19 | log = { version = "0.4", optional = true } 20 | env_logger = { version = "0.11", default-features = false, optional = true } 21 | rustversion = "1.0" 22 | 23 | [features] 24 | default = [] 25 | 26 | # Support complex 27 | complex = ["num-complex", "savvy-ffi/complex"] 28 | 29 | # Support ALTREP 30 | altrep = ["savvy-ffi/altrep"] 31 | 32 | # Support logger 33 | logger = ["log", "env_logger"] 34 | 35 | # By default, savvy provides `impl From for Error`. 36 | # However, this conflicts if the user implements their custom error and the 37 | # conversion from it to savvy::Error. This flag removes the impl to allow such a 38 | # custom error. 39 | use-custom-error = [] 40 | 41 | savvy-test = [] 42 | 43 | [build-dependencies] 44 | cc = "1.2.9" # 1.2.8 had a problem with webr build 45 | 46 | [package.metadata.docs.rs] 47 | features = ["complex", "altrep", "logger"] 48 | 49 | [workspace.metadata.release] 50 | tag = false # do not create tags for individual crates (e.g. "savvy-cli-v0.2.5") 51 | 52 | [package.metadata.release] 53 | tag = true # create a single tag for the version (e.g. "v0.2.5") 54 | pre-release-replacements = [ 55 | { file = "CHANGELOG.md", search = "Unreleased", replace = "v{{version}}", min = 1 }, 56 | { file = "CHANGELOG.md", search = "\\.\\.\\.HEAD", replace = "...{{tag_name}}", exactly = 1 }, 57 | { file = "CHANGELOG.md", search = "ReleaseDate", replace = "{{date}}", min = 1 }, 58 | { file = "CHANGELOG.md", search = "", replace = "\n## [Unreleased] (ReleaseDate)\n", exactly = 1 }, 59 | { file = "CHANGELOG.md", search = "", replace = "\n[Unreleased]: https://github.com/yutannihilation/savvy/compare/{{tag_name}}...HEAD", exactly = 1 }, 60 | ] 61 | 62 | [package.metadata.dist] 63 | dist = false 64 | 65 | [workspace] 66 | members = ["savvy-macro", "savvy-bindgen", "savvy-cli", "savvy-ffi", "xtask"] 67 | resolver = "2" 68 | 69 | [workspace.package] 70 | version = "0.8.14" 71 | edition = "2021" 72 | authors = ["Hiroaki Yutani"] 73 | license = "MIT" 74 | repository = "https://github.com/yutannihilation/savvy/" 75 | homepage = "https://yutannihilation.github.io/savvy/guide/" 76 | 77 | # The profile that 'cargo dist' will build with 78 | [profile.dist] 79 | inherits = "release" 80 | lto = "thin" 81 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2023 savvy authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /R-package/.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^savvy\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^LICENSE\.md$ 4 | ^README\.Rmd$ 5 | ^\.github$ 6 | 7 | ^src/rust/\.cargo$ 8 | ^src/rust/\.vscode$ 9 | ^src/rust/target$ 10 | 11 | ^src/Makevars$ 12 | ^src/Makevars\.win$ 13 | 14 | ^bootstrap\.R$ 15 | -------------------------------------------------------------------------------- /R-package/DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: savvyExamples 2 | Title: Examples of savvy 3 | Version: 0.0.0 4 | Authors@R: 5 | person(given = "Hiroaki", 6 | family = "Yutani", 7 | role = c("aut", "cre"), 8 | email = "yutani.ini@gmail.com", 9 | comment = c(ORCID = "0000-0002-3385-7233")) 10 | Description: No description. 11 | License: MIT + file LICENSE 12 | SystemRequirements: Cargo (Rust's package manager), rustc 13 | Encoding: UTF-8 14 | Roxygen: list(markdown = TRUE) 15 | RoxygenNote: 7.3.2 16 | Suggests: 17 | testthat (>= 3.0.0) 18 | Config/testthat/edition: 3 19 | Config/build/bootstrap: TRUE 20 | -------------------------------------------------------------------------------- /R-package/LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2023 2 | COPYRIGHT HOLDER: savvy authors 3 | -------------------------------------------------------------------------------- /R-package/NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method("$",Enum__bundle) 4 | S3method("$",FooEnum__bundle) 5 | S3method("$<-",savvy_savvyExamples__sealed) 6 | S3method("[[",Enum__bundle) 7 | S3method("[[",FooEnum__bundle) 8 | S3method("[[<-",savvy_savvyExamples__sealed) 9 | S3method(print,Enum) 10 | S3method(print,Enum__bundle) 11 | S3method(print,FooEnum) 12 | S3method(print,FooEnum__bundle) 13 | S3method(print,FooWithDefault__bundle) 14 | S3method(print,Person2__bundle) 15 | S3method(print,Person__bundle) 16 | S3method(print,StructWithConfig__bundle) 17 | S3method(print,ValuePair__bundle) 18 | S3method(print,Value__bundle) 19 | S3method(print,struct__bundle) 20 | export(FooEnum) 21 | export(Person) 22 | export(add_suffix) 23 | export(flip_logical) 24 | export(or_logical) 25 | export(print_list) 26 | export(times_any_int) 27 | export(times_any_real) 28 | export(times_two_int) 29 | export(times_two_real) 30 | export(to_upper) 31 | useDynLib(savvyExamples, .registration = TRUE) 32 | -------------------------------------------------------------------------------- /R-package/R/savvy-package.R: -------------------------------------------------------------------------------- 1 | ## usethis namespace: start 2 | ## usethis namespace: end 3 | NULL 4 | -------------------------------------------------------------------------------- /R-package/bootstrap.R: -------------------------------------------------------------------------------- 1 | # Tweak Cargo.toml 2 | cargo_toml <- "src/rust/Cargo.toml" 3 | lines <- readLines(cargo_toml) 4 | writeLines( 5 | gsub("../../../", "../dep_crates/", lines, fixed = TRUE), 6 | cargo_toml 7 | ) 8 | 9 | dir.create("src/dep_crates/") 10 | file.copy( 11 | c( 12 | "../src", 13 | "../Cargo.toml", 14 | "../build.rs", 15 | "../savvy-macro", 16 | "../savvy-bindgen", 17 | "../savvy-ffi" 18 | ), 19 | "src/dep_crates/", 20 | recursive = TRUE 21 | ) 22 | -------------------------------------------------------------------------------- /R-package/cleanup: -------------------------------------------------------------------------------- 1 | rm -f src/Makevars 2 | -------------------------------------------------------------------------------- /R-package/cleanup.win: -------------------------------------------------------------------------------- 1 | rm -f src/Makevars.win 2 | -------------------------------------------------------------------------------- /R-package/configure: -------------------------------------------------------------------------------- 1 | # Even when `cargo` is on `PATH`, `rustc` might not in some cases. This adds 2 | # ~/.cargo/bin to PATH to address such cases. Note that is not always available 3 | # (e.g. or on Ubuntu with Rust installed via APT). 4 | if [ -d "${HOME}/.cargo/bin" ]; then 5 | export PATH="${PATH}:${HOME}/.cargo/bin" 6 | fi 7 | 8 | CARGO_VERSION="$(cargo --version)" 9 | 10 | if [ $? -ne 0 ]; then 11 | echo "-------------- ERROR: CONFIGURATION FAILED --------------------" 12 | echo "" 13 | echo "The cargo command is not available. To install Rust, please refer" 14 | echo "to the official instruction:" 15 | echo "" 16 | echo "https://www.rust-lang.org/tools/install" 17 | echo "" 18 | echo "---------------------------------------------------------------" 19 | 20 | exit 1 21 | fi 22 | 23 | # There's a little chance that rustc is not available on PATH while cargo is. 24 | # So, just ignore the error case. 25 | RUSTC_VERSION="$(rustc --version || true)" 26 | 27 | # Report the version of Rustc to comply with the CRAN policy 28 | echo "using Rust package manager: '${CARGO_VERSION}'" 29 | echo "using Rust compiler: '${RUSTC_VERSION}'" 30 | 31 | if [ "$(uname)" = "Emscripten" ]; then 32 | TARGET="wasm32-unknown-emscripten" 33 | fi 34 | 35 | # allow overriding profile externally (e.g. on CI) 36 | if [ -n "${SAVVY_PROFILE}" ]; then 37 | PROFILE="${SAVVY_PROFILE}" 38 | # catch DEBUG envvar, which is passed from pkgbuild::compile_dll() 39 | elif [ "${DEBUG}" = "true" ]; then 40 | PROFILE=dev 41 | else 42 | PROFILE=release 43 | fi 44 | 45 | # e.g. SAVVY_FEATURES="a b" --> "--features 'a b'" 46 | if [ -n "${SAVVY_FEATURES}" ]; then 47 | FEATURE_FLAGS="--features '${SAVVY_FEATURES}'" 48 | fi 49 | 50 | sed \ 51 | -e "s/@TARGET@/${TARGET}/" \ 52 | -e "s/@PROFILE@/${PROFILE}/" \ 53 | -e "s/@FEATURE_FLAGS@/${FEATURE_FLAGS}/" \ 54 | src/Makevars.in > src/Makevars 55 | -------------------------------------------------------------------------------- /R-package/configure.win: -------------------------------------------------------------------------------- 1 | CARGO_VERSION="$(cargo --version)" 2 | 3 | if [ $? -ne 0 ]; then 4 | echo "-------------- ERROR: CONFIGURATION FAILED --------------------" 5 | echo "" 6 | echo "The cargo command is not available. To install Rust, please refer" 7 | echo "to the official instruction:" 8 | echo "" 9 | echo "https://www.rust-lang.org/tools/install" 10 | echo "" 11 | echo "---------------------------------------------------------------" 12 | 13 | exit 1 14 | fi 15 | 16 | # There's a little chance that rustc is not available on PATH while cargo is. 17 | # So, just ignore the error case. 18 | RUSTC_VERSION="$(rustc --version || true)" 19 | 20 | # Report the version of Rustc to comply with the CRAN policy 21 | echo "using Rust package manager: '${CARGO_VERSION}'" 22 | echo "using Rust compiler: '${RUSTC_VERSION}'" 23 | 24 | # allow overriding profile externally (e.g. on CI) 25 | if [ -n "${SAVVY_PROFILE}" ]; then 26 | PROFILE="${SAVVY_PROFILE}" 27 | # catch DEBUG envvar, which is passed from pkgbuild::compile_dll() 28 | elif [ "${DEBUG}" = "true" ]; then 29 | PROFILE=dev 30 | else 31 | PROFILE=release 32 | fi 33 | 34 | # e.g. SAVVY_FEATURES="a b" --> "--features 'a b'" 35 | if [ -n "${SAVVY_FEATURES}" ]; then 36 | FEATURE_FLAGS="--features '${SAVVY_FEATURES}'" 37 | fi 38 | 39 | sed \ 40 | -e "s/@TARGET@/x86_64-pc-windows-gnu/" \ 41 | -e "s/@PROFILE@/${PROFILE}/" \ 42 | -e "s/@FEATURE_FLAGS@/${FEATURE_FLAGS}/" \ 43 | src/Makevars.win.in > src/Makevars.win 44 | -------------------------------------------------------------------------------- /R-package/man/FooEnum.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/000-wrappers.R 3 | \docType{data} 4 | \name{FooEnum} 5 | \alias{FooEnum} 6 | \title{A Or B.} 7 | \format{ 8 | An object of class \code{FooEnum__bundle} (inherits from \code{savvy_savvyExamples__sealed}) of length 2. 9 | } 10 | \usage{ 11 | FooEnum 12 | } 13 | \description{ 14 | A Or B. 15 | } 16 | \keyword{datasets} 17 | -------------------------------------------------------------------------------- /R-package/man/Person.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/000-wrappers.R 3 | \docType{data} 4 | \name{Person} 5 | \alias{Person} 6 | \title{A person with a name} 7 | \format{ 8 | An object of class \code{Person__bundle} (inherits from \code{savvy_savvyExamples__sealed}) of length 5. 9 | } 10 | \usage{ 11 | Person 12 | } 13 | \description{ 14 | A person with a name 15 | } 16 | \keyword{datasets} 17 | -------------------------------------------------------------------------------- /R-package/man/add_suffix.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/000-wrappers.R 3 | \name{add_suffix} 4 | \alias{add_suffix} 5 | \title{Add suffix} 6 | \usage{ 7 | add_suffix(x, y) 8 | } 9 | \arguments{ 10 | \item{x}{A character vector.} 11 | 12 | \item{y}{A suffix.} 13 | } 14 | \value{ 15 | A character vector with upper case version of the input. 16 | } 17 | \description{ 18 | Add suffix 19 | } 20 | -------------------------------------------------------------------------------- /R-package/man/flip_logical.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/000-wrappers.R 3 | \name{flip_logical} 4 | \alias{flip_logical} 5 | \title{Flip Input} 6 | \usage{ 7 | flip_logical(x) 8 | } 9 | \arguments{ 10 | \item{x}{A logical vector.} 11 | } 12 | \value{ 13 | A logical vector with filled values (\code{NA} is converted to \code{TRUE}). 14 | } 15 | \description{ 16 | Flip Input 17 | } 18 | -------------------------------------------------------------------------------- /R-package/man/or_logical.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/000-wrappers.R 3 | \name{or_logical} 4 | \alias{or_logical} 5 | \title{Or operation} 6 | \usage{ 7 | or_logical(x, y) 8 | } 9 | \arguments{ 10 | \item{x}{A logical vector.} 11 | 12 | \item{y}{A logical value.} 13 | } 14 | \value{ 15 | A logical vector with filled values (\code{NA} is converted to \code{TRUE}). 16 | } 17 | \description{ 18 | Or operation 19 | } 20 | -------------------------------------------------------------------------------- /R-package/man/print_list.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/000-wrappers.R 3 | \name{print_list} 4 | \alias{print_list} 5 | \title{Print the content of list} 6 | \usage{ 7 | print_list(x) 8 | } 9 | \arguments{ 10 | \item{x}{A list vector.} 11 | } 12 | \value{ 13 | \code{NULL} 14 | } 15 | \description{ 16 | Print the content of list 17 | } 18 | -------------------------------------------------------------------------------- /R-package/man/reverse_bit_scalar.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/000-wrappers.R 3 | \name{reverse_bit_scalar} 4 | \alias{reverse_bit_scalar} 5 | \title{Reverse bits} 6 | \usage{ 7 | reverse_bit_scalar(x) 8 | } 9 | \arguments{ 10 | \item{x}{A raw vector.} 11 | } 12 | \description{ 13 | Reverse bits 14 | } 15 | -------------------------------------------------------------------------------- /R-package/man/reverse_bits.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/000-wrappers.R 3 | \name{reverse_bits} 4 | \alias{reverse_bits} 5 | \title{Reverse bits} 6 | \usage{ 7 | reverse_bits(x) 8 | } 9 | \arguments{ 10 | \item{x}{A raw vector.} 11 | } 12 | \description{ 13 | Reverse bits 14 | } 15 | -------------------------------------------------------------------------------- /R-package/man/times_any_int.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/000-wrappers.R 3 | \name{times_any_int} 4 | \alias{times_any_int} 5 | \title{Multiply Input By Another Input} 6 | \usage{ 7 | times_any_int(x, y) 8 | } 9 | \arguments{ 10 | \item{x}{An integer vector.} 11 | 12 | \item{y}{An integer to multiply.} 13 | } 14 | \value{ 15 | An integer vector with values multiplied by \code{y}. 16 | } 17 | \description{ 18 | Multiply Input By Another Input 19 | } 20 | -------------------------------------------------------------------------------- /R-package/man/times_any_real.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/000-wrappers.R 3 | \name{times_any_real} 4 | \alias{times_any_real} 5 | \title{Multiply Input By Another Input} 6 | \usage{ 7 | times_any_real(x, y) 8 | } 9 | \arguments{ 10 | \item{x}{A real vector.} 11 | 12 | \item{y}{A real to multiply.} 13 | } 14 | \value{ 15 | A real vector with values multiplied by \code{y}. 16 | } 17 | \description{ 18 | Multiply Input By Another Input 19 | } 20 | -------------------------------------------------------------------------------- /R-package/man/times_two_int.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/000-wrappers.R 3 | \name{times_two_int} 4 | \alias{times_two_int} 5 | \title{Multiply Input By Two} 6 | \usage{ 7 | times_two_int(x) 8 | } 9 | \arguments{ 10 | \item{x}{An integer vector.} 11 | } 12 | \value{ 13 | An integer vector with values multiplied by 2. 14 | } 15 | \description{ 16 | Multiply Input By Two 17 | } 18 | -------------------------------------------------------------------------------- /R-package/man/times_two_real.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/000-wrappers.R 3 | \name{times_two_real} 4 | \alias{times_two_real} 5 | \title{Multiply Input By Two} 6 | \usage{ 7 | times_two_real(x) 8 | } 9 | \arguments{ 10 | \item{x}{A numeric vector.} 11 | } 12 | \value{ 13 | A numeric vector with values multiplied by 2. 14 | } 15 | \description{ 16 | Multiply Input By Two 17 | } 18 | -------------------------------------------------------------------------------- /R-package/man/to_upper.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/000-wrappers.R 3 | \name{to_upper} 4 | \alias{to_upper} 5 | \title{Convert Input To Upper-Case} 6 | \usage{ 7 | to_upper(x) 8 | } 9 | \arguments{ 10 | \item{x}{A character vector.} 11 | } 12 | \value{ 13 | A character vector with upper case version of the input. 14 | } 15 | \description{ 16 | Convert Input To Upper-Case 17 | } 18 | -------------------------------------------------------------------------------- /R-package/savvyExamples.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | LineEndingConversion: Posix 18 | 19 | BuildType: Package 20 | PackageUseDevtools: Yes 21 | PackageInstallArgs: --no-multiarch --with-keep.source 22 | PackageRoxygenize: rd,collate,namespace 23 | -------------------------------------------------------------------------------- /R-package/src/Makevars.in: -------------------------------------------------------------------------------- 1 | TARGET = @TARGET@ 2 | 3 | PROFILE = @PROFILE@ 4 | FEATURE_FLAGS = @FEATURE_FLAGS@ 5 | 6 | # Add flags if necessary 7 | RUSTFLAGS = 8 | 9 | TARGET_DIR = $(CURDIR)/rust/target 10 | LIBDIR = $(TARGET_DIR)/$(TARGET)/$(subst dev,debug,$(PROFILE)) 11 | STATLIB = $(LIBDIR)/libsimple_savvy.a 12 | PKG_LIBS = -L$(LIBDIR) -lsimple_savvy 13 | 14 | CARGO_BUILD_ARGS = --lib --profile $(PROFILE) $(FEATURE_FLAGS) --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) 15 | 16 | all: $(SHLIB) clean_intermediate 17 | 18 | $(SHLIB): $(STATLIB) 19 | 20 | $(STATLIB): 21 | # In some environments, ~/.cargo/bin might not be included in PATH, so we need 22 | # to set it here to ensure cargo can be invoked. It is appended to PATH and 23 | # therefore is only used if cargo is absent from the user's PATH. 24 | export PATH="$(PATH):$(HOME)/.cargo/bin" && \ 25 | export CC="$(CC)" && \ 26 | export CFLAGS="$(CFLAGS)" && \ 27 | export RUSTFLAGS="$(RUSTFLAGS)" && \ 28 | if [ "$(TARGET)" != "wasm32-unknown-emscripten" ]; then \ 29 | cargo build $(CARGO_BUILD_ARGS); \ 30 | else \ 31 | export CARGO_PROFILE_DEV_PANIC="abort" && \ 32 | export CARGO_PROFILE_RELEASE_PANIC="abort" && \ 33 | export RUSTFLAGS="$(RUSTFLAGS) -Zdefault-visibility=hidden" && \ 34 | cargo +nightly build $(CARGO_BUILD_ARGS) --target $(TARGET) -Zbuild-std=panic_abort,std; \ 35 | fi 36 | 37 | clean_intermediate: $(SHLIB) 38 | rm -f $(STATLIB) 39 | 40 | clean: 41 | rm -Rf $(SHLIB) $(OBJECTS) $(STATLIB) ./rust/target 42 | 43 | .PHONY: all clean_intermediate clean 44 | -------------------------------------------------------------------------------- /R-package/src/Makevars.win.in: -------------------------------------------------------------------------------- 1 | TARGET = @TARGET@ 2 | 3 | PROFILE = @PROFILE@ 4 | FEATURE_FLAGS = @FEATURE_FLAGS@ 5 | 6 | # Add flags if necessary 7 | RUSTFLAGS = 8 | 9 | TARGET_DIR = $(CURDIR)/rust/target 10 | LIBDIR = $(TARGET_DIR)/$(TARGET)/$(subst dev,debug,$(PROFILE)) 11 | STATLIB = $(LIBDIR)/libsimple_savvy.a 12 | PKG_LIBS = -L$(LIBDIR) -lsimple_savvy -lws2_32 -ladvapi32 -luserenv -lbcrypt -lntdll 13 | 14 | # Rtools doesn't have the linker in the location that cargo expects, so we need 15 | # to overwrite it via configuration. 16 | CARGO_LINKER = x86_64-w64-mingw32.static.posix-gcc.exe 17 | 18 | all: $(SHLIB) clean_intermediate 19 | 20 | $(SHLIB): $(STATLIB) 21 | 22 | $(STATLIB): 23 | # When the GNU toolchain is used (i.e. on CRAN), -lgcc_eh is specified for 24 | # building proc-macro2, but Rtools doesn't contain libgcc_eh. This isn't used 25 | # in actual, but we need this tweak to please the compiler. 26 | mkdir -p $(LIBDIR)/libgcc_mock && touch $(LIBDIR)/libgcc_mock/libgcc_eh.a 27 | 28 | export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER="$(CARGO_LINKER)" && \ 29 | export CC="$(CC)" && \ 30 | export CFLAGS="$(CFLAGS)" && \ 31 | export RUSTFLAGS="$(RUSTFLAGS)" && \ 32 | export LIBRARY_PATH="$${LIBRARY_PATH};$(LIBDIR)/libgcc_mock" && \ 33 | cargo build --target $(TARGET) --lib --profile $(PROFILE) $(FEATURE_FLAGS) --manifest-path ./rust/Cargo.toml --target-dir $(TARGET_DIR) 34 | 35 | clean_intermediate: $(SHLIB) 36 | rm -f $(STATLIB) 37 | 38 | clean: 39 | rm -Rf $(SHLIB) $(OBJECTS) $(STATLIB) ./rust/target 40 | 41 | .PHONY: all clean_intermediate clean 42 | -------------------------------------------------------------------------------- /R-package/src/rust/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # On Windows, link.exe fails when the artifact contains unresolved symbols 2 | # (i.e., R's API, which cannot be used without a real R session). This option 3 | # makes the linker ignore these problems. 4 | # 5 | # This setting is needed only when you run `cargo test`, not when `R CMD check` 6 | # etc. The `.cargo` directory need to be excluded on building the package (i.e. 7 | # add `^src/rust/.cargo$` to `.Rbuildignore`) because otherwise you'll get the 8 | # "hidden files and directories" NOTE. 9 | [target.x86_64-pc-windows-msvc] 10 | rustflags = ["-C", "link-arg=/FORCE:UNRESOLVED"] 11 | -------------------------------------------------------------------------------- /R-package/src/rust/.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.so 3 | *.dll 4 | target 5 | 6 | Makevars 7 | Makevars.win 8 | -------------------------------------------------------------------------------- /R-package/src/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "simple-savvy" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["staticlib", "lib"] 8 | 9 | [dependencies] 10 | savvy = { path = "../../../", features = ["complex", "altrep", "logger"] } 11 | # for calling Rf_errorcall() to test error handling 12 | savvy-ffi = { path = "../../../savvy-ffi" } 13 | 14 | log = "0.4" 15 | env_logger = { version = "0.11", default-features = false } 16 | 17 | 18 | [profile.release] 19 | # By default, on release build, savvy terminates the R session when a panic 20 | # occurs. This is the right behavior in that a panic means such a fatal event 21 | # where we can have no hope of recovery. 22 | # 23 | # cf. https://doc.rust-lang.org/book/ch09-03-to-panic-or-not-to-panic.html 24 | # 25 | # However, it's possible that the panic is thrown by some of the dependency 26 | # crate and there's little you can do. In such cases, you can change the 27 | # following line to `panic = "unwind"` to always catch a panic. 28 | panic = "abort" 29 | 30 | [workspace] 31 | -------------------------------------------------------------------------------- /R-package/src/rust/src/attributes.rs: -------------------------------------------------------------------------------- 1 | use savvy::{savvy, OwnedIntegerSexp}; 2 | 3 | #[savvy] 4 | fn get_class_int(x: savvy::IntegerSexp) -> savvy::Result { 5 | match x.get_class() { 6 | Some(class) => class.try_into(), 7 | None => ().try_into(), 8 | } 9 | } 10 | 11 | #[savvy] 12 | fn get_names_int(x: savvy::IntegerSexp) -> savvy::Result { 13 | match x.get_names() { 14 | Some(names) => names.try_into(), 15 | None => ().try_into(), 16 | } 17 | } 18 | 19 | #[savvy] 20 | fn get_dim_int(x: savvy::IntegerSexp) -> savvy::Result { 21 | match x.get_dim() { 22 | Some(dim) => { 23 | let x: OwnedIntegerSexp = dim.to_vec().try_into()?; 24 | x.into() 25 | } 26 | None => ().try_into(), 27 | } 28 | } 29 | 30 | #[savvy] 31 | fn get_attr_int(x: savvy::IntegerSexp, attr: &str) -> savvy::Result { 32 | match x.get_attrib(attr)? { 33 | Some(attr) => Ok(attr), 34 | None => ().try_into(), 35 | } 36 | } 37 | 38 | #[savvy] 39 | fn set_class_int() -> savvy::Result { 40 | let mut x = OwnedIntegerSexp::new(1)?; 41 | 42 | x.set_class(["foo", "bar"])?; 43 | 44 | x.into() 45 | } 46 | 47 | #[savvy] 48 | fn set_names_int() -> savvy::Result { 49 | let x_vec = vec![1, 2]; 50 | let mut x: OwnedIntegerSexp = x_vec.try_into()?; 51 | 52 | x.set_names(["foo", "bar"])?; 53 | 54 | x.into() 55 | } 56 | 57 | #[savvy] 58 | fn set_dim_int() -> savvy::Result { 59 | let x_vec = vec![1, 2, 3, 4, 5, 6]; 60 | let mut x: OwnedIntegerSexp = x_vec.try_into()?; 61 | 62 | x.set_dim(&[2, 3])?; 63 | 64 | x.into() 65 | } 66 | 67 | #[savvy] 68 | fn set_attr_int(attr: &str, value: savvy::Sexp) -> savvy::Result { 69 | let mut x = OwnedIntegerSexp::new(1)?; 70 | 71 | x.set_attrib(attr, value)?; 72 | 73 | x.into() 74 | } 75 | -------------------------------------------------------------------------------- /R-package/src/rust/src/complex.rs: -------------------------------------------------------------------------------- 1 | use savvy::savvy; 2 | use savvy::NotAvailableValue; 3 | 4 | #[savvy] 5 | fn new_complex(size: i32) -> savvy::Result { 6 | savvy::OwnedComplexSexp::new(size as usize)?.into() 7 | } 8 | 9 | #[savvy] 10 | fn first_complex(x: savvy::ComplexSexp) -> savvy::Result { 11 | let x_first = x.as_slice()[0]; 12 | x_first.try_into() 13 | } 14 | 15 | #[savvy] 16 | fn abs_complex(x: savvy::ComplexSexp) -> savvy::Result { 17 | let mut out = savvy::OwnedRealSexp::new(x.len())?; 18 | 19 | for (i, c) in x.iter().enumerate() { 20 | if !c.is_na() { 21 | out[i] = (c.re * c.re + c.im * c.im).sqrt(); 22 | } else { 23 | out.set_na(i)?; 24 | } 25 | } 26 | 27 | out.into() 28 | } 29 | -------------------------------------------------------------------------------- /R-package/src/rust/src/consuming_type.rs: -------------------------------------------------------------------------------- 1 | use savvy::{r_println, savvy, Sexp}; 2 | 3 | #[savvy] 4 | #[derive(Clone, Debug)] 5 | pub(crate) struct Value(pub(crate) i32); 6 | 7 | #[savvy] 8 | impl Value { 9 | fn new(x: i32) -> Self { 10 | Self(x) 11 | } 12 | 13 | fn pair(self, b: Value) -> savvy::Result { 14 | Ok(ValuePair { a: self, b }) 15 | } 16 | 17 | fn get(&self) -> savvy::Result { 18 | self.0.try_into() 19 | } 20 | } 21 | 22 | #[allow(dead_code)] 23 | #[savvy] 24 | #[derive(Debug)] 25 | struct ValuePair { 26 | a: Value, 27 | b: Value, 28 | } 29 | 30 | #[savvy] 31 | impl ValuePair { 32 | fn new(a: Value, b: Value) -> Self { 33 | Self { a, b } 34 | } 35 | 36 | fn new_copy(a: &Value, b: &Value) -> Self { 37 | Self { 38 | a: a.clone(), 39 | b: b.clone(), 40 | } 41 | } 42 | 43 | fn print(&self) -> savvy::Result<()> { 44 | r_println!("{:?}", self); 45 | Ok(()) 46 | } 47 | } 48 | 49 | #[savvy] 50 | fn new_value_pair(a: Value, b: Value) -> savvy::Result { 51 | Ok(ValuePair { a, b }) 52 | } 53 | -------------------------------------------------------------------------------- /R-package/src/rust/src/convert_from_rust_types.rs: -------------------------------------------------------------------------------- 1 | use savvy::{ 2 | savvy, IntegerSexp, OwnedComplexSexp, OwnedIntegerSexp, OwnedLogicalSexp, OwnedRealSexp, 3 | OwnedStringSexp, RealSexp, 4 | }; 5 | 6 | // Scalar input, no out 7 | 8 | #[savvy] 9 | fn scalar_input_int(x: i32) -> savvy::Result<()> { 10 | savvy::r_println!("{x}"); 11 | Ok(()) 12 | } 13 | 14 | #[savvy] 15 | fn scalar_input_real(x: f64) -> savvy::Result<()> { 16 | savvy::r_println!("{x}"); 17 | Ok(()) 18 | } 19 | 20 | #[savvy] 21 | fn scalar_input_logical(x: bool) -> savvy::Result<()> { 22 | savvy::r_println!("{x}"); 23 | Ok(()) 24 | } 25 | 26 | #[savvy] 27 | fn scalar_input_string(x: &str) -> savvy::Result<()> { 28 | savvy::r_println!("{x}"); 29 | Ok(()) 30 | } 31 | 32 | // No input, scalar out 33 | 34 | #[savvy] 35 | fn scalar_output_int() -> savvy::Result { 36 | 1.try_into() 37 | } 38 | 39 | #[savvy] 40 | fn scalar_output_int2() -> savvy::Result { 41 | OwnedIntegerSexp::try_from_scalar(1)?.into() 42 | } 43 | 44 | #[savvy] 45 | fn scalar_output_real() -> savvy::Result { 46 | 1.3.try_into() 47 | } 48 | 49 | #[savvy] 50 | fn scalar_output_real2() -> savvy::Result { 51 | OwnedRealSexp::try_from_scalar(1.3)?.into() 52 | } 53 | 54 | #[savvy] 55 | fn scalar_output_logical() -> savvy::Result { 56 | false.try_into() 57 | } 58 | 59 | #[savvy] 60 | fn scalar_output_logical2() -> savvy::Result { 61 | OwnedLogicalSexp::try_from_scalar(false)?.into() 62 | } 63 | 64 | #[savvy] 65 | fn scalar_output_string() -> savvy::Result { 66 | "foo".try_into() 67 | } 68 | 69 | #[savvy] 70 | fn scalar_output_string2() -> savvy::Result { 71 | OwnedStringSexp::try_from_scalar("foo")?.into() 72 | } 73 | 74 | #[savvy] 75 | fn scalar_output_complex() -> savvy::Result { 76 | savvy::Complex64 { re: 1.0, im: 1.0 }.try_into() 77 | } 78 | 79 | #[savvy] 80 | fn scalar_output_complex2() -> savvy::Result { 81 | let x = savvy::Complex64 { re: 1.0, im: 1.0 }; 82 | OwnedComplexSexp::try_from_scalar(x)?.into() 83 | } 84 | 85 | // Vector input, scalar out 86 | 87 | #[savvy] 88 | fn sum_int(x: IntegerSexp) -> savvy::Result { 89 | let sum: i32 = x.iter().sum(); 90 | sum.try_into() 91 | } 92 | 93 | #[savvy] 94 | fn sum_real(x: RealSexp) -> savvy::Result { 95 | let sum: f64 = x.iter().sum(); 96 | sum.try_into() 97 | } 98 | 99 | // Scalar input, vector out 100 | 101 | #[savvy] 102 | fn rep_int_vec(x: i32) -> savvy::Result { 103 | let result: Vec = std::iter::repeat(0).take(x as usize).collect(); 104 | result.try_into() 105 | } 106 | 107 | #[savvy] 108 | fn rep_int_slice(x: i32) -> savvy::Result { 109 | let result: Vec = std::iter::repeat(0).take(x as usize).collect(); 110 | result.as_slice().try_into() 111 | } 112 | 113 | #[savvy] 114 | fn rep_real_vec(x: i32) -> savvy::Result { 115 | let result: Vec = std::iter::repeat(0.0).take(x as usize).collect(); 116 | result.try_into() 117 | } 118 | 119 | #[savvy] 120 | fn rep_real_slice(x: i32) -> savvy::Result { 121 | let result: Vec = std::iter::repeat(0.0).take(x as usize).collect(); 122 | result.as_slice().try_into() 123 | } 124 | 125 | #[savvy] 126 | fn rep_bool_vec(x: i32) -> savvy::Result { 127 | let result: Vec = std::iter::repeat(true).take(x as usize).collect(); 128 | result.try_into() 129 | } 130 | 131 | #[savvy] 132 | fn rep_bool_slice(x: i32) -> savvy::Result { 133 | let result: Vec = std::iter::repeat(true).take(x as usize).collect(); 134 | result.as_slice().try_into() 135 | } 136 | 137 | #[savvy] 138 | fn rep_str_vec(x: i32) -> savvy::Result { 139 | let result: Vec<&str> = std::iter::repeat("foo").take(x as usize).collect(); 140 | result.try_into() 141 | } 142 | 143 | #[savvy] 144 | fn rep_str_slice(x: i32) -> savvy::Result { 145 | let result: Vec<&str> = std::iter::repeat("foo").take(x as usize).collect(); 146 | result.as_slice().try_into() 147 | } 148 | -------------------------------------------------------------------------------- /R-package/src/rust/src/enum_support.rs: -------------------------------------------------------------------------------- 1 | use savvy::{r_println, savvy}; 2 | 3 | /// A Or B. 4 | /// 5 | /// @export 6 | #[savvy] 7 | #[derive(Debug)] 8 | enum FooEnum { 9 | A, 10 | B, 11 | } 12 | 13 | #[savvy] 14 | impl FooEnum { 15 | fn print(&self) -> savvy::Result<()> { 16 | r_println!("{:?}", self); 17 | Ok(()) 18 | } 19 | } 20 | 21 | #[savvy] 22 | fn print_foo_enum(x: FooEnum) -> savvy::Result<()> { 23 | x.print() 24 | } 25 | 26 | #[savvy] 27 | fn print_foo_enum_ref(x: &FooEnum) -> savvy::Result<()> { 28 | x.print() 29 | } 30 | 31 | #[savvy] 32 | fn foo_a() -> savvy::Result { 33 | Ok(FooEnum::A) 34 | } 35 | -------------------------------------------------------------------------------- /R-package/src/rust/src/environment.rs: -------------------------------------------------------------------------------- 1 | use savvy::{savvy, savvy_err, EnvironmentSexp, Sexp}; 2 | 3 | #[savvy] 4 | fn get_var_in_env(name: &str, env: Option) -> savvy::Result { 5 | let env = env.unwrap_or(EnvironmentSexp::global_env()); 6 | let obj = env.get(name)?; 7 | obj.ok_or(savvy_err!("Not found")) 8 | } 9 | 10 | #[savvy] 11 | fn var_exists_in_env(name: &str, env: Option) -> savvy::Result { 12 | let env = env.unwrap_or(EnvironmentSexp::global_env()); 13 | env.contains(name)?.try_into() 14 | } 15 | 16 | #[savvy] 17 | fn set_var_in_env(name: &str, value: Sexp, env: Option) -> savvy::Result<()> { 18 | let env = env.unwrap_or(EnvironmentSexp::global_env()); 19 | env.set(name, value) 20 | } 21 | -------------------------------------------------------------------------------- /R-package/src/rust/src/error_handling.rs: -------------------------------------------------------------------------------- 1 | use savvy::{savvy, savvy_err, savvy_init, NullSexp, Sexp}; 2 | use savvy_ffi::DllInfo; 3 | use std::ffi::CString; 4 | 5 | use std::sync::{Mutex, OnceLock}; 6 | 7 | static FOO_VALUE: OnceLock> = OnceLock::new(); 8 | 9 | #[savvy_init] 10 | fn init_foo_value(dll: *mut DllInfo) -> savvy::Result<()> { 11 | FOO_VALUE 12 | .set(Mutex::new(-1)) 13 | .map_err(|_| savvy_err!("Failed to set values"))?; 14 | Ok(()) 15 | } 16 | 17 | struct Foo {} 18 | 19 | impl Foo { 20 | fn new() -> Self { 21 | let v = FOO_VALUE.get().unwrap(); 22 | *v.lock().unwrap() = 1; 23 | Foo {} 24 | } 25 | } 26 | 27 | impl Drop for Foo { 28 | fn drop(&mut self) { 29 | let v = FOO_VALUE.get().unwrap(); 30 | *v.lock().unwrap() = 0; 31 | 32 | // If Foo is dropped, this message should be emmited. 33 | savvy::r_println!("Foo is Dropped!"); 34 | } 35 | } 36 | 37 | #[savvy] 38 | fn get_foo_value() -> savvy::Result { 39 | match FOO_VALUE.get() { 40 | Some(x) => { 41 | let v = *x.lock()?; 42 | v.try_into() 43 | } 44 | None => NullSexp.into(), 45 | } 46 | } 47 | 48 | #[savvy] 49 | fn safe_stop() -> savvy::Result<()> { 50 | let _ = Foo::new(); 51 | 52 | unsafe { 53 | savvy::unwind_protect::unwind_protect(|| { 54 | let msg = CString::new("This is an error from inside unwind_protect()!").unwrap(); 55 | savvy_ffi::Rf_errorcall(savvy_ffi::R_NilValue, msg.as_ptr()); 56 | })?; 57 | } 58 | 59 | Ok(()) 60 | } 61 | 62 | #[savvy] 63 | fn raise_error() -> savvy::Result { 64 | Err(savvy_err!("This is my custom error")) 65 | } 66 | 67 | #[allow(clippy::out_of_bounds_indexing, unconditional_panic)] 68 | #[savvy] 69 | fn must_panic() -> savvy::Result<()> { 70 | let x = &[1]; 71 | let _ = x[1]; 72 | Ok(()) 73 | } 74 | 75 | #[savvy] 76 | fn safe_warn() -> savvy::Result<()> { 77 | let _ = Foo::new(); 78 | 79 | savvy::io::r_warn("foo")?; 80 | 81 | Ok(()) 82 | } 83 | 84 | #[savvy] 85 | fn error_conversion() -> savvy::Result<()> { 86 | let _ = std::fs::read_to_string("no_such_file")?; 87 | Ok(()) 88 | } 89 | -------------------------------------------------------------------------------- /R-package/src/rust/src/escape.rs: -------------------------------------------------------------------------------- 1 | use savvy::savvy; 2 | 3 | #[savvy] 4 | fn r#fn(r#struct: bool) -> savvy::Result<()> { 5 | Ok(()) 6 | } 7 | 8 | #[savvy] 9 | struct r#struct { 10 | r#fn: bool, 11 | } 12 | 13 | #[savvy] 14 | impl r#struct { 15 | fn r#new() -> Self { 16 | Self { r#fn: true } 17 | } 18 | 19 | fn r#fn(r#fn: bool) -> savvy::Result<()> { 20 | Ok(()) 21 | } 22 | } 23 | 24 | #[savvy] 25 | enum Enum { 26 | r#enum, 27 | } 28 | -------------------------------------------------------------------------------- /R-package/src/rust/src/function.rs: -------------------------------------------------------------------------------- 1 | use savvy::{savvy, FunctionArgs, FunctionSexp, ListSexp}; 2 | 3 | #[savvy] 4 | pub fn do_call(fun: FunctionSexp, args: ListSexp) -> savvy::Result { 5 | let args = FunctionArgs::from_list(args)?; 6 | let res = fun.call(args)?; 7 | res.into() 8 | } 9 | 10 | #[savvy] 11 | pub fn call_with_args(fun: FunctionSexp) -> savvy::Result { 12 | let mut args = FunctionArgs::new(); 13 | args.add("a", 1)?; 14 | args.add("b", 2.0)?; 15 | args.add("c", "foo")?; 16 | let res = fun.call(args)?; 17 | res.into() 18 | } 19 | 20 | #[savvy] 21 | pub fn get_args(args: ListSexp) -> savvy::Result { 22 | let args = FunctionArgs::from_list(args)?; 23 | Ok(savvy::Sexp(args.inner())) 24 | } 25 | -------------------------------------------------------------------------------- /R-package/src/rust/src/init_vectors.rs: -------------------------------------------------------------------------------- 1 | use savvy::savvy; 2 | 3 | #[savvy] 4 | fn new_int(size: i32) -> savvy::Result { 5 | savvy::OwnedIntegerSexp::new(size as usize)?.into() 6 | } 7 | 8 | #[savvy] 9 | fn new_real(size: i32) -> savvy::Result { 10 | savvy::OwnedRealSexp::new(size as usize)?.into() 11 | } 12 | 13 | #[savvy] 14 | fn new_bool(size: i32) -> savvy::Result { 15 | savvy::OwnedLogicalSexp::new(size as usize)?.into() 16 | } 17 | -------------------------------------------------------------------------------- /R-package/src/rust/src/log.rs: -------------------------------------------------------------------------------- 1 | use savvy::savvy_init; 2 | use savvy_ffi::DllInfo; 3 | 4 | #[savvy_init] 5 | fn init_logger(dll_info: *mut DllInfo) -> savvy::Result<()> { 6 | savvy::log::env_logger().init(); 7 | Ok(()) 8 | } 9 | -------------------------------------------------------------------------------- /R-package/src/rust/src/missing_values.rs: -------------------------------------------------------------------------------- 1 | use savvy::savvy; 2 | 3 | #[savvy] 4 | fn is_scalar_na(x: savvy::Sexp) -> savvy::Result { 5 | x.is_scalar_na().try_into() 6 | } -------------------------------------------------------------------------------- /R-package/src/rust/src/mod1/mod.rs: -------------------------------------------------------------------------------- 1 | mod mod1_1; 2 | 3 | use savvy::{r_println, savvy}; 4 | 5 | #[savvy] 6 | fn fun_mod1() -> savvy::Result<()> { 7 | r_println!("foo!"); 8 | Ok(()) 9 | } 10 | -------------------------------------------------------------------------------- /R-package/src/rust/src/mod1/mod1_1/mod.rs: -------------------------------------------------------------------------------- 1 | mod mod_1_1_foo; 2 | -------------------------------------------------------------------------------- /R-package/src/rust/src/mod1/mod1_1/mod_1_1_foo.rs: -------------------------------------------------------------------------------- 1 | use savvy::{r_println, savvy}; 2 | 3 | #[savvy] 4 | fn fun_mod1_1_foo() -> savvy::Result<()> { 5 | r_println!("foo!"); 6 | Ok(()) 7 | } 8 | -------------------------------------------------------------------------------- /R-package/src/rust/src/mod2_ignored/mod.rs: -------------------------------------------------------------------------------- 1 | use savvy::{r_println, savvy}; 2 | 3 | #[savvy] 4 | fn fun_mod2() -> savvy::Result<()> { 5 | r_println!("bar!"); 6 | Ok(()) 7 | } 8 | -------------------------------------------------------------------------------- /R-package/src/rust/src/mod3_ignored: -------------------------------------------------------------------------------- 1 | use savvy::{r_println, savvy}; 2 | 3 | #[savvy] 4 | fn fun_mod3() -> savvy::Result<()> { 5 | r_println!("bar!"); 6 | Ok(()) 7 | } 8 | -------------------------------------------------------------------------------- /R-package/src/rust/src/multiple_defs.rs: -------------------------------------------------------------------------------- 1 | // This file is to check if savvy-cli can handle multiple definitions. 2 | 3 | use savvy::savvy; 4 | 5 | #[savvy] 6 | #[cfg(target_os = "windows")] 7 | fn fn_w_cfg(x: savvy::Sexp) -> savvy::Result<()> { 8 | Ok(()) 9 | } 10 | 11 | #[savvy] 12 | #[cfg(not(target_os = "windows"))] 13 | fn fn_w_cfg(x: savvy::Sexp) -> savvy::Result<()> { 14 | Ok(()) 15 | } 16 | 17 | #[savvy] 18 | struct StructWithConfig(i32); 19 | 20 | #[savvy] 21 | impl StructWithConfig { 22 | #[cfg(target_os = "windows")] 23 | fn new_method(&self, x: i32) -> savvy::Result { 24 | Ok(Self(x)) 25 | } 26 | 27 | #[cfg(not(target_os = "windows"))] 28 | fn new_method(&self, x: i32) -> savvy::Result { 29 | Ok(Self(x * 2)) 30 | } 31 | 32 | #[cfg(target_os = "windows")] 33 | fn new_associated_fn(x: i32) -> savvy::Result { 34 | Ok(Self(x)) 35 | } 36 | 37 | #[cfg(not(target_os = "windows"))] 38 | fn new_associated_fn(x: i32) -> savvy::Result { 39 | Ok(Self(x * 2)) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /R-package/src/rust/src/numeric.rs: -------------------------------------------------------------------------------- 1 | use savvy::{ 2 | r_println, savvy, NotAvailableValue, NumericScalar, NumericSexp, NumericTypedSexp, 3 | OwnedIntegerSexp, OwnedRealSexp, OwnedStringSexp, Sexp, 4 | }; 5 | 6 | #[savvy] 7 | fn times_two_numeric_f64(x: NumericSexp) -> savvy::Result { 8 | let mut out = OwnedRealSexp::new(x.len())?; 9 | 10 | for (i, v) in x.iter_f64().enumerate() { 11 | if v.is_na() { 12 | out[i] = f64::na(); 13 | } else { 14 | out[i] = v * 2.0; 15 | } 16 | } 17 | 18 | out.into() 19 | } 20 | 21 | #[savvy] 22 | fn times_two_numeric_i32(x: NumericSexp) -> savvy::Result { 23 | let mut out = OwnedIntegerSexp::new(x.len())?; 24 | 25 | for (i, v) in x.iter_i32().enumerate() { 26 | let v = v?; 27 | if v.is_na() { 28 | out[i] = i32::na(); 29 | } else { 30 | out[i] = v * 2; 31 | } 32 | } 33 | 34 | out.into() 35 | } 36 | 37 | #[savvy] 38 | fn usize_to_string(x: NumericSexp) -> savvy::Result { 39 | let mut out = OwnedStringSexp::new(x.len())?; 40 | for (i, v) in x.iter_usize().enumerate() { 41 | out.set_elt(i, &v?.to_string())?; 42 | } 43 | 44 | out.into() 45 | } 46 | 47 | #[savvy] 48 | fn times_two_numeric_f64_scalar(x: NumericScalar) -> savvy::Result { 49 | let v = x.as_f64(); 50 | if v.is_na() { 51 | (f64::na()).try_into() 52 | } else { 53 | (v * 2.0).try_into() 54 | } 55 | } 56 | 57 | #[savvy] 58 | fn times_two_numeric_i32_scalar(x: NumericScalar) -> savvy::Result { 59 | let v = x.as_i32()?; 60 | if v.is_na() { 61 | (i32::na()).try_into() 62 | } else { 63 | (v * 2).try_into() 64 | } 65 | } 66 | 67 | #[savvy] 68 | fn usize_to_string_scalar(x: NumericScalar) -> savvy::Result { 69 | x.as_usize()?.to_string().try_into() 70 | } 71 | 72 | #[savvy] 73 | fn print_numeric(x: NumericSexp) -> savvy::Result<()> { 74 | match x.into_typed() { 75 | NumericTypedSexp::Integer(i) => { 76 | r_println!("Integer {:?}", i.as_slice()); 77 | } 78 | NumericTypedSexp::Real(r) => { 79 | r_println!("Real {:?}", r.as_slice()); 80 | } 81 | } 82 | Ok(()) 83 | } 84 | 85 | // https://github.com/yutannihilation/savvy/issues/387 86 | #[savvy] 87 | fn is_numeric(x: Sexp) -> savvy::Result { 88 | x.is_numeric().try_into() 89 | } 90 | -------------------------------------------------------------------------------- /R-package/src/rust/src/optional_arg.rs: -------------------------------------------------------------------------------- 1 | use savvy::{savvy, IntegerSexp, Sexp}; 2 | 3 | use crate::enum_support::FooEnum; 4 | 5 | #[savvy] 6 | fn default_value_scalar(x: Option) -> savvy::Result { 7 | x.unwrap_or(-1).try_into() 8 | } 9 | 10 | #[savvy] 11 | fn default_value_vec(x: Option) -> savvy::Result { 12 | if let Some(x) = x { 13 | x.iter().sum::().try_into() 14 | } else { 15 | (-1).try_into() 16 | } 17 | } 18 | 19 | #[savvy] 20 | struct FooWithDefault { 21 | default_value: i32, 22 | } 23 | 24 | #[savvy] 25 | impl FooWithDefault { 26 | fn new(default_value: i32) -> Self { 27 | Self { default_value } 28 | } 29 | 30 | fn default_value_method(&self, x: Option) -> savvy::Result { 31 | x.unwrap_or(self.default_value).try_into() 32 | } 33 | 34 | fn default_value_associated_fn(x: Option) -> savvy::Result { 35 | x.unwrap_or(-1).try_into() 36 | } 37 | } 38 | 39 | #[savvy] 40 | fn default_value_struct(x: Option<&FooWithDefault>) -> savvy::Result { 41 | if let Some(x) = x { 42 | x.default_value.try_into() 43 | } else { 44 | (-1).try_into() 45 | } 46 | } 47 | 48 | #[savvy] 49 | fn default_value_enum(x: Option<&FooEnum>) -> savvy::Result { 50 | let res = match x { 51 | Some(FooEnum::A) => 1, 52 | Some(FooEnum::B) => 2, 53 | None => -1, 54 | }; 55 | 56 | res.try_into() 57 | } 58 | -------------------------------------------------------------------------------- /R-package/src/rust/src/separate_impl_definition.rs: -------------------------------------------------------------------------------- 1 | use savvy::{savvy, Sexp}; 2 | 3 | #[savvy] 4 | impl crate::consuming_type::Value { 5 | fn get2(&self) -> savvy::Result { 6 | self.0.try_into() 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /R-package/src/rust/src/try_from_iter.rs: -------------------------------------------------------------------------------- 1 | use savvy::{ 2 | savvy, ComplexSexp, IntegerSexp, LogicalSexp, NotAvailableValue, OwnedComplexSexp, 3 | OwnedIntegerSexp, OwnedLogicalSexp, OwnedRealSexp, OwnedStringSexp, RealSexp, Sexp, StringSexp, 4 | }; 5 | 6 | #[savvy] 7 | fn filter_integer_odd(x: IntegerSexp) -> savvy::Result { 8 | // is_na() is to propagate NAs 9 | let iter = x.iter().copied().filter(|i| i.is_na() || *i % 2 == 0); 10 | let out = OwnedIntegerSexp::try_from_iter(iter)?; 11 | out.into() 12 | } 13 | 14 | #[savvy] 15 | fn filter_real_negative(x: RealSexp) -> savvy::Result { 16 | // is_na() is to propagate NAs 17 | let iter = x.iter().copied().filter(|r| r.is_na() || *r >= 0.0); 18 | let out = OwnedRealSexp::try_from_iter(iter)?; 19 | out.into() 20 | } 21 | 22 | #[savvy] 23 | fn filter_complex_without_im(x: ComplexSexp) -> savvy::Result { 24 | // is_na() is to propagate NAs 25 | let iter = x.iter().copied().filter(|c| c.is_na() || c.im != 0.0); 26 | let out = OwnedComplexSexp::try_from_iter(iter)?; 27 | out.into() 28 | } 29 | 30 | #[savvy] 31 | fn filter_logical_duplicates(x: LogicalSexp) -> savvy::Result { 32 | let mut last: Option = None; 33 | 34 | // Note: bool cannot represent NA, so NAs are just treated as TRUE 35 | let iter = x.iter().filter(|l| { 36 | let pred = match &mut last { 37 | // if the value is the same as the last one, discard it 38 | Some(v) => *l != *v, 39 | // first element is always kept 40 | None => true, 41 | }; 42 | last = Some(*l); 43 | pred 44 | }); 45 | let out = OwnedLogicalSexp::try_from_iter(iter)?; 46 | out.into() 47 | } 48 | 49 | #[savvy] 50 | fn filter_string_ascii(x: StringSexp) -> savvy::Result { 51 | // is_na() is to propagate NAs 52 | let iter = x.iter().filter(|s| s.is_na() || s.is_ascii()); 53 | let out = OwnedStringSexp::try_from_iter(iter)?; 54 | out.into() 55 | } 56 | -------------------------------------------------------------------------------- /R-package/src/savvyExamples-win.def: -------------------------------------------------------------------------------- 1 | EXPORTS 2 | R_init_savvyExamples 3 | -------------------------------------------------------------------------------- /R-package/tests/testthat.R: -------------------------------------------------------------------------------- 1 | # This file is part of the standard setup for testthat. 2 | # It is recommended that you do not modify it. 3 | # 4 | # Where should you do additional test configuration? 5 | # Learn more about the roles of various files in: 6 | # * https://r-pkgs.org/testing-design.html#sec-tests-files-overview 7 | # * https://testthat.r-lib.org/articles/special-files.html 8 | 9 | library(testthat) 10 | library(savvyExamples) 11 | 12 | test_check("savvyExamples") 13 | -------------------------------------------------------------------------------- /R-package/tests/testthat/_snaps/consuming_types.md: -------------------------------------------------------------------------------- 1 | # consuming types work 2 | 3 | Code 4 | x1$print() 5 | Output 6 | ValuePair { a: Value(1), b: Value(2) } 7 | 8 | --- 9 | 10 | Code 11 | x2$print() 12 | Output 13 | ValuePair { a: Value(1), b: Value(2) } 14 | 15 | --- 16 | 17 | Code 18 | x3$print() 19 | Output 20 | ValuePair { a: Value(10), b: Value(20) } 21 | 22 | --- 23 | 24 | Code 25 | x4$print() 26 | Output 27 | ValuePair { a: Value(10), b: Value(20) } 28 | 29 | -------------------------------------------------------------------------------- /R-package/tests/testthat/_snaps/error_handling.md: -------------------------------------------------------------------------------- 1 | # error handling works 2 | 3 | Code 4 | safe_stop() 5 | Output 6 | Foo is Dropped! 7 | Condition 8 | Error: 9 | ! This is an error from inside unwind_protect()! 10 | 11 | --- 12 | 13 | Code 14 | safe_warn() 15 | Output 16 | Foo is Dropped! 17 | Condition 18 | Error: 19 | ! (converted from warning) foo 20 | 21 | -------------------------------------------------------------------------------- /R-package/tests/testthat/test-attributes.R: -------------------------------------------------------------------------------- 1 | test_that("Getting attributes works", { 2 | cl <- c("foo", "bar", "baz") 3 | no_class <- 1:10 4 | with_class <- `class<-`(no_class, cl) 5 | expect_equal(get_class_int(no_class), NULL) 6 | expect_equal(get_class_int(with_class), cl) 7 | 8 | no_names <- 1:26 9 | with_names <- setNames(no_names, LETTERS) 10 | expect_equal(get_names_int(no_names), NULL) 11 | expect_equal(get_names_int(with_names), LETTERS) 12 | 13 | expect_equal(get_dim_int(1L), NULL) 14 | expect_equal(get_dim_int(matrix(1:12, nrow = 3L)), c(3L, 4L)) 15 | 16 | attr <- c("foo", "bar", "baz") 17 | no_attr <- 1:10 18 | with_attr <- `attr<-`(no_attr, "foo", attr) 19 | expect_equal(get_attr_int(no_attr, "foo"), NULL) 20 | expect_equal(get_attr_int(with_attr, "foo"), attr) 21 | }) 22 | 23 | test_that("Setting attributes works", { 24 | expect_s3_class(set_class_int(), c("foo", "bar")) 25 | expect_equal(names(set_names_int()), c("foo", "bar")) 26 | expect_equal(dim(set_dim_int()), c(2L, 3L)) 27 | 28 | attr <- list(a = 10) 29 | x <- set_attr_int("foo", attr) 30 | expect_equal(attr(x, "foo"), attr) 31 | }) 32 | -------------------------------------------------------------------------------- /R-package/tests/testthat/test-complex.R: -------------------------------------------------------------------------------- 1 | test_that("complex works", { 2 | expect_equal(new_complex(3L), rep(0+0i, 3)) 3 | expect_equal(first_complex(1:3 + 1i * (3:1)), 1+3i) 4 | expect_equal(abs_complex(c(3+4i, NA, 1+1i)), c(5, NA, sqrt(2))) 5 | }) 6 | -------------------------------------------------------------------------------- /R-package/tests/testthat/test-consuming_types.R: -------------------------------------------------------------------------------- 1 | test_that("consuming types work", { 2 | a <- Value$new(1L) 3 | b <- Value$new(2L) 4 | expect_equal(a$get(), 1L) 5 | expect_equal(b$get(), 2L) 6 | 7 | # not consumed 8 | x1 <- ValuePair$new_copy(a, b) 9 | expect_snapshot(x1$print()) 10 | 11 | # since they are not consumed, they still can return value 12 | expect_equal(a$get(), 1L) 13 | expect_equal(b$get(), 2L) 14 | 15 | # consumed 16 | x2 <- ValuePair$new(a, b) 17 | expect_snapshot(x2$print()) 18 | 19 | # since they are consumed, this returns an error 20 | expect_error(a$get()) 21 | expect_error(b$get()) 22 | 23 | # method 24 | 25 | a3 <- Value$new(10L) 26 | b3 <- Value$new(20L) 27 | x3 <- a3$pair(b3) 28 | expect_snapshot(x3$print()) 29 | expect_error(a3$get()) 30 | expect_error(b3$get()) 31 | 32 | # bare function 33 | 34 | a4 <- Value$new(10L) 35 | b4 <- Value$new(20L) 36 | x4 <- new_value_pair(a4, b4) 37 | expect_snapshot(x4$print()) 38 | expect_error(a4$get()) 39 | expect_error(b4$get()) 40 | }) 41 | -------------------------------------------------------------------------------- /R-package/tests/testthat/test-enum.R: -------------------------------------------------------------------------------- 1 | test_that("enum works", { 2 | a <- FooEnum$A 3 | expect_s3_class(a, "FooEnum") 4 | expect_output(a$print(), "A") 5 | expect_output(print_foo_enum(a), "A") 6 | expect_output(print_foo_enum_ref(a), "A") 7 | 8 | # print method 9 | expect_output(print(FooEnum$A), "FooEnum::A") 10 | expect_output(print(FooEnum$B), "FooEnum::B") 11 | 12 | # Reject invalid specifications 13 | expect_error(FooEnum$C) 14 | expect_error(FooEnum[["C"]]) 15 | expect_error(FooEnum[[1]]) 16 | 17 | # cannot be modified in a usual way 18 | expect_error(FooEnum$C <- "C") 19 | expect_error(FooEnum[["C"]] <- "C") 20 | 21 | # corrupt 22 | assign(".ptr", 3L, envir = FooEnum$B) 23 | expect_error(print(FooEnum$B)) 24 | }) 25 | -------------------------------------------------------------------------------- /R-package/tests/testthat/test-environment.R: -------------------------------------------------------------------------------- 1 | test_that("environment", { 2 | e1 <- new.env(parent = emptyenv()) 3 | e1$a <- "foo" 4 | 5 | expect_true(var_exists_in_env("a", e1)) 6 | expect_false(var_exists_in_env("b", e1)) 7 | 8 | expect_equal(get_var_in_env("a", e1), "foo") 9 | expect_error(get_var_in_env("b", e1)) 10 | 11 | # doesn't climb up the parent environments 12 | e2 <- new.env(parent = e1) 13 | expect_false(var_exists_in_env("a", e2)) 14 | 15 | set_var_in_env("c", 100L, e1) 16 | expect_equal(e1$c, 100L) 17 | # overwrite 18 | set_var_in_env("c", 300L, e1) 19 | expect_equal(e1$c, 300L) 20 | 21 | # global env 22 | .GlobalEnv$global_obj <- "ABC" 23 | expect_equal(get_var_in_env("global_obj"), "ABC") 24 | }) 25 | -------------------------------------------------------------------------------- /R-package/tests/testthat/test-error_handling.R: -------------------------------------------------------------------------------- 1 | test_that("error handling works", { 2 | # check if a Rust objects are dropped properly 3 | expect_snapshot(safe_stop(), error = TRUE) 4 | expect_equal(get_foo_value(), 0L) # drop is properly done 5 | 6 | expect_warning(safe_warn()) 7 | withr::with_options(list(warn = 2), 8 | expect_snapshot(safe_warn(), error = TRUE) 9 | ) 10 | expect_equal(get_foo_value(), 0L) # drop is properly done 11 | 12 | expect_error(raise_error(), "This is my custom error") 13 | }) 14 | -------------------------------------------------------------------------------- /R-package/tests/testthat/test-from-rust-types.R: -------------------------------------------------------------------------------- 1 | test_that("scalar functions reject non-scalar values and missing values", { 2 | # no error 3 | expect_output(scalar_input_int(1L), "1") 4 | expect_output(scalar_input_real(1.3), "1.3") 5 | expect_output(scalar_input_logical(FALSE), "false") 6 | expect_output(scalar_input_string("foo"), "foo") 7 | 8 | # error 9 | expect_error(scalar_input_int(1:10)) 10 | expect_error(scalar_input_real(c(1, 2))) 11 | expect_error(scalar_input_logical(c(TRUE, FALSE))) 12 | expect_error(scalar_input_str(c("foo", "bar"))) 13 | 14 | expect_error(scalar_input_int(NA_integer_)) 15 | expect_error(scalar_input_real(NA_real_)) 16 | expect_error(scalar_input_logical(NA)) 17 | expect_error(scalar_input_str(NA_character_)) 18 | }) 19 | 20 | test_that("function can return scalar value", { 21 | # no error 22 | expect_equal(scalar_output_int(), 1L) 23 | expect_equal(scalar_output_int2(), 1L) 24 | expect_equal(scalar_output_real(), 1.3) 25 | expect_equal(scalar_output_real2(), 1.3) 26 | expect_equal(scalar_output_complex(), 1.0 + 1.0i) 27 | expect_equal(scalar_output_complex2(), 1.0 + 1.0i) 28 | expect_equal(scalar_output_logical(), FALSE) 29 | expect_equal(scalar_output_logical2(), FALSE) 30 | expect_equal(scalar_output_string(), "foo") 31 | expect_equal(scalar_output_string2(), "foo") 32 | }) 33 | 34 | test_that("sum functions", { 35 | expect_equal(sum_int(1:10), 55L) 36 | expect_equal(sum_real(c(1, 10, 100, 1000)), 1111) 37 | }) 38 | 39 | test_that("conversion from vectors", { 40 | expect_equal(rep_int_vec(3L), c(0L, 0L, 0L)) 41 | expect_equal(rep_int_slice(3L), c(0L, 0L, 0L)) 42 | expect_equal(rep_real_vec(3L), c(0, 0, 0)) 43 | expect_equal(rep_real_slice(3L), c(0, 0, 0)) 44 | expect_equal(rep_bool_vec(3L), c(TRUE, TRUE, TRUE)) 45 | expect_equal(rep_bool_slice(3L), c(TRUE, TRUE, TRUE)) 46 | expect_equal(rep_str_vec(3L), c("foo", "foo", "foo")) 47 | expect_equal(rep_str_slice(3L), c("foo", "foo", "foo")) 48 | }) 49 | 50 | test_that("user-defined structs", { 51 | expect_error(get_name_external(NULL)) 52 | x <- Person$new() 53 | class(x) <- "foo" 54 | expect_error(get_name_external(x)) 55 | 56 | # cannot be modified 57 | expect_error(Person$"aaa" <- "aaa") 58 | expect_error(Person[["aaa"]] <- "aaa") 59 | }) 60 | -------------------------------------------------------------------------------- /R-package/tests/testthat/test-function.R: -------------------------------------------------------------------------------- 1 | test_that("functions works", { 2 | x <- list(a = 1L, b = 2.0, c = "foo") 3 | expect_equal(do_call(list, x), x) 4 | expect_equal(do_call(function(...) list(...), x), x) 5 | 6 | # handle 0-length argument 7 | expect_equal(do_call(list, list()), list()) 8 | 9 | expect_equal(call_with_args(list), x) 10 | expect_equal(call_with_args(function(...) list(...)), x) 11 | }) 12 | -------------------------------------------------------------------------------- /R-package/tests/testthat/test-impls_over_multiple_files.R: -------------------------------------------------------------------------------- 1 | test_that("multiplication works", { 2 | v <- Value$new(1L) 3 | expect_equal(v$get2(), 1L) 4 | }) 5 | -------------------------------------------------------------------------------- /R-package/tests/testthat/test-init_vector.R: -------------------------------------------------------------------------------- 1 | test_that("Owned*Sexp is properly initialized", { 2 | expect_equal(new_real(3L), c(0.0, 0.0, 0.0)) 3 | expect_equal(new_int(3L), c(0, 0, 0)) 4 | expect_equal(new_bool(3L), c(FALSE, FALSE, FALSE)) 5 | }) 6 | -------------------------------------------------------------------------------- /R-package/tests/testthat/test-invalid_pointer.R: -------------------------------------------------------------------------------- 1 | # Taken from https://github.com/pola-rs/r-polars/issues/851#issuecomment-1971551241 2 | test_that("invalid pointer doesn't clash the session", { 3 | rds_file <- tempfile(fileext = ".rds") 4 | 5 | x <- Person$new() 6 | saveRDS(x, rds_file) 7 | 8 | x <- readRDS(rds_file) 9 | expect_error(x$name(), "This external pointer is already consumed or deleted") 10 | }) 11 | -------------------------------------------------------------------------------- /R-package/tests/testthat/test-missing_values.R: -------------------------------------------------------------------------------- 1 | test_that("is_scalar_na() works", { 2 | expect_true(is_scalar_na(NA)) 3 | expect_true(is_scalar_na(NA_character_)) 4 | expect_true(is_scalar_na(NA_real_)) 5 | expect_true(is_scalar_na(NA_integer_)) 6 | 7 | expect_false(is_scalar_na(c(NA, NA))) 8 | expect_false(is_scalar_na(c(NA, NA))) 9 | expect_false(is_scalar_na(NULL)) 10 | }) 11 | -------------------------------------------------------------------------------- /R-package/tests/testthat/test-numeric.R: -------------------------------------------------------------------------------- 1 | test_that("NumericSexp works", { 2 | # i32 to f64 3 | expect_equal( 4 | times_two_numeric_f64(c(1L, NA, 0L, -1L)), 5 | c(2L, NA, 0L, -2L) 6 | ) 7 | 8 | # f64 to f64 9 | expect_equal( 10 | times_two_numeric_f64(c(1.1, NA, 0.0, -1.1, Inf, -Inf)), 11 | c(2.2, NA, 0.0, -2.2, Inf, -Inf) 12 | ) 13 | 14 | # i32 to i32 15 | expect_equal( 16 | times_two_numeric_i32(c(1L, NA, 0L, -1L)), 17 | c(2L, NA, 0L, -2L) 18 | ) 19 | 20 | # f64 to i32 21 | expect_equal( 22 | times_two_numeric_i32(c(1, NA, 0, -1)), 23 | c(2L, NA, 0L, -2L) 24 | ) 25 | 26 | # error cases 27 | expect_error(times_two_numeric_i32(Inf)) # infinite 28 | expect_error(times_two_numeric_i32(2147483648)) # out of i32's range 29 | expect_error(times_two_numeric_i32(c(1.1, -1.1))) # not integer-ish 30 | }) 31 | 32 | test_that("NumericSexp can handle 0-length vectors", { 33 | expect_equal(times_two_numeric_f64(integer(0L)), numeric(0L)) 34 | expect_equal(times_two_numeric_f64(numeric(0L)), numeric(0L)) 35 | expect_equal(times_two_numeric_i32(integer(0L)), integer(0L)) 36 | expect_equal(times_two_numeric_i32(numeric(0L)), integer(0L)) 37 | }) 38 | 39 | test_that("NumericSexp works for usize conversions", { 40 | # i32 to usize 41 | expect_equal( 42 | usize_to_string(c(0L, 10L)), 43 | c("0", "10") 44 | ) 45 | 46 | # f64 to usize 47 | expect_equal( 48 | # 2147483647 = .Machine$integer.max 49 | usize_to_string(c(0.0, 10.0, 2147483648.0, 9007199254740991)), 50 | c("0", "10", "2147483648", "9007199254740991") 51 | ) 52 | 53 | # error cases 54 | expect_error(usize_to_string(NA_integer_)) 55 | expect_error(usize_to_string(NA_real_)) 56 | expect_error(usize_to_string(Inf)) 57 | expect_error(usize_to_string(NaN)) 58 | expect_error(usize_to_string(-1L)) 59 | expect_error(usize_to_string(-1.0)) 60 | expect_error(usize_to_string_scalar(9007199254740992.0)) 61 | }) 62 | 63 | 64 | test_that("NumericScalar works", { 65 | expect_equal(times_two_numeric_f64_scalar(1L), 2) 66 | expect_equal(times_two_numeric_f64_scalar(1), 2) 67 | expect_equal(times_two_numeric_f64_scalar(Inf), Inf) 68 | expect_error(times_two_numeric_f64_scalar(c(1, 2))) 69 | expect_error(times_two_numeric_f64_scalar(NA_integer_)) 70 | expect_error(times_two_numeric_f64_scalar(NA_real_)) 71 | expect_error(times_two_numeric_f64_scalar("1")) 72 | 73 | expect_equal(times_two_numeric_i32_scalar(1L), 2L) 74 | expect_equal(times_two_numeric_i32_scalar(1), 2L) 75 | expect_error(times_two_numeric_i32_scalar(NA_integer_)) 76 | expect_error(times_two_numeric_i32_scalar(NA_real_)) 77 | expect_error(times_two_numeric_i32_scalar(Inf)) # infinite 78 | expect_error(times_two_numeric_i32_scalar(2147483648)) # out of i32's range 79 | expect_error(times_two_numeric_i32_scalar(1.1)) # not integer-ish 80 | }) 81 | 82 | test_that("NumericScalar works for usize conversions", { 83 | # i32 to usize 84 | expect_equal(usize_to_string_scalar(0L), "0") 85 | expect_equal(usize_to_string_scalar(10L), "10") 86 | 87 | # f64 to usize 88 | expect_equal(usize_to_string_scalar(0.0), "0") 89 | expect_equal(usize_to_string_scalar(10.0), "10") 90 | # 2147483647 = .Machine$integer.max 91 | expect_equal(usize_to_string_scalar(2147483648.0), "2147483648") 92 | expect_equal(usize_to_string_scalar(9007199254740991), "9007199254740991") 93 | 94 | # error cases 95 | expect_error(usize_to_string_scalar(NA_integer_)) 96 | expect_error(usize_to_string_scalar(NA_real_)) 97 | expect_error(usize_to_string_scalar(Inf)) 98 | expect_error(usize_to_string_scalar(NaN)) 99 | expect_error(usize_to_string_scalar(-1L)) 100 | expect_error(usize_to_string_scalar(-1.0)) 101 | expect_error(usize_to_string_scalar(9007199254740992.0)) 102 | }) 103 | 104 | test_that("is_numeric() rejects logical (#387)", { 105 | expect_true(is_numeric(0)) 106 | expect_true(is_numeric(NA_real_)) 107 | expect_true(is_numeric(0L)) 108 | expect_true(is_numeric(NA_integer_)) 109 | 110 | expect_false(is_numeric(NA)) 111 | expect_false(is_numeric(NA_character_)) 112 | }) 113 | -------------------------------------------------------------------------------- /R-package/tests/testthat/test-optional_args.R: -------------------------------------------------------------------------------- 1 | test_that("optional arg works", { 2 | expect_equal(default_value_scalar(10L), 10L) 3 | expect_equal(default_value_scalar(), -1L) 4 | expect_equal(default_value_vec(1:10), 55L) 5 | expect_equal(default_value_vec(), -1L) 6 | 7 | expect_equal(FooWithDefault$default_value_associated_fn(10L), 10L) 8 | expect_equal(FooWithDefault$default_value_associated_fn(), -1L) 9 | 10 | x <- FooWithDefault$new(-100L) 11 | expect_equal(x$default_value_method(10L), 10L) 12 | expect_equal(x$default_value_method(), -100L) 13 | 14 | expect_equal(default_value_struct(x), -100L) 15 | expect_equal(default_value_struct(), -1L) 16 | 17 | expect_equal(default_value_enum(FooEnum$A), 1L) 18 | expect_equal(default_value_enum(), -1L) 19 | }) 20 | -------------------------------------------------------------------------------- /R-package/tests/testthat/test-panic.R: -------------------------------------------------------------------------------- 1 | # it seems devtools::test() re-compiles the source code with DEBUG=true, so, at 2 | # least on GitHub CI, this test should succeed. When this test is executed on 3 | # local after the package is build with release profile, this fails. 4 | test_that("panic doesn't crash R session", { 5 | skip_if_not(is_built_with_debug()) 6 | 7 | expect_error(must_panic()) 8 | }) 9 | -------------------------------------------------------------------------------- /R-package/tests/testthat/test-parse-nested.R: -------------------------------------------------------------------------------- 1 | test_that("nested Rust files are properly parsed", { 2 | # should be parsed 3 | expect_output(fun_mod1(), "foo!") 4 | expect_output(fun_mod1_1_foo(), "foo!") 5 | 6 | # should not be parsed 7 | expect_error(fun_mod2()) 8 | expect_error(fun_mod3()) 9 | }) 10 | -------------------------------------------------------------------------------- /R-package/tests/testthat/test-try_from_iter.R: -------------------------------------------------------------------------------- 1 | test_that("try_from_iter() works", { 2 | expect_equal(filter_integer_odd(c(1:10, NA)), c(2L, 4L, 6L, 8L, 10L, NA)) 3 | expect_equal(filter_real_negative(c(1, 0, NA, -1, -2)), c(1, 0, NA)) 4 | expect_equal(filter_logical_duplicates(c(TRUE, TRUE, FALSE, TRUE, FALSE, FALSE, FALSE)), c(TRUE, FALSE, TRUE, FALSE)) 5 | expect_equal(filter_complex_without_im(1 + 1i * c(1, 0, -1, NA)), c(1 + 1i * c(1, -1, NA))) 6 | expect_equal(filter_string_ascii(c("a", "A", "\u30b9\u30d7\u30e9\u30c8\u30a5\u30fc\u30f3", NA)), c("a", "A", NA)) 7 | }) 8 | -------------------------------------------------------------------------------- /R-package/tests/testthat/test-unittest.R: -------------------------------------------------------------------------------- 1 | test_that("functions work", { 2 | # character vector 3 | expect_equal( 4 | to_upper(c("a", NA, "A", "\u3042")), 5 | c("A", NA, "A", "\u3042") 6 | ) 7 | 8 | # character vector and scalar 9 | expect_equal( 10 | add_suffix(c("a", NA, "A", "\u3042"), "foo"), 11 | c("a_foo", NA, "A_foo", "\u3042_foo") 12 | ) 13 | 14 | # integer vector 15 | expect_equal( 16 | times_two_int(c(1L, NA, 0L, -1L)), 17 | c(2L, NA, 0L, -2L) 18 | ) 19 | 20 | # integer vector and scalar 21 | expect_equal( 22 | times_any_int(c(1L, NA, 0L, -1L), 100L), 23 | c(100L, NA, 0L, -100L) 24 | ) 25 | 26 | # real vector 27 | expect_equal( 28 | times_two_real(c(1.1, NA, 0.0, -1.1, Inf, -Inf)), 29 | c(2.2, NA, 0.0, -2.2, Inf, -Inf) 30 | ) 31 | 32 | # real vector and scalar 33 | expect_equal( 34 | times_any_real(c(1.1, NA, 0.0, -1.1, Inf, -Inf), 100.0), 35 | c(110.0, NA, 0.0, -110.0, Inf, -Inf) 36 | ) 37 | 38 | # bool vector 39 | # Note: bool cannot handle NA 40 | # c.f. https://cpp11.r-lib.org/articles/cpp11.html#boolean 41 | expect_equal( 42 | flip_logical(c(TRUE, FALSE, NA)), 43 | c(FALSE, TRUE, TRUE) 44 | ) 45 | 46 | # Use as_slice_raw() to handle NA 47 | expect_equal( 48 | flip_logical_expert_only(c(TRUE, FALSE, NA)), 49 | c(FALSE, TRUE, NA) 50 | ) 51 | 52 | # bool vector and scalar 53 | expect_equal( 54 | or_logical(c(TRUE, FALSE), TRUE), 55 | c(TRUE, TRUE) 56 | ) 57 | 58 | expect_equal( 59 | or_logical(c(TRUE, FALSE), FALSE), 60 | c(TRUE, FALSE) 61 | ) 62 | 63 | expect_equal( 64 | reverse_bits(as.raw(c(0x0f, 0x00, 0x12))), 65 | as.raw(c(0xf0, 0x00, 0x48)) 66 | ) 67 | 68 | expect_equal( 69 | reverse_bit_scalar(as.raw(0x12)), 70 | as.raw(0x48) 71 | ) 72 | 73 | expect_equal( 74 | list_with_no_values(), 75 | list(foo = NULL, bar = NULL) 76 | ) 77 | 78 | expect_equal( 79 | list_with_no_names(), 80 | list(100L, "cool") 81 | ) 82 | 83 | expect_equal( 84 | list_with_names_and_values(), 85 | list(foo = 100L, bar = "cool") 86 | ) 87 | }) 88 | 89 | test_that("functions can handle 0-length vectors", { 90 | expect_equal(to_upper(character(0L)), character(0L)) 91 | expect_equal(times_two_int(integer(0L)), integer(0L)) 92 | expect_equal(times_two_real(numeric(0L)), numeric(0L)) 93 | expect_equal(flip_logical(logical(0L)), logical(0L)) 94 | expect_equal(reverse_bits(raw(0L)), raw(0L)) 95 | }) 96 | 97 | test_that("functions can handle ALTREP", { 98 | expect_equal(times_two_int(1:10), 1:10 * 2L) 99 | }) 100 | 101 | test_that("structs work", { 102 | x <- Person$new() 103 | expect_s3_class(x, "Person") 104 | 105 | expect_equal(x$name(), "") 106 | 107 | x$set_name("foo") 108 | expect_equal(x$name(), "foo") 109 | 110 | expect_equal(Person$associated_function(), "associated_function") 111 | 112 | x2 <- Person$new2() 113 | expect_s3_class(x2, "Person") 114 | expect_equal(x2$name(), "") 115 | }) 116 | 117 | test_that("alternative constructor of a struct works", { 118 | x <- Person$new_with_name("123") 119 | expect_s3_class(x, "Person") 120 | expect_equal(x$name(), "123") 121 | }) 122 | 123 | test_that("function that returns a struct works", { 124 | # bare function 125 | x <- external_person_new() 126 | expect_s3_class(x, "Person") 127 | x$set_name("foo") 128 | expect_equal(x$name(), "foo") 129 | 130 | # method 131 | x2 <- x$another_person() # creates Person2 with copying the name 132 | expect_s3_class(x2, "Person2") 133 | expect_equal(x2$name(), "foo") 134 | }) 135 | 136 | test_that("function that takes a struct works", { 137 | x <- Person$new() 138 | x$set_name("foo") 139 | 140 | expect_equal(get_name_external(x), "foo") 141 | 142 | set_name_external(x, "bar") 143 | expect_equal(get_name_external(x), "bar") 144 | }) 145 | -------------------------------------------------------------------------------- /R-package/tests/testthat/test-unraw.R: -------------------------------------------------------------------------------- 1 | test_that("raw identifiers are treated correctly", { 2 | expect_no_error(fn(TRUE)) 3 | expect_no_error(struct$fn(TRUE)) 4 | expect_no_error(struct$new()) 5 | expect_no_error(print(Enum$enum)) 6 | }) 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Savvy - A simple R extension interface using Rust 2 | 3 | 4 | 5 | 6 | 7 | 8 | [![](https://img.shields.io/github/actions/workflow/status/yutannihilation/savvy/R-CMD-check.yaml?style=flat-square&logo=github)](https://github.com/yutannihilation/savvy/actions/workflows/R-CMD-check.yaml) 9 | [![](https://img.shields.io/crates/v/savvy.svg?style=flat-square&logo=rust)](https://crates.io/crates/savvy) 10 | [![](https://img.shields.io/docsrs/savvy.svg?style=flat-square&logo=docsdotrs)](https://docs.rs/savvy/latest/) 11 | [![](https://img.shields.io/badge/%C2%AF%5C_(%E3%83%84)_%2F%C2%AF-green?style=flat-square&logo=docsdotrs&label=docs%20(dev)&labelColor=grey)](https://yutannihilation.github.io/savvy/reference/savvy/) 12 | 13 | 14 | 15 | **savvy** is a simple R extension interface using Rust, like the 16 | [extendr](https://extendr.github.io/) framework. The name “savvy” comes 17 | from the Japanese word “錆” (pronounced as `sàbí`), which means “Rust”. 18 | 19 | With savvy, you can automatically generate R functions from Rust code. 20 | This is an example of what a savvy-powered function would look like: 21 | 22 | **Rust** 23 | 24 | ``` rust 25 | use savvy::savvy; 26 | 27 | /// Convert to Upper-case 28 | /// 29 | /// @param x A character vector. 30 | /// @export 31 | #[savvy] 32 | fn to_upper(x: StringSexp) -> savvy::Result { 33 | // Use `Owned{type}Sexp` to allocate an R vector for output. 34 | let mut out = OwnedStringSexp::new(x.len())?; 35 | 36 | for (i, e) in x.iter().enumerate() { 37 | // To Rust, missing value is an ordinary value. In `&str`'s case, it's just "NA". 38 | // You have to use `.is_na()` method to distinguish the missing value. 39 | if e.is_na() { 40 | // Set the i-th element to NA 41 | out.set_na(i)?; 42 | continue; 43 | } 44 | 45 | let e_upper = e.to_uppercase(); 46 | out.set_elt(i, e_upper.as_str())?; 47 | } 48 | 49 | out.into() 50 | } 51 | ``` 52 | 53 | **R** 54 | 55 | ``` r 56 | to_upper(c("a", "b", "c")) 57 | #> [1] "A" "B" "C" 58 | ``` 59 | 60 | ## Documents 61 | 62 | - [user guide](https://yutannihilation.github.io/savvy/guide/) 63 | - [savvy 入門](https://yutani.quarto.pub/intro-to-savvy-ja/) (Japanese) 64 | 65 | ## Contributing 66 | 67 | [CONTRIBUTING.md](./CONTRIBUTING.md) 68 | 69 | ## Examples 70 | 71 | A toy example R package can be found in [`R-package/` 72 | directory](https://github.com/yutannihilation/savvy/tree/main/R-package). 73 | 74 | Savvy is used in the following R packages: 75 | 76 | - [prqlr](https://prql.github.io/prqlc-r/) 77 | - [polars](https://github.com/pola-rs/r-polars) 78 | - [string2path](https://yutannihilation.github.io/string2path/) 79 | 80 | ## Thanks 81 | 82 | Savvy is not quite unique. This project is made possible by heavily 83 | taking inspiration from other great projects: 84 | 85 | - The basic idea is of course based on 86 | [extendr](https://github.com/extendr/extendr/). Savvy would not exist 87 | without extendr. 88 | - [cpp11](https://cpp11.r-lib.org/)’s “writable” concept influenced the 89 | design a lot. Also, I learned a lot from the great implementation such 90 | as [the protection 91 | mechanism](https://cpp11.r-lib.org/articles/internals.html#protection). 92 | - [PyO3](https://github.com/PyO3/pyo3) made me realize that the FFI 93 | crate doesn’t need to be a “sys” crate. 94 | -------------------------------------------------------------------------------- /README.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Savvy - A simple R extension interface using Rust" 3 | format: gfm 4 | default-image-extension: "" # quarto-dev/quarto-cli#6092 5 | --- 6 | 7 | 8 | 9 | ```{r} 10 | #| include: false 11 | 12 | knitr::opts_chunk$set( 13 | collapse = TRUE, 14 | comment = "#>", 15 | # fig.path = "man/figures/README-", 16 | out.width = "100%" 17 | ) 18 | ``` 19 | 20 | 21 | 22 | [![](https://img.shields.io/github/actions/workflow/status/yutannihilation/savvy/R-CMD-check.yaml?style=flat-square&logo=github)](https://github.com/yutannihilation/savvy/actions/workflows/R-CMD-check.yaml) 23 | [![](https://img.shields.io/crates/v/savvy.svg?style=flat-square&logo=rust)](https://crates.io/crates/savvy) 24 | [![](https://img.shields.io/docsrs/savvy.svg?style=flat-square&logo=docsdotrs)](https://docs.rs/savvy/latest/) 25 | [![](https://img.shields.io/badge/%C2%AF%5C_(%E3%83%84)_%2F%C2%AF-green?style=flat-square&logo=docsdotrs&label=docs%20(dev)&labelColor=grey)](https://yutannihilation.github.io/savvy/reference/savvy/) 26 | 27 | 28 | 29 | **savvy** is a simple R extension interface using Rust, like the [extendr] framework. 30 | The name "savvy" comes from the Japanese word "錆" (pronounced as `sàbí`), which means "Rust". 31 | 32 | With savvy, you can automatically generate R functions from Rust code. This is 33 | an example of what a savvy-powered function would look like: 34 | 35 | **Rust** 36 | 37 | ``` rust 38 | use savvy::savvy; 39 | 40 | /// Convert to Upper-case 41 | /// 42 | /// @param x A character vector. 43 | /// @export 44 | #[savvy] 45 | fn to_upper(x: StringSexp) -> savvy::Result { 46 | // Use `Owned{type}Sexp` to allocate an R vector for output. 47 | let mut out = OwnedStringSexp::new(x.len())?; 48 | 49 | for (i, e) in x.iter().enumerate() { 50 | // To Rust, missing value is an ordinary value. In `&str`'s case, it's just "NA". 51 | // You have to use `.is_na()` method to distinguish the missing value. 52 | if e.is_na() { 53 | // Set the i-th element to NA 54 | out.set_na(i)?; 55 | continue; 56 | } 57 | 58 | let e_upper = e.to_uppercase(); 59 | out.set_elt(i, e_upper.as_str())?; 60 | } 61 | 62 | out.into() 63 | } 64 | ``` 65 | 66 | **R** 67 | 68 | ``` r 69 | to_upper(c("a", "b", "c")) 70 | #> [1] "A" "B" "C" 71 | ``` 72 | 73 | [extendr]: https://extendr.github.io/ 74 | 75 | ## Documents 76 | 77 | - [user guide](https://yutannihilation.github.io/savvy/guide/) 78 | - [savvy 入門](https://yutani.quarto.pub/intro-to-savvy-ja/) (Japanese) 79 | 80 | ## Contributing 81 | 82 | [CONTRIBUTING.md](./CONTRIBUTING.md) 83 | 84 | ## Examples 85 | 86 | A toy example R package can be found in [`R-package/` directory](https://github.com/yutannihilation/savvy/tree/main/R-package). 87 | 88 | Savvy is used in the following R packages: 89 | 90 | - [prqlr](https://prql.github.io/prqlc-r/) 91 | - [polars](https://github.com/pola-rs/r-polars) 92 | - [string2path](https://yutannihilation.github.io/string2path/) 93 | 94 | ## Thanks 95 | 96 | Savvy is not quite unique. This project is made possible by heavily taking 97 | inspiration from other great projects: 98 | 99 | * The basic idea is of course based on 100 | [extendr](https://github.com/extendr/extendr/). Savvy would not exist without 101 | extendr. 102 | * [cpp11](https://cpp11.r-lib.org/)'s "writable" concept influenced the design a 103 | lot. Also, I learned a lot from the great implementation such as [the 104 | protection mechanism](https://cpp11.r-lib.org/articles/internals.html#protection). 105 | * [PyO3](https://github.com/PyO3/pyo3) made me realize that the FFI crate 106 | doesn't need to be a "sys" crate. 107 | -------------------------------------------------------------------------------- /book/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /book/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Hiroaki Yutani"] 3 | language = "en" 4 | src = "src" 5 | title = "Savvy - A simple R extension interface using Rust" 6 | 7 | [output.html] 8 | site-url = "/savvy/guide/" 9 | git-repository-url = "https://github.com/yutannihilation/savvy" 10 | additional-css = ["custom.css"] 11 | 12 | [output.html.playground] 13 | runnable = false 14 | -------------------------------------------------------------------------------- /book/custom.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;800&display=swap'); 2 | @import url('https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@100;200;300;400;500;600;700;800;900&display=swap'); 3 | 4 | html { 5 | font-family: "Roboto Slab", sans-serif !important; 6 | } 7 | 8 | code { 9 | font-family: "Fira Code", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace !important; 10 | font-weight: 400; 11 | } 12 | 13 | .content .header, 14 | .content .header code { 15 | color: #45b2c6 !important; 16 | font-weight: 800 !important; 17 | } -------------------------------------------------------------------------------- /book/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | [Introduction](./intro.md) 4 | 5 | # Get Started 6 | 7 | - [Get started](./get_started.md) 8 | 9 | # User Guide 10 | 11 | - [Key ideas](./key_ideas.md) 12 | - [`#[savvy]` macro](./savvy_macro.md) 13 | - [Handling Vector Input](./input.md) 14 | - [Handling Vector Output](./output.md) 15 | - [Handling Scalar](./scalar.md) 16 | - [Optional Argument](./optional_arg.md) 17 | - [Type-specific Topics](./type-overview.md) 18 | - [Integer, Real, String, Logical, Raw, And Complex](./atomic_types.md) 19 | - [List](./list.md) 20 | - [Struct](./struct.md) 21 | - [Enum](./enum.md) 22 | - [Error-handling](./error.md) 23 | - [Handling Attributes](./attributes.md) 24 | - [Handling Data Frames](./data_frames.md) 25 | - [Handling Factors](./factor.md) 26 | - [Handling Matrices And Arrays](./matrix.md) 27 | - [Calling R Function]() 28 | - [Testing](./test.md) 29 | - [Advanced Topics](./advanced_topics.md) 30 | - [Initialization Routine](./initialization_routine.md) 31 | - [ALTREP](./altrep.md) 32 | - [Linkage](./linkage.md) 33 | - [Comparison with extendr](./extendr.md) 34 | -------------------------------------------------------------------------------- /book/src/advanced_topics.md: -------------------------------------------------------------------------------- 1 | # Advanced Topics 2 | 3 | ## "External" external pointers 4 | 5 | As described in Struct section, a struct marked with `#[savvy]` is 6 | transparently converted from and into an SEXP of an external pointer. So, 7 | usually, you don't need to think about external pointers. 8 | 9 | However, in some cases, you might need to deal with an external pointer created 10 | by another R package. For example, you might want to access an Apache Arrow data 11 | created by nanoarrow R package. In such caes, you can use unsafe methods 12 | `.cast_unchecked()` or `.cast_mut_unchecked()`. 13 | 14 | ```rust 15 | let foo: &Foo = unsafe { &*ext_ptr_sexp.cast_unchecked::() }; 16 | ``` 17 | -------------------------------------------------------------------------------- /book/src/attributes.md: -------------------------------------------------------------------------------- 1 | # Handling Attributes 2 | 3 | You sometimes need to deal with attributes like `names` and `class`. Savvy 4 | provides the following methods for getting and setting the value of the 5 | attribute. 6 | 7 | 8 | | | Getter method | Setter method | Type | 9 | |:----------|:--------------|:--------------|:-------------| 10 | | `names` | `get_names()` |`set_names()` | `Vec<&str>` | 11 | | `class` | `get_class()` |`set_class()` | `Vec<&str>` | 12 | | `dim` | `get_dim()` |`set_dim()` | `&[i32]` | 13 | | arbitrary | `get_attrib()`|`set_attrib()` | `Sexp` | 14 | 15 | The getter methods return `Option` because the object doesn't always have the 16 | attribute. You can `match` the result like this: 17 | 18 | ```rust 19 | /// @export 20 | #[savvy] 21 | fn get_class_int(x: IntegerSexp) -> savvy::Result { 22 | match x.get_class() { 23 | Some(class) => class.try_into(), 24 | None => ().try_into(), 25 | } 26 | } 27 | ``` 28 | 29 | The setter methods are available only for owned SEXPs. The return type is 30 | `savvy::Result<()>` becuase the conversion from a Rust type to SEXP is fallible. 31 | 32 | ```rust 33 | /// @export 34 | #[savvy] 35 | fn set_class_int() -> savvy::Result { 36 | let mut x = OwnedIntegerSexp::new(1)?; 37 | 38 | x.set_class(&["foo", "bar"])?; 39 | 40 | x.into() 41 | } 42 | ``` 43 | 44 | For attributes other than `names`, `class`, `dim`, you can use `get_attrib()` 45 | and `set_attrib()`. Since an attribute can store arbitrary values, the type is 46 | `Sexp`. In order to extract the underlying value, you can use `.into_typed()` 47 | and `match`. 48 | 49 | ```rust 50 | /// @export 51 | #[savvy] 52 | fn print_attr_values_if_int(attr: &str, value: savvy::Sexp) -> savvy::Result<()> { 53 | let attr_value = value.get_attrib(attr)?; 54 | match attr_value.into_typed() { 55 | TypedSexp::Integer(i) => r_println!("int {:?}", i.as_slice()]), 56 | _ => r_println("not int") 57 | } 58 | 59 | Ok(()) 60 | } 61 | ``` 62 | 63 | In order to set values, you can use `.into()` to convert from the owned SEXP to 64 | a `savvy::Sexp`. 65 | 66 | ```rust 67 | /// @export 68 | #[savvy] 69 | fn set_attr_int(attr: &str) -> savvy::Result { 70 | let s: &[i32] = &[1, 2, 3]; 71 | let attr_value: OwnedIntegerSexp = s.try_into()?; 72 | let mut out = OwnedIntegerSexp::new(1)?; 73 | 74 | out.set_attrib(attr, attr_value.into())?; 75 | 76 | out.into() 77 | } 78 | ``` 79 | -------------------------------------------------------------------------------- /book/src/data_frames.md: -------------------------------------------------------------------------------- 1 | # Handling Data Frames 2 | 3 | A `data.frame` is a list. You should simply handle it as a list in Rust code, and 4 | all `data.frame`-related operations should be done in R code. 5 | 6 | For example, if you want to return the result as a `data.frame`, the Rust 7 | function should return a list, and wrapped by an R function that converts the 8 | list into a data.frame. `tibble::as_tibble()` should be the right choice for 9 | this purpose. Or, if you prefer lightweight dependency, you can use 10 | `vctrs::new_data_frame()`, or simply `as.data.frame()`. 11 | 12 | ```rust 13 | /// @export 14 | #[savvy] 15 | fn foo_impl() -> savvy::Result { 16 | // create a named list 17 | let mut out = savvy::OwnedListSexp::new(2, true)?; 18 | 19 | let x: Vec = some_function(); 20 | let y: Vec = another_function(); 21 | 22 | out.set_name_and_value(0, "x", OwnedRealSexp::try_from_slice(x)?)?; 23 | out.set_name_and_value(1, "y", OwnedRealSexp::try_from_slice(y)?)?; 24 | 25 | out.into() 26 | } 27 | ``` 28 | ```r 29 | foo <- function() { 30 | result <- foo_impl() 31 | tibble::as_tibble(result) 32 | } 33 | ``` -------------------------------------------------------------------------------- /book/src/enum.md: -------------------------------------------------------------------------------- 1 | # Enum 2 | 3 | Savvy supports **fieldless enum** to express the possible options for a 4 | parameter. For example, if you define such an enum with `#[savvy]`, 5 | 6 | ```rust 7 | /// @export 8 | #[savvy] 9 | enum LineType { 10 | Solid, 11 | Dashed, 12 | Dotted, 13 | } 14 | ``` 15 | 16 | it will be available on R's side as this. 17 | 18 | ```r 19 | LineType$Solid 20 | LineType$Dashed 21 | LineType$Dotted 22 | ``` 23 | 24 | You can use the enum type as the argument of such a function like this 25 | 26 | ```rust 27 | /// @export 28 | #[savvy] 29 | fn plot_line(x: IntegerSexp, y: IntegerSexp, line_type: &LineType) -> savvy::Result<()> { 30 | match line_type { 31 | LineType::Solid => { 32 | ... 33 | }, 34 | LineType::Dashed => { 35 | ... 36 | }, 37 | LineType::Dotted => { 38 | ... 39 | }, 40 | } 41 | } 42 | ``` 43 | 44 | so that the users can use it instead of specifying it by an integer or a 45 | character, which might be mistyped. 46 | 47 | ```r 48 | plot_line(x, y, LineType$Solid) 49 | ``` 50 | 51 | Of course, you can archive the same thing with `i32` or `&str` as the input and 52 | match the value. The difference is that enum is typo-proof. But, you might feel 53 | it more handy to use a plain integer or character. 54 | 55 | ```rust 56 | /// @export 57 | #[savvy] 58 | fn plot_line(x: IntegerSexp, y: IntegerSexp, line_type: &str) -> savvy::Result<()> { 59 | match line_type { 60 | "solid" => { 61 | ... 62 | }, 63 | "dashed" => { 64 | ... 65 | }, 66 | "dotted" => { 67 | ... 68 | }, 69 | _ => { 70 | return Err(savvy_err!("Unsupported line type!")); 71 | } 72 | } 73 | } 74 | ``` 75 | 76 | ## Limitation 77 | 78 | As noted above, savvy supports only fieldless enum for simplicity. If you want 79 | to use an enum that contains some value, please wrap it with struct. 80 | 81 | ```rust 82 | // You don't need to mark this with #[savvy] 83 | enum AnimalEnum { 84 | Dog(String, f64), 85 | Cat { name: String, weight: f64 }, 86 | } 87 | 88 | /// @export 89 | #[savvy] 90 | struct Animal(AnimalEnum); 91 | ``` 92 | 93 | Also, savvy currently doesn't support discriminants. For example, this one won't 94 | compile. 95 | 96 | ```rust 97 | /// @export 98 | #[savvy] 99 | enum HttpStatus { 100 | Ok = 200, 101 | NotFound = 404, 102 | } 103 | ``` 104 | -------------------------------------------------------------------------------- /book/src/extendr.md: -------------------------------------------------------------------------------- 1 | # Comparison with extendr 2 | 3 | ## What the hell is this?? Why do you need another framework when there's extendr? 4 | 5 | [extendr](https://extendr.github.io/) is great and ready to use! However, I needed 6 | to create a new, simple framework to experiment with. The main goal of savvy is to 7 | provide a simpler option other than extendr, not to be a complete alternative to 8 | extendr. 9 | 10 | [error]: https://github.com/extendr/extendr/issues/278 11 | 12 | ### Pros and cons compared to extendr 13 | 14 | Pros: 15 | 16 | (Now that extendr has been improved so much, I think savvy lost all the obvious pros. 17 | Kudos to the extendr developers!) 18 | 19 | Cos: 20 | 21 | * savvy prefers explicitness over ergonomics 22 | * savvy provides limited amount of APIs and might not fit for complex usages 23 | 24 | -------------------------------------------------------------------------------- /book/src/factor.md: -------------------------------------------------------------------------------- 1 | # Handling Factors 2 | 3 | A factor is internally an integer vector with the `levels` attribute. You can 4 | handle this on Rust's side, but the recommended way is to write a wrapper R 5 | function to convert the factor vector to a character vector. 6 | 7 | Say there's a Rust function that takes a character vector as its argument. 8 | 9 | ```rust 10 | /// @export 11 | #[extendr] 12 | fn foo_impl(x: StringSexp) -> savvy::Result<()> { 13 | ... 14 | } 15 | ``` 16 | 17 | Then, you can write a function like below to convert the input to a character 18 | vector. If you want better validation, you can use `vctrs::vec_cast()` instead. 19 | 20 | ```r 21 | foo <- function(x) { 22 | x <- as.character(x) 23 | foo_impl(x) 24 | } 25 | ``` 26 | 27 | If you need the information of the order of the levels, you should pass it as an 28 | another argument. 29 | 30 | ```rust 31 | /// @export 32 | #[extendr] 33 | fn foo_impl2(x: StringSexp, levels: StringSexp) -> savvy::Result<()> { 34 | ... 35 | } 36 | ``` 37 | 38 | ```r 39 | foo2 <- function(x) { 40 | levels <- levels(x) 41 | x <- as.character(x) 42 | foo_impl2(x, levels) 43 | } 44 | ``` 45 | 46 | -------------------------------------------------------------------------------- /book/src/get_started.md: -------------------------------------------------------------------------------- 1 | # Get Started 2 | 3 | ## Prerequisite 4 | 5 | ### Rust 6 | 7 | First of all, you need a Rust toolchain installed. You can follow [the official 8 | instruction](https://www.rust-lang.org/tools/install). 9 | 10 | If you are on Windows, you need an additional step of installing 11 | `x86_64-pc-windows-gnu` target. 12 | 13 | ```sh 14 | rustup target add x86_64-pc-windows-gnu 15 | ``` 16 | 17 | ### A helper R package 18 | 19 | Then, install a helper R package for savvy. 20 | 21 | ``` r 22 | install.packages( 23 | "savvy", 24 | repos = c("https://yutannihilation.r-universe.dev", "https://cloud.r-project.org") 25 | ) 26 | ``` 27 | 28 | Note that, under the hood, this is just a simple wrapper around `savvy-cli`. So, 29 | if you prefer shell, you can directly use the CLI instead, which is available on 30 | the [releases](https://github.com/yutannihilation/savvy/releases). 31 | 32 | ## Create a new R package 33 | 34 | First, create a new R package. `usethis::create_package()` is convenient for 35 | this. 36 | 37 | ``` r 38 | usethis::create_package("path/to/foo") 39 | ``` 40 | 41 | Then, move to the package directory and generate necessary files like `Makevars` 42 | and `Cargo.toml`, as well as the C and R wrapper code corresponding to the Rust 43 | code. `savvy::savvy_init()` does this all (under the hood, this simply runs 44 | `savvy-cli init`). 45 | 46 | Lastly, run `devtools::document()` to generate `NAMESPACE` and documents. 47 | 48 | ``` r 49 | savvy::savvy_init() 50 | devtools::document() 51 | ``` 52 | 53 | Now, this package is ready to install! After installing (e.g. by running 54 | "Install Package" on RStudio IDE), confirm you can run this example function 55 | that multiplies the first argument by the second argument. 56 | 57 | ```r 58 | library() 59 | 60 | int_times_int(1:4, 2L) 61 | #> [1] 2 4 6 8 62 | ``` 63 | 64 | ### Package structure 65 | 66 | After `savvy::savvy_init()`, the structure of your R package should look like below. 67 | 68 | ``` 69 | . 70 | ├── .Rbuildignore 71 | ├── DESCRIPTION 72 | ├── NAMESPACE 73 | ├── R 74 | │ └── 000-wrappers.R <-------(1) 75 | ├── configure <-------(2) 76 | ├── configure.win <-------(2) 77 | ├── cleanup <-------(2) 78 | ├── cleanup.win <-------(2) 79 | ├── foofoofoofoo.Rproj 80 | └── src 81 | ├── Makevars.in <-------(2) 82 | ├── Makevars.win.in <-------(2) 83 | ├── init.c <-------(3) 84 | ├── -win.def <---(4) 85 | └── rust 86 | ├── .cargo 87 | │ └── config.toml <-------(4) 88 | ├── api.h <-------(3) 89 | ├── Cargo.toml <-------(5) 90 | └── src 91 | └── lib.rs <-------(5) 92 | ``` 93 | 94 | 1. `000-wrappers.R`: R functions for the corresponding Rust functions 95 | 2. `configure*`, `cleanup*`, `Makevars.in`, and `Makevars.win.in`: Necessary 96 | build settings for compiling Rust code 97 | 3. `init.c` and `api.h`: C functions for the corresponding Rust functions 98 | 4. `-win.def` and `.cargo/config.toml`: These are tricks to avoid 99 | a minor error on Windows. See [extendr/rextendr#211][1] and [savvy#98][2] for 100 | the details. 101 | 5. `Cargo.toml` and `lib.rs`: Rust code 102 | 103 | [1]: https://github.com/extendr/rextendr/issues/211 104 | [2]: https://github.com/yutannihilation/savvy/pull/98 105 | 106 | ## Write your own function 107 | 108 | The most revolutionary point of `savvy::savvy_init()` is that it kindly leaves 109 | the most important task to you; let's define a typical hello-world function for 110 | practice! 111 | 112 | ### Write some Rust code 113 | 114 | Open `src/rust/lib.rs` and add the following lines. `r_println!` is the R 115 | version of `println!` macro. 116 | 117 | ```rust 118 | /// @export 119 | #[savvy] 120 | fn hello() -> savvy::Result<()> { 121 | savvy::r_println!("Hello world!"); 122 | Ok(()) 123 | } 124 | ``` 125 | 126 | ### Update wrapper files 127 | 128 | Every time you modify or add some Rust code, you need to update the C and R 129 | wrapper files by running `savvy::savvy_update()` (under the hood, this simply 130 | runs `savvy-cli update`). Don't forget to run `devtools::document()` as well. 131 | 132 | ``` r 133 | savvy::savvy_update() 134 | devtools::document() 135 | ``` 136 | 137 | After re-installing your package, you should be able to run the `hello()` 138 | function on your R session. 139 | 140 | ```r 141 | hello() 142 | #> Hello world! 143 | ``` -------------------------------------------------------------------------------- /book/src/initialization_routine.md: -------------------------------------------------------------------------------- 1 | # Initialization Routine 2 | 3 | `#[savvy_init]` is a special version of `#[savvy]`. The function marked with 4 | this macro is called when the package is loaded, which is what [Writing R 5 | Extension][wre] calls "initialization routine". The function must take `*mut 6 | DllInfo` as its argument. 7 | 8 | [wre]: https://cran.r-project.org/doc/manuals/r-release/R-exts.html#dyn_002eload-and-dyn_002eunload 9 | 10 | For example, if you write such a Rust function like this, 11 | 12 | ``` rust 13 | use savvy::ffi::DllInfo; 14 | 15 | #[savvy_init] 16 | fn init_foo(_dll_info: *mut DllInfo) -> savvy::Result<()> { 17 | r_eprintln!("Initialized!"); 18 | Ok(()) 19 | } 20 | ``` 21 | 22 | You'll see the following message on your R session when you load the package. 23 | 24 | ```r 25 | library(yourPackage) 26 | #> Initialized! 27 | ``` 28 | 29 | Under the hood, `savvy-cli update .` inserts the following line in a C function 30 | `R_init_*()`, which is called when the DLL is loaded. 31 | 32 | ``` c 33 | void R_init_yourPackage(DllInfo *dll) { 34 | R_registerRoutines(dll, NULL, CallEntries, NULL, NULL); 35 | R_useDynamicSymbols(dll, FALSE); 36 | 37 | savvy_init_foo__impl(dll); // added! 38 | } 39 | ``` 40 | 41 | This is useful for initializing resources. For example, you can initialize a 42 | global variable. 43 | 44 | ``` rust 45 | use std::sync::OnceLock; 46 | 47 | static GLOBAL_FOO: OnceLock = OnceLock::new(); 48 | 49 | #[savvy_init] 50 | fn init_global_foo(dll_info: *mut DllInfo) -> savvy::Result<()> { 51 | GLOBAL_FOO.get_or_init(|| Foo::new()); 52 | 53 | Ok(()) 54 | } 55 | ``` 56 | 57 | You can also register an ALTREP class using this mechanism see [the next page](./altrep.html). 58 | -------------------------------------------------------------------------------- /book/src/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | **savvy** is a simple R extension interface using Rust, like the 4 | [extendr](https://extendr.github.io/) framework. The name “savvy” comes 5 | from the Japanese word “錆” (pronounced as `sàbí`), which means “Rust”. 6 | 7 | With savvy, you can automatically generate R functions from Rust code. 8 | This is an example of what a savvy-powered function would look like: 9 | 10 | **Rust** 11 | 12 | ``` rust 13 | use savvy::savvy; 14 | use savvy::NotAvailableValue; // for is_na() and na() 15 | 16 | /// Convert to Upper-case 17 | /// 18 | /// @param x A character vector. 19 | /// @export 20 | #[savvy] 21 | fn to_upper(x: StringSexp) -> savvy::Result { 22 | // Use `Owned{type}Sexp` to allocate an R vector for output. 23 | let mut out = OwnedStringSexp::new(x.len())?; 24 | 25 | for (i, e) in x.iter().enumerate() { 26 | // To Rust, missing value is an ordinary value. In `&str`'s case, it's just "NA". 27 | // You have to use `.is_na()` method to distinguish the missing value. 28 | if e.is_na() { 29 | // Set the i-th element to NA 30 | out.set_na(i)?; 31 | continue; 32 | } 33 | 34 | let e_upper = e.to_uppercase(); 35 | out.set_elt(i, e_upper.as_str())?; 36 | } 37 | 38 | out.into() 39 | } 40 | ``` 41 | 42 | **R** 43 | 44 | ``` r 45 | to_upper(c("a", "b", "c")) 46 | #> [1] "A" "B" "C" 47 | ``` 48 | 49 | ## Examples 50 | 51 | A toy example R package can be found in [`R-package/` 52 | directory](https://github.com/yutannihilation/savvy/tree/main/R-package). 53 | 54 | ## Links 55 | 56 | * [crates.io](https://crates.io/crates/savvy) 57 | * [API reference](https://docs.rs/savvy/latest/) 58 | * [API reference (dev version)](https://yutannihilation.github.io/savvy/reference/savvy/) 59 | 60 | ## Thanks 61 | 62 | Savvy is not quite unique. This project is made possible by heavily taking 63 | inspiration from other great projects: 64 | 65 | * The basic idea is of course based on 66 | [extendr](https://github.com/extendr/extendr/). Savvy would not exist without 67 | extendr. 68 | * [cpp11](https://cpp11.r-lib.org/)'s "writable" concept influenced the design a 69 | lot. Also, I learned a lot from the great implementation such as [the 70 | protection mechanism](https://cpp11.r-lib.org/articles/internals.html#protection). 71 | * [PyO3](https://github.com/PyO3/pyo3) made me realize that the FFI crate 72 | doesn't need to be a "sys" crate. 73 | -------------------------------------------------------------------------------- /book/src/key_ideas.md: -------------------------------------------------------------------------------- 1 | # Key Ideas 2 | 3 | ## Treating external SEXP and owned SEXP differently 4 | 5 | Savvy is opinionated in many points. Among these, one thing I think should be 6 | explained first is that savvy uses separate types for SEXP passed from outside 7 | and that created within Rust function. The former, external SEXP, is read-only, 8 | and the latter, owned SEXP, is writable. Here's the list: 9 | 10 | | R type | Read-only version | Writable version | 11 | |:--------------------------------|:------------------------|:---------------------| 12 | | `INTSXP` (integer) | `IntegerSexp` | `OwnedIntegerSexp` | 13 | | `REALSXP` (double) | `RealSexp` | `OwnedRealSexp` | 14 | | `RAWSXP` (raw) | `RawSexp` | `OwnedRawSexp` | 15 | | `LGLSXP` (logical) | `LogicalSexp` | `OwnedLogicalSexp` | 16 | | `STRSXP` (character) | `StringSexp` | `OwnedStringSexp` | 17 | | `VECSXP` (list) | `ListSexp` | `OwnedListSexp` | 18 | | `EXTPTRSXP` (external pointer) | `ExternalPointerSexp` | n/a | 19 | | `CPLXSXP` (complex)[^1] | `ComplexSexp` | `OwnedComplexSexp` | 20 | 21 | [^1]: Complex is optionally supported under feature flag `complex` 22 | 23 | You might wonder why this is needed when we can just use `mut` to distinguish 24 | the difference of mutability. I mainly had two motivations for this: 25 | 26 | 1. **avoid unnecessary protection**: an external SEXP are already protected by 27 | the caller, while an owned SEXP needs to be protected by ourselves. 28 | 2. **avoid unnecessary ALTREP checks**: an external SEXP can be ALTREP, so it's 29 | better to handle them in ALTREP-aware way, while an owned SEXP is not. 30 | 31 | This would be a bit lengthy, so let's skip here. You can read the details on [my 32 | blog post][blog1]. But, one correction is that I found the second reason might 33 | not be very important because a benchmark showed it's more efficient to be 34 | non-ALTREP-aware in most of the cases. Actually, the current implementation of 35 | savvy is non-ALTREP-aware for int, real, and logical (See [#18][issue18]). 36 | 37 | [blog1]: https://yutani.rbind.io/post/intro-to-savvy-part1/ 38 | [issue18]: https://github.com/yutannihilation/savvy/issues/18 39 | 40 | ## No implicit conversions 41 | 42 | Savvy doesn't provide conversion between types unless you do explicitly. For 43 | example, you cannot supply a double vector to a function with a `IntegerSexp` 44 | argument. 45 | 46 | ```rust 47 | #[savvy] 48 | fn identity_int(x: IntegerSexp) -> savvy::Result { 49 | let mut out = OwnedIntegerSexp::new(x.len())?; 50 | 51 | for (i, &v) in x.iter().enumerate() { 52 | out[i] = v; 53 | } 54 | 55 | out.into() 56 | } 57 | ``` 58 | 59 | ``` r 60 | identity_int(c(1, 2)) 61 | #> Error in identity_int(c(1, 2)) : 62 | #> Unexpected type: Cannot convert double to integer 63 | ``` 64 | 65 | While you probably feel this is inconvenient, this is also a design decision. 66 | My concerns on supporting these conversion are 67 | 68 | * Complexity. It would make savvy's spec and implemenatation complicated. 69 | * Hidden allocation. Conversion requires a new allocation for storing the 70 | converted values, which might be unhappy in some cases. 71 | 72 | So, you have to write some wrapper R function like below. This might feel a bit 73 | tiring, but, in general, **please do not avoid writing R code**. Since you are 74 | creating an R package, there's a lot you can do in R code instead of making 75 | things complicated in Rust code. Especially, it's easier on R's side to show 76 | user-friendly error messages. 77 | 78 | ``` r 79 | identity_int_wrapper <- function(x) { 80 | x <- vctrs::vec_cast(x, integer()) 81 | identity_int(x) 82 | } 83 | ``` 84 | 85 | Alternatively, you can use `NumericSexp` as input. This provides a method to 86 | convert the input either to `i32` or to `f64` on the fly. For more details, 87 | please read [the section about `NumericSexp`](https://yutannihilation.github.io/savvy/guide/atomic_types.html#numericsexp) 88 | 89 | ```rust 90 | #[savvy] 91 | fn identity_num(x: NumericSexp) -> savvy::Result { 92 | let mut out = OwnedIntegerSexp::new(x.len())?; 93 | 94 | for (i, &v) in x.iter_i32().enumerate() { 95 | out[i] = v; 96 | } 97 | 98 | out.into() 99 | } 100 | ``` -------------------------------------------------------------------------------- /book/src/linkage.md: -------------------------------------------------------------------------------- 1 | # Linkage 2 | 3 | Savvy compiles the Rust code into a static library and then use it to generate a 4 | DLL for the R package. There's one tricky thing about static library. [The 5 | Rust's official document about linkage][linkage] says 6 | 7 | [linkage]: https://doc.rust-lang.org/reference/linkage.html 8 | 9 | > Note that any dynamic dependencies that the static library may have (such as 10 | > dependencies on system libraries, or dependencies on Rust libraries that are 11 | > compiled as dynamic libraries) will have to be specified manually when linking 12 | > that static library from somewhere. 13 | 14 | What does this mean? If some of the dependency crate needs linking to a native 15 | library, the necessary compiler flags are added by `cargo`. But, after creating 16 | the static library, `cargo`'s turn is over. It's you who have to tell the linker 17 | the necessary flags because there's no automatic mechanism. 18 | 19 | If some of the flags are missing, you'll see a "symbol not found" error. For 20 | example, this is what I got on macOS. Some dependency of my package uses the 21 | [objc2](https://github.com/madsmtm/objc2) crate, and it needs to be linked 22 | against Apple's Objective-C frameworks. 23 | 24 | ``` 25 | unable to load shared object '.../foo.so': 26 | dlopen(../foo.so, 0x0006): symbol not found in flat namespace '_NSAppKitVersionNumber' 27 | Execution halted 28 | ``` 29 | 30 | So, how can we know the necessary flags? The official document provides a 31 | pro-tip! 32 | 33 | > The `--print=native-static-libs` flag may help with this. 34 | 35 | You can add this option to `src/Makevars.in` and `src/Makevars.win.in` via 36 | `RUSTFLAGS` envvar. Please edit this line. 37 | 38 | ``` diff 39 | # Add flags if necessary 40 | - RUSTFLAGS = 41 | + RUSTFLAGS = --print=native-static-libs 42 | ``` 43 | 44 | Then, you'll find this note in the installation log. 45 | 46 | ```sh 47 | Compiling ahash v0.8.11 48 | Compiling serde v1.0.210 49 | Compiling zerocopy v0.7.35 50 | 51 | ...snip... 52 | 53 | note: Link against the following native artifacts when linking against this static library. The order and any duplication can be significant on some platforms. 54 | 55 | note: native-static-libs: -framework CoreText -framework CoreGraphics -framework CoreFoundation -framework Foundation -lobjc -liconv -lSystem -lc -lm 56 | 57 | Finished `dev` profile [unoptimized + debuginfo] target(s) in 19.17s 58 | gcc -shared -L/usr/lib64/R/lib -Wl,-O1 -Wl,--sort-common -Wl,... 59 | installing to /tmp/RtmpvQv8Ur/devtools_install_... 60 | ** checking absolute paths in shared objects and dynamic libraries 61 | ``` 62 | 63 | You can copy these flags to `cargo build`. Please be aware that this differs on 64 | platforms, so you probably need to run this command on CI, not on your local. 65 | Also, since Linux and macOS requires different options, you need to tweak it in 66 | the configure script. 67 | 68 | For example, here's my setup on [the vellogd package](https://github.com/yutannihilation/vellogd-r). 69 | 70 | `./configure`: 71 | 72 | ```sh 73 | if [ "$(uname)" = "Darwin" ]; then 74 | FEATURES="" 75 | # result of --print=native-static-libs 76 | ADDITIONAL_PKG_LIBS="-framework CoreText -framework CoreGraphics -framework CoreFoundation -framework Foundation -lobjc -liconv -lSystem -lc -lm" 77 | else 78 | FEATURES="--features use_winit" 79 | fi 80 | ``` 81 | 82 | `src/Makevars.in`: 83 | 84 | ```make 85 | PKG_LIBS = -L$(LIBDIR) -lvellogd @ADDITIONAL_PKG_LIBS@ 86 | ``` -------------------------------------------------------------------------------- /book/src/matrix.md: -------------------------------------------------------------------------------- 1 | # Handling Matrices And Arrays 2 | 3 | Savvy doesn't provide a convenient way of converting matrices and arrays. You 4 | have to do it by yourself. But, don't worry, it's probably not very difficult 5 | thanks to the fact that major Rust matrix crates are column-majo, or at least 6 | support column-major. 7 | 8 | * [ndarray](https://crates.io/crates/ndarray): row-major is default (probably for compatibility with Python ndarray?), but it offers column-major as well 9 | * [nalgebra](https://crates.io/crates/nalgebra): column-major 10 | * [glam](https://crates.io/crates/glam) (and probably all other rust-gamedev crates): column-major, probably because GLSL is column-major 11 | 12 | The example code can be found at . 13 | 14 | ## R to Rust 15 | 16 | ### ndarray 17 | 18 | By default, ndarray is row-major, but you can specify column-major by 19 | [`f()`](https://docs.rs/ndarray/latest/ndarray/struct.ArrayBase.html#impl-ArrayBase%3CS%2C%20D%3E). 20 | So, all you have to do is simply to extract the `dim` and pass it to ndarray. 21 | 22 | ```rust 23 | use ndarray::Array; 24 | use ndarray::ShapeBuilder; 25 | use savvy::{r_println, savvy, RealSexp}; 26 | 27 | /// @export 28 | #[savvy] 29 | fn ndarray_input(x: RealSexp) -> savvy::Result<()> { 30 | // In R, dim is i32, so you need to convert it to usize first. 31 | let dim_i32 = x.get_dim().ok_or("no dimension found")?; 32 | let dim: Vec = dim_i32.iter().map(|i| *i as usize).collect(); 33 | 34 | // f() changes the order from row-major (C-style convention) to column-major (Fortran-style convention). 35 | let a = Array::from_shape_vec(dim.f(), x.to_vec()); 36 | 37 | r_println!("{a:?}"); 38 | 39 | Ok(()) 40 | } 41 | ``` 42 | 43 | ### nalgebra 44 | 45 | nalgebra is column-major, so you can simply pass the `dim`. 46 | 47 | ```rust 48 | use nalgebra::DMatrix; 49 | use savvy::{r_println, savvy, RealSexp}; 50 | 51 | /// @export 52 | #[savvy] 53 | fn nalgebra_input(x: RealSexp) -> savvy::Result<()> { 54 | let dim = x.get_dim().ok_or("no dimension found")?; 55 | 56 | if dim.len() != 2 { 57 | return Err(savvy_err!("Input must be matrix!")); 58 | } 59 | 60 | let m = DMatrix::from_vec(dim[0] as _, dim[1] as _, x.to_vec()); 61 | 62 | r_println!("{m:?}"); 63 | 64 | Ok(()) 65 | } 66 | ``` 67 | 68 | ### glam 69 | 70 | glam is also column-major. In the case with glam, probably the dimension is 71 | fixed (e.g. 3 x 3 in the following code). You can check the dimension is as 72 | expected before passing it to the constructor of a matrix. 73 | 74 | ```rust 75 | use glam::{dmat3, dvec3, DMat3}; 76 | use savvy::{r_println, savvy, OwnedRealSexp, RealSexp}; 77 | 78 | /// @export 79 | #[savvy] 80 | fn glam_input(x: RealSexp) -> savvy::Result<()> { 81 | let dim = x.get_dim().ok_or("no dimension found")?; 82 | 83 | if dim != [3, 3] { 84 | return Err(savvy_err!("Input must be 3x3 matrix!")); 85 | } 86 | 87 | // As we already check the dimension, this must not fail 88 | let x_array: &[f64; 9] = x.as_slice().try_into().unwrap(); 89 | 90 | let m = DMat3::from_cols_array(x_array); 91 | 92 | r_println!("{m:?}"); 93 | 94 | Ok(()) 95 | } 96 | ``` 97 | 98 | ## Rust to R 99 | 100 | The matrix libraries typically provides method to get the dimension and the 101 | slice of underlying memory. You set the dimension by `set_dim()`. 102 | 103 | ```rust 104 | /// @export 105 | #[savvy] 106 | fn nalgebra_output() -> savvy::Result { 107 | let m = DMatrix::from_vec(2, 3, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); 108 | 109 | let shape = m.shape(); 110 | let dim = &[shape.0, shape.1]; 111 | 112 | let mut out = OwnedRealSexp::try_from(m.as_slice())?; 113 | out.set_dim(dim)?; 114 | 115 | out.into() 116 | } 117 | ``` 118 | -------------------------------------------------------------------------------- /book/src/optional_arg.md: -------------------------------------------------------------------------------- 1 | # Optional Argument 2 | 3 | To represent an optional argument, you can wrap it with `Option`. Then, the 4 | corresponding R function sets the default value of `NULL` on the argument. 5 | 6 | ``` rust 7 | #[savvy] 8 | fn default_value_vec(x: Option) -> savvy::Result { 9 | if let Some(x) = x { 10 | x.iter().sum::().try_into() 11 | } else { 12 | (-1).try_into() 13 | } 14 | } 15 | ``` 16 | 17 | ``` r 18 | function(x = NULL) { 19 | .Call(savvy_default_value_vec__impl, x) 20 | } 21 | ``` 22 | 23 | This function works with or without the argument. 24 | 25 | ``` r 26 | default_value_vec(1:10) 27 | #> [1] 55 28 | 29 | default_value_vec() 30 | #> [1] -1 31 | ``` 32 | -------------------------------------------------------------------------------- /book/src/output.md: -------------------------------------------------------------------------------- 1 | # Handling Vector Output 2 | 3 | Basically, there are two ways to prepare an output to the R session. 4 | 5 | ## 1. Create a new R object first and put values on it 6 | 7 | An owned SEXP can be allocated by using `Owned{type}Sexp::new()`. `new()` takes 8 | the length of the vector as the argument. If you need the same length of vector 9 | as the input, you can pass the `len()` of the input `SEXP`. 10 | 11 | `new()` returns `Result` because the memory allocation can fail in case when the 12 | vector is too large. You can probably just add `?` to it to handle the error. 13 | 14 | ```rust 15 | let mut out = OwnedStringSexp::new(x.len())?; 16 | ``` 17 | 18 | Use `set_elt()` to put the values one by one. Note that you can also assign 19 | values like `out[i] = value` for integer and double. See [Type-specific 20 | Topics](./07_type_specific.md) for more details. 21 | 22 | ```rust 23 | for (i, e) in x.iter().enumerate() { 24 | // ...snip... 25 | 26 | out.set_elt(i, &format!("{e}_{y}"))?; 27 | } 28 | ``` 29 | 30 | You can use `set_na()` to set the specified element as NA. For example, it's a 31 | common case to use this in order to propagate the missingness like below. 32 | 33 | ```rust 34 | for (i, e) in x.iter().enumerate() { 35 | // ...snip... 36 | if e.is_na() { 37 | out.set_na(i)?; 38 | } else { 39 | // ...snip... 40 | } 41 | } 42 | ``` 43 | 44 | After putting the values to the vector, you can convert it to `Result` by 45 | `into()`. 46 | 47 | ```rust 48 | /// @export 49 | #[savvy] 50 | fn foo() -> savvy::Result { 51 | let mut out = OwnedStringSexp::new(x.len())?; 52 | 53 | // ...snip... 54 | 55 | out.into() 56 | } 57 | ``` 58 | 59 | ## 2. Convert a Rust vector by methods like `try_into()` 60 | 61 | Another way is to use a Rust vector to store the results and convert it to an R 62 | object at the end of the function. This is also fallible because this anyway 63 | needs to create a new R object under the hood, which can fail. So, this time, 64 | the conversion is `try_into()`, not `into()`. 65 | 66 | ```rust 67 | // Let's not consider for handling NAs at all for simplicity... 68 | 69 | /// @export 70 | #[savvy] 71 | fn times_two(x: IntegerSexp) -> savvy::Result { 72 | let out: Vec = x.iter().map(|v| v * 2).collect(); 73 | out.try_into() 74 | } 75 | ``` 76 | 77 | Note that, while this looks handy, this might not be very efficient; for example, 78 | `times_two()` above allocates a Rust vector, and then copy the values into a new 79 | R vector in `try_into()`. The copying cost can be innegligible when the vector 80 | is very huge. 81 | 82 | 83 | ### `try_from_slice()` 84 | 85 | The same conversions are also available in the form of 86 | `Owned{type}Sexp::try_from_slice()`. While this says "slice", this accepts 87 | `AsRef<[T]>`, which means both `Vec` and `&[T]` can be used. 88 | 89 | For converting the return value, probably `try_from()` is shorter in most of the 90 | cases. But, sometimes you might find this useful (e.g., the return value is a 91 | list and you need to construct the elements of it). 92 | 93 | ```rust 94 | /// @export 95 | #[savvy] 96 | fn times_two2(x: IntegerSexp) -> savvy::Result { 97 | let out: Vec = x.iter().map(|v| v * 2).collect(); 98 | let out_sexp: OwnedIntegerSexp::try_from_slice(out); 99 | out_sexp.into() 100 | } 101 | ``` 102 | 103 | ### `try_from_iter()` 104 | 105 | If you only have an iterator, `try_from_iter()` is more efficient. This example 106 | function is the case. The previous examples first `collect()`ed into a `Vec`, 107 | but it's not necessary in theory. 108 | 109 | ```rust 110 | /// @export 111 | #[savvy] 112 | fn times_two3(x: IntegerSexp) -> savvy::Result { 113 | let iter = x.iter().map(|v| v * 2); 114 | let out_sexp: OwnedIntegerSexp::try_from_iter(iter); 115 | out_sexp.into() 116 | } 117 | ``` 118 | 119 | Note that, if you already have a slice or vec, you should use `try_from_slice()` 120 | instead of calling `iter()` on the slice or vec and using `try_from_iter()`. In 121 | such cases, `try_from_slice()` is more performant for integer, double, and 122 | complex because it just copies the underlying memory into SEXP rather than 123 | handling the elements one by one. 124 | -------------------------------------------------------------------------------- /book/src/savvy_macro.md: -------------------------------------------------------------------------------- 1 | # `#[savvy]` macro 2 | 3 | This is a simple Rust function to add the specified suffix to the input 4 | character vector. `#[savvy]` macro turns this into an R function. 5 | 6 | ```rust 7 | use savvy::NotAvailableValue; // for is_na() and na() 8 | 9 | /// Add Suffix 10 | /// 11 | /// @export 12 | #[savvy] 13 | fn add_suffix(x: StringSexp, y: &str) -> savvy::Result { 14 | let mut out = OwnedStringSexp::new(x.len())?; 15 | 16 | for (i, e) in x.iter().enumerate() { 17 | if e.is_na() { 18 | out.set_na(i)?; 19 | continue; 20 | } 21 | 22 | out.set_elt(i, &format!("{e}_{y}"))?; 23 | } 24 | 25 | out.into() 26 | } 27 | ``` 28 | 29 | ## Convention for a `#[savvy]` function 30 | 31 | The example function above has this signature. 32 | 33 | ```rust 34 | fn add_suffix(x: StringSexp, y: &str) -> savvy::Result 35 | ``` 36 | 37 | As you can guess, `#[savvy]` macro cannot be applied to arbitrary functions. The 38 | function must satisfy the following conditions: 39 | 40 | * The function's inputs can be 41 | * a non-owned savvy type (e.g., `IntegerSexp` and `RealSexp`) 42 | * a corresponding Rust type for scalar (e.g., `i32` and `f64`) 43 | * a user-defined struct marked with `#[savvy]` (`&T`, `&mut T`, or `T`) 44 | * a user-defined enum marked with `#[savvy]` (`&T`, or `T`) 45 | * any of above wrapped with `Option` (this is translated as an optional arg) 46 | * The function's return value must be either 47 | * `savvy::Result<()>` for the case of no actual return value 48 | * `savvy::Result` for the case of some return value of R object 49 | * `savvy::Result` for the case of some return value of a user-defined 50 | struct or enum marked with `#[savvy]` 51 | 52 | ## How things work under the hood 53 | 54 | If you mark a funtion with `#[savvy]` macro, the corresponding implementations are generated: 55 | 56 | 1. Rust functions 57 | 1. a wrapper function to handle Rust and R errors gracefully 58 | 2. a function with the original body and some conversion from raw `SEXP`s to savvy types. 59 | 2. C function signature for the Rust function 60 | 3. C implementation for bridging between R and Rust 61 | 4. R implementation 62 | 63 | For example, the above implementation generates the following codes. (`#[savvy]` 64 | macro can also be used on `struct` and `enum`, but let's focus on function's 65 | case for now for simplicity.) 66 | 67 | ### Rust functions 68 | 69 | (The actual code is a bit more complex to handle possible `panic!` properly.) 70 | 71 | ```rust 72 | #[allow(clippy::missing_safety_doc)] 73 | #[no_mangle] 74 | pub unsafe extern "C" fn savvy_add_suffix__ffi(x: SEXP, y: SEXP) -> SEXP { 75 | match savvy_add_suffix_inner(x, y) { 76 | Ok(result) => result.0, 77 | Err(e) => savvy::handle_error(e), 78 | } 79 | } 80 | 81 | unsafe fn savvy_add_suffix_inner(x: SEXP, y: SEXP) -> savvy::Result { 82 | let x = ::try_from(savvy::Sexp(x))?; 83 | let y = <&str>::try_from(savvy::Sexp(y))?; 84 | 85 | // original function 86 | add_suffix(x, y) 87 | } 88 | 89 | // original function 90 | fn add_suffix(x: StringSexp, y: &str) -> savvy::Result { 91 | 92 | // ..original body.. 93 | 94 | } 95 | ``` 96 | 97 | ### C function signature 98 | 99 | ```c 100 | SEXP savvy_add_suffix__ffi(SEXP c_arg__x, SEXP c_arg__y); 101 | ``` 102 | 103 | ### C implementation 104 | 105 | (let's skip the details about `handle_result` for now) 106 | 107 | ```c 108 | SEXP savvy_add_suffix__impl(SEXP c_arg__x, SEXP c_arg__y) { 109 | SEXP res = savvy_add_suffix__ffi(c_arg__x, c_arg__y); 110 | return handle_result(res); 111 | } 112 | ``` 113 | 114 | ### R implementation 115 | 116 | The Rust comments with three slashes (`///`) is converted into Roxygen comments 117 | on R code. 118 | 119 | ```r 120 | #' Add Suffix 121 | #' 122 | #' @export 123 | add_suffix <- function(x, y) { 124 | .Call(add_suffix__impl, x, y) 125 | } 126 | ``` 127 | 128 | ## Using `#[savvy]` on other files than `lib.rs` 129 | 130 | You can use `#[savvy]` macro just the same as `lib.rs`. Since `#[savvy]` 131 | automatically marks the functions necessary to be exposed as `pub`, you don't 132 | need to care about the visibility. 133 | 134 | For exampple, if you define a function in `src/foo.rs`, 135 | 136 | ```rust 137 | #[savvy] 138 | fn do_nothing() -> savvy::Result<()> { 139 | Ok(()) 140 | } 141 | ``` 142 | 143 | just declaring `mod foo` in `src/lib.rs` is enough to make `do_nothing()` 144 | available to R. 145 | 146 | ```rust 147 | mod foo; 148 | ``` 149 | -------------------------------------------------------------------------------- /book/src/scalar.md: -------------------------------------------------------------------------------- 1 | # Handling Scalar 2 | 3 | ## Input 4 | 5 | Scalar inputs are handled transparently. The corresponding types are shown in 6 | the table below. 7 | 8 | ```rust 9 | /// @export 10 | #[savvy] 11 | fn scalar_input_int(x: i32) -> savvy::Result<()> { 12 | savvy::r_println!("{x}"); 13 | Ok(()) 14 | } 15 | ``` 16 | 17 | | R type | Rust scalar type | 18 | | :---------------- | :----------------------- | 19 | | integer | `i32` | 20 | | double | `f64` | 21 | | logical | `bool` | 22 | | raw | `u8` | 23 | | character | `&str` | 24 | | complex | `num_complex::Complex64` | 25 | | integer or double | `savvy::NumericScalar` | 26 | 27 | ### `NumericScalar` 28 | 29 | `NumericScalar` is a special type that can handle both integeer and double. You 30 | can get the value from it by `as_i32()` for `i32`, or `as_f64()` for `f64`. 31 | These method converts the value if the input type is different from the target 32 | type. 33 | 34 | ```rust 35 | #[savvy] 36 | fn times_two_numeric_i32_scalar(x: NumericScalar) -> savvy::Result { 37 | let v = x.as_i32()?; 38 | if v.is_na() { 39 | (i32::na()).try_into() 40 | } else { 41 | (v * 2).try_into() 42 | } 43 | } 44 | ``` 45 | 46 | Note that, while `as_f64()` is infallible, `as_i32()` can fail when the 47 | conversion is from `f64` to `i32` and 48 | 49 | - the value is `Inf` or `-Inf` 50 | - the value is out of range for `i32` 51 | - the value is not integer-ish (e.g. `1.1`) 52 | 53 | For convenience, `NumericScalar` also provides a conversion to usize by 54 | `as_usize()`. What's good is that this can handle integer-ish numeric, which 55 | means you can allow users to input a larger number than the integer max 56 | (2147483647)! 57 | 58 | ```rust 59 | fn usize_to_string_scalar(x: NumericScalar) -> savvy::Result { 60 | let x_usize = x.as_usize()?; 61 | x_usize.to_string().try_into() 62 | } 63 | ``` 64 | 65 | ```r 66 | usize_to_string_scalar(2147483648) 67 | #> [1] "2147483648" 68 | ``` 69 | 70 | ## Output 71 | 72 | Just like a Rust vector, a Rust scalar value can be converted into `Sexp` by 73 | `try_from()`. It's as simple as. 74 | 75 | ```rust 76 | /// @export 77 | #[savvy] 78 | fn scalar_output_int() -> savvy::Result { 79 | 1.try_into() 80 | } 81 | ``` 82 | 83 | Alternatively, the same conversion is available in the form of 84 | `Owned{type}Sexp::try_from_scalar()`. 85 | 86 | ```rust 87 | /// @export 88 | #[savvy] 89 | fn scalar_output_int() -> savvy::Result { 90 | let out = OwnedIntegerSexp::try_from_scalar(1)?; 91 | out.into() 92 | } 93 | ``` 94 | 95 | ## Missing values 96 | 97 | If the type of the input is scalar, `NA` is always rejected. This is 98 | inconsistent with the rule for vector input, but, this is my design decision in 99 | the assumption that a scalar missing value is rarely found useful on Rust's 100 | side. 101 | 102 | ```rust 103 | /// @export 104 | #[savvy] 105 | fn identity_logical_single(x: bool) -> savvy::Result { 106 | let mut out = OwnedLogicalSexp::new(1)?; 107 | out.set_elt(0, x)?; 108 | out.into() 109 | } 110 | ``` 111 | 112 | ```r 113 | identity_logical_single(NA) 114 | #> Error in identity_logical_single(NA) : 115 | #> Must be length 1 of non-missing value 116 | ``` 117 | 118 | If you want to accept `NA`, the primary recommendation is to handle it in R 119 | code. But, you can also use `Sexp` as input. You can detect a missing value 120 | by `is_scalar_na()` and then convert it to a specific type by `try_into()`. 121 | 122 | ```rust 123 | /// @export 124 | #[savvy] 125 | fn times_two_numeric_i32_scalar_v2(x: savvy::Sexp) -> savvy::Result { 126 | if x.is_scalar_na() { 127 | return (i32::na()).try_into(); 128 | } 129 | 130 | let x_num: NumericScalar = x.try_into()?; 131 | let v = x_num.as_i32()?; 132 | 133 | // Note: NA check is already done, so you don't need to check v.is_na() 134 | 135 | (v * 2).try_into() 136 | } 137 | ``` 138 | -------------------------------------------------------------------------------- /book/src/type-overview.md: -------------------------------------------------------------------------------- 1 | # Type-specific Topics 2 | 3 | You can use these types as an argument of a `#[savvy]` function. 4 | 5 | | R type | vector | scalar | 6 | |:------------------|:----------------|:------------| 7 | | integer | `IntegerSexp` | `i32` | 8 | | double | `RealSexp` | `f64` | 9 | | integer or double | `NumericSexp` | `NumericScalar` | 10 | | logical | `LogicalSexp` | `bool` | 11 | | raw | `RawSexp` | `u8` | 12 | | character | `StringSexp` | `&str` | 13 | | complex[^1] | `ComplexSexp` | `Complex64` | 14 | | list | `ListSexp` | n/a | 15 | | (any) | `Sexp` | n/a | 16 | 17 | [^1]: Complex is optionally supported under feature flag `complex` 18 | 19 | If you want to handle multiple types, you can cast an `Sexp` into a specific 20 | type by `.into_typed()` and write `match` branches to deal with each type. This 21 | is important when the interface returns `Sexp`. For example, `ListSexp` returns 22 | `Sexp` because the list element can be any type. For more details about `List`, 23 | please read [List](./list.md) section. 24 | 25 | ```rust 26 | #[savvy] 27 | fn print_list(x: ListSexp) -> savvy::Result<()> { 28 | for (k, v) in x.iter() { 29 | let content = match v.into_typed() { 30 | TypedSexp::Integer(x) => { 31 | format!( 32 | "integer [{}]", 33 | x.iter().map(|i| i.to_string()).collect::>().join(", ") 34 | ) 35 | } 36 | TypedSexp::Real(x) => { 37 | format!( 38 | "double [{}]", 39 | x.iter().map(|r| r.to_string()).collect::>().join(", ") 40 | ) 41 | } 42 | TypedSexp::Logical(x) => { 43 | format!( 44 | "logical [{}]", 45 | x.iter().map(|l| if l { "TRUE" } else { "FALSE" }).collect::>().join(", ") 46 | ) 47 | } 48 | TypedSexp::String(x) => { 49 | format!( 50 | "character [{}]", 51 | x.iter().collect::>().join(", ") 52 | ) 53 | } 54 | TypedSexp::List(_) => "list".to_string(), 55 | TypedSexp::Null(_) => "NULL".to_string(), 56 | _ => "other".to_string(), 57 | }; 58 | 59 | let name = if k.is_empty() { "(no name)" } else { k }; 60 | 61 | r_print!("{name}: {content}\n"); 62 | } 63 | 64 | Ok(()) 65 | } 66 | ``` 67 | 68 | Likewise, `NumericSxep` also provides `into_typed()`. You can match it with 69 | either `IntegerSexp` or `RealSexp` and apply an appropriate function. 70 | Alternatively, you can rely on the type conversion that `NumericSexp` provides. 71 | See more details in [the next section](./atomic_types.md). 72 | 73 | ```rust 74 | #[savvy] 75 | fn identity_num(x: NumericSexp) -> savvy::Result { 76 | match x.into_typed() { 77 | NumericTypedSexp::Integer(i) => identity_int(i), 78 | NumericTypedSexp::Real(r) => identity_real(r), 79 | } 80 | } 81 | ``` 82 | 83 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | fn main() { 4 | let r_include_dir = std::env::var("R_INCLUDE_DIR"); 5 | 6 | // TODO: to pass the build of cargo-dist, this must be built without any errors. 7 | if let Ok(d) = r_include_dir { 8 | cc::Build::new() 9 | .file("src/unwind_protect_wrapper.c") 10 | .include(Path::new(d.as_str())) 11 | .compile("unwind_protect"); 12 | } else { 13 | println!("cargo:warning=R_INCLUDE_DIR envvar should be provided."); 14 | } 15 | 16 | println!("cargo:rerun-if-changed=src/unwind_protect_wrapper.c"); 17 | println!("cargo:rerun-if-env-changed=R_INCLUDE_DIR"); 18 | } 19 | -------------------------------------------------------------------------------- /dist-workspace.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["cargo:."] 3 | 4 | # Config for 'dist' 5 | [dist] 6 | # The preferred dist version to use in CI (Cargo.toml SemVer syntax) 7 | cargo-dist-version = "0.30.0" 8 | # CI backends to support 9 | ci = "github" 10 | # The installers to generate for each app 11 | installers = ["shell", "powershell"] 12 | # Target platforms to build apps for (Rust target-triple syntax) 13 | targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "aarch64-pc-windows-msvc", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] 14 | # Which actions to run on pull requests 15 | pr-run-mode = "plan" 16 | # Path that installers should place binaries in 17 | install-path = "CARGO_HOME" 18 | # Whether to install an updater program 19 | install-updater = false 20 | -------------------------------------------------------------------------------- /savvy-bindgen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "savvy-bindgen" 3 | description = "Parse Rust functions, and generate C and R code" 4 | version.workspace = true 5 | edition.workspace = true 6 | authors.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | readme = "README.md" 10 | 11 | [dependencies] 12 | proc-macro2 = "1" 13 | quote = "1" 14 | syn = { version = "2", features = ["full", "extra-traits"] } 15 | prettyplease = { version = "0.2", optional = true } 16 | 17 | [features] 18 | default = [] 19 | use_formatter = ["prettyplease"] 20 | 21 | [package.metadata.dist] 22 | dist = false 23 | -------------------------------------------------------------------------------- /savvy-bindgen/README.md: -------------------------------------------------------------------------------- 1 | # savvy-bindgen 2 | 3 | Parse Rust functions, and generate C and R code. 4 | 5 | For the full details, please read [savvy's crate 6 | documentation](https://docs.rs/savvy/latest/). 7 | 8 | ``` rust 9 | /// Convert to Upper-case 10 | /// 11 | /// @param x A character vector. 12 | /// @export 13 | #[savvy] 14 | fn to_upper(x: StringSexp) -> savvy::Result { 15 | // Use `Owned{type}Sexp` to allocate an R vector for output. 16 | let mut out = OwnedStringSexp::new(x.len())?; 17 | 18 | for (i, e) in x.iter().enumerate() { 19 | // To Rust, missing value is an ordinary value. In `&str`'s case, it's just "NA". 20 | // You have to use `.is_na()` method to distinguish the missing value. 21 | if e.is_na() { 22 | // Set the i-th element to NA 23 | out.set_na(i)?; 24 | continue; 25 | } 26 | 27 | let e_upper = e.to_uppercase(); 28 | out.set_elt(i, e_upper.as_str())?; 29 | } 30 | 31 | out.into() 32 | } 33 | ``` -------------------------------------------------------------------------------- /savvy-bindgen/src/gen/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod c; 2 | pub mod r; 3 | pub mod rust; 4 | pub mod static_files; 5 | -------------------------------------------------------------------------------- /savvy-bindgen/src/gen/static_files.rs: -------------------------------------------------------------------------------- 1 | pub fn generate_makevars_in(crate_name: &str) -> String { 2 | format!( 3 | include_str!("./templates/Makevars.in"), 4 | crate_name, crate_name 5 | ) 6 | } 7 | 8 | pub fn generate_configure() -> String { 9 | include_str!("./templates/configure").to_string() 10 | } 11 | 12 | pub fn generate_cleanup() -> String { 13 | include_str!("./templates/cleanup").to_string() 14 | } 15 | 16 | pub fn generate_makevars_win_in(crate_name: &str) -> String { 17 | format!( 18 | include_str!("./templates/Makevars.win.in"), 19 | crate_name, crate_name 20 | ) 21 | } 22 | 23 | pub fn generate_configure_win() -> String { 24 | include_str!("./templates/configure.win").to_string() 25 | } 26 | 27 | pub fn generate_cleanup_win() -> String { 28 | include_str!("./templates/cleanup.win").to_string() 29 | } 30 | 31 | pub fn generate_win_def(crate_name: &str) -> String { 32 | format!(include_str!("./templates/dllname-win.def"), crate_name) 33 | } 34 | 35 | pub fn generate_gitignore() -> String { 36 | include_str!("./templates/gitignore").to_string() 37 | } 38 | 39 | pub fn generate_cargo_toml(crate_name: &str, dependencies: &str) -> String { 40 | format!( 41 | include_str!("./templates/Cargo_toml"), 42 | crate_name, dependencies 43 | ) 44 | } 45 | 46 | pub fn generate_config_toml() -> String { 47 | include_str!("./templates/config_toml").to_string() 48 | } 49 | 50 | pub fn generate_example_lib_rs() -> String { 51 | include_str!("./templates/lib_rs").to_string() 52 | } 53 | -------------------------------------------------------------------------------- /savvy-bindgen/src/gen/templates/Cargo_toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "{}" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["staticlib", "lib"] 8 | 9 | {} 10 | 11 | [profile.release] 12 | # By default, on release build, savvy terminates the R session when a panic 13 | # occurs. This is the right behavior in that a panic means such a fatal event 14 | # where we can have no hope of recovery. 15 | # 16 | # cf. https://doc.rust-lang.org/book/ch09-03-to-panic-or-not-to-panic.html 17 | # 18 | # However, it's possible that the panic is thrown by some of the dependency 19 | # crate and there's little you can do. In such cases, you can change the 20 | # following line to `panic = "unwind"` to always catch a panic. 21 | panic = "abort" 22 | -------------------------------------------------------------------------------- /savvy-bindgen/src/gen/templates/Makevars.in: -------------------------------------------------------------------------------- 1 | TARGET = @TARGET@ 2 | 3 | PROFILE = @PROFILE@ 4 | FEATURE_FLAGS = @FEATURE_FLAGS@ 5 | 6 | # Add flags if necessary 7 | RUSTFLAGS = 8 | 9 | TARGET_DIR = $(CURDIR)/rust/target 10 | LIBDIR = $(TARGET_DIR)/$(TARGET)/$(subst dev,debug,$(PROFILE)) 11 | STATLIB = $(LIBDIR)/lib{}.a 12 | PKG_LIBS = -L$(LIBDIR) -l{} 13 | 14 | CARGO_BUILD_ARGS = --lib --profile $(PROFILE) $(FEATURE_FLAGS) --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) 15 | 16 | all: $(SHLIB) clean_intermediate 17 | 18 | $(SHLIB): $(STATLIB) 19 | 20 | $(STATLIB): 21 | # In some environments, ~/.cargo/bin might not be included in PATH, so we need 22 | # to set it here to ensure cargo can be invoked. It is appended to PATH and 23 | # therefore is only used if cargo is absent from the user's PATH. 24 | export PATH="$(PATH):$(HOME)/.cargo/bin" && \ 25 | export CC="$(CC)" && \ 26 | export CFLAGS="$(CFLAGS)" && \ 27 | export RUSTFLAGS="$(RUSTFLAGS)" && \ 28 | if [ "$(TARGET)" != "wasm32-unknown-emscripten" ]; then \ 29 | cargo build $(CARGO_BUILD_ARGS); \ 30 | else \ 31 | export CARGO_PROFILE_DEV_PANIC="abort" && \ 32 | export CARGO_PROFILE_RELEASE_PANIC="abort" && \ 33 | export RUSTFLAGS="$(RUSTFLAGS) -Zdefault-visibility=hidden" && \ 34 | cargo +nightly build $(CARGO_BUILD_ARGS) --target $(TARGET) -Zbuild-std=panic_abort,std; \ 35 | fi 36 | 37 | clean_intermediate: $(SHLIB) 38 | rm -f $(STATLIB) 39 | 40 | clean: 41 | rm -Rf $(SHLIB) $(OBJECTS) $(STATLIB) ./rust/target 42 | 43 | .PHONY: all clean_intermediate clean 44 | -------------------------------------------------------------------------------- /savvy-bindgen/src/gen/templates/Makevars.win.in: -------------------------------------------------------------------------------- 1 | TARGET = @TARGET@ 2 | 3 | PROFILE = @PROFILE@ 4 | FEATURE_FLAGS = @FEATURE_FLAGS@ 5 | 6 | # Add flags if necessary 7 | RUSTFLAGS = 8 | 9 | TARGET_DIR = $(CURDIR)/rust/target 10 | LIBDIR = $(TARGET_DIR)/$(TARGET)/$(subst dev,debug,$(PROFILE)) 11 | STATLIB = $(LIBDIR)/lib{}.a 12 | PKG_LIBS = -L$(LIBDIR) -l{} -lws2_32 -ladvapi32 -luserenv -lbcrypt -lntdll 13 | 14 | # Rtools doesn't have the linker in the location that cargo expects, so we need 15 | # to overwrite it via configuration. 16 | CARGO_LINKER = x86_64-w64-mingw32.static.posix-gcc.exe 17 | 18 | all: $(SHLIB) clean_intermediate 19 | 20 | $(SHLIB): $(STATLIB) 21 | 22 | $(STATLIB): 23 | # When the GNU toolchain is used (i.e. on CRAN), -lgcc_eh is specified for 24 | # building proc-macro2, but Rtools doesn't contain libgcc_eh. This isn't used 25 | # in actual, but we need this tweak to please the compiler. 26 | mkdir -p $(LIBDIR)/libgcc_mock && touch $(LIBDIR)/libgcc_mock/libgcc_eh.a 27 | 28 | export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER="$(CARGO_LINKER)" && \ 29 | export LIBRARY_PATH="$${{LIBRARY_PATH}};$(LIBDIR)/libgcc_mock" && \ 30 | export CC="$(CC)" && \ 31 | export CFLAGS="$(CFLAGS)" && \ 32 | export RUSTFLAGS="$(RUSTFLAGS)" && \ 33 | cargo build --target $(TARGET) --lib --profile $(PROFILE) $(FEATURE_FLAGS) --manifest-path ./rust/Cargo.toml --target-dir $(TARGET_DIR) 34 | 35 | clean_intermediate: $(SHLIB) 36 | rm -f $(STATLIB) 37 | 38 | clean: 39 | rm -Rf $(SHLIB) $(OBJECTS) $(STATLIB) ./rust/target 40 | 41 | .PHONY: all clean_intermediate clean 42 | -------------------------------------------------------------------------------- /savvy-bindgen/src/gen/templates/cleanup: -------------------------------------------------------------------------------- 1 | rm -f src/Makevars 2 | -------------------------------------------------------------------------------- /savvy-bindgen/src/gen/templates/cleanup.win: -------------------------------------------------------------------------------- 1 | rm -f src/Makevars.win 2 | -------------------------------------------------------------------------------- /savvy-bindgen/src/gen/templates/config_toml: -------------------------------------------------------------------------------- 1 | # On Windows, link.exe fails when the artifact contains unresolved symbols 2 | # (i.e., R's API, which cannot be used without a real R session). This option 3 | # makes the linker ignore these problems. 4 | # 5 | # This setting is needed only when you run `cargo test`, not when `R CMD check` 6 | # etc. The `.cargo` directory need to be excluded on building the package (i.e. 7 | # add `^src/rust/\.cargo$` to `.Rbuildignore`) because otherwise you'll get the 8 | # "hidden files and directories" NOTE. 9 | [target.x86_64-pc-windows-msvc] 10 | rustflags = ["-C", "link-arg=/FORCE:UNRESOLVED"] 11 | -------------------------------------------------------------------------------- /savvy-bindgen/src/gen/templates/configure: -------------------------------------------------------------------------------- 1 | # Even when `cargo` is on `PATH`, `rustc` might not in some cases. This adds 2 | # ~/.cargo/bin to PATH to address such cases. Note that is not always available 3 | # (e.g. or on Ubuntu with Rust installed via APT). 4 | if [ -d "${HOME}/.cargo/bin" ]; then 5 | export PATH="${PATH}:${HOME}/.cargo/bin" 6 | fi 7 | 8 | CARGO_VERSION="$(cargo --version)" 9 | 10 | if [ $? -ne 0 ]; then 11 | echo "-------------- ERROR: CONFIGURATION FAILED --------------------" 12 | echo "" 13 | echo "The cargo command is not available. To install Rust, please refer" 14 | echo "to the official instruction:" 15 | echo "" 16 | echo "https://www.rust-lang.org/tools/install" 17 | echo "" 18 | echo "---------------------------------------------------------------" 19 | 20 | exit 1 21 | fi 22 | 23 | # There's a little chance that rustc is not available on PATH while cargo is. 24 | # So, just ignore the error case. 25 | RUSTC_VERSION="$(rustc --version || true)" 26 | 27 | # Report the version of Rustc to comply with the CRAN policy 28 | echo "using Rust package manager: '${CARGO_VERSION}'" 29 | echo "using Rust compiler: '${RUSTC_VERSION}'" 30 | 31 | if [ "$(uname)" = "Emscripten" ]; then 32 | TARGET="wasm32-unknown-emscripten" 33 | fi 34 | 35 | # allow overriding profile externally (e.g. on CI) 36 | if [ -n "${SAVVY_PROFILE}" ]; then 37 | PROFILE="${SAVVY_PROFILE}" 38 | # catch DEBUG envvar, which is passed from pkgbuild::compile_dll() 39 | elif [ "${DEBUG}" = "true" ]; then 40 | PROFILE=dev 41 | else 42 | PROFILE=release 43 | fi 44 | 45 | # e.g. SAVVY_FEATURES="a b" --> "--features 'a b'" 46 | if [ -n "${SAVVY_FEATURES}" ]; then 47 | FEATURE_FLAGS="--features '${SAVVY_FEATURES}'" 48 | fi 49 | 50 | sed \ 51 | -e "s/@TARGET@/${TARGET}/" \ 52 | -e "s/@PROFILE@/${PROFILE}/" \ 53 | -e "s/@FEATURE_FLAGS@/${FEATURE_FLAGS}/" \ 54 | src/Makevars.in > src/Makevars 55 | -------------------------------------------------------------------------------- /savvy-bindgen/src/gen/templates/configure.win: -------------------------------------------------------------------------------- 1 | CARGO_VERSION="$(cargo --version)" 2 | 3 | if [ $? -ne 0 ]; then 4 | echo "-------------- ERROR: CONFIGURATION FAILED --------------------" 5 | echo "" 6 | echo "The cargo command is not available. To install Rust, please refer" 7 | echo "to the official instruction:" 8 | echo "" 9 | echo "https://www.rust-lang.org/tools/install" 10 | echo "" 11 | echo "---------------------------------------------------------------" 12 | 13 | exit 1 14 | fi 15 | 16 | # There's a little chance that rustc is not available on PATH while cargo is. 17 | # So, just ignore the error case. 18 | RUSTC_VERSION="$(rustc --version || true)" 19 | 20 | # Report the version of Rustc to comply with the CRAN policy 21 | echo "using Rust package manager: '${CARGO_VERSION}'" 22 | echo "using Rust compiler: '${RUSTC_VERSION}'" 23 | 24 | # allow overriding profile externally (e.g. on CI) 25 | if [ -n "${SAVVY_PROFILE}" ]; then 26 | PROFILE="${SAVVY_PROFILE}" 27 | # catch DEBUG envvar, which is passed from pkgbuild::compile_dll() 28 | elif [ "${DEBUG}" = "true" ]; then 29 | PROFILE=dev 30 | else 31 | PROFILE=release 32 | fi 33 | 34 | # e.g. SAVVY_FEATURES="a b" --> "--features 'a b'" 35 | if [ -n "${SAVVY_FEATURES}" ]; then 36 | FEATURE_FLAGS="--features '${SAVVY_FEATURES}'" 37 | fi 38 | 39 | sed \ 40 | -e "s/@TARGET@/x86_64-pc-windows-gnu/" \ 41 | -e "s/@PROFILE@/${PROFILE}/" \ 42 | -e "s/@FEATURE_FLAGS@/${FEATURE_FLAGS}/" \ 43 | src/Makevars.win.in > src/Makevars.win 44 | -------------------------------------------------------------------------------- /savvy-bindgen/src/gen/templates/dllname-win.def: -------------------------------------------------------------------------------- 1 | EXPORTS 2 | R_init_{} 3 | -------------------------------------------------------------------------------- /savvy-bindgen/src/gen/templates/gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.so 3 | *.dll 4 | target 5 | 6 | Makevars 7 | Makevars.win 8 | -------------------------------------------------------------------------------- /savvy-bindgen/src/gen/templates/lib_rs: -------------------------------------------------------------------------------- 1 | // Example functions 2 | 3 | use savvy::savvy; 4 | 5 | use savvy::{IntegerSexp, OwnedIntegerSexp, OwnedStringSexp, StringSexp}; 6 | 7 | use savvy::NotAvailableValue; 8 | 9 | /// Convert Input To Upper-Case 10 | /// 11 | /// @param x A character vector. 12 | /// @returns A character vector with upper case version of the input. 13 | /// @export 14 | #[savvy] 15 | fn to_upper(x: StringSexp) -> savvy::Result { 16 | let mut out = OwnedStringSexp::new(x.len())?; 17 | 18 | for (i, e) in x.iter().enumerate() { 19 | if e.is_na() { 20 | out.set_na(i)?; 21 | continue; 22 | } 23 | 24 | let e_upper = e.to_uppercase(); 25 | out.set_elt(i, &e_upper)?; 26 | } 27 | 28 | Ok(out.into()) 29 | } 30 | 31 | /// Multiply Input By Another Input 32 | /// 33 | /// @param x An integer vector. 34 | /// @param y An integer to multiply. 35 | /// @returns An integer vector with values multiplied by `y`. 36 | /// @export 37 | #[savvy] 38 | fn int_times_int(x: IntegerSexp, y: i32) -> savvy::Result { 39 | let mut out = OwnedIntegerSexp::new(x.len())?; 40 | 41 | for (i, e) in x.iter().enumerate() { 42 | if e.is_na() { 43 | out.set_na(i)?; 44 | } else { 45 | out[i] = e * y; 46 | } 47 | } 48 | 49 | Ok(out.into()) 50 | } 51 | 52 | #[savvy] 53 | struct Person { 54 | pub name: String, 55 | } 56 | 57 | /// A person with a name 58 | /// 59 | /// @export 60 | #[savvy] 61 | impl Person { 62 | fn new() -> Self { 63 | Self { 64 | name: "".to_string(), 65 | } 66 | } 67 | 68 | fn set_name(&mut self, name: &str) -> savvy::Result<()> { 69 | self.name = name.to_string(); 70 | Ok(()) 71 | } 72 | 73 | fn name(&self) -> savvy::Result { 74 | let mut out = OwnedStringSexp::new(1)?; 75 | out.set_elt(0, &self.name)?; 76 | Ok(out.into()) 77 | } 78 | 79 | fn associated_function() -> savvy::Result { 80 | let mut out = OwnedStringSexp::new(1)?; 81 | out.set_elt(0, "associated_function")?; 82 | Ok(out.into()) 83 | } 84 | } 85 | 86 | // This test is run by `cargo test`. You can put tests that don't need a real 87 | // R session here. 88 | #[cfg(test)] 89 | mod test1 { 90 | #[test] 91 | fn test_person() { 92 | let mut p = super::Person::new(); 93 | p.set_name("foo").expect("set_name() must succeed"); 94 | assert_eq!(&p.name, "foo"); 95 | } 96 | } 97 | 98 | // Tests marked under `#[cfg(feature = "savvy-test")]` are run by `savvy-cli test`, which 99 | // executes the Rust code on a real R session so that you can use R things for 100 | // testing. 101 | #[cfg(feature = "savvy-test")] 102 | mod test1 { 103 | // The return type must be `savvy::Result<()>` 104 | #[test] 105 | fn test_to_upper() -> savvy::Result<()> { 106 | // You can create a non-owned version of input by `.as_read_only()` 107 | let x = savvy::OwnedStringSexp::try_from_slice(["foo", "bar"])?.as_read_only(); 108 | 109 | let result = super::to_upper(x)?; 110 | 111 | // This function compares an SEXP with the result of R code specified in 112 | // the second argument. 113 | savvy::assert_eq_r_code(result, r#"c("FOO", "BAR")"#); 114 | 115 | Ok(()) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /savvy-bindgen/src/ir/savvy_enum.rs: -------------------------------------------------------------------------------- 1 | use syn::{parse_quote, ItemEnum, ItemImpl}; 2 | 3 | use crate::extract_docs; 4 | 5 | #[derive(Clone)] 6 | pub struct SavvyEnum { 7 | /// Doc comments 8 | pub docs: Vec, 9 | /// Attributes except for `#[savvy]` 10 | pub attrs: Vec, 11 | /// Type name of the enum 12 | pub ty: syn::Ident, 13 | /// Variants 14 | pub variants: Vec, 15 | } 16 | 17 | impl SavvyEnum { 18 | pub fn new(orig: &syn::ItemEnum) -> syn::Result { 19 | let mut attrs = orig.attrs.clone(); 20 | // Remove #[savvy] 21 | attrs.retain(|attr| attr != &parse_quote!(#[savvy])); 22 | // Extract doc comments 23 | let docs = extract_docs(attrs.as_slice()); 24 | 25 | let ty = orig.ident.clone(); 26 | 27 | let mut variants = Vec::new(); 28 | 29 | for v in &orig.variants { 30 | if !matches!(v.fields, syn::Fields::Unit) { 31 | let e = syn::Error::new_spanned( 32 | v.fields.clone(), 33 | "savvy only supports a fieldless enum", 34 | ); 35 | return Err(e); 36 | } 37 | 38 | if v.discriminant.is_some() { 39 | let e = syn::Error::new_spanned( 40 | v.discriminant.clone().unwrap().1, 41 | "savvy doesn't support an enum with discreminant", 42 | ); 43 | return Err(e); 44 | } 45 | 46 | variants.push(v.ident.clone()); 47 | } 48 | 49 | Ok(Self { 50 | docs, 51 | attrs, 52 | ty, 53 | variants, 54 | }) 55 | } 56 | 57 | pub fn generate_enum_with_discriminant(&self) -> ItemEnum { 58 | let ty = &self.ty; 59 | let attrs = &self.attrs; 60 | 61 | let variants_tweaked = self 62 | .variants 63 | .iter() 64 | .enumerate() 65 | .map(|(i, v)| { 66 | let lit_i = syn::LitInt::new(&i.to_string(), v.span()); 67 | parse_quote!(#v = #lit_i) 68 | }) 69 | .collect::>(); 70 | 71 | parse_quote!( 72 | #(#attrs)* 73 | pub enum #ty { 74 | #(#variants_tweaked),* 75 | } 76 | ) 77 | } 78 | 79 | pub fn generate_try_from_impls(&self) -> Vec { 80 | let ty = &self.ty; 81 | 82 | let match_arms_ref = self 83 | .variants 84 | .iter() 85 | .enumerate() 86 | .map(|(i, v)| { 87 | let lit_i = syn::LitInt::new(&i.to_string(), v.span()); 88 | parse_quote!(#lit_i => Ok(&#ty::#v)) 89 | }) 90 | .collect::>(); 91 | 92 | let match_arms = self 93 | .variants 94 | .iter() 95 | .enumerate() 96 | .map(|(i, v)| { 97 | let lit_i = syn::LitInt::new(&i.to_string(), v.span()); 98 | parse_quote!(#lit_i => Ok(#ty::#v)) 99 | }) 100 | .collect::>(); 101 | 102 | vec![ 103 | parse_quote!( 104 | impl TryFrom<#ty> for savvy::Sexp { 105 | type Error = savvy::Error; 106 | 107 | fn try_from(value: #ty) -> savvy::Result { 108 | (value as i32).try_into() 109 | } 110 | }), 111 | parse_quote!( 112 | impl TryFrom for &#ty { 113 | type Error = savvy::Error; 114 | 115 | fn try_from(value: savvy::Sexp) -> savvy::Result { 116 | let i = ::try_from(value)?; 117 | match i { 118 | #(#match_arms_ref),*, 119 | _ => Err(savvy::savvy_err!("Unexpected enum variant")), 120 | } 121 | } 122 | } 123 | ), 124 | parse_quote!( 125 | impl TryFrom for #ty { 126 | type Error = savvy::Error; 127 | 128 | fn try_from(value: savvy::Sexp) -> savvy::Result { 129 | let i = ::try_from(value)?; 130 | match i { 131 | #(#match_arms),*, 132 | _ => Err(savvy::savvy_err!("Unexpected enum variant")), 133 | } 134 | } 135 | } 136 | ), 137 | ] 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /savvy-bindgen/src/ir/savvy_impl.rs: -------------------------------------------------------------------------------- 1 | use syn::parse_quote; 2 | 3 | use super::savvy_fn::{SavvyFn, SavvyFnType}; 4 | use crate::utils::extract_docs; 5 | 6 | pub struct SavvyImpl { 7 | /// Doc comments 8 | pub docs: Vec, 9 | /// Attributes except for `#[savvy]` 10 | pub attrs: Vec, 11 | /// Original type name 12 | pub ty: syn::Ident, 13 | /// Methods and accociated functions 14 | pub fns: Vec, 15 | } 16 | 17 | impl SavvyImpl { 18 | pub fn new(orig: &syn::ItemImpl) -> syn::Result { 19 | let mut attrs = orig.attrs.clone(); 20 | // Remove #[savvy] 21 | attrs.retain(|attr| attr != &parse_quote!(#[savvy])); 22 | // Extract doc comments 23 | let docs = extract_docs(attrs.as_slice()); 24 | let self_ty = orig.self_ty.as_ref(); 25 | 26 | let ty = match self_ty { 27 | syn::Type::Path(type_path) => type_path.path.segments.last().unwrap().ident.clone(), 28 | _ => { 29 | return Err(syn::Error::new_spanned(self_ty, "Unexpected type")); 30 | } 31 | }; 32 | 33 | let fns = orig 34 | .items 35 | .clone() 36 | .iter() 37 | .filter_map(|f| match f { 38 | syn::ImplItem::Fn(impl_item_fn) => { 39 | let ty = self_ty.clone(); 40 | let fn_type = match impl_item_fn.sig.inputs.first() { 41 | Some(syn::FnArg::Receiver(syn::Receiver { 42 | reference, 43 | mutability, 44 | .. 45 | })) => SavvyFnType::Method { 46 | ty, 47 | reference: reference.is_some(), 48 | mutability: mutability.is_some(), 49 | }, 50 | _ => SavvyFnType::AssociatedFunction(ty), 51 | }; 52 | 53 | Some(SavvyFn::from_impl_fn(impl_item_fn, fn_type, self_ty)) 54 | } 55 | _ => None, 56 | }) 57 | .collect::>>()?; 58 | 59 | Ok(Self { 60 | docs, 61 | attrs, 62 | ty, 63 | fns, 64 | }) 65 | } 66 | 67 | #[allow(dead_code)] 68 | pub fn generate_inner_fns(&self) -> Vec { 69 | self.fns.iter().map(|f| f.generate_inner_fn()).collect() 70 | } 71 | 72 | #[allow(dead_code)] 73 | pub fn generate_ffi_fns(&self) -> Vec { 74 | self.fns.iter().map(|f| f.generate_ffi_fn()).collect() 75 | } 76 | } 77 | 78 | #[cfg(test)] 79 | mod tests { 80 | use super::SavvyFnType::*; 81 | use super::*; 82 | use syn::parse_quote; 83 | 84 | #[test] 85 | fn test_impl() { 86 | let item_impl: syn::ItemImpl = parse_quote!( 87 | #[savvy] 88 | impl Person { 89 | fn new() -> Self { 90 | Self { 91 | name: "".to_string(), 92 | } 93 | } 94 | 95 | fn set_name(&mut self, name: StringSexp) -> savvy::Result<()> { 96 | self.name = name.iter().next().unwrap().to_string(); 97 | Ok(()) 98 | } 99 | 100 | fn name(&self) -> savvy::Result { 101 | let mut out = OwnedStringSexp::new(1); 102 | out.set_elt(0, self.name.as_str()); 103 | Ok(out.into()) 104 | } 105 | 106 | fn do_nothing() -> savvy::Result<()> {} 107 | } 108 | ); 109 | 110 | let parsed = SavvyImpl::new(&item_impl).expect("Failed to parse"); 111 | assert_eq!(parsed.ty.to_string().as_str(), "Person"); 112 | 113 | assert_eq!(parsed.fns.len(), 4); 114 | 115 | assert_eq!(parsed.fns[0].fn_name.to_string().as_str(), "new"); 116 | assert!(matches!(parsed.fns[0].fn_type, AssociatedFunction(_))); 117 | 118 | assert_eq!(parsed.fns[1].fn_name.to_string().as_str(), "set_name"); 119 | assert!(matches!(parsed.fns[1].fn_type, Method { .. })); 120 | 121 | assert_eq!(parsed.fns[2].fn_name.to_string().as_str(), "name"); 122 | assert!(matches!(parsed.fns[2].fn_type, Method { .. })); 123 | 124 | assert_eq!(parsed.fns[3].fn_name.to_string().as_str(), "do_nothing"); 125 | assert!(matches!(parsed.fns[3].fn_type, AssociatedFunction(_))); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /savvy-bindgen/src/ir/savvy_struct.rs: -------------------------------------------------------------------------------- 1 | use syn::{parse_quote, spanned::Spanned}; 2 | 3 | use crate::extract_docs; 4 | 5 | pub struct SavvyStruct { 6 | /// Doc comments 7 | pub docs: Vec, 8 | /// Attributes except for `#[savvy]` 9 | pub attrs: Vec, 10 | /// Original struct name 11 | pub ty: syn::Ident, 12 | } 13 | 14 | impl SavvyStruct { 15 | pub fn new(orig: &syn::ItemStruct) -> syn::Result { 16 | if let Some(lt) = orig.generics.lifetimes().next() { 17 | return Err(syn::Error::new( 18 | lt.span(), 19 | "#[savvy] macro doesn't support lifetime", 20 | )); 21 | } 22 | 23 | let mut attrs = orig.attrs.clone(); 24 | // Remove #[savvy] 25 | attrs.retain(|attr| attr != &parse_quote!(#[savvy])); 26 | // Extract doc comments 27 | let docs = extract_docs(attrs.as_slice()); 28 | let ty = orig.ident.clone(); 29 | 30 | Ok(Self { docs, attrs, ty }) 31 | } 32 | 33 | pub fn generate_try_from_impls(&self) -> Vec { 34 | let ty = &self.ty; 35 | 36 | let impl_into_external_pointer: syn::ItemImpl = 37 | parse_quote!(impl savvy::IntoExtPtrSexp for #ty {}); 38 | 39 | let impl_try_from_ty_to_sexp: syn::ItemImpl = parse_quote!( 40 | impl TryFrom<#ty> for savvy::Sexp { 41 | type Error = savvy::Error; 42 | 43 | fn try_from(value: #ty) -> savvy::Result { 44 | use savvy::IntoExtPtrSexp; 45 | 46 | Ok(value.into_external_pointer()) 47 | } 48 | } 49 | ); 50 | 51 | let impl_try_from_sexp_to_ref_ty: syn::ItemImpl = parse_quote!( 52 | impl TryFrom for &#ty { 53 | type Error = savvy::Error; 54 | 55 | fn try_from(value: savvy::Sexp) -> savvy::Result { 56 | // Return error if the SEXP is not an external pointer 57 | value.assert_external_pointer()?; 58 | 59 | let x = unsafe { savvy::get_external_pointer_addr(value.0)? as *mut #ty }; 60 | let res = unsafe { x.as_ref() }; 61 | res.ok_or(savvy::savvy_err!("Failed to convert the external pointer to the Rust object")) 62 | } 63 | } 64 | ); 65 | 66 | let impl_try_from_sexp_to_ref_mut_ty: syn::ItemImpl = parse_quote!( 67 | impl TryFrom for &mut #ty { 68 | type Error = savvy::Error; 69 | 70 | fn try_from(value: savvy::Sexp) -> savvy::Result { 71 | // Return error if the SEXP is not an external pointer 72 | value.assert_external_pointer()?; 73 | 74 | let x = unsafe { savvy::get_external_pointer_addr(value.0)? as *mut #ty }; 75 | let res = unsafe { x.as_mut() }; 76 | res.ok_or(savvy::savvy_err!("Failed to convert the external pointer to the Rust object")) 77 | } 78 | } 79 | ); 80 | 81 | let impl_try_from_sexp_to_ty: syn::ItemImpl = parse_quote!( 82 | impl TryFrom for #ty { 83 | type Error = savvy::Error; 84 | 85 | fn try_from(value: savvy::Sexp) -> savvy::Result { 86 | // Return error if the SEXP is not an external pointer 87 | value.assert_external_pointer()?; 88 | 89 | unsafe { savvy::take_external_pointer_value::<#ty>(value.0) } 90 | } 91 | } 92 | ); 93 | 94 | vec![ 95 | impl_into_external_pointer, 96 | impl_try_from_ty_to_sexp, 97 | impl_try_from_sexp_to_ref_ty, 98 | impl_try_from_sexp_to_ref_mut_ty, 99 | impl_try_from_sexp_to_ty, 100 | ] 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /savvy-bindgen/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod gen; 2 | mod ir; 3 | mod parse_file; 4 | mod utils; 5 | 6 | pub use gen::c::{generate_c_header_file, generate_c_impl_file}; 7 | pub use gen::r::generate_r_impl_file; 8 | pub use gen::static_files::{ 9 | generate_cargo_toml, generate_cleanup, generate_cleanup_win, generate_config_toml, 10 | generate_configure, generate_configure_win, generate_example_lib_rs, generate_gitignore, 11 | generate_makevars_in, generate_makevars_win_in, generate_win_def, 12 | }; 13 | pub use ir::savvy_enum::SavvyEnum; 14 | pub use ir::savvy_fn::{SavvyFn, SavvyFnArg, SavvyFnType}; 15 | pub use ir::savvy_impl::SavvyImpl; 16 | pub use ir::savvy_struct::SavvyStruct; 17 | 18 | pub use ir::{merge_parsed_results, MergedResult, ParsedResult}; 19 | 20 | pub use utils::extract_docs; 21 | 22 | pub use parse_file::{generate_test_code, parse_file, read_file}; 23 | -------------------------------------------------------------------------------- /savvy-bindgen/src/utils.rs: -------------------------------------------------------------------------------- 1 | pub fn extract_docs(attrs: &[syn::Attribute]) -> Vec { 2 | attrs 3 | .iter() 4 | .filter_map(|attr| { 5 | match &attr.meta { 6 | syn::Meta::NameValue(nv) => { 7 | // Doc omments are transformed into the form of `#[doc = 8 | // r"comment"]` before macros are expanded. 9 | // cf., https://docs.rs/syn/latest/syn/struct.Attribute.html#doc-comments 10 | if nv.path.is_ident("doc") { 11 | match &nv.value { 12 | syn::Expr::Lit(syn::ExprLit { 13 | lit: syn::Lit::Str(doc), 14 | .. 15 | }) => Some(doc.value()), 16 | _ => None, 17 | } 18 | } else { 19 | None 20 | } 21 | } 22 | _ => None, 23 | } 24 | }) 25 | .collect() 26 | } 27 | 28 | pub(crate) fn add_indent(x: &str, indent: usize) -> String { 29 | x.lines() 30 | .map(|x| format!("{:indent$}{x}", "", indent = indent)) 31 | .collect::>() 32 | .join("\n") 33 | } 34 | -------------------------------------------------------------------------------- /savvy-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "savvy-cli" 3 | description = "A CLI for savvy framework" 4 | version.workspace = true 5 | edition.workspace = true 6 | authors.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | homepage.workspace = true 10 | readme = "README.md" 11 | 12 | # PanicHookInfo is introduced in 1.81 13 | rust-version = "1.81" 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | 17 | [dependencies] 18 | clap = { version = "4", features = ["derive"] } 19 | async-process = "2" 20 | futures-lite = "2" 21 | 22 | savvy-bindgen = { version = "0.8.14", path = "../savvy-bindgen", features = [ 23 | "use_formatter", 24 | ] } 25 | dirs = "6" 26 | toml = "0.9" 27 | 28 | [package.metadata.dist] 29 | dist = true 30 | -------------------------------------------------------------------------------- /savvy-cli/README.md: -------------------------------------------------------------------------------- 1 | # savvy-cli 2 | 3 | A helper CLI for savvy framework. For the full details, please read [savvy's crate 4 | documentation](https://docs.rs/savvy/latest/). 5 | 6 | ## Installation 7 | 8 | You can find the binary on [the GitHub releases 9 | page](https://github.com/yutannihilation/savvy/releases). If you prefer installing from source, please run cargo install. 10 | 11 | ``` shell 12 | cargo install savvy-cli 13 | ``` 14 | 15 | ## Usage 16 | 17 | ``` console 18 | Generate C bindings and R bindings for a Rust library 19 | 20 | Usage: savvy-cli 21 | 22 | Commands: 23 | update Update wrappers in an R package 24 | init Init savvy-powered Rust crate in an R package 25 | help Print this message or the help of the given subcommand(s) 26 | 27 | Options: 28 | -h, --help Print help 29 | ``` 30 | -------------------------------------------------------------------------------- /savvy-cli/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | pub(crate) fn to_snake_case(x: &str) -> String { 4 | let mut out = String::with_capacity(x.len() + 3); 5 | for c in x.chars() { 6 | if c.is_uppercase() { 7 | // first character doesn't need _ 8 | if !out.is_empty() { 9 | out.push('_'); 10 | } 11 | 12 | out.push_str(&c.to_lowercase().to_string()); 13 | } else { 14 | out.push(c) 15 | } 16 | } 17 | out 18 | } 19 | 20 | pub(crate) fn dot_containing_to_camel_case(x: &str) -> String { 21 | x.split('.') 22 | .enumerate() 23 | .map(|(i_split, split)| { 24 | if i_split == 0 { 25 | return split.to_string(); 26 | } 27 | split 28 | .chars() 29 | .enumerate() 30 | .map(|(i_char, c)| { 31 | if i_char == 0 { 32 | c.to_uppercase().next().unwrap() 33 | } else { 34 | c.to_lowercase().next().unwrap() 35 | } 36 | }) 37 | .collect() 38 | }) 39 | .collect::>() 40 | .join("") 41 | } 42 | 43 | pub(crate) fn canonicalize(path: &Path) -> Result { 44 | let crate_dir_abs = path.canonicalize()?; 45 | let crate_dir_abs = crate_dir_abs.to_string_lossy(); 46 | #[cfg(windows)] 47 | let crate_dir_abs = if crate_dir_abs.starts_with(r#"\\?\"#) { 48 | crate_dir_abs.get(4..).unwrap().replace('\\', "/") 49 | } else { 50 | crate_dir_abs.replace('\\', "/") 51 | }; 52 | Ok(crate_dir_abs.to_string()) 53 | } 54 | 55 | // Parse Cargo.toml and get the crate name in a dirty way 56 | 57 | #[cfg(test)] 58 | mod tests { 59 | use super::*; 60 | #[test] 61 | fn test_snake_case() { 62 | assert_eq!(&to_snake_case("foo"), "foo"); 63 | assert_eq!(&to_snake_case("Foo"), "foo"); 64 | assert_eq!(&to_snake_case("fooBar"), "foo_bar"); 65 | assert_eq!(&to_snake_case("FooBar"), "foo_bar"); 66 | assert_eq!(&to_snake_case("fooBarBaz"), "foo_bar_baz"); 67 | } 68 | 69 | #[test] 70 | fn test_camel_case() { 71 | assert_eq!(&dot_containing_to_camel_case("foo.bar"), "fooBar"); 72 | assert_eq!(&dot_containing_to_camel_case("foo.bar.baz"), "fooBarBaz"); 73 | assert_eq!(&dot_containing_to_camel_case("foo.BAR.baz"), "fooBarBaz"); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /savvy-ffi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "savvy-ffi" 3 | description = "Minimal FFI bindings for R's C API" 4 | version.workspace = true 5 | edition.workspace = true 6 | authors.workspace = true 7 | license.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | 11 | [package.metadata.dist] 12 | dist = false 13 | 14 | [dependencies] 15 | num-complex = { version = "0.4.5", optional = true } 16 | 17 | [features] 18 | default = [] 19 | complex = ["num-complex"] 20 | altrep = [] 21 | -------------------------------------------------------------------------------- /savvy-ffi/README.md: -------------------------------------------------------------------------------- 1 | # savvy-ffi 2 | 3 | Minimal FFI bindings for R's C API. This contains only a subset of APIs 4 | sufficient for savvy framework. If you are looking for more complete one, 5 | [libR-sys](https://crates.io/crates/libR-sys) is probably what you want. 6 | 7 | Some more notable differences between libR-sys are: 8 | 9 | * This is NOT a sys crate. Savvy-ffi is intended to be used within an R package, 10 | which compiles a staticlib from Rust code first and then links it to R. At the 11 | point of compilation by cargo, savvy-ffi is not yet linked, so this is fine. 12 | 13 | * All definitions are written by hand, with some help of bindgen, into a single 14 | file. There's no automatic version switch or platform switch. If some switch 15 | is needed, it will be provided as a feature (e.g. `r_4_4_0`) and it's user's 16 | responsibility to set it properly. 17 | -------------------------------------------------------------------------------- /savvy-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "savvy-macro" 3 | description = "Generate R-ready Rust functions by adding `#[savvy]` macro" 4 | version.workspace = true 5 | edition.workspace = true 6 | authors.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | homepage.workspace = true 10 | readme = "README.md" 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [dependencies] 16 | proc-macro2 = "1" 17 | quote = "1" 18 | syn = { version = "2", features = ["full", "extra-traits"] } 19 | 20 | savvy-bindgen = { version = "0.8.14", path = "../savvy-bindgen" } 21 | 22 | [dev-dependencies] 23 | trybuild = "1" 24 | prettyplease = "0.2" 25 | insta = { version = "1.38.0", features = ["yaml"] } 26 | 27 | [package.metadata.dist] 28 | dist = false 29 | -------------------------------------------------------------------------------- /savvy-macro/README.md: -------------------------------------------------------------------------------- 1 | # savvy-macro 2 | 3 | Generate R-ready Rust functions by adding `#[savvy]` macro. 4 | 5 | For the full details, please read [savvy's crate 6 | documentation](https://docs.rs/savvy/latest/). 7 | 8 | ``` rust 9 | /// Convert to Upper-case 10 | /// 11 | /// @param x A character vector. 12 | /// @export 13 | #[savvy] 14 | fn to_upper(x: StringSexp) -> savvy::Result { 15 | // Use `Owned{type}Sexp` to allocate an R vector for output. 16 | let mut out = OwnedStringSexp::new(x.len())?; 17 | 18 | for (i, e) in x.iter().enumerate() { 19 | // To Rust, missing value is an ordinary value. In `&str`'s case, it's just "NA". 20 | // You have to use `.is_na()` method to distinguish the missing value. 21 | if e.is_na() { 22 | // Set the i-th element to NA 23 | out.set_na(i)?; 24 | continue; 25 | } 26 | 27 | let e_upper = e.to_uppercase(); 28 | out.set_elt(i, e_upper.as_str())?; 29 | } 30 | 31 | out.into() 32 | } 33 | ``` -------------------------------------------------------------------------------- /savvy-macro/src/snapshots/savvy_macro__tests__assert_snapshot_ffi-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: savvy-macro/src/lib.rs 3 | expression: lines 4 | --- 5 | - "#[allow(clippy::missing_safety_doc)]" 6 | - "#[no_mangle]" 7 | - "pub unsafe extern \"C\" fn savvy_foo__ffi() -> savvy::ffi::SEXP {" 8 | - " match savvy_foo_inner() {" 9 | - " Ok(_) => savvy::sexp::null::null()," 10 | - " Err(e) => savvy::handle_error(e)," 11 | - " }" 12 | - "}" 13 | -------------------------------------------------------------------------------- /savvy-macro/src/snapshots/savvy_macro__tests__assert_snapshot_ffi-3.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: savvy-macro/src/lib.rs 3 | expression: lines 4 | --- 5 | - "#[allow(clippy::missing_safety_doc)]" 6 | - "#[no_mangle]" 7 | - "pub unsafe extern \"C\" fn savvy_foo__ffi(" 8 | - " x: savvy::ffi::SEXP," 9 | - " y: savvy::ffi::SEXP," 10 | - ") -> savvy::ffi::SEXP {" 11 | - " match savvy_foo_inner(x, y) {" 12 | - " Ok(result) => result.0," 13 | - " Err(e) => savvy::handle_error(e)," 14 | - " }" 15 | - "}" 16 | -------------------------------------------------------------------------------- /savvy-macro/src/snapshots/savvy_macro__tests__assert_snapshot_ffi.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: savvy-macro/src/lib.rs 3 | expression: lines 4 | --- 5 | - "#[allow(clippy::missing_safety_doc)]" 6 | - "#[no_mangle]" 7 | - "pub unsafe extern \"C\" fn savvy_foo__ffi() -> savvy::ffi::SEXP {" 8 | - " match savvy_foo_inner() {" 9 | - " Ok(result) => result.0," 10 | - " Err(e) => savvy::handle_error(e)," 11 | - " }" 12 | - "}" 13 | -------------------------------------------------------------------------------- /savvy-macro/src/snapshots/savvy_macro__tests__assert_snapshot_ffi_impl-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: savvy-macro/src/lib.rs 3 | expression: lines 4 | --- 5 | - "#[allow(clippy::missing_safety_doc)]" 6 | - "#[no_mangle]" 7 | - "pub unsafe extern \"C\" fn savvy_Person_new2__ffi() -> savvy::ffi::SEXP {" 8 | - " match savvy_Person_new2_inner() {" 9 | - " Ok(result) => {" 10 | - " match ::try_from(result) {" 11 | - " Ok(sexp) => sexp.0," 12 | - " Err(e) => savvy::handle_error(e)," 13 | - " }" 14 | - " }" 15 | - " Err(e) => savvy::handle_error(e)," 16 | - " }" 17 | - "}" 18 | -------------------------------------------------------------------------------- /savvy-macro/src/snapshots/savvy_macro__tests__assert_snapshot_ffi_impl-3.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: savvy-macro/src/lib.rs 3 | expression: lines 4 | --- 5 | - "#[allow(clippy::missing_safety_doc)]" 6 | - "#[no_mangle]" 7 | - "pub unsafe extern \"C\" fn savvy_Person_name__ffi(" 8 | - " self__: savvy::ffi::SEXP," 9 | - ") -> savvy::ffi::SEXP {" 10 | - " match savvy_Person_name_inner(self__) {" 11 | - " Ok(result) => result.0," 12 | - " Err(e) => savvy::handle_error(e)," 13 | - " }" 14 | - "}" 15 | -------------------------------------------------------------------------------- /savvy-macro/src/snapshots/savvy_macro__tests__assert_snapshot_ffi_impl-4.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: savvy-macro/src/lib.rs 3 | expression: lines 4 | --- 5 | - "#[allow(clippy::missing_safety_doc)]" 6 | - "#[no_mangle]" 7 | - "pub unsafe extern \"C\" fn savvy_Person_set_name__ffi(" 8 | - " self__: savvy::ffi::SEXP," 9 | - " name: savvy::ffi::SEXP," 10 | - ") -> savvy::ffi::SEXP {" 11 | - " match savvy_Person_set_name_inner(self__, name) {" 12 | - " Ok(_) => savvy::sexp::null::null()," 13 | - " Err(e) => savvy::handle_error(e)," 14 | - " }" 15 | - "}" 16 | -------------------------------------------------------------------------------- /savvy-macro/src/snapshots/savvy_macro__tests__assert_snapshot_ffi_impl.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: savvy-macro/src/lib.rs 3 | expression: lines 4 | --- 5 | - "#[allow(clippy::missing_safety_doc)]" 6 | - "#[no_mangle]" 7 | - "pub unsafe extern \"C\" fn savvy_Person_new__ffi() -> savvy::ffi::SEXP {" 8 | - " match savvy_Person_new_inner() {" 9 | - " Ok(result) => {" 10 | - " match ::try_from(result) {" 11 | - " Ok(sexp) => sexp.0," 12 | - " Err(e) => savvy::handle_error(e)," 13 | - " }" 14 | - " }" 15 | - " Err(e) => savvy::handle_error(e)," 16 | - " }" 17 | - "}" 18 | -------------------------------------------------------------------------------- /savvy-macro/src/snapshots/savvy_macro__tests__assert_snapshot_inner-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: savvy-macro/src/lib.rs 3 | expression: lines 4 | --- 5 | - "unsafe fn savvy_foo_inner() -> savvy::Result<()> {" 6 | - " let orig_hook = std::panic::take_hook();" 7 | - " std::panic::set_hook(Box::new(savvy::panic_hook::panic_hook));" 8 | - " let result = std::panic::catch_unwind(|| { foo() });" 9 | - " std::panic::set_hook(orig_hook);" 10 | - " match result {" 11 | - " Ok(orig_result) => orig_result," 12 | - " Err(_) => Err(savvy::savvy_err!(\"panic happened\"))," 13 | - " }" 14 | - "}" 15 | -------------------------------------------------------------------------------- /savvy-macro/src/snapshots/savvy_macro__tests__assert_snapshot_inner-3.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: savvy-macro/src/lib.rs 3 | expression: lines 4 | --- 5 | - unsafe fn savvy_foo_inner( 6 | - " x: savvy::ffi::SEXP," 7 | - " y: savvy::ffi::SEXP," 8 | - ") -> savvy::Result {" 9 | - " let orig_hook = std::panic::take_hook();" 10 | - " std::panic::set_hook(Box::new(savvy::panic_hook::panic_hook));" 11 | - " let result = std::panic::catch_unwind(|| {" 12 | - " let x = ::try_from(savvy::Sexp(x)).map_err(|e| e.with_arg_name(\"x\"))?;" 13 | - " let y = ::try_from(savvy::Sexp(y))" 14 | - " .map_err(|e| e.with_arg_name(\"y\"))?;" 15 | - " foo(x, y)" 16 | - " });" 17 | - " std::panic::set_hook(orig_hook);" 18 | - " match result {" 19 | - " Ok(orig_result) => orig_result," 20 | - " Err(_) => Err(savvy::savvy_err!(\"panic happened\"))," 21 | - " }" 22 | - "}" 23 | -------------------------------------------------------------------------------- /savvy-macro/src/snapshots/savvy_macro__tests__assert_snapshot_inner-4.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: savvy-macro/src/lib.rs 3 | expression: lines 4 | --- 5 | - "unsafe fn savvy_foo_inner(x: savvy::ffi::SEXP) -> savvy::Result {" 6 | - " let orig_hook = std::panic::take_hook();" 7 | - " std::panic::set_hook(Box::new(savvy::panic_hook::panic_hook));" 8 | - " let result = std::panic::catch_unwind(|| {" 9 | - " let x = ::try_from(savvy::Sexp(x)).map_err(|e| e.with_arg_name(\"x\"))?;" 10 | - " foo(x)" 11 | - " });" 12 | - " std::panic::set_hook(orig_hook);" 13 | - " match result {" 14 | - " Ok(orig_result) => orig_result," 15 | - " Err(_) => Err(savvy::savvy_err!(\"panic happened\"))," 16 | - " }" 17 | - "}" 18 | -------------------------------------------------------------------------------- /savvy-macro/src/snapshots/savvy_macro__tests__assert_snapshot_inner.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: savvy-macro/src/lib.rs 3 | expression: lines 4 | --- 5 | - "unsafe fn savvy_foo_inner() -> savvy::Result {" 6 | - " let orig_hook = std::panic::take_hook();" 7 | - " std::panic::set_hook(Box::new(savvy::panic_hook::panic_hook));" 8 | - " let result = std::panic::catch_unwind(|| { foo() });" 9 | - " std::panic::set_hook(orig_hook);" 10 | - " match result {" 11 | - " Ok(orig_result) => orig_result," 12 | - " Err(_) => Err(savvy::savvy_err!(\"panic happened\"))," 13 | - " }" 14 | - "}" 15 | -------------------------------------------------------------------------------- /savvy-macro/tests/cases/simple_cases.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables)] 2 | 3 | use savvy_macro::savvy; 4 | use savvy_macro::savvy_init; 5 | 6 | #[savvy] 7 | fn no_return_type(x: i32) {} 8 | 9 | #[savvy] 10 | fn wrong_return_type(x: i32) -> i32 {} 11 | 12 | #[savvy] 13 | fn wrong_type_owned_int(x: OwnedIntegerSexp) -> savvy::Result<()> { 14 | Ok(()) 15 | } 16 | 17 | #[savvy] 18 | fn wrong_type_owned_real(x: OwnedRealSexp) -> savvy::Result<()> { 19 | Ok(()) 20 | } 21 | 22 | #[savvy] 23 | fn wrong_type_owned_logical(x: OwnedLogicalSexp) -> savvy::Result<()> { 24 | Ok(()) 25 | } 26 | 27 | #[savvy] 28 | fn wrong_type_owned_string(x: OwnedStringSexp) -> savvy::Result<()> { 29 | Ok(()) 30 | } 31 | 32 | #[savvy] 33 | fn wrong_type_dllinfo(x: *mut DllInfo) -> savvy::Result<()> { 34 | Ok(()) 35 | } 36 | 37 | #[savvy] 38 | fn wrong_type_nested_option(x: Option>) -> savvy::Result<()> { 39 | Ok(()) 40 | } 41 | 42 | #[savvy] 43 | fn wrong_type_option_position(x: Option, y: i32) -> savvy::Result<()> { 44 | Ok(()) 45 | } 46 | 47 | #[savvy] 48 | fn wrong_type_option_owned_int(x: Option) -> savvy::Result<()> { 49 | Ok(()) 50 | } 51 | 52 | // wrong return type 53 | 54 | #[savvy] 55 | fn wrong_return_type1() -> savvy::Result { 56 | Ok(String::new()) 57 | } 58 | 59 | #[savvy] 60 | fn wrong_return_type2() -> savvy::Result { 61 | Ok(0) 62 | } 63 | 64 | #[savvy] 65 | fn wrong_return_type3() -> savvy::Result { 66 | Ok(0) 67 | } 68 | 69 | #[savvy] 70 | fn wrong_return_type4() -> savvy::Result { 71 | Ok(false) 72 | } 73 | 74 | #[savvy] 75 | fn wrong_return_type5() -> savvy::Result { 76 | Ok(0.0) 77 | } 78 | 79 | // lifetime is not supported 80 | #[savvy] 81 | struct Foo<'a>(External::Bar<'a>); 82 | 83 | // only fieldless enums is supported 84 | #[savvy] 85 | enum Foo { 86 | A(i32), 87 | B(&str), 88 | } 89 | 90 | // discreminant is not supported 91 | #[savvy] 92 | enum Foo { 93 | A, 94 | B = 100, 95 | } 96 | 97 | #[savvy_init] 98 | fn init_wrong_type(x: DllInfo) -> savvy::Result<()> { 99 | Ok(()) 100 | } 101 | 102 | #[savvy_init] 103 | fn init_wrong_type2(x: *const DllInfo) -> savvy::Result<()> { 104 | Ok(()) 105 | } 106 | 107 | #[savvy_init] 108 | fn init_wrong_type3(x: *mut DllInfo, y: i32) -> savvy::Result<()> { 109 | Ok(()) 110 | } 111 | 112 | fn main() {} 113 | -------------------------------------------------------------------------------- /savvy-macro/tests/trybuild.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn test_macro_failures() { 3 | let t = trybuild::TestCases::new(); 4 | t.compile_fail("tests/cases/*.rs"); 5 | } 6 | -------------------------------------------------------------------------------- /src/eval.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CString; 2 | 3 | use savvy_ffi::{R_ParseEvalString, R_compute_identical, Rboolean_TRUE, SEXP}; 4 | 5 | use crate::{ 6 | protect::{self}, 7 | unwind_protect, Sexp, 8 | }; 9 | 10 | /// A result of a function call. Since the result does not yet belong to any 11 | /// environment or object, so it needs protection and unprotection. This struct 12 | /// is solely for handling the unprotection in `Drop`. 13 | pub struct EvalResult { 14 | pub(crate) inner: SEXP, 15 | pub(crate) token: SEXP, 16 | } 17 | 18 | impl EvalResult { 19 | pub fn inner(&self) -> SEXP { 20 | self.inner 21 | } 22 | } 23 | 24 | impl Drop for EvalResult { 25 | fn drop(&mut self) { 26 | protect::release_from_preserved_list(self.token); 27 | } 28 | } 29 | 30 | impl From for Sexp { 31 | fn from(value: EvalResult) -> Self { 32 | Self(value.inner()) 33 | } 34 | } 35 | 36 | impl From for crate::error::Result { 37 | fn from(value: EvalResult) -> Self { 38 | Ok(::from(value)) 39 | } 40 | } 41 | 42 | /// Parse and evaluate an R code. This is equivalent to `eval(parse(text = ))`. 43 | /// 44 | /// For simplicity, this function accept only a single line of R code. 45 | /// 46 | pub fn eval_parse_text>(text: T) -> crate::error::Result { 47 | unsafe { 48 | let text_cstr = CString::new(text.as_ref()).unwrap(); 49 | let eval_result = 50 | unwind_protect(|| R_ParseEvalString(text_cstr.as_ptr(), savvy_ffi::R_GlobalEnv))?; 51 | let token = protect::insert_to_preserved_list(eval_result); 52 | let out = EvalResult { 53 | inner: eval_result, 54 | token, 55 | }; 56 | 57 | Ok(out) 58 | } 59 | } 60 | 61 | /// Check if the two SEXPs are identical in the sense that the R function 62 | /// `identical()` returns `TRUE`. 63 | pub fn is_r_identical, T2: Into>(x: T1, y: T2) -> bool { 64 | let x_sexp: Sexp = x.into(); 65 | let y_sexp: Sexp = y.into(); 66 | // They say 16 is the same as identical()'s default 67 | unsafe { R_compute_identical(x_sexp.0, y_sexp.0, 16) == Rboolean_TRUE } 68 | } 69 | 70 | /// Assert that the SEXPs have the same data inside. The second argument is a 71 | /// string of R code. 72 | /// 73 | /// ``` 74 | /// use savvy::assert_eq_r_code; 75 | /// 76 | /// let mut x = savvy::OwnedRealSexp::new(3)?; 77 | /// x[1] = 1.0; 78 | /// x[2] = 2.0; 79 | /// assert_eq_r_code(x, "c(0.0, 1.0, 2.0)"); 80 | /// ``` 81 | pub fn assert_eq_r_code, T2: AsRef>(actual: T1, expected: T2) { 82 | let parsed = eval_parse_text(expected).expect("Failed to parse R code"); 83 | assert!(is_r_identical(actual, parsed)); 84 | } 85 | 86 | #[cfg(feature = "savvy-test")] 87 | mod test { 88 | use crate::{IntegerSexp, RealSexp}; 89 | 90 | use super::eval_parse_text; 91 | 92 | fn assert_invalid_r_code(code: &str) { 93 | assert!(eval_parse_text(code).is_err()); 94 | } 95 | 96 | #[test] 97 | fn test_eval() -> crate::Result<()> { 98 | let parse_int = eval_parse_text("1L")?; 99 | let x = crate::Sexp(parse_int.inner()); 100 | assert!(x.is_integer()); 101 | assert_eq!(IntegerSexp::try_from(x)?.as_slice(), &[1]); 102 | 103 | let parse_real = eval_parse_text("1.0")?; 104 | let x = crate::Sexp(parse_real.inner()); 105 | assert!(x.is_real()); 106 | assert_eq!(RealSexp::try_from(x)?.as_slice(), &[1.0]); 107 | 108 | let parse_vec = eval_parse_text("c(1, 2, 3)")?; 109 | let x = crate::Sexp(parse_vec.inner()); 110 | assert!(x.is_real()); 111 | assert_eq!(RealSexp::try_from(x)?.as_slice(), &[1.0, 2.0, 3.0]); 112 | 113 | // error cases 114 | assert_invalid_r_code("foo("); 115 | assert_invalid_r_code("<- a"); 116 | assert_invalid_r_code("1; 2; 3"); 117 | 118 | Ok(()) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/ffi.rs: -------------------------------------------------------------------------------- 1 | pub use savvy_ffi::DllInfo; 2 | pub use savvy_ffi::SEXP; 3 | -------------------------------------------------------------------------------- /src/io.rs: -------------------------------------------------------------------------------- 1 | use savvy_ffi::{REprintf, R_NilValue, Rprintf}; 2 | 3 | use std::{ffi::CString, io::Write, os::raw::c_char}; 4 | 5 | pub(crate) const LINEBREAK: [c_char; 2] = [b'\n' as _, b'\0' as _]; 6 | 7 | pub fn r_print(msg: &str, linebreak: bool) { 8 | if !msg.is_empty() { 9 | // ignore error 10 | let _ = r_stdout().write_all(msg.replace('%', "%%").as_bytes()); 11 | } 12 | 13 | unsafe { 14 | if linebreak { 15 | Rprintf(LINEBREAK.as_ptr()); 16 | } 17 | } 18 | } 19 | 20 | pub fn r_eprint(msg: &str, linebreak: bool) { 21 | if !msg.is_empty() { 22 | // ignore error 23 | let _ = r_stderr().write_all(msg.replace('%', "%%").as_bytes()); 24 | } 25 | 26 | unsafe { 27 | if linebreak { 28 | REprintf(LINEBREAK.as_ptr()); 29 | } 30 | } 31 | } 32 | 33 | /// Show a warning. 34 | /// 35 | /// Note that, a warning can raise error when `options(warn = 2)`, so you should 36 | /// not ignore the error from `r_warn()`. The error should be propagated to the 37 | /// R session. 38 | pub fn r_warn(msg: &str) -> crate::error::Result<()> { 39 | unsafe { 40 | let msg = CString::new(msg).unwrap_or_default(); 41 | crate::unwind_protect(|| { 42 | savvy_ffi::Rf_warningcall(R_NilValue, msg.as_ptr()); 43 | R_NilValue 44 | })?; 45 | Ok(()) 46 | } 47 | } 48 | 49 | pub struct RStdout {} 50 | 51 | pub fn r_stdout() -> RStdout { 52 | RStdout {} 53 | } 54 | 55 | impl std::io::Write for RStdout { 56 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 57 | let msg = CString::new(buf)?; 58 | unsafe { savvy_ffi::Rprintf(msg.as_ptr()) }; 59 | Ok(buf.len()) 60 | } 61 | 62 | fn flush(&mut self) -> std::io::Result<()> { 63 | Ok(()) 64 | } 65 | } 66 | 67 | pub struct RStderr {} 68 | 69 | pub fn r_stderr() -> RStderr { 70 | RStderr {} 71 | } 72 | 73 | impl std::io::Write for RStderr { 74 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 75 | let msg = CString::new(buf)?; 76 | unsafe { savvy_ffi::REprintf(msg.as_ptr()) }; 77 | Ok(buf.len()) 78 | } 79 | 80 | fn flush(&mut self) -> std::io::Result<()> { 81 | Ok(()) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/log.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_macros)] 2 | #![allow(unused_imports)] 3 | 4 | #[cfg(feature = "logger")] 5 | macro_rules! debug { 6 | ($($arg:tt)+) => (log::debug!($($arg)+)) 7 | } 8 | 9 | #[cfg(not(feature = "logger"))] 10 | macro_rules! debug { 11 | ($($arg:tt)+) => {}; 12 | } 13 | 14 | #[cfg(feature = "logger")] 15 | macro_rules! trace { 16 | ($($arg:tt)+) => (log::trace!($($arg)+)) 17 | } 18 | 19 | #[cfg(not(feature = "logger"))] 20 | macro_rules! trace { 21 | ($($arg:tt)+) => {}; 22 | } 23 | 24 | pub(crate) use {debug, trace}; 25 | 26 | #[cfg(feature = "logger")] 27 | pub fn env_logger() -> env_logger::Builder { 28 | let r_stderr = Box::new(crate::io::r_stderr()); 29 | let target = env_logger::Target::Pipe(r_stderr); 30 | let mut builder = env_logger::builder(); 31 | builder.target(target); 32 | builder 33 | } 34 | -------------------------------------------------------------------------------- /src/panic_hook.rs: -------------------------------------------------------------------------------- 1 | // PanicInfo is deprecated since 1.82 (PanicHookInfo exists since 1.81) 2 | // cf. https://github.com/rust-lang/rust/pull/115974/ 3 | #[rustversion::since(1.81)] 4 | type PanicHookInfo<'a> = std::panic::PanicHookInfo<'a>; 5 | #[rustversion::before(1.81)] 6 | type PanicHookInfo<'a> = std::panic::PanicInfo<'a>; 7 | 8 | pub fn panic_hook(panic_info: &PanicHookInfo) { 9 | // Add indent 10 | let panic_info_indented = format!("{panic_info}") 11 | .lines() 12 | .map(|x| format!("{:indent$}{x}", "", indent = 4)) 13 | .collect::>() 14 | .join("\n"); 15 | 16 | // Backtrace is available only when the debug info is available. 17 | #[cfg(debug_assertions)] 18 | let bt = get_backtrace(); 19 | #[cfg(not(debug_assertions))] 20 | let bt = " (Backtrace is not available on the release build)"; 21 | 22 | crate::io::r_eprint( 23 | &format!( 24 | "panic occured! 25 | 26 | Original message: 27 | {panic_info_indented} 28 | 29 | Backtrace: 30 | {bt} 31 | " 32 | ), 33 | true, 34 | ); 35 | } 36 | 37 | // Since savvy generates many wrappers, the backtrace is boringly deep. Try 38 | // cutting the uninteresting part. 39 | #[cfg(debug_assertions)] 40 | fn get_backtrace() -> String { 41 | let show_full = if let Ok(v) = std::env::var("RUST_BACKTRACE") { 42 | &v == "1" 43 | } else { 44 | false 45 | }; 46 | 47 | // Forcibly captures a full backtrace regardless of RUST_BACKTRACE 48 | let bt = std::backtrace::Backtrace::force_capture().to_string(); 49 | 50 | // try to shorten if the user doesn't require the full backtrace 51 | if !show_full { 52 | let bt_short = bt 53 | .lines() 54 | .skip_while(|line| !line.contains("std::panic::catch_unwind")) 55 | // C stacks are not visible from Rust's side and shown as ``. 56 | .take_while(|line| !line.contains("")) 57 | // Add indent 58 | .map(|x| format!("{:indent$}{x}", "", indent = 4)) 59 | .collect::>() 60 | .join("\n"); 61 | 62 | if !bt_short.is_empty() { 63 | return format!( 64 | " ... 65 | {bt_short} 66 | ... 67 | 68 | note: Run with `RUST_BACKTRACE=1` for a full backtrace. 69 | " 70 | ); 71 | } 72 | } 73 | 74 | // if the user require the full backtrace or the shortened backtrace became mistakenly empty string, show the full backtrace. 75 | bt.lines() 76 | .map(|x| format!("{:indent$}{x}", "", indent = 4)) 77 | .collect::>() 78 | .join("\n") 79 | } 80 | -------------------------------------------------------------------------------- /src/protect.rs: -------------------------------------------------------------------------------- 1 | // This protection mechanism is basically a simple Rust translation of the 2 | // implementation of cpp11. 3 | // 4 | // https://github.com/r-lib/cpp11/blob/main/inst/include/cpp11/protect.hpp 5 | // 6 | // The more explanation on this can be found on the following links: 7 | // 8 | // - https://github.com/RcppCore/Rcpp/issues/1081 9 | // - https://cpp11.r-lib.org/articles/internals.html#protection 10 | // 11 | // However, this implementation differs from these two in several points. First, 12 | // cpp11 stores the anchor Robj in the global options. It says it's because 13 | // 14 | // It is not constructed as a static variable directly since many 15 | // translation units may be compiled, resulting in unrelated instances of each 16 | // static variable. 17 | // 18 | // I'm not immediately sure when this actually happens, but I think I can skip 19 | // the consideration. 20 | // 21 | // Note that, extendr uses a different mechanism of using HashMap to track the 22 | // reference counts. 23 | // 24 | // https://github.com/extendr/extendr/blob/main/extendr-api/src/ownership.rs 25 | // 26 | // I'm not sure why they chose this design, but probably it is because 27 | // 28 | // - for parallel-proof implementation 29 | // - `Robj` might be cloned 30 | // 31 | // But, my implementation doesn't implement `Clone` trait, so I don't need to 32 | // worry that there still exists another instance on dropping it. 33 | 34 | use savvy_ffi::{ 35 | R_NilValue, R_PreserveObject, Rf_cons, Rf_protect, Rf_unprotect, CAR, CDR, SETCAR, SETCDR, 36 | SET_TAG, SEXP, 37 | }; 38 | use std::sync::OnceLock; 39 | 40 | // Protection mechanism by `Rf_protect()`. This struct is needed for 41 | // auto-unprotect when returning from the scope. 42 | 43 | pub(crate) struct LocalProtection {} 44 | 45 | impl Drop for LocalProtection { 46 | fn drop(&mut self) { 47 | unsafe { Rf_unprotect(1) }; 48 | } 49 | } 50 | 51 | /// Provide a protection that lasts within the function scope, i.e., 52 | /// automatically cleans up by `Rf_unprotect()`. This might not be very 53 | /// efficient as this can execute `Rf_unprotect(1)` multiple times where it 54 | /// could be `Rf_unprotect(n)` once. But, I found manual `Rf_unprotect()` is 55 | /// almost impossible for human considering there are many early return by `?`, 56 | /// so this should be better than failure. 57 | pub(crate) fn local_protect(obj: SEXP) -> LocalProtection { 58 | unsafe { Rf_protect(obj) }; 59 | LocalProtection {} 60 | } 61 | 62 | // Protection mechanism by a doubly-linked pairlist. 63 | // cf. https://cpp11.r-lib.org/articles/internals.html#protection 64 | 65 | pub(crate) struct PreservedList(SEXP); 66 | 67 | // cf. https://doc.rust-lang.org/stable/nomicon/send-and-sync.html 68 | unsafe impl Send for PreservedList {} 69 | unsafe impl Sync for PreservedList {} 70 | 71 | pub(crate) static PRESERVED_LIST: OnceLock = OnceLock::new(); 72 | 73 | #[allow(clippy::not_unsafe_ptr_arg_deref)] 74 | pub fn insert_to_preserved_list(obj: SEXP) -> SEXP { 75 | unsafe { 76 | if obj == R_NilValue { 77 | return R_NilValue; 78 | } 79 | 80 | // Protect the object until the operation finishes 81 | let _obj_guard = local_protect(obj); 82 | 83 | let preserved = PRESERVED_LIST.get_or_init(|| { 84 | let r = Rf_cons(R_NilValue, R_NilValue); 85 | R_PreserveObject(r); 86 | PreservedList(r) 87 | }); 88 | let token = Rf_cons(preserved.0, CDR(preserved.0)); 89 | 90 | let _token_guard = local_protect(token); 91 | 92 | SET_TAG(token, obj); 93 | SETCDR(preserved.0, token); 94 | 95 | if CDR(token) != R_NilValue { 96 | SETCAR(CDR(token), token); 97 | } 98 | 99 | token 100 | } 101 | } 102 | 103 | #[allow(clippy::not_unsafe_ptr_arg_deref)] 104 | pub fn release_from_preserved_list(token: SEXP) { 105 | unsafe { 106 | if token == R_NilValue { 107 | return; 108 | } 109 | 110 | let before = CAR(token); 111 | let after = CDR(token); 112 | 113 | SETCDR(before, after); 114 | 115 | if after != R_NilValue { 116 | SETCAR(after, before); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/sexp/environment.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CString; 2 | 3 | use savvy_ffi::{R_GlobalEnv, R_NilValue, R_UnboundValue, Rboolean_TRUE, SEXP}; 4 | 5 | use crate::{savvy_err, Sexp}; 6 | 7 | use super::utils::str_to_symsxp; 8 | 9 | /// An environment. 10 | pub struct EnvironmentSexp(pub SEXP); 11 | 12 | impl EnvironmentSexp { 13 | /// Returns the raw SEXP. 14 | #[inline] 15 | pub fn inner(&self) -> savvy_ffi::SEXP { 16 | self.0 17 | } 18 | 19 | /// Returns the SEXP bound to a variable of the specified name in the 20 | /// specified environment. 21 | /// 22 | /// The absense of an object with the specified name is represented as 23 | /// `None`. `Some(NilSexp)` means there's a variable whose value is `NULL`. 24 | /// 25 | /// # Protection 26 | /// 27 | /// The result `Sexp` is unprotected. In most of the cases, you don't need 28 | /// to worry about this because existing in an environment means it won't be 29 | /// GC-ed as long as the environment exists (it's possible the correspondig 30 | /// variable gets explicitly removed, but it should be rare). However, if 31 | /// the environment is a temporary one (e.g. an exectuion environment of a 32 | /// function call), it's your responsibility to protect the object. In other 33 | /// words, you should never use this if you don't understand how R's 34 | /// protection mechanism works. 35 | pub fn get>(&self, name: T) -> crate::error::Result> { 36 | let sym = str_to_symsxp(name)?.ok_or(savvy_err!("name must not be empty"))?; 37 | 38 | // Note: since this SEXP already belongs to an environment, this doesn't 39 | // need protection. 40 | let sexp = unsafe { 41 | crate::unwind_protect(|| { 42 | if savvy_ffi::R_existsVarInFrame(self.0, sym) == Rboolean_TRUE { 43 | // TODO: replace this with R_getVar() when savvy drop supports on R <4.5 44 | savvy_ffi::Rf_eval(sym, self.0) 45 | } else { 46 | R_UnboundValue 47 | } 48 | })? 49 | }; 50 | 51 | if sexp == unsafe { R_UnboundValue } { 52 | Ok(None) 53 | } else { 54 | Ok(Some(Sexp(sexp))) 55 | } 56 | } 57 | 58 | /// Returns `true` the specified environment contains the specified 59 | /// variable. 60 | pub fn contains>(&self, name: T) -> crate::error::Result { 61 | let sym = str_to_symsxp(name)?.ok_or(savvy_err!("name must not be empty"))?; 62 | 63 | let res = unsafe { 64 | crate::unwind_protect(|| { 65 | if savvy_ffi::R_existsVarInFrame(self.0, sym) == Rboolean_TRUE { 66 | // Note: Since `unwind_protect()` can only return an SEXP, 67 | // this needs to be some sentinel value. Any SEXP can be 68 | // used here as long as it can be used as a signal of a 69 | // success. 70 | R_NilValue 71 | } else { 72 | R_UnboundValue 73 | } 74 | })? == R_NilValue 75 | }; 76 | 77 | Ok(res) 78 | } 79 | 80 | /// Bind the SEXP to the specified environment as the specified name. 81 | pub fn set>(&self, name: T, value: Sexp) -> crate::error::Result<()> { 82 | let name_cstr = CString::new(name.as_ref())?; 83 | 84 | unsafe { 85 | crate::unwind_protect(|| { 86 | savvy_ffi::Rf_defineVar(savvy_ffi::Rf_install(name_cstr.as_ptr()), value.0, self.0); 87 | R_NilValue 88 | })? 89 | }; 90 | 91 | Ok(()) 92 | } 93 | 94 | /// Return the global env. 95 | pub fn global_env() -> Self { 96 | Self(unsafe { R_GlobalEnv }) 97 | } 98 | } 99 | 100 | // conversions from/to EnvironmentSexp *************** 101 | 102 | impl TryFrom for EnvironmentSexp { 103 | type Error = crate::error::Error; 104 | 105 | fn try_from(value: Sexp) -> crate::error::Result { 106 | value.assert_environment()?; 107 | Ok(Self(value.0)) 108 | } 109 | } 110 | 111 | impl From for Sexp { 112 | fn from(value: EnvironmentSexp) -> Self { 113 | Self(value.inner()) 114 | } 115 | } 116 | 117 | impl From for crate::error::Result { 118 | fn from(value: EnvironmentSexp) -> Self { 119 | Ok(::from(value)) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/sexp/na.rs: -------------------------------------------------------------------------------- 1 | // NOTE: No implementation is provided for bool because R's bool is tricky. 2 | // https://cpp11.r-lib.org/articles/cpp11.html#na 3 | 4 | pub trait NotAvailableValue { 5 | fn is_na(&self) -> bool; 6 | fn na() -> Self; 7 | } 8 | 9 | impl NotAvailableValue for f64 { 10 | fn is_na(&self) -> bool { 11 | unsafe { savvy_ffi::R_IsNA(*self) != 0 } 12 | } 13 | 14 | fn na() -> Self { 15 | unsafe { savvy_ffi::R_NaReal } 16 | } 17 | } 18 | 19 | impl NotAvailableValue for i32 { 20 | fn is_na(&self) -> bool { 21 | unsafe { *self == savvy_ffi::R_NaInt } 22 | } 23 | 24 | fn na() -> Self { 25 | unsafe { savvy_ffi::R_NaInt } 26 | } 27 | } 28 | 29 | #[cfg(feature = "complex")] 30 | impl NotAvailableValue for num_complex::Complex64 { 31 | fn is_na(&self) -> bool { 32 | unsafe { self.re == savvy_ffi::R_NaReal } 33 | } 34 | 35 | fn na() -> Self { 36 | unsafe { 37 | num_complex::Complex64 { 38 | re: savvy_ffi::R_NaReal, 39 | im: savvy_ffi::R_NaReal, 40 | } 41 | } 42 | } 43 | } 44 | 45 | use std::sync::OnceLock; 46 | 47 | pub(crate) static NA_CHAR_PTR: OnceLock<&str> = OnceLock::new(); 48 | 49 | impl NotAvailableValue for &str { 50 | fn is_na(&self) -> bool { 51 | self.as_ptr() == Self::na().as_ptr() 52 | } 53 | 54 | // I use the underlying "NA" string of R_NaString directry here, but this 55 | // wasn't possible on extendr due to some unobvious reason related to 56 | // concurrency. 57 | // 58 | // cf., https://github.com/extendr/extendr/issues/483#issuecomment-1435499525 59 | fn na() -> Self { 60 | NA_CHAR_PTR.get_or_init(|| unsafe { 61 | let c_ptr = savvy_ffi::R_CHAR(savvy_ffi::R_NaString) as _; 62 | std::str::from_utf8_unchecked(std::slice::from_raw_parts(c_ptr, 2)) 63 | }) 64 | } 65 | } 66 | 67 | /// Return true if the SEXP is a length-1 of vector containing NA. 68 | pub(crate) unsafe fn is_scalar_na(x: savvy_ffi::SEXP) -> bool { 69 | if unsafe { savvy_ffi::Rf_xlength(x) } != 1 { 70 | return false; 71 | } 72 | 73 | let ty = unsafe { savvy_ffi::TYPEOF(x) }; 74 | match ty { 75 | savvy_ffi::INTSXP => unsafe { savvy_ffi::INTEGER_ELT(x, 0) }.is_na(), 76 | savvy_ffi::REALSXP => unsafe { savvy_ffi::REAL_ELT(x, 0) }.is_na(), 77 | #[cfg(feature = "complex")] 78 | savvy_ffi::CPLXSXP => unsafe { savvy_ffi::COMPLEX_ELT(x, 0) }.is_na(), 79 | savvy_ffi::LGLSXP => unsafe { savvy_ffi::LOGICAL_ELT(x, 0) }.is_na(), 80 | savvy_ffi::RAWSXP => false, // raw doesn't have NA 81 | savvy_ffi::STRSXP => unsafe { savvy_ffi::STRING_ELT(x, 0) == savvy_ffi::R_NaString }, 82 | _ => false, 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/sexp/null.rs: -------------------------------------------------------------------------------- 1 | use savvy_ffi::SEXP; 2 | 3 | use crate::Sexp; 4 | 5 | /// This is a dummy struct solely for providing `NULL` [Result]. 6 | pub struct NullSexp; 7 | 8 | // Conversion into SEXP is infallible as it's just extract the inner one. 9 | impl From for Sexp { 10 | fn from(_value: NullSexp) -> Self { 11 | Self(unsafe { savvy_ffi::R_NilValue }) 12 | } 13 | } 14 | 15 | impl From for crate::error::Result { 16 | fn from(value: NullSexp) -> Self { 17 | Ok(::from(value)) 18 | } 19 | } 20 | 21 | // Conversion into SEXP is infallible as it's just extract the inner one. 22 | impl From for SEXP { 23 | fn from(value: NullSexp) -> Self { 24 | ::from(value).0 25 | } 26 | } 27 | 28 | pub fn null() -> SEXP { 29 | unsafe { savvy_ffi::R_NilValue } 30 | } 31 | 32 | impl TryFrom<()> for NullSexp { 33 | type Error = crate::error::Error; 34 | 35 | fn try_from(_: ()) -> crate::error::Result { 36 | Ok(NullSexp) 37 | } 38 | } 39 | 40 | impl TryFrom<()> for Sexp { 41 | type Error = crate::error::Error; 42 | 43 | fn try_from(value: ()) -> crate::error::Result { 44 | ::try_from(value).map(|x| x.into()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/sexp/scalar.rs: -------------------------------------------------------------------------------- 1 | use savvy_ffi::{LOGICAL_ELT, RAW_ELT}; 2 | 3 | use crate::{IntegerSexp, LogicalSexp, RawSexp, RealSexp, Sexp, StringSexp}; 4 | 5 | use super::na::NotAvailableValue; 6 | 7 | macro_rules! impl_try_from_scalar { 8 | ($scalar_ty: ty, $sexp_ty: ty) => { 9 | impl TryFrom for $scalar_ty { 10 | type Error = crate::error::Error; 11 | 12 | fn try_from(value: Sexp) -> crate::error::Result { 13 | let value = <$sexp_ty>::try_from(value)?; 14 | if value.len() != 1 { 15 | return Err(crate::error::Error::NotScalar); 16 | } 17 | 18 | let result = value.iter().next().unwrap(); 19 | 20 | if result.is_na() { 21 | return Err(crate::error::Error::NotScalar); 22 | } 23 | 24 | Ok(result.clone()) 25 | } 26 | } 27 | }; 28 | } 29 | 30 | impl_try_from_scalar!(i32, IntegerSexp); 31 | impl_try_from_scalar!(f64, RealSexp); 32 | impl_try_from_scalar!(&str, StringSexp); 33 | 34 | // bool doesn't have na() method, so define manually. 35 | impl TryFrom for bool { 36 | type Error = crate::error::Error; 37 | 38 | fn try_from(value: Sexp) -> crate::error::Result { 39 | let value = ::try_from(value)?; 40 | if value.len() != 1 { 41 | return Err(crate::error::Error::NotScalar); 42 | } 43 | 44 | let result_int = unsafe { LOGICAL_ELT(value.0, 0) }; 45 | if result_int.is_na() { 46 | return Err(crate::error::Error::NotScalar); 47 | } 48 | 49 | Ok(result_int == 1) 50 | } 51 | } 52 | 53 | // raw doesn't have na() method, so define manually. 54 | impl TryFrom for u8 { 55 | type Error = crate::error::Error; 56 | 57 | fn try_from(value: Sexp) -> crate::error::Result { 58 | let value = ::try_from(value)?; 59 | if value.len() != 1 { 60 | return Err(crate::error::Error::NotScalar); 61 | } 62 | 63 | Ok(unsafe { RAW_ELT(value.0, 0) }) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/sexp/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ffi::{CStr, CString}, 3 | os::raw::c_char, 4 | }; 5 | 6 | use savvy_ffi::{cetype_t_CE_UTF8, Rf_mkCharLenCE, Rf_xlength, R_CHAR, SEXP}; 7 | 8 | use crate::{savvy_err, NotAvailableValue}; 9 | 10 | pub(crate) fn assert_len(len: usize, i: usize) -> crate::error::Result<()> { 11 | if i >= len { 12 | Err(savvy_err!( 13 | "index out of bounds: the length is {len} but the index is {i}", 14 | )) 15 | } else { 16 | Ok(()) 17 | } 18 | } 19 | 20 | pub(crate) unsafe fn str_to_charsxp(v: &str) -> crate::error::Result { 21 | unsafe { 22 | // We might be able to put `R_NaString` directly without using 23 | // <&str>::na(), but probably this is an inevitable cost of 24 | // providing <&str>::na(). 25 | if v.is_na() { 26 | Ok(savvy_ffi::R_NaString) 27 | } else { 28 | crate::unwind_protect(|| { 29 | Rf_mkCharLenCE( 30 | v.as_ptr() as *const c_char, 31 | v.len() as i32, 32 | cetype_t_CE_UTF8, 33 | ) 34 | }) 35 | } 36 | } 37 | } 38 | 39 | // This doesn't handle NA. 40 | pub(crate) unsafe fn charsxp_to_str(v: SEXP) -> &'static str { 41 | unsafe { 42 | // I bravely assume all strings are valid UTF-8 and don't use 43 | // `Rf_translateCharUTF8()`! 44 | let ptr = R_CHAR(v) as *const u8; 45 | let v_utf8 = std::slice::from_raw_parts(ptr, Rf_xlength(v) as usize + 1); // +1 for NUL 46 | 47 | // Use CStr to check the UTF-8 validity. 48 | CStr::from_bytes_with_nul_unchecked(v_utf8) 49 | .to_str() 50 | .unwrap_or_default() 51 | } 52 | } 53 | 54 | // Note: the result is not protected (although symbol is probably not GC-ed?) 55 | pub(crate) fn str_to_symsxp>(name: T) -> crate::error::Result> { 56 | let name = name.as_ref(); 57 | if name.is_empty() { 58 | return Ok(None); 59 | } 60 | 61 | let name_cstr = CString::new(name)?; 62 | let sym = unsafe { crate::unwind_protect(|| savvy_ffi::Rf_install(name_cstr.as_ptr())) }?; 63 | 64 | Ok(Some(sym)) 65 | } 66 | -------------------------------------------------------------------------------- /src/unwind_protect.rs: -------------------------------------------------------------------------------- 1 | use std::os::raw::c_void; 2 | 3 | use savvy_ffi::SEXP; 4 | 5 | extern "C" { 6 | fn unwind_protect_impl( 7 | fun: Option SEXP>, 8 | data: *mut c_void, 9 | ) -> SEXP; 10 | } 11 | 12 | /// # Safety 13 | /// 14 | /// This function wraps around `R_UnwindProtect()` API, which is very unsafe in 15 | /// its nature. So, please use this with care. 16 | pub unsafe fn unwind_protect(f: F) -> crate::error::Result 17 | where 18 | F: FnOnce() -> SEXP + Copy, 19 | { 20 | unsafe { 21 | unsafe extern "C" fn do_call(data: *mut c_void) -> SEXP 22 | where 23 | F: FnOnce() -> SEXP + Copy, 24 | { 25 | unsafe { 26 | let data = data as *const (); 27 | let f: &F = &*(data as *const F); 28 | f() 29 | } 30 | } 31 | 32 | let do_call_ptr = std::mem::transmute::< 33 | *const (), 34 | Option SEXP>, 35 | >(do_call:: as *const ()); 36 | let actual_fn_ptr = std::mem::transmute::<*const F, *mut c_void>(&f as *const F); 37 | let res: SEXP = unwind_protect_impl(do_call_ptr, actual_fn_ptr); 38 | 39 | if (res as usize & 1) == 1 { 40 | return Err(crate::error::Error::Aborted(res)); 41 | } 42 | 43 | Ok(res) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/unwind_protect_wrapper.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | 6 | void not_so_long_jump(void *jmpbuf, Rboolean jump) { 7 | if (jump == TRUE) { 8 | longjmp(*(jmp_buf *)jmpbuf, 1); 9 | } 10 | } 11 | 12 | SEXP unwind_protect_impl(SEXP (*fun)(void *data), void *data) { 13 | SEXP token = R_MakeUnwindCont(); 14 | PROTECT(token); 15 | 16 | jmp_buf jmpbuf; 17 | if (setjmp(jmpbuf)) { 18 | // Tag the pointer 19 | return (SEXP)((uintptr_t)token | 1); 20 | } 21 | 22 | SEXP res = R_UnwindProtect(fun, data, not_so_long_jump, &jmpbuf, token); 23 | 24 | UNPROTECT(1); 25 | return res; 26 | } 27 | -------------------------------------------------------------------------------- /wrapper.h: -------------------------------------------------------------------------------- 1 | // From r83513 (R 4.3), R defines the `NORET` macro differently depending on the 2 | // C/C++ standard the compiler uses. It matters when the header is used in C/C++ 3 | // libraries, but all we want to do here is to make bindgen interpret `NOREP` to 4 | // `!`. However, for some reason, bindgen doesn't handle other no-return 5 | // attributes like `_Noreturn` (for C11) and `[[noreturn]]` (for C++ and C23), 6 | // so we define it here. 7 | #define NORET __attribute__((__noreturn__)) 8 | 9 | #include 10 | 11 | // For R_ParseVector() 12 | #include 13 | 14 | // For ALTREP 15 | #include 16 | -------------------------------------------------------------------------------- /xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xtask" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | 9 | publish = false 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | bindgen = "0.72.0" 15 | 16 | [package.metadata.dist] 17 | dist = false 18 | --------------------------------------------------------------------------------