├── .github ├── actions │ └── publish-doc │ │ └── action.yml ├── dependabot.yml └── workflows │ ├── build-docs.yml │ ├── build-gems.yml │ ├── ci.yml │ ├── memcheck.yml │ └── release.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .standard.yml ├── .yardopts ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── NIGHTLY_VERSION ├── README.md ├── Rakefile ├── bench ├── bench.rb ├── compile.rb ├── component_id.rb ├── func_call.rb ├── host_call.rb └── instantiate.rb ├── bin ├── console └── setup ├── examples ├── epoch.rb ├── externref.rb ├── externref.wat ├── fuel.rb ├── fuel.wat ├── gcd.rb ├── gcd.wat ├── hello.rb ├── hello.wat ├── linking.rb ├── linking1.wat ├── linking2.wat ├── memory.rb ├── memory.wat ├── multi.rb ├── multi.wat ├── rust-crate │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── wasi.rb ├── ext ├── Cargo.toml ├── build.rs ├── extconf.rb └── src │ ├── helpers │ ├── macros.rs │ ├── mod.rs │ ├── nogvl.rs │ ├── output_limited_buffer.rs │ ├── static_id.rs │ ├── symbol_enum.rs │ └── tmplock.rs │ ├── lib.rs │ └── ruby_api │ ├── caller.rs │ ├── component.rs │ ├── component │ ├── convert.rs │ ├── func.rs │ ├── instance.rs │ └── linker.rs │ ├── config.rs │ ├── convert.rs │ ├── engine.rs │ ├── errors.rs │ ├── externals.rs │ ├── func.rs │ ├── global.rs │ ├── instance.rs │ ├── linker.rs │ ├── memory.rs │ ├── memory │ └── unsafe_slice.rs │ ├── mod.rs │ ├── module.rs │ ├── params.rs │ ├── pooling_allocation_config.rs │ ├── store.rs │ ├── table.rs │ ├── trap.rs │ ├── wasi_ctx.rs │ └── wasi_ctx_builder.rs ├── lib ├── wasmtime.rb └── wasmtime │ ├── component.rb │ ├── error.rb │ └── version.rb ├── rakelib ├── bench.rake ├── compile.rake ├── doc.rake ├── env.rake ├── examples.rake ├── helpers.rake ├── mem.rake ├── pkg.rake └── spec.rake ├── spec ├── convert_spec.rb ├── fixtures │ ├── .gitignore │ ├── component-types │ │ ├── .cargo │ │ │ └── config.toml │ │ ├── .gitignore │ │ ├── .vscode │ │ │ └── settings.json │ │ ├── .zed │ │ │ └── settings.json │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── src │ │ │ └── lib.rs │ │ └── wit │ │ │ └── world.wit │ ├── component_adder.wat │ ├── component_trap.wat │ ├── component_types.wasm │ ├── empty.wat │ ├── empty_component.wat │ ├── wasi-debug.wasm │ ├── wasi-debug │ │ ├── .cargo │ │ │ └── config.toml │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src │ │ │ └── main.rs │ ├── wasi-deterministic.wasm │ └── wasi-deterministic │ │ ├── .cargo │ │ └── config.toml │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src │ │ └── main.rs ├── integration │ ├── epoch_interruption_spec.rb │ ├── hello_world_spec.rb │ ├── insanity_spec.rb │ └── ractor_spec.rb ├── spec_helper.rb ├── unit │ ├── component │ │ ├── component_spec.rb │ │ ├── convert_spec.rb │ │ ├── func_spec.rb │ │ ├── instance_spec.rb │ │ ├── linker_spec.rb │ │ ├── result_spec.rb │ │ └── variant_spec.rb │ ├── engine_spec.rb │ ├── error_spec.rb │ ├── extern_spec.rb │ ├── fuel_spec.rb │ ├── func_spec.rb │ ├── global_spec.rb │ ├── instance_spec.rb │ ├── linker_spec.rb │ ├── memory_spec.rb │ ├── module_spec.rb │ ├── pooling_allocation_config_spec.rb │ ├── store_spec.rb │ ├── table_spec.rb │ ├── trap_spec.rb │ ├── wasi_spec.rb │ └── wasmtime_spec.rb └── wasmtime_spec.rb ├── suppressions ├── readme.md └── ruby-3.1.supp └── wasmtime.gemspec /.github/actions/publish-doc/action.yml: -------------------------------------------------------------------------------- 1 | name: Publish documentation to GiHub Pages 2 | runs: 3 | using: composite 4 | steps: 5 | - uses: actions/checkout@v3 6 | with: 7 | ref: gh-pages 8 | 9 | - name: Download docs 10 | uses: actions/download-artifact@v4 11 | with: 12 | name: doc 13 | path: doc 14 | 15 | - name: Current doc dir 16 | id: doc-dir 17 | uses: k1LoW/github-script-ruby@v2 18 | with: 19 | result-encoding: string 20 | script: | 21 | context.ref 22 | .gsub(%r{\Arefs/heads/}, "") 23 | .gsub(%r{\Arefs/tags/}, "") 24 | 25 | - name: Move docs to dest folder 26 | shell: bash 27 | run: | 28 | ls -lah 29 | rm -rf ${{steps.doc-dir.outputs.result}} 30 | mv doc ${{steps.doc-dir.outputs.result}} 31 | 32 | - name: Find the latest doc 33 | uses: k1LoW/github-script-ruby@v2 34 | id: latest-dir 35 | with: 36 | result-encoding: string 37 | script: | 38 | build_version = -> (str) do 39 | Gem::Version.new(str.gsub(/\Av/, "")) 40 | rescue nil 41 | end 42 | 43 | Dir 44 | .glob('v*') 45 | .select { File.directory?(_1) && build_version[_1] } 46 | .max_by(&build_version) 47 | &.to_s || "main" 48 | 49 | - name: Commit the changes 50 | shell: bash 51 | run: | 52 | rm -f latest 53 | ln -s ${{steps.latest-dir.outputs.result}} latest 54 | git add ${{steps.doc-dir.outputs.result}} latest 55 | 56 | # Exit if there's no changes 57 | if [[ ! $(git diff --name-only --cached) ]]; then 58 | exit 0 59 | fi 60 | 61 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 62 | git config user.name "github-actions[bot]" 63 | git commit -m "Bump doc" 64 | 65 | - name: Push changes 66 | uses: ad-m/github-push-action@master 67 | with: 68 | github_token: ${{ github.token }} 69 | branch: gh-pages 70 | 71 | # Return to the original REF so that post-action 72 | # can still run with the action available. 73 | - uses: actions/checkout@v3 74 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | 9 | - package-ecosystem: "bundler" 10 | directory: "/" 11 | schedule: 12 | interval: "monthly" 13 | 14 | - package-ecosystem: "cargo" 15 | directory: "/" 16 | schedule: 17 | interval: "monthly" 18 | -------------------------------------------------------------------------------- /.github/workflows/build-docs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build documentation 3 | 4 | on: 5 | workflow_dispatch: 6 | push: 7 | pull_request: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | build_doc: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Derive nightly version 20 | run: | 21 | version="$(cat ./NIGHTLY_VERSION)" 22 | echo "NIGHTLY_VERSION=$version" >> $GITHUB_ENV 23 | 24 | - name: Remove Gemfile.lock 25 | run: rm Gemfile.lock 26 | 27 | - uses: oxidize-rb/actions/setup-ruby-and-rust@v1 28 | with: 29 | ruby-version: "3.4" 30 | rustup-toolchain: "${{ env.NIGHTLY_VERSION }}" 31 | bundler-cache: true 32 | cargo-cache: true 33 | cache-version: docs-v1 34 | 35 | - name: Generate doc 36 | run: bundle exec rake doc 37 | 38 | - name: Upload generated doc 39 | uses: actions/upload-artifact@v4 40 | with: 41 | name: doc 42 | path: doc 43 | retention-days: 1 44 | 45 | - name: Publish doc 46 | if: contains(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main' 47 | uses: ./.github/actions/publish-doc 48 | 49 | -------------------------------------------------------------------------------- /.github/workflows/build-gems.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build gems 3 | 4 | on: 5 | workflow_dispatch: 6 | push: 7 | branches: ["main", "cross-gem/*", "pkg/*"] 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | ci-data: 15 | runs-on: ubuntu-latest 16 | outputs: 17 | result: ${{ steps.fetch.outputs.result }} 18 | steps: 19 | - id: fetch 20 | uses: oxidize-rb/actions/fetch-ci-data@v1 21 | with: 22 | supported-ruby-platforms: | 23 | # Excluding: 24 | # `arm-linux`: Cranelift doesn't support 32-bit architectures 25 | # `x64-mingw32`: `x64-mingw-ucrt` should be used for Ruby 3.1+ (https://github.com/rake-compiler/rake-compiler-dock?tab=readme-ov-file#windows) 26 | # 3.0 is deprecated as stable ruby version according to: 27 | # https://github.com/oxidize-rb/actions/blob/main/fetch-ci-data/evaluate.rb#L54 28 | exclude: [arm-linux, x64-mingw32] 29 | stable-ruby-versions: | 30 | exclude: [head] 31 | 32 | native: 33 | name: Build native gems 34 | needs: ci-data 35 | runs-on: ubuntu-latest 36 | strategy: 37 | fail-fast: false 38 | matrix: 39 | ruby-platform: ${{ fromJSON(needs.ci-data.outputs.result).supported-ruby-platforms }} 40 | steps: 41 | - uses: actions/checkout@v4 42 | 43 | - uses: ruby/setup-ruby@v1 44 | with: 45 | ruby-version: "3.4" 46 | 47 | - uses: oxidize-rb/actions/cross-gem@v1 48 | id: cross-gem 49 | with: 50 | platform: ${{ matrix.ruby-platform }} 51 | ruby-versions: ${{ join(fromJSON(needs.ci-data.outputs.result).stable-ruby-versions, ',') }} 52 | 53 | - uses: actions/upload-artifact@v4 54 | with: 55 | name: cross-gem-${{ matrix.ruby-platform }} 56 | path: ${{ steps.cross-gem.outputs.gem-path }} 57 | if-no-files-found: error 58 | 59 | - name: Smoke gem install 60 | if: matrix.ruby-platform == 'x86_64-linux' # GitHub actions architecture 61 | run: bundle install && bundle exec rake pkg:${{ matrix.ruby-platform }}:test 62 | 63 | source: 64 | name: Build source gem 65 | runs-on: ${{ matrix.os }} 66 | strategy: 67 | matrix: 68 | os: ["ubuntu-latest"] 69 | ruby: ["3.4"] 70 | steps: 71 | - uses: actions/checkout@v4 72 | 73 | - uses: oxidize-rb/actions/setup-ruby-and-rust@v1 74 | with: 75 | ruby-version: ${{ matrix.ruby }} 76 | bundler-cache: true 77 | cargo-cache: false 78 | cache-version: v2 79 | 80 | - name: Smoke test gem install 81 | shell: bash 82 | run: bundle exec rake pkg:ruby:test 83 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | on: 5 | workflow_dispatch: 6 | pull_request: 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | env: 13 | RSPEC_FORMATTER: doc 14 | 15 | jobs: 16 | ci-data: 17 | runs-on: ubuntu-latest 18 | outputs: 19 | result: ${{ steps.fetch.outputs.result }} 20 | steps: 21 | - id: fetch 22 | uses: oxidize-rb/actions/fetch-ci-data@v1 23 | with: 24 | stable-ruby-versions: | 25 | # See https://github.com/bytecodealliance/wasmtime-rb/issues/286 26 | # for details. 27 | exclude: [head] 28 | 29 | ci: 30 | runs-on: ${{ matrix.os }} 31 | needs: ci-data 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | os: ["ubuntu-latest", "macos-latest", "windows-latest"] 36 | ruby: ${{ fromJSON(needs.ci-data.outputs.result).stable-ruby-versions }} 37 | # include: 38 | # mswin relies on head and we're not creating releases for it 39 | # so disabling it as well. 40 | # - os: windows-latest 41 | # ruby: mswin 42 | steps: 43 | - uses: actions/checkout@v4 44 | 45 | - name: Remove Gemfile.lock 46 | run: rm Gemfile.lock 47 | 48 | - uses: oxidize-rb/actions/setup-ruby-and-rust@v1 49 | with: 50 | ruby-version: ${{ matrix.ruby }} 51 | bundler-cache: true 52 | cargo-cache: true 53 | cache-version: v5 54 | 55 | - name: Compile rust ext 56 | run: bundle exec rake compile:release 57 | 58 | - name: Run ruby tests 59 | run: bundle exec rake spec 60 | 61 | - name: Run ruby tests (hard-mode with GC.stress) 62 | run: bundle exec rake spec 63 | env: 64 | GC_STRESS: "true" 65 | 66 | - name: Run examples 67 | run: bundle exec rake examples 68 | 69 | - name: Run benchmarks 70 | run: bundle exec rake bench:all 71 | 72 | - name: Lint ruby 73 | run: bundle exec rake standard 74 | 75 | - name: Lint rust 76 | run: cargo clippy -- -D warnings && cargo fmt --check 77 | -------------------------------------------------------------------------------- /.github/workflows/memcheck.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Memcheck 3 | 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | ruby-version: 8 | description: "Ruby version to memcheck" 9 | required: true 10 | default: "3.4" 11 | type: choice 12 | options: 13 | - "head" 14 | - "3.4" 15 | - "3.3" 16 | - "3.2" 17 | - "3.1" 18 | - "3.0" 19 | debug: 20 | description: "Enable debug mode" 21 | required: false 22 | default: "false" 23 | type: boolean 24 | push: 25 | branches: ["*"] 26 | tags-ignore: ["v*"] # Skip Memcheck for releases 27 | 28 | concurrency: 29 | group: ${{ github.workflow }}-${{ github.ref }} 30 | cancel-in-progress: true 31 | 32 | jobs: 33 | memcheck: 34 | name: Memcheck 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v4 38 | 39 | - uses: oxidize-rb/actions/setup-ruby-and-rust@v1 40 | with: 41 | ruby-version: ${{ inputs.ruby-version || '3.4' }} 42 | bundler-cache: true 43 | cargo-cache: true 44 | cache-version: v2 45 | 46 | - name: Install deps 47 | run: | 48 | bundle config unset deployment 49 | bundle add ruby_memcheck --version '~> 1.3.1' # avoid usage in Gemfile bc it pulls in nokogiri 50 | sudo apt-get update 51 | sudo apt-get install -y valgrind 52 | bundle config set deployment true 53 | 54 | - name: Run "mem:check" task 55 | env: 56 | RSPEC_FORMATTER: "progress" 57 | RSPEC_FAILURE_EXIT_CODE: "0" 58 | GC_AT_EXIT: "1" 59 | DEBUG: ${{ inputs.debug || 'false' }} 60 | RB_SYS_CARGO_PROFILE: ${{ inputs.debug == 'true' && 'dev' || 'release' }} 61 | WASMTIME_TARGET: "x86_64-unknown-linux-gnu" # use generic target for memcheck 62 | run: | 63 | if ! bundle exec rake mem:check; then 64 | echo "::error::Valgrind memory check failed, for more info please see ./suppressions/readme.md" 65 | exit 1 66 | fi 67 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release 3 | 4 | on: 5 | workflow_dispatch: 6 | push: 7 | tags: ["v*"] 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | ci-data: 15 | runs-on: ubuntu-latest 16 | outputs: 17 | result: ${{ steps.fetch.outputs.result }} 18 | steps: 19 | - id: fetch 20 | uses: oxidize-rb/actions/fetch-ci-data@v1 21 | with: 22 | supported-ruby-platforms: | 23 | # Excluding: 24 | # `arm-linux`: Cranelift doesn't support 32-bit architectures 25 | # `x64-mingw32`: `x64-mingw-ucrt` should be used for Ruby 3.1+ (https://github.com/rake-compiler/rake-compiler-dock?tab=readme-ov-file#windows) 26 | # 3.0 is deprecated as stable ruby version according to: 27 | # https://github.com/oxidize-rb/actions/blob/main/fetch-ci-data/evaluate.rb#L54 28 | exclude: [arm-linux, x64-mingw32] 29 | stable-ruby-versions: | 30 | exclude: [head] 31 | 32 | build: 33 | name: Build native gems 34 | needs: ci-data 35 | runs-on: ubuntu-latest 36 | strategy: 37 | fail-fast: false 38 | matrix: 39 | ruby-platform: ${{ fromJSON(needs.ci-data.outputs.result).supported-ruby-platforms }} 40 | steps: 41 | - uses: actions/checkout@v4 42 | 43 | - uses: ruby/setup-ruby@v1 44 | with: 45 | ruby-version: "3.4" 46 | 47 | - uses: oxidize-rb/actions/cross-gem@v1 48 | id: cross-gem 49 | with: 50 | platform: ${{ matrix.ruby-platform }} 51 | ruby-versions: ${{ join(fromJSON(needs.ci-data.outputs.result).stable-ruby-versions, ',') }} 52 | 53 | - uses: actions/upload-artifact@v4 54 | with: 55 | name: cross-gem-${{ matrix.ruby-platform }} 56 | path: pkg/*-${{ matrix.ruby-platform }}.gem 57 | if-no-files-found: error 58 | 59 | - name: Smoke gem install 60 | if: matrix.ruby-platform == 'x86_64-linux' # GitHub actions architecture 61 | run: | 62 | gem install pkg/wasmtime-*.gem --verbose 63 | script="puts Wasmtime::Engine.new.precompile_module('(module)')" 64 | ruby -rwasmtime -e "$script" | grep wasmtime.info 65 | echo "✅ Successfully gem installed" 66 | 67 | release: 68 | name: Release 69 | needs: build 70 | runs-on: ubuntu-latest 71 | steps: 72 | - uses: actions/checkout@v4 73 | 74 | - uses: oxidize-rb/actions/setup-ruby-and-rust@v1 75 | with: 76 | ruby-version: "3.4" 77 | bundler-cache: true 78 | cargo-cache: true 79 | cache-version: v1 80 | 81 | - uses: actions/download-artifact@v4 82 | with: 83 | pattern: cross-gem-* 84 | merge-multiple: true 85 | path: pkg/ 86 | 87 | - name: Package source gem 88 | run: bundle exec rake pkg:ruby 89 | 90 | - name: Ensure version matches the tag 91 | run: | 92 | GEM_VERSION=$(grep VERSION lib/wasmtime/version.rb | head -n 1 | cut -d'"' -f2) 93 | if [ "v$GEM_VERSION" != "${{ github.ref_name }}" ]; then 94 | echo "Gem version does not match tag" 95 | echo " v$GEM_VERSION != ${{ github.ref_name }}" 96 | exit 1 97 | fi 98 | 99 | - name: Push Gem 100 | working-directory: pkg/ 101 | env: 102 | GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_KEY }} 103 | run: | 104 | mkdir -p $HOME/.gem 105 | touch $HOME/.gem/credentials 106 | chmod 0600 $HOME/.gem/credentials 107 | printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials 108 | ls -l 109 | for i in *.gem; do 110 | if [ -f "$i" ]; then 111 | if ! gem push "$i" >push.out; then 112 | gemerr=$? 113 | sed 's/^/::error:: /' push.out 114 | if ! grep -q "Repushing of gem" push.out; then 115 | exit $gemerr 116 | fi 117 | fi 118 | fi 119 | done 120 | 121 | - name: Create GitHub release 122 | uses: ncipollo/release-action@v1 123 | with: 124 | allowUpdates: true 125 | generateReleaseNotes: true 126 | draft: true 127 | omitBodyDuringUpdate: true 128 | omitNameDuringUpdate: true 129 | omitPrereleaseDuringUpdate: true 130 | skipIfReleaseExists: true 131 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | *.bundle 10 | *.so 11 | *.o 12 | *.a 13 | mkmf.log 14 | target/ 15 | 16 | # rspec failure tracking 17 | .rspec_status 18 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: standard 2 | 3 | inherit_gem: 4 | standard: config/base.yml 5 | 6 | AllCops: 7 | Exclude: 8 | - 'vendor/**/*' 9 | - 'pkg/**/*' 10 | - 'tmp/**/*' 11 | -------------------------------------------------------------------------------- /.standard.yml: -------------------------------------------------------------------------------- 1 | # For available configuration options, see: 2 | # https://github.com/testdouble/standard 3 | ruby_version: 3.0 4 | ignore: 5 | - 'vendor/**/*' 6 | - 'pkg/**/*' 7 | - 'tmp/**/*' 8 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --plugin rustdoc 2 | lib 3 | tmp/doc/wasmtime_rb.json 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | `wasmtime-rb` is a [Bytecode Alliance] project. It follows the Bytecode 4 | Alliance's [Code of Conduct] and [Organizational Code of Conduct]. 5 | 6 | ## Getting started 7 | 8 | Install dependencies: 9 | 10 | ``` 11 | bundle install 12 | ``` 13 | 14 | Compile the gem, run the tests & Ruby linter: 15 | 16 | ``` 17 | bundle exec rake 18 | ``` 19 | 20 | ## Releasing 21 | 22 | 1. Bump the `VERSION` in `lib/wasmtime/version.rb` 23 | 1. Run `bundle install` to bump the version in `Gemfile.lock` 24 | 1. Update the changelog (requires the `github_changelog_generator` gem and being authenticated with `gh`) 25 | 26 | ``` 27 | github_changelog_generator \ 28 | -u bytecodealliance \ 29 | -p wasmtime-rb \ 30 | -t $(gh auth token) \ 31 | --future-release v$(grep VERSION lib/wasmtime/version.rb | head -n 1 | cut -d'"' -f2) 32 | ``` 33 | 1. Commit your changes to the `main` branch and push them. Ensure you are not doing this on a fork of the repository. 34 | 1. Create a new tag for that release, prefixed with `v` (`git tag v1.0.0`): 35 | 36 | ``` 37 | git tag v$(grep VERSION lib/wasmtime/version.rb | head -n 1 | cut -d'"' -f2) 38 | git push --tags 39 | ``` 40 | 1. The release workflow will run and push a new version to RubyGems and create 41 | a new draft release on GitHub. Edit the release notes if needed, then 42 | mark the release as published when the release workflow succeeds. 43 | 44 | 45 | [Bytecode Alliance]: https://bytecodealliance.org/ 46 | [Code of Conduct]: https://github.com/bytecodealliance/wasmtime/blob/main/CODE_OF_CONDUCT.md 47 | [Organizational Code of Conduct]: https://github.com/bytecodealliance/wasmtime/blob/main/ORG_CODE_OF_CONDUCT.md 48 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["ext"] 4 | exclude = [ 5 | "examples/rust-crate", 6 | "spec/fixtures/component-types", 7 | "spec/fixtures/wasi-debug", 8 | "spec/fixtures/wasi-deterministic", 9 | ] 10 | 11 | [profile.release] 12 | codegen-units = 1 # more llvm optimizations 13 | debug = 2 # make perfomance engineers happy 14 | lto = "thin" # cross-crate inlining 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in wasmtime.gemspec 6 | gemspec 7 | 8 | group :development do 9 | gem "rake", "~> 13.2" 10 | gem "rake-compiler" 11 | gem "standard", "~> 1.50" 12 | gem "get_process_mem" 13 | gem "yard", require: false 14 | gem "yard-rustdoc", "~> 0.4.0", require: false 15 | gem "benchmark-ips", require: false 16 | gem "fiddle" 17 | end 18 | 19 | group :test do 20 | gem "rspec", "~> 3.13" 21 | end 22 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | wasmtime (33.0.0) 5 | rb_sys (~> 0.9.108) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | ast (2.4.3) 11 | benchmark-ips (2.14.0) 12 | bigdecimal (3.1.9) 13 | diff-lcs (1.6.2) 14 | ffi (1.17.1) 15 | ffi (1.17.1-aarch64-linux-gnu) 16 | ffi (1.17.1-aarch64-linux-musl) 17 | ffi (1.17.1-arm-linux-gnu) 18 | ffi (1.17.1-arm-linux-musl) 19 | ffi (1.17.1-arm64-darwin) 20 | ffi (1.17.1-x86-linux-gnu) 21 | ffi (1.17.1-x86-linux-musl) 22 | ffi (1.17.1-x86_64-darwin) 23 | ffi (1.17.1-x86_64-linux-gnu) 24 | ffi (1.17.1-x86_64-linux-musl) 25 | fiddle (1.1.8) 26 | get_process_mem (1.0.0) 27 | bigdecimal (>= 2.0) 28 | ffi (~> 1.0) 29 | json (2.12.2) 30 | language_server-protocol (3.17.0.5) 31 | lint_roller (1.1.0) 32 | parallel (1.27.0) 33 | parser (3.3.8.0) 34 | ast (~> 2.4.1) 35 | racc 36 | prettier_print (1.2.1) 37 | prism (1.4.0) 38 | racc (1.8.1) 39 | rainbow (3.1.1) 40 | rake (13.2.1) 41 | rake-compiler (1.3.0) 42 | rake 43 | rake-compiler-dock (1.9.1) 44 | rb_sys (0.9.111) 45 | rake-compiler-dock (= 1.9.1) 46 | regexp_parser (2.10.0) 47 | rspec (3.13.1) 48 | rspec-core (~> 3.13.0) 49 | rspec-expectations (~> 3.13.0) 50 | rspec-mocks (~> 3.13.0) 51 | rspec-core (3.13.4) 52 | rspec-support (~> 3.13.0) 53 | rspec-expectations (3.13.5) 54 | diff-lcs (>= 1.2.0, < 2.0) 55 | rspec-support (~> 3.13.0) 56 | rspec-mocks (3.13.5) 57 | diff-lcs (>= 1.2.0, < 2.0) 58 | rspec-support (~> 3.13.0) 59 | rspec-support (3.13.4) 60 | rubocop (1.75.8) 61 | json (~> 2.3) 62 | language_server-protocol (~> 3.17.0.2) 63 | lint_roller (~> 1.1.0) 64 | parallel (~> 1.10) 65 | parser (>= 3.3.0.2) 66 | rainbow (>= 2.2.2, < 4.0) 67 | regexp_parser (>= 2.9.3, < 3.0) 68 | rubocop-ast (>= 1.44.0, < 2.0) 69 | ruby-progressbar (~> 1.7) 70 | unicode-display_width (>= 2.4.0, < 4.0) 71 | rubocop-ast (1.45.0) 72 | parser (>= 3.3.7.2) 73 | prism (~> 1.4) 74 | rubocop-performance (1.25.0) 75 | lint_roller (~> 1.1) 76 | rubocop (>= 1.75.0, < 2.0) 77 | rubocop-ast (>= 1.38.0, < 2.0) 78 | ruby-progressbar (1.13.0) 79 | standard (1.50.0) 80 | language_server-protocol (~> 3.17.0.2) 81 | lint_roller (~> 1.0) 82 | rubocop (~> 1.75.5) 83 | standard-custom (~> 1.0.0) 84 | standard-performance (~> 1.8) 85 | standard-custom (1.0.2) 86 | lint_roller (~> 1.0) 87 | rubocop (~> 1.50) 88 | standard-performance (1.8.0) 89 | lint_roller (~> 1.1) 90 | rubocop-performance (~> 1.25.0) 91 | syntax_tree (6.2.0) 92 | prettier_print (>= 1.2.0) 93 | unicode-display_width (3.1.4) 94 | unicode-emoji (~> 4.0, >= 4.0.4) 95 | unicode-emoji (4.0.4) 96 | yard (0.9.37) 97 | yard-rustdoc (0.4.1) 98 | syntax_tree (~> 6.0) 99 | yard (~> 0.9) 100 | 101 | PLATFORMS 102 | aarch64-linux-gnu 103 | aarch64-linux-musl 104 | arm-linux-gnu 105 | arm-linux-musl 106 | arm64-darwin 107 | ruby 108 | x86-linux-gnu 109 | x86-linux-musl 110 | x86_64-darwin 111 | x86_64-linux-gnu 112 | x86_64-linux-musl 113 | 114 | DEPENDENCIES 115 | benchmark-ips 116 | fiddle 117 | get_process_mem 118 | rake (~> 13.2) 119 | rake-compiler 120 | rspec (~> 3.13) 121 | standard (~> 1.50) 122 | wasmtime! 123 | yard 124 | yard-rustdoc (~> 0.4.0) 125 | 126 | BUNDLED WITH 127 | 2.5.7 128 | -------------------------------------------------------------------------------- /NIGHTLY_VERSION: -------------------------------------------------------------------------------- 1 | nightly-2025-03-04 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

wasmtime-rb

3 | 4 |

5 | Ruby embedding of 6 | Wasmtime 7 |

8 | 9 | A Bytecode Alliance project 10 | 11 |

12 | 13 | CI status 14 | 15 |

16 |
17 | 18 | ## Goal 19 | 20 | `wasmtime-rb`'s goal is to expose the full power of Wasmtime in Ruby with 21 | minimal overhead, serving as a foundation layer for other projects or gems. 22 | 23 | ## Installation 24 | 25 | Add the `wasmtime` gem to your Gemfile and run `bundle install`: 26 | 27 | ```ruby 28 | gem "wasmtime" 29 | ``` 30 | 31 | Alternatively, you can install the gem manually: 32 | 33 | ```sh 34 | gem install wasmtime 35 | ``` 36 | 37 | ### Precompiled gems 38 | 39 | We recommend installing the `wasmtime` precompiled gems available for Linux, macOS, and Windows. Installing a precompiled gem avoids the need to compile from source code, which is generally slower and less reliable. 40 | 41 | When installing the `wasmtime` gem for the first time using `bundle install`, Bundler will automatically download the precompiled gem for your current platform. However, you will need to inform Bundler of any additional platforms you plan to use. 42 | 43 | To do this, lock your Bundle to the required platforms you will need from the list of supported platforms below: 44 | 45 | ```sh 46 | bundle lock --add-platform x86_64-linux # Standard Linux (e.g. Heroku, GitHub Actions, etc.) 47 | bundle lock --add-platform x86_64-linux-musl # MUSL Linux deployments (i.e. Alpine Linux) 48 | bundle lock --add-platform aarch64-linux # ARM64 Linux deployments (i.e. AWS Graviton2) 49 | bundle lock --add-platform x86_64-darwin # Intel MacOS (i.e. pre-M1) 50 | bundle lock --add-platform arm64-darwin # Apple Silicon MacOS (i.e. M1) 51 | ``` 52 | 53 | ## Usage 54 | 55 | Example usage: 56 | 57 | ```ruby 58 | require "wasmtime" 59 | 60 | # Create an engine. Generally, you only need a single engine and can 61 | # re-use it throughout your program. 62 | engine = Wasmtime::Engine.new 63 | 64 | # Compile a Wasm module from either Wasm or WAT. The compiled module is 65 | # specific to the Engine's configuration. 66 | mod = Wasmtime::Module.new(engine, <<~WAT) 67 | (module 68 | (func $hello (import "" "hello")) 69 | (func (export "run") (call $hello)) 70 | ) 71 | WAT 72 | 73 | # Create a store. Store can keep state to be re-used in Funcs. 74 | store = Wasmtime::Store.new(engine, {count: 0}) 75 | 76 | # Define a Wasm function from Ruby code. 77 | func = Wasmtime::Func.new(store, [], []) do |caller| 78 | puts "Hello from Func!" 79 | caller.store_data[:count] += 1 80 | puts "Ran #{caller.store_data[:count]} time(s)" 81 | end 82 | 83 | # Build the Wasm instance by providing its imports. 84 | instance = Wasmtime::Instance.new(store, mod, [func]) 85 | 86 | # Run the `run` export. 87 | instance.invoke("run") 88 | 89 | # Or: get the `run` export and call it. 90 | instance.export("run").to_func.call 91 | ``` 92 | 93 | For more, see [examples](https://github.com/bytecodealliance/wasmtime-rb/tree/main/examples) 94 | or the [API documentation](https://bytecodealliance.github.io/wasmtime-rb/latest/). 95 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "standard/rake" 4 | 5 | GEMSPEC = Gem::Specification.load("wasmtime.gemspec") 6 | 7 | task build: "pkg:ruby" 8 | 9 | task default: %w[env:dev compile spec standard] 10 | -------------------------------------------------------------------------------- /bench/bench.rb: -------------------------------------------------------------------------------- 1 | require "benchmark/ips" 2 | require "wasmtime" 3 | 4 | module Bench 5 | extend(self) 6 | 7 | def ips 8 | Benchmark.ips do |x| 9 | yield(x) 10 | 11 | x.config(time: 0, warmup: 0) if ENV["CI"] 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /bench/compile.rb: -------------------------------------------------------------------------------- 1 | require_relative "bench" 2 | 3 | Bench.ips do |x| 4 | engine = Wasmtime::Engine.new 5 | 6 | x.report("empty module") do 7 | Wasmtime::Module.new(engine, "(module)") 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /bench/component_id.rb: -------------------------------------------------------------------------------- 1 | require_relative "bench" 2 | 3 | Bench.ips do |x| 4 | engine = Wasmtime::Engine.new 5 | linker = Wasmtime::Component::Linker.new(engine) 6 | component = Wasmtime::Component::Component.from_file(engine, "spec/fixtures/component_types.wasm") 7 | store = Wasmtime::Store.new(engine) 8 | instance = linker.instantiate(store, component) 9 | id_record = instance.get_func("id-record") 10 | id_u32 = instance.get_func("id-u32") 11 | 12 | point_record = {"x" => 1, "y" => 2} 13 | 14 | x.report("identity point record") do 15 | id_record.call(point_record) 16 | end 17 | 18 | x.report("identity u32") do 19 | id_u32.call(10) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /bench/func_call.rb: -------------------------------------------------------------------------------- 1 | require_relative "bench" 2 | 3 | Bench.ips do |x| 4 | engine = Wasmtime::Engine.new 5 | linker = Wasmtime::Linker.new(engine) 6 | mod = Wasmtime::Module.from_file(engine, "examples/gcd.wat") 7 | store = Wasmtime::Store.new(engine) 8 | instance = linker.instantiate(store, mod) 9 | func = instance.export("gcd").to_func 10 | 11 | x.report("Instance#invoke") do 12 | instance.invoke("gcd", 5, 1) 13 | end 14 | 15 | x.report("Func#call") do 16 | func.call(5, 1) 17 | end 18 | 19 | x.compare! 20 | end 21 | -------------------------------------------------------------------------------- /bench/host_call.rb: -------------------------------------------------------------------------------- 1 | require_relative "bench" 2 | 3 | Bench.ips do |x| 4 | engine = Wasmtime::Engine.new 5 | [4, 16, 64, 128, 256].each do |n| 6 | result_type_wat = Array.new(n) { |_| :i32 }.join(" ") 7 | mod = Wasmtime::Module.new(engine, <<~WAT) 8 | (module 9 | (import "host" "succ" (func (param i32) (result #{result_type_wat}))) 10 | (export "run" (func 0))) 11 | WAT 12 | linker = Wasmtime::Linker.new(engine) 13 | results = Array.new(n) { |_| :i32 } 14 | result_array = Array.new(n) { |i| i } 15 | linker.func_new("host", "succ", [:i32], results) do |_caller, arg1| 16 | result_array 17 | end 18 | 19 | x.report("Call host func (#{n} args)") do 20 | store = Wasmtime::Store.new(engine) 21 | linker.instantiate(store, mod).invoke("run", 101) 22 | end 23 | end 24 | 25 | x.compare! 26 | end 27 | -------------------------------------------------------------------------------- /bench/instantiate.rb: -------------------------------------------------------------------------------- 1 | require_relative "bench" 2 | 3 | Bench.ips do |x| 4 | engine = Wasmtime::Engine.new 5 | linker = Wasmtime::Linker.new(engine) 6 | mod = Wasmtime::Module.new(engine, "(module)") 7 | 8 | x.report("Linker#instantiate") do 9 | store = Wasmtime::Store.new(engine) 10 | linker.instantiate(store, mod) 11 | end 12 | 13 | x.report("Instance#new") do 14 | store = Wasmtime::Store.new(engine) 15 | Wasmtime::Instance.new(store, mod) 16 | end 17 | 18 | x.compare! 19 | end 20 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "wasmtime" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require "irb" 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /examples/epoch.rb: -------------------------------------------------------------------------------- 1 | require "wasmtime" 2 | 3 | engine = Wasmtime::Engine.new(epoch_interruption: true) 4 | # Re-use fibonacci function from the Fuel example 5 | mod = Wasmtime::Module.from_file(engine, "examples/fuel.wat") 6 | store = Wasmtime::Store.new(engine) 7 | instance = Wasmtime::Instance.new(store, mod) 8 | 9 | # Store starts with an epoch deadline of 0, meaning Wasm execution 10 | # will be halted right away. 11 | puts "Running Wasm with default epoch deadline of 0..." 12 | begin 13 | instance.invoke("fibonacci", 5) 14 | raise "Unexpected: Wasm executed past deadline" 15 | rescue Wasmtime::Trap => trap 16 | puts " Wasm trap, code: #{trap.code.inspect}" 17 | puts 18 | end 19 | 20 | # Epoch deadline is manipulated with `Store#set_epoch_deadline`. 21 | store.set_epoch_deadline(1) 22 | puts "Running Wasm with default epoch deadline of 1..." 23 | puts " result: #{instance.invoke("fibonacci", 5)}" 24 | puts 25 | 26 | # The engine's epoch can be incremented manually with `Engine#increment_epoch`. 27 | engine.increment_epoch 28 | puts "Running Wasm after incrementing epoch past the store's deadline..." 29 | begin 30 | instance.invoke("fibonacci", 5) 31 | raise "Unexpected: Wasm executed past deadline" 32 | rescue Wasmtime::Trap => trap 33 | puts " Wasm trap, code: #{trap.code.inspect}" 34 | puts 35 | end 36 | 37 | # The engine provides a method to increment epoch based on a timer. 38 | # This is done with native thread because Wasm execution does not release 39 | # Ruby's Global VM lock. 40 | puts "Setting the store's deadline to be 2 ticks from current epoch..." 41 | store.set_epoch_deadline(2) 42 | 43 | puts "Incrementing epoch interval every 100ms..." 44 | engine.start_epoch_interval(100) 45 | start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) 46 | puts "Computing fibonacci of 100..." 47 | begin 48 | instance.invoke("fibonacci", 100) 49 | raise "Unexpected: computed fibonacci of 100 in 200ms" 50 | rescue Wasmtime::Trap => _ 51 | elapsed_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000 52 | # This should be around 200ms 53 | puts " Wasm trapped after #{elapsed_ms.round}ms" 54 | end 55 | -------------------------------------------------------------------------------- /examples/externref.rb: -------------------------------------------------------------------------------- 1 | require "wasmtime" 2 | 3 | engine = Wasmtime::Engine.new 4 | mod = Wasmtime::Module.from_file(engine, "examples/externref.wat") 5 | store = Wasmtime::Store.new(engine) 6 | instance = Wasmtime::Instance.new(store, mod) 7 | 8 | # externrefs can be any Ruby object, here we're using a String. 9 | hello_world = +"Hello World" 10 | 11 | puts "Set and get from table..." 12 | table = instance.export("table").to_table 13 | table.set(3, hello_world) 14 | puts " get: #{table.get(3).inspect}" # => Hello World 15 | puts " same Ruby object: #{table.get(3).eql?(hello_world).inspect}" # => true 16 | puts 17 | 18 | puts "Set and get from global..." 19 | global = instance.export("global").to_global 20 | global.set(hello_world) 21 | puts " get: #{global.get.inspect}" # => "Hello World" 22 | puts 23 | 24 | puts "Return an externref from a function..." 25 | func = instance.export("func").to_func 26 | puts " return: #{func.call(hello_world).inspect}" # => "Hello World" 27 | puts 28 | 29 | puts "nil is a valid externref..." 30 | puts " nil roundtrip: #{func.call(nil).inspect}" # => nil 31 | puts 32 | 33 | puts "nil is also a 'null reference' externref..." 34 | puts " null externref: #{table.get(6).inspect}" # => nil 35 | -------------------------------------------------------------------------------- /examples/externref.wat: -------------------------------------------------------------------------------- 1 | (module 2 | (table $table (export "table") 10 externref) 3 | 4 | (global $global (export "global") (mut externref) (ref.null extern)) 5 | 6 | (func (export "func") (param externref) (result externref) 7 | local.get 0 8 | ) 9 | ) 10 | -------------------------------------------------------------------------------- /examples/fuel.rb: -------------------------------------------------------------------------------- 1 | require "wasmtime" 2 | 3 | engine = Wasmtime::Engine.new(consume_fuel: true) 4 | mod = Wasmtime::Module.from_file(engine, "examples/fuel.wat") 5 | 6 | store = Wasmtime::Store.new(engine) 7 | store.set_fuel(10_000) 8 | 9 | instance = Wasmtime::Instance.new(store, mod) 10 | 11 | begin 12 | (1..).each do |i| 13 | initial_fuel = store.get_fuel 14 | result = instance.invoke("fibonacci", i) 15 | fuel_consumed = initial_fuel - store.get_fuel 16 | puts "fib(#{i}) = #{result} [consumed #{fuel_consumed} fuel]" 17 | end 18 | rescue Wasmtime::Trap => trap 19 | puts 20 | puts "Wasm trap, code: #{trap.code.inspect}" 21 | end 22 | -------------------------------------------------------------------------------- /examples/fuel.wat: -------------------------------------------------------------------------------- 1 | (module 2 | (func $fibonacci (param $n i32) (result i32) 3 | (if 4 | (i32.lt_s (local.get $n) (i32.const 2)) 5 | (then 6 | (return (local.get $n)) 7 | ) 8 | ) 9 | (i32.add 10 | (call $fibonacci (i32.sub (local.get $n) (i32.const 1))) 11 | (call $fibonacci (i32.sub (local.get $n) (i32.const 2))) 12 | ) 13 | ) 14 | (export "fibonacci" (func $fibonacci)) 15 | ) 16 | -------------------------------------------------------------------------------- /examples/gcd.rb: -------------------------------------------------------------------------------- 1 | require "wasmtime" 2 | 3 | engine = Wasmtime::Engine.new 4 | mod = Wasmtime::Module.from_file(engine, "examples/gcd.wat") 5 | store = Wasmtime::Store.new(engine) 6 | instance = Wasmtime::Instance.new(store, mod) 7 | 8 | puts "gcd(6, 27) = #{instance.invoke("gcd", 6, 27)}" 9 | -------------------------------------------------------------------------------- /examples/gcd.wat: -------------------------------------------------------------------------------- 1 | (module 2 | (func $gcd (param i32 i32) (result i32) 3 | (local i32) 4 | block ;; label = @1 5 | block ;; label = @2 6 | local.get 0 7 | br_if 0 (;@2;) 8 | local.get 1 9 | local.set 2 10 | br 1 (;@1;) 11 | end 12 | loop ;; label = @2 13 | local.get 1 14 | local.get 0 15 | local.tee 2 16 | i32.rem_u 17 | local.set 0 18 | local.get 2 19 | local.set 1 20 | local.get 0 21 | br_if 0 (;@2;) 22 | end 23 | end 24 | local.get 2 25 | ) 26 | (export "gcd" (func $gcd)) 27 | ) 28 | -------------------------------------------------------------------------------- /examples/hello.rb: -------------------------------------------------------------------------------- 1 | require "wasmtime" 2 | 3 | class MyData 4 | attr_reader :count 5 | 6 | def initialize 7 | @count = 0 8 | end 9 | 10 | def increment! 11 | @count += 1 12 | end 13 | end 14 | 15 | data = MyData.new 16 | engine = Wasmtime::Engine.new 17 | mod = Wasmtime::Module.from_file(engine, "examples/hello.wat") 18 | store = Wasmtime::Store.new(engine, data) 19 | func = Wasmtime::Func.new(store, [], []) do |caller| 20 | puts "Hello from Func!" 21 | caller.store_data.increment! 22 | end 23 | 24 | instance = Wasmtime::Instance.new(store, mod, [func]) 25 | instance.invoke("run") 26 | 27 | puts "Store's count: #{store.data.count}" 28 | -------------------------------------------------------------------------------- /examples/hello.wat: -------------------------------------------------------------------------------- 1 | (module 2 | (func $hello (import "" "hello")) 3 | (func (export "run") (call $hello)) 4 | ) 5 | -------------------------------------------------------------------------------- /examples/linking.rb: -------------------------------------------------------------------------------- 1 | require "wasmtime" 2 | 3 | engine = Wasmtime::Engine.new 4 | 5 | # Create a linker to link modules together. We want to use WASI with 6 | # the linker, so we pass in `wasi: true`. 7 | linker = Wasmtime::Linker.new(engine, wasi: true) 8 | 9 | mod1 = Wasmtime::Module.from_file(engine, "examples/linking1.wat") 10 | mod2 = Wasmtime::Module.from_file(engine, "examples/linking2.wat") 11 | 12 | wasi_ctx_builder = Wasmtime::WasiCtxBuilder.new 13 | .inherit_stdin 14 | .inherit_stdout 15 | .build 16 | 17 | store = Wasmtime::Store.new(engine, wasi_ctx: wasi_ctx_builder) 18 | 19 | # Instantiate `mod2` which only uses WASI, then register 20 | # that instance with the linker so `mod1` can use it. 21 | instance2 = linker.instantiate(store, mod2) 22 | linker.instance(store, "linking2", instance2) 23 | 24 | # Perform the final link and execute mod1's "run" function. 25 | instance1 = linker.instantiate(store, mod1) 26 | instance1.invoke("run") 27 | -------------------------------------------------------------------------------- /examples/linking1.wat: -------------------------------------------------------------------------------- 1 | (module 2 | (import "linking2" "double" (func $double (param i32) (result i32))) 3 | (import "linking2" "log" (func $log (param i32 i32))) 4 | (import "linking2" "memory" (memory 1)) 5 | (import "linking2" "memory_offset" (global $offset i32)) 6 | 7 | (func (export "run") 8 | ;; Call into the other module to double our number, and we could print it 9 | ;; here but for now we just drop it 10 | i32.const 2 11 | call $double 12 | drop 13 | 14 | ;; Our `data` segment initialized our imported memory, so let's print the 15 | ;; string there now. 16 | global.get $offset 17 | i32.const 14 18 | call $log 19 | ) 20 | 21 | (data (global.get $offset) "Hello, world!\n") 22 | ) 23 | -------------------------------------------------------------------------------- /examples/linking2.wat: -------------------------------------------------------------------------------- 1 | (module 2 | (type $fd_write_ty (func (param i32 i32 i32 i32) (result i32))) 3 | (import "wasi_snapshot_preview1" "fd_write" (func $fd_write (type $fd_write_ty))) 4 | 5 | (func (export "double") (param i32) (result i32) 6 | local.get 0 7 | i32.const 2 8 | i32.mul 9 | ) 10 | 11 | (func (export "log") (param i32 i32) 12 | ;; store the pointer in the first iovec field 13 | i32.const 4 14 | local.get 0 15 | i32.store 16 | 17 | ;; store the length in the first iovec field 18 | i32.const 4 19 | local.get 1 20 | i32.store offset=4 21 | 22 | ;; call the `fd_write` import 23 | i32.const 1 ;; stdout fd 24 | i32.const 4 ;; iovs start 25 | i32.const 1 ;; number of iovs 26 | i32.const 0 ;; where to write nwritten bytes 27 | call $fd_write 28 | drop 29 | ) 30 | 31 | (memory (export "memory") 2) 32 | (global (export "memory_offset") i32 (i32.const 65536)) 33 | ) 34 | -------------------------------------------------------------------------------- /examples/memory.rb: -------------------------------------------------------------------------------- 1 | require "wasmtime" 2 | 3 | engine = Wasmtime::Engine.new 4 | mod = Wasmtime::Module.from_file(engine, "examples/memory.wat") 5 | store = Wasmtime::Store.new(engine) 6 | instance = Wasmtime::Instance.new(store, mod) 7 | memory = instance.export("memory").to_memory 8 | size_fn = instance.export("size").to_func 9 | load_fn = instance.export("load").to_func 10 | store_fn = instance.export("store").to_func 11 | 12 | puts "Checking memory..." 13 | puts " size: #{memory.size}" # => 2 14 | puts " read(0, 1): #{memory.read(0, 1).inspect}" # => "\x00" 15 | puts 16 | puts " size_fn.call: #{size_fn.call}" # => 2 17 | puts " load_fn.call(0): #{load_fn.call(0)}" # => 0 18 | puts 19 | 20 | puts "Reading out of bounds..." 21 | begin 22 | load_fn.call(0x20000) 23 | rescue Wasmtime::Trap => error 24 | puts " Trap! code: #{error.code.inspect}" 25 | end 26 | puts 27 | 28 | puts "Mutating memory..." 29 | memory.write(0x1002, "\x06") 30 | puts " load_fn.call(0x1002): #{load_fn.call(0x1002).inspect}" # => 6 31 | store_fn.call(0x1003, 7) 32 | puts " load_fn.call(0x1003): #{load_fn.call(0x1003).inspect}" # => 7 33 | puts 34 | 35 | puts "Growing memory..." 36 | memory.grow(1) 37 | puts " new size: #{memory.size}" # => 3 38 | puts 39 | 40 | puts "Creating stand-alone memory..." 41 | memory = Wasmtime::Memory.new(store, min_size: 5, max_size: 5) # size in pages 42 | puts " size: #{memory.size}" # => 5 43 | puts 44 | puts "Growing beyond limit fails..." 45 | begin 46 | memory.grow(1) 47 | rescue Wasmtime::Error => error 48 | puts " exception: #{error.inspect}" 49 | end 50 | -------------------------------------------------------------------------------- /examples/memory.wat: -------------------------------------------------------------------------------- 1 | (module 2 | (memory (export "memory") 2 3) 3 | 4 | (func (export "size") (result i32) (memory.size)) 5 | (func (export "load") (param i32) (result i32) 6 | (i32.load8_s (local.get 0)) 7 | ) 8 | (func (export "store") (param i32 i32) 9 | (i32.store8 (local.get 0) (local.get 1)) 10 | ) 11 | 12 | (data (i32.const 0x1000) "\01\02\03\04") 13 | ) 14 | -------------------------------------------------------------------------------- /examples/multi.rb: -------------------------------------------------------------------------------- 1 | require "wasmtime" 2 | 3 | engine = Wasmtime::Engine.new 4 | mod = Wasmtime::Module.from_file(engine, "examples/multi.wat") 5 | store = Wasmtime::Store.new(engine) 6 | 7 | # Host call with multiple params and results 8 | callback = Wasmtime::Func.new(store, [:i32, :i64], [:i64, :i32]) do |_caller, a, b| 9 | # Return an array with 2 elements for 2 results 10 | [b + 1, a + 1] 11 | end 12 | 13 | instance = Wasmtime::Instance.new(store, mod, [callback]) 14 | 15 | g = instance.export("g").to_func 16 | 17 | puts "Calling export 'g'..." 18 | result = g.call(1, 3) # => [2, 4] 19 | puts " g result: #{result.inspect}" 20 | puts 21 | 22 | round_trip_many = instance.export("round_trip_many").to_func 23 | 24 | puts "Calling export 'round_trip_many'..." 25 | result = round_trip_many.call(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) # => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 26 | puts " round_trip_many result: #{result.inspect}" 27 | -------------------------------------------------------------------------------- /examples/multi.wat: -------------------------------------------------------------------------------- 1 | (module 2 | (func $f (import "" "f") (param i32 i64) (result i64 i32)) 3 | 4 | (func $g (export "g") (param i32 i64) (result i64 i32) 5 | (call $f (local.get 0) (local.get 1)) 6 | ) 7 | 8 | (func $round_trip_many 9 | (export "round_trip_many") 10 | (param i64 i64 i64 i64 i64 i64 i64 i64 i64 i64) 11 | (result i64 i64 i64 i64 i64 i64 i64 i64 i64 i64) 12 | 13 | local.get 0 14 | local.get 1 15 | local.get 2 16 | local.get 3 17 | local.get 4 18 | local.get 5 19 | local.get 6 20 | local.get 7 21 | local.get 8 22 | local.get 9) 23 | ) 24 | -------------------------------------------------------------------------------- /examples/rust-crate/.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | -------------------------------------------------------------------------------- /examples/rust-crate/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-crate" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | doctest = false 8 | 9 | [dependencies] 10 | wasmtime-rb = { path = "../../ext", features = ["ruby-api"] } 11 | 12 | [dev-dependencies] 13 | magnus = { version = "0.7.1", features = ["embed"] } # Only need embed feature for tests 14 | wasmtime-rb = { path = "../../ext", features = ["embed"] } # Only need embed feature for tests 15 | -------------------------------------------------------------------------------- /examples/rust-crate/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub fn create_an_engine() -> wasmtime_rb::Engine { 2 | wasmtime_rb::Engine::new(&[]).unwrap() 3 | } 4 | 5 | #[cfg(test)] 6 | mod tests { 7 | use super::*; 8 | 9 | #[test] 10 | fn it_works() { 11 | let _cleanup = unsafe { magnus::embed::init() }; 12 | let engine = create_an_engine(); 13 | 14 | assert!(engine.get().precompile_module(b"(module)").is_ok()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/wasi.rb: -------------------------------------------------------------------------------- 1 | require "wasmtime" 2 | 3 | engine = Wasmtime::Engine.new 4 | mod = Wasmtime::Module.from_file(engine, "spec/fixtures/wasi-debug.wasm") 5 | 6 | linker = Wasmtime::Linker.new(engine, wasi: true) 7 | 8 | wasi_ctx = Wasmtime::WasiCtxBuilder.new 9 | .set_stdin_string("hi!") 10 | .inherit_stdout 11 | .inherit_stderr 12 | .set_argv(ARGV) 13 | .set_env(ENV) 14 | .build 15 | store = Wasmtime::Store.new(engine, wasi_ctx: wasi_ctx) 16 | 17 | instance = linker.instantiate(store, mod) 18 | instance.invoke("_start") 19 | -------------------------------------------------------------------------------- /ext/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasmtime-rb" 3 | version = "9.0.4" 4 | edition = "2021" 5 | authors = ["The Wasmtime Project Developers"] 6 | license = "Apache-2.0" 7 | publish = false 8 | build = "build.rs" 9 | 10 | [lints.rust] 11 | unexpected_cfgs = { level = "warn", check-cfg = ['cfg(ruby_gte_3_0)'] } 12 | 13 | [features] 14 | default = ["tokio", "all-arch", "winch"] 15 | embed = ["magnus/embed"] 16 | tokio = ["dep:tokio", "dep:async-timer"] 17 | all-arch = ["wasmtime/all-arch"] 18 | ruby-api = [] 19 | winch = ["wasmtime/winch"] 20 | 21 | [dependencies] 22 | lazy_static = "1.5.0" 23 | magnus = { version = "0.7", features = ["rb-sys"] } 24 | rb-sys = { version = "*", default-features = false, features = [ 25 | "stable-api-compiled-fallback", 26 | ] } 27 | wasmtime = { version = "=33.0.0", features = ["memory-protection-keys"] } 28 | wasmtime-wasi = "=33.0.0" 29 | wasi-common = "=33.0.0" 30 | cap-std = "3.4.0" 31 | wat = "1.227.1" 32 | tokio = { version = "1.40.0", features = [ 33 | "rt", 34 | "rt-multi-thread", 35 | "time", 36 | "net", 37 | ], optional = true } 38 | async-timer = { version = "1.0.0-beta.15", features = [ 39 | "tokio1", 40 | ], optional = true } 41 | static_assertions = "1.1.0" 42 | wasmtime-environ = "=33.0.0" 43 | deterministic-wasi-ctx = { version = "=1.2.1", features = ["wasi-common"] } 44 | 45 | [build-dependencies] 46 | rb-sys-env = "0.2.2" 47 | -------------------------------------------------------------------------------- /ext/build.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env, 3 | error::Error, 4 | fs::{create_dir_all, File}, 5 | io::{Read, Write}, 6 | path::PathBuf, 7 | }; 8 | 9 | fn main() -> Result<(), Box> { 10 | // Propogate linking from rb-sys for usage in the wasmtime-rb Rust crate 11 | let _ = rb_sys_env::activate()?; 12 | 13 | bundle_ruby_file("lib/wasmtime/error.rb")?; 14 | bundle_ruby_file("lib/wasmtime/component.rb")?; 15 | 16 | Ok(()) 17 | } 18 | 19 | fn bundle_ruby_file(filename: &str) -> Result<(), Box> { 20 | let out_dir = PathBuf::from(env::var("OUT_DIR")?).join("bundled"); 21 | let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?); 22 | let file = manifest_dir.join("..").join(filename); 23 | println!("cargo:rerun-if-changed={}", file.display()); 24 | let out_path = file.file_name().unwrap(); 25 | let out_path = out_path.to_string_lossy().replace(".rb", ".rs"); 26 | let out_path = out_dir.join(out_path); 27 | create_dir_all(out_dir)?; 28 | let mut out = File::create(out_path)?; 29 | 30 | let contents = { 31 | let mut file = File::open(file)?; 32 | let mut contents = String::new(); 33 | file.read_to_string(&mut contents)?; 34 | contents 35 | }; 36 | 37 | let template = r##" 38 | // This file is generated by build.rs 39 | 40 | use magnus::eval; 41 | 42 | pub fn init() -> Result<(), magnus::Error> { 43 | let _: magnus::Value = eval!( 44 | r#" 45 | __FILE_CONTENTS__ 46 | "# 47 | )?; 48 | 49 | Ok(()) 50 | } 51 | 52 | "##; 53 | 54 | let contents = template.replace("__FILE_CONTENTS__", &contents); 55 | 56 | out.write_all(contents.as_bytes())?; 57 | 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /ext/extconf.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "mkmf" 4 | require "rb_sys/mkmf" 5 | 6 | create_rust_makefile("wasmtime/wasmtime_rb") do |ext| 7 | ext.extra_cargo_args += ["--crate-type", "cdylib"] 8 | ext.extra_cargo_args += ["--package", "wasmtime-rb"] 9 | ext.extra_rustflags = ["--cfg=rustix_use_libc"] 10 | end 11 | -------------------------------------------------------------------------------- /ext/src/helpers/macros.rs: -------------------------------------------------------------------------------- 1 | /// A macro to define a new `Id` const for a given string. 2 | #[macro_export] 3 | macro_rules! define_rb_intern { 4 | ($($name:ident => $id:expr,)*) => { 5 | $( 6 | lazy_static::lazy_static! { 7 | /// Define a Ruby internal `Id`. Equivalent to `rb_intern("$name")` 8 | pub static ref $name: $crate::helpers::StaticId = $crate::helpers::StaticId::intern_str($id); 9 | } 10 | )* 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /ext/src/helpers/mod.rs: -------------------------------------------------------------------------------- 1 | mod macros; 2 | mod nogvl; 3 | mod output_limited_buffer; 4 | mod static_id; 5 | mod symbol_enum; 6 | mod tmplock; 7 | 8 | pub use nogvl::nogvl; 9 | pub use output_limited_buffer::OutputLimitedBuffer; 10 | pub use static_id::StaticId; 11 | pub use symbol_enum::SymbolEnum; 12 | pub use tmplock::Tmplock; 13 | -------------------------------------------------------------------------------- /ext/src/helpers/nogvl.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::c_void, mem::MaybeUninit, ptr::null_mut}; 2 | 3 | use rb_sys::rb_thread_call_without_gvl; 4 | 5 | unsafe extern "C" fn call_without_gvl(arg: *mut c_void) -> *mut c_void 6 | where 7 | F: FnMut() -> R, 8 | R: Sized, 9 | { 10 | let arg = arg as *mut (&mut F, &mut MaybeUninit); 11 | let (func, result) = unsafe { &mut *arg }; 12 | result.write(func()); 13 | 14 | null_mut() 15 | } 16 | 17 | pub fn nogvl(mut func: F) -> R 18 | where 19 | F: FnMut() -> R, 20 | R: Sized, 21 | { 22 | let result = MaybeUninit::uninit(); 23 | let arg_ptr = &(&mut func, &result) as *const _ as *mut c_void; 24 | 25 | unsafe { 26 | rb_thread_call_without_gvl(Some(call_without_gvl::), arg_ptr, None, null_mut()); 27 | result.assume_init() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ext/src/helpers/output_limited_buffer.rs: -------------------------------------------------------------------------------- 1 | use magnus::{ 2 | value::{InnerValue, Opaque, ReprValue}, 3 | RString, Ruby, 4 | }; 5 | use std::io; 6 | use std::io::ErrorKind; 7 | 8 | /// A buffer that limits the number of bytes that can be written to it. 9 | /// If the buffer is full, it will truncate the data. 10 | /// Is used in the buffer implementations of stdout and stderr in `WasiCtx` and `WasiCtxBuilder`. 11 | pub struct OutputLimitedBuffer { 12 | buffer: Opaque, 13 | /// The maximum number of bytes that can be written to the output stream buffer. 14 | capacity: usize, 15 | } 16 | 17 | impl OutputLimitedBuffer { 18 | #[must_use] 19 | pub fn new(buffer: Opaque, capacity: usize) -> Self { 20 | Self { buffer, capacity } 21 | } 22 | } 23 | 24 | impl io::Write for OutputLimitedBuffer { 25 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 26 | // Append a buffer to the string and truncate when hitting the capacity. 27 | // We return the input buffer size regardless of whether we truncated or not to avoid a panic. 28 | let ruby = Ruby::get().unwrap(); 29 | 30 | let mut inner_buffer = self.buffer.get_inner_with(&ruby); 31 | 32 | // Handling frozen case here is necessary because magnus does not check if a string is frozen before writing to it. 33 | let is_frozen = inner_buffer.as_value().is_frozen(); 34 | if is_frozen { 35 | return Err(io::Error::new( 36 | ErrorKind::WriteZero, 37 | "Cannot write to a frozen buffer.", 38 | )); 39 | } 40 | 41 | if buf.is_empty() { 42 | return Ok(0); 43 | } 44 | 45 | if inner_buffer 46 | .len() 47 | .checked_add(buf.len()) 48 | .is_some_and(|val| val < self.capacity) 49 | { 50 | let amount_written = inner_buffer.write(buf)?; 51 | if amount_written < buf.len() { 52 | return Ok(amount_written); 53 | } 54 | } else { 55 | let portion = self.capacity - inner_buffer.len(); 56 | let amount_written = inner_buffer.write(&buf[0..portion])?; 57 | if amount_written < portion { 58 | return Ok(amount_written); 59 | } 60 | }; 61 | 62 | Ok(buf.len()) 63 | } 64 | 65 | fn flush(&mut self) -> io::Result<()> { 66 | let ruby = Ruby::get().unwrap(); 67 | 68 | self.buffer.get_inner_with(&ruby).flush() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /ext/src/helpers/static_id.rs: -------------------------------------------------------------------------------- 1 | use magnus::rb_sys::{AsRawId, FromRawId}; 2 | use magnus::{ 3 | value::{Id, IntoId}, 4 | Ruby, Symbol, 5 | }; 6 | use std::convert::TryInto; 7 | use std::num::NonZeroUsize; 8 | 9 | /// A static `Id` that can be used to refer to a Ruby ID. 10 | /// 11 | /// Use `define_rb_intern!` to define it so that it will be cached in a global variable. 12 | /// 13 | /// Magnus' `Id` can't be used for this purpose since it is not `Sync`, so cannot 14 | /// be used as a global variable with `lazy_static` in `define_rb_intern!`. 15 | /// See [this commit on the Magnus repo][commit]. 16 | /// 17 | /// [commit]: https://github.com/matsadler/magnus/commit/1a1c1ee874e15b0b222f7aae68bb9b5360072e57 18 | #[derive(Clone, Copy)] 19 | #[repr(transparent)] 20 | pub struct StaticId(NonZeroUsize); 21 | 22 | impl StaticId { 23 | // Use `define_rb_intern!` instead, which uses this function. 24 | pub fn intern_str(id: &'static str) -> Self { 25 | let id: Id = magnus::StaticSymbol::new(id).into(); 26 | 27 | // SAFETY: Ruby will never return a `0` ID. 28 | StaticId(unsafe { NonZeroUsize::new_unchecked(id.as_raw() as _) }) 29 | } 30 | } 31 | 32 | impl IntoId for StaticId { 33 | fn into_id_with(self, _: &Ruby) -> Id { 34 | // SAFEFY: This is safe because we know that the `Id` is something 35 | // returned from ruby. 36 | unsafe { Id::from_raw(self.0.get().try_into().expect("ID to be a usize")) } 37 | } 38 | } 39 | 40 | impl From for Symbol { 41 | fn from(static_id: StaticId) -> Self { 42 | let id: Id = static_id.into_id(); 43 | id.into() 44 | } 45 | } 46 | 47 | impl std::cmp::PartialEq for StaticId { 48 | fn eq(&self, other: &Id) -> bool { 49 | other.as_raw() as usize == self.0.get() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ext/src/helpers/symbol_enum.rs: -------------------------------------------------------------------------------- 1 | use super::static_id::StaticId; 2 | use magnus::{exception::arg_error, prelude::*, Error, Symbol, TryConvert, Value}; 3 | use std::fmt::Display; 4 | 5 | /// Represents an enum as a set of Symbols (using `StaticId`). 6 | /// Each symbol maps to a value of the enum's type. 7 | pub struct SymbolEnum<'a, T: Clone> { 8 | what: &'a str, 9 | mapping: Mapping, 10 | } 11 | 12 | impl<'a, T: Clone> SymbolEnum<'a, T> { 13 | pub fn new(what: &'a str, mapping: Vec<(StaticId, T)>) -> Self { 14 | Self { 15 | what, 16 | mapping: Mapping(mapping), 17 | } 18 | } 19 | 20 | /// Map a Magnus `Value` to the entry in the enum. 21 | /// Returns an `ArgumentError` with a message enumerating all valid symbols 22 | /// when `needle` isn't a valid symbol. 23 | pub fn get(&self, needle: Value) -> Result { 24 | let needle = Symbol::try_convert(needle).map_err(|_| self.error(needle))?; 25 | let id = magnus::value::Id::from(needle); 26 | 27 | self.mapping 28 | .0 29 | .iter() 30 | .find(|(haystack, _)| *haystack == id) 31 | .map(|found| found.1.clone()) 32 | .ok_or_else(|| self.error(needle.as_value())) 33 | } 34 | 35 | pub fn error(&self, value: Value) -> Error { 36 | Error::new( 37 | arg_error(), 38 | format!( 39 | "invalid {}, expected one of {}, got {:?}", 40 | self.what, self.mapping, value 41 | ), 42 | ) 43 | } 44 | } 45 | 46 | struct Mapping(Vec<(StaticId, T)>); 47 | 48 | /// Mimicks `Array#inpsect`'s output with all valid symbols. 49 | /// E.g.: `[:s1, :s2, :s3]` 50 | impl Display for Mapping { 51 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 52 | write!(f, "[")?; 53 | 54 | if let Some(((last, _), elems)) = self.0.split_last() { 55 | for (id, _) in elems.iter() { 56 | write!(f, ":{}, ", Symbol::from(*id).name().unwrap())?; 57 | } 58 | write!(f, ":{}", Symbol::from(*last).name().unwrap())?; 59 | } 60 | 61 | write!(f, "]")?; 62 | Ok(()) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /ext/src/helpers/tmplock.rs: -------------------------------------------------------------------------------- 1 | use magnus::{ 2 | rb_sys::{protect, AsRawValue}, 3 | RString, 4 | }; 5 | 6 | pub trait Tmplock { 7 | fn as_locked_slice(&self) -> Result<(&[u8], TmplockGuard), magnus::Error>; 8 | fn as_locked_str(&self) -> Result<(&str, TmplockGuard), magnus::Error>; 9 | } 10 | 11 | #[derive(Debug)] 12 | #[repr(transparent)] 13 | pub struct TmplockGuard { 14 | raw: rb_sys::VALUE, 15 | } 16 | 17 | impl Drop for TmplockGuard { 18 | fn drop(&mut self) { 19 | let result = unsafe { protect(|| rb_sys::rb_str_unlocktmp(self.raw)) }; 20 | debug_assert!( 21 | result.is_ok(), 22 | "failed to unlock tmplock for unknown reason" 23 | ); 24 | } 25 | } 26 | 27 | impl Tmplock for RString { 28 | fn as_locked_slice(&self) -> Result<(&[u8], TmplockGuard), magnus::Error> { 29 | let raw = self.as_raw(); 30 | let slice = unsafe { self.as_slice() }; 31 | let raw = protect(|| unsafe { rb_sys::rb_str_locktmp(raw) })?; 32 | let guard = TmplockGuard { raw }; 33 | 34 | Ok((slice, guard)) 35 | } 36 | 37 | fn as_locked_str(&self) -> Result<(&str, TmplockGuard), magnus::Error> { 38 | let str_result = unsafe { self.as_str()? }; 39 | let raw = self.as_raw(); 40 | let raw = protect(|| unsafe { rb_sys::rb_str_locktmp(raw) })?; 41 | let guard = TmplockGuard { raw }; 42 | 43 | Ok((str_result, guard)) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ext/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::doc_lazy_continuation)] 2 | use magnus::{Error, Ruby}; 3 | mod helpers; 4 | mod ruby_api; 5 | 6 | #[cfg(feature = "ruby-api")] 7 | pub use ruby_api::*; 8 | 9 | #[cfg(not(feature = "ruby-api"))] 10 | pub(crate) use ruby_api::*; 11 | 12 | #[cfg(not(feature = "ruby-api"))] // Let the upstream crate handle this 13 | rb_sys::set_global_tracking_allocator!(); 14 | 15 | #[magnus::init] 16 | pub fn init(ruby: &Ruby) -> Result<(), Error> { 17 | #[cfg(ruby_gte_3_0)] 18 | unsafe { 19 | rb_sys::rb_ext_ractor_safe(true); 20 | } 21 | ruby_api::init(ruby) 22 | } 23 | -------------------------------------------------------------------------------- /ext/src/ruby_api/caller.rs: -------------------------------------------------------------------------------- 1 | use super::{convert::WrapWasmtimeType, externals::Extern, root, store::StoreData}; 2 | use crate::error; 3 | use magnus::{class, method, typed_data::Obj, Error, Module as _, RString, Value}; 4 | use std::cell::UnsafeCell; 5 | use wasmtime::{AsContext, AsContextMut, Caller as CallerImpl, StoreContext, StoreContextMut}; 6 | 7 | /// A handle to a [`wasmtime::Caller`] that's only valid during a Func execution. 8 | /// [`UnsafeCell`] wraps the wasmtime::Caller because the Value's lifetime can't 9 | /// be tied to the Caller: the Value is handed back to Ruby and we can't control 10 | /// whether the user keeps a handle to it or not. 11 | #[derive(Debug)] 12 | pub struct CallerHandle<'a> { 13 | caller: UnsafeCell>>, 14 | } 15 | 16 | impl<'a> CallerHandle<'a> { 17 | pub fn new(caller: CallerImpl<'a, StoreData>) -> Self { 18 | Self { 19 | caller: UnsafeCell::new(Some(caller)), 20 | } 21 | } 22 | 23 | pub fn get_mut(&self) -> Result<&mut CallerImpl<'a, StoreData>, Error> { 24 | unsafe { &mut *self.caller.get() } 25 | .as_mut() 26 | .ok_or_else(|| error!("Caller outlived its Func execution")) 27 | } 28 | 29 | pub fn get(&self) -> Result<&CallerImpl<'a, StoreData>, Error> { 30 | unsafe { (*self.caller.get()).as_ref() } 31 | .ok_or_else(|| error!("Caller outlived its Func execution")) 32 | } 33 | 34 | pub fn expire(&self) { 35 | unsafe { *self.caller.get() = None } 36 | } 37 | } 38 | 39 | /// @yard 40 | /// @rename Wasmtime::Caller 41 | /// Represents the Caller's context within a Func execution. An instance of 42 | /// Caller is sent as the first parameter to Func's implementation (the 43 | /// block argument in {Func.new}). 44 | /// @see https://docs.rs/wasmtime/latest/wasmtime/struct.Caller.html Wasmtime's Rust doc 45 | #[derive(Debug)] 46 | #[magnus::wrap(class = "Wasmtime::Caller", free_immediately, unsafe_generics)] 47 | pub struct Caller<'a> { 48 | handle: CallerHandle<'a>, 49 | } 50 | 51 | impl<'a> Caller<'a> { 52 | pub fn new(caller: CallerImpl<'a, StoreData>) -> Self { 53 | Self { 54 | handle: CallerHandle::new(caller), 55 | } 56 | } 57 | 58 | /// @yard 59 | /// Returns the store's data. Akin to {Store#data}. 60 | /// @return [Object] The store's data (the object passed to {Store.new}). 61 | pub fn store_data(&self) -> Result { 62 | self.context().map(|ctx| ctx.data().user_data()) 63 | } 64 | 65 | /// @yard 66 | /// @def export(name) 67 | /// @see Instance#export 68 | pub fn export(rb_self: Obj>, name: RString) -> Result>, Error> { 69 | let inner = rb_self.handle.get_mut()?; 70 | 71 | if let Some(export) = inner.get_export(unsafe { name.as_str() }?) { 72 | export.wrap_wasmtime_type(rb_self.into()).map(Some) 73 | } else { 74 | Ok(None) 75 | } 76 | } 77 | 78 | /// @yard 79 | /// (see Store#get_fuel) 80 | /// @def get_fuel 81 | pub fn get_fuel(&self) -> Result { 82 | self.handle 83 | .get() 84 | .map(|c| c.get_fuel())? 85 | .map_err(|e| error!("{}", e)) 86 | } 87 | 88 | /// @yard 89 | /// (see Store#set_fuel) 90 | /// @def set_fuel(fuel) 91 | pub fn set_fuel(&self, fuel: u64) -> Result<(), Error> { 92 | self.handle 93 | .get_mut() 94 | .and_then(|c| c.set_fuel(fuel).map_err(|e| error!("{}", e)))?; 95 | 96 | Ok(()) 97 | } 98 | 99 | pub fn context(&self) -> Result, Error> { 100 | self.handle.get().map(|c| c.as_context()) 101 | } 102 | 103 | pub fn context_mut(&self) -> Result, Error> { 104 | self.handle.get_mut().map(|c| c.as_context_mut()) 105 | } 106 | 107 | pub fn expire(&self) { 108 | self.handle.expire(); 109 | } 110 | } 111 | 112 | unsafe impl Send for Caller<'_> {} 113 | 114 | pub fn init() -> Result<(), Error> { 115 | let klass = root().define_class("Caller", class::object())?; 116 | klass.define_method("store_data", method!(Caller::store_data, 0))?; 117 | klass.define_method("export", method!(Caller::export, 1))?; 118 | klass.define_method("get_fuel", method!(Caller::get_fuel, 0))?; 119 | klass.define_method("set_fuel", method!(Caller::set_fuel, 1))?; 120 | 121 | Ok(()) 122 | } 123 | -------------------------------------------------------------------------------- /ext/src/ruby_api/component.rs: -------------------------------------------------------------------------------- 1 | mod convert; 2 | mod func; 3 | mod instance; 4 | mod linker; 5 | 6 | use super::root; 7 | use magnus::{ 8 | class, class::RClass, function, method, prelude::*, r_string::RString, value::Lazy, Error, 9 | Module, Object, RModule, Ruby, 10 | }; 11 | use rb_sys::tracking_allocator::ManuallyTracked; 12 | use wasmtime::component::Component as ComponentImpl; 13 | 14 | pub use func::Func; 15 | pub use instance::Instance; 16 | 17 | pub fn component_namespace(ruby: &Ruby) -> RModule { 18 | static COMPONENT_NAMESPACE: Lazy = 19 | Lazy::new(|_| root().define_module("Component").unwrap()); 20 | ruby.get_inner(&COMPONENT_NAMESPACE) 21 | } 22 | 23 | use crate::{ 24 | error, 25 | helpers::{nogvl, Tmplock}, 26 | Engine, 27 | }; 28 | /// @yard 29 | /// @rename Wasmtime::Component::Component 30 | /// Represents a WebAssembly component. 31 | /// @see https://docs.rs/wasmtime/latest/wasmtime/component/struct.Component.html Wasmtime's Rust doc 32 | #[magnus::wrap( 33 | class = "Wasmtime::Component::Component", 34 | size, 35 | free_immediately, 36 | frozen_shareable 37 | )] 38 | pub struct Component { 39 | inner: ComponentImpl, 40 | _track_memory_usage: ManuallyTracked<()>, 41 | } 42 | 43 | // Needed for ManuallyTracked 44 | unsafe impl Send for Component {} 45 | 46 | impl Component { 47 | /// @yard 48 | /// Creates a new component from the given binary data. 49 | /// @def new(engine, wat_or_wasm) 50 | /// @param engine [Wasmtime::Engine] 51 | /// @param wat_or_wasm [String] The String of WAT or Wasm. 52 | /// @return [Wasmtime::Component::Component] 53 | pub fn new(engine: &Engine, wat_or_wasm: RString) -> Result { 54 | let eng = engine.get(); 55 | let (locked_slice, _locked_slice_guard) = wat_or_wasm.as_locked_slice()?; 56 | let component = nogvl(|| ComponentImpl::new(eng, locked_slice)) 57 | .map_err(|e| error!("Could not build component: {}", e))?; 58 | 59 | Ok(component.into()) 60 | } 61 | 62 | /// @yard 63 | /// @def from_file(engine, path) 64 | /// @param engine [Wasmtime::Engine] 65 | /// @param path [String] 66 | /// @return [Wasmtime::Component::Component] 67 | pub fn from_file(engine: &Engine, path: RString) -> Result { 68 | let eng = engine.get(); 69 | let (path, _locked_str_guard) = path.as_locked_str()?; 70 | // SAFETY: this string is immediately copied and never moved off the stack 71 | let component = nogvl(|| ComponentImpl::from_file(eng, path)) 72 | .map_err(|e| error!("Could not build component from file: {}", e))?; 73 | 74 | Ok(component.into()) 75 | } 76 | 77 | /// @yard 78 | /// Instantiates a serialized component coming from either {#serialize} or {Wasmtime::Engine#precompile_component}. 79 | /// 80 | /// The engine serializing and the engine deserializing must: 81 | /// * have the same configuration 82 | /// * be of the same gem version 83 | /// 84 | /// @def deserialize(engine, compiled) 85 | /// @param engine [Wasmtime::Engine] 86 | /// @param compiled [String] String obtained with either {Wasmtime::Engine#precompile_component} or {#serialize}. 87 | /// @return [Wasmtime::Component::Component] 88 | pub fn deserialize(engine: &Engine, compiled: RString) -> Result { 89 | // SAFETY: this string is immediately copied and never moved off the stack 90 | unsafe { ComponentImpl::deserialize(engine.get(), compiled.as_slice()) } 91 | .map(Into::into) 92 | .map_err(|e| error!("Could not deserialize component: {}", e)) 93 | } 94 | 95 | /// @yard 96 | /// Instantiates a serialized component from a file. 97 | /// 98 | /// @def deserialize_file(engine, path) 99 | /// @param engine [Wasmtime::Engine] 100 | /// @param path [String] 101 | /// @return [Wasmtime::Component::Component] 102 | /// @see .deserialize 103 | pub fn deserialize_file(engine: &Engine, path: RString) -> Result { 104 | unsafe { ComponentImpl::deserialize_file(engine.get(), path.as_str()?) } 105 | .map(Into::into) 106 | .map_err(|e| error!("Could not deserialize component from file: {}", e)) 107 | } 108 | 109 | /// @yard 110 | /// Serialize the component. 111 | /// @return [String] 112 | /// @see .deserialize 113 | pub fn serialize(&self) -> Result { 114 | let bytes = self.get().serialize(); 115 | 116 | bytes 117 | .map(|bytes| RString::from_slice(&bytes)) 118 | .map_err(|e| error!("{:?}", e)) 119 | } 120 | 121 | pub fn get(&self) -> &ComponentImpl { 122 | &self.inner 123 | } 124 | } 125 | 126 | impl From for Component { 127 | fn from(inner: ComponentImpl) -> Self { 128 | let range = inner.image_range(); 129 | let start = range.start; 130 | let end = range.end; 131 | 132 | assert!(end > start); 133 | let size = unsafe { end.offset_from(start) }; 134 | 135 | Self { 136 | inner, 137 | _track_memory_usage: ManuallyTracked::new(size as usize), 138 | } 139 | } 140 | } 141 | 142 | mod bundled { 143 | include!(concat!(env!("OUT_DIR"), "/bundled/component.rs")); 144 | } 145 | 146 | pub fn init(ruby: &Ruby) -> Result<(), Error> { 147 | bundled::init()?; 148 | 149 | let namespace = component_namespace(ruby); 150 | 151 | let class = namespace.define_class("Component", class::object())?; 152 | class.define_singleton_method("new", function!(Component::new, 2))?; 153 | class.define_singleton_method("from_file", function!(Component::from_file, 2))?; 154 | class.define_singleton_method("deserialize", function!(Component::deserialize, 2))?; 155 | class.define_singleton_method( 156 | "deserialize_file", 157 | function!(Component::deserialize_file, 2), 158 | )?; 159 | class.define_method("serialize", method!(Component::serialize, 0))?; 160 | 161 | linker::init(ruby, &namespace)?; 162 | instance::init(ruby, &namespace)?; 163 | func::init(ruby, &namespace)?; 164 | convert::init(ruby)?; 165 | 166 | Ok(()) 167 | } 168 | -------------------------------------------------------------------------------- /ext/src/ruby_api/component/func.rs: -------------------------------------------------------------------------------- 1 | use crate::ruby_api::{ 2 | component::{ 3 | convert::{component_val_to_rb, rb_to_component_val}, 4 | Instance, 5 | }, 6 | errors::ExceptionMessage, 7 | store::{Store, StoreContextValue}, 8 | }; 9 | use magnus::{ 10 | class, exception::arg_error, gc::Marker, method, prelude::*, typed_data::Obj, value, 11 | DataTypeFunctions, Error, IntoValue, RArray, RModule, Ruby, TypedData, Value, 12 | }; 13 | use wasmtime::component::{Func as FuncImpl, Type, Val}; 14 | 15 | /// @yard 16 | /// @rename Wasmtime::Component::Func 17 | /// Represents a WebAssembly component Function 18 | /// @see https://docs.wasmtime.dev/api/wasmtime/component/struct.Func.html Wasmtime's Rust doc 19 | /// 20 | /// == Component model types conversion 21 | /// 22 | /// Here's how component model types map to Ruby objects: 23 | /// 24 | /// bool:: 25 | /// Ruby +true+ or +false+, no automatic conversion happens. 26 | /// s8, u8, s16, u16, etc.:: 27 | /// Ruby +Integer+. Overflows raise. 28 | /// f32, f64:: 29 | /// Ruby +Float+. 30 | /// string:: 31 | /// Ruby +String+. Exception will be raised if the string is not valid UTF-8. 32 | /// list:: 33 | /// Ruby +Array+. 34 | /// tuple:: 35 | /// Ruby +Array+ of the same size of tuple. Example: +tuple+ would be converted to +[T, U]+. 36 | /// record:: 37 | /// Ruby +Hash+ where field names are +String+s 38 | /// (for performance, see {this benchmark}[https://github.com/bytecodealliance/wasmtime-rb/issues/400#issuecomment-2496097993]). 39 | /// result:: 40 | /// {Result} instance. When converting a result branch of the none 41 | /// type, the {Result}’s value MUST be +nil+. 42 | /// 43 | /// Examples of none type in a result: unparametrized +result+, +result+, +result<_, E>+. 44 | /// option:: 45 | /// +nil+ is mapped to +None+, anything else is mapped to +Some(T)+. 46 | /// flags:: 47 | /// Ruby +Array+ of +String+s. 48 | /// enum:: 49 | /// Ruby +String+. Exception will be raised of the +String+ is not a valid enum value. 50 | /// variant:: 51 | /// {Variant} instance wrapping the variant's name and optionally its value. 52 | /// Exception will be raised for: 53 | /// - invalid {Variant#name}, 54 | /// - unparametrized variant and not nil {Variant#value}. 55 | /// resource (own or borrow):: 56 | /// Not yet supported. 57 | #[derive(TypedData)] 58 | #[magnus(class = "Wasmtime::Component::Func", size, mark, free_immediately)] 59 | pub struct Func { 60 | store: Obj, 61 | instance: Obj, 62 | inner: FuncImpl, 63 | } 64 | unsafe impl Send for Func {} 65 | 66 | impl DataTypeFunctions for Func { 67 | fn mark(&self, marker: &Marker) { 68 | marker.mark(self.store); 69 | marker.mark(self.instance); 70 | } 71 | } 72 | 73 | impl Func { 74 | /// @yard 75 | /// Calls a Wasm component model function. 76 | /// @def call(*args) 77 | /// @param args [Array] the function's arguments as per its Wasm definition 78 | /// @return [Object] the function's return value as per its Wasm definition 79 | /// @see Func Func class-level documentation for type conversion logic 80 | pub fn call(&self, args: &[Value]) -> Result { 81 | Func::invoke(self.store, &self.inner, args) 82 | } 83 | 84 | pub fn from_inner(inner: FuncImpl, instance: Obj, store: Obj) -> Self { 85 | Self { 86 | store, 87 | instance, 88 | inner, 89 | } 90 | } 91 | 92 | pub fn invoke(store: Obj, func: &FuncImpl, args: &[Value]) -> Result { 93 | let store_context_value = StoreContextValue::from(store); 94 | let results_ty = func.results(store.context_mut()); 95 | let mut results = vec![wasmtime::component::Val::Bool(false); results_ty.len()]; 96 | let params = convert_params( 97 | &store_context_value, 98 | &func.params(store.context_mut()), 99 | args, 100 | )?; 101 | 102 | func.call(store.context_mut(), ¶ms, &mut results) 103 | .map_err(|e| store_context_value.handle_wasm_error(e))?; 104 | 105 | let result = match results_ty.len() { 106 | 0 => Ok(value::qnil().as_value()), 107 | 1 => component_val_to_rb(results.into_iter().next().unwrap(), &store_context_value), 108 | _ => results 109 | .into_iter() 110 | .map(|val| component_val_to_rb(val, &store_context_value)) 111 | .collect::>() 112 | .map(IntoValue::into_value), 113 | }; 114 | 115 | func.post_return(store.context_mut()) 116 | .map_err(|e| store_context_value.handle_wasm_error(e))?; 117 | 118 | result 119 | } 120 | } 121 | 122 | fn convert_params( 123 | store: &StoreContextValue, 124 | ty: &[(String, Type)], 125 | params_slice: &[Value], 126 | ) -> Result, Error> { 127 | if ty.len() != params_slice.len() { 128 | return Err(Error::new( 129 | arg_error(), 130 | format!( 131 | "wrong number of arguments (given {}, expected {})", 132 | params_slice.len(), 133 | ty.len() 134 | ), 135 | )); 136 | } 137 | 138 | let mut params = Vec::with_capacity(ty.len()); 139 | for (i, (ty, value)) in ty.iter().zip(params_slice.iter()).enumerate() { 140 | let i: u32 = i 141 | .try_into() 142 | .map_err(|_| Error::new(arg_error(), "too many params"))?; 143 | 144 | let component_val = rb_to_component_val(*value, store, &ty.1) 145 | .map_err(|error| error.append(format!(" (param at index {})", i)))?; 146 | 147 | params.push(component_val); 148 | } 149 | 150 | Ok(params) 151 | } 152 | 153 | pub fn init(_ruby: &Ruby, namespace: &RModule) -> Result<(), Error> { 154 | let func = namespace.define_class("Func", class::object())?; 155 | func.define_method("call", method!(Func::call, -1))?; 156 | 157 | Ok(()) 158 | } 159 | -------------------------------------------------------------------------------- /ext/src/ruby_api/component/instance.rs: -------------------------------------------------------------------------------- 1 | use crate::ruby_api::{component::Func, Store}; 2 | use std::{borrow::BorrowMut, cell::RefCell}; 3 | 4 | use crate::error; 5 | use magnus::{ 6 | class, 7 | error::ErrorType, 8 | exception::{arg_error, type_error}, 9 | function, 10 | gc::Marker, 11 | method, 12 | prelude::*, 13 | r_string::RString, 14 | scan_args, 15 | typed_data::Obj, 16 | value::{self, ReprValue}, 17 | DataTypeFunctions, Error, RArray, Ruby, TryConvert, TypedData, Value, 18 | }; 19 | use magnus::{IntoValue, RModule}; 20 | use wasmtime::component::{ComponentExportIndex, Instance as InstanceImpl, Type, Val}; 21 | 22 | /// @yard 23 | /// Represents a WebAssembly component instance. 24 | /// @see https://docs.rs/wasmtime/latest/wasmtime/component/struct.Instance.html Wasmtime's Rust doc 25 | #[derive(Clone, TypedData)] 26 | #[magnus(class = "Wasmtime::Component::Instance", mark, free_immediately)] 27 | pub struct Instance { 28 | inner: InstanceImpl, 29 | store: Obj, 30 | } 31 | 32 | unsafe impl Send for Instance {} 33 | 34 | impl DataTypeFunctions for Instance { 35 | fn mark(&self, marker: &Marker) { 36 | marker.mark(self.store) 37 | } 38 | } 39 | 40 | impl Instance { 41 | pub fn from_inner(store: Obj, inner: InstanceImpl) -> Self { 42 | Self { inner, store } 43 | } 44 | 45 | /// @yard 46 | /// Retrieves a Wasm function from the component instance. 47 | /// 48 | /// @def get_func(handle) 49 | /// @param handle [String, Array] The path of the function to retrieve 50 | /// @return [Func, nil] The function if it exists, nil otherwise 51 | /// 52 | /// @example Retrieve a top-level +add+ export: 53 | /// instance.get_func("add") 54 | /// 55 | /// @example Retrieve an +add+ export nested under an +adder+ instance top-level export: 56 | /// instance.get_func(["adder", "add"]) 57 | pub fn get_func(rb_self: Obj, handle: Value) -> Result, Error> { 58 | let func = rb_self 59 | .export_index(handle)? 60 | .and_then(|index| rb_self.inner.get_func(rb_self.store.context_mut(), index)) 61 | .map(|inner| Func::from_inner(inner, rb_self, rb_self.store)); 62 | 63 | Ok(func) 64 | } 65 | 66 | fn export_index(&self, handle: Value) -> Result, Error> { 67 | let invalid_arg = || { 68 | Error::new( 69 | type_error(), 70 | format!( 71 | "invalid argument for component index, expected String | Array, got {}", 72 | handle.inspect() 73 | ), 74 | ) 75 | }; 76 | 77 | let index = if let Some(name) = RString::from_value(handle) { 78 | self.inner 79 | .get_export(self.store.context_mut(), None, unsafe { name.as_str()? }) 80 | .map(|(_, index)| index) 81 | } else if let Some(names) = RArray::from_value(handle) { 82 | unsafe { names.as_slice() } 83 | .iter() 84 | .try_fold::<_, _, Result<_, Error>>(None, |index, name| { 85 | let name = RString::from_value(*name).ok_or_else(invalid_arg)?; 86 | 87 | Ok(self 88 | .inner 89 | .get_export(self.store.context_mut(), index.as_ref(), unsafe { 90 | name.as_str()? 91 | }) 92 | .map(|(_, index)| index)) 93 | })? 94 | } else { 95 | return Err(invalid_arg()); 96 | }; 97 | 98 | Ok(index) 99 | } 100 | } 101 | 102 | pub fn init(_ruby: &Ruby, namespace: &RModule) -> Result<(), Error> { 103 | let instance = namespace.define_class("Instance", class::object())?; 104 | instance.define_method("get_func", method!(Instance::get_func, 1))?; 105 | 106 | Ok(()) 107 | } 108 | -------------------------------------------------------------------------------- /ext/src/ruby_api/errors.rs: -------------------------------------------------------------------------------- 1 | use crate::ruby_api::root; 2 | use magnus::{error::ErrorType, value::Lazy, Error, ExceptionClass, Module, Ruby}; 3 | use std::borrow::Cow; 4 | 5 | /// Base error class for all Wasmtime errors. 6 | pub fn base_error() -> ExceptionClass { 7 | static ERR: Lazy = Lazy::new(|_| root().const_get("Error").unwrap()); 8 | let ruby = Ruby::get().unwrap(); 9 | ruby.get_inner(&ERR) 10 | } 11 | 12 | /// Raised when failing to convert the return value of a Ruby-backed Func to 13 | /// Wasm types. 14 | pub fn result_error() -> ExceptionClass { 15 | static ERR: Lazy = Lazy::new(|_| root().const_get("ResultError").unwrap()); 16 | let ruby = Ruby::get().unwrap(); 17 | ruby.get_inner(&ERR) 18 | } 19 | 20 | /// Raised when converting an {Extern} to its concrete type fails. 21 | pub fn conversion_error() -> ExceptionClass { 22 | static ERR: Lazy = Lazy::new(|_| root().const_get("ConversionError").unwrap()); 23 | let ruby = Ruby::get().unwrap(); 24 | ruby.get_inner(&ERR) 25 | } 26 | 27 | /// Raised when a WASI program terminates early by calling +exit+. 28 | pub fn wasi_exit_error() -> ExceptionClass { 29 | static ERR: Lazy = Lazy::new(|_| root().const_get("WasiExit").unwrap()); 30 | let ruby = Ruby::get().unwrap(); 31 | ruby.get_inner(&ERR) 32 | } 33 | 34 | #[macro_export] 35 | macro_rules! err { 36 | ($($arg:expr),*) => { 37 | Result::Err($crate::error!($($arg),*)) 38 | }; 39 | } 40 | 41 | #[macro_export] 42 | macro_rules! error { 43 | ($($arg:expr),*) => { 44 | Error::new($crate::ruby_api::errors::base_error(), format!($($arg),*)) 45 | }; 46 | } 47 | 48 | #[macro_export] 49 | macro_rules! not_implemented { 50 | ($($arg:expr),*) => { 51 | Err(Error::new(magnus::exception::not_imp_error(), format!($($arg),*))) 52 | }; 53 | } 54 | 55 | #[macro_export] 56 | macro_rules! conversion_err { 57 | ($($arg:expr),*) => { 58 | Err(Error::new($crate::ruby_api::errors::conversion_error(), format!("cannot convert {} to {}", $($arg),*))) 59 | }; 60 | } 61 | 62 | /// Utilities for reformatting error messages 63 | pub trait ExceptionMessage { 64 | /// Append a message to an exception 65 | fn append(self, extra: T) -> Self 66 | where 67 | T: Into>; 68 | } 69 | 70 | impl ExceptionMessage for magnus::Error { 71 | fn append(self, extra: T) -> Self 72 | where 73 | T: Into>, 74 | { 75 | match self.error_type() { 76 | ErrorType::Error(class, msg) => Error::new(*class, format!("{}{}", msg, extra.into())), 77 | ErrorType::Exception(exception) => Error::new( 78 | exception.exception_class(), 79 | format!("{}{}", exception, extra.into()), 80 | ), 81 | _ => self, 82 | } 83 | } 84 | } 85 | 86 | mod bundled { 87 | include!(concat!(env!("OUT_DIR"), "/bundled/error.rs")); 88 | } 89 | 90 | pub fn init() -> Result<(), Error> { 91 | bundled::init()?; 92 | 93 | let _ = base_error(); 94 | let _ = result_error(); 95 | let _ = conversion_error(); 96 | let _ = wasi_exit_error(); 97 | 98 | Ok(()) 99 | } 100 | -------------------------------------------------------------------------------- /ext/src/ruby_api/instance.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | convert::{ToExtern, WrapWasmtimeType}, 3 | func::Func, 4 | module::Module, 5 | root, 6 | store::{Store, StoreContextValue, StoreData}, 7 | }; 8 | use crate::err; 9 | use magnus::{ 10 | class, function, gc::Marker, method, prelude::*, scan_args, typed_data::Obj, DataTypeFunctions, 11 | Error, Object, RArray, RHash, RString, Ruby, TryConvert, TypedData, Value, 12 | }; 13 | use wasmtime::{Extern, Instance as InstanceImpl, StoreContextMut}; 14 | 15 | /// @yard 16 | /// Represents a WebAssembly instance. 17 | /// @see https://docs.rs/wasmtime/latest/wasmtime/struct.Instance.html Wasmtime's Rust doc 18 | #[derive(Clone, Debug, TypedData)] 19 | #[magnus(class = "Wasmtime::Instance", mark, free_immediately)] 20 | pub struct Instance { 21 | inner: InstanceImpl, 22 | store: Obj, 23 | } 24 | 25 | unsafe impl Send for Instance {} 26 | 27 | impl DataTypeFunctions for Instance { 28 | fn mark(&self, marker: &Marker) { 29 | marker.mark(self.store) 30 | } 31 | } 32 | 33 | impl Instance { 34 | /// @yard 35 | /// @def new(store, mod, imports = []) 36 | /// @param store [Store] The store to instantiate the module in. 37 | /// @param mod [Module] The module to instantiate. 38 | /// @param imports [Array] 39 | /// The module's import, in orders that that they show up in the module. 40 | /// @return [Instance] 41 | pub fn new(ruby: &Ruby, args: &[Value]) -> Result { 42 | let args = 43 | scan_args::scan_args::<(Obj, &Module), (Option,), (), (), (), ()>(args)?; 44 | let (wrapped_store, module) = args.required; 45 | let mut context = wrapped_store.context_mut(); 46 | let imports = args 47 | .optional 48 | .0 49 | .and_then(|v| if v.is_nil() { None } else { Some(v) }); 50 | 51 | let imports: Vec = match imports { 52 | Some(arr) => { 53 | let arr = RArray::try_convert(arr)?; 54 | let mut imports = Vec::with_capacity(arr.len()); 55 | // SAFETY: arr won't get gc'd (it's on the stack) and we don't mutate it. 56 | for import in unsafe { arr.as_slice() } { 57 | context.data_mut().retain(*import); 58 | imports.push(import.to_extern(ruby)?); 59 | } 60 | imports 61 | } 62 | None => vec![], 63 | }; 64 | 65 | let module = module.get(); 66 | let inner = InstanceImpl::new(context, module, &imports) 67 | .map_err(|e| StoreContextValue::from(wrapped_store).handle_wasm_error(e))?; 68 | 69 | Ok(Self { 70 | inner, 71 | store: wrapped_store, 72 | }) 73 | } 74 | 75 | pub fn get(&self) -> InstanceImpl { 76 | self.inner 77 | } 78 | 79 | pub fn from_inner(store: Obj, inner: InstanceImpl) -> Self { 80 | Self { inner, store } 81 | } 82 | 83 | /// @yard 84 | /// Returns a +Hash+ of exports where keys are export names as +String+s 85 | /// and values are {Extern}s. 86 | /// 87 | /// @def exports 88 | /// @return [Hash{String => Extern}] 89 | pub fn exports(&self) -> Result { 90 | let mut ctx = self.store.context_mut(); 91 | let hash = RHash::new(); 92 | 93 | for export in self.inner.exports(&mut ctx) { 94 | let export_name = RString::new(export.name()); 95 | let wrapped_store = self.store; 96 | let wrapped_export = export 97 | .into_extern() 98 | .wrap_wasmtime_type(wrapped_store.into())?; 99 | hash.aset(export_name, wrapped_export)?; 100 | } 101 | 102 | Ok(hash) 103 | } 104 | 105 | /// @yard 106 | /// Get an export by name. 107 | /// 108 | /// @def export(name) 109 | /// @param name [String] 110 | /// @return [Extern, nil] The export if it exists, nil otherwise. 111 | pub fn export(&self, str: RString) -> Result, Error> { 112 | let export = self 113 | .inner 114 | .get_export(self.store.context_mut(), unsafe { str.as_str()? }); 115 | match export { 116 | Some(export) => export.wrap_wasmtime_type(self.store.into()).map(Some), 117 | None => Ok(None), 118 | } 119 | } 120 | 121 | /// @yard 122 | /// Retrieves a Wasm function from the instance and calls it. 123 | /// Essentially a shortcut for +instance.export(name).call(...)+. 124 | /// 125 | /// @def invoke(name, *args) 126 | /// @param name [String] The name of function to run. 127 | /// @param (see Func#call) 128 | /// @return (see Func#call) 129 | /// @see Func#call 130 | pub fn invoke(&self, args: &[Value]) -> Result { 131 | let name = RString::try_convert(*args.first().ok_or_else(|| { 132 | Error::new( 133 | magnus::exception::type_error(), 134 | "wrong number of arguments (given 0, expected 1+)", 135 | ) 136 | })?)?; 137 | 138 | let func = self.get_func(self.store.context_mut(), unsafe { name.as_str()? })?; 139 | Func::invoke(&self.store.into(), &func, &args[1..]) 140 | } 141 | 142 | fn get_func( 143 | &self, 144 | context: StoreContextMut<'_, StoreData>, 145 | name: &str, 146 | ) -> Result { 147 | let instance = self.inner; 148 | 149 | if let Some(func) = instance.get_func(context, name) { 150 | Ok(func) 151 | } else { 152 | err!("function \"{}\" not found", name) 153 | } 154 | } 155 | } 156 | 157 | pub fn init() -> Result<(), Error> { 158 | let class = root().define_class("Instance", class::object())?; 159 | 160 | class.define_singleton_method("new", function!(Instance::new, -1))?; 161 | class.define_method("invoke", method!(Instance::invoke, -1))?; 162 | class.define_method("exports", method!(Instance::exports, 0))?; 163 | class.define_method("export", method!(Instance::export, 1))?; 164 | 165 | Ok(()) 166 | } 167 | -------------------------------------------------------------------------------- /ext/src/ruby_api/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(rustdoc::broken_intra_doc_links)] 2 | #![allow(rustdoc::invalid_html_tags)] 3 | #![allow(rustdoc::bare_urls)] 4 | #![allow(rustdoc::invalid_rust_codeblocks)] 5 | // The `pub use` imports below need to be publicly exposed when the ruby_api 6 | // feature is enabled, else they must be publicly exposed to the crate only 7 | // (`pub(crate) use`). Allowing unused imports is easier and less repetitive. 8 | // Also the feature is already correctly gated in lib.rs. 9 | #![allow(unused_imports)] 10 | use magnus::{function, value::Lazy, Error, RModule, RString, Ruby}; 11 | 12 | mod caller; 13 | mod component; 14 | mod config; 15 | mod convert; 16 | mod engine; 17 | mod errors; 18 | mod externals; 19 | mod func; 20 | mod global; 21 | mod instance; 22 | mod linker; 23 | mod memory; 24 | mod module; 25 | mod params; 26 | mod pooling_allocation_config; 27 | mod store; 28 | mod table; 29 | mod trap; 30 | mod wasi_ctx; 31 | mod wasi_ctx_builder; 32 | 33 | pub use caller::Caller; 34 | pub use engine::Engine; 35 | pub use func::Func; 36 | pub use instance::Instance; 37 | pub use linker::Linker; 38 | pub use memory::Memory; 39 | pub use module::Module; 40 | pub use params::Params; 41 | pub use pooling_allocation_config::PoolingAllocationConfig; 42 | pub use store::Store; 43 | pub use trap::Trap; 44 | pub use wasi_ctx::WasiCtx; 45 | pub use wasi_ctx_builder::WasiCtxBuilder; 46 | 47 | /// The "Wasmtime" Ruby module. 48 | pub fn root() -> RModule { 49 | static ROOT: Lazy = Lazy::new(|ruby| ruby.define_module("Wasmtime").unwrap()); 50 | let ruby = Ruby::get().unwrap(); 51 | ruby.get_inner(&ROOT) 52 | } 53 | 54 | // This Struct is a placeholder for documentation, so that we can hang methods 55 | // to it and have yard-rustdoc discover them. 56 | /// @yard 57 | /// @module 58 | pub struct Wasmtime; 59 | impl Wasmtime { 60 | /// @yard 61 | /// Converts a WAT +String+ into Wasm. 62 | /// @param wat [String] 63 | /// @def wat2wasm(wat) 64 | /// @return [String] The Wasm represented as a binary +String+. 65 | pub fn wat2wasm(wat: RString) -> Result { 66 | wat::parse_str(unsafe { wat.as_str()? }) 67 | .map(|bytes| RString::from_slice(bytes.as_slice())) 68 | .map_err(|e| crate::error!("{}", e)) 69 | } 70 | } 71 | 72 | pub fn init(ruby: &Ruby) -> Result<(), Error> { 73 | let wasmtime = root(); 74 | 75 | wasmtime.define_module_function("wat2wasm", function!(Wasmtime::wat2wasm, 1))?; 76 | 77 | errors::init()?; 78 | trap::init()?; 79 | engine::init()?; 80 | module::init()?; 81 | store::init()?; 82 | instance::init()?; 83 | func::init()?; 84 | caller::init()?; 85 | memory::init(ruby)?; 86 | linker::init()?; 87 | externals::init()?; 88 | wasi_ctx_builder::init()?; 89 | table::init()?; 90 | global::init()?; 91 | wasi_ctx::init()?; 92 | pooling_allocation_config::init()?; 93 | component::init(ruby)?; 94 | 95 | Ok(()) 96 | } 97 | -------------------------------------------------------------------------------- /ext/src/ruby_api/params.rs: -------------------------------------------------------------------------------- 1 | use super::{convert::ToWasmVal, errors::ExceptionMessage, store::StoreContextValue}; 2 | use magnus::{error::ErrorType, exception::arg_error, Error, Value}; 3 | use static_assertions::assert_eq_size; 4 | use wasmtime::{FuncType, ValType}; 5 | 6 | #[derive(Debug, Clone)] 7 | #[repr(C)] 8 | struct Param { 9 | val: Value, 10 | index: u32, 11 | ty: ValType, 12 | } 13 | 14 | impl Param { 15 | pub fn new(index: u32, ty: ValType, val: Value) -> Self { 16 | Self { index, ty, val } 17 | } 18 | 19 | fn to_wasmtime_val(&self, store: &StoreContextValue) -> Result { 20 | self.val 21 | .to_wasm_val(store, self.ty.clone()) 22 | .map_err(|error| error.append(format!(" (param at index {})", self.index))) 23 | } 24 | } 25 | 26 | pub struct Params<'a>(&'a FuncType, &'a [Value]); 27 | 28 | impl<'a> Params<'a> { 29 | pub fn new(ty: &'a FuncType, params_slice: &'a [Value]) -> Result { 30 | if ty.params().len() != params_slice.len() { 31 | return Err(Error::new( 32 | arg_error(), 33 | format!( 34 | "wrong number of arguments (given {}, expected {})", 35 | params_slice.len(), 36 | ty.params().len() 37 | ), 38 | )); 39 | } 40 | Ok(Self(ty, params_slice)) 41 | } 42 | 43 | pub fn to_vec(&self, store: &StoreContextValue) -> Result, Error> { 44 | let mut vals = Vec::with_capacity(self.0.params().len()); 45 | for (i, (param, value)) in self.0.params().zip(self.1.iter()).enumerate() { 46 | let i: u32 = i 47 | .try_into() 48 | .map_err(|_| Error::new(arg_error(), "too many params"))?; 49 | let param = Param::new(i, param, *value); 50 | vals.push(param.to_wasmtime_val(store)?); 51 | } 52 | 53 | Ok(vals) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ext/src/ruby_api/trap.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | 3 | use crate::ruby_api::{errors::base_error, root}; 4 | use magnus::Error; 5 | use magnus::{ 6 | method, prelude::*, rb_sys::AsRawValue, typed_data::Obj, value::Lazy, DataTypeFunctions, 7 | ExceptionClass, IntoValue, Ruby, Symbol, TypedData, 8 | }; 9 | 10 | pub fn trap_error() -> ExceptionClass { 11 | static ERR: Lazy = 12 | Lazy::new(|_| root().define_error("Trap", base_error()).unwrap()); 13 | let ruby = Ruby::get().unwrap(); 14 | ruby.get_inner(&ERR) 15 | } 16 | 17 | macro_rules! trap_const { 18 | ($trap:ident) => { 19 | trap_error().const_get(stringify!($trap)).map(Some) 20 | }; 21 | } 22 | 23 | #[derive(TypedData, Debug)] 24 | #[magnus(class = "Wasmtime::Trap", size, free_immediately)] 25 | /// @yard 26 | pub struct Trap { 27 | trap: wasmtime::Trap, 28 | wasm_backtrace: Option, 29 | } 30 | impl DataTypeFunctions for Trap {} 31 | 32 | impl Trap { 33 | pub fn new(trap: wasmtime::Trap, wasm_backtrace: Option) -> Self { 34 | Self { 35 | trap, 36 | wasm_backtrace, 37 | } 38 | } 39 | 40 | /// @yard 41 | /// Returns a textual description of the trap error, for example: 42 | /// wasm trap: wasm `unreachable` instruction executed 43 | /// @return [String] 44 | pub fn message(&self) -> String { 45 | self.trap.to_string() 46 | } 47 | 48 | /// @yard 49 | /// Returns a textual representation of the Wasm backtrce, if it exists. 50 | /// For example: 51 | /// error while executing at wasm backtrace: 52 | /// 0: 0x1a - ! 53 | /// @return [String, nil] 54 | pub fn wasm_backtrace_message(&self) -> Option { 55 | self.wasm_backtrace.as_ref().map(|bt| format!("{bt}")) 56 | } 57 | 58 | /// @yard 59 | /// Returns the trap code as a Symbol, possibly nil if the trap did not 60 | /// origin from Wasm code. All possible trap codes are defined as constants on {Trap}. 61 | /// @return [Symbol, nil] 62 | pub fn code(&self) -> Result, Error> { 63 | match self.trap { 64 | wasmtime::Trap::StackOverflow => trap_const!(STACK_OVERFLOW), 65 | wasmtime::Trap::MemoryOutOfBounds => trap_const!(MEMORY_OUT_OF_BOUNDS), 66 | wasmtime::Trap::HeapMisaligned => trap_const!(HEAP_MISALIGNED), 67 | wasmtime::Trap::TableOutOfBounds => trap_const!(TABLE_OUT_OF_BOUNDS), 68 | wasmtime::Trap::IndirectCallToNull => trap_const!(INDIRECT_CALL_TO_NULL), 69 | wasmtime::Trap::BadSignature => trap_const!(BAD_SIGNATURE), 70 | wasmtime::Trap::IntegerOverflow => trap_const!(INTEGER_OVERFLOW), 71 | wasmtime::Trap::IntegerDivisionByZero => trap_const!(INTEGER_DIVISION_BY_ZERO), 72 | wasmtime::Trap::BadConversionToInteger => trap_const!(BAD_CONVERSION_TO_INTEGER), 73 | wasmtime::Trap::UnreachableCodeReached => trap_const!(UNREACHABLE_CODE_REACHED), 74 | wasmtime::Trap::Interrupt => trap_const!(INTERRUPT), 75 | wasmtime::Trap::AlwaysTrapAdapter => trap_const!(ALWAYS_TRAP_ADAPTER), 76 | wasmtime::Trap::OutOfFuel => trap_const!(OUT_OF_FUEL), 77 | // When adding a trap code here, define a matching constant on Wasmtime::Trap (in Ruby) 78 | _ => trap_const!(UNKNOWN), 79 | } 80 | } 81 | 82 | pub fn inspect(rb_self: Obj) -> Result { 83 | Ok(format!( 84 | "#", 85 | rb_self.as_raw(), 86 | rb_self.code()?.into_value().inspect() 87 | )) 88 | } 89 | } 90 | 91 | impl From for Error { 92 | fn from(trap: Trap) -> Self { 93 | magnus::Exception::from_value(Obj::wrap(trap).as_value()) 94 | .unwrap() // Can't fail: Wasmtime::Trap is an Exception 95 | .into() 96 | } 97 | } 98 | 99 | impl TryFrom for Trap { 100 | type Error = wasmtime::Error; 101 | 102 | fn try_from(value: wasmtime::Error) -> Result { 103 | match value.downcast_ref::() { 104 | Some(trap) => { 105 | let trap = trap.to_owned(); 106 | let bt = value.downcast::(); 107 | Ok(Trap::new(trap, bt.map(Some).unwrap_or(None))) 108 | } 109 | None => Err(value), 110 | } 111 | } 112 | } 113 | 114 | pub fn init() -> Result<(), Error> { 115 | let class = trap_error(); 116 | class.define_method("message", method!(Trap::message, 0))?; 117 | class.define_method( 118 | "wasm_backtrace_message", 119 | method!(Trap::wasm_backtrace_message, 0), 120 | )?; 121 | class.define_method("code", method!(Trap::code, 0))?; 122 | class.define_method("inspect", method!(Trap::inspect, 0))?; 123 | class.define_alias("to_s", "message")?; 124 | Ok(()) 125 | } 126 | -------------------------------------------------------------------------------- /ext/src/ruby_api/wasi_ctx.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | root, 3 | wasi_ctx_builder::{file_r, file_w, wasi_file}, 4 | WasiCtxBuilder, 5 | }; 6 | use crate::error; 7 | use crate::helpers::OutputLimitedBuffer; 8 | use deterministic_wasi_ctx::build_wasi_ctx as wasi_deterministic_ctx; 9 | use magnus::{ 10 | class, function, gc::Marker, method, prelude::*, typed_data::Obj, Error, Object, RString, 11 | RTypedData, Ruby, TypedData, Value, 12 | }; 13 | use std::{borrow::Borrow, cell::RefCell, fs::File, path::PathBuf}; 14 | use wasi_common::pipe::{ReadPipe, WritePipe}; 15 | use wasi_common::WasiCtx as WasiCtxImpl; 16 | 17 | /// @yard 18 | /// WASI context to be sent as {Store#new}’s +wasi_ctx+ keyword argument. 19 | /// 20 | /// Instance methods mutate the current object and return +self+. 21 | /// 22 | /// @see https://docs.rs/wasmtime-wasi/latest/wasmtime_wasi/struct.WasiCtx.html 23 | /// Wasmtime's Rust doc 24 | #[magnus::wrap(class = "Wasmtime::WasiCtx", size, free_immediately)] 25 | pub struct WasiCtx { 26 | inner: RefCell, 27 | } 28 | 29 | type RbSelf = Obj; 30 | 31 | impl WasiCtx { 32 | /// @yard 33 | /// Create a new deterministic {WasiCtx}. See https://github.com/Shopify/deterministic-wasi-ctx for more details 34 | /// @return [WasiCtx] 35 | pub fn deterministic() -> Self { 36 | Self { 37 | inner: RefCell::new(wasi_deterministic_ctx()), 38 | } 39 | } 40 | 41 | /// @yard 42 | /// Set stdin to read from the specified file. 43 | /// @def set_stdin_file(path) 44 | /// @param path [String] The path of the file to read from. 45 | /// @return [WasiCtxBuilder] +self+ 46 | fn set_stdin_file(rb_self: RbSelf, path: RString) -> RbSelf { 47 | let inner = rb_self.inner.borrow_mut(); 48 | let cs = file_r(path).map(wasi_file).unwrap(); 49 | inner.set_stdin(cs); 50 | rb_self 51 | } 52 | 53 | /// @yard 54 | /// Set stdin to the specified String. 55 | /// @def set_stdin_string(content) 56 | /// @param content [String] 57 | /// @return [WasiCtx] +self+ 58 | fn set_stdin_string(rb_self: RbSelf, content: RString) -> RbSelf { 59 | let inner = rb_self.inner.borrow_mut(); 60 | let str = unsafe { content.as_slice() }; 61 | let pipe = ReadPipe::from(str); 62 | inner.set_stdin(Box::new(pipe)); 63 | rb_self 64 | } 65 | 66 | /// @yard 67 | /// Set stdout to write to a file. Will truncate the file if it exists, 68 | /// otherwise try to create it. 69 | /// @def set_stdout_file(path) 70 | /// @param path [String] The path of the file to write to. 71 | /// @return [WasiCtx] +self+ 72 | fn set_stdout_file(rb_self: RbSelf, path: RString) -> RbSelf { 73 | let inner = rb_self.inner.borrow_mut(); 74 | let cs = file_w(path).map(wasi_file).unwrap(); 75 | inner.set_stdout(cs); 76 | rb_self 77 | } 78 | 79 | /// @yard 80 | /// Set stdout to write to a string buffer. 81 | /// If the string buffer is frozen, Wasm execution will raise a Wasmtime::Error error. 82 | /// No encoding checks are done on the resulting string, it is the caller's responsibility to ensure the string contains a valid encoding 83 | /// @def set_stdout_buffer(buffer, capacity) 84 | /// @param buffer [String] The string buffer to write to. 85 | /// @param capacity [Integer] The maximum number of bytes that can be written to the output buffer. 86 | /// @return [WasiCtx] +self+ 87 | fn set_stdout_buffer(rb_self: RbSelf, buffer: RString, capacity: usize) -> RbSelf { 88 | let inner = rb_self.inner.borrow_mut(); 89 | let pipe = WritePipe::new(OutputLimitedBuffer::new(buffer.into(), capacity)); 90 | inner.set_stdout(Box::new(pipe)); 91 | rb_self 92 | } 93 | 94 | /// @yard 95 | /// Set stderr to write to a file. Will truncate the file if it exists, 96 | /// otherwise try to create it. 97 | /// @def set_stderr_file(path) 98 | /// @param path [String] The path of the file to write to. 99 | /// @return [WasiCtx] +self+ 100 | fn set_stderr_file(rb_self: RbSelf, path: RString) -> RbSelf { 101 | let inner = rb_self.inner.borrow_mut(); 102 | let cs = file_w(path).map(wasi_file).unwrap(); 103 | inner.set_stderr(cs); 104 | rb_self 105 | } 106 | 107 | /// @yard 108 | /// Set stderr to write to a string buffer. 109 | /// If the string buffer is frozen, Wasm execution will raise a Wasmtime::Error error. 110 | /// No encoding checks are done on the resulting string, it is the caller's responsibility to ensure the string contains a valid encoding 111 | /// @def set_stderr_buffer(buffer, capacity) 112 | /// @param buffer [String] The string buffer to write to. 113 | /// @param capacity [Integer] The maximum number of bytes that can be written to the output buffer. 114 | /// @return [WasiCtx] +self+ 115 | fn set_stderr_buffer(rb_self: RbSelf, buffer: RString, capacity: usize) -> RbSelf { 116 | let inner = rb_self.inner.borrow_mut(); 117 | let pipe = WritePipe::new(OutputLimitedBuffer::new(buffer.into(), capacity)); 118 | inner.set_stderr(Box::new(pipe)); 119 | rb_self 120 | } 121 | 122 | pub fn from_inner(inner: WasiCtxImpl) -> Self { 123 | Self { 124 | inner: RefCell::new(inner), 125 | } 126 | } 127 | 128 | pub fn get_inner(&self) -> WasiCtxImpl { 129 | return self.inner.borrow().clone(); 130 | } 131 | } 132 | 133 | pub fn init() -> Result<(), Error> { 134 | let class = root().define_class("WasiCtx", class::object())?; 135 | class.define_singleton_method("deterministic", function!(WasiCtx::deterministic, 0))?; 136 | class.define_method("set_stdin_file", method!(WasiCtx::set_stdin_file, 1))?; 137 | class.define_method("set_stdin_string", method!(WasiCtx::set_stdin_string, 1))?; 138 | class.define_method("set_stdout_file", method!(WasiCtx::set_stdout_file, 1))?; 139 | class.define_method("set_stdout_buffer", method!(WasiCtx::set_stdout_buffer, 2))?; 140 | class.define_method("set_stderr_file", method!(WasiCtx::set_stderr_file, 1))?; 141 | class.define_method("set_stderr_buffer", method!(WasiCtx::set_stderr_buffer, 2))?; 142 | Ok(()) 143 | } 144 | -------------------------------------------------------------------------------- /lib/wasmtime.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "wasmtime/version" 4 | 5 | module Wasmtime 6 | end 7 | 8 | # Tries to require the extension for the given Ruby version first 9 | begin 10 | RUBY_VERSION =~ /(\d+\.\d+)/ 11 | require "wasmtime/#{Regexp.last_match(1)}/wasmtime_rb" 12 | rescue LoadError 13 | require "wasmtime/wasmtime_rb" 14 | end 15 | -------------------------------------------------------------------------------- /lib/wasmtime/component.rb: -------------------------------------------------------------------------------- 1 | # To prevent double loading of this file when `ruby-api` is enabled 2 | return if defined?(Wasmtime::Component::Result) 3 | 4 | module Wasmtime 5 | module Component 6 | # Represents a component model's +result+ type. 7 | class Result 8 | class << self 9 | # Construct an ok result. 10 | # @param ok [Object] the ok value 11 | # @return [Result] 12 | def ok(ok) 13 | new(true, ok) 14 | end 15 | 16 | # Construct an error result. 17 | # @param error [Object] the error value 18 | # @return [Result] 19 | def error(error) 20 | new(false, error) 21 | end 22 | 23 | private :new 24 | end 25 | 26 | # Returns the ok value of this Result if it is {#ok?}, otherwise raises. 27 | # @raise [UncheckedResult] if this is an error 28 | # @return [Object] 29 | def ok 30 | raise UncheckedResult, "expected ok, was error" unless ok? 31 | 32 | @value 33 | end 34 | 35 | # Returns the error value of this Result if it is {#error?}, otherwise raises. 36 | # @raise [UncheckedResult] if this is an ok 37 | # @return [Object] 38 | def error 39 | raise UncheckedResult, "expected error, was ok" unless error? 40 | 41 | @value 42 | end 43 | 44 | # @return [Boolean] Whether the result is ok 45 | def ok? 46 | @ok 47 | end 48 | 49 | # @return [Boolean] Whether the result is an error 50 | def error? 51 | !@ok 52 | end 53 | 54 | def ==(other) 55 | eql?(other) 56 | end 57 | 58 | def eql?(other) 59 | return false unless self.class == other.class 60 | return false unless ok? == other.ok? 61 | 62 | if ok? 63 | ok == other.ok 64 | else 65 | error == other.error 66 | end 67 | end 68 | 69 | def hash 70 | [self.class, @ok, @value].hash 71 | end 72 | 73 | def initialize(ok, value) 74 | @ok = ok 75 | @value = value 76 | end 77 | 78 | class UncheckedResult < Wasmtime::Error; end 79 | 80 | # Hide the constructor from YARD's doc so that `.ok` or 81 | # `.error` is used over `.new`. 82 | private :initialize 83 | end 84 | 85 | # Represents a value for component model's variant case. 86 | # A variant case has a name that uniquely identify the case within the 87 | # variant and optionally a value. 88 | # 89 | # @example Constructing variants 90 | # # Given the following variant: 91 | # # variant filter { 92 | # # all, 93 | # # none, 94 | # # lt(u32), 95 | # # } 96 | # 97 | # Variant.new("all") 98 | # Variant.new("none") 99 | # Variant.new("lt", 100) 100 | class Variant 101 | # The name of the variant case 102 | # @return [String] 103 | attr_reader :name 104 | 105 | # The optional payload of the variant case 106 | # @return [Object] 107 | attr_reader :value 108 | 109 | # @param name [String] the name of variant case 110 | # @param value [Object] the optional payload of the variant case 111 | def initialize(name, value = nil) 112 | @name = name 113 | @value = value 114 | end 115 | 116 | def ==(other) 117 | eql?(other) 118 | end 119 | 120 | def eql?(other) 121 | self.class == other.class && 122 | name == other.name && 123 | value == other.value 124 | end 125 | 126 | def hash 127 | [self.class, @name, @value].hash 128 | end 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /lib/wasmtime/error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # To prevent double loading of this file when `ruby-api` is enabled 4 | return if defined?(Wasmtime::Error) 5 | 6 | module Wasmtime 7 | class Error < StandardError; end 8 | 9 | # Raised when failing to convert the return value of a Ruby-backed Func to 10 | # Wasm types. 11 | class ResultError < Error; end 12 | 13 | # Raised when converting an {Wasmtime::Extern} to its concrete type fails. 14 | class ConversionError < Error; end 15 | 16 | # Raised on Wasm trap. 17 | class Trap < Error 18 | STACK_OVERFLOW = :stack_overflow 19 | MEMORY_OUT_OF_BOUNDS = :memory_out_of_bounds 20 | HEAP_MISALIGNED = :heap_misaligned 21 | TABLE_OUT_OF_BOUNDS = :table_out_of_bounds 22 | INDIRECT_CALL_TO_NULL = :indirect_call_to_null 23 | BAD_SIGNATURE = :bad_signature 24 | INTEGER_OVERFLOW = :integer_overflow 25 | INTEGER_DIVISION_BY_ZERO = :integer_division_by_zero 26 | BAD_CONVERSION_TO_INTEGER = :bad_conversion_to_integer 27 | UNREACHABLE_CODE_REACHED = :unreachable_code_reached 28 | INTERRUPT = :interrupt 29 | ALWAYS_TRAP_ADAPTER = :always_trap_adapter 30 | OUT_OF_FUEL = :out_of_fuel 31 | UNKNOWN = :unknown 32 | end 33 | 34 | # Raised when a WASI program terminates early by calling +exit+. 35 | class WasiExit < Error 36 | # @return [Integer] The system exit code. 37 | attr_reader(:code) 38 | 39 | def initialize(code) 40 | @code = code 41 | end 42 | 43 | # @return [String] 44 | def message 45 | "WASI exit with code #{code}" 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/wasmtime/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Wasmtime 4 | VERSION = "33.0.0" 5 | end 6 | -------------------------------------------------------------------------------- /rakelib/bench.rake: -------------------------------------------------------------------------------- 1 | namespace :bench do 2 | task all: :compile 3 | 4 | Dir.glob("bench/*.rb").each do |path| 5 | task_name = File.basename(path, ".rb") 6 | next if task_name == "bench" # Bench helper 7 | 8 | desc "Run #{path} benchmark" 9 | task task_name do 10 | sh "ruby -Ilib #{path}" 11 | puts 12 | end 13 | 14 | task all: task_name 15 | end 16 | end 17 | 18 | desc "Run all benchmarks" 19 | task bench: "bench:all" 20 | -------------------------------------------------------------------------------- /rakelib/compile.rake: -------------------------------------------------------------------------------- 1 | require "rb_sys/extensiontask" 2 | 3 | RbSys::ExtensionTask.new("wasmtime-rb", GEMSPEC) do |ext| 4 | ext.lib_dir = "lib/wasmtime" 5 | end 6 | -------------------------------------------------------------------------------- /rakelib/doc.rake: -------------------------------------------------------------------------------- 1 | require "yard/rake/yardoc_task" 2 | 3 | CLOBBER.include("doc") 4 | CLEAN.include(".yardoc") 5 | CLEAN.include("tmp/doc") 6 | 7 | YARD::Rake::YardocTask.new do |t| 8 | t.options += ["--fail-on-warn"] 9 | 10 | t.before = -> { require "yard" } 11 | 12 | t.after = -> do 13 | $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) 14 | 15 | require "wasmtime" 16 | 17 | errors = [] 18 | YARD::Registry.each do |yard_object| 19 | case yard_object.type 20 | when :module 21 | mod = Object.const_get(yard_object.path) 22 | errors << "Not a module: #{mod}" unless mod.is_a?(::Module) 23 | when :class 24 | klass = Object.const_get(yard_object.path) 25 | errors << "Not a class: #{klass}" unless klass.is_a?(::Class) 26 | when :method 27 | namespace = Object.const_get(yard_object.namespace.path) 28 | case yard_object.scope 29 | when :class 30 | namespace.singleton_method(yard_object.name) 31 | when :instance 32 | namespace.instance_method(yard_object.name.to_s) 33 | else 34 | # Unknown scope, we should improve this script 35 | errors << "unknown method scope '#{yard_object.scope}' for #{yard_object.path}" 36 | end 37 | end 38 | rescue NameError => e 39 | errors << "Documented `#{yard_object.path}` not found: \n #{e.message.split("\n").first}" 40 | end 41 | 42 | if errors.any? 43 | errors.each { |error| log.warn(error) } 44 | exit 1 45 | end 46 | end 47 | end 48 | 49 | namespace :doc do 50 | task default: [:rustdoc, :yard] 51 | 52 | desc "Run YARD and Rust documentation servers" 53 | task serve: [:rustdoc, :yard] do 54 | yarddoc_port = 4999 55 | pids = [] 56 | 57 | pids << fork do 58 | mtimes = {} 59 | loop do 60 | sleep 1 61 | new_mtimes = mtimes_for(/\.rs$/) 62 | next if new_mtimes == mtimes 63 | mtimes.replace(new_mtimes) 64 | system "rake doc:rustdoc > /dev/null" || warn("Failed to regenerate Rust documentation") 65 | end 66 | rescue Interrupt 67 | exit 0 68 | end 69 | 70 | pids << Process.spawn("yard server --reload --port #{yarddoc_port}") 71 | 72 | sleep 73 | rescue Interrupt 74 | puts "Shutting down..." 75 | pids.each { |pid| Process.kill("INT", pid) } 76 | pids.each { |pid| Process.wait(pid) } 77 | end 78 | 79 | desc "Run YARD" 80 | task yard: "yard" 81 | 82 | desc "Generate Rust documentation as JSON" 83 | task :rustdoc do 84 | nightly = File.readlines("NIGHTLY_VERSION").first.strip 85 | sh <<~CMD 86 | cargo +#{nightly} rustdoc \ 87 | --target-dir tmp/doc/target \ 88 | -p wasmtime-rb \ 89 | -- -Zunstable-options --output-format json \ 90 | --document-private-items 91 | CMD 92 | 93 | cp "tmp/doc/target/doc/wasmtime_rb.json", "tmp/doc/wasmtime_rb.json" 94 | end 95 | end 96 | 97 | task doc: ["env:dev", "compile", "doc:default"] 98 | -------------------------------------------------------------------------------- /rakelib/env.rake: -------------------------------------------------------------------------------- 1 | namespace :env do 2 | desc 'Sets up environment variables "dev" builds' 3 | task :dev do 4 | ENV["RUST_BACKTRACE"] = "1" 5 | ENV["WASMTIME_BACKTRACE_DETAILS"] = "1" 6 | ENV["RB_SYS_CARGO_PROFILE"] ||= "dev" 7 | end 8 | 9 | desc 'Sets up environment variables "release" builds' 10 | task :release do 11 | ENV["RB_SYS_CARGO_PROFILE"] = "release" 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /rakelib/examples.rake: -------------------------------------------------------------------------------- 1 | namespace :examples do 2 | task all: :compile 3 | 4 | Dir.glob("examples/*.rb").each do |path| 5 | task_name = File.basename(path, ".rb") 6 | 7 | desc "Run #{path}" 8 | task task_name do 9 | sh "ruby -Ilib #{path}" 10 | puts 11 | end 12 | 13 | task all: task_name 14 | end 15 | 16 | desc "Run rust-crate/" 17 | task :rust_crate do 18 | Dir.chdir("examples/rust-crate") do 19 | sh "cargo test" 20 | end 21 | end 22 | 23 | task all: :rust_crate 24 | end 25 | 26 | desc "Run all the examples" 27 | task examples: "examples:all" 28 | -------------------------------------------------------------------------------- /rakelib/helpers.rake: -------------------------------------------------------------------------------- 1 | REPO_FILES = Rake::FileList.new 2 | 3 | def dirglob(pattern) 4 | result = Dir.glob(pattern, File::FNM_DOTMATCH) 5 | raise "No files found for pattern: #{pattern}" if result.empty? 6 | result 7 | end 8 | 9 | def filesize(*files) 10 | bytes = files.sum { |f| File.size(f) } 11 | (bytes / 1024.0 / 1024.0).round(2) 12 | end 13 | 14 | def repo_files 15 | REPO_FILES.include(`git ls-files`.split("\n")) if REPO_FILES.empty? 16 | REPO_FILES 17 | end 18 | 19 | def mtimes_for(regex) 20 | repo_files.each_with_object({}) do |path, mtimes| 21 | next unless path.match?(regex) 22 | mtimes[path] = File.mtime(path) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /rakelib/mem.rake: -------------------------------------------------------------------------------- 1 | class SomeError < StandardError; end 2 | 3 | namespace :mem do 4 | desc "Runs a WebAssembly function ENV['TIMES'] in a loop looking for memory leaks. " 5 | task :growth do 6 | require "wasmtime" 7 | require "get_process_mem" 8 | 9 | precompiled = Wasmtime::Engine.new.precompile_module(<<~WAT) 10 | (module 11 | (import "" "" (func (param externref) (result externref))) 12 | (import "" "" (func)) 13 | (func $module/hello (result i32 i64 f32 f64) 14 | i32.const 1 15 | i64.const 2 16 | f32.const 3.0 17 | f64.const 4.0 18 | ) 19 | 20 | (export "hello" (func $module/hello)) 21 | (export "f0" (func 0)) 22 | (export "f1" (func 1)) 23 | ) 24 | WAT 25 | 26 | wasmtime_interaction = -> do 27 | engine = Wasmtime::Engine.new 28 | store = Wasmtime::Store.new(engine, {}) 29 | mod = Wasmtime::Module.deserialize(engine, precompiled) 30 | import0 = Wasmtime::Func.new(store, [:externref], [:externref]) { |o| o } 31 | import1 = Wasmtime::Func.new(store, [], []) { raise SomeError } 32 | instance = Wasmtime::Instance.new(store, mod, [import0, import1]) 33 | instance.invoke("hello") 34 | instance.invoke("f0", BasicObject.new) 35 | begin 36 | instance.invoke("f1") 37 | rescue SomeError # no-op 38 | end 39 | end 40 | 41 | wasmtime_interaction.call # warm-up 42 | GC.start 43 | 44 | before = GetProcessMem.new.kb 45 | iterations = (ENV["TIMES"] || 500_000).to_i 46 | gc_every = (ENV["GC_EVERY"] || iterations / 100).to_i 47 | iterations.to_i.times do |i| 48 | wasmtime_interaction.call 49 | if i % gc_every == 0 50 | GC.start 51 | after = GetProcessMem.new.kb 52 | if before != after 53 | puts format("Mem change: %d KiB -> %d KiB (%+d), i=#{i}", before, after, after - before) 54 | end 55 | before = after 56 | end 57 | end 58 | end 59 | 60 | if RbConfig::CONFIG["host_os"] == "linux" 61 | begin 62 | require "ruby_memcheck" 63 | require "ruby_memcheck/rspec/rake_task" 64 | 65 | RubyMemcheck.config(binary_name: "ext") 66 | 67 | RubyMemcheck::RSpec::RakeTask.new(check: "compile:dev") 68 | rescue LoadError 69 | task :check do 70 | abort 'Please add `gem "ruby_memcheck"` to your Gemfile to use the "mem:check" task' 71 | end 72 | end 73 | else 74 | task :check do 75 | abort 'The "mem:check" task is only available on Linux' 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /rakelib/pkg.rake: -------------------------------------------------------------------------------- 1 | CLOBBER.include("pkg/**/*.gem") 2 | CLEAN.include("tmp/pkg") 3 | CLEAN.include("tmp/pkg") 4 | 5 | def gem_install_test(dotgem) 6 | dotgem = File.expand_path(File.join("..", dotgem), __dir__) 7 | tmpdir = File.expand_path("../tmp/pkg-test-#{Time.now.to_i}", __dir__) 8 | sh "gem install --verbose --install-dir #{tmpdir} #{dotgem}" 9 | 10 | wrapper = if defined?(Bundler) 11 | ->(&blk) { Bundler.with_unbundled_env { blk.call } } 12 | else 13 | ->(&blk) { blk.call } 14 | end 15 | 16 | testrun = ->(cmd) do 17 | cmd = cmd.chomp 18 | 19 | wrapper.call do 20 | old = ENV["GEM_HOME"] 21 | ENV["GEM_HOME"] = tmpdir 22 | ruby "-rwasmtime -e '(#{cmd}) || abort'" 23 | puts "✅ Passed (#{cmd})" 24 | rescue 25 | abort "❌ Failed (#{cmd})" 26 | ensure 27 | ENV["GEM_HOME"] = old 28 | end 29 | end 30 | 31 | testrun.call <<~RUBY 32 | Wasmtime::VERSION == "#{GEMSPEC.version}" 33 | RUBY 34 | 35 | testrun.call <<~RUBY 36 | Wasmtime::Engine.new.precompile_module("(module)").include?("ELF") 37 | RUBY 38 | 39 | FileUtils.rm_rf(tmpdir) 40 | end 41 | 42 | namespace :pkg do 43 | directory "pkg" 44 | 45 | desc "Build the source gem (#{GEMSPEC.name}-#{GEMSPEC.version}.gem)" 46 | task ruby: "pkg" do 47 | slug = "#{GEMSPEC.name}-#{GEMSPEC.version}" 48 | output_gempath = File.expand_path("../pkg/#{slug}.gem", __dir__) 49 | gemspec_path = "wasmtime.gemspec" 50 | base_dir = File.join("tmp/pkg", slug) 51 | staging_dir = File.join(base_dir, "stage") 52 | unpacked_dir = File.join(base_dir, "unpacked") 53 | vendor_dir = "ext/cargo-vendor" # this file gets cleaned up during gem install 54 | staging_gem_path = File.join(staging_dir, "#{slug}.gem") 55 | 56 | puts "Building source gem..." 57 | 58 | rm(output_gempath) if File.exist?(output_gempath) 59 | rm_rf(staging_dir) 60 | rm_rf(unpacked_dir) 61 | mkdir_p(staging_dir) 62 | cp(gemspec_path, staging_dir) 63 | 64 | GEMSPEC.files.each do |file| 65 | dest = File.join(staging_dir, file) 66 | mkdir_p(File.dirname(dest)) 67 | cp(file, dest) if File.file?(file) 68 | end 69 | 70 | Dir.chdir(staging_dir) do 71 | cargo_config_path = ".cargo/config" 72 | final_gemspec = Gem::Specification.load(File.basename(gemspec_path)) 73 | 74 | puts "Vendoring cargo dependencies to #{cargo_config_path}..." 75 | mkdir_p ".cargo" 76 | sh "cargo vendor --versioned-dirs --locked #{vendor_dir} >> #{cargo_config_path} 2>/dev/null" 77 | 78 | vendor_files = dirglob("./#{vendor_dir}/**/*").reject { |f| File.directory?(f) } 79 | # Ensure that all vendor files have the right read permissions, 80 | # which are needed to build the gem. 81 | # The permissions that we want _at least_ is readable by all for example `.rw-r--r--` 82 | vendor_files.each { |f| FileUtils.chmod("a+r", f) } 83 | final_gemspec.files += vendor_files 84 | final_gemspec.files += dirglob("**/.cargo/**/*").reject { |f| File.directory?(f) } 85 | 86 | puts "Building gem to #{unpacked_dir}.gem..." 87 | Gem::Package.build(final_gemspec, false, true, "#{slug}.gem") 88 | end 89 | 90 | puts "Unpacking gem to #{unpacked_dir}..." 91 | sh "gem unpack #{staging_gem_path} --target #{File.dirname(unpacked_dir)} --quiet" 92 | mv File.join(base_dir, slug), unpacked_dir 93 | 94 | puts "Verifying cargo dependencies are vendored..." 95 | sh "cargo verify-project --manifest-path #{File.join(unpacked_dir, "Cargo.toml")}" 96 | 97 | cp staging_gem_path, output_gempath 98 | 99 | puts <<~STATS 100 | \n\e[1m==== Source gem stats (#{File.basename(output_gempath)}) ====\e[0m 101 | - Path: #{output_gempath.delete_prefix(Dir.pwd + "/")} 102 | - Number of files: #{dirglob("#{unpacked_dir}/**/*").count} 103 | - Number of vendored deps: #{dirglob("#{unpacked_dir}/#{vendor_dir}/*").count} 104 | - Size (packed): #{filesize(output_gempath)} MB 105 | - Size (unpacked): #{filesize(*dirglob("#{unpacked_dir}/**/*"))} MB 106 | STATS 107 | end 108 | 109 | desc "Test source gem installation" 110 | task "ruby:test" => "pkg:ruby" do 111 | gem_install_test("pkg/#{GEMSPEC.name}-#{GEMSPEC.version}.gem") 112 | end 113 | 114 | ["x86_64-darwin", "arm64-darwin", "x86_64-linux"].each do |platform| 115 | desc "Test #{platform} gem installation" 116 | task "#{platform}:test" do 117 | gem_install_test("pkg/#{GEMSPEC.name}-#{GEMSPEC.version}-#{platform}.gem") 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /rakelib/spec.rake: -------------------------------------------------------------------------------- 1 | CLEAN.include(".rspec_status") 2 | 3 | begin 4 | require "rspec/core/rake_task" 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | rescue LoadError 8 | # No RSpec installed 9 | end 10 | -------------------------------------------------------------------------------- /spec/convert_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module Wasmtime 4 | RSpec.describe "Type conversions" do 5 | describe "for numbers" do 6 | [ 7 | [:i32, 4], 8 | [:i64, 2**40], 9 | [:f32, 5.5], 10 | [:f64, 5.5] 11 | ].each do |type, value| 12 | it "converts #{type} back and forth" do 13 | expect(roundtrip_value(type, value)).to eq(value) 14 | end 15 | end 16 | 17 | it "raises on i32 overflow" do 18 | expect { roundtrip_value(:i32, 2**50) }.to raise_error(RangeError) 19 | end 20 | 21 | it "raises on i64 overflow" do 22 | expect { roundtrip_value(:i64, 2**65) }.to raise_error(RangeError) 23 | end 24 | 25 | it "returns FLOAT::INFINITY on f32 overflow" do 26 | expect(roundtrip_value(:f32, 5 * 10**40)).to eq(Float::INFINITY) 27 | end 28 | 29 | it "returns FLOAT::INFINITY on f64 overflow" do 30 | expect(roundtrip_value(:f64, 2 * 10**310)).to eq(Float::INFINITY) 31 | end 32 | end 33 | 34 | describe "for externref" do 35 | let(:basic_object) { BasicObject.new } 36 | 37 | it("converts nil back and forth") { expect(roundtrip_value(:externref, nil)).to be_nil } 38 | it("converts string back and forth") { expect(roundtrip_value(:externref, "foo")).to eq("foo") } 39 | it("converts a nil funcref back and forth") { expect(roundtrip_value(:funcref, nil)).to be_nil } 40 | it "converts BasicObject back and forth" do 41 | expect(roundtrip_value(:externref, basic_object)).to equal(basic_object) 42 | end 43 | end 44 | 45 | it "converts ref.null to nil" do 46 | instance = compile(<<~WAT) 47 | (module 48 | (func (export "main") (result externref) 49 | ref.null extern)) 50 | WAT 51 | expect(instance.invoke("main")).to be_nil 52 | end 53 | 54 | describe "for funcref" do 55 | it "converts back and forth" do 56 | store = Store.new(engine) 57 | f1 = Func.new(store, [], []) {} 58 | f2 = Func.new(store, [:funcref], [:funcref]) { |_, arg1| arg1 } 59 | returned_func = f2.call(f1) 60 | expect(returned_func).to be_instance_of(Func) 61 | end 62 | 63 | it "converts ref.null to nil" do 64 | instance = compile(<<~WAT) 65 | (module 66 | (func (export "main") (result funcref) 67 | ref.null func)) 68 | WAT 69 | expect(instance.invoke("main")).to be_nil 70 | end 71 | end 72 | 73 | private 74 | 75 | def roundtrip_value(type, value) 76 | Func 77 | .new(Store.new(engine), [type], [type]) do |_caller, arg| 78 | arg 79 | end 80 | .call(value) 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/fixtures/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore lockfile for fixtures to avoid irrelevant GitHub security alerts 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /spec/fixtures/component-types/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-unknown-unknown" 3 | -------------------------------------------------------------------------------- /spec/fixtures/component-types/.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | src/bindings.rs 3 | -------------------------------------------------------------------------------- /spec/fixtures/component-types/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.check.overrideCommand": [ 3 | "cargo", 4 | "component", 5 | "check", 6 | "--workspace", 7 | "--all-targets", 8 | "--message-format=json" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /spec/fixtures/component-types/.zed/settings.json: -------------------------------------------------------------------------------- 1 | // Folder-specific settings 2 | // 3 | // For a full list of overridable settings, and general information on folder-specific settings, 4 | // see the documentation: https://zed.dev/docs/configuring-zed#settings-files 5 | { 6 | "lsp": { 7 | "rust-analyzer": { 8 | "check": { 9 | "overrideCommand": [ 10 | "cargo", 11 | "component", 12 | "check", 13 | "--workspace", 14 | "--all-targets", 15 | "--message-format=json" 16 | ] 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /spec/fixtures/component-types/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "component-types" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | paste = "1.0.15" 8 | wit-bindgen-rt = { version = "0.33.0", features = ["bitflags"] } 9 | 10 | [lib] 11 | crate-type = ["cdylib"] 12 | 13 | [profile.release] 14 | codegen-units = 1 15 | opt-level = "s" 16 | debug = false 17 | strip = true 18 | lto = true 19 | 20 | [package.metadata.component] 21 | package = "fixtures:component-types" 22 | 23 | [package.metadata.component.dependencies] 24 | -------------------------------------------------------------------------------- /spec/fixtures/component-types/README.md: -------------------------------------------------------------------------------- 1 | Wasm component fixture to test converting types back and forth between the guest 2 | and the Ruby host. 3 | 4 | Prerequisite: `cargo install cargo-component` 5 | 6 | To rebuild, run the following from the wasmtime-rb's root: 7 | ``` 8 | ( 9 | cd spec/fixtures/component-types && \ 10 | cargo component build --release && \ 11 | cp target/wasm32-unknown-unknown/release/component_types.wasm ../ 12 | ) 13 | ``` 14 | -------------------------------------------------------------------------------- /spec/fixtures/component-types/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[allow(warnings)] 2 | mod bindings; 3 | 4 | use bindings::exports::resource; 5 | use bindings::Guest; 6 | use paste::paste; 7 | 8 | macro_rules! id_function { 9 | ($wasm_ty:ident, $rust_ty:ty) => { 10 | paste! { 11 | fn [](v: $rust_ty) -> $rust_ty { 12 | v 13 | } 14 | } 15 | }; 16 | } 17 | 18 | struct WrappedString(String); 19 | impl resource::GuestWrappedString for WrappedString { 20 | fn new(v: String) -> Self { 21 | Self(v) 22 | } 23 | 24 | fn to_string(&self) -> String { 25 | self.0.clone() 26 | } 27 | } 28 | 29 | struct Component; 30 | impl resource::Guest for Component { 31 | type WrappedString = WrappedString; 32 | 33 | fn resource_owned(_: resource::WrappedString) {} 34 | } 35 | 36 | impl Guest for Component { 37 | id_function!(bool, bool); 38 | id_function!(s8, i8); 39 | id_function!(u8, u8); 40 | id_function!(s16, i16); 41 | id_function!(u16, u16); 42 | id_function!(s32, i32); 43 | id_function!(u32, u32); 44 | id_function!(s64, i64); 45 | id_function!(u64, u64); 46 | id_function!(f32, f32); 47 | id_function!(f64, f64); 48 | id_function!(char, char); 49 | id_function!(string, String); 50 | id_function!(list, Vec); 51 | id_function!(record, bindings::Point); 52 | id_function!(tuple, (u32, String)); 53 | id_function!(variant, bindings::Filter); 54 | id_function!(enum, bindings::Size); 55 | id_function!(option, Option); 56 | id_function!(result, Result); 57 | id_function!(flags, bindings::Permission); 58 | id_function!(result_unit, Result<(), ()>); 59 | } 60 | 61 | bindings::export!(Component with_types_in bindings); 62 | -------------------------------------------------------------------------------- /spec/fixtures/component-types/wit/world.wit: -------------------------------------------------------------------------------- 1 | package fixtures:component-types; 2 | 3 | world fixtures { 4 | record point { 5 | x: u32, 6 | y: u32, 7 | } 8 | variant filter { 9 | all, 10 | none, 11 | lt(u32), 12 | } 13 | enum size { 14 | s, 15 | m, 16 | l, 17 | } 18 | flags permission { 19 | read, 20 | write, 21 | exec, 22 | } 23 | 24 | export id-bool: func(v: bool) -> bool; 25 | export id-s8: func(v: s8) -> s8; 26 | export id-u8: func(v: u8) -> u8; 27 | export id-s16: func(v: s16) -> s16; 28 | export id-u16: func(v: u16) -> u16; 29 | export id-s32: func(v: s32) -> s32; 30 | export id-u32: func(v: u32) -> u32; 31 | export id-s64: func(v: s64) -> s64; 32 | export id-u64: func(v: u64) -> u64; 33 | export id-f32: func(v: f32) -> f32; 34 | export id-f64: func(v: f64) -> f64; 35 | export id-char: func(v: char) -> char; 36 | export id-string: func(v: string) -> string; 37 | export id-list: func(v: list) -> list; 38 | export id-record: func(v: point) -> point; 39 | export id-tuple: func(v: tuple) -> tuple; 40 | export id-variant: func(v: filter) -> filter; 41 | export id-enum: func(v: size) -> size; 42 | export id-option: func(v: option) -> option; 43 | export id-result: func(v: result) -> result; 44 | export id-result-unit: func(v: result) -> result; 45 | export id-flags: func(v: permission) -> permission; 46 | 47 | export %resource: interface { 48 | resource wrapped-string { 49 | constructor(v: string); 50 | to-string: func() -> string; 51 | } 52 | resource-owned: func(v: wrapped-string); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /spec/fixtures/component_adder.wat: -------------------------------------------------------------------------------- 1 | (component 2 | ;; Define a nested component so we can export an instance of a component 3 | (component $c 4 | (core module $m 5 | (func (export "add") (param $a i32) (param $b i32) (result i32) 6 | local.get $a 7 | local.get $b 8 | i32.add 9 | ) 10 | ) 11 | (core instance $i (instantiate $m)) 12 | (func $add (param "a" s32) (param "b" s32) (result s32) (canon lift (core func $i "add"))) 13 | (export "add" (func $add)) 14 | ) 15 | (instance $adder (instantiate $c)) 16 | 17 | ;; Export the adder instance 18 | (export "adder" (instance $adder)) 19 | 20 | ;; Re-export add as a top level 21 | (export "add" (func $adder "add")) 22 | ) 23 | -------------------------------------------------------------------------------- /spec/fixtures/component_trap.wat: -------------------------------------------------------------------------------- 1 | (component 2 | (core module $m 3 | (func (export "unreachable") (unreachable)) 4 | ) 5 | (core instance $i (instantiate $m)) 6 | (func $unreachable (canon lift (core func $i "unreachable"))) 7 | (export "unreachable" (func $unreachable)) 8 | ) 9 | -------------------------------------------------------------------------------- /spec/fixtures/component_types.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytecodealliance/wasmtime-rb/673ec9c455e045fbfaac8853aa7d37b460643ca6/spec/fixtures/component_types.wasm -------------------------------------------------------------------------------- /spec/fixtures/empty.wat: -------------------------------------------------------------------------------- 1 | (module) 2 | -------------------------------------------------------------------------------- /spec/fixtures/empty_component.wat: -------------------------------------------------------------------------------- 1 | (component) 2 | -------------------------------------------------------------------------------- /spec/fixtures/wasi-debug.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytecodealliance/wasmtime-rb/673ec9c455e045fbfaac8853aa7d37b460643ca6/spec/fixtures/wasi-debug.wasm -------------------------------------------------------------------------------- /spec/fixtures/wasi-debug/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-wasip1" 3 | -------------------------------------------------------------------------------- /spec/fixtures/wasi-debug/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasi-debug" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [workspace] 9 | 10 | [dependencies] 11 | miniserde = "0.1.27" 12 | -------------------------------------------------------------------------------- /spec/fixtures/wasi-debug/README.md: -------------------------------------------------------------------------------- 1 | Example WASI program used to test the WASI integration. 2 | 3 | To update: 4 | 5 | ```shell 6 | cargo build --release && \ 7 | wasm-opt -O \ 8 | --enable-bulk-memory \ 9 | target/wasm32-wasip1/release/wasi-debug.wasm \ 10 | -o ../wasi-debug.wasm 11 | ``` 12 | -------------------------------------------------------------------------------- /spec/fixtures/wasi-debug/src/main.rs: -------------------------------------------------------------------------------- 1 | use miniserde::{json, Serialize}; 2 | use std::io::{Write, Read}; 3 | 4 | #[derive(Serialize)] 5 | struct Wasi { 6 | args: Vec, 7 | env: Vec<(String, String)>, 8 | pwd: String, 9 | stdin: String, 10 | } 11 | 12 | #[derive(Serialize)] 13 | struct Log<'a> { 14 | name: &'static str, 15 | wasi: &'a Wasi, 16 | } 17 | 18 | fn main() { 19 | let args: Vec = std::env::args().collect(); 20 | let env: Vec<(String, String)> = std::env::vars().collect(); 21 | let pwd : String = std::env::current_dir() 22 | .expect("current working directory") 23 | .to_string_lossy() 24 | .into(); 25 | 26 | let mut stdin = String::new(); 27 | std::io::stdin().read_to_string(&mut stdin) 28 | .expect("failed to read stdin"); 29 | 30 | let wasi = Wasi {args, env, pwd, stdin }; 31 | let stdout = Log { name: "stdout", wasi: &wasi }; 32 | let stderr = Log { name: "stderr", wasi: &wasi }; 33 | 34 | std::io::stdout().write_all(json::to_string(&stdout).as_bytes()) 35 | .expect("failed to write to stdout"); 36 | std::io::stderr().write_all(json::to_string(&stderr).as_bytes()) 37 | .expect("failed to write to stderr"); 38 | } 39 | -------------------------------------------------------------------------------- /spec/fixtures/wasi-deterministic.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytecodealliance/wasmtime-rb/673ec9c455e045fbfaac8853aa7d37b460643ca6/spec/fixtures/wasi-deterministic.wasm -------------------------------------------------------------------------------- /spec/fixtures/wasi-deterministic/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-wasip1" 3 | -------------------------------------------------------------------------------- /spec/fixtures/wasi-deterministic/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasi-deterministic" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [profile.release] 7 | codegen-units = 1 8 | opt-level = "s" 9 | debug = false 10 | strip = true 11 | lto = true 12 | 13 | [dependencies] 14 | chrono = "0.4.38" 15 | rand = "0.8.5" 16 | serde = "1.0.210" 17 | serde_json = "1.0.128" 18 | -------------------------------------------------------------------------------- /spec/fixtures/wasi-deterministic/README.md: -------------------------------------------------------------------------------- 1 | Example WASI program used to test the WASI deterministic context integration. 2 | 3 | To update: 4 | 5 | ```shell 6 | cargo build --release && \ 7 | wasm-opt -O \ 8 | --enable-bulk-memory \ 9 | target/wasm32-wasip1/release/wasi-deterministic.wasm \ 10 | -o ../wasi-deterministic.wasm 11 | ``` 12 | 13 | -------------------------------------------------------------------------------- /spec/fixtures/wasi-deterministic/src/main.rs: -------------------------------------------------------------------------------- 1 | // In a determinisitc build, the output of this program should be the same 2 | // for every execution. 3 | // 4 | // This program generates random numbers, sleeps for 2 seconds, 5 | // and prints to stdout and stderr. 6 | // 7 | // Expected Output: 8 | // 9 | // stdout: json string with the following keys: 10 | // - Random numbers: rang1, rang2, rang3 11 | // - UTC time before sleep: utc1 12 | // - UTC time after sleep: utc2 13 | // - System time before sleep: system_time1 14 | // - System time after sleep: system_time2 15 | // - Elapsed time: system_time1_elapsed 16 | // 17 | // stderr: "Error: This is an error message" 18 | 19 | // Import rust's io and filesystem module 20 | use chrono::{DateTime, Utc}; 21 | use rand::Rng; 22 | use serde_json; 23 | use std::collections::HashMap; 24 | use std::thread::sleep; 25 | use std::time::{Duration, SystemTime}; 26 | 27 | // Entry point to our WASI applications 28 | fn main() { 29 | // Define a dict to store our output 30 | let mut dict = HashMap::new(); 31 | 32 | // Define random numbers 33 | let mut rng = rand::thread_rng(); 34 | let n1: u32 = rng.gen(); 35 | let n2: u32 = rng.gen(); 36 | let n3: u32 = rng.gen(); 37 | 38 | dict.insert("rang1".to_string(), n1.to_string()); 39 | dict.insert("rang2".to_string(), n2.to_string()); 40 | dict.insert("rang3".to_string(), n3.to_string()); 41 | 42 | let utc1 = Utc::now(); 43 | let utc1_str = utc1.format("%+").to_string(); 44 | dict.insert("utc1".to_string(), utc1_str); 45 | 46 | // Define system time, elaspsed time 47 | let system_time1 = SystemTime::now(); 48 | 49 | let date_time1: DateTime = system_time1.into(); 50 | let system_time_str = date_time1.format("%+"); 51 | dict.insert("system_time1".to_string(), system_time_str.to_string()); 52 | 53 | // we sleep for 2 seconds 54 | sleep(Duration::new(2, 0)); 55 | match system_time1.elapsed() { 56 | Ok(elapsed) => { 57 | // it prints '2' 58 | println!("{}", elapsed.as_secs()); 59 | dict.insert( 60 | "system_time1_elapsed".to_string(), 61 | elapsed.as_secs().to_string(), 62 | ); 63 | } 64 | Err(e) => { 65 | // an error occurred! 66 | println!("Error: {e:?}"); 67 | } 68 | } 69 | 70 | // Declare a new UTC after the pause 71 | let utc2 = Utc::now(); 72 | let utc2_str = utc2.format("%+").to_string(); 73 | dict.insert("utc2".to_string(), utc2_str); 74 | 75 | let json = serde_json::to_string(&dict).unwrap(); 76 | 77 | // write to stdout 78 | println!("{}", json); 79 | 80 | // write to stderr 81 | eprintln!("Error: {}", "This is an error message"); 82 | } 83 | -------------------------------------------------------------------------------- /spec/integration/epoch_interruption_spec.rb: -------------------------------------------------------------------------------- 1 | module Wasmtime 2 | RSpec.describe "Epoch interruption" do 3 | let(:engine) { Engine.new(epoch_interruption: true) } 4 | 5 | let(:store_deadline_0) { Store.new(engine) } 6 | let(:store_deadline_1) { Store.new(engine).tap { |store| store.set_epoch_deadline(1) } } 7 | 8 | let(:mod) do 9 | Module.new(engine, <<~WAT) 10 | (module 11 | (func (export "42") (result i32) 12 | (i32.const 42)) 13 | (func (export "loop_forever") 14 | (loop br 0))) 15 | WAT 16 | end 17 | 18 | let(:autostart_mod) do 19 | Module.new(engine, <<~WAT) 20 | (module 21 | (func nop) 22 | (start 0)) 23 | WAT 24 | end 25 | 26 | it "starts with epoch deadline 0 and traps immediately" do 27 | instance = Instance.new(store_deadline_0, mod) 28 | 29 | expect { instance.invoke("42") }.to raise_error(Trap) do |trap| 30 | expect(trap.code).to eq(:interrupt) 31 | end 32 | 33 | expect { Instance.new(store_deadline_0, autostart_mod) }.to raise_error(Trap) 34 | end 35 | 36 | it "runs to completion when epoch deadline is non-zero" do 37 | instance = Instance.new(store_deadline_1, mod) 38 | expect(instance.invoke("42")).to eq(42) 39 | 40 | expect { Instance.new(store_deadline_1, autostart_mod) }.not_to raise_error 41 | end 42 | 43 | it "allows incrementing epoch manually" do 44 | instance = Instance.new(store_deadline_1, mod) 45 | # No error: engine is still on epoch 0 46 | instance.invoke("42") 47 | 48 | engine.increment_epoch 49 | expect { instance.invoke("42") }.to raise_error(Trap) 50 | end 51 | 52 | describe "Engine timer" do 53 | it "prevents infinite loop from running forever" do 54 | instance = Instance.new(store_deadline_1, mod) 55 | engine.start_epoch_interval(10) 56 | expect { instance.invoke("loop_forever") }.to raise_error(Trap) 57 | end 58 | 59 | it "can stop a previously started timer" do 60 | store = Store.new(engine) 61 | engine.start_epoch_interval(1) 62 | engine.stop_epoch_interval 63 | store.set_epoch_deadline(1) 64 | 65 | sleep_ms(5) 66 | 67 | expect { Instance.new(store, autostart_mod) }.not_to raise_error 68 | end 69 | 70 | it "can start and stop timers at will" do 71 | engine.stop_epoch_interval 72 | engine.start_epoch_interval(1) 73 | engine.start_epoch_interval(2) 74 | engine.stop_epoch_interval 75 | engine.stop_epoch_interval 76 | end 77 | 78 | it "does not interrupt host call" do 79 | host_call_finished = false 80 | mod = Module.new(engine, <<~WAT) 81 | (module 82 | (func $host_call (import "" "")) 83 | (func $noop) 84 | (func (export "f") 85 | call $host_call 86 | call $noop ;; new func call forces epoch check 87 | ) 88 | ) 89 | WAT 90 | f = Func.new(store_deadline_1, [], []) do |c| 91 | sleep_ms(30) 92 | engine.increment_epoch 93 | host_call_finished = true 94 | end 95 | 96 | instance = Instance.new(store_deadline_1, mod, [f]) 97 | # GC stress makes Ruby very slow; we always tick before intering Wasm. 98 | engine.start_epoch_interval(1) unless GC.stress 99 | expect { instance.invoke("f") }.to raise_error(Trap) 100 | expect(host_call_finished).to be true 101 | end 102 | end 103 | 104 | def sleep_ms(ms) 105 | sleep ms.to_f / 1000 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /spec/integration/hello_world_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe "Hello World" do 2 | it "properly converts return args (i32, i64, f32, f64)" do 3 | instance = compile <<~WAT 4 | (module 5 | (func $module/hello (result i32 i64 f32 f64) 6 | i32.const 1 7 | i64.const 2 8 | f32.const 3.0 9 | f64.const 4.0 10 | ) 11 | 12 | (export "hello" (func $module/hello)) 13 | ) 14 | WAT 15 | 16 | result = instance.invoke("hello") 17 | 18 | expect(result).to eq([1, 2, 3.0, 4.0]) 19 | end 20 | 21 | it "can accept basic args" do 22 | instance = compile <<~WAT 23 | (module 24 | (func $module/add_three (param $0 i32) (param $1 i64) (param $2 f32) (param $3 f64) (result i32 i64 f32 f64) 25 | local.get $0 26 | i32.const 3 27 | i32.add 28 | 29 | local.get $1 30 | i64.const 3 31 | i64.add 32 | 33 | local.get $2 34 | f32.const 3.0 35 | f32.add 36 | 37 | local.get $3 38 | f64.const 3.0 39 | f64.add 40 | ) 41 | (export "add_three" (func $module/add_three)) 42 | ) 43 | WAT 44 | result = instance.invoke("add_three", 1, 2, 3.0, 4.0) 45 | 46 | expect(result).to eq([4, 5, 6.0, 7.0]) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/integration/insanity_spec.rb: -------------------------------------------------------------------------------- 1 | require "securerandom" 2 | 3 | module Wasmtime 4 | # Note: the heavy usage of `without_gc_stress` is to make the specs reasonably 5 | # performant to run locally. These specs are meant to to smoke test the gem by 6 | # exercising the most insane edge cases we can think of. 7 | RSpec.describe "Insanity" do 8 | # see https://github.com/bytecodealliance/wasmtime-rb/issues/156 9 | it "ensures result errors are never GC'd" do 10 | store = Store.new(engine, Object.new) 11 | func = Func.new(store, [], [:i32, :i32]) { [1, nil] } 12 | 13 | n_times.times do 14 | func.call 15 | rescue Wasmtime::ResultError 16 | end 17 | end 18 | 19 | it "ensures user exceptions are never GC'd" do 20 | store = Store.new(engine, Object.new) 21 | call_times = 0 22 | func = Func.new(store, [], [:i32, :i32]) do 23 | call_times += 1 24 | # most GC-able exception ever? 25 | raise Class.new(StandardError).new((+"hello") + SecureRandom.hex(6)) 26 | end 27 | 28 | n_times.times do |i| 29 | expect { func.call }.to raise_error(StandardError, /^hello\h{12}$/) 30 | end 31 | 32 | expect(call_times).to eq(n_times) 33 | end 34 | 35 | it "ensures results are never GC'd" do 36 | n_times = n_times(max: 100) 37 | store = Store.new(engine, Object.new) 38 | results_length = 512 39 | big_array = without_gc_stress { Array.new(results_length) { :i32 } } 40 | expected_result = without_gc_stress { Array.new(results_length) { |i| i.to_s.to_i } } 41 | 42 | func = Func.new(store, [], big_array) { Array.new(results_length) { |i| i } } 43 | 44 | n_times.times do 45 | expect(func.call).to eq(expected_result) 46 | end 47 | end 48 | 49 | it "ensures params are never GC'd" do 50 | n_times = n_times(max: 100) 51 | arrayish = Struct.new(:to_ary) 52 | size = 8 53 | params_type = without_gc_stress { Array.new(size) { :i32 } } 54 | results_type = without_gc_stress { arrayish.new(Array.new(size) { :i32 }) } 55 | params = build_sequential_int_array(size) 56 | called_times = 0 57 | 58 | store = Store.new(engine, Object.new) 59 | func = Func.new(store, params_type, results_type) do |_, *args| 60 | called_times += 1 61 | 62 | fiber = Fiber.new do 63 | result = without_gc_stress { Struct.new(:to_ary).new([*args]) } 64 | Fiber.yield "yielded" 65 | Fiber.yield result 66 | end 67 | 68 | result = fiber.resume 69 | without_gc_stress { expect(result).to eq("yielded") } 70 | fiber.resume 71 | end 72 | 73 | expected_result = build_sequential_int_array(size) 74 | 75 | n_times.times do 76 | result = func.call(*params) 77 | 78 | without_gc_stress do 79 | expect(result).to eq(expected_result) 80 | end 81 | end 82 | 83 | expect(called_times).to eq(n_times) 84 | end 85 | 86 | def n_times(max: 1000) 87 | ENV["GC_STRESS"] ? 1 : max 88 | end 89 | 90 | def build_sequential_int_array(size) 91 | without_gc_stress { Array.new(size) { |i| i } } 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /spec/integration/ractor_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe "Ractor", ractor: true do 2 | let(:wat) { <<~WAT } 3 | (module 4 | (func $module/hello (result i32 i64 f32 f64) 5 | i32.const 1 6 | i64.const 2 7 | f32.const 3.0 8 | f64.const 4.0 9 | ) 10 | 11 | (export "hello" (func $module/hello)) 12 | ) 13 | WAT 14 | 15 | it "supports running inside Ractors" do 16 | r = Ractor.new(wat) do |wat| 17 | engine = Wasmtime::Engine.new 18 | mod = Wasmtime::Module.new(engine, wat) 19 | store_data = Object.new 20 | store = Wasmtime::Store.new(engine, store_data) 21 | Wasmtime::Instance.new(store, mod).invoke("hello") 22 | end 23 | 24 | result = r.take 25 | expect(result).to eq([1, 2, 3.0, 4.0]) 26 | end 27 | 28 | it "supports sharing Engine & Module with Ractors" do 29 | engine = Wasmtime::Engine.new 30 | mod = Wasmtime::Module.new(engine, wat) 31 | 32 | Ractor.make_shareable(engine) 33 | Ractor.make_shareable(mod) 34 | 35 | ractors = [] 36 | 3.times do 37 | ractors << Ractor.new(engine, mod) do |engine, mod| 38 | store_data = Object.new 39 | store = Wasmtime::Store.new(engine, store_data) 40 | Wasmtime::Instance.new(store, mod).invoke("hello") 41 | end 42 | end 43 | 44 | ractors.each do |ractor| 45 | expect(ractor.take).to eq([1, 2, 3.0, 4.0]) 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "wasmtime" 4 | 5 | DEBUG = ENV["DEBUG"] == "true" || ENV["DEBUG"] == "1" || ENV["RB_SYS_CARGO_PROFILE"] == "dev" 6 | 7 | GLOBAL_ENGINE = Wasmtime::Engine.new( 8 | debug_info: false, # see https://github.com/bytecodealliance/wasmtime/issues/3999 9 | wasm_backtrace_details: DEBUG, 10 | target: ENV["WASMTIME_TARGET"] 11 | ) 12 | 13 | RSpec.shared_context("default lets") do 14 | let(:engine) { GLOBAL_ENGINE } 15 | let(:store_data) { Object.new } 16 | let(:store) { Wasmtime::Store.new(engine, store_data) } 17 | let(:wat) { "(module)" } 18 | 19 | def compile(wat) 20 | mod = Wasmtime::Module.new(engine, wat) 21 | Wasmtime::Instance.new(store, mod) 22 | end 23 | end 24 | 25 | RSpec.shared_context(:tmpdir) do 26 | let(:tmpdir) { Dir.mktmpdir } 27 | 28 | after(:each) do 29 | FileUtils.rm_rf(tmpdir) 30 | rescue Errno::EACCES => e 31 | warn "WARN: Failed to remove #{tmpdir} (#{e})" 32 | end 33 | end 34 | 35 | module WasmFixtures 36 | include Wasmtime 37 | extend self 38 | 39 | def wasi_debug 40 | @wasi_debug_module ||= Module.from_file(engine, "spec/fixtures/wasi-debug.wasm") 41 | end 42 | end 43 | 44 | module GcHelpers 45 | def without_gc_stress 46 | old = GC.stress 47 | GC.stress = false 48 | yield 49 | ensure 50 | GC.stress = old 51 | end 52 | 53 | def with_gc_stress 54 | old = GC.stress 55 | GC.stress = true 56 | yield 57 | ensure 58 | GC.stress = old 59 | end 60 | 61 | def measure_gc_stat(name) 62 | without_gc_stress do 63 | 10.times { GC.start(full_mark: true, immediate_sweep: true) } 64 | before = GC.stat(name) 65 | ret = yield 66 | after = GC.stat(name) 67 | [ret, after - before] 68 | end 69 | end 70 | end 71 | 72 | RSpec.configure do |config| 73 | config.filter_run focus: true 74 | config.run_all_when_everything_filtered = true 75 | if ENV["CI"] 76 | config.before(focus: true) { raise "Do not commit focused tests (`fit` or `focus: true`)" } 77 | end 78 | 79 | config.include_context("default lets") 80 | config.include GcHelpers 81 | 82 | # So memcheck steps can still pass if RSpec fails 83 | config.failure_exit_code = ENV.fetch("RSPEC_FAILURE_EXIT_CODE", 1).to_i 84 | config.default_formatter = ENV.fetch("RSPEC_FORMATTER") do 85 | next "doc" if DEBUG 86 | config.files_to_run.one? ? "doc" : "progress" 87 | end 88 | 89 | # Enable flags like --only-failures and --next-failure 90 | config.example_status_persistence_file_path = ".rspec_status" unless ENV["CI"] 91 | 92 | # Disable RSpec exposing methods globally on `Module` and `main` 93 | config.disable_monkey_patching! 94 | 95 | config.expect_with :rspec do |c| 96 | c.syntax = :expect 97 | end 98 | 99 | if ENV["GC_STRESS"] 100 | config.around :each do |ex| 101 | with_gc_stress { ex.run } 102 | end 103 | end 104 | 105 | config.around(:each, :ractor) do |example| 106 | was = Warning[:experimental] 107 | Warning[:experimental] = false 108 | example.run 109 | ensure 110 | Warning[:experimental] = was 111 | end 112 | end 113 | 114 | at_exit { GC.start(full_mark: true) } if ENV["GC_AT_EXIT"] == "1" 115 | -------------------------------------------------------------------------------- /spec/unit/component/component_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module Wasmtime 4 | module Component 5 | RSpec.describe Component do 6 | it "can be serialized and deserialized" do 7 | component = Component.new(engine, "(component)") 8 | serialized = component.serialize 9 | deserialized = Component.deserialize(engine, serialized) 10 | expect(deserialized.serialize).to eq(serialized) 11 | end 12 | 13 | describe ".from_file" do 14 | it "loads the Component" do 15 | component = Component.from_file(engine, "spec/fixtures/empty_component.wat") 16 | expect(component).to be_instance_of(Component) 17 | end 18 | 19 | it "tracks memory usage" do 20 | _, increase_bytes = measure_gc_stat(:malloc_increase_bytes) do 21 | Component.from_file(engine, "spec/fixtures/empty_component.wat") 22 | end 23 | 24 | # This is a rough estimate of the memory usage of the Component, subject to compiler changes 25 | expect(increase_bytes).to be > 3000 26 | end 27 | end 28 | 29 | describe ".deserialize_file" do 30 | include_context(:tmpdir) 31 | let(:tmpdir) { Dir.mktmpdir } 32 | 33 | after(:each) do 34 | FileUtils.rm_rf(tmpdir) 35 | rescue Errno::EACCES => e 36 | warn "WARN: Failed to remove #{tmpdir} (#{e})" 37 | end 38 | 39 | it("can deserialize a Component from a file") do 40 | tmpfile = create_tmpfile(Component.new(engine, "(component)").serialize) 41 | component = Component.deserialize_file(engine, tmpfile) 42 | 43 | expect(component.serialize).to eq(Component.new(engine, "(component)").serialize) 44 | end 45 | 46 | it "deserialize from a Component multiple times" do 47 | tmpfile = create_tmpfile(Component.new(engine, "(component)").serialize) 48 | 49 | component_one = Component.deserialize_file(engine, tmpfile) 50 | component_two = Component.deserialize_file(engine, tmpfile) 51 | expected = Component.new(engine, "(component)").serialize 52 | 53 | expect(component_one.serialize).to eq(expected) 54 | expect(component_two.serialize).to eq(expected) 55 | end 56 | 57 | it "tracks memory usage" do 58 | tmpfile = create_tmpfile(Component.new(engine, "(component)").serialize) 59 | component, increase_bytes = measure_gc_stat(:malloc_increase_bytes) { Component.deserialize_file(engine, tmpfile) } 60 | 61 | expect(increase_bytes).to be > File.size(tmpfile) 62 | expect(component).to be_a(Component) 63 | end 64 | 65 | def create_tmpfile(content) 66 | uuid = SecureRandom.uuid 67 | path = File.join(tmpdir, "deserialize-file-test-#{uuid}.so") 68 | File.binwrite(path, content) 69 | path 70 | end 71 | end 72 | 73 | describe ".deserialize" do 74 | it "raises on invalid Component" do 75 | expect { Component.deserialize(engine, "foo") } 76 | .to raise_error(Wasmtime::Error) 77 | end 78 | 79 | it "tracks memory usage" do 80 | serialized = Component.new(engine, "(component)").serialize 81 | component, increase_bytes = measure_gc_stat(:malloc_increase_bytes) { Component.deserialize(engine, serialized) } 82 | 83 | expect(increase_bytes).to be > serialized.bytesize 84 | expect(component).to be_a(Wasmtime::Component::Component) 85 | end 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /spec/unit/component/convert_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module Wasmtime 4 | module Component 5 | RSpec.describe "Component type conversions" do 6 | before(:all) do 7 | @types_component = Component.from_file(GLOBAL_ENGINE, "spec/fixtures/component_types.wasm") 8 | end 9 | 10 | let(:linker) { Linker.new(GLOBAL_ENGINE) } 11 | let(:instance) { linker.instantiate(Store.new(GLOBAL_ENGINE), @types_component) } 12 | 13 | def call_func(name, *args) 14 | func = instance.get_func(name) 15 | raise "Unknown func: #{name}" if func.nil? 16 | 17 | func.call(*args) 18 | end 19 | 20 | describe "successful round-trips" do 21 | [ 22 | ["bool", true, false], 23 | ["u8", 0, 2**8 - 1], 24 | ["s8", 0, -2**7 + 1, 2**7 - 1], 25 | ["u16", 0, 2**16 - 1], 26 | ["s16", 0, -2**15 + 1, 2**15 - 1], 27 | ["u32", 0, 2**32 - 1], 28 | ["s32", 0, -2**31 + 1, 2**31 - 1], 29 | ["u64", 0, 2**64 - 1], 30 | ["s64", 0, -2**63 + 1, 2**63 - 1], 31 | ["f32", 0, -5.5, 5.5], 32 | ["f64", 0, -5.5, 5.5], 33 | ["char", "0", "✅"], # char: Unicode Scalar Value 34 | ["string", "Olá"], 35 | ["list", [1, 2, 2**32 - 1]], # list 36 | ["record", {"x" => 1, "y" => 2}], 37 | ["tuple", [1, "foo"]], # tuple 38 | ["variant", Variant.new("all"), Variant.new("lt", 12)], 39 | ["enum", "l"], 40 | ["option", 0, nil], # option 41 | ["result", Result.ok(1), Result.error(2)], # result 42 | ["result-unit", Result.ok(nil), Result.error(nil)], 43 | ["flags", [], ["read"], ["read", "write", "exec"]] 44 | ].each do |type, *values| 45 | values.each do |v| 46 | it "#{type} #{v.inspect}" do 47 | expect(call_func("id-#{type}", v)).to eq(v) 48 | end 49 | end 50 | end 51 | 52 | it "returns FLOAT::INFINITY on f32 overflow" do 53 | expect(call_func("id-f32", 5 * 10**40)).to eq(Float::INFINITY) 54 | end 55 | 56 | it "returns FLOAT::INFINITY on f64 overflow" do 57 | expect(call_func("id-f64", 2 * 10**310)).to eq(Float::INFINITY) 58 | end 59 | end 60 | 61 | # TODO resource 62 | 63 | describe "failures" do 64 | [ 65 | ["bool", "", TypeError, /conversion of String into boolean/], 66 | ["bool", nil, TypeError, /conversion of NilClass into boolean/], 67 | ["u8", "1", TypeError, /conversion of String into Integer/], 68 | ["u8", -1, RangeError, /negative/], 69 | ["u8", 2**9, RangeError, /too big/], 70 | ["s8", "1", TypeError, /conversion of String into Integer/], 71 | ["s8", 2**8, RangeError, /too big/], 72 | ["u16", "1", TypeError, /conversion of String into Integer/], 73 | ["u16", -1, RangeError, /negative/], 74 | ["u16", 2**17, RangeError, /too big/], 75 | ["s16", "1", TypeError, /conversion of String into Integer/], 76 | ["s16", 2**16, RangeError, /too big/], 77 | ["u32", "1", TypeError, /conversion of String into Integer/], 78 | ["u32", -1, RangeError, /negative/], 79 | ["u32", 2**33, RangeError, /too big/], 80 | ["s32", "1", TypeError, /conversion of String into Integer/], 81 | ["s32", 2**32, RangeError, /too big/], 82 | ["u64", "1", TypeError, /conversion of String into Integer/], 83 | ["u64", -1, RangeError, /negative/], 84 | ["u64", 2**65, RangeError, /too big/], 85 | ["s64", "1", TypeError, /conversion of String into Integer/], 86 | ["s64", 2**64, RangeError, /too big/], 87 | ["string", 1, TypeError, /conversion of Integer into String/], 88 | ["string", "\xFF\xFF", EncodingError, /invalid utf-8 sequence/], 89 | ["char", "ab", TypeError, /too many characters in string/], 90 | ["list", nil, /no implicit conversion of NilClass into Array/], 91 | ["record", {"x" => 1}, /struct field missing: y/], 92 | ["record", nil, /no implicit conversion of NilClass into Hash/], 93 | ["tuple", nil, /no implicit conversion of NilClass into Array/], 94 | ["variant", Variant.new("no"), /invalid variant case "no", valid cases: \["all", "none", "lt"\]/], 95 | ["variant", Variant.new("lt", "nah"), /(variant value for "lt")/], 96 | ["enum", "no", /enum variant name `no` is not valid/], 97 | ["result", nil, /undefined method [`']ok\?/], # [`']: various ruby version 98 | ["result-unit", Result.ok(""), /expected nil for result<_, E> ok branch/], 99 | ["result-unit", Result.error(""), /expected nil for result error branch/], 100 | ["flags", ["no"], /unknown flag: `no`/], 101 | ["flags", [1], /no implicit conversion of Integer into String/], 102 | ["flags", 1, /no implicit conversion of Integer into Array/] 103 | ].each do |type, value, klass, msg| 104 | it "fails on #{type} #{value.inspect}" do 105 | expect { call_func("id-#{type}", value) }.to raise_error(klass, msg) 106 | end 107 | end 108 | 109 | it "has item index in list conversion error" do 110 | expect { call_func("id-list", [1, "foo"]) } 111 | .to raise_error(TypeError, /list item at index 1/) 112 | end 113 | 114 | it "has tuple index in tuple conversion error" do 115 | expect { call_func("id-tuple", ["foo", 1]) } 116 | .to raise_error(TypeError, /tuple value at index 0/) 117 | end 118 | 119 | it "has field name in record conversion error" do 120 | expect { call_func("id-record", {"y" => 1, "x" => nil}) } 121 | .to raise_error(TypeError, /struct field "x"/) 122 | end 123 | end 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /spec/unit/component/func_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module Wasmtime 4 | module Component 5 | RSpec.describe Func do 6 | before(:all) do 7 | @adder_component = Component.from_file(GLOBAL_ENGINE, "spec/fixtures/component_adder.wat") 8 | @trap_component = Component.from_file(GLOBAL_ENGINE, "spec/fixtures/component_trap.wat") 9 | end 10 | 11 | let(:linker) { Linker.new(engine) } 12 | let(:add) { linker.instantiate(store, @adder_component).get_func("add") } 13 | let(:unreachable) { linker.instantiate(store, @trap_component).get_func("unreachable") } 14 | 15 | describe "#call" do 16 | it "calls the func" do 17 | expect(add.call(1, 2)).to eq(3) 18 | end 19 | 20 | it "allows multiple calls into the same component instance" do 21 | expect(add.call(1, 2)).to eq(3) 22 | expect(add.call(1, 2)).to eq(3) 23 | end 24 | 25 | it "raises on invalid arg count" do 26 | expect { add.call(1) } 27 | .to raise_error(ArgumentError, /(given 1, expected 2)/) 28 | end 29 | 30 | it "raises on invalid arg type" do 31 | expect { add.call(nil, nil) } 32 | .to raise_error(TypeError, "no implicit conversion of nil into Integer (param at index 0)") 33 | end 34 | 35 | it "raises trap when component traps" do 36 | expect { unreachable.call }.to raise_error(Trap) do |trap| 37 | expect(trap.code).to eq(Trap::UNREACHABLE_CODE_REACHED) 38 | end 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/unit/component/instance_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module Wasmtime 4 | module Component 5 | RSpec.describe Instance do 6 | before(:all) do 7 | @adder_component = Component.from_file(GLOBAL_ENGINE, "spec/fixtures/component_adder.wat") 8 | end 9 | 10 | let(:linker) { Linker.new(engine) } 11 | let(:adder_instance) { linker.instantiate(store, @adder_component) } 12 | 13 | describe "#get_func" do 14 | it "returns a root func" do 15 | expect(adder_instance.get_func("add")).to be_instance_of(Wasmtime::Component::Func) 16 | end 17 | 18 | it "returns a nested func" do 19 | expect(adder_instance.get_func(["adder", "add"])).to be_instance_of(Wasmtime::Component::Func) 20 | end 21 | 22 | it "returns nil for invalid func" do 23 | expect(adder_instance.get_func("no")).to be_nil 24 | expect(adder_instance.get_func(["add", "no"])).to be_nil 25 | end 26 | 27 | it "raises for invalid arg" do 28 | expect { adder_instance.get_func(3) } 29 | .to raise_error(TypeError, /invalid argument for component index/) 30 | 31 | expect { adder_instance.get_func([nil]) } 32 | .to raise_error(TypeError, /invalid argument for component index/) 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/unit/component/linker_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module Wasmtime 4 | module Component 5 | RSpec.describe Linker do 6 | let(:linker) { Linker.new(engine) } 7 | 8 | it "disallows linker reentrance" do 9 | linker.root do 10 | expect { linker.root }.to raise_error(Wasmtime::Error, /reentrant/) 11 | end 12 | end 13 | 14 | it "disallows linker instance reentrance" do 15 | linker.instance("foo") do |foo| 16 | foo.instance("bar") do |_| 17 | expect { foo.instance("bar") {} }.to raise_error(Wasmtime::Error, /reentrant/) 18 | expect { foo.module("bar", Module.new(engine, wat)) {} }.to raise_error(Wasmtime::Error, /reentrant/) 19 | end 20 | end 21 | end 22 | 23 | it "disallows using LinkerInstance outside its block" do 24 | leaked_instance = nil 25 | linker.root { |root| leaked_instance = root } 26 | expect { leaked_instance.instance("foo") {} } 27 | .to raise_error(Wasmtime::Error, /LinkerInstance went out of scope/) 28 | end 29 | 30 | describe "#instantiate" do 31 | it "returns a Component::Instance" do 32 | component = Component.new(engine, "(component)") 33 | store = Store.new(engine) 34 | expect(linker.instantiate(store, component)) 35 | .to be_instance_of(Wasmtime::Component::Instance) 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/unit/component/result_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module Wasmtime 4 | module Component 5 | RSpec.describe Result do 6 | let(:ok) { Result.ok(1) } 7 | let(:error) { Result.error(1) } 8 | 9 | it "creates a new ok result" do 10 | expect(ok).to be_instance_of(Result) 11 | expect(ok).to be_ok 12 | expect(ok).not_to be_error 13 | end 14 | 15 | it "creates a new error result" do 16 | expect(error).to be_instance_of(Result) 17 | expect(error).to be_error 18 | expect(error).not_to be_ok 19 | end 20 | 21 | it "raises when accessing unchecked value" do 22 | expect { error.ok }.to raise_error(Result::UncheckedResult) 23 | expect { ok.error }.to raise_error(Result::UncheckedResult) 24 | end 25 | 26 | it "behaves like a value object" do 27 | expect(Result.ok(1)).to eq(Result.ok(1)) 28 | expect(Result.ok(1).hash).to eq(Result.ok(1).hash) 29 | 30 | expect(Result.ok(1)).not_to eq(Result.ok(2)) 31 | expect(Result.ok(1).hash).not_to eq(Result.ok(2).hash) 32 | expect(Result.ok(1).hash).not_to eq(Result.error(1).hash) 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/unit/component/variant_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module Wasmtime 4 | module Component 5 | RSpec.describe Variant do 6 | it "has name" do 7 | expect(Variant.new("a", 1).name).to eq("a") 8 | end 9 | 10 | it "has value" do 11 | expect(Variant.new("a", 1).value).to eq(1) 12 | end 13 | 14 | it "behaves like a value object" do 15 | expect(Variant.new("a", 1)).to eq(Variant.new("a", 1)) 16 | expect(Variant.new("a", 1).hash).to eq(Variant.new("a", 1).hash) 17 | 18 | expect(Variant.new("a")).not_to eq(Variant.new("b")) 19 | expect(Variant.new("a", 1)).not_to eq(Variant.new("a", 2)) 20 | expect(Variant.new("a").hash).not_to eq(Variant.new("b").hash) 21 | expect(Variant.new("a", 1).hash).not_to eq(Variant.new("a", 2).hash) 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/unit/engine_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module Wasmtime 4 | RSpec.describe Engine do 5 | describe ".new" do 6 | it("accepts a config Hash") { Engine.new(consume_fuel: true) } 7 | it("accepts no config") { Engine.new } 8 | it("accepts nil config") { Engine.new(nil) } 9 | it "rejects non-hash config" do 10 | expect { Engine.new(1) }.to raise_error(TypeError) 11 | end 12 | it "rejects unknown options" do 13 | expect { Engine.new(nope: 1) }.to raise_error(ArgumentError, "Unknown option: :nope") 14 | end 15 | it "rejects multiple args" do 16 | expect { Engine.new(1, 2) }.to raise_error(ArgumentError) 17 | end 18 | 19 | # bool & numeric options 20 | [ 21 | [:debug_info, true], 22 | [:wasm_backtrace_details, true], 23 | [:native_unwind_info, true], 24 | [:consume_fuel, true], 25 | [:epoch_interruption, true], 26 | [:max_wasm_stack, 400, true], 27 | [:wasm_threads, true], 28 | [:wasm_multi_memory, true], 29 | [:wasm_memory64, true], 30 | [:parallel_compilation, true], 31 | [:wasm_reference_types, true], 32 | [:async_stack_zeroing, true] 33 | ].each do |option, valid, invalid = nil| 34 | it "supports #{option}" do 35 | Engine.new(option => valid) 36 | expect { Engine.new(option => invalid) }.to raise_error(TypeError, /#{option}/) if invalid 37 | end 38 | end 39 | 40 | profiler_options = [:none] 41 | profiler_options.push(:jitdump, :vtune) if Gem::Platform.local.os == "linux" 42 | 43 | # enum options represented as symbols 44 | [ 45 | [:strategy, [:auto, :cranelift, :winch]], 46 | [:cranelift_opt_level, [:none, :speed, :speed_and_size]], 47 | [:profiler, profiler_options] 48 | ].each do |option, valid| 49 | it "supports #{option}" do 50 | valid.each { |value| Engine.new(option => value) } 51 | expect { Engine.new(option => :nope) } 52 | .to raise_error(ArgumentError, /invalid :#{option}.*:nope/) 53 | end 54 | end 55 | 56 | it "supports allocation_strategy config" do 57 | expect(Engine.new(allocation_strategy: :pooling)).to be_a(Engine) 58 | expect(Engine.new(allocation_strategy: :on_demand)).to be_a(Engine) 59 | expect(Engine.new(allocation_strategy: PoolingAllocationConfig.new)).to be_a(Engine) 60 | expect { Engine.new(allocation_strategy: :nope) }.to raise_error(ArgumentError, /invalid instance allocation strategy: :nope/) 61 | end 62 | 63 | it "supports target options" do 64 | expect { Engine.new(target: "x86_64-unknown-linux-gnu") }.not_to raise_error 65 | expect { Engine.new(target: "nope") }.to raise_error(ArgumentError, /Unrecognized architecture/) 66 | end 67 | end 68 | 69 | describe ".precompile_module" do 70 | it "returns a String" do 71 | serialized = engine.precompile_module("(module)") 72 | expect(serialized).to be_instance_of(String) 73 | end 74 | 75 | it "can be used by Module.deserialize" do 76 | serialized = engine.precompile_module("(module)") 77 | mod = Module.deserialize(engine, serialized) 78 | expect(mod).to be_instance_of(Wasmtime::Module) 79 | end 80 | end 81 | 82 | describe ".precompile_component" do 83 | it "returns a String" do 84 | serialized = engine.precompile_component("(component)") 85 | expect(serialized).to be_instance_of(String) 86 | end 87 | 88 | it "can be used by Component.deserialize" do 89 | serialized = engine.precompile_component("(component)") 90 | component = Component::Component.deserialize(engine, serialized) 91 | expect(component).to be_instance_of(Component::Component) 92 | end 93 | end 94 | 95 | describe "#precompile_compatibility_key" do 96 | it "is the same amongst similar engines" do 97 | engine_one = Engine.new(target: "x86_64-unknown-linux-gnu", parallel_compilation: true) 98 | engine_two = Engine.new(target: "x86_64-unknown-linux-gnu", parallel_compilation: false) 99 | 100 | expect(engine_one.precompile_compatibility_key).to eq(engine_two.precompile_compatibility_key) 101 | end 102 | 103 | it "is different amongst different engines" do 104 | engine_one = Engine.new(target: "x86_64-unknown-linux-gnu") 105 | engine_two = Engine.new(target: "arm64-apple-darwin") 106 | 107 | expect(engine_one.precompile_compatibility_key).not_to eq(engine_two.precompile_compatibility_key) 108 | end 109 | 110 | it "freezes and caches the result to avoid repeated allocation" do 111 | engine = Engine.new(target: "x86_64-unknown-linux-gnu") 112 | 113 | expect(engine.precompile_compatibility_key).to be_frozen 114 | expect(engine.precompile_compatibility_key.object_id).to eq(engine.precompile_compatibility_key.object_id) 115 | end 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /spec/unit/error_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module Wasmtime 4 | RSpec.describe "Error" do 5 | let(:error_class) { Class.new(StandardError) } 6 | 7 | context "on instance start" do 8 | context "from Instance.new" do 9 | it "bubbles host exception" do 10 | func = Func.new(store, [], []) { raise error_class } 11 | 12 | expect { Wasmtime::Instance.new(store, module_import_func_start, [func]) } 13 | .to raise_error(error_class) 14 | end 15 | 16 | it "bubbles trap" do 17 | expect { Wasmtime::Instance.new(store, module_trapping_on_start, []) } 18 | .to raise_error(Trap) 19 | end 20 | end 21 | 22 | context "from Linker#instantiate" do 23 | it "bubbles host exception" do 24 | linker = Linker.new(engine) 25 | linker.func_new("", "", [], []) { raise error_class } 26 | store = Store.new(engine) 27 | 28 | expect { linker.instantiate(store, module_import_func_start) }.to raise_error(error_class) 29 | end 30 | 31 | it "bubbles trap" do 32 | linker = Linker.new(engine) 33 | store = Store.new(engine) 34 | 35 | expect { linker.instantiate(store, module_trapping_on_start) } 36 | .to raise_error(Trap) 37 | end 38 | end 39 | end 40 | 41 | context "on call" do 42 | context "from Func#call" do 43 | it "bubbles host exception" do 44 | store = Store.new(engine) 45 | func = Func.new(store, [], []) { raise error_class } 46 | 47 | expect { func.call }.to raise_error(error_class) 48 | end 49 | 50 | it "bubbles trap" do 51 | func = Instance.new(Store.new(engine), module_trapping_on_func) 52 | .export("f") 53 | .to_func 54 | 55 | expect { func.call }.to raise_error(Trap) 56 | end 57 | end 58 | 59 | context "from Instance#invoke" do 60 | it "bubbles host exception" do 61 | store = Store.new(engine) 62 | mod = Module.new(engine, <<~WAT) 63 | (module 64 | (import "" "" (func)) 65 | (export "f" (func 0))) 66 | WAT 67 | func = Func.new(store, [], []) { raise error_class } 68 | instance = Wasmtime::Instance.new(store, mod, [func]) 69 | 70 | expect { instance.invoke("f") }.to raise_error(error_class) 71 | end 72 | 73 | it "bubbles trap" do 74 | instance = Instance.new(Store.new(engine), module_trapping_on_func) 75 | expect { instance.invoke("f") }.to raise_error(Trap) 76 | end 77 | end 78 | end 79 | 80 | it "raises WasiExit on WASI's proc_exit" do 81 | linker = Linker.new(engine, wasi: true) 82 | store = Store.new(engine, wasi_ctx: WasiCtxBuilder.new.build) 83 | instance = linker.instantiate(store, wasi_module_exiting) 84 | 85 | expect { instance.invoke("_start") }.to raise_error(WasiExit) do |wasi_exit| 86 | expect(wasi_exit.code).to eq(0) 87 | expect(wasi_exit.message).to eq("WASI exit with code 0") 88 | end 89 | end 90 | 91 | def module_import_func_start 92 | Wasmtime::Module.new(engine, <<~WAT) 93 | (module 94 | (import "" "" (func)) 95 | (start 0)) 96 | WAT 97 | end 98 | 99 | def module_trapping_on_start 100 | Wasmtime::Module.new(engine, <<~WAT) 101 | (module 102 | (func unreachable) 103 | (start 0)) 104 | WAT 105 | end 106 | 107 | def module_trapping_on_func 108 | Wasmtime::Module.new(engine, <<~WAT) 109 | (module 110 | (func (export "f") unreachable)) 111 | WAT 112 | end 113 | 114 | def wasi_module_exiting 115 | Module.new(engine, <<~WAT) 116 | (module 117 | (import "wasi_snapshot_preview1" "proc_exit" 118 | (func $__wasi_proc_exit (param i32))) 119 | (memory (export "memory") 0) 120 | (func $_start 121 | (call $__wasi_proc_exit (i32.const 0))) 122 | (export "_start" (func $_start))) 123 | WAT 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /spec/unit/extern_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module Wasmtime 4 | RSpec.describe Extern do 5 | cases = { 6 | f: [:to_func, Func], 7 | m: [:to_memory, Memory], 8 | t: [:to_table, Table], 9 | g: [:to_global, Global] 10 | } 11 | 12 | cases.each do |name, (meth, klass)| 13 | describe "##{meth}" do 14 | it "returns an instance of #{klass}" do 15 | extern_mod = new_extern_module 16 | export = extern_mod.exports[name.to_s] 17 | 18 | expect(export.public_send(meth)).to be_instance_of(klass) 19 | end 20 | 21 | it "raises an error when extern is not a #{klass}" do 22 | extern_mod = new_extern_module 23 | invalid_methods = cases.flat_map { |_, (meth, _)| meth } - [meth] 24 | 25 | invalid_methods.each do |invalid_method| 26 | export = extern_mod.exports[name.to_s] 27 | expect { export.public_send(invalid_method) }.to raise_error(Wasmtime::ConversionError) 28 | end 29 | end 30 | end 31 | end 32 | 33 | describe "#inspect" do 34 | it "looks pretty" do 35 | result = new_extern_module.exports["f"].inspect 36 | 37 | expect(result).to match(/\A#>$/) 38 | end 39 | end 40 | 41 | def new_extern_module 42 | compile <<~WAT 43 | (module 44 | (func (export "f")) 45 | (memory (export "m") 1) 46 | (table (export "t") 1 funcref) 47 | (global (export "g") (mut i32) (i32.const 1)) 48 | ) 49 | WAT 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/unit/fuel_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module Wasmtime 4 | RSpec.describe "Fuel" do 5 | def self.test_on_store_and_caller(desc, store = :store, &block) 6 | context "on Store" do 7 | it desc do 8 | instance_exec(send(store), &block) 9 | end 10 | end 11 | 12 | context "on Caller" do 13 | it desc do 14 | func = Func.new(send(store), [], []) do |caller| 15 | instance_exec(caller, &block) 16 | end 17 | func.call 18 | end 19 | end 20 | end 21 | 22 | let(:engine) { Engine.new(consume_fuel: true) } 23 | let(:store) { Store.new(engine) } 24 | let(:store_without_fuel) { Store.new(Engine.new) } 25 | 26 | describe "#set_fuel" do 27 | test_on_store_and_caller "returns nil on success" do |store_like| 28 | expect(store_like.set_fuel(100)).to be_nil 29 | end 30 | 31 | test_on_store_and_caller "raises when fuel isn't configured", :store_without_fuel do |store_like| 32 | expect { store_like.set_fuel(100) } 33 | .to(raise_error(Wasmtime::Error, /fuel is not configured in this store/)) 34 | end 35 | end 36 | 37 | describe "#get_fuel" do 38 | test_on_store_and_caller "starts at 0" do |store_like| 39 | expect(store_like.get_fuel).to eq(0) 40 | end 41 | 42 | test_on_store_and_caller "raises an error when fuel is not configured", :store_without_fuel do |store_like| 43 | expect { store_like.get_fuel }.to(raise_error(Wasmtime::Error, /fuel is not configured in this store/)) 44 | end 45 | end 46 | 47 | it "traps when Wasm execution runs out of fuel" do 48 | mod = Module.new(engine, <<~WAT) 49 | (module 50 | (func (export "f") (result i32) 51 | i32.const 42)) 52 | WAT 53 | instance = Instance.new(store, mod) 54 | store.set_fuel(1) 55 | expect { instance.invoke("f") }.to raise_error(Trap, /all fuel consumed/) do |error| 56 | expect(error.code).to eq(Trap::OUT_OF_FUEL) 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/unit/global_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module Wasmtime 4 | RSpec.describe Global do 5 | describe ".const" do 6 | it "creates a const global" do 7 | global = Global.const(store, :i32, 1) 8 | expect(global).to be_const 9 | expect(global).not_to be_var 10 | end 11 | 12 | it "raises on invalid Wasm type" do 13 | expect { Global.const(store, :nope, 1) } 14 | .to raise_error(ArgumentError, /invalid WebAssembly type/) 15 | end 16 | end 17 | 18 | describe ".var" do 19 | it "creates a var global" do 20 | global = Global.var(store, :i32, 1) 21 | expect(global).to be_var 22 | expect(global).not_to be_const 23 | end 24 | 25 | it "raises on invalid Wasm type" do 26 | expect { Global.var(store, :nope, 1) } 27 | .to raise_error(ArgumentError, /invalid WebAssembly type/) 28 | end 29 | end 30 | 31 | describe "#type" do 32 | it "returns the Wasm type as symbol" do 33 | global = Global.const(store, :i32, 1) 34 | expect(global.type).to eq(:i32) 35 | end 36 | end 37 | 38 | describe "#get" do 39 | it "returns the global value" do 40 | global = Global.var(store, :i32, 1) 41 | expect(global.get).to eq(1) 42 | end 43 | end 44 | 45 | describe "#set" do 46 | it "changes the value" do 47 | global = Global.var(store, :i32, 1) 48 | global.set(2) 49 | expect(global.get).to eq(2) 50 | end 51 | 52 | it "raises when the global is constant" do 53 | global = Global.const(store, :i32, 1) 54 | expect { global.set(2) } 55 | .to raise_error(Wasmtime::Error, "immutable global cannot be set") 56 | end 57 | end 58 | 59 | it "keeps externrefs alive" do 60 | global = Global.var(store, :externref, +"foo") 61 | generate_new_objects 62 | expect(global.get).to eq("foo") 63 | 64 | global.set(+"bar") 65 | generate_new_objects 66 | expect(global.get).to eq("bar") 67 | end 68 | 69 | private 70 | 71 | def generate_new_objects 72 | "hi" * 3 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /spec/unit/instance_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module Wasmtime 4 | RSpec.describe Instance do 5 | describe "#new" do 6 | it "raises a TypeError when receiving invalid imports" do 7 | mod = Wasmtime::Module.new(engine, "(module)") 8 | 9 | expect { Wasmtime::Instance.new(store, mod, [:not_extern]) } 10 | .to raise_error(TypeError, "unexpected extern: :not_extern") 11 | end 12 | 13 | it "accepts nil for imports" do 14 | mod = Wasmtime::Module.new(engine, "(module)") 15 | 16 | expect { Wasmtime::Instance.new(store, mod, nil) } 17 | .not_to raise_error 18 | end 19 | 20 | it "imports memory" do 21 | mod = Module.new(engine, <<~WAT) 22 | (module 23 | (import "" "" (memory 1))) 24 | WAT 25 | memory = Memory.new(store, min_size: 1) 26 | Wasmtime::Instance.new(store, mod, [memory]) 27 | end 28 | end 29 | 30 | describe "#exports" do 31 | it "returns a Hash of Extern" do 32 | instance = compile <<~WAT 33 | (module 34 | (memory $module/mem 1) 35 | (func $module/hello (result i32) 36 | i32.const 1 37 | ) 38 | (export "hello" (func $module/hello)) 39 | (export "mem" (memory $module/mem)) 40 | ) 41 | WAT 42 | 43 | expect(instance.exports).to include("hello" => be_a(Extern), "mem" => be_a(Extern)) 44 | expect(instance.exports["hello"].to_func).to be_a(Func) 45 | expect(instance.exports["mem"].to_memory).to be_a(Memory) 46 | end 47 | end 48 | 49 | describe "export" do 50 | it "returns a single Extern" do 51 | instance = compile(<<~WAT) 52 | (module 53 | (func (export "f"))) 54 | WAT 55 | expect(instance.export("f").to_func).to be_a(Func) 56 | end 57 | end 58 | 59 | describe "invoke" do 60 | it "returns nil when func has no return value" do 61 | instance = compile(<<~WAT) 62 | (module 63 | (func (export "main"))) 64 | WAT 65 | expect(instance.invoke("main")).to be_nil 66 | end 67 | 68 | it "returns a value when func has single return value" do 69 | instance = compile(<<~WAT) 70 | (module 71 | (func (export "main") (result i32) 72 | i32.const 42)) 73 | WAT 74 | expect(instance.invoke("main")).to eq(42) 75 | end 76 | 77 | it "returns an array when func has multiple return values" do 78 | instance = compile(<<~WAT) 79 | (module 80 | (func (export "main") (result i32) (result i32) 81 | i32.const 42 82 | i32.const 43)) 83 | WAT 84 | expect(instance.invoke("main")).to eq([42, 43]) 85 | end 86 | end 87 | 88 | private 89 | 90 | def invoke_identity_function(type, arg) 91 | instance = compile(<<~WAT) 92 | (module 93 | (func (export "main") (param #{type}) (result #{type}) 94 | local.get 0)) 95 | WAT 96 | instance.invoke("main", arg) 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /spec/unit/memory_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "fiddle" 3 | 4 | module Wasmtime 5 | RSpec.describe Memory do 6 | describe ".new" do 7 | it "creates a memory" do 8 | mem = Memory.new(store, min_size: 1) 9 | expect(mem).to be_instance_of(Wasmtime::Memory) 10 | end 11 | end 12 | 13 | describe "#size" do 14 | it "returns its size" do 15 | mem = Memory.new(store, min_size: 1) 16 | expect(mem.size).to eq(1) 17 | end 18 | end 19 | 20 | describe "#min_size" do 21 | it "returns the memory type's min size" do 22 | mem = Memory.new(store, min_size: 1) 23 | expect(mem.min_size).to eq(1) 24 | end 25 | end 26 | 27 | describe "#max_size" do 28 | it "defaults to nil" do 29 | mem = Memory.new(store, min_size: 1) 30 | expect(mem.max_size).to be_nil 31 | end 32 | 33 | it "returns the memory type's max size" do 34 | mem = Memory.new(store, min_size: 1, max_size: 2) 35 | expect(mem.max_size).to eq(2) 36 | end 37 | end 38 | 39 | describe "#grow" do 40 | it "returns the previous size" do 41 | mem = Memory.new(store, min_size: 2) 42 | expect(mem.grow(1)).to eq(2) 43 | end 44 | 45 | it "raises when growing past the maximum" do 46 | mem = Memory.new(store, min_size: 1, max_size: 1) 47 | expect { mem.grow(1) }.to raise_error(Wasmtime::Error, "failed to grow memory by `1`") 48 | end 49 | 50 | it "tracks memory usage" do 51 | wasm_page_size = 0x10000 52 | mem = Memory.new(store, min_size: 1, max_size: 4) 53 | _, increase_bytes = measure_gc_stat(:malloc_increase_bytes) { mem.grow(3) } 54 | expect(increase_bytes).to be >= (3 * wasm_page_size) 55 | end 56 | end 57 | 58 | describe "#read, #write" do 59 | it "reads and writes a Binary string" do 60 | mem = Memory.new(store, min_size: 1) 61 | expect(mem.write(0, "foo")).to be_nil 62 | str = mem.read(0, 3) 63 | expect(str).to eq("foo") 64 | expect(str.encoding).to eq(Encoding::ASCII_8BIT) 65 | end 66 | 67 | it "raises when reading past the end of the buffer" do 68 | mem = Memory.new(store, min_size: 1) 69 | expect { mem.read(64 * 2**10, 1) } 70 | .to raise_error(Wasmtime::Error, "out of bounds memory access") 71 | end 72 | 73 | it "raises when writing past the end of the buffer" do 74 | mem = Memory.new(store, min_size: 1) 75 | expect { mem.write(64 * 2**10, "f") } 76 | .to raise_error(Wasmtime::Error, "out of bounds memory access") 77 | end 78 | end 79 | 80 | describe "#read_utf8" do 81 | it "reads a UTF-8 string" do 82 | mem = Memory.new(store, min_size: 1) 83 | expect(mem.write(0, "foo")).to be_nil 84 | str = mem.read_utf8(0, 3) 85 | expect(str).to eq("foo") 86 | expect(str.encoding).to eq(Encoding::UTF_8) 87 | end 88 | 89 | it "raises when the utf8 is invalid" do 90 | invalid_utf8 = [0x80, 0x80, 0x80].pack("C*") 91 | mem = Memory.new(store, min_size: 1) 92 | expect(mem.write(0, invalid_utf8)).to be_nil 93 | 94 | expect { mem.read_utf8(0, 3) }.to raise_error(Wasmtime::Error, /invalid utf-8/) 95 | end 96 | end 97 | 98 | describe "#unsafe_slice" do 99 | it "exposes a frozen string" do 100 | mem = Memory.new(store, min_size: 1) 101 | mem.write(0, "foo") 102 | str = String(mem.read_unsafe_slice(0, 3)) 103 | 104 | expect(str).to eq("foo") 105 | expect(str.encoding).to eq(Encoding::ASCII_8BIT) 106 | expect(str).to be_frozen 107 | end 108 | 109 | if defined?(Fiddle::MemoryView) 110 | it "exposes a memory view" do 111 | mem = Memory.new(store, min_size: 3) 112 | mem.write(0, "foo") 113 | view = mem.read_unsafe_slice(0, 3).to_memory_view 114 | 115 | expect(view).to be_a(Fiddle::MemoryView) 116 | expect(view).to be_readonly 117 | expect(view.ndim).to eq(1) 118 | expect(view.to_s).to eq("foo") unless RUBY_VERSION.start_with?("3.0") 119 | end 120 | end 121 | 122 | it "invalidates the size when the memory is resized" do 123 | mem = Memory.new(store, min_size: 1) 124 | mem.write(0, "foo") 125 | slice = mem.read_unsafe_slice(0, 3) 126 | mem.grow(1) 127 | 128 | expect { slice.to_str } 129 | .to raise_error(Wasmtime::Error, "memory slice was invalidated by resize") 130 | 131 | if defined?(Fiddle::MemoryView) 132 | expect { slice.to_memory_view } 133 | .to raise_error(ArgumentError, /Unable to get a memory view from/) 134 | end 135 | end 136 | 137 | it "errors when the memory is out of bounds" do 138 | mem = Memory.new(store, min_size: 1) 139 | 140 | expect { mem.read_unsafe_slice(64 * 2**10, 1) } 141 | .to raise_error(Wasmtime::Error, "out of bounds memory access") 142 | end 143 | end 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /spec/unit/module_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "tempfile" 3 | require "securerandom" 4 | require "pathname" 5 | 6 | module Wasmtime 7 | RSpec.describe Module do 8 | it "can be serialized and deserialized" do 9 | mod = Module.new(engine, wat) 10 | serialized = mod.serialize 11 | deserialized = Module.deserialize(engine, serialized) 12 | expect(deserialized.serialize).to eq(serialized) 13 | end 14 | 15 | describe ".from_file" do 16 | it "loads the module" do 17 | mod = Module.from_file(engine, "spec/fixtures/empty.wat") 18 | expect(mod).to be_instance_of(Module) 19 | end 20 | 21 | it "tracks memory usage" do 22 | _, increase_bytes = measure_gc_stat(:malloc_increase_bytes) do 23 | Module.from_file(engine, "spec/fixtures/empty.wat") 24 | end 25 | 26 | # This is a rough estimate of the memory usage of the module, subject to compiler changes 27 | expect(increase_bytes).to be > 3000 28 | end 29 | end 30 | 31 | describe ".deserialize_file" do 32 | include_context(:tmpdir) 33 | let(:tmpdir) { Dir.mktmpdir } 34 | 35 | after(:each) do 36 | FileUtils.rm_rf(tmpdir) 37 | rescue Errno::EACCES => e 38 | warn "WARN: Failed to remove #{tmpdir} (#{e})" 39 | end 40 | 41 | it("can deserialize a module from a file") do 42 | tmpfile = create_tmpfile(Module.new(engine, "(module)").serialize) 43 | mod = Module.deserialize_file(engine, tmpfile) 44 | 45 | expect(mod.serialize).to eq(Module.new(engine, "(module)").serialize) 46 | end 47 | 48 | it "deserialize from a module multiple times" do 49 | tmpfile = create_tmpfile(Module.new(engine, wat).serialize) 50 | 51 | mod_one = Module.deserialize_file(engine, tmpfile) 52 | mod_two = Module.deserialize_file(engine, tmpfile) 53 | expected = Module.new(engine, wat).serialize 54 | 55 | expect(mod_one.serialize).to eq(expected) 56 | expect(mod_two.serialize).to eq(expected) 57 | end 58 | 59 | it "tracks memory usage" do 60 | tmpfile = create_tmpfile(Module.new(engine, "(module)").serialize) 61 | mod, increase_bytes = measure_gc_stat(:malloc_increase_bytes) { Module.deserialize_file(engine, tmpfile) } 62 | 63 | expect(increase_bytes).to be > File.size(tmpfile) 64 | expect(mod).to be_a(Wasmtime::Module) 65 | end 66 | 67 | def create_tmpfile(content) 68 | uuid = SecureRandom.uuid 69 | path = File.join(tmpdir, "deserialize-file-test-#{uuid}.so") 70 | File.binwrite(path, content) 71 | path 72 | end 73 | end 74 | 75 | describe ".deserialize" do 76 | it "raises on invalid module" do 77 | expect { Module.deserialize(engine, "foo") } 78 | .to raise_error(Wasmtime::Error) 79 | end 80 | 81 | it "tracks memory usage" do 82 | serialized = Module.new(engine, wat).serialize 83 | mod, increase_bytes = measure_gc_stat(:malloc_increase_bytes) { Module.deserialize(engine, serialized) } 84 | 85 | expect(increase_bytes).to be > serialized.bytesize 86 | expect(mod).to be_a(Wasmtime::Module) 87 | end 88 | end 89 | 90 | describe "#imports" do 91 | cases = { 92 | f: [:to_func_type, FuncType, {params: [:i32], results: [:i32]}], 93 | m: [:to_memory_type, MemoryType, {min_size: 1, max_size: nil}], 94 | t: [:to_table_type, TableType, {type: :funcref, min_size: 1, max_size: nil}], 95 | g: [:to_global_type, GlobalType, {const?: false, var?: true, type: :i32}] 96 | } 97 | 98 | cases.each do |name, (meth, klass, calls)| 99 | describe "##{meth}" do 100 | it "returns a type that is an instance of #{klass}" do 101 | import = get_import_by_name(name) 102 | expect(import["type"].public_send(meth)).to be_instance_of(klass) 103 | end 104 | 105 | it "raises an error when extern type is not a #{klass}" do 106 | import = get_import_by_name(name) 107 | invalid_methods = cases.values.map(&:first) - [meth] 108 | 109 | invalid_methods.each do |invalid_method| 110 | expect { import["type"].public_send(invalid_method) }.to raise_error(Wasmtime::ConversionError) 111 | end 112 | end 113 | 114 | it "has a type that responds to the expected methods for #{klass}" do 115 | import = get_import_by_name(name) 116 | extern_type = import["type"].public_send(meth) 117 | 118 | calls.each do |(meth_name, expected_return)| 119 | expect(extern_type.public_send(meth_name)).to eq(expected_return) 120 | end 121 | end 122 | end 123 | end 124 | 125 | it "has a module name" do 126 | mod_with_imports = new_import_module 127 | imports = mod_with_imports.imports 128 | 129 | imports.each do |import| 130 | expect(import["module"]).to eq("env") 131 | end 132 | end 133 | 134 | it "returns an empty array for a module with no imports" do 135 | mod = Module.new(engine, "(module)") 136 | 137 | expect(mod.imports).to be_an(Array) 138 | expect(mod.imports).to be_empty 139 | end 140 | 141 | def get_import_by_name(name) 142 | mod_with_imports = new_import_module 143 | imports = mod_with_imports.imports 144 | imports.find { _1["name"] == name.to_s } 145 | end 146 | 147 | def new_import_module 148 | Module.new(engine, <<~WAT 149 | (module 150 | (import "env" "f" (func (param i32) (result i32))) 151 | (import "env" "m" (memory 1)) 152 | (import "env" "t" (table 1 funcref)) 153 | (global (import "env" "g") (mut i32)) 154 | ) 155 | WAT 156 | ) 157 | end 158 | end 159 | end 160 | end 161 | -------------------------------------------------------------------------------- /spec/unit/pooling_allocation_config_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module Wasmtime 4 | RSpec.describe PoolingAllocationConfig do 5 | let(:config) { PoolingAllocationConfig.new } 6 | 7 | it "allows total_memories configuration" do 8 | config.total_memories = 1 9 | expect(config.inspect).to include("total_memories: 1,") 10 | end 11 | 12 | it "allows total_tables configuration" do 13 | config.total_tables = 1 14 | expect(config.inspect).to include("total_tables: 1,") 15 | end 16 | 17 | it "allows max_memories_per_module configuration" do 18 | config.max_memories_per_module = 1 19 | expect(config.inspect).to include("max_memories_per_module: 1,") 20 | end 21 | 22 | it "allows max_tables_per_module configuration" do 23 | config.max_tables_per_module = 1 24 | expect(config.inspect).to include("max_tables_per_module: 1,") 25 | end 26 | 27 | it "allows async_stack_keep_resident configuration" do 28 | config.async_stack_keep_resident = 1 29 | expect(config.inspect).to include("async_stack_keep_resident: 1,") 30 | end 31 | 32 | it "allows linear_memory_keep_resident configuration" do 33 | config.linear_memory_keep_resident = 1 34 | expect(config.inspect).to include("linear_memory_keep_resident: 1,") 35 | end 36 | 37 | it "allows max_component_instance_size configuration" do 38 | config.max_component_instance_size = 1 39 | expect(config.inspect).to include("component_instance_size: 1,") 40 | end 41 | 42 | it "allows max_core_instance_size configuration" do 43 | config.max_core_instance_size = 1 44 | expect(config.inspect).to include("core_instance_size: 1,") 45 | end 46 | 47 | it "allows max_memories_per_component configuration" do 48 | config.max_memories_per_component = 1 49 | expect(config.inspect).to include("max_memories_per_component: 1,") 50 | end 51 | 52 | it "allows max_memory_protection_keys configuration" do 53 | config.max_memory_protection_keys = 1 54 | expect(config.inspect).to include("max_memory_protection_keys: 1") 55 | end 56 | 57 | it "allows max_tables_per_component configuration" do 58 | config.max_tables_per_component = 1 59 | expect(config.inspect).to include("max_tables_per_component: 1,") 60 | end 61 | 62 | it "allows max_unused_warm_slots configuration" do 63 | config.max_unused_warm_slots = 1 64 | expect(config.inspect).to include("max_unused_warm_slots: 1,") 65 | end 66 | 67 | it "allows max_memory_size configuration" do 68 | config.max_memory_size = 1 69 | expect(config.inspect).to include("max_memory_size: 1") 70 | end 71 | 72 | it "allows memory_protection_keys configuration" do 73 | config.memory_protection_keys = :enable 74 | expect(config.inspect).to include("memory_protection_keys: Enable") 75 | config.memory_protection_keys = :disable 76 | expect(config.inspect).to include("memory_protection_keys: Disable") 77 | config.memory_protection_keys = :auto 78 | expect(config.inspect).to include("memory_protection_keys: Auto") 79 | end 80 | 81 | it "allows table_elements configuration" do 82 | config.table_elements = 1 83 | expect(config.inspect).to include("table_elements: 1,") 84 | end 85 | 86 | it "allows table_keep_resident configuration" do 87 | config.table_keep_resident = 1 88 | expect(config.inspect).to include("table_keep_resident: 1,") 89 | end 90 | 91 | it "allows total_component_instances configuration" do 92 | config.total_component_instances = 1 93 | expect(config.inspect).to include("total_component_instances: 1,") 94 | end 95 | 96 | it "allows total_core_instances configuration" do 97 | config.total_core_instances = 1 98 | expect(config.inspect).to include("total_core_instances: 1,") 99 | end 100 | 101 | it "allows total_stacks configuration" do 102 | config.total_stacks = 1 103 | expect(config.inspect).to include("total_stacks: 1,") 104 | end 105 | 106 | it "allows checking memory_protection_keys_available?" do 107 | expect(PoolingAllocationConfig.memory_protection_keys_available?).to be(true).or(be(false)) 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /spec/unit/store_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module Wasmtime 4 | RSpec.describe Store do 5 | describe ".new" do 6 | it "default to nil data" do 7 | store = Store.new(engine) 8 | expect(store.data).to be_nil 9 | end 10 | 11 | it "accepts user-provided data" do 12 | data = BasicObject.new 13 | store = Store.new(engine, data) 14 | expect(store.data).to equal(data) 15 | end 16 | 17 | it "can be gc compacted" do 18 | data = {foo: "bar"} 19 | 10.times { data[:baz] = SecureRandom.hex(1024) } 20 | obj = Struct.new(:value).new(data) 21 | store = Store.new(engine, obj) 22 | 10.times { data[:baz] = SecureRandom.hex(1024) } 23 | data[:baz] = "qux" 24 | 4.times { GC.start(full_mark: true) } 25 | GC.compact 26 | expect(store.data.value).to eql({foo: "bar", baz: "qux"}) 27 | end 28 | 29 | context "limits" do 30 | [ 31 | :memory_size, 32 | :table_elements, 33 | :instances, 34 | :tables, 35 | :memories 36 | ].each do |limit_prop| 37 | it "rejects non-numeric #{limit_prop}" do 38 | expect { Store.new(engine, limits: {limit_prop => "bad"}) }.to raise_error(TypeError) 39 | end 40 | end 41 | 42 | it "sets a memory size limit" do 43 | store = Store.new(engine, limits: {memory_size: 150_000}) 44 | 45 | mem = Memory.new(store, min_size: 1) 46 | mem.grow(1) 47 | expect { mem.grow(1) }.to raise_error(Wasmtime::Error, "failed to grow memory by `1`") 48 | end 49 | 50 | it "sets a table elements limit" do 51 | store = Store.new(engine, limits: {table_elements: 1}) 52 | 53 | table = Table.new(store, :funcref, nil, min_size: 1) 54 | expect { table.grow(1, nil) }.to raise_error(Wasmtime::Error, "failed to grow table by `1`") 55 | end 56 | 57 | it "sets a instances limit" do 58 | store = Store.new(engine, limits: {instances: 1}) 59 | 60 | mod = Module.new(engine, <<~WAT) 61 | (module 62 | (func nop) 63 | (start 0)) 64 | WAT 65 | 66 | Instance.new(store, mod) 67 | expect { Instance.new(store, mod) }.to raise_error(Wasmtime::Error, "resource limit exceeded: instance count too high at 2") 68 | end 69 | 70 | it "sets a tables limit" do 71 | store = Store.new(engine, limits: {tables: 1}) 72 | 73 | mod = Module.new(engine, <<~WAT) 74 | (module 75 | (table $table1 1 funcref) 76 | (table $table2 1 funcref) 77 | (func nop) 78 | (start 0)) 79 | WAT 80 | 81 | expect { Instance.new(store, mod) }.to raise_error(Wasmtime::Error, "resource limit exceeded: table count too high at 2") 82 | end 83 | 84 | it "sets a memories limit" do 85 | store = Store.new(engine, limits: {memories: 1}) 86 | 87 | mod = Module.new(engine, <<~WAT) 88 | (module 89 | (memory $memory1 1) 90 | (memory $memory2 1) 91 | (func nop) 92 | (start 0)) 93 | WAT 94 | 95 | expect { Instance.new(store, mod) }.to raise_error(Wasmtime::Error, "resource limit exceeded: memory count too high at 2") 96 | end 97 | 98 | it "handles multiple keywords" do 99 | store = Store.new(engine, limits: {memories: 1, tables: 1}) 100 | 101 | memory_mod = Module.new(engine, <<~WAT) 102 | (module 103 | (memory $memory1 1) 104 | (memory $memory2 1) 105 | (func nop) 106 | (start 0)) 107 | WAT 108 | 109 | table_mod = Module.new(engine, <<~WAT) 110 | (module 111 | (table $table1 1 funcref) 112 | (table $table2 1 funcref) 113 | (func nop) 114 | (start 0)) 115 | WAT 116 | 117 | expect { Instance.new(store, memory_mod) }.to raise_error(Wasmtime::Error, "resource limit exceeded: memory count too high at 2") 118 | expect { Instance.new(store, table_mod) }.to raise_error(Wasmtime::Error, "resource limit exceeded: table count too high at 2") 119 | end 120 | end 121 | 122 | describe "#linear_memory_limit_hit?" do 123 | it "returns false when the limit hasn't been hit" do 124 | store = Store.new(engine, limits: {memory_size: 1_000_000}) 125 | Memory.new(store, min_size: 1) 126 | expect(store.linear_memory_limit_hit?).to be false 127 | end 128 | 129 | it "returns true when the limit has been hit" do 130 | store = Store.new(engine, limits: {memory_size: 65536}) 131 | mem = Memory.new(store, min_size: 1) 132 | expect { 133 | # Try to grow the memory beyond the limit 134 | mem.grow(2) 135 | }.to raise_error(Wasmtime::Error, /failed to grow memory/) 136 | expect(store.linear_memory_limit_hit?).to be true 137 | end 138 | end 139 | 140 | describe "#max_linear_memory_consumed" do 141 | it "returns the maximum linear memory consumed" do 142 | store = Store.new(engine, limits: {memory_size: 1_000_000}) 143 | Memory.new(store, min_size: 2) 144 | expect(store.max_linear_memory_consumed).to be >= 65536 * 2 145 | end 146 | end 147 | end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /spec/unit/table_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module Wasmtime 4 | RSpec.describe Table do 5 | describe ".new" do 6 | it "creates a table with no default" do 7 | table = Table.new(store, :funcref, nil, min_size: 1) 8 | expect(table).to be_instance_of(Wasmtime::Table) 9 | end 10 | 11 | it "creates a table with default func" do 12 | table = Table.new(store, :funcref, noop_func, min_size: 1) 13 | expect(table).to be_instance_of(Wasmtime::Table) 14 | end 15 | end 16 | 17 | describe "#type" do 18 | it "returns the Wasm type as a symbol" do 19 | table = Table.new(store, :funcref, nil, min_size: 1) 20 | expect(table.type).to eq(:funcref) 21 | end 22 | end 23 | 24 | describe "#min_size" do 25 | it "returns its min size" do 26 | table = Table.new(store, :funcref, nil, min_size: 1) 27 | expect(table.min_size).to eq(1) 28 | end 29 | end 30 | 31 | describe "#max_size" do 32 | it "returns its max size" do 33 | table = Table.new(store, :funcref, nil, min_size: 1) 34 | expect(table.max_size).to be_nil 35 | 36 | table = Table.new(store, :funcref, nil, min_size: 1, max_size: 2) 37 | expect(table.max_size).to eq(2) 38 | end 39 | end 40 | 41 | describe "#size" do 42 | it "returns its size" do 43 | table = Table.new(store, :funcref, nil, min_size: 1) 44 | expect(table.size).to eq(1) 45 | end 46 | end 47 | 48 | describe "#grow" do 49 | it "increases the size" do 50 | table = Table.new(store, :funcref, nil, min_size: 1) 51 | expect { table.grow(2, nil) }.to change { table.size }.by(2) 52 | end 53 | 54 | it "returns the previous size" do 55 | table = Table.new(store, :funcref, nil, min_size: 1) 56 | expect(table.grow(1, nil)).to eq(1) 57 | end 58 | 59 | it "raises when growing past the maximum" do 60 | table = Table.new(store, :funcref, nil, min_size: 1, max_size: 1) 61 | expect { table.grow(1, nil) }.to raise_error(Wasmtime::Error, "failed to grow table by `1`") 62 | end 63 | end 64 | 65 | describe "#get" do 66 | it "returns a Func" do 67 | table = Table.new(store, :funcref, noop_func, min_size: 1) 68 | expect(table.get(0)).to be_instance_of(Func) 69 | end 70 | 71 | it "returns an externref" do 72 | value = BasicObject.new 73 | table = Table.new(store, :externref, value, min_size: 1) 74 | expect(table.get(0)).to eq(value) 75 | end 76 | 77 | it "returns nil for null ref" do 78 | table = Table.new(store, :funcref, nil, min_size: 1) 79 | expect(table.get(0)).to be_nil 80 | end 81 | 82 | it "returns nil for out of bound" do 83 | table = Table.new(store, :funcref, noop_func, min_size: 1) 84 | expect(table.get(5)).to be_nil 85 | end 86 | end 87 | 88 | describe "#set" do 89 | it "writes nil" do 90 | table = Table.new(store, :funcref, noop_func, min_size: 1) 91 | table.set(0, nil) 92 | expect(table.get(0)).to be_nil 93 | end 94 | 95 | it "writes a Func" do 96 | table = Table.new(store, :funcref, noop_func, min_size: 1) 97 | table.set(0, noop_func) 98 | expect(table.get(0)).to be_instance_of(Func) 99 | end 100 | 101 | it "rejects invalid type" do 102 | table = Table.new(store, :funcref, noop_func, min_size: 1) 103 | expect { table.set(0, 1) }.to raise_error(TypeError) 104 | end 105 | end 106 | 107 | it "keeps externrefs alive" do 108 | table = Table.new(store, :externref, +"foo", min_size: 2) 109 | generate_new_objects 110 | expect(table.get(0)).to eq("foo") 111 | 112 | table.set(1, +"bar") 113 | generate_new_objects 114 | expect(table.get(1)).to eq("bar") 115 | 116 | table.grow(1, +"baz") 117 | generate_new_objects 118 | expect(table.get(2)).to eq("baz") 119 | end 120 | 121 | private 122 | 123 | def noop_func 124 | Func.new(store, [], []) { |_| } 125 | end 126 | 127 | def generate_new_objects 128 | "hi" * 3 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /spec/unit/trap_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module Wasmtime 4 | RSpec.describe Trap do 5 | let(:trap) do 6 | Wasmtime::Instance.new(store, module_trapping_on_start) 7 | rescue Trap => trap 8 | trap 9 | end 10 | 11 | describe "#message" do 12 | it "has a short message" do 13 | expect(trap.message).to eq("wasm trap: wasm `unreachable` instruction executed") 14 | end 15 | end 16 | 17 | describe "#message_with_backtrace" do 18 | it "includes the backtrace" do 19 | expect(trap.wasm_backtrace_message).to eq(<<~MSG.rstrip) 20 | error while executing at wasm backtrace: 21 | 0: 0x1a - ! 22 | MSG 23 | end 24 | end 25 | 26 | describe "#wasm_backtrace" do 27 | it "returns an enumerable of trace entries" do 28 | end 29 | end 30 | 31 | describe "#code" do 32 | it "returns a symbol matching a constant" do 33 | expect(trap.code).to eq(Trap::UNREACHABLE_CODE_REACHED) 34 | end 35 | end 36 | 37 | describe "#to_s" do 38 | it "is the same as message" do 39 | expect(trap.to_s).to eq(trap.message) 40 | end 41 | end 42 | 43 | describe "#inspect" do 44 | it "looks pretty" do 45 | expect(trap.inspect).to match(/\A#$/) 46 | end 47 | end 48 | 49 | def module_trapping_on_start 50 | Wasmtime::Module.new(engine, <<~WAT) 51 | (module 52 | (func unreachable) 53 | (start 0)) 54 | WAT 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/unit/wasmtime_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module Wasmtime 4 | RSpec.describe Wasmtime do 5 | describe ".wat2wasm" do 6 | it "returns a binary string" do 7 | wasm = Wasmtime.wat2wasm("(module)") 8 | expect(wasm.encoding).to eq(Encoding::ASCII_8BIT) 9 | end 10 | 11 | it "returns a valid module" do 12 | wasm = Wasmtime.wat2wasm("(module)") 13 | expect(wasm).to start_with("\x00asm") 14 | end 15 | 16 | it "raises on invalid WAT" do 17 | expect { Wasmtime.wat2wasm("not wat") }.to raise_error(Wasmtime::Error) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/wasmtime_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Wasmtime do 4 | it "has a version number" do 5 | expect(Wasmtime::VERSION).not_to be nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /suppressions/readme.md: -------------------------------------------------------------------------------- 1 | # Suppressions 2 | 3 | This folder includes Valgrind suppressions used by the 4 | [`ruby_memcheck`][ruby_memcheck] gem. If the `memcheck` CI job fails, you 5 | may need to add a suppression to `ruby-3.1.supp` to fix it. 6 | 7 | [ruby_memcheck]: https://github.com/Shopify/ruby_memcheck 8 | -------------------------------------------------------------------------------- /suppressions/ruby-3.1.supp: -------------------------------------------------------------------------------- 1 | { 2 | Valgrind is detecting a "Invalid read of size 8" during this process, not sure why 3 | Memcheck:Addr8 4 | fun:each_location.constprop.1 5 | fun:gc_mark_children 6 | ... 7 | } 8 | -------------------------------------------------------------------------------- /wasmtime.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/wasmtime/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "wasmtime" 7 | spec.version = Wasmtime::VERSION 8 | spec.authors = ["The Wasmtime Project Developers"] 9 | spec.email = ["hello@bytecodealliance.org"] 10 | 11 | spec.summary = "Wasmtime bindings for Ruby" 12 | spec.description = "A Ruby binding for Wasmtime, a WebAssembly runtime." 13 | spec.homepage = "https://github.com/BytecodeAlliance/wasmtime-rb" 14 | spec.license = "Apache-2.0" 15 | spec.required_ruby_version = ">= 3.1.0" 16 | 17 | spec.metadata["source_code_uri"] = "https://github.com/BytecodeAlliance/wasmtime-rb" 18 | spec.metadata["cargo_crate_name"] = "wasmtime-rb" 19 | spec.metadata["changelog_uri"] = "https://github.com/bytecodealliance/wasmtime-rb/blob/main/CHANGELOG.md" 20 | 21 | spec.files = Dir["{lib,ext}/**/*", "LICENSE", "README.md", "Cargo.*"] 22 | spec.files.reject! { |f| File.directory?(f) } 23 | spec.files.reject! { |f| f =~ /\.(dll|so|dylib|lib|bundle)\Z/ } 24 | spec.bindir = "exe" 25 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } 26 | spec.require_paths = ["lib"] 27 | 28 | spec.extensions = ["ext/extconf.rb"] 29 | 30 | spec.rdoc_options += ["--exclude", "vendor"] 31 | 32 | spec.add_dependency "rb_sys", "~> 0.9.108" 33 | end 34 | --------------------------------------------------------------------------------