diff --git a/.dependabot/config.yml b/.dependabot/config.yml new file mode 100644 index 0000000000..ed349bd7c0 --- /dev/null +++ b/.dependabot/config.yml @@ -0,0 +1,10 @@ +# See https://dependabot.com/docs/config-file/ for a reference. +version: 1 +update_configs: + - package_manager: "rust:cargo" + directory: "/" + update_schedule: "live" + version_requirement_updates: "auto" + allowed_updates: + - match: + update_type: "security" diff --git a/.github/actions/binary-compatible-builds/README.md b/.github/actions/binary-compatible-builds/README.md new file mode 100644 index 0000000000..8368fd4f1a --- /dev/null +++ b/.github/actions/binary-compatible-builds/README.md @@ -0,0 +1,9 @@ +# binary-compatible-builds + +A small (ish) action which is intended to be used and will configure builds of +Rust projects to be "more binary compatible". On Windows and macOS this +involves setting a few env vars, and on Linux this involves spinning up a CentOS +6 container which is running in the background. + +All subsequent build commands need to be wrapped in `$CENTOS` to optionally run +on `$CENTOS` on Linux to ensure builds happen inside the container. diff --git a/.github/actions/binary-compatible-builds/action.yml b/.github/actions/binary-compatible-builds/action.yml new file mode 100644 index 0000000000..de2e74ec77 --- /dev/null +++ b/.github/actions/binary-compatible-builds/action.yml @@ -0,0 +1,6 @@ +name: 'Set up a CentOS 6 container to build releases in' +description: 'Set up a CentOS 6 container to build releases in' + +runs: + using: node12 + main: 'main.js' diff --git a/.github/actions/binary-compatible-builds/main.js b/.github/actions/binary-compatible-builds/main.js new file mode 100755 index 0000000000..05fc93f8ab --- /dev/null +++ b/.github/actions/binary-compatible-builds/main.js @@ -0,0 +1,69 @@ +#!/usr/bin/env node + +const child_process = require('child_process'); +const stdio = { stdio: 'inherit' }; + +// On OSX all we need to do is configure our deployment target as old as +// possible. For now 10.9 is the limit. +if (process.platform == 'darwin') { + console.log("::set-env name=MACOSX_DEPLOYMENT_TARGET::10.9"); + console.log("::set-env name=python::python3"); + return; +} + +// On Windows we build against the static CRT to reduce dll dependencies +if (process.platform == 'win32') { + console.log("::set-env name=RUSTFLAGS::-Ctarget-feature=+crt-static"); + console.log("::set-env name=python::python"); + return; +} + +// ... and on Linux we do fancy things with containers. We'll spawn an old +// CentOS container in the background with a super old glibc, and then we'll run +// commands in there with the `$CENTOS` env var. + +if (process.env.CENTOS !== undefined) { + const args = ['exec', '-w', process.cwd(), '-i', 'centos']; + for (const arg of process.argv.slice(2)) { + args.push(arg); + } + child_process.execFileSync('docker', args, stdio); + return; +} + +// Add our rust mount onto PATH, but also add some stuff to PATH from +// the packages that we install. +let path = process.env.PATH; +path = `${path}:/rust/bin`; +path = `/opt/rh/devtoolset-8/root/usr/bin:${path}`; +path = `/opt/rh/rh-python36/root/usr/bin:${path}`; + +// Spawn a container daemonized in the background which we'll connect to via +// `docker exec`. This'll have access to the current directory. +child_process.execFileSync('docker', [ + 'run', + '-di', + '--name', 'centos', + '-v', `${process.cwd()}:${process.cwd()}`, + '-v', `${child_process.execSync('rustc --print sysroot').toString().trim()}:/rust:ro`, + '--env', `PATH=${path}`, + 'centos:6', +], stdio); + +// Use ourselves to run future commands +console.log(`::set-env name=CENTOS::${__filename}`) + +// See https://edwards.sdsu.edu/research/c11-on-centos-6/ for where these +const exec = s => { + child_process.execSync(`docker exec centos ${s}`, stdio); +}; +exec('yum install -y centos-release-scl cmake xz epel-release'); +exec('yum install -y rh-python36 patchelf unzip'); +exec('yum install -y devtoolset-8-gcc devtoolset-8-binutils devtoolset-8-gcc-c++'); +exec('yum install -y git'); + +// Delete `libstdc++.so` to force gcc to link against `libstdc++.a` instead. +// This is a hack and not the right way to do this, but it ends up doing the +// right thing for now. +exec('rm -f /opt/rh/devtoolset-8/root/usr/lib/gcc/x86_64-redhat-linux/8/libstdc++.so'); +console.log("::set-env name=python::python3"); diff --git a/.github/actions/define-dwarfdump-env/README.md b/.github/actions/define-dwarfdump-env/README.md new file mode 100644 index 0000000000..035ac379a1 --- /dev/null +++ b/.github/actions/define-dwarfdump-env/README.md @@ -0,0 +1,3 @@ +# define-dwarfdump-env + +Defines `DWARFDUMP` path executable. diff --git a/.github/actions/define-dwarfdump-env/action.yml b/.github/actions/define-dwarfdump-env/action.yml new file mode 100644 index 0000000000..36f77b60b8 --- /dev/null +++ b/.github/actions/define-dwarfdump-env/action.yml @@ -0,0 +1,6 @@ +name: 'Set up a DWARFDUMP env' +description: 'Set up a DWARFDUMP env (see tests/debug/dump.rs)' + +runs: + using: node12 + main: 'main.js' diff --git a/.github/actions/define-dwarfdump-env/main.js b/.github/actions/define-dwarfdump-env/main.js new file mode 100755 index 0000000000..cddcc7f552 --- /dev/null +++ b/.github/actions/define-dwarfdump-env/main.js @@ -0,0 +1,11 @@ +#!/usr/bin/env node + +// On OSX pointing to brew's LLVM location. +if (process.platform == 'darwin') { + console.log("::set-env name=DWARFDUMP::/usr/local/opt/llvm/bin/llvm-dwarfdump"); +} + +// On Linux pointing to specific version +if (process.platform == 'linux') { + console.log("::set-env name=DWARFDUMP::/usr/bin/llvm-dwarfdump-9"); +} diff --git a/.github/actions/github-release/Dockerfile b/.github/actions/github-release/Dockerfile new file mode 100644 index 0000000000..5849eac7d2 --- /dev/null +++ b/.github/actions/github-release/Dockerfile @@ -0,0 +1,8 @@ +FROM node:slim + +COPY . /action +WORKDIR /action + +RUN npm install --production + +ENTRYPOINT ["node", "/action/main.js"] diff --git a/.github/actions/github-release/README.md b/.github/actions/github-release/README.md new file mode 100644 index 0000000000..c70ba8f495 --- /dev/null +++ b/.github/actions/github-release/README.md @@ -0,0 +1,18 @@ +# github-release + +An action used to publish GitHub releases for `wasmtime`. + +As of the time of this writing there's a few actions floating around which +perform github releases but they all tend to have their set of drawbacks. +Additionally nothing handles deleting releases which we need for our rolling +`dev` release. + +To handle all this this action rolls-its-own implementation using the +actions/toolkit repository and packages published there. These run in a Docker +container and take various inputs to orchestrate the release from the build. + +More comments can be found in `main.js`. + +Testing this is really hard. If you want to try though run `npm install` and +then `node main.js`. You'll have to configure a bunch of env vars though to get +anything reasonably working. diff --git a/.github/actions/github-release/action.yml b/.github/actions/github-release/action.yml new file mode 100644 index 0000000000..51a074adfa --- /dev/null +++ b/.github/actions/github-release/action.yml @@ -0,0 +1,15 @@ +name: 'wasmtime github releases' +description: 'wasmtime github releases' +inputs: + token: + description: '' + required: true + name: + description: '' + required: true + files: + description: '' + required: true +runs: + using: 'docker' + image: 'Dockerfile' diff --git a/.github/actions/github-release/main.js b/.github/actions/github-release/main.js new file mode 100644 index 0000000000..567fe3967f --- /dev/null +++ b/.github/actions/github-release/main.js @@ -0,0 +1,117 @@ +const core = require('@actions/core'); +const path = require("path"); +const fs = require("fs"); +const github = require('@actions/github'); +const glob = require('glob'); + +function sleep(milliseconds) { + return new Promise(resolve => setTimeout(resolve, milliseconds)) +} + +async function runOnce() { + // Load all our inputs and env vars. Note that `getInput` reads from `INPUT_*` + const files = core.getInput('files'); + const name = core.getInput('name'); + const token = core.getInput('token'); + const slug = process.env.GITHUB_REPOSITORY; + const owner = slug.split('/')[0]; + const repo = slug.split('/')[1]; + const sha = process.env.GITHUB_SHA; + + core.info(`files: ${files}`); + core.info(`name: ${name}`); + core.info(`token: ${token}`); + + const octokit = new github.GitHub(token); + + // Delete the previous release since we can't overwrite one. This may happen + // due to retrying an upload or it may happen because we're doing the dev + // release. + const releases = await octokit.paginate("GET /repos/:owner/:repo/releases", { owner, repo }); + for (const release of releases) { + if (release.tag_name !== name) { + continue; + } + const release_id = release.id; + core.info(`deleting release ${release_id}`); + await octokit.repos.deleteRelease({ owner, repo, release_id }); + } + + // We also need to update the `dev` tag while we're at it on the `dev` branch. + if (name == 'dev') { + try { + core.info(`updating dev tag`); + await octokit.git.updateRef({ + owner, + repo, + ref: 'tags/dev', + sha, + force: true, + }); + } catch (e) { + console.log("ERROR: ", JSON.stringify(e, null, 2)); + core.info(`creating dev tag`); + await octokit.git.createTag({ + owner, + repo, + tag: 'dev', + message: 'dev release', + object: sha, + type: 'commit', + }); + } + } + + // Creates an official GitHub release for this `tag`, and if this is `dev` + // then we know that from the previous block this should be a fresh release. + core.info(`creating a release`); + const release = await octokit.repos.createRelease({ + owner, + repo, + tag_name: name, + prerelease: name === 'dev', + }); + + // Upload all the relevant assets for this release as just general blobs. + for (const file of glob.sync(files)) { + const size = fs.statSync(file).size; + core.info(`upload ${file}`); + await octokit.repos.uploadReleaseAsset({ + data: fs.createReadStream(file), + headers: { 'content-length': size, 'content-type': 'application/octet-stream' }, + name: path.basename(file), + url: release.data.upload_url, + }); + } +} + +async function run() { + const retries = 10; + for (let i = 0; i < retries; i++) { + try { + await runOnce(); + break; + } catch (e) { + if (i === retries - 1) + throw e; + logError(e); + console.log("RETRYING after 10s"); + await sleep(10000) + } + } +} + +function logError(e) { + console.log("ERROR: ", e.message); + try { + console.log(JSON.stringify(e, null, 2)); + } catch (e) { + // ignore json errors for now + } + console.log(e.stack); +} + +run().catch(err => { + logError(err); + core.setFailed(err.message); +}); diff --git a/.github/actions/github-release/package.json b/.github/actions/github-release/package.json new file mode 100644 index 0000000000..abfc55f6ff --- /dev/null +++ b/.github/actions/github-release/package.json @@ -0,0 +1,10 @@ +{ + "name": "wasmtime-github-release", + "version": "0.0.0", + "main": "main.js", + "dependencies": { + "@actions/core": "^1.0.0", + "@actions/github": "^1.0.0", + "glob": "^7.1.5" + } +} diff --git a/.github/actions/install-rust/README.md b/.github/actions/install-rust/README.md new file mode 100644 index 0000000000..df8e94dccc --- /dev/null +++ b/.github/actions/install-rust/README.md @@ -0,0 +1,18 @@ +# install-rust + +A small github action to install `rustup` and a Rust toolchain. This is +generally expressed inline, but it was repeated enough in this repository it +seemed worthwhile to extract. + +Some gotchas: + +* Can't `--self-update` on Windows due to permission errors (a bug in Github + Actions) +* `rustup` isn't installed on macOS (a bug in Github Actions) + +When the above are fixed we should delete this action and just use this inline: + +```yml +- run: rustup update $toolchain && rustup default $toolchain + shell: bash +``` diff --git a/.github/actions/install-rust/action.yml b/.github/actions/install-rust/action.yml new file mode 100644 index 0000000000..7a19659184 --- /dev/null +++ b/.github/actions/install-rust/action.yml @@ -0,0 +1,12 @@ +name: 'Install Rust toolchain' +description: 'Install both `rustup` and a Rust toolchain' + +inputs: + toolchain: + description: 'Default toolchan to install' + required: false + default: 'stable' + +runs: + using: node12 + main: 'main.js' diff --git a/.github/actions/install-rust/main.js b/.github/actions/install-rust/main.js new file mode 100644 index 0000000000..b2355abfa1 --- /dev/null +++ b/.github/actions/install-rust/main.js @@ -0,0 +1,13 @@ +const child_process = require('child_process'); +const toolchain = process.env.INPUT_TOOLCHAIN; + +if (process.platform === 'darwin') { + child_process.execSync(`curl https://sh.rustup.rs | sh -s -- -y --default-toolchain=none --profile=minimal`); + const bindir = `${process.env.HOME}/.cargo/bin`; + console.log(`::add-path::${bindir}`); + process.env.PATH = `${process.env.PATH}:${bindir}`; +} + +child_process.execFileSync('rustup', ['set', 'profile', 'minimal']); +child_process.execFileSync('rustup', ['update', toolchain, '--no-self-update']); +child_process.execFileSync('rustup', ['default', toolchain]); diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000000..f1679f2e79 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,546 @@ +name: CI +on: + push: + branches: [master] + tags-ignore: [dev] + pull_request: + branches: [master] + +jobs: + # Check Code style quickly by running `rustfmt` over all code + rustfmt: + name: Rustfmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + with: + submodules: true + - uses: ./.github/actions/install-rust + - run: cargo fmt --all -- --check + + # Build `mdBook` documentation for `wasmtime`, and upload it as a temporary + # build artifact + doc_book: + name: Doc - build the book + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + with: + submodules: true + - run: | + set -e + curl -L https://github.com/rust-lang-nursery/mdBook/releases/download/v0.3.1/mdbook-v0.3.1-x86_64-unknown-linux-gnu.tar.gz | tar xzf - + echo ::add-path::`pwd` + - run: (cd docs && mdbook build) + - run: cargo build -p wasmtime + - run: (cd docs && mdbook test -L ../target/debug/deps) + - uses: actions/upload-artifact@v1 + with: + name: doc-book + path: docs/book + + # Build rustdoc API documentation for `wasmtime*` crates. Note that we don't + # want to document all our transitive dependencies, hence `--no-deps`. This is + # a temporary build artifact we upload to consume later. + doc_api: + name: Doc - build the API documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + with: + submodules: true + - uses: ./.github/actions/install-rust + with: + toolchain: nightly + - run: cargo doc --no-deps --all --exclude wasmtime-cli --exclude test-programs + - uses: actions/upload-artifact@v1 + with: + name: doc-api + path: target/doc + + # Download our libFuzzer corpus and make sure that we can still handle all the + # inputs. + fuzz_corpora: + name: Fuzz Corpora + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + with: + submodules: true + - uses: actions/checkout@v1 + with: + repository: bytecodealliance/wasmtime-libfuzzer-corpus + path: ./wasmtime/fuzz/corpus + ref: refs/heads/master + - uses: ./.github/actions/install-rust + with: + toolchain: nightly + - run: cargo install cargo-fuzz --vers "^0.7" + - run: cargo fetch + working-directory: ./fuzz + - run: cargo fuzz build --release --debug-assertions + # Our corpora are too large to run in full on every pull request, they just + # take too long. Instead, we sample some of them and make sure that running + # our fuzzers over the sampled inputs still works OK. + - run: | + find fuzz/corpus/compile -type f \ + | shuf \ + | head -n 3000 \ + | xargs cargo fuzz run compile --release --debug-assertions + - run: | + find fuzz/corpus/instantiate -type f \ + | shuf \ + | head -n 2000 \ + | xargs cargo fuzz run instantiate --release --debug-assertions + - run: | + find fuzz/corpus/instantiate_translated -type f \ + | shuf \ + | head -n 1000 \ + | xargs cargo fuzz run instantiate_translated --release --debug-assertions + - run: | + find fuzz/corpus/api_calls -type f \ + | shuf \ + | head -n 100 \ + | xargs cargo fuzz run api_calls --release --debug-assertions + - run: | + find fuzz/corpus/differential -type f \ + | shuf \ + | head -n 100 \ + | xargs cargo fuzz run differential --release --debug-assertions + + # Install wasm32-unknown-emscripten target, and ensure `crates/wasi-common` + # compiles to Emscripten. + # TODO enable once rust-lang/rust#66308 is fixed + # emscripten: + # name: Emscripten + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v1 + # with: + # submodules: true + # - uses: ./.github/actions/install-rust + # - run: rustup target add wasm32-unknown-emscripten + # - run: cargo build --target wasm32-unknown-emscripten -p wasi-common + + # Perform all tests (debug mode) for `wasmtime`. This runs stable/beta/nightly + # channels of Rust as well as macOS/Linux/Windows. + test: + name: Test + runs-on: ${{ matrix.os }} + strategy: + matrix: + build: [stable, beta, nightly, windows, macos] + include: + - build: stable + os: ubuntu-latest + rust: stable + - build: beta + os: ubuntu-latest + rust: beta + - build: nightly + os: ubuntu-latest + rust: nightly + - build: macos + os: macos-latest + rust: stable + - build: windows + os: windows-latest + rust: stable + steps: + - uses: actions/checkout@v1 + with: + submodules: true + - uses: ./.github/actions/install-rust + with: + toolchain: ${{ matrix.rust }} + - uses: ./.github/actions/define-dwarfdump-env + + - name: Install libclang + # Note: libclang is pre-installed on the macOS and linux images. + if: matrix.os == 'windows-latest' + run: | + Invoke-WebRequest http://releases.llvm.org/9.0.0/LLVM-9.0.0-win64.exe -OutFile llvm-installer.exe + 7z x llvm-installer.exe -oC:\llvm-binary + Write-Host ::set-env name=LIBCLANG_PATH::C:\llvm-binary\bin\libclang.dll + Write-Host ::add-path::C:\llvm-binary\bin + + - name: Query Clang Version + if: matrix.os == 'windows-latest' + run: | + Get-Command clang.exe + clang.exe --version + + # Install wasm32-wasi target in order to build wasi-common's integration + # tests + - run: rustup target add wasm32-wasi + + - run: cargo fetch --locked + - run: cargo fetch --locked --manifest-path crates/test-programs/wasi-tests/Cargo.toml + + # Build some various feature combinations + - run: cargo build --manifest-path crates/api/Cargo.toml --no-default-features + - run: cargo build --manifest-path crates/api/Cargo.toml --features wat + - run: cargo build --manifest-path crates/api/Cargo.toml --features lightbeam + if: matrix.rust == 'nightly' + + # Build and test all features except for lightbeam + - run: cargo test --features test_programs --all --exclude lightbeam --exclude wasmtime-c-api -- --nocapture + env: + RUST_BACKTRACE: 1 + RUSTFLAGS: "-D warnings" + + # Build and test lightbeam if we're using the nightly toolchain. Note that + # Lightbeam tests fail right now, but we don't want to block on that. + - run: cargo build --package lightbeam + if: matrix.rust == 'nightly' + - run: cargo test --package lightbeam -- --nocapture + if: matrix.rust == 'nightly' + continue-on-error: true + env: + RUST_BACKTRACE: 1 + + # Build and test c-api examples. Skipping testing on Windows due to + # GNU make dependency when executing the wasm-c-api examples. + - run: cargo build --package wasmtime-c-api + if: matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest' + - run: cargo test --package wasmtime-c-api -- --nocapture --test-threads 1 + if: matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest' + continue-on-error: true + env: + RUST_BACKTRACE: 1 + + # Builds a Python wheel (package) for Windows/Mac/Linux. Note that we're + # careful to create binary-compatible releases here to old releases of + # Windows/Mac/Linux. This will also build wheels for Python 3.6, 3.7 and 3.8. + wheels: + name: Python Wheel + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v1 + with: + submodules: true + - uses: ./.github/actions/install-rust + with: + toolchain: nightly-2020-01-06 + - uses: ./.github/actions/binary-compatible-builds + - run: mkdir crates/misc/py/wheelhouse + shell: bash + + # Install Python & dependencies needed for our `setup.py` scripts + - name: Setup Python 3.6 + uses: actions/setup-python@v1 + with: + python-version: '3.6' + architecture: x64 + - run: $CENTOS pip3 install setuptools wheel setuptools-rust + shell: bash + - run: (cd crates/misc/py && $CENTOS $python setup.py bdist_wheel) + shell: bash + + # Clear the build directory between building different wheels for different + # Python versions to ensure that we don't package dynamic libraries twice by + # accident. + - run: $CENTOS rm -rf crates/misc/py/build + shell: bash + + # Set up Python 3.7 (and build it on Linux), reinstall dependencies, then + # rebuild our wheels + - name: Setup Python 3.7 + uses: actions/setup-python@v1 + with: + python-version: '3.7' + architecture: x64 + if: matrix.os != 'ubuntu-latest' + - name: Build Python 3.7 + run: $CENTOS sh ci/setup_centos6_python3.sh 3.7.3 + if: matrix.os == 'ubuntu-latest' + - run: $CENTOS pip3 install setuptools wheel setuptools-rust auditwheel + shell: bash + - run: (cd crates/misc/py && $CENTOS $python setup.py bdist_wheel) + shell: bash + + # Clear the build directory between building different wheels for different + # Python versions to ensure that we don't package dynamic libraries twice by + # accident. + - run: $CENTOS rm -rf crates/misc/py/build + shell: bash + + # Set up Python 3.8 (and build it on Linux), reinstall dependencies, then + # rebuild our wheels + - name: Setup Python 3.8 + uses: actions/setup-python@v1 + with: + python-version: '3.8' + architecture: x64 + if: matrix.os != 'ubuntu-latest' + - name: Build Python 3.8 + run: $CENTOS sh ci/setup_centos6_python3.sh 3.8.0 + if: matrix.os == 'ubuntu-latest' + - run: $CENTOS pip3 install setuptools wheel setuptools-rust auditwheel + shell: bash + - run: (cd crates/misc/py && $CENTOS $python setup.py bdist_wheel) + shell: bash + + # Move `dist/*.whl` into `wheelhouse/` so we can deploy them, but on Linux we + # need to run an `auditwheel` command as well to turn these into "manylinux" + # wheels to run across a number of distributions. + - run: cp crates/misc/py/dist/*.whl crates/misc/py/wheelhouse/ + shell: bash + if: matrix.os != 'ubuntu-latest' + - run: | + set -e + cd crates/misc/py + for whl in dist/*.whl; do + $CENTOS auditwheel repair "$whl" -w wheelhouse/ + done + shell: bash + if: matrix.os == 'ubuntu-latest' + + # Upload this for the publishing stage of pipelines + - uses: actions/upload-artifact@v1 + with: + name: wheels-${{ matrix.os }} + path: crates/misc/py/wheelhouse + + # Perform release builds of `wasmtime` and `libwasmtime.so`. Builds on + # Windows/Mac/Linux, and artifacts are uploaded after the build is finished. + # Note that we also run tests here to test exactly what we're deploying. + build: + name: Build wasmtime + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v1 + with: + submodules: true + - uses: ./.github/actions/install-rust + - uses: ./.github/actions/binary-compatible-builds + + # Install wasm32-wasi target in order to build wasi-common's integration + # tests + - run: rustup target add wasm32-wasi + + # Build `wasmtime` and executables + - run: $CENTOS cargo build --release --bin wasmtime --bin wasm2obj + shell: bash + # Build `libwasmtime.so` + - run: $CENTOS cargo build --release --manifest-path crates/c-api/Cargo.toml + shell: bash + # Test what we just built + - run: $CENTOS cargo test --features test_programs --release --all --exclude lightbeam --exclude wasmtime --exclude wasmtime-c-api --exclude wasmtime-fuzzing -- --skip test_debug_dwarf_ + shell: bash + env: + RUST_BACKTRACE: 1 + + # Postprocess the macOS dylib a bit to have a more reasonable `LC_ID_DYLIB` + # directive than the default one that comes out of the linker when typically + # doing `cargo build`. For more info see #984 + - run: install_name_tool -id "@rpath/libwasmtime.dylib" target/release/libwasmtime.dylib + if: matrix.os == 'macos-latest' + + # ... and now perform some goop to move all the relevant artifacts into + # something that we'll upload from this action. + + - run: mkdir dist + shell: bash + + # Move binaries to dist folder + - run: cp target/release/{wasmtime,wasm2obj} dist + if: matrix.os != 'windows-latest' + - run: cp target/release/{wasmtime,wasm2obj}.exe dist + shell: bash + if: matrix.os == 'windows-latest' + + # Move libwasmtime dylib to dist folder + - run: cp target/release/libwasmtime.{so,a} dist + if: matrix.os == 'ubuntu-latest' + - run: cp target/release/libwasmtime.{dylib,a} dist + if: matrix.os == 'macos-latest' + - run: cp target/release/wasmtime.{dll,lib} dist + shell: bash + if: matrix.os == 'windows-latest' + + # Make a Windows MSI installer if we're on Windows + - run: | + export WT_VERSION=`cat Cargo.toml | sed -n 's/^version = "\([^"]*\)".*/\1/p'` + "$WIX/bin/candle" -arch x64 -out target/wasmtime.wixobj ci/wasmtime.wxs + "$WIX/bin/light" -out dist/installer.msi target/wasmtime.wixobj -ext WixUtilExtension + rm dist/installer.wixpdb + shell: bash + if: matrix.os == 'windows-latest' + + - uses: actions/upload-artifact@v1 + with: + name: bins-${{ matrix.os }} + path: dist + + # Build and test the .NET bindings + dotnet: + name: Test Wasmtime for .NET bindings + runs-on: ${{ matrix.os }} + strategy: + matrix: + build: [linux-debug, linux-release, macos-debug, macos-release, windows-debug, windows-release] + include: + - build: linux-debug + os: ubuntu-latest + config: debug + - build: linux-release + os: ubuntu-latest + config: release + - build: macos-debug + os: macos-latest + config: debug + - build: macos-release + os: macos-latest + config: release + - build: windows-debug + os: windows-latest + config: debug + - build: windows-release + os: windows-latest + config: release + steps: + - uses: actions/checkout@v1 + with: + submodules: true + - uses: ./.github/actions/install-rust + - uses: ./.github/actions/binary-compatible-builds + - run: rustup target add wasm32-wasi + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: '3.0.101' + - name: Test + run: | + cd crates/misc/dotnet/tests + dotnet test -c ${{ matrix.config }} + - name: Create package + run: | + cd crates/misc/dotnet/src + dotnet pack -c ${{ matrix.config }} + if: matrix.os == 'macos-latest' # Currently the pack target only supports macOS + + # Consumes all published artifacts from all the previous build steps, creates + # a bunch of tarballs for all of them, and then publishes the tarballs + # themselves as an artifact (for inspection) and then optionally creates + # github releases and/or tags for pushes. + publish: + name: Publish + needs: [doc_book, doc_api, wheels, build] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + with: + submodules: true + - run: rustup update stable && rustup default stable + + # Download all the artifacts that we'll be publishing. Should keep an eye on + # the `download-artifact` repository to see if we can ever get something + # like "download all artifacts" or "download this list of artifacts" + - name: Download book + uses: actions/download-artifact@v1 + with: + name: doc-book + - name: Download API docs + uses: actions/download-artifact@v1 + with: + name: doc-api + - name: Download macOS Wheel + uses: actions/download-artifact@v1 + with: + name: wheels-macos-latest + - name: Download macOS binaries + uses: actions/download-artifact@v1 + with: + name: bins-macos-latest + - name: Download Linux Wheel + uses: actions/download-artifact@v1 + with: + name: wheels-ubuntu-latest + - name: Download Linux binaries + uses: actions/download-artifact@v1 + with: + name: bins-ubuntu-latest + - name: Download Windows Wheel + uses: actions/download-artifact@v1 + with: + name: wheels-windows-latest + - name: Download Windows binaries + uses: actions/download-artifact@v1 + with: + name: bins-windows-latest + + - name: Assemble gh-pages + run: | + mv doc-book gh-pages + mv doc-api gh-pages/api + + # If this is a push to the master branch push to the `gh-pages` using a + # deploy key. Note that a deploy key is necessary for now because otherwise + # using the default token for github actions doesn't actually trigger a page + # rebuild. + - name: Push to gh-pages + run: curl -LsSf https://git.io/fhJ8n | rustc - && (cd gh-pages && ../rust_out) + env: + GITHUB_DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} + BUILD_REPOSITORY_ID: ${{ github.repository }} + BUILD_SOURCEVERSION: ${{ github.sha }} + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + + - name: Calculate tag name + run: | + name=dev + if [[ $GITHUB_REF == refs/tags* ]]; then + name=${GITHUB_REF:10} + fi + echo ::set-output name=val::$name + echo ::set-env name=TAG::$name + id: tagname + + # Assemble all the build artifacts into tarballs and zip archives. + - name: Assemble tarballs + run: | + ./ci/build-tarballs.sh x86_64-linux ubuntu-latest + ./ci/build-tarballs.sh x86_64-windows windows-latest .exe + ./ci/build-tarballs.sh x86_64-macos macos-latest + + # Upload all assembled tarballs as an artifact of the github action run, so + # that way even PRs can inspect the output. + - uses: actions/upload-artifact@v1 + with: + name: tarballs + path: dist + + # The action 'pypa/gh-action-pypi-publish' will try to upload all files in the + # dist/ folder. This folder also contains non-package files, and therefore the + # action fails. + # + # To prevent the action from failing all .whl files are copied into a new + # directory. + - run: | + mkdir -p tmp/whl + find dist/ -name '*.whl' -type f -exec cp '{}' tmp/whl -v \; + + - name: Publish Python wheels on Pypi + uses: pypa/gh-action-pypi-publish@37e305e7413032d8422456179fee28fac7d25187 + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + with: + user: __token__ + password: ${{ secrets.pypi_password }} + packages_dir: tmp/whl + + # ... and if this was an actual push (tag or `master`) then we publish a + # new release. This'll automatically publish a tag release or update `dev` + # with this `sha` + - name: Publish Release + uses: ./.github/actions/github-release + if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags')) + with: + files: "dist/*" + name: ${{ steps.tagname.outputs.val }} + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..daa279dbaf --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +*.bk +*.swp +*.swo +*.swx +tags +target +.*.rustfmt +cranelift.dbg* +rusty-tags.* +*~ +\#*\# +docs/book +.vscode/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..59646f986c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "spec_testsuite"] + path = tests/spec_testsuite + url = https://github.com/WebAssembly/testsuite +[submodule "crates/c-api/examples/wasm-c-api"] + path = crates/c-api/examples/wasm-c-api + url = https://github.com/WebAssembly/wasm-c-api +[submodule "crates/wasi-common/WASI"] + path = crates/wasi-common/wig/WASI + url = https://github.com/WebAssembly/WASI diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000000..8148fc6346 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1 @@ +# This file tells tools we use rustfmt. We use the default settings. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..b671a33264 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,49 @@ +# Contributor Covenant Code of Conduct + +*Note*: this Code of Conduct pertains to individuals' behavior. Please also see the [Organizational Code of Conduct][OCoC]. + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the Bytecode Alliance CoC team at [report@bytecodealliance.org](mailto:report@bytecodealliance.org). The CoC team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The CoC team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the Bytecode Alliance's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[OCoC]: https://github.com/bytecodealliance/wasmtime/blob/master/ORG_CODE_OF_CONDUCT.md +[homepage]: https://www.contributor-covenant.org +[version]: https://www.contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..ba67ac54da --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,13 @@ +# Contributing to Wasmtime + +Wasmtime is a [Bytecode Alliance] project, and follows the Bytecode Alliance's [Code of Conduct] and [Organizational Code of Conduct]. + +Wasmtime follows the same development style as Cranelift, so check out +[Cranelift's CONTRIBUTING.md]. Of course, for Wasmtime-specific issues, please +use the [Wasmtime issue tracker]. + +[Bytecode Alliance]: https://bytecodealliance.org/ +[Code of Conduct]: CODE_OF_CONDUCT.md +[Organizational Code of Conduct]: ORG_CODE_OF_CONDUCT.md +[Cranelift's CONTRIBUTING.md]: https://github.com/bytecodealliance/cranelift/blob/master/CONTRIBUTING.md +[Wasmtime issue tracker]: https://github.com/bytecodealliance/wasmtime/issues/new diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000000..1df0ef9d1b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2376 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "adler32" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2" + +[[package]] +name = "aho-corasick" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743ad5a418686aad3b87fd14c43badd828cf26e214a00f92a384291cf22e1811" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7825f6833612eb2414095684fcf6c635becf3ce97fe48cf6421321e93bfbd53c" + +[[package]] +name = "arbitrary" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cf76cb6e2222ed0ea86b2b0ee2f71c96ec6edd5af42e84d59160e91b836ec4" + +[[package]] +name = "arbitrary" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "491d5e42b1a073ff1fc1e0a02744b3f8bee9cf4bfd552053cac36c64b879795d" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" + +[[package]] +name = "backtrace" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4036b9bf40f3cf16aba72a3d65e8a520fc4bafcdc7079aea8f848c58c5b5536" +dependencies = [ + "backtrace-sys", + "cfg-if", + "libc", + "rustc-demangle", +] + +[[package]] +name = "backtrace-sys" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6575f128516de27e3ce99689419835fce9643a9b215a14d2b5b685be018491" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" + +[[package]] +name = "binaryen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a51ad23b3c7ab468d9daa948201921879ef0052e561c250fd0b326e6f000f2dd" +dependencies = [ + "binaryen-sys", +] + +[[package]] +name = "binaryen-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023e318da5cf481b0d243295d3ca764a047f645f1d46ef7bb67263473e8c48c5" +dependencies = [ + "bindgen", + "cc", + "cmake", + "heck", + "regex", +] + +[[package]] +name = "bincode" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5753e2a71534719bf3f4e57006c3a4f0d2c672a4b676eec84161f763eca87dbf" +dependencies = [ + "byteorder", + "serde", +] + +[[package]] +name = "bindgen" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c85344eb535a31b62f0af37be84441ba9e7f0f4111eb0530f43d15e513fe57" +dependencies = [ + "bitflags", + "cexpr", + "cfg-if", + "clang-sys", + "clap", + "env_logger 0.7.1", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "which", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "blake2b_simd" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding", + "byte-tools", + "byteorder", + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", +] + +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + +[[package]] +name = "byteorder" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" + +[[package]] +name = "c2-chacha" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb" +dependencies = [ + "ppv-lite86", +] + +[[package]] +name = "capstone" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031ba51c39151a1d6336ec859646153187204b0147c7b3f6fe2de636f1b8dbb3" +dependencies = [ + "capstone-sys", +] + +[[package]] +name = "capstone-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fae25eddcb80e24f98c35952c37a91ff7f8d0f60dbbdafb9763e8d5cc566b8d7" +dependencies = [ + "cc", +] + +[[package]] +name = "cc" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cexpr" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fce5b5fb86b0c57c20c834c1b412fd09c77c8a59b9473f86272709e78874cd1d" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "chrono" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31850b4a4d6bae316f7a09e691c944c28299298837edc0a03f755618c23cbc01" +dependencies = [ + "num-integer", + "num-traits", + "time", +] + +[[package]] +name = "clang-sys" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81de550971c976f176130da4b2978d3b524eaa0fd9ac31f3ceb5ae1231fb4853" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "2.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "cmake" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fb25b677f8bf1eb325017cb6bb8452f87969db0fedb4f757b297bee78a7c62" +dependencies = [ + "cc", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "cpu-time" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e393a7668fe1fad3075085b86c781883000b4ede868f43627b34a87c8b7ded" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "cranelift-bforest" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45a9c21f8042b9857bda93f6c1910b9f9f24100187a3d3d52f214a34e3dc5818" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-codegen" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7853f77a6e4a33c67a69c40f5e1bb982bd2dc5c4a22e17e67b65bbccf9b33b2e" +dependencies = [ + "byteorder", + "cranelift-bforest", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-entity", + "gimli", + "log", + "serde", + "smallvec", + "target-lexicon", + "thiserror", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084cd6d5fb0d1da28acd72c199471bfb09acc703ec8f3bf07b1699584272a3b9" +dependencies = [ + "cranelift-codegen-shared", + "cranelift-entity", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "701b599783305a58c25027a4d73f2d6b599b2d8ef3f26677275f480b4d51e05d" + +[[package]] +name = "cranelift-entity" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b88e792b28e1ebbc0187b72ba5ba880dad083abe9231a99d19604d10c9e73f38" +dependencies = [ + "serde", +] + +[[package]] +name = "cranelift-frontend" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "518344698fa6c976d853319218415fdfb4f1bc6b42d0b2e2df652e55dff1f778" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-native" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32daf082da21c0c05d93394ff4842c2ab7c4991b1f3186a1d952f8ac660edd0b" +dependencies = [ + "cranelift-codegen", + "raw-cpuid", + "target-lexicon", +] + +[[package]] +name = "cranelift-wasm" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2aa816f554a3ef739a5d17ca3081a1f8983f04c944ea8ff60fb8d9dd8cd2d7b" +dependencies = [ + "cranelift-codegen", + "cranelift-entity", + "cranelift-frontend", + "log", + "serde", + "thiserror", + "wasmparser 0.51.2", +] + +[[package]] +name = "crc32fast" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "lazy_static", + "maybe-uninit", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-queue" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c695eeca1e7173472a32221542ae469b3e9aac3a4fc81f7696bcad82029493db" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg", + "cfg-if", + "lazy_static", +] + +[[package]] +name = "ctor" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd8ce37ad4184ab2ce004c33bf6379185d3b1c95801cab51026bd271bf68eedc" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "cvt" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ac344c7efccb80cd25bc61b2170aec26f2f693fd40e765a539a1243db48c71" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "derive_arbitrary" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab2f0544254a47cabc58956cc7ebda74c3b796bb2761e3fe8f29fdde632ad95" +dependencies = [ + "proc-macro2", + "syn", + "synstructure", +] + +[[package]] +name = "diff" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "directories" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551a778172a450d7fc12e629ca3b0428d00f6afa9a43da1b630d54604e97371c" +dependencies = [ + "cfg-if", + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa0b23de8fd801745c471deffa6e12d248f962c9fd4b4c33787b055599bde7b" +dependencies = [ + "cfg-if", + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dynasm" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a814e1edeb85dd2a3c6fc0d6bf76d02ca5695d438c70ecee3d90774f3259c5" +dependencies = [ + "bitflags", + "byteorder", + "lazy_static", + "owning_ref", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dynasmrt" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a393aaeb4441a48bcf47b5b6155971f82cc1eb77e22855403ccc0415ac8328d" +dependencies = [ + "byteorder", + "memmap", +] + +[[package]] +name = "either" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" + +[[package]] +name = "env_logger" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aafcde04e90a5226a6443b7aabdb016ba2f8307c847d524724bd9b346dd1a2d3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "errno" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a071601ed01b988f896ab14b95e67335d1eeb50190932a1320f7fe3cadc84e" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14ca354e36190500e1e1fb267c647932382b54053c50b14970856c0b00a35067" +dependencies = [ + "gcc", + "libc", +] + +[[package]] +name = "faerie" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74b9ed6159e4a6212c61d9c6a86bee01876b192a64accecf58d5b5ae3b667b52" +dependencies = [ + "anyhow", + "goblin", + "indexmap", + "log", + "scroll", + "string-interner", + "target-lexicon", + "thiserror", +] + +[[package]] +name = "failure" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8273f13c977665c5db7eb2b99ae520952fe5ac831ae4cd09d80c4c7042b5ed9" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bc225b78e0391e4b8683440bf2e63c2deeeb2ce5189eab46e2b68c6d3725d08" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "file-per-thread-logger" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505b75b31ef7285168dd237c4a7db3c1f3e0927e7d314e670bc98e854272fe9" +dependencies = [ + "env_logger 0.6.2", + "log", +] + +[[package]] +name = "filecheck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded7985594ab426ef685362e5183168eb3b5aacc9f4e26819e8d82d224f33449" +dependencies = [ + "failure", + "failure_derive", + "regex", +] + +[[package]] +name = "filetime" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ff6d4dab0aa0c8e6346d46052e93b13a16cf847b54ed357087c35011048cc7d" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "winapi", +] + +[[package]] +name = "flate2" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd6d6f4752952feb71363cffc9ebac9411b75b87c6ab6058c40c8900cf43c0f" +dependencies = [ + "cfg-if", + "crc32fast", + "libc", + "miniz_oxide", +] + +[[package]] +name = "gcc" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" + +[[package]] +name = "generic-array" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" +dependencies = [ + "typenum", +] + +[[package]] +name = "getrandom" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "ghost" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a36606a68532b5640dc86bb1f33c64b45c4682aad4c50f3937b317ea387f3d6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "gimli" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dd6190aad0f05ddbbf3245c54ed14ca4aa6dd32f22312b70d8f168c3e3e633" +dependencies = [ + "arrayvec", + "byteorder", + "fallible-iterator", + "indexmap", + "smallvec", + "stable_deref_trait", +] + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "goblin" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3081214398d39e4bd7f2c1975f0488ed04614ffdd976c6fc7a0708278552c0da" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2c55f143919fbc0bc77e427fe2d74cf23786d7c1875666f2fde3ac3c659bb67" +dependencies = [ + "libc", +] + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + +[[package]] +name = "indexmap" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076f042c5b7b98f31d205f1249267e12a6518c1481e9dae9764af19b707d2292" +dependencies = [ + "autocfg", +] + +[[package]] +name = "indoc" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9553c1e16c114b8b77ebeb329e5f2876eed62a8d51178c8bc6bff0d65f98f8" +dependencies = [ + "indoc-impl", + "proc-macro-hack", +] + +[[package]] +name = "indoc-impl" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b714fc08d0961716390977cdff1536234415ac37b509e34e5a983def8340fb75" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", + "unindent", +] + +[[package]] +name = "inventory" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf98296081bd2cb540acc09ef9c97f22b7e487841520350293605db1b2c7a27" +dependencies = [ + "ctor", + "ghost", + "inventory-impl", +] + +[[package]] +name = "inventory-impl" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a8e30575afe28eea36a9a39136b70b2fb6b0dd0a212a5bd1f30a498395c0274" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "itertools" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" + +[[package]] +name = "jobserver" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2" +dependencies = [ + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b294d6fa9ee409a054354afc4352b0b9ef7ca222c69b8812cbea9e7d2bf3783f" + +[[package]] +name = "leb128" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3576a87f2ba00f6f106fdfcd16db1d698d648a26ad8e0573cad8537c3c362d2a" + +[[package]] +name = "libc" +version = "0.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018" + +[[package]] +name = "libfuzzer-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e969cd2be7a2aae0acbbe205da49134148db2287fb45a811bf441ed72f09a35" +dependencies = [ + "arbitrary 0.3.3", + "cc", +] + +[[package]] +name = "libloading" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b111a074963af1d37a139918ac6d49ad1d0d5e47f72fd55388619691a7d753" +dependencies = [ + "cc", + "winapi", +] + +[[package]] +name = "lightbeam" +version = "0.12.0" +dependencies = [ + "anyhow", + "capstone", + "cranelift-codegen", + "dynasm", + "dynasmrt", + "either", + "itertools", + "lazy_static", + "memoffset", + "more-asserts", + "multi_mut", + "quickcheck", + "smallvec", + "thiserror", + "typemap", + "wasmparser 0.51.2", + "wat", +] + +[[package]] +name = "log" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "mach" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86dd2487cdfea56def77b88438a2c915fb45113c5319bfe7e14306ca4cd0b0e1" +dependencies = [ + "libc", +] + +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + +[[package]] +name = "memchr" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" + +[[package]] +name = "memmap" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "memoffset" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75189eb85871ea5c2e2c15abbdd541185f63b408415e5051f5cac122d8c774b9" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "miniz_oxide" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa679ff6578b1cddee93d7e82e263b94a575e0bfced07284eb0c037c1d2416a5" +dependencies = [ + "adler32", +] + +[[package]] +name = "more-asserts" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0debeb9fcf88823ea64d64e4a815ab1643f33127d995978e099942ce38f25238" + +[[package]] +name = "multi_mut" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "816df386e5557ac1843a96f1ba8a7cbf4ab175d05ccc15c87a3cda27b4fbdece" + +[[package]] +name = "nom" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" +dependencies = [ + "memchr", + "version_check 0.1.5", +] + +[[package]] +name = "num" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" +dependencies = [ + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfb0800a0291891dd9f4fe7bd9c19384f98f7fbe0cd0f39a2c6b88b9868bbc00" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da4dc79f9e6c81bef96148c8f6b8e72ad4541caa4a24373e900a36da07de03a3" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46203554f085ff89c235cd12f7075f3233af9b11ed7c9e16dfe2560d03313ce6" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea44a4fd660ab0f38434934ca0212e90fbeaaee54126ef20a3451c30c95bafae" +dependencies = [ + "flate2", + "goblin", + "parity-wasm", + "scroll", + "target-lexicon", + "uuid", +] + +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + +[[package]] +name = "os_pipe" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4d06355a7090ce852965b2d08e11426c315438462638c6d721448d0b47aa22" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "owning_ref" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a4b8ea2179e6a2e27411d3bca09ca6dd630821cf6894c6c7c8467a8ee7ef13" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "parity-wasm" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc878dac00da22f8f61e7af3157988424567ab01d9920b962ef7dcbd7cd865" + +[[package]] +name = "paste" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "423a519e1c6e828f1e73b720f9d9ed2fa643dce8a7737fb43235ce0b41eeaa49" +dependencies = [ + "paste-impl", + "proc-macro-hack", +] + +[[package]] +name = "paste-impl" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4214c9e912ef61bf42b81ba9a47e8aad1b2ffaf739ab162bf96d1e011f54e6c5" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "ppv-lite86" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" + +[[package]] +name = "pretty_env_logger" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "717ee476b1690853d222af4634056d830b5197ffd747726a9a1eee6da9f49074" +dependencies = [ + "chrono", + "env_logger 0.6.2", + "log", +] + +[[package]] +name = "proc-macro-error" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052b3c9af39c7e5e94245f820530487d19eb285faedcb40e0c3275132293f242" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "proc-macro-error-attr" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d175bef481c7902e63e3165627123fff3502f06ac043d3ef42d08c1246da9253" +dependencies = [ + "proc-macro2", + "quote", + "rustversion", + "syn", + "syn-mid", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd45702f76d6d3c75a80564378ae228a85f0b59d2f3ed43c91b4a69eb2ebfc5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acb317c6ff86a4e579dfa00fc5e6cca91ecbb4e7eb2df0468805b674eb88548" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "pyo3" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1bfe257586436fbe1296d917f14a167d4253d0873bf43e2c9b9bdd58a3f9f35" +dependencies = [ + "indoc", + "inventory", + "lazy_static", + "libc", + "num-traits", + "paste", + "pyo3cls", + "regex", + "serde", + "serde_json", + "spin", + "unindent", + "version_check 0.9.1", +] + +[[package]] +name = "pyo3-derive-backend" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4882d8237fd8c7373cc25cb802fe0dab9ff70830fd56f47ef6c7f3f287fcc057" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pyo3cls" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf321cfab555f7411298733c86d21e5136f5ded13f5872fabf9de3337beecda" +dependencies = [ + "proc-macro2", + "pyo3-derive-backend", + "quote", + "syn", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quickcheck" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44883e74aa97ad63db83c4bf8ca490f02b2fc02f92575e720c8551e843c945f" +dependencies = [ + "env_logger 0.7.1", + "log", + "rand", + "rand_core", +] + +[[package]] +name = "quote" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand_chacha" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853" +dependencies = [ + "c2-chacha", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core", +] + +[[package]] +name = "raw-cpuid" +version = "7.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a349ca83373cfa5d6dbb66fd76e58b2cca08da71a5f6400de0a0a6a9bceeaf" +dependencies = [ + "bitflags", + "cc", + "rustc_version", +] + +[[package]] +name = "rayon" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6ce3297f9c85e16621bb8cca38a06779ffc31bb8184e1be4bed2be4678a098" +dependencies = [ + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a89b46efaf957e52b18062fb2f4660f8b8a4dde1807ca002690868ef2c85a9" +dependencies = [ + "crossbeam-deque", + "crossbeam-queue", + "crossbeam-utils", + "lazy_static", + "num_cpus", +] + +[[package]] +name = "redox_syscall" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" + +[[package]] +name = "redox_users" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b23093265f8d200fa7b4c2c76297f47e681c655f6f1285a8780d6a022f7431" +dependencies = [ + "getrandom", + "redox_syscall", + "rust-argon2", +] + +[[package]] +name = "regex" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322cf97724bea3ee221b78fe25ac9c46114ebb51747ad5babd51a2fc6a8235a8" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "regex-syntax" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b28dfe3fe9badec5dbf0a79a9cccad2cfc2ab5484bdb3e44cbd1ae8b3ba2be06" + +[[package]] +name = "region" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "448e868c6e4cfddfa49b6a72c95906c04e8547465e9536575b95c70a4044f856" +dependencies = [ + "bitflags", + "libc", + "mach", + "winapi", +] + +[[package]] +name = "remove_dir_all" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" +dependencies = [ + "winapi", +] + +[[package]] +name = "rust-argon2" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017" +dependencies = [ + "base64", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bba175698996010c4f6dce5e7f173b6eb781fce25d2cfc45e27091ce0b79f6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ryu" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "scroll" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb2332cb595d33f7edd5700f4cbf94892e680c7f0ae56adab58a35190b66cb1" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8584eea9b9ff42825b46faf46a8c24d2cff13ec152fa2a50df788b87c07ee28" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9371ade75d4c2d6cb154141b9752cf3781ec9c05e0e5cf35060e1e70ee7b9c25" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27044adfd2e1f077f649f59deb9490d3941d674002f7d062870a60ebe9bd47a0" +dependencies = [ + "block-buffer", + "digest", + "fake-simd", + "opaque-debug", +] + +[[package]] +name = "shlex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" + +[[package]] +name = "smallvec" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2fb2ec9bcd216a5b0d0ccf31ab17b5ed1d627960edff65bbe95d3ce221cefc" + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "stable_deref_trait" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" + +[[package]] +name = "string-interner" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd710eadff449a1531351b0e43eb81ea404336fa2f56c777427ab0e32a4cf183" +dependencies = [ + "serde", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "structopt" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1bcbed7d48956fcbb5d80c6b95aedb553513de0a1b451ea92679d999c010e98" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "095064aa1f5b94d14e635d0a5684cf140c43ae40a0fd990708d38f5d669e5f64" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0294dc449adc58bb6592fff1a23d3e5e6e235afc6a0ffca2657d19e7bbffe5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "syn-mid" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "synstructure" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "target-lexicon" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab0e7238dcc7b40a7be719a25365910f6807bd864f4cce6b2e6b873658e2b19d" + +[[package]] +name = "tempfile" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +dependencies = [ + "cfg-if", + "libc", + "rand", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "test-programs" +version = "0.12.0" +dependencies = [ + "anyhow", + "cfg-if", + "os_pipe", + "pretty_env_logger", + "target-lexicon", + "tempfile", + "wasi-common", + "wasmtime", + "wasmtime-wasi", + "wat", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee14bf8e6767ab4c687c9e8bc003879e042a96fd67a3ba5934eadb6536bef4db" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7b51e1fbc44b5a0840be594fbc0f960be09050f2617e61e6aa43bef97cd3ef4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "time" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" +dependencies = [ + "libc", + "redox_syscall", + "winapi", +] + +[[package]] +name = "toml" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a" +dependencies = [ + "serde", +] + +[[package]] +name = "traitobject" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" + +[[package]] +name = "typemap" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653be63c80a3296da5551e1bfd2cca35227e13cdd08c6668903ae2f4f77aa1f6" +dependencies = [ + "unsafe-any", +] + +[[package]] +name = "typenum" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2783fe2d6b8c1101136184eb41be8b1ad379e4657050b8aaff0c79ee7575f9" + +[[package]] +name = "unicode-segmentation" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" + +[[package]] +name = "unicode-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" + +[[package]] +name = "unicode-xid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" + +[[package]] +name = "unindent" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63f18aa3b0e35fed5a0048f029558b1518095ffe2a0a31fb87c93dece93a4993" + +[[package]] +name = "unsafe-any" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30360d7979f5e9c6e6cea48af192ea8fab4afb3cf72597154b8f08935bc9c7f" +dependencies = [ + "traitobject", +] + +[[package]] +name = "uuid" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11" + +[[package]] +name = "vec_map" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" + +[[package]] +name = "version_check" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" + +[[package]] +name = "version_check" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" + +[[package]] +name = "walrus" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84651d7b04ebbecf715a28859d1dc5053b98c6a5a20bd7343d8f015947b86481" +dependencies = [ + "anyhow", + "id-arena", + "leb128", + "log", + "walrus-macro", + "wasmparser 0.48.2", +] + +[[package]] +name = "walrus-macro" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b1845ca33b1fcec5624f0e5ea617e41a8adea37f71eb117d3ff81a089d5e3f9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi-common" +version = "0.12.0" +dependencies = [ + "anyhow", + "cfg-if", + "cpu-time", + "filetime", + "getrandom", + "lazy_static", + "libc", + "log", + "num", + "thiserror", + "wig", + "winapi", + "winx", + "yanix", +] + +[[package]] +name = "wasm-webidl-bindings" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42298d64a28700ecd5aa47d8037bcf832b7be0c88ad6b272ebb5d92d7e052a8c" +dependencies = [ + "anyhow", + "id-arena", + "leb128", + "walrus", +] + +[[package]] +name = "wasmparser" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "073da89bf1c84db000dd68ce660c1b4a08e3a2d28fd1e3394ab9e7abdde4a0f8" + +[[package]] +name = "wasmparser" +version = "0.51.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a40d24f114a3f24b459ec292019220cff6388673b4a2c0a11483665b599ef15c" + +[[package]] +name = "wasmprinter" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bd423d45b95fcee11775472bfdce66c63c45ada23c1b338e0a63d623a6c475b" +dependencies = [ + "anyhow", + "wasmparser 0.51.2", +] + +[[package]] +name = "wasmtime" +version = "0.12.0" +dependencies = [ + "anyhow", + "backtrace", + "cfg-if", + "file-per-thread-logger", + "lazy_static", + "libc", + "pretty_env_logger", + "rayon", + "region", + "rustc-demangle", + "target-lexicon", + "tempfile", + "wasi-common", + "wasmparser 0.51.2", + "wasmtime-environ", + "wasmtime-jit", + "wasmtime-profiling", + "wasmtime-runtime", + "wat", + "winapi", +] + +[[package]] +name = "wasmtime-c-api" +version = "0.12.0" +dependencies = [ + "wasi-common", + "wasmtime", + "wasmtime-wasi", +] + +[[package]] +name = "wasmtime-cli" +version = "0.12.0" +dependencies = [ + "anyhow", + "faerie", + "file-per-thread-logger", + "filecheck", + "libc", + "more-asserts", + "pretty_env_logger", + "rayon", + "structopt", + "target-lexicon", + "tempfile", + "test-programs", + "wasi-common", + "wasm-webidl-bindings", + "wasmtime", + "wasmtime-debug", + "wasmtime-environ", + "wasmtime-interface-types", + "wasmtime-jit", + "wasmtime-obj", + "wasmtime-profiling", + "wasmtime-runtime", + "wasmtime-wasi", + "wasmtime-wast", + "wat", +] + +[[package]] +name = "wasmtime-debug" +version = "0.12.0" +dependencies = [ + "anyhow", + "faerie", + "gimli", + "more-asserts", + "target-lexicon", + "thiserror", + "wasmparser 0.51.2", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-environ" +version = "0.12.0" +dependencies = [ + "anyhow", + "base64", + "bincode", + "cranelift-codegen", + "cranelift-entity", + "cranelift-wasm", + "directories", + "errno", + "file-per-thread-logger", + "filetime", + "indexmap", + "lazy_static", + "libc", + "lightbeam", + "log", + "more-asserts", + "pretty_env_logger", + "rand", + "rayon", + "serde", + "sha2", + "target-lexicon", + "tempfile", + "thiserror", + "toml", + "wasmparser 0.51.2", + "winapi", + "zstd", +] + +[[package]] +name = "wasmtime-fuzz" +version = "0.12.0" +dependencies = [ + "arbitrary 0.2.0", + "libfuzzer-sys", + "wasmtime", + "wasmtime-fuzzing", +] + +[[package]] +name = "wasmtime-fuzzing" +version = "0.12.0" +dependencies = [ + "anyhow", + "arbitrary 0.3.3", + "binaryen", + "env_logger 0.7.1", + "log", + "wasmparser 0.51.2", + "wasmprinter", + "wasmtime", + "wat", +] + +[[package]] +name = "wasmtime-interface-types" +version = "0.12.0" +dependencies = [ + "anyhow", + "walrus", + "wasm-webidl-bindings", + "wasmparser 0.51.2", + "wasmtime", + "wasmtime-environ", + "wasmtime-jit", + "wasmtime-runtime", + "wasmtime-wasi", +] + +[[package]] +name = "wasmtime-jit" +version = "0.12.0" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "cranelift-wasm", + "more-asserts", + "region", + "target-lexicon", + "thiserror", + "wasmparser 0.51.2", + "wasmtime-debug", + "wasmtime-environ", + "wasmtime-profiling", + "wasmtime-runtime", + "winapi", +] + +[[package]] +name = "wasmtime-obj" +version = "0.12.0" +dependencies = [ + "anyhow", + "faerie", + "more-asserts", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-profiling" +version = "0.12.0" +dependencies = [ + "gimli", + "goblin", + "lazy_static", + "libc", + "object", + "scroll", + "serde", + "target-lexicon", +] + +[[package]] +name = "wasmtime-py" +version = "0.12.0" +dependencies = [ + "anyhow", + "pyo3", + "region", + "target-lexicon", + "wasmparser 0.51.2", + "wasmtime", + "wasmtime-interface-types", + "wasmtime-wasi", +] + +[[package]] +name = "wasmtime-runtime" +version = "0.12.0" +dependencies = [ + "backtrace", + "cc", + "cfg-if", + "indexmap", + "libc", + "memoffset", + "more-asserts", + "region", + "thiserror", + "wasmtime-environ", + "wasmtime-profiling", + "winapi", +] + +[[package]] +name = "wasmtime-rust" +version = "0.12.0" +dependencies = [ + "anyhow", + "wasmtime", + "wasmtime-interface-types", + "wasmtime-rust-macro", + "wasmtime-wasi", +] + +[[package]] +name = "wasmtime-rust-macro" +version = "0.12.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasmtime-wasi" +version = "0.12.0" +dependencies = [ + "anyhow", + "log", + "wasi-common", + "wasmtime", + "wasmtime-runtime", + "wig", +] + +[[package]] +name = "wasmtime-wast" +version = "0.12.0" +dependencies = [ + "anyhow", + "wasmtime", + "wast 9.0.0", +] + +[[package]] +name = "wast" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233648f540f07fce9b972436f2fbcae8a750c1121b6d32d949e1a44b4d9fc7b1" +dependencies = [ + "leb128", +] + +[[package]] +name = "wast" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee7b16105405ca2aa2376ba522d8d4b1a11604941dd3bb7df9fd2ece60f8d16a" +dependencies = [ + "leb128", +] + +[[package]] +name = "wat" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56173f7f4fb59aebe35a7e71423845e1c6c7144bfb56362d497931b6b3bed0f6" +dependencies = [ + "wast 9.0.0", +] + +[[package]] +name = "which" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5475d47078209a02e60614f7ba5e645ef3ed60f771920ac1906d7c1cc65024c8" +dependencies = [ + "libc", +] + +[[package]] +name = "wig" +version = "0.12.0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "witx", +] + +[[package]] +name = "winapi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ccfbf554c6ad11084fb7517daca16cfdcaccbdadba4fc336f032a8b12c2ad80" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winx" +version = "0.12.0" +dependencies = [ + "bitflags", + "cvt", + "winapi", +] + +[[package]] +name = "witx" +version = "0.8.0" +dependencies = [ + "anyhow", + "diff", + "log", + "pretty_env_logger", + "structopt", + "thiserror", + "wast 3.0.4", +] + +[[package]] +name = "yanix" +version = "0.12.0" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "log", + "thiserror", +] + +[[package]] +name = "zstd" +version = "0.5.1+zstd.1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5d978b793ae64375b80baf652919b148f6a496ac8802922d9999f5a553194f" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "2.0.3+zstd.1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee25eac9753cfedd48133fa1736cbd23b774e253d89badbeac7d12b23848d3f" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "1.4.15+zstd.1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89719b034dc22d240d5b407fb0a3fe6d29952c181cff9a9f95c0bd40b4f8f7d8" +dependencies = [ + "cc", + "glob", + "libc", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000000..6aa979ad64 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,83 @@ +[package] +name = "wasmtime-cli" +version = "0.12.0" +authors = ["The Wasmtime Project Developers"] +description = "Command-line interface for Wasmtime" +license = "Apache-2.0 WITH LLVM-exception" +documentation = "https://cranelift.readthedocs.io/" +categories = ["wasm"] +keywords = ["webassembly", "wasm"] +repository = "https://github.com/bytecodealliance/wasmtime" +readme = "README.md" +edition = "2018" +default-run = "wasmtime" +publish = false + +[lib] +doctest = false + +[[bin]] +name = "wasmtime" +path = "src/bin/wasmtime.rs" +doc = false + +[dependencies] +# Enable all supported architectures by default. +wasmtime = { path = "crates/api" } +wasmtime-debug = { path = "crates/debug" } +wasmtime-environ = { path = "crates/environ" } +wasmtime-interface-types = { path = "crates/interface-types" } +wasmtime-jit = { path = "crates/jit" } +wasmtime-obj = { path = "crates/obj" } +wasmtime-profiling = { path = "crates/profiling" } +wasmtime-wast = { path = "crates/wast" } +wasmtime-wasi = { path = "crates/wasi" } +wasi-common = { path = "crates/wasi-common" } +structopt = { version = "0.3.5", features = ["color", "suggestions"] } +faerie = "0.14.0" +anyhow = "1.0.19" +target-lexicon = { version = "0.10.0", default-features = false } +pretty_env_logger = "0.3.0" +file-per-thread-logger = "0.1.1" +wat = "1.0.10" +libc = "0.2.60" +rayon = "1.2.1" +wasm-webidl-bindings = "0.8" + +[dev-dependencies] +wasmtime-runtime = { path = "crates/runtime" } +more-asserts = "0.2.1" +# This feature requires the wasm32-wasi target be installed. It enables +# wasm32-wasi integration tests. To enable, run +# `cargo test --features test-programs`. +test-programs = { path = "crates/test-programs" } +tempfile = "3.1.0" +filecheck = "0.4.0" + +[build-dependencies] +anyhow = "1.0.19" + +[profile.release.build-override] +opt-level = 0 + +[workspace] +members = [ + "crates/fuzzing", + "crates/misc/rust", + "crates/misc/py", + "crates/c-api", + "fuzz", +] + +[features] +lightbeam = [ + "wasmtime-environ/lightbeam", + "wasmtime-jit/lightbeam", + "wasmtime-wast/lightbeam", + "wasmtime/lightbeam", +] +jitdump = ["wasmtime-profiling/jitdump"] +test_programs = ["test-programs/test_programs"] + +[badges] +maintenance = { status = "actively-developed" } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..f9d81955f4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,220 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + diff --git a/ORG_CODE_OF_CONDUCT.md b/ORG_CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..6f4fb3f537 --- /dev/null +++ b/ORG_CODE_OF_CONDUCT.md @@ -0,0 +1,143 @@ +# Bytecode Alliance Organizational Code of Conduct (OCoC) + +*Note*: this Code of Conduct pertains to organizations' behavior. Please also see the [Individual Code of Conduct](CODE_OF_CONDUCT.md). + +## Preamble + +The Bytecode Alliance (BA) welcomes involvement from organizations, +including commercial organizations. This document is an +*organizational* code of conduct, intended particularly to provide +guidance to commercial organizations. It is distinct from the +[Individual Code of Conduct (ICoC)](CODE_OF_CONDUCT.md), and does not +replace the ICoC. This OCoC applies to any group of people acting in +concert as a BA member or as a participant in BA activities, whether +or not that group is formally incorporated in some jurisdiction. + +The code of conduct described below is not a set of rigid rules, and +we did not write it to encompass every conceivable scenario that might +arise. For example, it is theoretically possible there would be times +when asserting patents is in the best interest of the BA community as +a whole. In such instances, consult with the BA, strive for +consensus, and interpret these rules with an intent that is generous +to the community the BA serves. + +While we may revise these guidelines from time to time based on +real-world experience, overall they are based on a simple principle: + +*Bytecode Alliance members should observe the distinction between + public community functions and private functions — especially + commercial ones — and should ensure that the latter support, or at + least do not harm, the former.* + +## Guidelines + + * **Do not cause confusion about Wasm standards or interoperability.** + + Having an interoperable WebAssembly core is a high priority for + the BA, and members should strive to preserve that core. It is fine + to develop additional non-standard features or APIs, but they + should always be clearly distinguished from the core interoperable + Wasm. + + Treat the WebAssembly name and any BA-associated names with + respect, and follow BA trademark and branding guidelines. If you + distribute a customized version of software originally produced by + the BA, or if you build a product or service using BA-derived + software, use names that clearly distinguish your work from the + original. (You should still provide proper attribution to the + original, of course, wherever such attribution would normally be + given.) + + Further, do not use the WebAssembly name or BA-associated names in + other public namespaces in ways that could cause confusion, e.g., + in company names, names of commercial service offerings, domain + names, publicly-visible social media accounts or online service + accounts, etc. It may sometimes be reasonable, however, to + register such a name in a new namespace and then immediately donate + control of that account to the BA, because that would help the project + maintain its identity. + + For further guidance, see the BA Trademark and Branding Policy + [TODO: create policy, then insert link]. + + * **Do not restrict contributors.** If your company requires + employees or contractors to sign non-compete agreements, those + agreements must not prevent people from participating in the BA or + contributing to related projects. + + This does not mean that all non-compete agreements are incompatible + with this code of conduct. For example, a company may restrict an + employee's ability to solicit the company's customers. However, an + agreement must not block any form of technical or social + participation in BA activities, including but not limited to the + implementation of particular features. + + The accumulation of experience and expertise in individual persons, + who are ultimately free to direct their energy and attention as + they decide, is one of the most important drivers of progress in + open source projects. A company that limits this freedom may hinder + the success of the BA's efforts. + + * **Do not use patents as offensive weapons.** If any BA participant + prevents the adoption or development of BA technologies by + asserting its patents, that undermines the purpose of the + coalition. The collaboration fostered by the BA cannot include + members who act to undermine its work. + + * **Practice responsible disclosure** for security vulnerabilities. + Use designated, non-public reporting channels to disclose technical + vulnerabilities, and give the project a reasonable period to + respond, remediate, and patch. [TODO: optionally include the + security vulnerability reporting URL here.] + + Vulnerability reporters may patch their company's own offerings, as + long as that patching does not significantly delay the reporting of + the vulnerability. Vulnerability information should never be used + for unilateral commercial advantage. Vendors may legitimately + compete on the speed and reliability with which they deploy + security fixes, but withholding vulnerability information damages + everyone in the long run by risking harm to the BA project's + reputation and to the security of all users. + + * **Respect the letter and spirit of open source practice.** While + there is not space to list here all possible aspects of standard + open source practice, some examples will help show what we mean: + + * Abide by all applicable open source license terms. Do not engage + in copyright violation or misattribution of any kind. + + * Do not claim others' ideas or designs as your own. + + * When others engage in publicly visible work (e.g., an upcoming + demo that is coordinated in a public issue tracker), do not + unilaterally announce early releases or early demonstrations of + that work ahead of their schedule in order to secure private + advantage (such as marketplace advantage) for yourself. + + The BA reserves the right to determine what constitutes good open + source practices and to take action as it deems appropriate to + encourage, and if necessary enforce, such practices. + +## Enforcement + +Instances of organizational behavior in violation of the OCoC may +be reported by contacting the Bytecode Alliance CoC team at +[report@bytecodealliance.org](mailto:report@bytecodealliance.org). The +CoC team will review and investigate all complaints, and will respond +in a way that it deems appropriate to the circumstances. The CoC team +is obligated to maintain confidentiality with regard to the reporter of +an incident. Further details of specific enforcement policies may be +posted separately. + +When the BA deems an organization in violation of this OCoC, the BA +will, at its sole discretion, determine what action to take. The BA +will decide what type, degree, and duration of corrective action is +needed, if any, before a violating organization can be considered for +membership (if it was not already a member) or can have its membership +reinstated (if it was a member and the BA canceled its membership due +to the violation). + +In practice, the BA's first approach will be to start a conversation, +with punitive enforcement used only as a last resort. Violations +often turn out to be unintentional and swiftly correctable with all +parties acting in good faith. diff --git a/README.md b/README.md new file mode 100644 index 0000000000..afac75192a --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +
+

wasmtime

+ +

+ A standalone runtime for + WebAssembly +

+ + A Bytecode Alliance project + +

+ build status + zulip chat + min rustc +

+ +

+ Guide + | + Contributing + | + Website + | + Chat +

+
+ +## Installation + +The Wasmtime CLI can be installed on Linux and macOS with a small install +script: + +```sh +$ curl https://wasmtime.dev/install.sh -sSf | bash +``` + +Windows or otherwise interested users can download installers and +binaries directly from the [GitHub +Releases](https://github.com/bytecodealliance/wasmtime/releases) page. + +## Example + +If you've got the [Rust compiler +installed](https://www.rust-lang.org/tools/install) then you can take some Rust +source code: + +```rust +fn main() { + println!("Hello, world!"); +} +``` + +and compile/run it with: + +```sh +$ rustup target add wasm32-wasi +$ rustc hello.rs --target wasm32-wasi +$ wasmtime hello.wasm +Hello, world! +``` + +## Features + +* **Lightweight**. Wasmtime is a standalone runtime for WebAssembly that scales + with your needs. It fits on tiny chips as well as makes use of huge servers. + Wasmtime can be embedded into almost any application too. + +* **Fast**. Wasmtime is built on the optimizing Cranelift code generator to + quickly generate high-quality machine code at runtime. + +* **Configurable**. Whether you need to precompile your wasm ahead of time, + generate code blazingly fast with Lightbeam, or interpret it at runtime, + Wasmtime has you covered for all your wasm-executing needs. + +* **WASI**. Wasmtime supports a rich set of APIs for interacting with the host + environment through the [WASI standard](https://wasi.dev). + +* **Standards Compliant**. Wasmtime passes the [official WebAssembly test + suite](https://github.com/WebAssembly/testsuite), implements the [official C + API of wasm](https://github.com/WebAssembly/wasm-c-api), and implements + [future proposals to WebAssembly](https://github.com/WebAssembly/proposals) as + well. Wasmtime developers are intimately engaged with the WebAssembly + standards process all along the way too. + +## Documentation + +[📚 Read the Wasmtime guide here! 📚][guide] + +The [wasmtime guide][guide] is the best starting point to learn about what +Wasmtime can do for you or help answer your questions about Wasmtime. If you're +curious in contributing to Wasmtime, [it can also help you do +that][contributing]!. + +[contributing]: https://bytecodealliance.github.io/wasmtime/contributing.html +[guide]: https://bytecodealliance.github.io/wasmtime + +--- + +It's Wasmtime. diff --git a/RELEASES.md b/RELEASES.md new file mode 100644 index 0000000000..202f673e51 --- /dev/null +++ b/RELEASES.md @@ -0,0 +1,50 @@ +# Wasmtime Releases + +-------------------------------------------------------------------------------- + +## 0.12.0 + +Released 2020-02-26. + +### Added + +* Support for the [WebAssembly text annotations proposal][annotations-proposal] + has been added. + [#998](https://github.com/bytecodealliance/wasmtime/pull/998) + +* An initial C API for instantiating WASI modules has been added. + [#977](https://github.com/bytecodealliance/wasmtime/pull/977) + +* A new suite of `Func::getN` functions have been added to the `wasmtime` API to + call statically-known function signatures in a highly optimized fashion. + [#955](https://github.com/bytecodealliance/wasmtime/pull/955) + +* Initial support for profiling JIT code through perf jitdump has been added. + [#360](https://github.com/bytecodealliance/wasmtime/pull/360) + +* More CLI flags corresponding to proposed WebAssembly features have been added. + [#917](https://github.com/bytecodealliance/wasmtime/pull/917) + +[annotations-proposal]: https://github.com/webassembly/annotations + +### Changed + +* The `wasmtime` CLI as well as embedding API will optimize WebAssembly code by + default now. + [#973](https://github.com/bytecodealliance/wasmtime/pull/973) + [#988](https://github.com/bytecodealliance/wasmtime/pull/988) + +* The `verifier` pass in Cranelift is now no longer run by default when using + the embedding API. + [#882](https://github.com/bytecodealliance/wasmtime/pull/882) + +### Fixed + +* Code caching now accurately accounts for optimization levels, ensuring that if + you ask for optimized code you're not accidentally handed unoptimized code + from the cache. + [#974](https://github.com/bytecodealliance/wasmtime/pull/974) + +* Automated releases for tags should be up and running again, along with + automatic publication of the `wasmtime` Python package. + [#971](https://github.com/bytecodealliance/wasmtime/pull/971) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..3513b9cb35 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,29 @@ +# Security Policy + +Building secure foundations for software development is at the core of what we do in the Bytecode Alliance. Contributions of external security researchers are a vital part of that. + +## Scope + +If you believe you've found a security issue in any website, service, or software owned or operated by the Bytecode Alliance, we encourage you to notify us. + +## How to Submit a Report + +To submit a vulnerability report to the Bytecode Alliance, please contact us at [security@bytecodealliance.org](mailto:security@bytecodealliance.org). Your submission will be reviewed and validated by a member of our security team. + +## Safe Harbor + +The Bytecode Alliance supports safe harbor for security researchers who: + +* Make a good faith effort to avoid privacy violations, destruction of data, and interruption or degradation of our services. +* Only interact with accounts you own or with explicit permission of the account holder. If you do encounter Personally Identifiable Information (PII) contact us immediately, do not proceed with access, and immediately purge any local information. +* Provide us with a reasonable amount of time to resolve vulnerabilities prior to any disclosure to the public or a third-party. + +We will consider activities conducted consistent with this policy to constitute "authorized" conduct and will not pursue civil action or initiate a complaint to law enforcement. We will help to the extent we can if legal action is initiated by a third party against you. + +Please submit a report to us before engaging in conduct that may be inconsistent with or unaddressed by this policy. + +## Preferences + +* Please provide detailed reports with reproducible steps and a clearly defined impact. +* Submit one vulnerability per report. +* Social engineering (e.g. phishing, vishing, smishing) is prohibited. diff --git a/build.rs b/build.rs new file mode 100644 index 0000000000..f8a847abe2 --- /dev/null +++ b/build.rs @@ -0,0 +1,211 @@ +//! Build program to generate a program which runs all the testsuites. +//! +//! By generating a separate `#[test]` test for each file, we allow cargo test +//! to automatically run the files in parallel. + +use anyhow::Context; +use std::env; +use std::fmt::Write; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn main() -> anyhow::Result<()> { + println!("cargo:rerun-if-changed=build.rs"); + let out_dir = PathBuf::from( + env::var_os("OUT_DIR").expect("The OUT_DIR environment variable must be set"), + ); + let mut out = String::new(); + + for strategy in &[ + "Cranelift", + #[cfg(feature = "lightbeam")] + "Lightbeam", + ] { + writeln!(out, "#[cfg(test)]")?; + writeln!(out, "#[allow(non_snake_case)]")?; + writeln!(out, "mod {} {{", strategy)?; + + with_test_module(&mut out, "misc", |out| { + test_directory(out, "tests/misc_testsuite", strategy)?; + test_directory_module(out, "tests/misc_testsuite/bulk-memory-operations", strategy)?; + test_directory_module(out, "tests/misc_testsuite/reference-types", strategy)?; + Ok(()) + })?; + + with_test_module(&mut out, "spec", |out| { + let spec_tests = test_directory(out, "tests/spec_testsuite", strategy)?; + // Skip running spec_testsuite tests if the submodule isn't checked + // out. + if spec_tests > 0 { + test_directory_module(out, "tests/spec_testsuite/proposals/simd", strategy)?; + test_directory_module(out, "tests/spec_testsuite/proposals/multi-value", strategy)?; + test_directory_module( + out, + "tests/spec_testsuite/proposals/reference-types", + strategy, + )?; + test_directory_module( + out, + "tests/spec_testsuite/proposals/bulk-memory-operations", + strategy, + )?; + } else { + println!( + "cargo:warning=The spec testsuite is disabled. To enable, run `git submodule \ + update --remote`." + ); + } + Ok(()) + })?; + + writeln!(out, "}}")?; + } + + // Write out our auto-generated tests and opportunistically format them with + // `rustfmt` if it's installed. + let output = out_dir.join("wast_testsuite_tests.rs"); + fs::write(&output, out)?; + drop(Command::new("rustfmt").arg(&output).status()); + Ok(()) +} + +fn test_directory_module( + out: &mut String, + path: impl AsRef, + strategy: &str, +) -> anyhow::Result { + let path = path.as_ref(); + let testsuite = &extract_name(path); + with_test_module(out, testsuite, |out| test_directory(out, path, strategy)) +} + +fn test_directory( + out: &mut String, + path: impl AsRef, + strategy: &str, +) -> anyhow::Result { + let path = path.as_ref(); + let mut dir_entries: Vec<_> = path + .read_dir() + .context(format!("failed to read {:?}", path))? + .map(|r| r.expect("reading testsuite directory entry")) + .filter_map(|dir_entry| { + let p = dir_entry.path(); + let ext = p.extension()?; + // Only look at wast files. + if ext != "wast" { + return None; + } + // Ignore files starting with `.`, which could be editor temporary files + if p.file_stem()?.to_str()?.starts_with(".") { + return None; + } + Some(p) + }) + .collect(); + + dir_entries.sort(); + + let testsuite = &extract_name(path); + for entry in dir_entries.iter() { + write_testsuite_tests(out, entry, testsuite, strategy)?; + } + + Ok(dir_entries.len()) +} + +/// Extract a valid Rust identifier from the stem of a path. +fn extract_name(path: impl AsRef) -> String { + path.as_ref() + .file_stem() + .expect("filename should have a stem") + .to_str() + .expect("filename should be representable as a string") + .replace("-", "_") + .replace("/", "_") +} + +fn with_test_module( + out: &mut String, + testsuite: &str, + f: impl FnOnce(&mut String) -> anyhow::Result, +) -> anyhow::Result { + out.push_str("mod "); + out.push_str(testsuite); + out.push_str(" {\n"); + + let result = f(out)?; + + out.push_str("}\n"); + Ok(result) +} + +fn write_testsuite_tests( + out: &mut String, + path: impl AsRef, + testsuite: &str, + strategy: &str, +) -> anyhow::Result<()> { + let path = path.as_ref(); + let testname = extract_name(path); + + writeln!(out, "#[test]")?; + if ignore(testsuite, &testname, strategy) { + writeln!(out, "#[ignore]")?; + } + writeln!(out, "fn r#{}() -> anyhow::Result<()> {{", &testname)?; + writeln!( + out, + "crate::run_wast(r#\"{}\"#, crate::Strategy::{})", + path.display(), + strategy + )?; + writeln!(out, "}}")?; + writeln!(out)?; + Ok(()) +} + +/// Ignore tests that aren't supported yet. +fn ignore(testsuite: &str, testname: &str, strategy: &str) -> bool { + match strategy { + #[cfg(feature = "lightbeam")] + "Lightbeam" => match (testsuite, testname) { + ("simd", _) => return true, + ("multi_value", _) => return true, + ("reference_types", _) => return true, + ("bulk_memory_operations", _) => return true, + // Lightbeam doesn't support float arguments on the stack. + ("spec_testsuite", "call") => return true, + _ => (), + }, + "Cranelift" => match (testsuite, testname) { + ("simd", "simd_bit_shift") => return true, // FIXME Unsupported feature: proposed SIMD operator I8x16Shl + ("simd", "simd_conversions") => return true, // FIXME Unsupported feature: proposed SIMD operator I16x8NarrowI32x4S + ("simd", "simd_f32x4") => return true, // FIXME expected V128(F32x4([CanonicalNan, CanonicalNan, Value(Float32 { bits: 0 }), Value(Float32 { bits: 0 })])), got V128(18428729675200069632) + ("simd", "simd_f64x2") => return true, // FIXME expected V128(F64x2([Value(Float64 { bits: 9221120237041090560 }), Value(Float64 { bits: 0 })])), got V128(0) + ("simd", "simd_f64x2_arith") => return true, // FIXME expected V128(F64x2([Value(Float64 { bits: 9221120237041090560 }), Value(Float64 { bits: 13835058055282163712 })])), got V128(255211775190703847615975447847722024960) + ("simd", "simd_i64x2_arith") => return true, // FIXME Unsupported feature: proposed SIMD operator I64x2Mul + ("simd", "simd_lane") => return true, // FIXME invalid u8 number: constant out of range: (v8x16.shuffle -1 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14... + ("simd", "simd_load") => return true, // FIXME Unsupported feature: proposed SIMD operator I8x16Shl + ("simd", "simd_load_extend") => return true, // FIXME Unsupported feature: proposed SIMD operator I16x8Load8x8S { memarg: MemoryImmediate { flags: 0, offset: 0 } } + ("simd", "simd_load_splat") => return true, // FIXME Unsupported feature: proposed SIMD operator V8x16LoadSplat { memarg: MemoryImmediate { flags: 0, offset: 0 } } + ("simd", "simd_splat") => return true, // FIXME Unsupported feature: proposed SIMD operator I8x16ShrS + + // Still working on implementing these. See #929. + ("reference_types", "table_copy_on_imported_tables") => return false, + ("reference_types", _) => return true, + + // Still working on implementing these. See #928 + ("bulk_memory_operations", "bulk") + | ("bulk_memory_operations", "data") + | ("bulk_memory_operations", "memory_init") + | ("bulk_memory_operations", "imports") => return true, + + _ => {} + }, + _ => panic!("unrecognized strategy"), + } + + false +} diff --git a/ci/build-tarballs.sh b/ci/build-tarballs.sh new file mode 100755 index 0000000000..433450116b --- /dev/null +++ b/ci/build-tarballs.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +# A small shell script invoked from CI on the final Linux builder which actually +# assembles the release artifacts for a particular platform. This will take the +# binary artifacts of previous builders and create associated tarballs to +# publish to GitHub. +# +# The first argument of this is the "platform" name to put into the tarball, and +# the second argument is the name of the github actions platform which is where +# we source binaries from. The final third argument is ".exe" on Windows to +# handle executable extensions right. + +set -ex + +platform=$1 +src=$2 +exe=$3 + +rm -rf tmp +mkdir tmp +mkdir -p dist + +mktarball() { + dir=$1 + if [ "$exe" = "" ]; then + tar cJf dist/$dir.tar.xz -C tmp $dir + else + (cd tmp && zip -r ../dist/$dir.zip $dir) + fi +} + +# Create the main tarball of binaries +bin_pkgname=wasmtime-$TAG-$platform +mkdir tmp/$bin_pkgname +cp LICENSE README.md tmp/$bin_pkgname +mv bins-$src/{wasmtime,wasm2obj}$exe tmp/$bin_pkgname +chmod +x tmp/$bin_pkgname/{wasmtime,wasm2obj}$exe +mktarball $bin_pkgname + +if [ "$exe" = ".exe" ]; then + mv bins-$src/installer.msi dist/$bin_pkgname.msi +fi + +# Create tarball of API libraries +api_pkgname=wasmtime-$TAG-$platform-c-api +mkdir tmp/$api_pkgname +mkdir tmp/$api_pkgname/lib +mkdir tmp/$api_pkgname/include +cp LICENSE README.md tmp/$api_pkgname +mv bins-$src/* tmp/$api_pkgname/lib +cp crates/c-api/examples/wasm-c-api/include/wasm.h tmp/$api_pkgname/include +mktarball $api_pkgname + +# Move wheels to dist folder +mv wheels-$src/* dist diff --git a/ci/setup_centos6_python3.sh b/ci/setup_centos6_python3.sh new file mode 100644 index 0000000000..35ab465469 --- /dev/null +++ b/ci/setup_centos6_python3.sh @@ -0,0 +1,34 @@ +#!/bin/bash +set -e + +VERSION=${1:-3.7.3} + +# Python 3.6 stands in our way -- nuking it +yum erase -y rh-python36 +rm -rf /opt/rh/rh-python36 + +yum install -y gcc bzip2-devel libffi-devel zlib-devel + +cd /usr/src/ + +# pip3.7 needs new openssl +curl -O -L https://github.com/openssl/openssl/archive/OpenSSL_1_1_1c.tar.gz +tar -zxvf OpenSSL_1_1_1c.tar.gz +cd openssl-OpenSSL_1_1_1c +./Configure shared zlib linux-x86_64 +make -sj4 +make install +cd .. +rm -rf openssl-OpenSSL_1_1_1c + +# Fixing libssl.so.1.1: cannot open shared object file +echo "/usr/local/lib64" >> /etc/ld.so.conf && ldconfig + +curl -O -L https://www.python.org/ftp/python/${VERSION}/Python-${VERSION}.tgz +tar xzf Python-${VERSION}.tgz +cd Python-${VERSION} +./configure +make -sj4 +make install +cd .. +rm -rf Python-${VERSION} diff --git a/ci/wasmtime.wxs b/ci/wasmtime.wxs new file mode 100644 index 0000000000..8110932cb9 --- /dev/null +++ b/ci/wasmtime.wxs @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000000..152f137769 --- /dev/null +++ b/clippy.toml @@ -0,0 +1 @@ +doc-valid-idents = ["WebAssembly"] diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml new file mode 100644 index 0000000000..b4adb27975 --- /dev/null +++ b/crates/api/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "wasmtime" +version = "0.12.0" +authors = ["The Wasmtime Project Developers"] +description = "High-level API to expose the Wasmtime runtime" +license = "Apache-2.0 WITH LLVM-exception" +repository = "https://github.com/bytecodealliance/wasmtime" +readme = "README.md" +edition = "2018" + +[dependencies] +wasmtime-runtime = { path = "../runtime", version = "0.12.0" } +wasmtime-environ = { path = "../environ", version = "0.12.0" } +wasmtime-jit = { path = "../jit", version = "0.12.0" } +wasmtime-profiling = { path = "../profiling", version = "0.12.0" } +wasmparser = "0.51.2" +target-lexicon = { version = "0.10.0", default-features = false } +anyhow = "1.0.19" +region = "2.0.0" +libc = "0.2" +cfg-if = "0.1.9" +backtrace = "0.3.42" +rustc-demangle = "0.1.16" +lazy_static = "1.4" +wat = { version = "1.0.10", optional = true } + +[target.'cfg(target_os = "windows")'.dependencies] +winapi = "0.3.7" + +[dev-dependencies] +# for wasmtime.rs +wasi-common = { path = "../wasi-common", version = "0.12.0" } +pretty_env_logger = "0.3.0" +rayon = "1.2.1" +file-per-thread-logger = "0.1.1" +wat = "1.0.10" +tempfile = "3.1" + +[badges] +maintenance = { status = "actively-developed" } + +[features] +default = ['wat'] + +# Enables experimental support for the lightbeam codegen backend, an alternative +# to cranelift. Requires Nightly Rust currently, and this is not enabled by +# default. +lightbeam = ["wasmtime-jit/lightbeam"] + +[[test]] +name = "host-segfault" +harness = false diff --git a/crates/api/LICENSE b/crates/api/LICENSE new file mode 100644 index 0000000000..f9d81955f4 --- /dev/null +++ b/crates/api/LICENSE @@ -0,0 +1,220 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + diff --git a/crates/api/README.md b/crates/api/README.md new file mode 100644 index 0000000000..49ca113c15 --- /dev/null +++ b/crates/api/README.md @@ -0,0 +1,8 @@ +## Wasmtime Embedding API + +The `wasmtime` crate is an embedding API of the `wasmtime` WebAssembly runtime. +This is intended to be used in Rust projects and provides a high-level API of +working with WebAssembly modules. + +If you're interested in embedding `wasmtime` in other languages, you may wish to +take a look a the [C embedding API](../c-api) instead! diff --git a/crates/api/examples/gcd.rs b/crates/api/examples/gcd.rs new file mode 100644 index 0000000000..5f68b87417 --- /dev/null +++ b/crates/api/examples/gcd.rs @@ -0,0 +1,60 @@ +//! Example of instantiating of the WebAssembly module and +//! invoking its exported function. + +use wasmtime::*; + +const WAT: &str = r#" +(module + (func $gcd (param i32 i32) (result i32) + (local i32) + block ;; label = @1 + block ;; label = @2 + local.get 0 + br_if 0 (;@2;) + local.get 1 + local.set 2 + br 1 (;@1;) + end + loop ;; label = @2 + local.get 1 + local.get 0 + local.tee 2 + i32.rem_u + local.set 0 + local.get 2 + local.set 1 + local.get 0 + br_if 0 (;@2;) + end + end + local.get 2 + ) + (export "gcd" (func $gcd)) +) +"#; + +fn main() -> anyhow::Result<()> { + // Load our WebAssembly (parsed WAT in our case), and then load it into a + // `Module` which is attached to a `Store` cache. + let store = Store::default(); + let module = Module::new(&store, WAT)?; + + // Find index of the `gcd` export. + let gcd_index = module + .exports() + .iter() + .enumerate() + .find(|(_, export)| export.name().to_string() == "gcd") + .unwrap() + .0; + + // Instantiate the module. + let instance = Instance::new(&module, &[])?; + + // Invoke `gcd` export + let gcd = instance.exports()[gcd_index].func().expect("gcd"); + let result = gcd.call(&[Val::from(6i32), Val::from(27i32)])?; + + println!("{:?}", result); + Ok(()) +} diff --git a/crates/api/examples/hello.rs b/crates/api/examples/hello.rs new file mode 100644 index 0000000000..9a461569ba --- /dev/null +++ b/crates/api/examples/hello.rs @@ -0,0 +1,50 @@ +//! Translation of hello example + +use anyhow::{ensure, Context as _, Result}; +use wasmtime::*; + +fn main() -> Result<()> { + // Configure the initial compilation environment, creating the global + // `Store` structure. Note that you can also tweak configuration settings + // with a `Config` and an `Engine` if desired. + println!("Initializing..."); + let store = Store::default(); + + // Compile the wasm binary into an in-memory instance of a `Module`. + println!("Compiling module..."); + let wat = r#" + (module + (func $hello (import "" "hello")) + (func (export "run") (call $hello)) + ) + "#; + let module = Module::new(&store, wat).context("> Error compiling module!")?; + + // Here we handle the imports of the module, which in this case is our + // `HelloCallback` type and its associated implementation of `Callback. + println!("Creating callback..."); + let hello_func = Func::wrap0(&store, || { + println!("Calling back..."); + println!("> Hello World!"); + }); + + // Once we've got that all set up we can then move to the instantiation + // phase, pairing together a compiled module as well as a set of imports. + // Note that this is where the wasm `start` function, if any, would run. + println!("Instantiating module..."); + let imports = vec![hello_func.into()]; + let instance = Instance::new(&module, &imports).context("> Error instantiating module!")?; + + // Next we poke around a bit to extract the `run` function from the module. + println!("Extracting export..."); + let exports = instance.exports(); + ensure!(!exports.is_empty(), "> Error accessing exports!"); + let run_func = exports[0].func().context("> Error accessing exports!")?; + + // And last but not least we can call it! + println!("Calling export..."); + run_func.call(&[])?; + + println!("Done."); + Ok(()) +} diff --git a/crates/api/examples/memory.rs b/crates/api/examples/memory.rs new file mode 100644 index 0000000000..dd2908c005 --- /dev/null +++ b/crates/api/examples/memory.rs @@ -0,0 +1,160 @@ +//! Translation of the memory example + +use anyhow::{bail, ensure, Context as _, Error}; +use wasmtime::*; + +fn get_export_memory(exports: &[Extern], i: usize) -> Result { + if exports.len() <= i { + bail!("> Error accessing memory export {}!", i); + } + Ok(exports[i] + .memory() + .with_context(|| format!("> Error accessing memory export {}!", i))? + .clone()) +} + +fn get_export_func(exports: &[Extern], i: usize) -> Result { + if exports.len() <= i { + bail!("> Error accessing function export {}!", i); + } + Ok(exports[i] + .func() + .with_context(|| format!("> Error accessing function export {}!", i))? + .clone()) +} + +macro_rules! check { + ($actual:expr, $expected:expr) => { + if $actual != $expected { + bail!("> Error on result, expected {}, got {}", $expected, $actual); + } + }; +} + +macro_rules! check_ok { + ($func:expr, $($p:expr),*) => { + if let Err(_) = $func.call(&[$($p.into()),*]) { + bail!("> Error on result, expected return"); + } + } +} + +macro_rules! check_trap { + ($func:expr, $($p:expr),*) => { + if let Ok(_) = $func.call(&[$($p.into()),*]) { + bail!("> Error on result, expected trap"); + } + } +} + +macro_rules! call { + ($func:expr, $($p:expr),*) => { + match $func.call(&[$($p.into()),*]) { + Ok(result) => { + let result: i32 = result[0].unwrap_i32(); + result + } + Err(_) => { bail!("> Error on result, expected return"); } + } + } +} + +fn main() -> Result<(), Error> { + // Initialize. + println!("Initializing..."); + let store = Store::default(); + + // Load binary. + println!("Loading binary..."); + let wat = r#" + (module + (memory (export "memory") 2 3) + + (func (export "size") (result i32) (memory.size)) + (func (export "load") (param i32) (result i32) + (i32.load8_s (local.get 0)) + ) + (func (export "store") (param i32 i32) + (i32.store8 (local.get 0) (local.get 1)) + ) + + (data (i32.const 0x1000) "\01\02\03\04") + ) + "#; + + // Compile. + println!("Compiling module..."); + let module = Module::new(&store, &wat).context("> Error compiling module!")?; + + // Instantiate. + println!("Instantiating module..."); + let instance = Instance::new(&module, &[]).context("> Error instantiating module!")?; + + // Extract export. + println!("Extracting export..."); + let exports = instance.exports(); + ensure!(!exports.is_empty(), "> Error accessing exports!"); + let memory = get_export_memory(&exports, 0)?; + let size_func = get_export_func(&exports, 1)?; + let load_func = get_export_func(&exports, 2)?; + let store_func = get_export_func(&exports, 3)?; + + // Check initial memory. + println!("Checking memory..."); + check!(memory.size(), 2u32); + check!(memory.data_size(), 0x20000usize); + check!(unsafe { memory.data_unchecked_mut()[0] }, 0); + check!(unsafe { memory.data_unchecked_mut()[0x1000] }, 1); + check!(unsafe { memory.data_unchecked_mut()[0x1003] }, 4); + + check!(call!(size_func,), 2); + check!(call!(load_func, 0), 0); + check!(call!(load_func, 0x1000), 1); + check!(call!(load_func, 0x1003), 4); + check!(call!(load_func, 0x1ffff), 0); + check_trap!(load_func, 0x20000); + + // Mutate memory. + println!("Mutating memory..."); + unsafe { + memory.data_unchecked_mut()[0x1003] = 5; + } + + check_ok!(store_func, 0x1002, 6); + check_trap!(store_func, 0x20000, 0); + + check!(unsafe { memory.data_unchecked()[0x1002] }, 6); + check!(unsafe { memory.data_unchecked()[0x1003] }, 5); + check!(call!(load_func, 0x1002), 6); + check!(call!(load_func, 0x1003), 5); + + // Grow memory. + println!("Growing memory..."); + memory.grow(1)?; + check!(memory.size(), 3u32); + check!(memory.data_size(), 0x30000usize); + + check!(call!(load_func, 0x20000), 0); + check_ok!(store_func, 0x20000, 0); + check_trap!(load_func, 0x30000); + check_trap!(store_func, 0x30000, 0); + + memory.grow(1).unwrap_err(); + memory.grow(0).unwrap(); + + // Create stand-alone memory. + // TODO(wasm+): Once Wasm allows multiple memories, turn this into import. + println!("Creating stand-alone memory..."); + let memorytype = MemoryType::new(Limits::new(5, Some(5))); + let memory2 = Memory::new(&store, memorytype); + check!(memory2.size(), 5u32); + memory2.grow(1).unwrap_err(); + memory2.grow(0).unwrap(); + + // Shut down. + println!("Shutting down..."); + drop(store); + + println!("Done."); + Ok(()) +} diff --git a/crates/api/examples/multi.rs b/crates/api/examples/multi.rs new file mode 100644 index 0000000000..663281eb09 --- /dev/null +++ b/crates/api/examples/multi.rs @@ -0,0 +1,129 @@ +//! Translation of multi example + +use anyhow::{ensure, format_err, Context as _, Result}; +use std::rc::Rc; +use wasmtime::*; + +struct Callback; + +impl Callable for Callback { + fn call(&self, args: &[Val], results: &mut [Val]) -> Result<(), Trap> { + println!("Calling back..."); + println!("> {} {}", args[0].unwrap_i32(), args[1].unwrap_i64()); + + results[0] = Val::I64(args[1].unwrap_i64() + 1); + results[1] = Val::I32(args[0].unwrap_i32() + 1); + Ok(()) + } +} + +const WAT: &str = r#" +(module + (func $f (import "" "f") (param i32 i64) (result i64 i32)) + + (func $g (export "g") (param i32 i64) (result i64 i32) + (call $f (local.get 0) (local.get 1)) + ) + + (func $round_trip_many + (export "round_trip_many") + (param i64 i64 i64 i64 i64 i64 i64 i64 i64 i64) + (result i64 i64 i64 i64 i64 i64 i64 i64 i64 i64) + local.get 0 + local.get 1 + local.get 2 + local.get 3 + local.get 4 + local.get 5 + local.get 6 + local.get 7 + local.get 8 + local.get 9) +) +"#; + +fn main() -> Result<()> { + // Initialize. + println!("Initializing..."); + let engine = Engine::new(Config::new().wasm_multi_value(true)); + let store = Store::new(&engine); + + // Compile. + println!("Compiling module..."); + let module = Module::new(&store, WAT).context("Error compiling module!")?; + + // Create external print functions. + println!("Creating callback..."); + let callback_type = FuncType::new( + Box::new([ValType::I32, ValType::I64]), + Box::new([ValType::I64, ValType::I32]), + ); + let callback_func = Func::new(&store, callback_type, Rc::new(Callback)); + + // Instantiate. + println!("Instantiating module..."); + let imports = vec![callback_func.into()]; + let instance = + Instance::new(&module, imports.as_slice()).context("Error instantiating module!")?; + + // Extract exports. + println!("Extracting export..."); + let exports = instance.exports(); + ensure!(!exports.is_empty(), "Error accessing exports!"); + let g = exports[0].func().context("> Error accessing export $g!")?; + let round_trip_many = exports[1] + .func() + .context("> Error accessing export $round_trip_many")?; + + // Call `$g`. + println!("Calling export \"g\"..."); + let args = vec![Val::I32(1), Val::I64(3)]; + let results = g + .call(&args) + .map_err(|e| format_err!("> Error calling g! {:?}", e))?; + + println!("Printing result..."); + println!("> {} {}", results[0].unwrap_i64(), results[1].unwrap_i32()); + + debug_assert_eq!(results[0].unwrap_i64(), 4); + debug_assert_eq!(results[1].unwrap_i32(), 2); + + // Call `$round_trip_many`. + println!("Calling export \"round_trip_many\"..."); + let args = vec![ + Val::I64(0), + Val::I64(1), + Val::I64(2), + Val::I64(3), + Val::I64(4), + Val::I64(5), + Val::I64(6), + Val::I64(7), + Val::I64(8), + Val::I64(9), + ]; + let results = round_trip_many + .call(&args) + .map_err(|e| format_err!("> Error calling round_trip_many! {:?}", e))?; + + println!("Printing result..."); + print!(">"); + for r in results.iter() { + print!(" {}", r.unwrap_i64()); + } + println!(); + + debug_assert_eq!(results.len(), 10); + debug_assert!(args + .iter() + .zip(results.iter()) + .all(|(a, r)| a.i64() == r.i64())); + + // Shut down. + println!("Shutting down..."); + drop(store); + + // All done. + println!("Done."); + Ok(()) +} diff --git a/crates/api/src/callable.rs b/crates/api/src/callable.rs new file mode 100644 index 0000000000..c4983df8c8 --- /dev/null +++ b/crates/api/src/callable.rs @@ -0,0 +1,225 @@ +use crate::runtime::Store; +use crate::trampoline::generate_func_export; +use crate::trap::Trap; +use crate::types::FuncType; +use crate::values::Val; +use std::ptr; +use std::rc::Rc; +use wasmtime_environ::ir; +use wasmtime_runtime::{Export, InstanceHandle}; + +/// A trait representing a function that can be imported and called from inside +/// WebAssembly. +/// # Example +/// ``` +/// use wasmtime::Val; +/// +/// struct TimesTwo; +/// +/// impl wasmtime::Callable for TimesTwo { +/// fn call(&self, params: &[Val], results: &mut [Val]) -> Result<(), wasmtime::Trap> { +/// let mut value = params[0].unwrap_i32(); +/// value *= 2; +/// results[0] = value.into(); +/// +/// Ok(()) +/// } +/// } +/// +/// # fn main () -> Result<(), Box> { +/// // Simple module that imports our host function ("times_two") and re-exports +/// // it as "run". +/// let wat = r#" +/// (module +/// (func $times_two (import "" "times_two") (param i32) (result i32)) +/// (func +/// (export "run") +/// (param i32) +/// (result i32) +/// (local.get 0) +/// (call $times_two)) +/// ) +/// "#; +/// +/// // Initialise environment and our module. +/// let store = wasmtime::Store::default(); +/// let module = wasmtime::Module::new(&store, wat)?; +/// +/// // Define the type of the function we're going to call. +/// let times_two_type = wasmtime::FuncType::new( +/// // Parameters +/// Box::new([wasmtime::ValType::I32]), +/// // Results +/// Box::new([wasmtime::ValType::I32]) +/// ); +/// +/// // Build a reference to the "times_two" function that can be used. +/// let times_two_function = +/// wasmtime::Func::new(&store, times_two_type, std::rc::Rc::new(TimesTwo)); +/// +/// // Create module instance that imports our function +/// let instance = wasmtime::Instance::new( +/// &module, +/// &[times_two_function.into()] +/// )?; +/// +/// // Get "run" function from the exports. +/// let run_function = instance.exports()[0].func().unwrap(); +/// +/// // Borrow and call "run". Returning any error message from Wasm as a string. +/// let original = 5i32; +/// let results = run_function +/// .call(&[original.into()]) +/// .map_err(|trap| trap.to_string())?; +/// +/// // Compare that the results returned matches what we expect. +/// assert_eq!(original * 2, results[0].unwrap_i32()); +/// # Ok(()) +/// # } +/// ``` +pub trait Callable { + /// What is called when the function is invoked in WebAssembly. + /// `params` is an immutable list of parameters provided to the function. + /// `results` is mutable list of results to be potentially set by your + /// function. Produces a `Trap` if the function encounters any errors. + fn call(&self, params: &[Val], results: &mut [Val]) -> Result<(), Trap>; +} + +pub(crate) trait WrappedCallable { + fn call(&self, params: &[Val], results: &mut [Val]) -> Result<(), Trap>; + fn signature(&self) -> &ir::Signature { + match self.wasmtime_export() { + Export::Function { signature, .. } => signature, + _ => panic!("unexpected export type in Callable"), + } + } + fn wasmtime_handle(&self) -> &InstanceHandle; + fn wasmtime_export(&self) -> &Export; +} + +pub(crate) struct WasmtimeFn { + store: Store, + instance: InstanceHandle, + export: Export, +} + +impl WasmtimeFn { + pub fn new(store: &Store, instance: InstanceHandle, export: Export) -> WasmtimeFn { + WasmtimeFn { + store: store.clone(), + instance, + export, + } + } +} + +impl WrappedCallable for WasmtimeFn { + fn call(&self, params: &[Val], results: &mut [Val]) -> Result<(), Trap> { + use std::cmp::max; + use std::mem; + + let (vmctx, body, signature) = match self.wasmtime_export() { + Export::Function { + vmctx, + address, + signature, + } => (*vmctx, *address, signature.clone()), + _ => panic!("unexpected export type in Callable"), + }; + if signature.params.len() - 2 != params.len() { + return Err(Trap::new(format!( + "expected {} arguments, got {}", + signature.params.len() - 2, + params.len() + ))); + } + if signature.returns.len() != results.len() { + return Err(Trap::new(format!( + "expected {} results, got {}", + signature.returns.len(), + results.len() + ))); + } + + let value_size = mem::size_of::(); + let mut values_vec = vec![0; max(params.len(), results.len())]; + + // Store the argument values into `values_vec`. + let param_tys = signature.params.iter().skip(2); + for ((arg, slot), ty) in params.iter().zip(&mut values_vec).zip(param_tys) { + if arg.ty().get_wasmtime_type() != Some(ty.value_type) { + return Err(Trap::new("argument type mismatch")); + } + unsafe { + arg.write_value_to(slot); + } + } + + // Get the trampoline to call for this function. + let exec_code_buf = self + .store + .compiler_mut() + .get_published_trampoline(&signature, value_size) + .map_err(|e| Trap::new(format!("trampoline error: {:?}", e)))?; + + // Call the trampoline. + if let Err(error) = unsafe { + wasmtime_runtime::wasmtime_call_trampoline( + vmctx, + ptr::null_mut(), + exec_code_buf, + body, + values_vec.as_mut_ptr() as *mut u8, + ) + } { + return Err(Trap::from_jit(error)); + } + + // Load the return values out of `values_vec`. + for (index, abi_param) in signature.returns.iter().enumerate() { + unsafe { + let ptr = values_vec.as_ptr().add(index); + + results[index] = Val::read_value_from(ptr, abi_param.value_type); + } + } + + Ok(()) + } + fn wasmtime_handle(&self) -> &InstanceHandle { + &self.instance + } + fn wasmtime_export(&self) -> &Export { + &self.export + } +} + +pub struct NativeCallable { + callable: Rc, + instance: InstanceHandle, + export: Export, +} + +impl NativeCallable { + pub(crate) fn new(callable: Rc, ft: &FuncType, store: &Store) -> Self { + let (instance, export) = + generate_func_export(ft, &callable, store).expect("generated func"); + NativeCallable { + callable, + instance, + export, + } + } +} + +impl WrappedCallable for NativeCallable { + fn call(&self, params: &[Val], results: &mut [Val]) -> Result<(), Trap> { + self.callable.call(params, results) + } + fn wasmtime_handle(&self) -> &InstanceHandle { + &self.instance + } + fn wasmtime_export(&self) -> &Export { + &self.export + } +} diff --git a/crates/api/src/externals.rs b/crates/api/src/externals.rs new file mode 100644 index 0000000000..1735c10be6 --- /dev/null +++ b/crates/api/src/externals.rs @@ -0,0 +1,643 @@ +use crate::trampoline::{generate_global_export, generate_memory_export, generate_table_export}; +use crate::values::{from_checked_anyfunc, into_checked_anyfunc, Val}; +use crate::Mutability; +use crate::{ExternType, GlobalType, MemoryType, TableType, ValType}; +use crate::{Func, Store}; +use anyhow::{anyhow, bail, Result}; +use std::slice; +use wasmtime_environ::{ir, wasm}; +use wasmtime_runtime::{self as runtime, InstanceHandle}; + +// Externals + +/// An external item to a WebAssembly module, or a list of what can possibly be +/// exported from a wasm module. +/// +/// This is both returned from [`Instance::exports`](crate::Instance::exports) +/// as well as required by [`Instance::new`](crate::Instance::new). In other +/// words, this is the type of extracted values from an instantiated module, and +/// it's also used to provide imported values when instantiating a module. +#[derive(Clone)] +pub enum Extern { + /// A WebAssembly `func` which can be called. + Func(Func), + /// A WebAssembly `global` which acts like a `Cell` of sorts, supporting + /// `get` and `set` operations. + Global(Global), + /// A WebAssembly `table` which is an array of `Val` types. + Table(Table), + /// A WebAssembly linear memory. + Memory(Memory), +} + +impl Extern { + /// Returns the underlying `Func`, if this external is a function. + /// + /// Returns `None` if this is not a function. + pub fn func(&self) -> Option<&Func> { + match self { + Extern::Func(func) => Some(func), + _ => None, + } + } + + /// Returns the underlying `Global`, if this external is a global. + /// + /// Returns `None` if this is not a global. + pub fn global(&self) -> Option<&Global> { + match self { + Extern::Global(global) => Some(global), + _ => None, + } + } + + /// Returns the underlying `Table`, if this external is a table. + /// + /// Returns `None` if this is not a table. + pub fn table(&self) -> Option<&Table> { + match self { + Extern::Table(table) => Some(table), + _ => None, + } + } + + /// Returns the underlying `Memory`, if this external is a memory. + /// + /// Returns `None` if this is not a memory. + pub fn memory(&self) -> Option<&Memory> { + match self { + Extern::Memory(memory) => Some(memory), + _ => None, + } + } + + /// Returns the type associated with this `Extern`. + pub fn ty(&self) -> ExternType { + match self { + Extern::Func(ft) => ExternType::Func(ft.ty().clone()), + Extern::Memory(ft) => ExternType::Memory(ft.ty().clone()), + Extern::Table(tt) => ExternType::Table(tt.ty().clone()), + Extern::Global(gt) => ExternType::Global(gt.ty().clone()), + } + } + + pub(crate) fn get_wasmtime_export(&self) -> wasmtime_runtime::Export { + match self { + Extern::Func(f) => f.wasmtime_export().clone(), + Extern::Global(g) => g.wasmtime_export().clone(), + Extern::Memory(m) => m.wasmtime_export().clone(), + Extern::Table(t) => t.wasmtime_export().clone(), + } + } + + pub(crate) fn from_wasmtime_export( + store: &Store, + instance_handle: InstanceHandle, + export: wasmtime_runtime::Export, + ) -> Extern { + match export { + wasmtime_runtime::Export::Function { .. } => { + Extern::Func(Func::from_wasmtime_function(export, store, instance_handle)) + } + wasmtime_runtime::Export::Memory { .. } => { + Extern::Memory(Memory::from_wasmtime_memory(export, store, instance_handle)) + } + wasmtime_runtime::Export::Global { .. } => { + Extern::Global(Global::from_wasmtime_global(export, store, instance_handle)) + } + wasmtime_runtime::Export::Table { .. } => { + Extern::Table(Table::from_wasmtime_table(export, store, instance_handle)) + } + } + } +} + +impl From for Extern { + fn from(r: Func) -> Self { + Extern::Func(r) + } +} + +impl From for Extern { + fn from(r: Global) -> Self { + Extern::Global(r) + } +} + +impl From for Extern { + fn from(r: Memory) -> Self { + Extern::Memory(r) + } +} + +impl From for Extern { + fn from(r: Table) -> Self { + Extern::Table(r) + } +} + +/// A WebAssembly `global` value which can be read and written to. +/// +/// A `global` in WebAssembly is sort of like a global variable within an +/// [`Instance`](crate::Instance). The `global.get` and `global.set` +/// instructions will modify and read global values in a wasm module. Globals +/// can either be imported or exported from wasm modules. +/// +/// If you're familiar with Rust already you can think of a `Global` as a sort +/// of `Rc>`, more or less. +/// +/// # `Global` and `Clone` +/// +/// Globals are internally reference counted so you can `clone` a `Global`. The +/// cloning process only performs a shallow clone, so two cloned `Global` +/// instances are equivalent in their functionality. +#[derive(Clone)] +pub struct Global { + _store: Store, + ty: GlobalType, + wasmtime_export: wasmtime_runtime::Export, + wasmtime_handle: InstanceHandle, +} + +impl Global { + /// Creates a new WebAssembly `global` value with the provide type `ty` and + /// initial value `val`. + /// + /// The `store` argument provided is used as a general global cache for + /// information, and otherwise the `ty` and `val` arguments are used to + /// initialize the global. + /// + /// # Errors + /// + /// Returns an error if the `ty` provided does not match the type of the + /// value `val`. + pub fn new(store: &Store, ty: GlobalType, val: Val) -> Result { + if val.ty() != *ty.content() { + bail!("value provided does not match the type of this global"); + } + let (wasmtime_handle, wasmtime_export) = generate_global_export(store, &ty, val)?; + Ok(Global { + _store: store.clone(), + ty, + wasmtime_export, + wasmtime_handle, + }) + } + + /// Returns the underlying type of this `global`. + pub fn ty(&self) -> &GlobalType { + &self.ty + } + + fn wasmtime_global_definition(&self) -> *mut wasmtime_runtime::VMGlobalDefinition { + match self.wasmtime_export { + wasmtime_runtime::Export::Global { definition, .. } => definition, + _ => panic!("global definition not found"), + } + } + + /// Returns the current [`Val`] of this global. + pub fn get(&self) -> Val { + let definition = unsafe { &mut *self.wasmtime_global_definition() }; + unsafe { + match self.ty().content() { + ValType::I32 => Val::from(*definition.as_i32()), + ValType::I64 => Val::from(*definition.as_i64()), + ValType::F32 => Val::F32(*definition.as_u32()), + ValType::F64 => Val::F64(*definition.as_u64()), + _ => unimplemented!("Global::get for {:?}", self.ty().content()), + } + } + } + + /// Attempts to set the current value of this global to [`Val`]. + /// + /// # Errors + /// + /// Returns an error if this global has a different type than `Val`, or if + /// it's not a mutable global. + pub fn set(&self, val: Val) -> Result<()> { + if self.ty().mutability() != Mutability::Var { + bail!("immutable global cannot be set"); + } + if val.ty() != *self.ty().content() { + bail!( + "global of type {:?} cannot be set to {:?}", + self.ty().content(), + val.ty() + ); + } + let definition = unsafe { &mut *self.wasmtime_global_definition() }; + unsafe { + match val { + Val::I32(i) => *definition.as_i32_mut() = i, + Val::I64(i) => *definition.as_i64_mut() = i, + Val::F32(f) => *definition.as_u32_mut() = f, + Val::F64(f) => *definition.as_u64_mut() = f, + _ => unimplemented!("Global::set for {:?}", val.ty()), + } + } + Ok(()) + } + + pub(crate) fn wasmtime_export(&self) -> &wasmtime_runtime::Export { + &self.wasmtime_export + } + + pub(crate) fn from_wasmtime_global( + export: wasmtime_runtime::Export, + store: &Store, + wasmtime_handle: InstanceHandle, + ) -> Global { + let global = if let wasmtime_runtime::Export::Global { ref global, .. } = export { + global + } else { + panic!("wasmtime export is not global") + }; + // The original export is coming from wasmtime_runtime itself we should + // support all the types coming out of it, so assert such here. + let ty = GlobalType::from_wasmtime_global(&global) + .expect("core wasm global type should be supported"); + Global { + _store: store.clone(), + ty: ty, + wasmtime_export: export, + wasmtime_handle, + } + } +} + +/// A WebAssembly `table`, or an array of values. +/// +/// Like [`Memory`] a table is an indexed array of values, but unlike [`Memory`] +/// it's an array of WebAssembly values rather than bytes. One of the most +/// common usages of a table is a function table for wasm modules, where each +/// element has the `Func` type. +/// +/// Tables, like globals, are not threadsafe and can only be used on one thread. +/// Tables can be grown in size and each element can be read/written. +/// +/// # `Table` and `Clone` +/// +/// Tables are internally reference counted so you can `clone` a `Table`. The +/// cloning process only performs a shallow clone, so two cloned `Table` +/// instances are equivalent in their functionality. +#[derive(Clone)] +pub struct Table { + store: Store, + ty: TableType, + wasmtime_handle: InstanceHandle, + wasmtime_export: wasmtime_runtime::Export, +} + +fn set_table_item( + handle: &mut InstanceHandle, + table_index: wasm::DefinedTableIndex, + item_index: u32, + item: wasmtime_runtime::VMCallerCheckedAnyfunc, +) -> Result<()> { + handle + .table_set(table_index, item_index, item) + .map_err(|()| anyhow!("table element index out of bounds")) +} + +impl Table { + /// Creates a new `Table` with the given parameters. + /// + /// * `store` - a global cache to store information in + /// * `ty` - the type of this table, containing both the element type as + /// well as the initial size and maximum size, if any. + /// * `init` - the initial value to fill all table entries with, if the + /// table starts with an initial size. + /// + /// # Errors + /// + /// Returns an error if `init` does not match the element type of the table. + pub fn new(store: &Store, ty: TableType, init: Val) -> Result
{ + let item = into_checked_anyfunc(init, store)?; + let (mut wasmtime_handle, wasmtime_export) = generate_table_export(store, &ty)?; + + // Initialize entries with the init value. + match wasmtime_export { + wasmtime_runtime::Export::Table { definition, .. } => { + let index = wasmtime_handle.table_index(unsafe { &*definition }); + let len = unsafe { (*definition).current_elements }; + for i in 0..len { + set_table_item(&mut wasmtime_handle, index, i, item.clone())?; + } + } + _ => unreachable!("export should be a table"), + } + + Ok(Table { + store: store.clone(), + ty, + wasmtime_handle, + wasmtime_export, + }) + } + + /// Returns the underlying type of this table, including its element type as + /// well as the maximum/minimum lower bounds. + pub fn ty(&self) -> &TableType { + &self.ty + } + + fn wasmtime_table_index(&self) -> wasm::DefinedTableIndex { + match self.wasmtime_export { + wasmtime_runtime::Export::Table { definition, .. } => { + self.wasmtime_handle.table_index(unsafe { &*definition }) + } + _ => panic!("global definition not found"), + } + } + + /// Returns the table element value at `index`. + /// + /// Returns `None` if `index` is out of bounds. + pub fn get(&self, index: u32) -> Option { + let table_index = self.wasmtime_table_index(); + let item = self.wasmtime_handle.table_get(table_index, index)?; + Some(from_checked_anyfunc(item, &self.store)) + } + + /// Writes the `val` provided into `index` within this table. + /// + /// # Errors + /// + /// Returns an error if `index` is out of bounds or if `val` does not have + /// the right type to be stored in this table. + pub fn set(&self, index: u32, val: Val) -> Result<()> { + let table_index = self.wasmtime_table_index(); + let mut wasmtime_handle = self.wasmtime_handle.clone(); + let item = into_checked_anyfunc(val, &self.store)?; + set_table_item(&mut wasmtime_handle, table_index, index, item) + } + + /// Returns the current size of this table. + pub fn size(&self) -> u32 { + match self.wasmtime_export { + wasmtime_runtime::Export::Table { definition, .. } => unsafe { + (*definition).current_elements + }, + _ => panic!("global definition not found"), + } + } + + /// Grows the size of this table by `delta` more elements, initialization + /// all new elements to `init`. + /// + /// # Errors + /// + /// Returns an error if the table cannot be grown by `delta`, for example + /// if it would cause the table to exceed its maximum size. Also returns an + /// error if `init` is not of the right type. + pub fn grow(&self, delta: u32, init: Val) -> Result { + let index = self.wasmtime_table_index(); + let item = into_checked_anyfunc(init, &self.store)?; + if let Some(len) = self.wasmtime_handle.clone().table_grow(index, delta) { + let mut wasmtime_handle = self.wasmtime_handle.clone(); + for i in 0..delta { + let i = len - (delta - i); + set_table_item(&mut wasmtime_handle, index, i, item.clone())?; + } + Ok(len) + } else { + bail!("failed to grow table by `{}`", delta) + } + } + + /// Copy `len` elements from `src_table[src_index..]` into + /// `dst_table[dst_index..]`. + /// + /// # Errors + /// + /// Returns an error if the range is out of bounds of either the source or + /// destination tables. + pub fn copy( + dst_table: &Table, + dst_index: u32, + src_table: &Table, + src_index: u32, + len: u32, + ) -> Result<()> { + // NB: We must use the `dst_table`'s `wasmtime_handle` for the + // `dst_table_index` and vice versa for `src_table` since each table can + // come from different modules. + + let dst_table_index = dst_table.wasmtime_table_index(); + let dst_table = dst_table.wasmtime_handle.get_defined_table(dst_table_index); + + let src_table_index = src_table.wasmtime_table_index(); + let src_table = src_table.wasmtime_handle.get_defined_table(src_table_index); + + runtime::Table::copy( + dst_table, + src_table, + dst_index, + src_index, + len, + ir::SourceLoc::default(), + )?; + Ok(()) + } + + pub(crate) fn wasmtime_export(&self) -> &wasmtime_runtime::Export { + &self.wasmtime_export + } + + pub(crate) fn from_wasmtime_table( + export: wasmtime_runtime::Export, + store: &Store, + instance_handle: wasmtime_runtime::InstanceHandle, + ) -> Table { + let table = if let wasmtime_runtime::Export::Table { ref table, .. } = export { + table + } else { + panic!("wasmtime export is not table") + }; + let ty = TableType::from_wasmtime_table(&table.table); + Table { + store: store.clone(), + ty: ty, + wasmtime_handle: instance_handle, + wasmtime_export: export, + } + } +} + +/// A WebAssembly linear memory. +/// +/// WebAssembly memories represent a contiguous array of bytes that have a size +/// that is always a multiple of the WebAssembly page size, currently 64 +/// kilobytes. +/// +/// WebAssembly memory is used for global data, statics in C/C++/Rust, shadow +/// stack memory, etc. Accessing wasm memory is generally quite fast! +/// +/// # `Memory` and `Clone` +/// +/// Memories are internally reference counted so you can `clone` a `Memory`. The +/// cloning process only performs a shallow clone, so two cloned `Memory` +/// instances are equivalent in their functionality. +/// +/// # `Memory` and threads +/// +/// It is intended that `Memory` is safe to share between threads. At this time +/// this is not implemented in `wasmtime`, however. This is planned to be +/// implemented though! +#[derive(Clone)] +pub struct Memory { + _store: Store, + ty: MemoryType, + wasmtime_handle: InstanceHandle, + wasmtime_export: wasmtime_runtime::Export, +} + +impl Memory { + /// Creates a new WebAssembly memory given the configuration of `ty`. + /// + /// The `store` argument is a general location for cache information, and + /// otherwise the memory will immediately be allocated according to the + /// type's configuration. All WebAssembly memory is initialized to zero. + pub fn new(store: &Store, ty: MemoryType) -> Memory { + let (wasmtime_handle, wasmtime_export) = + generate_memory_export(store, &ty).expect("generated memory"); + Memory { + _store: store.clone(), + ty, + wasmtime_handle, + wasmtime_export, + } + } + + /// Returns the underlying type of this memory. + pub fn ty(&self) -> &MemoryType { + &self.ty + } + + fn wasmtime_memory_definition(&self) -> *mut wasmtime_runtime::VMMemoryDefinition { + match self.wasmtime_export { + wasmtime_runtime::Export::Memory { definition, .. } => definition, + _ => panic!("memory definition not found"), + } + } + + /// Returns this memory as a slice view that can be read natively in Rust. + /// + /// # Safety + /// + /// This is an unsafe operation because there is no guarantee that the + /// following operations do not happen concurrently while the slice is in + /// use: + /// + /// * Data could be modified by calling into a wasm module. + /// * Memory could be relocated through growth by calling into a wasm + /// module. + /// * When threads are supported, non-atomic reads will race with other + /// writes. + /// + /// Extreme care need be taken when the data of a `Memory` is read. The + /// above invariants all need to be upheld at a bare minimum, and in + /// general you'll need to ensure that while you're looking at slice you're + /// the only one who can possibly look at the slice and read/write it. + /// + /// Be sure to keep in mind that `Memory` is reference counted, meaning + /// that there may be other users of this `Memory` instance elsewhere in + /// your program. Additionally `Memory` can be shared and used in any number + /// of wasm instances, so calling any wasm code should be considered + /// dangerous while you're holding a slice of memory. + pub unsafe fn data_unchecked(&self) -> &[u8] { + self.data_unchecked_mut() + } + + /// Returns this memory as a slice view that can be read and written + /// natively in Rust. + /// + /// # Safety + /// + /// All of the same safety caveats of [`Memory::data_unchecked`] apply + /// here, doubly so because this is returning a mutable slice! As a + /// double-extra reminder, remember that `Memory` is reference counted, so + /// you can very easily acquire two mutable slices by simply calling this + /// function twice. Extreme caution should be used when using this method, + /// and in general you probably want to result to unsafe accessors and the + /// `data` methods below. + pub unsafe fn data_unchecked_mut(&self) -> &mut [u8] { + let definition = &*self.wasmtime_memory_definition(); + slice::from_raw_parts_mut(definition.base, definition.current_length) + } + + /// Returns the base pointer, in the host's address space, that the memory + /// is located at. + /// + /// When reading and manipulating memory be sure to read up on the caveats + /// of [`Memory::data_unchecked`] to make sure that you can safely + /// read/write the memory. + pub fn data_ptr(&self) -> *mut u8 { + unsafe { (*self.wasmtime_memory_definition()).base } + } + + /// Returns the byte length of this memory. + /// + /// The returned value will be a multiple of the wasm page size, 64k. + pub fn data_size(&self) -> usize { + unsafe { (*self.wasmtime_memory_definition()).current_length } + } + + /// Returns the size, in pages, of this wasm memory. + pub fn size(&self) -> u32 { + (self.data_size() / wasmtime_environ::WASM_PAGE_SIZE as usize) as u32 + } + + /// Grows this WebAssembly memory by `delta` pages. + /// + /// This will attempt to add `delta` more pages of memory on to the end of + /// this `Memory` instance. If successful this may relocate the memory and + /// cause [`Memory::data_ptr`] to return a new value. Additionally previous + /// slices into this memory may no longer be valid. + /// + /// On success returns the number of pages this memory previously had + /// before the growth succeeded. + /// + /// # Errors + /// + /// Returns an error if memory could not be grown, for example if it exceeds + /// the maximum limits of this memory. + pub fn grow(&self, delta: u32) -> Result { + match self.wasmtime_export { + wasmtime_runtime::Export::Memory { definition, .. } => { + let definition = unsafe { &(*definition) }; + let index = self.wasmtime_handle.memory_index(definition); + self.wasmtime_handle + .clone() + .memory_grow(index, delta) + .ok_or_else(|| anyhow!("failed to grow memory")) + } + _ => panic!("memory definition not found"), + } + } + + pub(crate) fn wasmtime_export(&self) -> &wasmtime_runtime::Export { + &self.wasmtime_export + } + + pub(crate) fn from_wasmtime_memory( + export: wasmtime_runtime::Export, + store: &Store, + instance_handle: wasmtime_runtime::InstanceHandle, + ) -> Memory { + let memory = if let wasmtime_runtime::Export::Memory { ref memory, .. } = export { + memory + } else { + panic!("wasmtime export is not memory") + }; + let ty = MemoryType::from_wasmtime_memory(&memory.memory); + Memory { + _store: store.clone(), + ty: ty, + wasmtime_handle: instance_handle, + wasmtime_export: export, + } + } +} diff --git a/crates/api/src/frame_info.rs b/crates/api/src/frame_info.rs new file mode 100644 index 0000000000..5e0ea13331 --- /dev/null +++ b/crates/api/src/frame_info.rs @@ -0,0 +1,183 @@ +use crate::module::Names; +use std::collections::BTreeMap; +use std::sync::{Arc, RwLock}; +use wasmtime_environ::entity::EntityRef; +use wasmtime_environ::wasm::FuncIndex; +use wasmtime_jit::CompiledModule; + +lazy_static::lazy_static! { + /// This is a global cache of backtrace frame information for all active + /// + /// This global cache is used during `Trap` creation to symbolicate frames. + /// This is populated on module compilation, and it is cleared out whenever + /// all references to a module are dropped. + pub static ref FRAME_INFO: GlobalFrameInfo = GlobalFrameInfo::default(); +} + +#[derive(Default)] +pub struct GlobalFrameInfo { + /// An internal map that keeps track of backtrace frame information for + /// each module. + /// + /// This map is morally a map of ranges to a map of information for that + /// module. Each module is expected to reside in a disjoint section of + /// contiguous memory. No modules can overlap. + /// + /// The key of this map is the highest address in the module and the value + /// is the module's information, which also contains the start address. + ranges: RwLock>, +} + +/// An RAII structure used to unregister a module's frame information when the +/// module is destroyed. +pub struct GlobalFrameInfoRegistration { + /// The key that will be removed from the global `ranges` map when this is + /// dropped. + key: usize, +} + +struct ModuleFrameInfo { + start: usize, + functions: BTreeMap, + names: Arc, +} + +impl GlobalFrameInfo { + /// Registers a new compiled module's frame information. + /// + /// This function will register the `names` information for all of the + /// compiled functions within `module`. If the `module` has no functions + /// then `None` will be returned. Otherwise the returned object, when + /// dropped, will be used to unregister all name information from this map. + pub fn register( + &self, + names: &Arc, + module: &CompiledModule, + ) -> Option { + let mut min = usize::max_value(); + let mut max = 0; + let mut functions = BTreeMap::new(); + for (i, allocated) in module.finished_functions() { + let (start, end) = unsafe { + let ptr = (**allocated).as_ptr(); + let len = (**allocated).len(); + (ptr as usize, ptr as usize + len) + }; + if start < min { + min = start; + } + if end > max { + max = end; + } + let func_index = module.module().local.func_index(i); + assert!(functions.insert(end, (start, func_index)).is_none()); + } + if functions.len() == 0 { + return None; + } + + let mut ranges = self.ranges.write().unwrap(); + // First up assert that our chunk of jit functions doesn't collide with + // any other known chunks of jit functions... + if let Some((_, prev)) = ranges.range(max..).next() { + assert!(prev.start > max); + } + if let Some((prev_end, _)) = ranges.range(..=min).next_back() { + assert!(*prev_end < min); + } + + // ... then insert our range and assert nothing was there previously + let prev = ranges.insert( + max, + ModuleFrameInfo { + start: min, + functions, + names: names.clone(), + }, + ); + assert!(prev.is_none()); + Some(GlobalFrameInfoRegistration { key: max }) + } + + /// Fetches information about a program counter in a backtrace. + /// + /// Returns an object if this `pc` is known to some previously registered + /// module, or returns `None` if no information can be found. + pub fn lookup(&self, pc: usize) -> Option { + let ranges = self.ranges.read().ok()?; + let (end, info) = ranges.range(pc..).next()?; + if pc < info.start || *end < pc { + return None; + } + let (end, (start, func_index)) = info.functions.range(pc..).next()?; + if pc < *start || *end < pc { + return None; + } + Some(FrameInfo { + module_name: info.names.module_name.clone(), + func_index: func_index.index() as u32, + func_name: info.names.module.func_names.get(func_index).cloned(), + }) + } +} + +impl Drop for GlobalFrameInfoRegistration { + fn drop(&mut self) { + if let Ok(mut map) = FRAME_INFO.ranges.write() { + map.remove(&self.key); + } + } +} + +/// Description of a frame in a backtrace for a [`Trap`]. +/// +/// Whenever a WebAssembly trap occurs an instance of [`Trap`] is created. Each +/// [`Trap`] has a backtrace of the WebAssembly frames that led to the trap, and +/// each frame is described by this structure. +#[derive(Debug)] +pub struct FrameInfo { + module_name: Option, + func_index: u32, + func_name: Option, +} + +impl FrameInfo { + /// Returns the WebAssembly function index for this frame. + /// + /// This function index is the index in the function index space of the + /// WebAssembly module that this frame comes from. + pub fn func_index(&self) -> u32 { + self.func_index + } + + /// Returns the identifer of the module that this frame is for. + /// + /// Module identifiers are present in the `name` section of a WebAssembly + /// binary, but this may not return the exact item in the `name` section. + /// Module names can be overwritten at construction time or perhaps inferred + /// from file names. The primary purpose of this function is to assist in + /// debugging and therefore may be tweaked over time. + /// + /// This function returns `None` when no name can be found or inferred. + pub fn module_name(&self) -> Option<&str> { + self.module_name.as_deref() + } + + /// Returns a descriptive name of the function for this frame, if one is + /// available. + /// + /// The name of this function may come from the `name` section of the + /// WebAssembly binary, or wasmtime may try to infer a better name for it if + /// not available, for example the name of the export if it's exported. + /// + /// This return value is primarily used for debugging and human-readable + /// purposes for things like traps. Note that the exact return value may be + /// tweaked over time here and isn't guaranteed to be something in + /// particular about a wasm module due to its primary purpose of assisting + /// in debugging. + /// + /// This function returns `None` when no name could be inferred. + pub fn func_name(&self) -> Option<&str> { + self.func_name.as_deref() + } +} diff --git a/crates/api/src/func.rs b/crates/api/src/func.rs new file mode 100644 index 0000000000..de0255feff --- /dev/null +++ b/crates/api/src/func.rs @@ -0,0 +1,621 @@ +use crate::callable::{NativeCallable, WasmtimeFn, WrappedCallable}; +use crate::{Callable, FuncType, Store, Trap, Val, ValType}; +use anyhow::{ensure, Context as _}; +use std::fmt; +use std::mem; +use std::panic::{self, AssertUnwindSafe}; +use std::ptr; +use std::rc::Rc; +use wasmtime_runtime::{InstanceHandle, VMContext, VMFunctionBody}; + +/// A WebAssembly function which can be called. +/// +/// This type can represent a number of callable items, such as: +/// +/// * An exported function from a WebAssembly module. +/// * A user-defined function used to satisfy an import. +/// +/// These types of callable items are all wrapped up in this `Func` and can be +/// used to both instantiate an [`Instance`](crate::Instance) as well as be +/// extracted from an [`Instance`](crate::Instance). +/// +/// # `Func` and `Clone` +/// +/// Functions are internally reference counted so you can `clone` a `Func`. The +/// cloning process only performs a shallow clone, so two cloned `Func` +/// instances are equivalent in their functionality. +#[derive(Clone)] +pub struct Func { + _store: Store, + callable: Rc, + ty: FuncType, +} + +macro_rules! wrappers { + ($( + $(#[$doc:meta])* + ($name:ident $(,$args:ident)*) + )*) => ($( + $(#[$doc])* + pub fn $name(store: &Store, func: F) -> Func + where + F: Fn($($args),*) -> R + 'static, + $($args: WasmTy,)* + R: WasmRet, + { + #[allow(non_snake_case)] + unsafe extern "C" fn shim( + vmctx: *mut VMContext, + _caller_vmctx: *mut VMContext, + $($args: $args::Abi,)* + ) -> R::Abi + where + F: Fn($($args),*) -> R + 'static, + $($args: WasmTy,)* + R: WasmRet, + { + let ret = { + let instance = InstanceHandle::from_vmctx(vmctx); + let func = instance.host_state().downcast_ref::().expect("state"); + panic::catch_unwind(AssertUnwindSafe(|| { + func($($args::from_abi(_caller_vmctx, $args)),*) + })) + }; + match ret { + Ok(ret) => ret.into_abi(), + Err(panic) => wasmtime_runtime::resume_panic(panic), + } + } + + let mut _args = Vec::new(); + $($args::push(&mut _args);)* + let mut ret = Vec::new(); + R::push(&mut ret); + let ty = FuncType::new(_args.into(), ret.into()); + unsafe { + let (instance, export) = crate::trampoline::generate_raw_func_export( + &ty, + std::slice::from_raw_parts_mut( + shim:: as *mut _, + 0, + ), + store, + Box::new(func), + ) + .expect("failed to generate export"); + let callable = Rc::new(WasmtimeFn::new(store, instance, export)); + Func::from_wrapped(store, ty, callable) + } + } + )*) +} + +macro_rules! getters { + ($( + $(#[$doc:meta])* + ($name:ident $(,$args:ident)*) + )*) => ($( + $(#[$doc])* + #[allow(non_snake_case)] + pub fn $name<$($args,)* R>(&self) + -> anyhow::Result Result> + where + $($args: WasmTy,)* + R: WasmTy, + { + // Verify all the paramers match the expected parameters, and that + // there are no extra parameters... + let mut params = self.ty().params().iter().cloned(); + let n = 0; + $( + let n = n + 1; + $args::matches(&mut params) + .with_context(|| format!("Type mismatch in argument {}", n))?; + )* + ensure!(params.next().is_none(), "Type mismatch: too many arguments (expected {})", n); + + // ... then do the same for the results... + let mut results = self.ty().results().iter().cloned(); + R::matches(&mut results) + .context("Type mismatch in return type")?; + ensure!(results.next().is_none(), "Type mismatch: too many return values (expected 1)"); + + // ... and then once we've passed the typechecks we can hand out our + // object since our `transmute` below should be safe! + let (address, vmctx) = match self.wasmtime_export() { + wasmtime_runtime::Export::Function { address, vmctx, signature: _} => { + (*address, *vmctx) + } + _ => panic!("expected function export"), + }; + Ok(move |$($args: $args),*| -> Result { + unsafe { + let f = mem::transmute::< + *const VMFunctionBody, + unsafe extern "C" fn( + *mut VMContext, + *mut VMContext, + $($args::Abi,)* + ) -> R::Abi, + >(address); + let mut ret = None; + $(let $args = $args.into_abi();)* + wasmtime_runtime::catch_traps(vmctx, || { + ret = Some(f(vmctx, ptr::null_mut(), $($args,)*)); + }).map_err(Trap::from_jit)?; + Ok(R::from_abi(vmctx, ret.unwrap())) + } + }) + } + )*) +} + +impl Func { + /// Creates a new `Func` with the given arguments, typically to create a + /// user-defined function to pass as an import to a module. + /// + /// * `store` - a cache of data where information is stored, typically + /// shared with a [`Module`](crate::Module). + /// + /// * `ty` - the signature of this function, used to indicate what the + /// inputs and outputs are, which must be WebAssembly types. + /// + /// * `callable` - a type implementing the [`Callable`] trait which + /// is the implementation of this `Func` value. + /// + /// Note that the implementation of `callable` must adhere to the `ty` + /// signature given, error or traps may occur if it does not respect the + /// `ty` signature. + pub fn new(store: &Store, ty: FuncType, callable: Rc) -> Self { + let callable = Rc::new(NativeCallable::new(callable, &ty, &store)); + Func::from_wrapped(store, ty, callable) + } + + wrappers! { + /// Creates a new `Func` from the given Rust closure, which takes 0 + /// arguments. + /// + /// For more information about this function, see [`Func::wrap1`]. + (wrap0) + + /// Creates a new `Func` from the given Rust closure, which takes 1 + /// argument. + /// + /// This function will create a new `Func` which, when called, will + /// execute the given Rust closure. Unlike [`Func::new`] the target + /// function being called is known statically so the type signature can + /// be inferred. Rust types will map to WebAssembly types as follows: + /// + /// + /// | Rust Argument Type | WebAssembly Type | + /// |--------------------|------------------| + /// | `i32` | `i32` | + /// | `i64` | `i64` | + /// | `f32` | `f32` | + /// | `f64` | `f64` | + /// | (not supported) | `v128` | + /// | (not supported) | `anyref` | + /// + /// Any of the Rust types can be returned from the closure as well, in + /// addition to some extra types + /// + /// | Rust Return Type | WebAssembly Return Type | Meaning | + /// |-------------------|-------------------------|-------------------| + /// | `()` | nothing | no return value | + /// | `Result` | `T` | function may trap | + /// + /// Note that when using this API (and the related `wrap*` family of + /// functions), the intention is to create as thin of a layer as + /// possible for when WebAssembly calls the function provided. With + /// sufficient inlining and optimization the WebAssembly will call + /// straight into `func` provided, with no extra fluff entailed. + (wrap1, A1) + + /// Creates a new `Func` from the given Rust closure, which takes 2 + /// arguments. + /// + /// For more information about this function, see [`Func::wrap1`]. + (wrap2, A1, A2) + + /// Creates a new `Func` from the given Rust closure, which takes 3 + /// arguments. + /// + /// For more information about this function, see [`Func::wrap1`]. + (wrap3, A1, A2, A3) + + /// Creates a new `Func` from the given Rust closure, which takes 4 + /// arguments. + /// + /// For more information about this function, see [`Func::wrap1`]. + (wrap4, A1, A2, A3, A4) + + /// Creates a new `Func` from the given Rust closure, which takes 5 + /// arguments. + /// + /// For more information about this function, see [`Func::wrap1`]. + (wrap5, A1, A2, A3, A4, A5) + + /// Creates a new `Func` from the given Rust closure, which takes 6 + /// arguments. + /// + /// For more information about this function, see [`Func::wrap1`]. + (wrap6, A1, A2, A3, A4, A5, A6) + + /// Creates a new `Func` from the given Rust closure, which takes 7 + /// arguments. + /// + /// For more information about this function, see [`Func::wrap1`]. + (wrap7, A1, A2, A3, A4, A5, A6, A7) + + /// Creates a new `Func` from the given Rust closure, which takes 8 + /// arguments. + /// + /// For more information about this function, see [`Func::wrap1`]. + (wrap8, A1, A2, A3, A4, A5, A6, A7, A8) + + /// Creates a new `Func` from the given Rust closure, which takes 9 + /// arguments. + /// + /// For more information about this function, see [`Func::wrap1`]. + (wrap9, A1, A2, A3, A4, A5, A6, A7, A8, A9) + + /// Creates a new `Func` from the given Rust closure, which takes 10 + /// arguments. + /// + /// For more information about this function, see [`Func::wrap1`]. + (wrap10, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10) + } + + fn from_wrapped( + store: &Store, + ty: FuncType, + callable: Rc, + ) -> Func { + Func { + _store: store.clone(), + callable, + ty, + } + } + + /// Returns the underlying wasm type that this `Func` has. + pub fn ty(&self) -> &FuncType { + &self.ty + } + + /// Returns the number of parameters that this function takes. + pub fn param_arity(&self) -> usize { + self.ty.params().len() + } + + /// Returns the number of results this function produces. + pub fn result_arity(&self) -> usize { + self.ty.results().len() + } + + /// Invokes this function with the `params` given, returning the results and + /// any trap, if one occurs. + /// + /// The `params` here must match the type signature of this `Func`, or a + /// trap will occur. If a trap occurs while executing this function, then a + /// trap will also be returned. + /// + /// This function should not panic unless the underlying function itself + /// initiates a panic. + pub fn call(&self, params: &[Val]) -> Result, Trap> { + let mut results = vec![Val::null(); self.result_arity()]; + self.callable.call(params, &mut results)?; + Ok(results.into_boxed_slice()) + } + + pub(crate) fn wasmtime_export(&self) -> &wasmtime_runtime::Export { + self.callable.wasmtime_export() + } + + pub(crate) fn from_wasmtime_function( + export: wasmtime_runtime::Export, + store: &Store, + instance_handle: InstanceHandle, + ) -> Self { + // This is only called with `Export::Function`, and since it's coming + // from wasmtime_runtime itself we should support all the types coming + // out of it, so assert such here. + let ty = if let wasmtime_runtime::Export::Function { signature, .. } = &export { + FuncType::from_wasmtime_signature(signature.clone()) + .expect("core wasm signature should be supported") + } else { + panic!("expected function export") + }; + let callable = WasmtimeFn::new(store, instance_handle, export); + Func::from_wrapped(store, ty, Rc::new(callable)) + } + + getters! { + /// Extracts a natively-callable object from this `Func`, if the + /// signature matches. + /// + /// See the [`Func::get1`] method for more documentation. + (get0) + + /// Extracts a natively-callable object from this `Func`, if the + /// signature matches. + /// + /// This function serves as an optimized version of the [`Func::call`] + /// method if the type signature of a function is statically known to + /// the program. This method is faster than `call` on a few metrics: + /// + /// * Runtime type-checking only happens once, when this method is + /// called. + /// * The result values, if any, aren't boxed into a vector. + /// * Arguments and return values don't go through boxing and unboxing. + /// * No trampolines are used to transfer control flow to/from JIT code, + /// instead this function jumps directly into JIT code. + /// + /// For more information about which Rust types match up to which wasm + /// types, see the documentation on [`Func::wrap1`]. + /// + /// # Return + /// + /// This function will return `None` if the type signature asserted + /// statically does not match the runtime type signature. `Some`, + /// however, will be returned if the underlying function takes one + /// parameter of type `A` and returns the parameter `R`. Currently `R` + /// can either be `()` (no return values) or one wasm type. At this time + /// a multi-value return isn't supported. + /// + /// The returned closure will always return a `Result` and an + /// `Err` is returned if a trap happens while the wasm is executing. + (get1, A1) + + /// Extracts a natively-callable object from this `Func`, if the + /// signature matches. + /// + /// See the [`Func::get1`] method for more documentation. + (get2, A1, A2) + + /// Extracts a natively-callable object from this `Func`, if the + /// signature matches. + /// + /// See the [`Func::get1`] method for more documentation. + (get3, A1, A2, A3) + + /// Extracts a natively-callable object from this `Func`, if the + /// signature matches. + /// + /// See the [`Func::get1`] method for more documentation. + (get4, A1, A2, A3, A4) + + /// Extracts a natively-callable object from this `Func`, if the + /// signature matches. + /// + /// See the [`Func::get1`] method for more documentation. + (get5, A1, A2, A3, A4, A5) + + /// Extracts a natively-callable object from this `Func`, if the + /// signature matches. + /// + /// See the [`Func::get1`] method for more documentation. + (get6, A1, A2, A3, A4, A5, A6) + + /// Extracts a natively-callable object from this `Func`, if the + /// signature matches. + /// + /// See the [`Func::get1`] method for more documentation. + (get7, A1, A2, A3, A4, A5, A6, A7) + + /// Extracts a natively-callable object from this `Func`, if the + /// signature matches. + /// + /// See the [`Func::get1`] method for more documentation. + (get8, A1, A2, A3, A4, A5, A6, A7, A8) + + /// Extracts a natively-callable object from this `Func`, if the + /// signature matches. + /// + /// See the [`Func::get1`] method for more documentation. + (get9, A1, A2, A3, A4, A5, A6, A7, A8, A9) + + /// Extracts a natively-callable object from this `Func`, if the + /// signature matches. + /// + /// See the [`Func::get1`] method for more documentation. + (get10, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10) + } +} + +impl fmt::Debug for Func { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Func") + } +} + +/// A trait implemented for types which can be arguments to closures passed to +/// [`Func::wrap1`] and friends. +/// +/// This trait should not be implemented by user types. This trait may change at +/// any time internally. The types which implement this trait, however, are +/// stable over time. +/// +/// For more information see [`Func::wrap1`] +pub trait WasmTy { + #[doc(hidden)] + type Abi: Copy; + #[doc(hidden)] + fn push(dst: &mut Vec); + #[doc(hidden)] + fn matches(tys: impl Iterator) -> anyhow::Result<()>; + #[doc(hidden)] + fn from_abi(vmctx: *mut VMContext, abi: Self::Abi) -> Self; + #[doc(hidden)] + fn into_abi(self) -> Self::Abi; +} + +impl WasmTy for () { + type Abi = (); + fn push(_dst: &mut Vec) {} + fn matches(_tys: impl Iterator) -> anyhow::Result<()> { + Ok(()) + } + #[inline] + fn from_abi(_vmctx: *mut VMContext, abi: Self::Abi) -> Self { + abi + } + #[inline] + fn into_abi(self) -> Self::Abi { + self + } +} + +impl WasmTy for i32 { + type Abi = Self; + fn push(dst: &mut Vec) { + dst.push(ValType::I32); + } + fn matches(mut tys: impl Iterator) -> anyhow::Result<()> { + let next = tys.next(); + ensure!( + next == Some(ValType::I32), + "Type mismatch, expected i32, got {:?}", + next + ); + Ok(()) + } + #[inline] + fn from_abi(_vmctx: *mut VMContext, abi: Self::Abi) -> Self { + abi + } + #[inline] + fn into_abi(self) -> Self::Abi { + self + } +} + +impl WasmTy for i64 { + type Abi = Self; + fn push(dst: &mut Vec) { + dst.push(ValType::I64); + } + fn matches(mut tys: impl Iterator) -> anyhow::Result<()> { + let next = tys.next(); + ensure!( + next == Some(ValType::I64), + "Type mismatch, expected i64, got {:?}", + next + ); + Ok(()) + } + #[inline] + fn from_abi(_vmctx: *mut VMContext, abi: Self::Abi) -> Self { + abi + } + #[inline] + fn into_abi(self) -> Self::Abi { + self + } +} + +impl WasmTy for f32 { + type Abi = Self; + fn push(dst: &mut Vec) { + dst.push(ValType::F32); + } + fn matches(mut tys: impl Iterator) -> anyhow::Result<()> { + let next = tys.next(); + ensure!( + next == Some(ValType::F32), + "Type mismatch, expected f32, got {:?}", + next + ); + Ok(()) + } + #[inline] + fn from_abi(_vmctx: *mut VMContext, abi: Self::Abi) -> Self { + abi + } + #[inline] + fn into_abi(self) -> Self::Abi { + self + } +} + +impl WasmTy for f64 { + type Abi = Self; + fn push(dst: &mut Vec) { + dst.push(ValType::F64); + } + fn matches(mut tys: impl Iterator) -> anyhow::Result<()> { + let next = tys.next(); + ensure!( + next == Some(ValType::F64), + "Type mismatch, expected f64, got {:?}", + next + ); + Ok(()) + } + #[inline] + fn from_abi(_vmctx: *mut VMContext, abi: Self::Abi) -> Self { + abi + } + #[inline] + fn into_abi(self) -> Self::Abi { + self + } +} + +/// A trait implemented for types which can be returned from closures passed to +/// [`Func::wrap1`] and friends. +/// +/// This trait should not be implemented by user types. This trait may change at +/// any time internally. The types which implement this trait, however, are +/// stable over time. +/// +/// For more information see [`Func::wrap1`] +pub trait WasmRet { + #[doc(hidden)] + type Abi; + #[doc(hidden)] + fn push(dst: &mut Vec); + #[doc(hidden)] + fn matches(tys: impl Iterator) -> anyhow::Result<()>; + #[doc(hidden)] + fn into_abi(self) -> Self::Abi; +} + +impl WasmRet for T { + type Abi = T::Abi; + fn push(dst: &mut Vec) { + T::push(dst) + } + + fn matches(tys: impl Iterator) -> anyhow::Result<()> { + T::matches(tys) + } + + #[inline] + fn into_abi(self) -> Self::Abi { + T::into_abi(self) + } +} + +impl WasmRet for Result { + type Abi = T::Abi; + fn push(dst: &mut Vec) { + T::push(dst) + } + + fn matches(tys: impl Iterator) -> anyhow::Result<()> { + T::matches(tys) + } + + #[inline] + fn into_abi(self) -> Self::Abi { + match self { + Ok(val) => return val.into_abi(), + Err(trap) => handle_trap(trap), + } + + fn handle_trap(trap: Trap) -> ! { + unsafe { wasmtime_runtime::raise_user_trap(Box::new(trap)) } + } + } +} diff --git a/crates/api/src/instance.rs b/crates/api/src/instance.rs new file mode 100644 index 0000000000..7d0b26260a --- /dev/null +++ b/crates/api/src/instance.rs @@ -0,0 +1,186 @@ +use crate::externals::Extern; +use crate::module::Module; +use crate::runtime::{Config, Store}; +use crate::trap::Trap; +use anyhow::{Error, Result}; +use wasmtime_jit::{CompiledModule, Resolver}; +use wasmtime_runtime::{Export, InstanceHandle, InstantiationError}; + +struct SimpleResolver<'a> { + imports: &'a [Extern], +} + +impl Resolver for SimpleResolver<'_> { + fn resolve(&mut self, idx: u32, _name: &str, _field: &str) -> Option { + self.imports + .get(idx as usize) + .map(|i| i.get_wasmtime_export()) + } +} + +fn instantiate( + config: &Config, + compiled_module: &CompiledModule, + imports: &[Extern], +) -> Result { + let mut resolver = SimpleResolver { imports }; + unsafe { + let instance = compiled_module + .instantiate( + config.validating_config.operator_config.enable_bulk_memory, + &mut resolver, + ) + .map_err(|e| -> Error { + match e { + InstantiationError::StartTrap(trap) | InstantiationError::Trap(trap) => { + Trap::from_jit(trap).into() + } + other => other.into(), + } + })?; + Ok(instance) + } +} + +/// An instantiated WebAssembly module. +/// +/// This type represents the instantiation of a [`Module`]. Once instantiated +/// you can access the [`exports`](Instance::exports) which are of type +/// [`Extern`] and provide the ability to call functions, set globals, read +/// memory, etc. This is where all the fun stuff happens! +/// +/// An [`Instance`] is created from two inputs, a [`Module`] and a list of +/// imports, provided as a list of [`Extern`] values. The [`Module`] is the wasm +/// code that was compiled and we're instantiating, and the [`Extern`] imports +/// are how we're satisfying the imports of the module provided. On successful +/// instantiation an [`Instance`] will automatically invoke the wasm `start` +/// function. +/// +/// When interacting with any wasm code you'll want to make an [`Instance`] to +/// call any code or execute anything! +#[derive(Clone)] +pub struct Instance { + pub(crate) instance_handle: InstanceHandle, + module: Module, + exports: Box<[Extern]>, +} + +impl Instance { + /// Creates a new [`Instance`] from the previously compiled [`Module`] and + /// list of `imports` specified. + /// + /// This method instantiates the `module` provided with the `imports`, + /// following the procedure in the [core specification][inst] to + /// instantiate. Instantiation can fail for a number of reasons (many + /// specified below), but if successful the `start` function will be + /// automatically run (if provided) and then the [`Instance`] will be + /// returned. + /// + /// ## Providing Imports + /// + /// The `imports` array here is a bit tricky. The entries in the list of + /// `imports` are intended to correspond 1:1 with the list of imports + /// returned by [`Module::imports`]. Before calling [`Instance::new`] you'll + /// want to inspect the return value of [`Module::imports`] and, for each + /// import type, create an [`Extern`] which corresponds to that type. + /// These [`Extern`] values are all then collected into a list and passed to + /// this function. + /// + /// Note that this function is intentionally relatively low level. It is the + /// intention that we'll soon provide a [higher level API][issue] which will + /// be much more ergonomic for instantiating modules. If you need the full + /// power of customization of imports, though, this is the method for you! + /// + /// ## Errors + /// + /// This function can fail for a number of reasons, including, but not + /// limited to: + /// + /// * The number of `imports` provided doesn't match the number of imports + /// returned by the `module`'s [`Module::imports`] method. + /// * The type of any [`Extern`] doesn't match the corresponding + /// [`ExternType`] entry that it maps to. + /// * The `start` function in the instance, if present, traps. + /// * Module/instance resource limits are exceeded. + /// + /// When instantiation fails it's recommended to inspect the return value to + /// see why it failed, or bubble it upwards. If you'd like to specifically + /// check for trap errors, you can use `error.downcast::()`. + /// + /// [inst]: https://webassembly.github.io/spec/core/exec/modules.html#exec-instantiation + /// [issue]: https://github.com/bytecodealliance/wasmtime/issues/727 + pub fn new(module: &Module, imports: &[Extern]) -> Result { + let store = module.store(); + let config = store.engine().config(); + let instance_handle = instantiate(config, module.compiled_module(), imports)?; + + let exports = { + let mut exports = Vec::with_capacity(module.exports().len()); + for export in module.exports() { + let name = export.name().to_string(); + let export = instance_handle.lookup(&name).expect("export"); + exports.push(Extern::from_wasmtime_export( + store, + instance_handle.clone(), + export, + )); + } + exports.into_boxed_slice() + }; + module.register_frame_info(); + Ok(Instance { + instance_handle, + module: module.clone(), + exports, + }) + } + + /// Returns the associated [`Store`] that this `Instance` is compiled into. + /// + /// This is the [`Store`] that generally serves as a sort of global cache + /// for various instance-related things. + pub fn store(&self) -> &Store { + self.module.store() + } + + /// Returns the associated [`Module`] that this `Instance` instantiated. + /// + /// The corresponding [`Module`] here is a static version of this `Instance` + /// which can be used to learn information such as naming information about + /// various functions. + pub fn module(&self) -> &Module { + &self.module + } + + /// Returns the list of exported items from this [`Instance`]. + /// + /// Note that the exports here do not have names associated with them, + /// they're simply the values that are exported. To learn the value of each + /// export you'll need to consult [`Module::exports`]. The list returned + /// here maps 1:1 with the list that [`Module::exports`] returns, and + /// [`ExportType`] contains the name of each export. + pub fn exports(&self) -> &[Extern] { + &self.exports + } + + /// Looks up an exported [`Extern`] value by name. + /// + /// This method will search the module for an export named `name` and return + /// the value, if found. + /// + /// Returns `None` if there was no export named `name`. + pub fn get_export(&self, name: &str) -> Option<&Extern> { + let (i, _) = self + .module + .exports() + .iter() + .enumerate() + .find(|(_, e)| e.name() == name)?; + Some(&self.exports()[i]) + } + + #[doc(hidden)] + pub fn handle(&self) -> &InstanceHandle { + &self.instance_handle + } +} diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs new file mode 100644 index 0000000000..4e5bce5b6c --- /dev/null +++ b/crates/api/src/lib.rs @@ -0,0 +1,44 @@ +//! Wasmtime's embedding API +//! +//! This crate contains a high-level API used to interact with WebAssembly +//! modules. The API here is intended to mirror the proposed [WebAssembly C +//! API](https://github.com/WebAssembly/wasm-c-api), with small extensions here +//! and there to implement Rust idioms. This crate also defines the actual C API +//! itself for consumption from other languages. + +#![deny(missing_docs)] + +mod callable; +mod externals; +mod frame_info; +mod func; +mod instance; +mod module; +mod r#ref; +mod runtime; +mod trampoline; +mod trap; +mod types; +mod values; + +pub use crate::callable::Callable; +pub use crate::externals::*; +pub use crate::frame_info::FrameInfo; +pub use crate::func::{Func, WasmRet, WasmTy}; +pub use crate::instance::Instance; +pub use crate::module::Module; +pub use crate::r#ref::{AnyRef, HostInfo, HostRef}; +pub use crate::runtime::{Config, Engine, OptLevel, Store, Strategy}; +pub use crate::trap::Trap; +pub use crate::types::*; +pub use crate::values::*; + +cfg_if::cfg_if! { + if #[cfg(unix)] { + pub mod unix; + } else if #[cfg(windows)] { + pub mod windows; + } else { + // ... unknown os! + } +} diff --git a/crates/api/src/module.rs b/crates/api/src/module.rs new file mode 100644 index 0000000000..dd463d2fc5 --- /dev/null +++ b/crates/api/src/module.rs @@ -0,0 +1,442 @@ +use crate::frame_info::{GlobalFrameInfoRegistration, FRAME_INFO}; +use crate::runtime::Store; +use crate::types::{ + ExportType, ExternType, FuncType, GlobalType, ImportType, Limits, MemoryType, Mutability, + TableType, ValType, +}; +use anyhow::{bail, Error, Result}; +use std::path::Path; +use std::sync::{Arc, Mutex}; +use wasmparser::{ + validate, CustomSectionKind, ExternalKind, ImportSectionEntryType, ModuleReader, Name, + SectionCode, +}; +use wasmtime_jit::CompiledModule; + +fn into_memory_type(mt: wasmparser::MemoryType) -> Result { + if mt.shared { + bail!("shared memories are not supported yet"); + } + Ok(MemoryType::new(Limits::new( + mt.limits.initial, + mt.limits.maximum, + ))) +} + +fn into_global_type(gt: wasmparser::GlobalType) -> GlobalType { + let mutability = if gt.mutable { + Mutability::Var + } else { + Mutability::Const + }; + GlobalType::new(into_valtype(>.content_type), mutability) +} + +// `into_valtype` is used for `map` which requires `&T`. +#[allow(clippy::trivially_copy_pass_by_ref)] +fn into_valtype(ty: &wasmparser::Type) -> ValType { + use wasmparser::Type::*; + match ty { + I32 => ValType::I32, + I64 => ValType::I64, + F32 => ValType::F32, + F64 => ValType::F64, + V128 => ValType::V128, + AnyFunc => ValType::FuncRef, + AnyRef => ValType::AnyRef, + _ => unimplemented!("types in into_valtype"), + } +} + +fn into_func_type(mt: wasmparser::FuncType) -> FuncType { + assert_eq!(mt.form, wasmparser::Type::Func); + let params = mt.params.iter().map(into_valtype).collect::>(); + let returns = mt.returns.iter().map(into_valtype).collect::>(); + FuncType::new(params.into_boxed_slice(), returns.into_boxed_slice()) +} + +fn into_table_type(tt: wasmparser::TableType) -> TableType { + assert!( + tt.element_type == wasmparser::Type::AnyFunc || tt.element_type == wasmparser::Type::AnyRef + ); + let ty = into_valtype(&tt.element_type); + let limits = Limits::new(tt.limits.initial, tt.limits.maximum); + TableType::new(ty, limits) +} + +/// A compiled WebAssembly module, ready to be instantiated. +/// +/// A `Module` is a compiled in-memory representation of an input WebAssembly +/// binary. A `Module` is then used to create an [`Instance`](crate::Instance) +/// through an instantiation process. You cannot call functions or fetch +/// globals, for example, on a `Module` because it's purely a code +/// representation. Instead you'll need to create an +/// [`Instance`](crate::Instance) to interact with the wasm module. +/// +/// ## Modules and `Clone` +/// +/// Using `clone` on a `Module` is a cheap operation. It will not create an +/// entirely new module, but rather just a new reference to the existing module. +/// In other words it's a shallow copy, not a deep copy. +#[derive(Clone)] +pub struct Module { + inner: Arc, +} + +struct ModuleInner { + store: Store, + imports: Box<[ImportType]>, + exports: Box<[ExportType]>, + compiled: CompiledModule, + frame_info_registration: Mutex>>, + names: Arc, +} + +pub struct Names { + pub module: Arc, + pub module_name: Option, +} + +impl Module { + /// Creates a new WebAssembly `Module` from the given in-memory `bytes`. + /// + /// The `bytes` provided must be in one of two formats: + /// + /// * It can be a [binary-encoded][binary] WebAssembly module. This + /// is always supported. + /// * It may also be a [text-encoded][text] instance of the WebAssembly + /// text format. This is only supported when the `wat` feature of this + /// crate is enabled. If this is supplied then the text format will be + /// parsed before validation. Note that the `wat` feature is enabled by + /// default. + /// + /// The data for the wasm module must be loaded in-memory if it's present + /// elsewhere, for example on disk. This requires that the entire binary is + /// loaded into memory all at once, this API does not support streaming + /// compilation of a module. + /// + /// The WebAssembly binary will be decoded and validated. It will also be + /// compiled according to the configuration of the provided `store` and + /// cached in this type. + /// + /// The provided `store` is a global cache for compiled resources as well as + /// configuration for what wasm features are enabled. It's recommended to + /// share a `store` among modules if possible. + /// + /// # Errors + /// + /// This function may fail and return an error. Errors may include + /// situations such as: + /// + /// * The binary provided could not be decoded because it's not a valid + /// WebAssembly binary + /// * The WebAssembly binary may not validate (e.g. contains type errors) + /// * Implementation-specific limits were exceeded with a valid binary (for + /// example too many locals) + /// * The wasm binary may use features that are not enabled in the + /// configuration of `store` + /// * If the `wat` feature is enabled and the input is text, then it may be + /// rejected if it fails to parse. + /// + /// The error returned should contain full information about why module + /// creation failed if one is returned. + /// + /// [binary]: https://webassembly.github.io/spec/core/binary/index.html + /// [text]: https://webassembly.github.io/spec/core/text/index.html + pub fn new(store: &Store, bytes: impl AsRef<[u8]>) -> Result { + #[cfg(feature = "wat")] + let bytes = wat::parse_bytes(bytes.as_ref())?; + Module::from_binary(store, bytes.as_ref()) + } + + /// Creates a new WebAssembly `Module` from the given in-memory `binary` + /// data. The provided `name` will be used in traps/backtrace details. + /// + /// See [`Module::new`] for other details. + pub fn new_with_name(store: &Store, bytes: impl AsRef<[u8]>, name: &str) -> Result { + let mut module = Module::new(store, bytes.as_ref())?; + let inner = Arc::get_mut(&mut module.inner).unwrap(); + Arc::get_mut(&mut inner.names).unwrap().module_name = Some(name.to_string()); + Ok(module) + } + + /// Creates a new WebAssembly `Module` from the contents of the given + /// `file` on disk. + /// + /// This is a convenience function that will read the `file` provided and + /// pass the bytes to the [`Module::new`] function. For more information + /// see [`Module::new`] + pub fn from_file(store: &Store, file: impl AsRef) -> Result { + #[cfg(feature = "wat")] + let wasm = wat::parse_file(file)?; + #[cfg(not(feature = "wat"))] + let wasm = std::fs::read(file)?; + Module::new(store, &wasm) + } + + /// Creates a new WebAssembly `Module` from the given in-memory `binary` + /// data. + /// + /// This is similar to [`Module::new`] except that it requires that the + /// `binary` input is a WebAssembly binary, the text format is not supported + /// by this function. It's generally recommended to use [`Module::new`], + /// but if it's required to not support the text format this function can be + /// used instead. + pub fn from_binary(store: &Store, binary: &[u8]) -> Result { + Module::validate(store, binary)?; + // Note that the call to `from_binary_unchecked` here should be ok + // because we previously validated the binary, meaning we're guaranteed + // to pass a valid binary for `store`. + unsafe { Module::from_binary_unchecked(store, binary) } + } + + /// Creates a new WebAssembly `Module` from the given in-memory `binary` + /// data, skipping validation and asserting that `binary` is a valid + /// WebAssembly module. + /// + /// This function is the same as [`Module::new`] except that it skips the + /// call to [`Module::validate`] and it does not support the text format of + /// WebAssembly. The WebAssembly binary is not validated for + /// correctness and it is simply assumed as valid. + /// + /// For more information about creation of a module and the `store` argument + /// see the documentation of [`Module::new`]. + /// + /// # Unsafety + /// + /// This function is `unsafe` due to the unchecked assumption that the input + /// `binary` is valid. If the `binary` is not actually a valid wasm binary it + /// may cause invalid machine code to get generated, cause panics, etc. + /// + /// It is only safe to call this method if [`Module::validate`] succeeds on + /// the same arguments passed to this function. + /// + /// # Errors + /// + /// This function may fail for many of the same reasons as [`Module::new`]. + /// While this assumes that the binary is valid it still needs to actually + /// be somewhat valid for decoding purposes, and the basics of decoding can + /// still fail. + pub unsafe fn from_binary_unchecked(store: &Store, binary: &[u8]) -> Result { + let mut ret = Module::compile(store, binary)?; + ret.read_imports_and_exports(binary)?; + Ok(ret) + } + + /// Validates `binary` input data as a WebAssembly binary given the + /// configuration in `store`. + /// + /// This function will perform a speedy validation of the `binary` input + /// WebAssembly module (which is in [binary form][binary], the text format + /// is not accepted by this function) and return either `Ok` or `Err` + /// depending on the results of validation. The `store` argument indicates + /// configuration for WebAssembly features, for example, which are used to + /// indicate what should be valid and what shouldn't be. + /// + /// Validation automatically happens as part of [`Module::new`], but is a + /// requirement for [`Module::new_unchecked`] to be safe. + /// + /// # Errors + /// + /// If validation fails for any reason (type check error, usage of a feature + /// that wasn't enabled, etc) then an error with a description of the + /// validation issue will be returned. + /// + /// [binary]: https://webassembly.github.io/spec/core/binary/index.html + pub fn validate(store: &Store, binary: &[u8]) -> Result<()> { + let config = store.engine().config().validating_config.clone(); + validate(binary, Some(config)).map_err(Error::new) + } + + unsafe fn compile(store: &Store, binary: &[u8]) -> Result { + let compiled = CompiledModule::new( + &mut store.compiler_mut(), + binary, + store.engine().config().debug_info, + store.engine().config().profiler.as_ref(), + )?; + + let names = Arc::new(Names { + module_name: None, + module: compiled.module().clone(), + }); + Ok(Module { + inner: Arc::new(ModuleInner { + store: store.clone(), + imports: Box::new([]), + exports: Box::new([]), + names, + compiled, + frame_info_registration: Mutex::new(None), + }), + }) + } + + pub(crate) fn compiled_module(&self) -> &CompiledModule { + &self.inner.compiled + } + + /// Returns identifier/name that this [`Module`] has. This name + /// is used in traps/backtrace details. + pub fn name(&self) -> Option<&str> { + self.inner.names.module_name.as_deref() + } + + /// Returns the list of imports that this [`Module`] has and must be + /// satisfied. + pub fn imports(&self) -> &[ImportType] { + &self.inner.imports + } + + /// Returns the list of exports that this [`Module`] has and will be + /// available after instantiation. + pub fn exports(&self) -> &[ExportType] { + &self.inner.exports + } + + /// Returns the [`Store`] that this [`Module`] was compiled into. + pub fn store(&self) -> &Store { + &self.inner.store + } + + fn read_imports_and_exports(&mut self, binary: &[u8]) -> Result<()> { + let inner = Arc::get_mut(&mut self.inner).unwrap(); + let mut reader = ModuleReader::new(binary)?; + let mut imports = Vec::new(); + let mut exports = Vec::new(); + let mut memories = Vec::new(); + let mut tables = Vec::new(); + let mut func_sig = Vec::new(); + let mut sigs = Vec::new(); + let mut globals = Vec::new(); + while !reader.eof() { + let section = reader.read()?; + match section.code { + SectionCode::Memory => { + let section = section.get_memory_section_reader()?; + memories.reserve_exact(section.get_count() as usize); + for entry in section { + memories.push(into_memory_type(entry?)?); + } + } + SectionCode::Type => { + let section = section.get_type_section_reader()?; + sigs.reserve_exact(section.get_count() as usize); + for entry in section { + sigs.push(into_func_type(entry?)); + } + } + SectionCode::Function => { + let section = section.get_function_section_reader()?; + func_sig.reserve_exact(section.get_count() as usize); + for entry in section { + func_sig.push(entry?); + } + } + SectionCode::Global => { + let section = section.get_global_section_reader()?; + globals.reserve_exact(section.get_count() as usize); + for entry in section { + globals.push(into_global_type(entry?.ty)); + } + } + SectionCode::Table => { + let section = section.get_table_section_reader()?; + tables.reserve_exact(section.get_count() as usize); + for entry in section { + tables.push(into_table_type(entry?)) + } + } + SectionCode::Import => { + let section = section.get_import_section_reader()?; + imports.reserve_exact(section.get_count() as usize); + for entry in section { + let entry = entry?; + let r#type = match entry.ty { + ImportSectionEntryType::Function(index) => { + func_sig.push(index); + let sig = &sigs[index as usize]; + ExternType::Func(sig.clone()) + } + ImportSectionEntryType::Table(tt) => { + let table = into_table_type(tt); + tables.push(table.clone()); + ExternType::Table(table) + } + ImportSectionEntryType::Memory(mt) => { + let memory = into_memory_type(mt)?; + memories.push(memory.clone()); + ExternType::Memory(memory) + } + ImportSectionEntryType::Global(gt) => { + let global = into_global_type(gt); + globals.push(global.clone()); + ExternType::Global(global) + } + }; + imports.push(ImportType::new(entry.module, entry.field, r#type)); + } + } + SectionCode::Export => { + let section = section.get_export_section_reader()?; + exports.reserve_exact(section.get_count() as usize); + for entry in section { + let entry = entry?; + let r#type = match entry.kind { + ExternalKind::Function => { + let sig_index = func_sig[entry.index as usize] as usize; + let sig = &sigs[sig_index]; + ExternType::Func(sig.clone()) + } + ExternalKind::Table => { + ExternType::Table(tables[entry.index as usize].clone()) + } + ExternalKind::Memory => { + ExternType::Memory(memories[entry.index as usize].clone()) + } + ExternalKind::Global => { + ExternType::Global(globals[entry.index as usize].clone()) + } + }; + exports.push(ExportType::new(entry.field, r#type)); + } + } + SectionCode::Custom { + kind: CustomSectionKind::Name, + .. + } => { + // Read name section. Per spec, ignore invalid custom section. + if let Ok(mut reader) = section.get_name_section_reader() { + while let Ok(entry) = reader.read() { + if let Name::Module(name) = entry { + if let Ok(name) = name.get_name() { + Arc::get_mut(&mut inner.names).unwrap().module_name = + Some(name.to_string()); + } + break; + } + } + } + } + _ => { + // skip other sections + } + } + } + + inner.imports = imports.into(); + inner.exports = exports.into(); + Ok(()) + } + + /// Register this module's stack frame information into the global scope. + /// + /// This is required to ensure that any traps can be properly symbolicated. + pub(crate) fn register_frame_info(&self) { + let mut info = self.inner.frame_info_registration.lock().unwrap(); + if info.is_some() { + return; + } + *info = Some(FRAME_INFO.register(&self.inner.names, &self.inner.compiled)); + } +} diff --git a/crates/api/src/ref.rs b/crates/api/src/ref.rs new file mode 100644 index 0000000000..f5dbd2ee42 --- /dev/null +++ b/crates/api/src/ref.rs @@ -0,0 +1,243 @@ +#![allow(missing_docs)] + +use std::any::Any; +use std::cell::{self, RefCell}; +use std::fmt; +use std::rc::{Rc, Weak}; + +pub trait HostInfo { + fn finalize(&mut self) {} +} + +trait InternalRefBase: Any { + fn as_any(&self) -> &dyn Any; + fn host_info(&self) -> Option>>; + fn set_host_info(&self, info: Option>); + fn ptr_eq(&self, other: &dyn InternalRefBase) -> bool; +} + +#[derive(Clone)] +pub struct InternalRef(Rc); + +impl InternalRef { + pub fn is_ref(&self) -> bool { + let r = self.0.as_any(); + Any::is::>(r) + } + pub fn get_ref(&self) -> HostRef { + let r = self.0.as_any(); + r.downcast_ref::>() + .expect("reference is not T type") + .clone() + } +} + +struct AnyAndHostInfo { + any: Box, + host_info: Option>, +} + +impl Drop for AnyAndHostInfo { + fn drop(&mut self) { + if let Some(info) = &mut self.host_info { + info.finalize(); + } + } +} + +#[derive(Clone)] +pub struct OtherRef(Rc>); + +/// Represents an opaque reference to any data within WebAssembly. +#[derive(Clone)] +pub enum AnyRef { + /// A reference to no data. + Null, + /// A reference to data stored internally in `wasmtime`. + Ref(InternalRef), + /// A reference to data located outside of `wasmtime`. + Other(OtherRef), +} + +impl AnyRef { + /// Creates a new instance of `AnyRef` from `Box`. + pub fn new(data: Box) -> Self { + let info = AnyAndHostInfo { + any: data, + host_info: None, + }; + AnyRef::Other(OtherRef(Rc::new(RefCell::new(info)))) + } + + /// Creates a `Null` reference. + pub fn null() -> Self { + AnyRef::Null + } + + /// Returns the data stored in the reference if available. + /// # Panics + /// Panics if the variant isn't `AnyRef::Other`. + pub fn data(&self) -> cell::Ref> { + match self { + AnyRef::Other(OtherRef(r)) => cell::Ref::map(r.borrow(), |r| &r.any), + _ => panic!("expected AnyRef::Other"), + } + } + + /// Returns true if the two `AnyRef`'s point to the same value (not just + /// values that compare as equal). + pub fn ptr_eq(&self, other: &AnyRef) -> bool { + match (self, other) { + (AnyRef::Null, AnyRef::Null) => true, + (AnyRef::Ref(InternalRef(ref a)), AnyRef::Ref(InternalRef(ref b))) => { + a.ptr_eq(b.as_ref()) + } + (AnyRef::Other(OtherRef(ref a)), AnyRef::Other(OtherRef(ref b))) => Rc::ptr_eq(a, b), + _ => false, + } + } + + /// Returns a mutable reference to the host information if available. + /// # Panics + /// Panics if `AnyRef` is already borrowed or `AnyRef` is `Null`. + pub fn host_info(&self) -> Option>> { + match self { + AnyRef::Null => panic!("null"), + AnyRef::Ref(r) => r.0.host_info(), + AnyRef::Other(r) => { + let info = cell::RefMut::map(r.0.borrow_mut(), |b| &mut b.host_info); + if info.is_none() { + return None; + } + Some(cell::RefMut::map(info, |info| info.as_mut().unwrap())) + } + } + } + + /// Sets the host information for an `AnyRef`. + /// # Panics + /// Panics if `AnyRef` is already borrowed or `AnyRef` is `Null`. + pub fn set_host_info(&self, info: Option>) { + match self { + AnyRef::Null => panic!("null"), + AnyRef::Ref(r) => r.0.set_host_info(info), + AnyRef::Other(r) => { + r.0.borrow_mut().host_info = info; + } + } + } +} + +impl fmt::Debug for AnyRef { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AnyRef::Null => write!(f, "null"), + AnyRef::Ref(_) => write!(f, "anyref"), + AnyRef::Other(_) => write!(f, "other ref"), + } + } +} + +struct ContentBox { + content: T, + host_info: Option>, + anyref_data: Weak, +} + +impl Drop for ContentBox { + fn drop(&mut self) { + if let Some(info) = &mut self.host_info { + info.finalize(); + } + } +} + +/// Represents a piece of data located in the host environment. +pub struct HostRef(Rc>>); + +impl HostRef { + /// Creates a new `HostRef` from `T`. + pub fn new(item: T) -> HostRef { + let anyref_data: Weak> = Weak::new(); + let content = ContentBox { + content: item, + host_info: None, + anyref_data, + }; + HostRef(Rc::new(RefCell::new(content))) + } + + /// Immutably borrows the wrapped data. + /// # Panics + /// Panics if the value is currently mutably borrowed. + pub fn borrow(&self) -> cell::Ref { + cell::Ref::map(self.0.borrow(), |b| &b.content) + } + + /// Mutably borrows the wrapped data. + /// # Panics + /// Panics if the `HostRef` is already borrowed. + pub fn borrow_mut(&self) -> cell::RefMut { + cell::RefMut::map(self.0.borrow_mut(), |b| &mut b.content) + } + + /// Returns true if the two `HostRef`'s point to the same value (not just + /// values that compare as equal). + pub fn ptr_eq(&self, other: &HostRef) -> bool { + Rc::ptr_eq(&self.0, &other.0) + } + + /// Returns an opaque reference to the wrapped data in the form of + /// an `AnyRef`. + /// # Panics + /// Panics if `HostRef` is already mutably borrowed. + pub fn anyref(&self) -> AnyRef { + let r = self.0.borrow_mut().anyref_data.upgrade(); + if let Some(r) = r { + return AnyRef::Ref(InternalRef(r)); + } + let anyref_data: Rc = Rc::new(self.clone()); + self.0.borrow_mut().anyref_data = Rc::downgrade(&anyref_data); + AnyRef::Ref(InternalRef(anyref_data)) + } +} + +impl InternalRefBase for HostRef { + fn ptr_eq(&self, other: &dyn InternalRefBase) -> bool { + if let Some(other) = other.as_any().downcast_ref() { + self.ptr_eq(other) + } else { + false + } + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn host_info(&self) -> Option>> { + let info = cell::RefMut::map(self.0.borrow_mut(), |b| &mut b.host_info); + if info.is_none() { + return None; + } + Some(cell::RefMut::map(info, |info| info.as_mut().unwrap())) + } + + fn set_host_info(&self, info: Option>) { + self.0.borrow_mut().host_info = info; + } +} + +impl Clone for HostRef { + fn clone(&self) -> HostRef { + HostRef(self.0.clone()) + } +} + +impl fmt::Debug for HostRef { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Ref(")?; + self.0.borrow().content.fmt(f)?; + write!(f, ")") + } +} diff --git a/crates/api/src/runtime.rs b/crates/api/src/runtime.rs new file mode 100644 index 0000000000..25aa3f6a58 --- /dev/null +++ b/crates/api/src/runtime.rs @@ -0,0 +1,577 @@ +use anyhow::Result; +use std::cell::RefCell; +use std::fmt; +use std::path::Path; +use std::rc::Rc; +use std::sync::{Arc, Mutex}; +use wasmparser::{OperatorValidatorConfig, ValidatingParserConfig}; +use wasmtime_environ::settings::{self, Configurable}; +use wasmtime_environ::CacheConfig; +use wasmtime_jit::{native, CompilationStrategy, Compiler}; +use wasmtime_profiling::{JitDumpAgent, ProfilingAgent, ProfilingStrategy}; + +// Runtime Environment + +// Configuration + +/// Global configuration options used to create an [`Engine`] and customize its +/// behavior. +/// +/// This structure exposed a builder-like interface and is primarily consumed by +/// [`Engine::new()`] +#[derive(Clone)] +pub struct Config { + pub(crate) flags: settings::Builder, + pub(crate) validating_config: ValidatingParserConfig, + pub(crate) debug_info: bool, + pub(crate) strategy: CompilationStrategy, + pub(crate) cache_config: CacheConfig, + pub(crate) profiler: Option>>>, +} + +impl Config { + /// Creates a new configuration object with the default configuration + /// specified. + pub fn new() -> Config { + let mut flags = settings::builder(); + + // There are two possible traps for division, and this way + // we get the proper one if code traps. + flags + .enable("avoid_div_traps") + .expect("should be valid flag"); + + // Invert cranelift's default-on verification to instead default off. + flags + .set("enable_verifier", "false") + .expect("should be valid flag"); + + // Turn on cranelift speed optimizations by default + flags + .set("opt_level", "speed") + .expect("should be valid flag"); + + Config { + debug_info: false, + validating_config: ValidatingParserConfig { + operator_config: OperatorValidatorConfig { + enable_threads: false, + enable_reference_types: false, + enable_bulk_memory: false, + enable_simd: false, + enable_multi_value: false, + }, + }, + flags, + strategy: CompilationStrategy::Auto, + cache_config: CacheConfig::new_cache_disabled(), + profiler: None, + } + } + + /// Configures whether DWARF debug information will be emitted during + /// compilation. + /// + /// By default this option is `false`. + pub fn debug_info(&mut self, enable: bool) -> &mut Self { + self.debug_info = enable; + self + } + + /// Configures whether the WebAssembly threads proposal will be enabled for + /// compilation. + /// + /// The [WebAssembly threads proposal][threads] is not currently fully + /// standardized and is undergoing development. Additionally the support in + /// wasmtime itself is still being worked on. Support for this feature can + /// be enabled through this method for appropriate wasm modules. + /// + /// This feature gates items such as shared memories and atomic + /// instructions. Note that enabling the threads feature will + /// also enable the bulk memory feature. + /// + /// This is `false` by default. + /// + /// [threads]: https://github.com/webassembly/threads + pub fn wasm_threads(&mut self, enable: bool) -> &mut Self { + self.validating_config.operator_config.enable_threads = enable; + // The threads proposal depends on the bulk memory proposal + if enable { + self.wasm_bulk_memory(true); + } + self + } + + /// Configures whether the WebAssembly reference types proposal will be + /// enabled for compilation. + /// + /// The [WebAssembly reference types proposal][proposal] is not currently + /// fully standardized and is undergoing development. Additionally the + /// support in wasmtime itself is still being worked on. Support for this + /// feature can be enabled through this method for appropriate wasm + /// modules. + /// + /// This feature gates items such as the `anyref` type and multiple tables + /// being in a module. Note that enabling the reference types feature will + /// also enable the bulk memory feature. + /// + /// This is `false` by default. + /// + /// [proposal]: https://github.com/webassembly/reference-types + pub fn wasm_reference_types(&mut self, enable: bool) -> &mut Self { + self.validating_config + .operator_config + .enable_reference_types = enable; + // The reference types proposal depends on the bulk memory proposal + if enable { + self.wasm_bulk_memory(true); + } + self + } + + /// Configures whether the WebAssembly SIMD proposal will be + /// enabled for compilation. + /// + /// The [WebAssembly SIMD proposal][proposal] is not currently + /// fully standardized and is undergoing development. Additionally the + /// support in wasmtime itself is still being worked on. Support for this + /// feature can be enabled through this method for appropriate wasm + /// modules. + /// + /// This feature gates items such as the `v128` type and all of its + /// operators being in a module. + /// + /// This is `false` by default. + /// + /// [proposal]: https://github.com/webassembly/simd + pub fn wasm_simd(&mut self, enable: bool) -> &mut Self { + self.validating_config.operator_config.enable_simd = enable; + let val = if enable { "true" } else { "false" }; + self.flags + .set("enable_simd", val) + .expect("should be valid flag"); + self + } + + /// Configures whether the WebAssembly bulk memory operations proposal will + /// be enabled for compilation. + /// + /// The [WebAssembly bulk memory operations proposal][proposal] is not + /// currently fully standardized and is undergoing development. + /// Additionally the support in wasmtime itself is still being worked on. + /// Support for this feature can be enabled through this method for + /// appropriate wasm modules. + /// + /// This feature gates items such as the `memory.copy` instruction, passive + /// data/table segments, etc, being in a module. + /// + /// This is `false` by default. + /// + /// [proposal]: https://github.com/webassembly/bulk-memory-operations + pub fn wasm_bulk_memory(&mut self, enable: bool) -> &mut Self { + self.validating_config.operator_config.enable_bulk_memory = enable; + self + } + + /// Configures whether the WebAssembly multi-value proposal will + /// be enabled for compilation. + /// + /// The [WebAssembly multi-value proposal][proposal] is not + /// currently fully standardized and is undergoing development. + /// Additionally the support in wasmtime itself is still being worked on. + /// Support for this feature can be enabled through this method for + /// appropriate wasm modules. + /// + /// This feature gates functions and blocks returning multiple values in a + /// module, for example. + /// + /// This is `false` by default. + /// + /// [proposal]: https://github.com/webassembly/multi-value + pub fn wasm_multi_value(&mut self, enable: bool) -> &mut Self { + self.validating_config.operator_config.enable_multi_value = enable; + self + } + + /// Configures which compilation strategy will be used for wasm modules. + /// + /// This method can be used to configure which compiler is used for wasm + /// modules, and for more documentation consult the [`Strategy`] enumeration + /// and its documentation. + /// + /// The default value for this is `Strategy::Auto`. + /// + /// # Errors + /// + /// Some compilation strategies require compile-time options of `wasmtime` + /// itself to be set, but if they're not set and the strategy is specified + /// here then an error will be returned. + pub fn strategy(&mut self, strategy: Strategy) -> Result<&mut Self> { + self.strategy = match strategy { + Strategy::Auto => CompilationStrategy::Auto, + Strategy::Cranelift => CompilationStrategy::Cranelift, + #[cfg(feature = "lightbeam")] + Strategy::Lightbeam => CompilationStrategy::Lightbeam, + #[cfg(not(feature = "lightbeam"))] + Strategy::Lightbeam => { + anyhow::bail!("lightbeam compilation strategy wasn't enabled at compile time"); + } + }; + Ok(self) + } + + /// Creates a default profiler based on the profiling strategy choosen + /// + /// Profiler creation calls the type's default initializer where the purpose is + /// really just to put in place the type used for profiling. + pub fn profiler(&mut self, profile: ProfilingStrategy) -> Result<&mut Self> { + match profile { + ProfilingStrategy::JitDumpProfiler => { + self.profiler = { Some(Arc::new(Mutex::new(Box::new(JitDumpAgent::default())))) } + } + _ => self.profiler = { None }, + }; + Ok(self) + } + + /// Configures whether the debug verifier of Cranelift is enabled or not. + /// + /// When Cranelift is used as a code generation backend this will configure + /// it to have the `enable_verifier` flag which will enable a number of debug + /// checks inside of Cranelift. This is largely only useful for the + /// developers of wasmtime itself. + /// + /// The default value for this is `false` + pub fn cranelift_debug_verifier(&mut self, enable: bool) -> &mut Self { + let val = if enable { "true" } else { "false" }; + self.flags + .set("enable_verifier", val) + .expect("should be valid flag"); + self + } + + /// Configures the Cranelift code generator optimization level. + /// + /// When the Cranelift code generator is used you can configure the + /// optimization level used for generated code in a few various ways. For + /// more information see the documentation of [`OptLevel`]. + /// + /// The default value for this is `OptLevel::None`. + pub fn cranelift_opt_level(&mut self, level: OptLevel) -> &mut Self { + let val = match level { + OptLevel::None => "none", + OptLevel::Speed => "speed", + OptLevel::SpeedAndSize => "speed_and_size", + }; + self.flags + .set("opt_level", val) + .expect("should be valid flag"); + self + } + + /// Loads cache configuration specified at `path`. + /// + /// This method will read the file specified by `path` on the filesystem and + /// attempt to load cache configuration from it. This method can also fail + /// due to I/O errors, misconfiguration, syntax errors, etc. For expected + /// syntax in the configuration file see the [documentation online][docs]. + /// + /// By default cache configuration is not enabled or loaded. + /// + /// # Errors + /// + /// This method can fail due to any error that happens when loading the file + /// pointed to by `path` and attempting to load the cache configuration. + /// + /// [docs]: https://bytecodealliance.github.io/wasmtime/cli-cache.html + pub fn cache_config_load(&mut self, path: impl AsRef) -> Result<&mut Self> { + self.cache_config = wasmtime_environ::CacheConfig::from_file(Some(path.as_ref()))?; + Ok(self) + } + + /// Loads cache configuration from the system default path. + /// + /// This commit is the same as [`Config::cache_config_load`] except that it + /// does not take a path argument and instead loads the default + /// configuration present on the system. This is located, for example, on + /// Unix at `$HOME/.config/wasmtime/config.toml` and is typically created + /// with the `wasmtime config new` command. + /// + /// By default cache configuration is not enabled or loaded. + /// + /// # Errors + /// + /// This method can fail due to any error that happens when loading the + /// default system configuration. Note that it is not an error if the + /// default config file does not exist, in which case the default settings + /// for an enabled cache are applied. + /// + /// [docs]: https://bytecodealliance.github.io/wasmtime/cli-cache.html + pub fn cache_config_load_default(&mut self) -> Result<&mut Self> { + self.cache_config = wasmtime_environ::CacheConfig::from_file(None)?; + Ok(self) + } +} + +impl Default for Config { + fn default() -> Config { + Config::new() + } +} + +impl fmt::Debug for Config { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let features = &self.validating_config.operator_config; + f.debug_struct("Config") + .field("debug_info", &self.debug_info) + .field("strategy", &self.strategy) + .field("wasm_threads", &features.enable_threads) + .field("wasm_reference_types", &features.enable_reference_types) + .field("wasm_bulk_memory", &features.enable_bulk_memory) + .field("wasm_simd", &features.enable_simd) + .field("wasm_multi_value", &features.enable_multi_value) + .field( + "flags", + &settings::Flags::new(self.flags.clone()).to_string(), + ) + .finish() + } +} + +/// Possible Compilation strategies for a wasm module. +/// +/// This is used as an argument to the [`Config::strategy`] method. +#[non_exhaustive] +#[derive(Clone, Debug)] +pub enum Strategy { + /// An indicator that the compilation strategy should be automatically + /// selected. + /// + /// This is generally what you want for most projects and indicates that the + /// `wasmtime` crate itself should make the decision about what the best + /// code generator for a wasm module is. + /// + /// Currently this always defaults to Cranelift, but the default value will + /// change over time. + Auto, + + /// Currently the default backend, Cranelift aims to be a reasonably fast + /// code generator which generates high quality machine code. + Cranelift, + + /// A single-pass code generator that is faster than Cranelift but doesn't + /// produce as high-quality code. + /// + /// To successfully pass this argument to [`Config::strategy`] the + /// `lightbeam` feature of this crate must be enabled. + Lightbeam, +} + +/// Possible optimization levels for the Cranelift codegen backend. +#[non_exhaustive] +#[derive(Clone, Debug)] +pub enum OptLevel { + /// No optimizations performed, minimizes compilation time by disabling most + /// optimizations. + None, + /// Generates the fastest possible code, but may take longer. + Speed, + /// Similar to `speed`, but also performs transformations aimed at reducing + /// code size. + SpeedAndSize, +} + +// Engine + +/// An `Engine` which is a global context for compilation and management of wasm +/// modules. +/// +/// An engine can be safely shared across threads and is a cheap cloneable +/// handle to the actual engine. The engine itself will be deallocate once all +/// references to it have gone away. +/// +/// Engines store global configuration preferences such as compilation settings, +/// enabled features, etc. You'll likely only need at most one of these for a +/// program. +/// +/// ## Engines and `Clone` +/// +/// Using `clone` on an `Engine` is a cheap operation. It will not create an +/// entirely new engine, but rather just a new reference to the existing engine. +/// In other words it's a shallow copy, not a deep copy. +/// +/// ## Engines and `Default` +/// +/// You can create an engine with default configuration settings using +/// `Engine::default()`. Be sure to consult the documentation of [`Config`] for +/// default settings. +#[derive(Default, Clone)] +pub struct Engine { + config: Arc, +} + +impl Engine { + /// Creates a new [`Engine`] with the specified compilation and + /// configuration settings. + pub fn new(config: &Config) -> Engine { + Engine { + config: Arc::new(config.clone()), + } + } + + /// Returns the configuration settings that this engine is using. + pub fn config(&self) -> &Config { + &self.config + } +} + +// Store + +/// A `Store` is a shared cache of information between WebAssembly modules. +/// +/// Each `Module` is compiled into a `Store` and a `Store` is associated with an +/// [`Engine`]. You'll use a `Store` to attach to a number of global items in +/// the production of various items for wasm modules. +/// +/// # Stores and `Clone` +/// +/// Using `clone` on a `Store` is a cheap operation. It will not create an +/// entirely new store, but rather just a new reference to the existing object. +/// In other words it's a shallow copy, not a deep copy. +/// +/// ## Stores and `Default` +/// +/// You can create a store with default configuration settings using +/// `Store::default()`. This will create a brand new [`Engine`] with default +/// ocnfiguration (see [`Config`] for more information). +#[derive(Clone)] +pub struct Store { + // FIXME(#777) should be `Arc` and this type should be thread-safe + inner: Rc, +} + +struct StoreInner { + engine: Engine, + compiler: RefCell, +} + +impl Store { + /// Creates a new store to be associated with the given [`Engine`]. + pub fn new(engine: &Engine) -> Store { + let isa = native::builder().finish(settings::Flags::new(engine.config.flags.clone())); + let compiler = Compiler::new( + isa, + engine.config.strategy, + engine.config.cache_config.clone(), + ); + Store { + inner: Rc::new(StoreInner { + engine: engine.clone(), + compiler: RefCell::new(compiler), + }), + } + } + + /// Returns the [`Engine`] that this store is associated with. + pub fn engine(&self) -> &Engine { + &self.inner.engine + } + + pub(crate) fn compiler(&self) -> std::cell::Ref<'_, Compiler> { + self.inner.compiler.borrow() + } + + pub(crate) fn compiler_mut(&self) -> std::cell::RefMut<'_, Compiler> { + self.inner.compiler.borrow_mut() + } + + /// Returns whether the stores `a` and `b` refer to the same underlying + /// `Store`. + /// + /// Because the `Store` type is reference counted multiple clones may point + /// to the same underlying storage, and this method can be used to determine + /// whether two stores are indeed the same. + pub fn same(a: &Store, b: &Store) -> bool { + Rc::ptr_eq(&a.inner, &b.inner) + } +} + +impl Default for Store { + fn default() -> Store { + Store::new(&Engine::default()) + } +} + +fn _assert_send_sync() { + fn _assert() {} + _assert::(); + _assert::(); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Module; + use tempfile::TempDir; + + #[test] + fn cache_accounts_for_opt_level() -> Result<()> { + let td = TempDir::new()?; + let config_path = td.path().join("config.toml"); + std::fs::write( + &config_path, + &format!( + " + [cache] + enabled = true + directory = '{}' + ", + td.path().join("cache").display() + ), + )?; + let mut cfg = Config::new(); + cfg.cranelift_opt_level(OptLevel::None) + .cache_config_load(&config_path)?; + let store = Store::new(&Engine::new(&cfg)); + Module::new(&store, "(module (func))")?; + assert_eq!(store.engine().config.cache_config.cache_hits(), 0); + assert_eq!(store.engine().config.cache_config.cache_misses(), 1); + Module::new(&store, "(module (func))")?; + assert_eq!(store.engine().config.cache_config.cache_hits(), 1); + assert_eq!(store.engine().config.cache_config.cache_misses(), 1); + + let mut cfg = Config::new(); + cfg.cranelift_opt_level(OptLevel::Speed) + .cache_config_load(&config_path)?; + let store = Store::new(&Engine::new(&cfg)); + Module::new(&store, "(module (func))")?; + assert_eq!(store.engine().config.cache_config.cache_hits(), 0); + assert_eq!(store.engine().config.cache_config.cache_misses(), 1); + Module::new(&store, "(module (func))")?; + assert_eq!(store.engine().config.cache_config.cache_hits(), 1); + assert_eq!(store.engine().config.cache_config.cache_misses(), 1); + + let mut cfg = Config::new(); + cfg.cranelift_opt_level(OptLevel::SpeedAndSize) + .cache_config_load(&config_path)?; + let store = Store::new(&Engine::new(&cfg)); + Module::new(&store, "(module (func))")?; + assert_eq!(store.engine().config.cache_config.cache_hits(), 0); + assert_eq!(store.engine().config.cache_config.cache_misses(), 1); + Module::new(&store, "(module (func))")?; + assert_eq!(store.engine().config.cache_config.cache_hits(), 1); + assert_eq!(store.engine().config.cache_config.cache_misses(), 1); + + let mut cfg = Config::new(); + cfg.debug_info(true).cache_config_load(&config_path)?; + let store = Store::new(&Engine::new(&cfg)); + Module::new(&store, "(module (func))")?; + assert_eq!(store.engine().config.cache_config.cache_hits(), 0); + assert_eq!(store.engine().config.cache_config.cache_misses(), 1); + Module::new(&store, "(module (func))")?; + assert_eq!(store.engine().config.cache_config.cache_hits(), 1); + assert_eq!(store.engine().config.cache_config.cache_misses(), 1); + + Ok(()) + } +} diff --git a/crates/api/src/trampoline/create_handle.rs b/crates/api/src/trampoline/create_handle.rs new file mode 100644 index 0000000000..d3d295de9d --- /dev/null +++ b/crates/api/src/trampoline/create_handle.rs @@ -0,0 +1,54 @@ +//! Support for a calling of an imported function. + +use crate::runtime::Store; +use anyhow::Result; +use std::any::Any; +use std::collections::HashSet; +use std::sync::Arc; +use wasmtime_environ::entity::PrimaryMap; +use wasmtime_environ::wasm::DefinedFuncIndex; +use wasmtime_environ::Module; +use wasmtime_runtime::{Imports, InstanceHandle, VMFunctionBody}; + +pub(crate) fn create_handle( + module: Module, + store: &Store, + finished_functions: PrimaryMap, + state: Box, +) -> Result { + let imports = Imports::new( + HashSet::new(), + PrimaryMap::new(), + PrimaryMap::new(), + PrimaryMap::new(), + PrimaryMap::new(), + ); + let data_initializers = Vec::new(); + + // Compute indices into the shared signature table. + let signatures = module + .local + .signatures + .values() + .map(|sig| store.compiler().signatures().register(sig)) + .collect::>(); + + unsafe { + Ok(InstanceHandle::new( + Arc::new(module), + store.compiler().trap_registry().register_traps(Vec::new()), + finished_functions.into_boxed_slice(), + imports, + &data_initializers, + signatures.into_boxed_slice(), + None, + store + .engine() + .config() + .validating_config + .operator_config + .enable_bulk_memory, + state, + )?) + } +} diff --git a/crates/api/src/trampoline/func.rs b/crates/api/src/trampoline/func.rs new file mode 100644 index 0000000000..83753be403 --- /dev/null +++ b/crates/api/src/trampoline/func.rs @@ -0,0 +1,326 @@ +//! Support for a calling of an imported function. + +use super::create_handle::create_handle; +use crate::{Callable, FuncType, Store, Trap, Val}; +use anyhow::{bail, Result}; +use std::any::Any; +use std::cmp; +use std::panic::{self, AssertUnwindSafe}; +use std::rc::Rc; +use wasmtime_environ::entity::{EntityRef, PrimaryMap}; +use wasmtime_environ::ir::types; +use wasmtime_environ::isa::TargetIsa; +use wasmtime_environ::wasm::{DefinedFuncIndex, FuncIndex}; +use wasmtime_environ::{ + ir, settings, CompiledFunction, CompiledFunctionUnwindInfo, Export, Module, +}; +use wasmtime_jit::trampoline::ir::{ + ExternalName, Function, InstBuilder, MemFlags, StackSlotData, StackSlotKind, +}; +use wasmtime_jit::trampoline::{ + binemit, pretty_error, Context, FunctionBuilder, FunctionBuilderContext, +}; +use wasmtime_jit::{native, CodeMemory}; +use wasmtime_runtime::{InstanceHandle, VMContext, VMFunctionBody}; + +struct TrampolineState { + func: Rc, + #[allow(dead_code)] + code_memory: CodeMemory, +} + +impl TrampolineState { + fn new(func: Rc, code_memory: CodeMemory) -> Self { + TrampolineState { func, code_memory } + } +} + +unsafe extern "C" fn stub_fn( + vmctx: *mut VMContext, + _caller_vmctx: *mut VMContext, + call_id: u32, + values_vec: *mut i128, +) { + // Here we are careful to use `catch_unwind` to ensure Rust panics don't + // unwind past us. The primary reason for this is that Rust considers it UB + // to unwind past an `extern "C"` function. Here we are in an `extern "C"` + // function and the cross into wasm was through an `extern "C"` function at + // the base of the stack as well. We'll need to wait for assorted RFCs and + // language features to enable this to be done in a sound and stable fashion + // before avoiding catching the panic here. + // + // Also note that there are intentionally no local variables on this stack + // frame. The reason for that is that some of the "raise" functions we have + // below will trigger a longjmp, which won't run local destructors if we + // have any. To prevent leaks we avoid having any local destructors by + // avoiding local variables. + let result = panic::catch_unwind(AssertUnwindSafe(|| call_stub(vmctx, call_id, values_vec))); + + match result { + Ok(Ok(())) => {} + + // If a trap was raised (an error returned from the imported function) + // then we smuggle the trap through `Box` through to the + // call-site, which gets unwrapped in `Trap::from_jit` later on as we + // convert from the internal `Trap` type to our own `Trap` type in this + // crate. + Ok(Err(trap)) => wasmtime_runtime::raise_user_trap(Box::new(trap)), + + // And finally if the imported function panicked, then we trigger the + // form of unwinding that's safe to jump over wasm code on all + // platforms. + Err(panic) => wasmtime_runtime::resume_panic(panic), + } + + unsafe fn call_stub( + vmctx: *mut VMContext, + call_id: u32, + values_vec: *mut i128, + ) -> Result<(), Trap> { + let instance = InstanceHandle::from_vmctx(vmctx); + + let (args, returns_len) = { + let module = instance.module_ref(); + let signature = + &module.local.signatures[module.local.functions[FuncIndex::new(call_id as usize)]]; + + let mut args = Vec::new(); + for i in 2..signature.params.len() { + args.push(Val::read_value_from( + values_vec.offset(i as isize - 2), + signature.params[i].value_type, + )) + } + (args, signature.returns.len()) + }; + + let mut returns = vec![Val::null(); returns_len]; + let state = &instance + .host_state() + .downcast_ref::() + .expect("state"); + state.func.call(&args, &mut returns)?; + + let module = instance.module_ref(); + let signature = + &module.local.signatures[module.local.functions[FuncIndex::new(call_id as usize)]]; + for (i, ret) in returns.iter_mut().enumerate() { + if ret.ty().get_wasmtime_type() != Some(signature.returns[i].value_type) { + return Err(Trap::new( + "`Callable` attempted to return an incompatible value", + )); + } + ret.write_value_to(values_vec.add(i)); + } + Ok(()) + } +} + +/// Create a trampoline for invoking a Callable. +fn make_trampoline( + isa: &dyn TargetIsa, + code_memory: &mut CodeMemory, + fn_builder_ctx: &mut FunctionBuilderContext, + call_id: u32, + signature: &ir::Signature, +) -> *mut [VMFunctionBody] { + // Mostly reverse copy of the similar method from wasmtime's + // wasmtime-jit/src/compiler.rs. + let pointer_type = isa.pointer_type(); + let mut stub_sig = ir::Signature::new(isa.frontend_config().default_call_conv); + + // Add the caller/callee `vmctx` parameters. + stub_sig.params.push(ir::AbiParam::special( + pointer_type, + ir::ArgumentPurpose::VMContext, + )); + + // Add the caller `vmctx` parameter. + stub_sig.params.push(ir::AbiParam::new(pointer_type)); + + // Add the `call_id` parameter. + stub_sig.params.push(ir::AbiParam::new(types::I32)); + + // Add the `values_vec` parameter. + stub_sig.params.push(ir::AbiParam::new(pointer_type)); + + // Compute the size of the values vector. The vmctx and caller vmctx are passed separately. + let value_size = 16; + let values_vec_len = ((value_size as usize) + * cmp::max(signature.params.len() - 2, signature.returns.len())) + as u32; + + let mut context = Context::new(); + context.func = Function::with_name_signature(ExternalName::user(0, 0), signature.clone()); + context.func.collect_frame_layout_info(); + + let ss = context.func.create_stack_slot(StackSlotData::new( + StackSlotKind::ExplicitSlot, + values_vec_len, + )); + + { + let mut builder = FunctionBuilder::new(&mut context.func, fn_builder_ctx); + let block0 = builder.create_block(); + + builder.append_block_params_for_function_params(block0); + builder.switch_to_block(block0); + builder.seal_block(block0); + + let values_vec_ptr_val = builder.ins().stack_addr(pointer_type, ss, 0); + let mflags = MemFlags::trusted(); + for i in 2..signature.params.len() { + if i == 0 { + continue; + } + + let val = builder.func.dfg.block_params(block0)[i]; + builder.ins().store( + mflags, + val, + values_vec_ptr_val, + ((i - 2) * value_size) as i32, + ); + } + + let block_params = builder.func.dfg.block_params(block0); + let vmctx_ptr_val = block_params[0]; + let caller_vmctx_ptr_val = block_params[1]; + let call_id_val = builder.ins().iconst(types::I32, call_id as i64); + + let callee_args = vec![ + vmctx_ptr_val, + caller_vmctx_ptr_val, + call_id_val, + values_vec_ptr_val, + ]; + + let new_sig = builder.import_signature(stub_sig); + + let callee_value = builder + .ins() + .iconst(pointer_type, stub_fn as *const VMFunctionBody as i64); + builder + .ins() + .call_indirect(new_sig, callee_value, &callee_args); + + let mflags = MemFlags::trusted(); + let mut results = Vec::new(); + for (i, r) in signature.returns.iter().enumerate() { + let load = builder.ins().load( + r.value_type, + mflags, + values_vec_ptr_val, + (i * value_size) as i32, + ); + results.push(load); + } + builder.ins().return_(&results); + builder.finalize() + } + + let mut code_buf: Vec = Vec::new(); + let mut reloc_sink = binemit::TrampolineRelocSink {}; + let mut trap_sink = binemit::NullTrapSink {}; + let mut stackmap_sink = binemit::NullStackmapSink {}; + context + .compile_and_emit( + isa, + &mut code_buf, + &mut reloc_sink, + &mut trap_sink, + &mut stackmap_sink, + ) + .map_err(|error| pretty_error(&context.func, Some(isa), error)) + .expect("compile_and_emit"); + + let unwind_info = CompiledFunctionUnwindInfo::new(isa, &context); + + code_memory + .allocate_for_function(&CompiledFunction { + body: code_buf, + jt_offsets: context.func.jt_offsets, + unwind_info, + }) + .expect("allocate_for_function") +} + +pub fn create_handle_with_function( + ft: &FuncType, + func: &Rc, + store: &Store, +) -> Result { + let isa = { + let isa_builder = native::builder(); + let flag_builder = settings::builder(); + isa_builder.finish(settings::Flags::new(flag_builder)) + }; + + let pointer_type = isa.pointer_type(); + let sig = match ft.get_wasmtime_signature(pointer_type) { + Some(sig) => sig.clone(), + None => bail!("not a supported core wasm signature {:?}", ft), + }; + + let mut fn_builder_ctx = FunctionBuilderContext::new(); + let mut module = Module::new(); + let mut finished_functions: PrimaryMap = + PrimaryMap::new(); + let mut code_memory = CodeMemory::new(); + + let sig_id = module.local.signatures.push(sig.clone()); + let func_id = module.local.functions.push(sig_id); + module + .exports + .insert("trampoline".to_string(), Export::Function(func_id)); + let trampoline = make_trampoline( + isa.as_ref(), + &mut code_memory, + &mut fn_builder_ctx, + func_id.index() as u32, + &sig, + ); + code_memory.publish(); + + finished_functions.push(trampoline); + + let trampoline_state = TrampolineState::new(func.clone(), code_memory); + + create_handle( + module, + store, + finished_functions, + Box::new(trampoline_state), + ) +} + +pub unsafe fn create_handle_with_raw_function( + ft: &FuncType, + func: *mut [VMFunctionBody], + store: &Store, + state: Box, +) -> Result { + let isa = { + let isa_builder = native::builder(); + let flag_builder = settings::builder(); + isa_builder.finish(settings::Flags::new(flag_builder)) + }; + + let pointer_type = isa.pointer_type(); + let sig = match ft.get_wasmtime_signature(pointer_type) { + Some(sig) => sig.clone(), + None => bail!("not a supported core wasm signature {:?}", ft), + }; + + let mut module = Module::new(); + let mut finished_functions = PrimaryMap::new(); + + let sig_id = module.local.signatures.push(sig.clone()); + let func_id = module.local.functions.push(sig_id); + module + .exports + .insert("trampoline".to_string(), Export::Function(func_id)); + finished_functions.push(func); + + create_handle(module, store, finished_functions, state) +} diff --git a/crates/api/src/trampoline/global.rs b/crates/api/src/trampoline/global.rs new file mode 100644 index 0000000000..ad87386a3e --- /dev/null +++ b/crates/api/src/trampoline/global.rs @@ -0,0 +1,35 @@ +use super::create_handle::create_handle; +use crate::Store; +use crate::{GlobalType, Mutability, Val}; +use anyhow::{bail, Result}; +use wasmtime_environ::entity::PrimaryMap; +use wasmtime_environ::{wasm, Module}; +use wasmtime_runtime::InstanceHandle; + +pub fn create_global(store: &Store, gt: &GlobalType, val: Val) -> Result { + let global = wasm::Global { + ty: match gt.content().get_wasmtime_type() { + Some(t) => t, + None => bail!("cannot support {:?} as a wasm global type", gt.content()), + }, + mutability: match gt.mutability() { + Mutability::Const => false, + Mutability::Var => true, + }, + initializer: match val { + Val::I32(i) => wasm::GlobalInit::I32Const(i), + Val::I64(i) => wasm::GlobalInit::I64Const(i), + Val::F32(f) => wasm::GlobalInit::F32Const(f), + Val::F64(f) => wasm::GlobalInit::F64Const(f), + _ => unimplemented!("create_global for {:?}", gt), + }, + }; + let mut module = Module::new(); + let global_id = module.local.globals.push(global); + module.exports.insert( + "global".to_string(), + wasmtime_environ::Export::Global(global_id), + ); + let handle = create_handle(module, store, PrimaryMap::new(), Box::new(()))?; + Ok(handle) +} diff --git a/crates/api/src/trampoline/memory.rs b/crates/api/src/trampoline/memory.rs new file mode 100644 index 0000000000..5d87dc6dca --- /dev/null +++ b/crates/api/src/trampoline/memory.rs @@ -0,0 +1,27 @@ +use super::create_handle::create_handle; +use crate::MemoryType; +use crate::Store; +use anyhow::Result; +use wasmtime_environ::entity::PrimaryMap; +use wasmtime_environ::{wasm, Module}; +use wasmtime_runtime::InstanceHandle; + +pub fn create_handle_with_memory(store: &Store, memory: &MemoryType) -> Result { + let mut module = Module::new(); + + let memory = wasm::Memory { + minimum: memory.limits().min(), + maximum: memory.limits().max(), + shared: false, // TODO + }; + let tunable = Default::default(); + + let memory_plan = wasmtime_environ::MemoryPlan::for_memory(memory, &tunable); + let memory_id = module.local.memory_plans.push(memory_plan); + module.exports.insert( + "memory".to_string(), + wasmtime_environ::Export::Memory(memory_id), + ); + + create_handle(module, store, PrimaryMap::new(), Box::new(())) +} diff --git a/crates/api/src/trampoline/mod.rs b/crates/api/src/trampoline/mod.rs new file mode 100644 index 0000000000..287ae66b5e --- /dev/null +++ b/crates/api/src/trampoline/mod.rs @@ -0,0 +1,69 @@ +//! Utility module to create trampolines in/out WebAssembly module. + +mod create_handle; +mod func; +mod global; +mod memory; +mod table; + +use self::func::create_handle_with_function; +use self::global::create_global; +use self::memory::create_handle_with_memory; +use self::table::create_handle_with_table; +use super::{Callable, FuncType, GlobalType, MemoryType, Store, TableType, Val}; +use anyhow::Result; +use std::any::Any; +use std::rc::Rc; +use wasmtime_runtime::VMFunctionBody; + +pub fn generate_func_export( + ft: &FuncType, + func: &Rc, + store: &Store, +) -> Result<(wasmtime_runtime::InstanceHandle, wasmtime_runtime::Export)> { + let instance = create_handle_with_function(ft, func, store)?; + let export = instance.lookup("trampoline").expect("trampoline export"); + Ok((instance, export)) +} + +/// Note that this is `unsafe` since `func` must be a valid function pointer and +/// have a signature which matches `ft`, otherwise the returned +/// instance/export/etc may exhibit undefined behavior. +pub unsafe fn generate_raw_func_export( + ft: &FuncType, + func: *mut [VMFunctionBody], + store: &Store, + state: Box, +) -> Result<(wasmtime_runtime::InstanceHandle, wasmtime_runtime::Export)> { + let instance = func::create_handle_with_raw_function(ft, func, store, state)?; + let export = instance.lookup("trampoline").expect("trampoline export"); + Ok((instance, export)) +} + +pub fn generate_global_export( + store: &Store, + gt: &GlobalType, + val: Val, +) -> Result<(wasmtime_runtime::InstanceHandle, wasmtime_runtime::Export)> { + let instance = create_global(store, gt, val)?; + let export = instance.lookup("global").expect("global export"); + Ok((instance, export)) +} + +pub fn generate_memory_export( + store: &Store, + m: &MemoryType, +) -> Result<(wasmtime_runtime::InstanceHandle, wasmtime_runtime::Export)> { + let instance = create_handle_with_memory(store, m)?; + let export = instance.lookup("memory").expect("memory export"); + Ok((instance, export)) +} + +pub fn generate_table_export( + store: &Store, + t: &TableType, +) -> Result<(wasmtime_runtime::InstanceHandle, wasmtime_runtime::Export)> { + let instance = create_handle_with_table(store, t)?; + let export = instance.lookup("table").expect("table export"); + Ok((instance, export)) +} diff --git a/crates/api/src/trampoline/table.rs b/crates/api/src/trampoline/table.rs new file mode 100644 index 0000000000..7f067b6393 --- /dev/null +++ b/crates/api/src/trampoline/table.rs @@ -0,0 +1,30 @@ +use super::create_handle::create_handle; +use crate::Store; +use crate::{TableType, ValType}; +use anyhow::{bail, Result}; +use wasmtime_environ::entity::PrimaryMap; +use wasmtime_environ::{wasm, Module}; +use wasmtime_runtime::InstanceHandle; + +pub fn create_handle_with_table(store: &Store, table: &TableType) -> Result { + let mut module = Module::new(); + + let table = wasm::Table { + minimum: table.limits().min(), + maximum: table.limits().max(), + ty: match table.element() { + ValType::FuncRef => wasm::TableElementType::Func, + _ => bail!("cannot support {:?} as a table element", table.element()), + }, + }; + let tunable = Default::default(); + + let table_plan = wasmtime_environ::TablePlan::for_table(table, &tunable); + let table_id = module.local.table_plans.push(table_plan); + module.exports.insert( + "table".to_string(), + wasmtime_environ::Export::Table(table_id), + ); + + create_handle(module, store, PrimaryMap::new(), Box::new(())) +} diff --git a/crates/api/src/trap.rs b/crates/api/src/trap.rs new file mode 100644 index 0000000000..8b5d6f7b98 --- /dev/null +++ b/crates/api/src/trap.rs @@ -0,0 +1,119 @@ +use crate::frame_info::FRAME_INFO; +use crate::FrameInfo; +use backtrace::Backtrace; +use std::fmt; +use std::sync::Arc; + +/// A struct representing an aborted instruction execution, with a message +/// indicating the cause. +#[derive(Clone)] +pub struct Trap { + inner: Arc, +} + +struct TrapInner { + message: String, + wasm_trace: Vec, + native_trace: Backtrace, +} + +fn _assert_trap_is_sync_and_send(t: &Trap) -> (&dyn Sync, &dyn Send) { + (t, t) +} + +impl Trap { + /// Creates a new `Trap` with `message`. + /// # Example + /// ``` + /// let trap = wasmtime::Trap::new("unexpected error"); + /// assert_eq!("unexpected error", trap.message()); + /// ``` + pub fn new>(message: I) -> Self { + Trap::new_with_trace(message.into(), Backtrace::new_unresolved()) + } + + pub(crate) fn from_jit(jit: wasmtime_runtime::Trap) -> Self { + match jit { + wasmtime_runtime::Trap::User(error) => { + // Since we're the only one using the wasmtime internals (in + // theory) we should only see user errors which were originally + // created from our own `Trap` type (see the trampoline module + // with functions). + // + // If this unwrap trips for someone we'll need to tweak the + // return type of this function to probably be `anyhow::Error` + // or something like that. + *error + .downcast() + .expect("only `Trap` user errors are supported") + } + wasmtime_runtime::Trap::Wasm { desc, backtrace } => { + Trap::new_with_trace(desc.to_string(), backtrace) + } + } + } + + fn new_with_trace(message: String, native_trace: Backtrace) -> Self { + let mut wasm_trace = Vec::new(); + for frame in native_trace.frames() { + let pc = frame.ip() as usize; + if let Some(info) = FRAME_INFO.lookup(pc) { + wasm_trace.push(info); + } + } + Trap { + inner: Arc::new(TrapInner { + message, + wasm_trace, + native_trace, + }), + } + } + + /// Returns a reference the `message` stored in `Trap`. + pub fn message(&self) -> &str { + &self.inner.message + } + + /// Returns a list of function frames in WebAssembly code that led to this + /// trap happening. + pub fn trace(&self) -> &[FrameInfo] { + &self.inner.wasm_trace + } +} + +impl fmt::Debug for Trap { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Trap") + .field("message", &self.inner.message) + .field("wasm_trace", &self.inner.wasm_trace) + .field("native_trace", &self.inner.native_trace) + .finish() + } +} + +impl fmt::Display for Trap { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.inner.message)?; + let trace = self.trace(); + if trace.is_empty() { + return Ok(()); + } + writeln!(f, "\nwasm backtrace:")?; + for (i, frame) in self.trace().iter().enumerate() { + let name = frame.module_name().unwrap_or(""); + write!(f, " {}: {}!", i, name)?; + match frame.func_name() { + Some(name) => match rustc_demangle::try_demangle(name) { + Ok(name) => write!(f, "{}", name)?, + Err(_) => write!(f, "{}", name)?, + }, + None => write!(f, "", frame.func_index())?, + } + writeln!(f, "")?; + } + Ok(()) + } +} + +impl std::error::Error for Trap {} diff --git a/crates/api/src/types.rs b/crates/api/src/types.rs new file mode 100644 index 0000000000..8e344d5d0a --- /dev/null +++ b/crates/api/src/types.rs @@ -0,0 +1,437 @@ +use wasmtime_environ::{ir, wasm}; + +// Type Representations + +// Type attributes + +/// Indicator of whether a global is mutable or not +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Mutability { + /// The global is constant and its value does not change + Const, + /// The value of the global can change over time + Var, +} + +/// Limits of tables/memories where the units of the limits are defined by the +/// table/memory types. +/// +/// A minimum is always available but the maximum may not be present. +#[derive(Debug, Clone)] +pub struct Limits { + min: u32, + max: Option, +} + +impl Limits { + /// Creates a new set of limits with the minimum and maximum both specified. + pub fn new(min: u32, max: Option) -> Limits { + Limits { min, max } + } + + /// Creates a new `Limits` with the `min` specified and no maximum specified. + pub fn at_least(min: u32) -> Limits { + Limits::new(min, None) + } + + /// Returns the minimum amount for these limits. + pub fn min(&self) -> u32 { + self.min + } + + /// Returns the maximum amount for these limits, if specified. + pub fn max(&self) -> Option { + self.max + } +} + +// Value Types + +/// A list of all possible value types in WebAssembly. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum ValType { + /// Signed 32 bit integer. + I32, + /// Signed 64 bit integer. + I64, + /// Floating point 32 bit integer. + F32, + /// Floating point 64 bit integer. + F64, + /// A 128 bit number. + V128, + /// A reference to opaque data in the Wasm instance. + AnyRef, /* = 128 */ + /// A reference to a Wasm function. + FuncRef, +} + +impl ValType { + /// Returns true if `ValType` matches any of the numeric types. (e.g. `I32`, + /// `I64`, `F32`, `F64`). + pub fn is_num(&self) -> bool { + match self { + ValType::I32 | ValType::I64 | ValType::F32 | ValType::F64 => true, + _ => false, + } + } + + /// Returns true if `ValType` matches either of the reference types. + pub fn is_ref(&self) -> bool { + match self { + ValType::AnyRef | ValType::FuncRef => true, + _ => false, + } + } + + pub(crate) fn get_wasmtime_type(&self) -> Option { + match self { + ValType::I32 => Some(ir::types::I32), + ValType::I64 => Some(ir::types::I64), + ValType::F32 => Some(ir::types::F32), + ValType::F64 => Some(ir::types::F64), + ValType::V128 => Some(ir::types::I8X16), + _ => None, + } + } + + pub(crate) fn from_wasmtime_type(ty: ir::Type) -> Option { + match ty { + ir::types::I32 => Some(ValType::I32), + ir::types::I64 => Some(ValType::I64), + ir::types::F32 => Some(ValType::F32), + ir::types::F64 => Some(ValType::F64), + ir::types::I8X16 => Some(ValType::V128), + _ => None, + } + } +} + +// External Types + +/// A list of all possible types which can be externally referenced from a +/// WebAssembly module. +/// +/// This list can be found in [`ImportType`] or [`ExportType`], so these types +/// can either be imported or exported. +#[derive(Debug, Clone)] +pub enum ExternType { + /// This external type is the type of a WebAssembly function. + Func(FuncType), + /// This external type is the type of a WebAssembly global. + Global(GlobalType), + /// This external type is the type of a WebAssembly table. + Table(TableType), + /// This external type is the type of a WebAssembly memory. + Memory(MemoryType), +} + +macro_rules! accessors { + ($(($variant:ident($ty:ty) $get:ident $unwrap:ident))*) => ($( + /// Attempt to return the underlying type of this external type, + /// returning `None` if it is a different type. + pub fn $get(&self) -> Option<&$ty> { + if let ExternType::$variant(e) = self { + Some(e) + } else { + None + } + } + + /// Returns the underlying descriptor of this [`ExternType`], panicking + /// if it is a different type. + /// + /// # Panics + /// + /// Panics if `self` is not of the right type. + pub fn $unwrap(&self) -> &$ty { + self.$get().expect(concat!("expected ", stringify!($ty))) + } + )*) +} + +impl ExternType { + accessors! { + (Func(FuncType) func unwrap_func) + (Global(GlobalType) global unwrap_global) + (Table(TableType) table unwrap_table) + (Memory(MemoryType) memory unwrap_memory) + } +} + +// Function Types +fn from_wasmtime_abiparam(param: &ir::AbiParam) -> Option { + assert_eq!(param.purpose, ir::ArgumentPurpose::Normal); + ValType::from_wasmtime_type(param.value_type) +} + +/// A descriptor for a function in a WebAssembly module. +/// +/// WebAssembly functions can have 0 or more parameters and results. +#[derive(Debug, Clone, PartialEq)] +pub struct FuncType { + params: Box<[ValType]>, + results: Box<[ValType]>, +} + +impl FuncType { + /// Creates a new function descriptor from the given parameters and results. + /// + /// The function descriptor returned will represent a function which takes + /// `params` as arguments and returns `results` when it is finished. + pub fn new(params: Box<[ValType]>, results: Box<[ValType]>) -> FuncType { + FuncType { params, results } + } + + /// Returns the list of parameter types for this function. + pub fn params(&self) -> &[ValType] { + &self.params + } + + /// Returns the list of result types for this function. + pub fn results(&self) -> &[ValType] { + &self.results + } + + /// Returns `Some` if this function signature was compatible with cranelift, + /// or `None` if one of the types/results wasn't supported or compatible + /// with cranelift. + pub(crate) fn get_wasmtime_signature(&self, pointer_type: ir::Type) -> Option { + use wasmtime_environ::ir::{types, AbiParam, ArgumentPurpose, Signature}; + use wasmtime_jit::native; + let call_conv = native::call_conv(); + let mut params = self + .params + .iter() + .map(|p| p.get_wasmtime_type().map(AbiParam::new)) + .collect::>>()?; + let returns = self + .results + .iter() + .map(|p| p.get_wasmtime_type().map(AbiParam::new)) + .collect::>>()?; + params.insert(0, AbiParam::special(types::I64, ArgumentPurpose::VMContext)); + params.insert(1, AbiParam::new(pointer_type)); + + Some(Signature { + params, + returns, + call_conv, + }) + } + + /// Returns `None` if any types in the signature can't be converted to the + /// types in this crate, but that should very rarely happen and largely only + /// indicate a bug in our cranelift integration. + pub(crate) fn from_wasmtime_signature(signature: ir::Signature) -> Option { + let params = signature + .params + .iter() + .skip(2) // skip the caller/callee vmctx + .map(|p| from_wasmtime_abiparam(p)) + .collect::>>()?; + let results = signature + .returns + .iter() + .map(|p| from_wasmtime_abiparam(p)) + .collect::>>()?; + Some(FuncType { + params: params.into_boxed_slice(), + results: results.into_boxed_slice(), + }) + } +} + +// Global Types + +/// A WebAssembly global descriptor. +/// +/// This type describes an instance of a global in a WebAssembly module. Globals +/// are local to an [`Instance`](crate::Instance) and are either immutable or +/// mutable. +#[derive(Debug, Clone)] +pub struct GlobalType { + content: ValType, + mutability: Mutability, +} + +impl GlobalType { + /// Creates a new global descriptor of the specified `content` type and + /// whether or not it's mutable. + pub fn new(content: ValType, mutability: Mutability) -> GlobalType { + GlobalType { + content, + mutability, + } + } + + /// Returns the value type of this global descriptor. + pub fn content(&self) -> &ValType { + &self.content + } + + /// Returns whether or not this global is mutable. + pub fn mutability(&self) -> Mutability { + self.mutability + } + + /// Returns `None` if the wasmtime global has a type that we can't + /// represent, but that should only very rarely happen and indicate a bug. + pub(crate) fn from_wasmtime_global(global: &wasm::Global) -> Option { + let ty = ValType::from_wasmtime_type(global.ty)?; + let mutability = if global.mutability { + Mutability::Var + } else { + Mutability::Const + }; + Some(GlobalType::new(ty, mutability)) + } +} + +// Table Types + +/// A descriptor for a table in a WebAssembly module. +/// +/// Tables are contiguous chunks of a specific element, typically a `funcref` or +/// an `anyref`. The most common use for tables is a function table through +/// which `call_indirect` can invoke other functions. +#[derive(Debug, Clone)] +pub struct TableType { + element: ValType, + limits: Limits, +} + +impl TableType { + /// Creates a new table descriptor which will contain the specified + /// `element` and have the `limits` applied to its length. + pub fn new(element: ValType, limits: Limits) -> TableType { + TableType { element, limits } + } + + /// Returns the element value type of this table. + pub fn element(&self) -> &ValType { + &self.element + } + + /// Returns the limits, in units of elements, of this table. + pub fn limits(&self) -> &Limits { + &self.limits + } + + pub(crate) fn from_wasmtime_table(table: &wasm::Table) -> TableType { + assert!(if let wasm::TableElementType::Func = table.ty { + true + } else { + false + }); + let ty = ValType::FuncRef; + let limits = Limits::new(table.minimum, table.maximum); + TableType::new(ty, limits) + } +} + +// Memory Types + +/// A descriptor for a WebAssembly memory type. +/// +/// Memories are described in units of pages (64KB) and represent contiguous +/// chunks of addressable memory. +#[derive(Debug, Clone)] +pub struct MemoryType { + limits: Limits, +} + +impl MemoryType { + /// Creates a new descriptor for a WebAssembly memory given the specified + /// limits of the memory. + pub fn new(limits: Limits) -> MemoryType { + MemoryType { limits } + } + + /// Returns the limits (in pages) that are configured for this memory. + pub fn limits(&self) -> &Limits { + &self.limits + } + + pub(crate) fn from_wasmtime_memory(memory: &wasm::Memory) -> MemoryType { + MemoryType::new(Limits::new(memory.minimum, memory.maximum)) + } +} + +// Import Types + +/// A descriptor for an imported value into a wasm module. +/// +/// This type is primarily accessed from the +/// [`Module::imports`](crate::Module::imports) API. Each [`ImportType`] +/// describes an import into the wasm module with the module/name that it's +/// imported from as well as the type of item that's being imported. +#[derive(Debug, Clone)] +pub struct ImportType { + module: String, + name: String, + ty: ExternType, +} + +impl ImportType { + /// Creates a new import descriptor which comes from `module` and `name` and + /// is of type `ty`. + pub fn new(module: &str, name: &str, ty: ExternType) -> ImportType { + ImportType { + module: module.to_string(), + name: name.to_string(), + ty, + } + } + + /// Returns the module name that this import is expected to come from. + pub fn module(&self) -> &str { + &self.module + } + + /// Returns the field name of the module that this import is expected to + /// come from. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the expected type of this import. + pub fn ty(&self) -> &ExternType { + &self.ty + } +} + +// Export Types + +/// A descriptor for an exported WebAssembly value. +/// +/// This type is primarily accessed from the +/// [`Module::exports`](crate::Module::exports) accessor and describes what +/// names are exported from a wasm module and the type of the item that is +/// exported. +#[derive(Debug, Clone)] +pub struct ExportType { + name: String, + ty: ExternType, +} + +impl ExportType { + /// Creates a new export which is exported with the given `name` and has the + /// given `ty`. + pub fn new(name: &str, ty: ExternType) -> ExportType { + ExportType { + name: name.to_string(), + ty, + } + } + + /// Returns the name by which this export is known by. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the type of this export. + pub fn ty(&self) -> &ExternType { + &self.ty + } +} diff --git a/crates/api/src/unix.rs b/crates/api/src/unix.rs new file mode 100644 index 0000000000..f17eecfe13 --- /dev/null +++ b/crates/api/src/unix.rs @@ -0,0 +1,31 @@ +//! Unix-specific extension for the `wasmtime` crate. +//! +//! This module is only available on Unix targets, for example Linux and macOS. +//! It is not available on Windows, for example. Note that the import path for +//! this module is `wasmtime::unix::...`, which is intended to emphasize that it +//! is platform-specific. +//! +//! The traits contained in this module are intended to extend various types +//! throughout the `wasmtime` crate with extra functionality that's only +//! available on Unix. + +use crate::Instance; + +/// Extensions for the [`Instance`] type only available on Unix. +pub trait InstanceExt { + // TODO: needs more docs? + /// The signal handler must be + /// [async-signal-safe](http://man7.org/linux/man-pages/man7/signal-safety.7.html). + unsafe fn set_signal_handler(&self, handler: H) + where + H: 'static + Fn(libc::c_int, *const libc::siginfo_t, *const libc::c_void) -> bool; +} + +impl InstanceExt for Instance { + unsafe fn set_signal_handler(&self, handler: H) + where + H: 'static + Fn(libc::c_int, *const libc::siginfo_t, *const libc::c_void) -> bool, + { + self.instance_handle.clone().set_signal_handler(handler); + } +} diff --git a/crates/api/src/values.rs b/crates/api/src/values.rs new file mode 100644 index 0000000000..de071d312b --- /dev/null +++ b/crates/api/src/values.rs @@ -0,0 +1,224 @@ +use crate::r#ref::AnyRef; +use crate::{Func, Store, ValType}; +use anyhow::{bail, Result}; +use std::ptr; +use wasmtime_environ::ir; + +/// Possible runtime values that a WebAssembly module can either consume or +/// produce. +#[derive(Debug, Clone)] +pub enum Val { + /// A 32-bit integer + I32(i32), + + /// A 64-bit integer + I64(i64), + + /// A 32-bit float. + /// + /// Note that the raw bits of the float are stored here, and you can use + /// `f32::from_bits` to create an `f32` value. + F32(u32), + + /// A 64-bit float. + /// + /// Note that the raw bits of the float are stored here, and you can use + /// `f64::from_bits` to create an `f64` value. + F64(u64), + + /// An `anyref` value which can hold opaque data to the wasm instance itself. + /// + /// Note that this is a nullable value as well. + AnyRef(AnyRef), + + /// A first-class reference to a WebAssembly function. + FuncRef(Func), + + /// A 128-bit number + V128(u128), +} + +macro_rules! accessors { + ($bind:ident $(($variant:ident($ty:ty) $get:ident $unwrap:ident $cvt:expr))*) => ($( + /// Attempt to access the underlying value of this `Val`, returning + /// `None` if it is not the correct type. + pub fn $get(&self) -> Option<$ty> { + if let Val::$variant($bind) = self { + Some($cvt) + } else { + None + } + } + + /// Returns the underlying value of this `Val`, panicking if it's the + /// wrong type. + /// + /// # Panics + /// + /// Panics if `self` is not of the right type. + pub fn $unwrap(&self) -> $ty { + self.$get().expect(concat!("expected ", stringify!($ty))) + } + )*) +} + +impl Val { + /// Returns a null `anyref` value. + pub fn null() -> Val { + Val::AnyRef(AnyRef::null()) + } + + /// Returns the corresponding [`ValType`] for this `Val`. + pub fn ty(&self) -> ValType { + match self { + Val::I32(_) => ValType::I32, + Val::I64(_) => ValType::I64, + Val::F32(_) => ValType::F32, + Val::F64(_) => ValType::F64, + Val::AnyRef(_) => ValType::AnyRef, + Val::FuncRef(_) => ValType::FuncRef, + Val::V128(_) => ValType::V128, + } + } + + pub(crate) unsafe fn write_value_to(&self, p: *mut i128) { + match self { + Val::I32(i) => ptr::write(p as *mut i32, *i), + Val::I64(i) => ptr::write(p as *mut i64, *i), + Val::F32(u) => ptr::write(p as *mut u32, *u), + Val::F64(u) => ptr::write(p as *mut u64, *u), + Val::V128(b) => ptr::write(p as *mut u128, *b), + _ => unimplemented!("Val::write_value_to"), + } + } + + pub(crate) unsafe fn read_value_from(p: *const i128, ty: ir::Type) -> Val { + match ty { + ir::types::I32 => Val::I32(ptr::read(p as *const i32)), + ir::types::I64 => Val::I64(ptr::read(p as *const i64)), + ir::types::F32 => Val::F32(ptr::read(p as *const u32)), + ir::types::F64 => Val::F64(ptr::read(p as *const u64)), + ir::types::I8X16 => Val::V128(ptr::read(p as *const u128)), + _ => unimplemented!("Val::read_value_from"), + } + } + + accessors! { + e + (I32(i32) i32 unwrap_i32 *e) + (I64(i64) i64 unwrap_i64 *e) + (F32(f32) f32 unwrap_f32 f32::from_bits(*e)) + (F64(f64) f64 unwrap_f64 f64::from_bits(*e)) + (FuncRef(&Func) funcref unwrap_funcref e) + (V128(u128) v128 unwrap_v128 *e) + } + + /// Attempt to access the underlying value of this `Val`, returning + /// `None` if it is not the correct type. + /// + /// This will return `Some` for both the `AnyRef` and `FuncRef` types. + pub fn anyref(&self) -> Option { + match self { + Val::AnyRef(e) => Some(e.clone()), + _ => None, + } + } + + /// Returns the underlying value of this `Val`, panicking if it's the + /// wrong type. + /// + /// # Panics + /// + /// Panics if `self` is not of the right type. + pub fn unwrap_anyref(&self) -> AnyRef { + self.anyref().expect("expected anyref") + } +} + +impl From for Val { + fn from(val: i32) -> Val { + Val::I32(val) + } +} + +impl From for Val { + fn from(val: i64) -> Val { + Val::I64(val) + } +} + +impl From for Val { + fn from(val: f32) -> Val { + Val::F32(val.to_bits()) + } +} + +impl From for Val { + fn from(val: f64) -> Val { + Val::F64(val.to_bits()) + } +} + +impl From for Val { + fn from(val: AnyRef) -> Val { + Val::AnyRef(val) + } +} + +impl From for Val { + fn from(val: Func) -> Val { + Val::FuncRef(val) + } +} + +pub(crate) fn into_checked_anyfunc( + val: Val, + store: &Store, +) -> Result { + Ok(match val { + Val::AnyRef(AnyRef::Null) => wasmtime_runtime::VMCallerCheckedAnyfunc { + func_ptr: ptr::null(), + type_index: wasmtime_runtime::VMSharedSignatureIndex::default(), + vmctx: ptr::null_mut(), + }, + Val::FuncRef(f) => { + let (vmctx, func_ptr, signature) = match f.wasmtime_export() { + wasmtime_runtime::Export::Function { + vmctx, + address, + signature, + } => (*vmctx, *address, signature), + _ => panic!("expected function export"), + }; + let type_index = store.compiler().signatures().register(signature); + wasmtime_runtime::VMCallerCheckedAnyfunc { + func_ptr, + type_index, + vmctx, + } + } + _ => bail!("val is not funcref"), + }) +} + +pub(crate) fn from_checked_anyfunc( + item: wasmtime_runtime::VMCallerCheckedAnyfunc, + store: &Store, +) -> Val { + if item.type_index == wasmtime_runtime::VMSharedSignatureIndex::default() { + return Val::AnyRef(AnyRef::Null); + } + let signature = store + .compiler() + .signatures() + .lookup(item.type_index) + .expect("signature"); + let instance_handle = unsafe { wasmtime_runtime::InstanceHandle::from_vmctx(item.vmctx) }; + let export = wasmtime_runtime::Export::Function { + address: item.func_ptr, + signature, + vmctx: item.vmctx, + }; + let f = Func::from_wasmtime_function(export, store, instance_handle); + Val::FuncRef(f) +} diff --git a/crates/api/src/windows.rs b/crates/api/src/windows.rs new file mode 100644 index 0000000000..962cae7458 --- /dev/null +++ b/crates/api/src/windows.rs @@ -0,0 +1,31 @@ +//! windows-specific extension for the `wasmtime` crate. +//! +//! This module is only available on Windows targets. +//! It is not available on Linux or macOS, for example. Note that the import path for +//! this module is `wasmtime::windows::...`, which is intended to emphasize that it +//! is platform-specific. +//! +//! The traits contained in this module are intended to extend various types +//! throughout the `wasmtime` crate with extra functionality that's only +//! available on Windows. + +use crate::Instance; + +/// Extensions for the [`Instance`] type only available on Windows. +pub trait InstanceExt { + /// Configures a custom signal handler to execute. + /// + /// TODO: needs more documentation. + unsafe fn set_signal_handler(&self, handler: H) + where + H: 'static + Fn(winapi::um::winnt::PEXCEPTION_POINTERS) -> bool; +} + +impl InstanceExt for Instance { + unsafe fn set_signal_handler(&self, handler: H) + where + H: 'static + Fn(winapi::um::winnt::PEXCEPTION_POINTERS) -> bool, + { + self.instance_handle.clone().set_signal_handler(handler); + } +} diff --git a/crates/api/tests/examples.rs b/crates/api/tests/examples.rs new file mode 100644 index 0000000000..24de77f3ed --- /dev/null +++ b/crates/api/tests/examples.rs @@ -0,0 +1,38 @@ +use std::env; +use std::process::{Command, Stdio}; + +fn run_example(name: &'static str) { + let cargo = env::var("CARGO").unwrap_or("cargo".to_string()); + let pkg_dir = env!("CARGO_MANIFEST_DIR"); + assert!( + Command::new(cargo) + .current_dir(pkg_dir) + .stdout(Stdio::null()) + .args(&["run", "-q", "--example", name]) + .status() + .expect("success") + .success(), + "failed to execute the example '{}'", + name, + ); +} + +#[test] +fn test_run_hello_example() { + run_example("hello"); +} + +#[test] +fn test_run_gcd_example() { + run_example("gcd"); +} + +#[test] +fn test_run_memory_example() { + run_example("memory"); +} + +#[test] +fn test_run_multi_example() { + run_example("multi"); +} diff --git a/crates/api/tests/externals.rs b/crates/api/tests/externals.rs new file mode 100644 index 0000000000..749af41e2b --- /dev/null +++ b/crates/api/tests/externals.rs @@ -0,0 +1,54 @@ +use wasmtime::*; + +#[test] +fn bad_globals() { + let ty = GlobalType::new(ValType::I32, Mutability::Var); + assert!(Global::new(&Store::default(), ty.clone(), Val::I64(0)).is_err()); + assert!(Global::new(&Store::default(), ty.clone(), Val::F32(0)).is_err()); + assert!(Global::new(&Store::default(), ty.clone(), Val::F64(0)).is_err()); + + let ty = GlobalType::new(ValType::I32, Mutability::Const); + let g = Global::new(&Store::default(), ty.clone(), Val::I32(0)).unwrap(); + assert!(g.set(Val::I32(1)).is_err()); + + let ty = GlobalType::new(ValType::I32, Mutability::Var); + let g = Global::new(&Store::default(), ty.clone(), Val::I32(0)).unwrap(); + assert!(g.set(Val::I64(0)).is_err()); +} + +#[test] +fn bad_tables() { + // i32 not supported yet + let ty = TableType::new(ValType::I32, Limits::new(0, Some(1))); + assert!(Table::new(&Store::default(), ty.clone(), Val::I32(0)).is_err()); + + // mismatched initializer + let ty = TableType::new(ValType::FuncRef, Limits::new(0, Some(1))); + assert!(Table::new(&Store::default(), ty.clone(), Val::I32(0)).is_err()); + + // get out of bounds + let ty = TableType::new(ValType::FuncRef, Limits::new(0, Some(1))); + let t = Table::new(&Store::default(), ty.clone(), Val::AnyRef(AnyRef::Null)).unwrap(); + assert!(t.get(0).is_none()); + assert!(t.get(u32::max_value()).is_none()); + + // set out of bounds or wrong type + let ty = TableType::new(ValType::FuncRef, Limits::new(1, Some(1))); + let t = Table::new(&Store::default(), ty.clone(), Val::AnyRef(AnyRef::Null)).unwrap(); + assert!(t.set(0, Val::I32(0)).is_err()); + assert!(t.set(0, Val::AnyRef(AnyRef::Null)).is_ok()); + assert!(t.set(1, Val::AnyRef(AnyRef::Null)).is_err()); + + // grow beyond max + let ty = TableType::new(ValType::FuncRef, Limits::new(1, Some(1))); + let t = Table::new(&Store::default(), ty.clone(), Val::AnyRef(AnyRef::Null)).unwrap(); + assert!(t.grow(0, Val::AnyRef(AnyRef::Null)).is_ok()); + assert!(t.grow(1, Val::AnyRef(AnyRef::Null)).is_err()); + assert_eq!(t.size(), 1); + + // grow wrong type + let ty = TableType::new(ValType::FuncRef, Limits::new(1, Some(2))); + let t = Table::new(&Store::default(), ty.clone(), Val::AnyRef(AnyRef::Null)).unwrap(); + assert!(t.grow(1, Val::I32(0)).is_err()); + assert_eq!(t.size(), 1); +} diff --git a/crates/api/tests/func.rs b/crates/api/tests/func.rs new file mode 100644 index 0000000000..dcb1a1d8c9 --- /dev/null +++ b/crates/api/tests/func.rs @@ -0,0 +1,291 @@ +use anyhow::Result; +use std::rc::Rc; +use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; +use wasmtime::{Callable, Func, FuncType, Instance, Module, Store, Trap, Val, ValType}; + +#[test] +fn func_constructors() { + let store = Store::default(); + Func::wrap0(&store, || {}); + Func::wrap1(&store, |_: i32| {}); + Func::wrap2(&store, |_: i32, _: i64| {}); + Func::wrap2(&store, |_: f32, _: f64| {}); + Func::wrap0(&store, || -> i32 { 0 }); + Func::wrap0(&store, || -> i64 { 0 }); + Func::wrap0(&store, || -> f32 { 0.0 }); + Func::wrap0(&store, || -> f64 { 0.0 }); + + Func::wrap0(&store, || -> Result<(), Trap> { loop {} }); + Func::wrap0(&store, || -> Result { loop {} }); + Func::wrap0(&store, || -> Result { loop {} }); + Func::wrap0(&store, || -> Result { loop {} }); + Func::wrap0(&store, || -> Result { loop {} }); +} + +#[test] +fn dtor_runs() { + static HITS: AtomicUsize = AtomicUsize::new(0); + + struct A; + + impl Drop for A { + fn drop(&mut self) { + HITS.fetch_add(1, SeqCst); + } + } + + let store = Store::default(); + let a = A; + assert_eq!(HITS.load(SeqCst), 0); + Func::wrap0(&store, move || { + drop(&a); + }); + assert_eq!(HITS.load(SeqCst), 1); +} + +#[test] +fn dtor_delayed() -> Result<()> { + static HITS: AtomicUsize = AtomicUsize::new(0); + + struct A; + + impl Drop for A { + fn drop(&mut self) { + HITS.fetch_add(1, SeqCst); + } + } + + let store = Store::default(); + let a = A; + let func = Func::wrap0(&store, move || drop(&a)); + + assert_eq!(HITS.load(SeqCst), 0); + let wasm = wat::parse_str(r#"(import "" "" (func))"#)?; + let module = Module::new(&store, &wasm)?; + let instance = Instance::new(&module, &[func.into()])?; + assert_eq!(HITS.load(SeqCst), 0); + drop(instance); + assert_eq!(HITS.load(SeqCst), 1); + Ok(()) +} + +#[test] +fn signatures_match() { + let store = Store::default(); + + let f = Func::wrap0(&store, || {}); + assert_eq!(f.ty().params(), &[]); + assert_eq!(f.ty().results(), &[]); + + let f = Func::wrap0(&store, || -> i32 { loop {} }); + assert_eq!(f.ty().params(), &[]); + assert_eq!(f.ty().results(), &[ValType::I32]); + + let f = Func::wrap0(&store, || -> i64 { loop {} }); + assert_eq!(f.ty().params(), &[]); + assert_eq!(f.ty().results(), &[ValType::I64]); + + let f = Func::wrap0(&store, || -> f32 { loop {} }); + assert_eq!(f.ty().params(), &[]); + assert_eq!(f.ty().results(), &[ValType::F32]); + + let f = Func::wrap0(&store, || -> f64 { loop {} }); + assert_eq!(f.ty().params(), &[]); + assert_eq!(f.ty().results(), &[ValType::F64]); + + let f = Func::wrap5(&store, |_: f32, _: f64, _: i32, _: i64, _: i32| -> f64 { + loop {} + }); + assert_eq!( + f.ty().params(), + &[ + ValType::F32, + ValType::F64, + ValType::I32, + ValType::I64, + ValType::I32 + ] + ); + assert_eq!(f.ty().results(), &[ValType::F64]); +} + +#[test] +fn import_works() -> Result<()> { + static HITS: AtomicUsize = AtomicUsize::new(0); + + let wasm = wat::parse_str( + r#" + (import "" "" (func)) + (import "" "" (func (param i32) (result i32))) + (import "" "" (func (param i32) (param i64))) + (import "" "" (func (param i32 i64 i32 f32 f64))) + + (func $foo + call 0 + i32.const 0 + call 1 + i32.const 1 + i32.add + i64.const 3 + call 2 + + i32.const 100 + i64.const 200 + i32.const 300 + f32.const 400 + f64.const 500 + call 3 + ) + (start $foo) + "#, + )?; + let store = Store::default(); + let module = Module::new(&store, &wasm)?; + Instance::new( + &module, + &[ + Func::wrap0(&store, || { + assert_eq!(HITS.fetch_add(1, SeqCst), 0); + }) + .into(), + Func::wrap1(&store, |x: i32| -> i32 { + assert_eq!(x, 0); + assert_eq!(HITS.fetch_add(1, SeqCst), 1); + 1 + }) + .into(), + Func::wrap2(&store, |x: i32, y: i64| { + assert_eq!(x, 2); + assert_eq!(y, 3); + assert_eq!(HITS.fetch_add(1, SeqCst), 2); + }) + .into(), + Func::wrap5(&store, |a: i32, b: i64, c: i32, d: f32, e: f64| { + assert_eq!(a, 100); + assert_eq!(b, 200); + assert_eq!(c, 300); + assert_eq!(d, 400.0); + assert_eq!(e, 500.0); + assert_eq!(HITS.fetch_add(1, SeqCst), 3); + }) + .into(), + ], + )?; + Ok(()) +} + +#[test] +fn trap_smoke() { + let store = Store::default(); + let f = Func::wrap0(&store, || -> Result<(), Trap> { Err(Trap::new("test")) }); + let err = f.call(&[]).unwrap_err(); + assert_eq!(err.message(), "test"); +} + +#[test] +fn trap_import() -> Result<()> { + let wasm = wat::parse_str( + r#" + (import "" "" (func)) + (start 0) + "#, + )?; + let store = Store::default(); + let module = Module::new(&store, &wasm)?; + let trap = Instance::new( + &module, + &[Func::wrap0(&store, || -> Result<(), Trap> { Err(Trap::new("foo")) }).into()], + ) + .err() + .unwrap() + .downcast::()?; + assert_eq!(trap.message(), "foo"); + Ok(()) +} + +#[test] +fn get_from_wrapper() { + let store = Store::default(); + let f = Func::wrap0(&store, || {}); + assert!(f.get0::<()>().is_ok()); + assert!(f.get0::().is_err()); + assert!(f.get1::<(), ()>().is_ok()); + assert!(f.get1::().is_err()); + assert!(f.get1::().is_err()); + assert!(f.get2::<(), (), ()>().is_ok()); + assert!(f.get2::().is_err()); + assert!(f.get2::().is_err()); + + let f = Func::wrap0(&store, || -> i32 { loop {} }); + assert!(f.get0::().is_ok()); + let f = Func::wrap0(&store, || -> f32 { loop {} }); + assert!(f.get0::().is_ok()); + let f = Func::wrap0(&store, || -> f64 { loop {} }); + assert!(f.get0::().is_ok()); + + let f = Func::wrap1(&store, |_: i32| {}); + assert!(f.get1::().is_ok()); + assert!(f.get1::().is_err()); + assert!(f.get1::().is_err()); + assert!(f.get1::().is_err()); + let f = Func::wrap1(&store, |_: i64| {}); + assert!(f.get1::().is_ok()); + let f = Func::wrap1(&store, |_: f32| {}); + assert!(f.get1::().is_ok()); + let f = Func::wrap1(&store, |_: f64| {}); + assert!(f.get1::().is_ok()); +} + +#[test] +fn get_from_signature() { + struct Foo; + impl Callable for Foo { + fn call(&self, _params: &[Val], _results: &mut [Val]) -> Result<(), Trap> { + panic!() + } + } + let store = Store::default(); + let ty = FuncType::new(Box::new([]), Box::new([])); + let f = Func::new(&store, ty, Rc::new(Foo)); + assert!(f.get0::<()>().is_ok()); + assert!(f.get0::().is_err()); + assert!(f.get1::().is_err()); + + let ty = FuncType::new(Box::new([ValType::I32]), Box::new([ValType::F64])); + let f = Func::new(&store, ty, Rc::new(Foo)); + assert!(f.get0::<()>().is_err()); + assert!(f.get0::().is_err()); + assert!(f.get1::().is_err()); + assert!(f.get1::().is_ok()); +} + +#[test] +fn get_from_module() -> anyhow::Result<()> { + let store = Store::default(); + let module = Module::new( + &store, + r#" + (module + (func (export "f0")) + (func (export "f1") (param i32)) + (func (export "f2") (result i32) + i32.const 0) + ) + + "#, + )?; + let instance = Instance::new(&module, &[])?; + let f0 = instance.get_export("f0").unwrap().func().unwrap(); + assert!(f0.get0::<()>().is_ok()); + assert!(f0.get0::().is_err()); + let f1 = instance.get_export("f1").unwrap().func().unwrap(); + assert!(f1.get0::<()>().is_err()); + assert!(f1.get1::().is_ok()); + assert!(f1.get1::().is_err()); + let f2 = instance.get_export("f2").unwrap().func().unwrap(); + assert!(f2.get0::<()>().is_err()); + assert!(f2.get0::().is_ok()); + assert!(f2.get1::().is_err()); + assert!(f2.get1::().is_err()); + Ok(()) +} diff --git a/crates/api/tests/globals.rs b/crates/api/tests/globals.rs new file mode 100644 index 0000000000..8f5406d398 --- /dev/null +++ b/crates/api/tests/globals.rs @@ -0,0 +1,93 @@ +use wasmtime::*; + +#[test] +fn smoke() -> anyhow::Result<()> { + let store = Store::default(); + let g = Global::new( + &store, + GlobalType::new(ValType::I32, Mutability::Const), + 0.into(), + )?; + assert_eq!(g.get().i32(), Some(0)); + assert!(g.set(0.into()).is_err()); + + let g = Global::new( + &store, + GlobalType::new(ValType::I32, Mutability::Const), + 1i32.into(), + )?; + assert_eq!(g.get().i32(), Some(1)); + + let g = Global::new( + &store, + GlobalType::new(ValType::I64, Mutability::Const), + 2i64.into(), + )?; + assert_eq!(g.get().i64(), Some(2)); + + let g = Global::new( + &store, + GlobalType::new(ValType::F32, Mutability::Const), + 3.0f32.into(), + )?; + assert_eq!(g.get().f32(), Some(3.0)); + + let g = Global::new( + &store, + GlobalType::new(ValType::F64, Mutability::Const), + 4.0f64.into(), + )?; + assert_eq!(g.get().f64(), Some(4.0)); + Ok(()) +} + +#[test] +fn mutability() -> anyhow::Result<()> { + let store = Store::default(); + let g = Global::new( + &store, + GlobalType::new(ValType::I32, Mutability::Var), + 0.into(), + )?; + assert_eq!(g.get().i32(), Some(0)); + g.set(1.into())?; + assert_eq!(g.get().i32(), Some(1)); + Ok(()) +} + +// Make sure that a global is still usable after its original instance is +// dropped. This is a bit of a weird test and really only fails depending on the +// implementation, but for now should hopefully be resilient enough to catch at +// least some cases of heap corruption. +#[test] +fn use_after_drop() -> anyhow::Result<()> { + let store = Store::default(); + let module = Module::new( + &store, + r#" + (module + (global (export "foo") (mut i32) (i32.const 100))) + "#, + )?; + let instance = Instance::new(&module, &[])?; + let g = instance.exports()[0].global().unwrap().clone(); + assert_eq!(g.get().i32(), Some(100)); + g.set(101.into())?; + drop(instance); + assert_eq!(g.get().i32(), Some(101)); + Instance::new(&module, &[])?; + assert_eq!(g.get().i32(), Some(101)); + drop(module); + assert_eq!(g.get().i32(), Some(101)); + drop(store); + assert_eq!(g.get().i32(), Some(101)); + + // spray some heap values + let mut x = Vec::new(); + for _ in 0..100 { + x.push("xy".to_string()); + } + drop(x); + assert_eq!(g.get().i32(), Some(101)); + Ok(()) +} diff --git a/crates/api/tests/host-segfault.rs b/crates/api/tests/host-segfault.rs new file mode 100644 index 0000000000..127a6d7943 --- /dev/null +++ b/crates/api/tests/host-segfault.rs @@ -0,0 +1,100 @@ +// To handle out-of-bounds reads and writes we use segfaults right now. We only +// want to catch a subset of segfaults, however, rather than all segfaults +// happening everywhere. The purpose of this test is to ensure that we *don't* +// catch segfaults if it happens in a random place in the code, but we instead +// bail out of our segfault handler early. +// +// This is sort of hard to test for but the general idea here is that we confirm +// that execution made it to our `segfault` function by printing something, and +// then we also make sure that stderr is empty to confirm that no weird panics +// happened or anything like that. + +use std::env; +use std::process::{Command, ExitStatus}; +use wasmtime::*; + +const VAR_NAME: &str = "__TEST_TO_RUN"; +const CONFIRM: &str = "well at least we ran up to the segfault\n"; + +fn segfault() -> ! { + unsafe { + print!("{}", CONFIRM); + *(0x4 as *mut i32) = 3; + unreachable!() + } +} + +fn main() { + let tests: &[(&str, fn())] = &[ + ("normal segfault", || segfault()), + ("make instance then segfault", || { + let store = Store::default(); + let module = Module::new(&store, "(module)").unwrap(); + let _instance = Instance::new(&module, &[]).unwrap(); + segfault(); + }), + ]; + match env::var(VAR_NAME) { + Ok(s) => { + let test = tests + .iter() + .find(|p| p.0 == s) + .expect("failed to find test") + .1; + test(); + } + Err(_) => { + for (name, _test) in tests { + runtest(name); + } + } + } +} + +fn runtest(name: &str) { + let me = env::current_exe().unwrap(); + let mut cmd = Command::new(me); + cmd.env(VAR_NAME, name); + let output = cmd.output().expect("failed to spawn subprocess"); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + let mut desc = format!("got status: {}", output.status); + if !stdout.trim().is_empty() { + desc.push_str("\nstdout: ----\n"); + desc.push_str(" "); + desc.push_str(&stdout.replace("\n", "\n ")); + } + if !stderr.trim().is_empty() { + desc.push_str("\nstderr: ----\n"); + desc.push_str(" "); + desc.push_str(&stderr.replace("\n", "\n ")); + } + if is_segfault(&output.status) { + assert!( + stdout.ends_with(CONFIRM) && stderr.is_empty(), + "failed to find confirmation in test `{}`\n{}", + name, + desc + ); + } else { + panic!("\n\nexpected a segfault on `{}`\n{}\n\n", name, desc); + } +} + +#[cfg(unix)] +fn is_segfault(status: &ExitStatus) -> bool { + use std::os::unix::prelude::*; + + match status.signal() { + Some(libc::SIGSEGV) | Some(libc::SIGBUS) => true, + _ => false, + } +} + +#[cfg(windows)] +fn is_segfault(status: &ExitStatus) -> bool { + match status.code().map(|s| s as u32) { + Some(0xc0000005) => true, + _ => false, + } +} diff --git a/crates/api/tests/import-indexes.rs b/crates/api/tests/import-indexes.rs new file mode 100644 index 0000000000..e1e0d41238 --- /dev/null +++ b/crates/api/tests/import-indexes.rs @@ -0,0 +1,67 @@ +use std::rc::Rc; +use wasmtime::*; + +#[test] +fn same_import_names_still_distinct() -> anyhow::Result<()> { + const WAT: &str = r#" +(module + (import "" "" (func $a (result i32))) + (import "" "" (func $b (result f32))) + (func (export "foo") (result i32) + call $a + call $b + i32.trunc_f32_u + i32.add) +) + "#; + + struct Ret1; + + impl Callable for Ret1 { + fn call(&self, params: &[Val], results: &mut [Val]) -> Result<(), Trap> { + assert!(params.is_empty()); + assert_eq!(results.len(), 1); + results[0] = 1i32.into(); + Ok(()) + } + } + + struct Ret2; + + impl Callable for Ret2 { + fn call(&self, params: &[Val], results: &mut [Val]) -> Result<(), Trap> { + assert!(params.is_empty()); + assert_eq!(results.len(), 1); + results[0] = 2.0f32.into(); + Ok(()) + } + } + + let store = Store::default(); + let module = Module::new(&store, WAT)?; + + let imports = [ + Func::new( + &store, + FuncType::new(Box::new([]), Box::new([ValType::I32])), + Rc::new(Ret1), + ) + .into(), + Func::new( + &store, + FuncType::new(Box::new([]), Box::new([ValType::F32])), + Rc::new(Ret2), + ) + .into(), + ]; + let instance = Instance::new(&module, &imports)?; + + let func = instance.get_export("foo").unwrap().func().unwrap(); + let results = func.call(&[])?; + assert_eq!(results.len(), 1); + match results[0] { + Val::I32(n) => assert_eq!(n, 3), + _ => panic!("unexpected type of return"), + } + Ok(()) +} diff --git a/crates/api/tests/import_calling_export.rs b/crates/api/tests/import_calling_export.rs new file mode 100644 index 0000000000..c755e9bb58 --- /dev/null +++ b/crates/api/tests/import_calling_export.rs @@ -0,0 +1,116 @@ +use std::cell::RefCell; +use std::rc::Rc; +use wasmtime::*; + +#[test] +fn test_import_calling_export() { + const WAT: &str = r#" + (module + (type $t0 (func)) + (import "" "imp" (func $.imp (type $t0))) + (func $run call $.imp) + (func $other) + (export "run" (func $run)) + (export "other" (func $other)) + ) + "#; + + struct Callback { + pub other: RefCell>, + } + + impl Callable for Callback { + fn call(&self, _params: &[Val], _results: &mut [Val]) -> Result<(), Trap> { + self.other + .borrow() + .as_ref() + .expect("expected a function ref") + .call(&[]) + .expect("expected function not to trap"); + Ok(()) + } + } + + let store = Store::default(); + let module = Module::new(&store, WAT).expect("failed to create module"); + + let callback = Rc::new(Callback { + other: RefCell::new(None), + }); + + let callback_func = Func::new( + &store, + FuncType::new(Box::new([]), Box::new([])), + callback.clone(), + ); + + let imports = vec![callback_func.into()]; + let instance = + Instance::new(&module, imports.as_slice()).expect("failed to instantiate module"); + + let exports = instance.exports(); + assert!(!exports.is_empty()); + + let run_func = exports[0] + .func() + .expect("expected a run func in the module"); + + *callback.other.borrow_mut() = Some( + exports[1] + .func() + .expect("expected an other func in the module") + .clone(), + ); + + run_func.call(&[]).expect("expected function not to trap"); +} + +#[test] +fn test_returns_incorrect_type() { + const WAT: &str = r#" + (module + (import "env" "evil" (func $evil (result i32))) + (func (export "run") (result i32) + (call $evil) + ) + ) + "#; + + struct EvilCallback; + + impl Callable for EvilCallback { + fn call(&self, _params: &[Val], results: &mut [Val]) -> Result<(), Trap> { + // Evil! Returns I64 here instead of promised in the signature I32. + results[0] = Val::I64(228); + Ok(()) + } + } + + let store = Store::default(); + let module = Module::new(&store, WAT).expect("failed to create module"); + + let callback = Rc::new(EvilCallback); + + let callback_func = Func::new( + &store, + FuncType::new(Box::new([]), Box::new([ValType::I32])), + callback.clone(), + ); + + let imports = vec![callback_func.into()]; + let instance = + Instance::new(&module, imports.as_slice()).expect("failed to instantiate module"); + + let exports = instance.exports(); + assert!(!exports.is_empty()); + + let run_func = exports[0] + .func() + .expect("expected a run func in the module"); + + let trap = run_func.call(&[]).expect_err("the execution should fail"); + assert_eq!( + trap.message(), + "`Callable` attempted to return an incompatible value" + ); +} diff --git a/crates/api/tests/invoke_func_via_table.rs b/crates/api/tests/invoke_func_via_table.rs new file mode 100644 index 0000000000..28ba779ec9 --- /dev/null +++ b/crates/api/tests/invoke_func_via_table.rs @@ -0,0 +1,32 @@ +use anyhow::{Context as _, Result}; +use wasmtime::*; + +#[test] +fn test_invoke_func_via_table() -> Result<()> { + let store = Store::default(); + + let wat = r#" + (module + (func $f (result i64) (i64.const 42)) + + (table (export "table") 1 1 anyfunc) + (elem (i32.const 0) $f) + ) + "#; + let module = Module::new(&store, wat).context("> Error compiling module!")?; + let instance = Instance::new(&module, &[]).context("> Error instantiating module!")?; + + let f = instance + .get_export("table") + .unwrap() + .table() + .unwrap() + .get(0) + .unwrap() + .funcref() + .unwrap() + .clone(); + let result = f.call(&[]).unwrap(); + assert_eq!(result[0].unwrap_i64(), 42); + Ok(()) +} diff --git a/crates/api/tests/name.rs b/crates/api/tests/name.rs new file mode 100644 index 0000000000..d6cea5a50c --- /dev/null +++ b/crates/api/tests/name.rs @@ -0,0 +1,34 @@ +use wasmtime::*; + +#[test] +fn test_module_no_name() -> anyhow::Result<()> { + let store = Store::default(); + let wat = r#" + (module + (func (export "run") (nop)) + ) + "#; + + let module = Module::new(&store, wat)?; + assert_eq!(module.name(), None); + + Ok(()) +} + +#[test] +fn test_module_name() -> anyhow::Result<()> { + let store = Store::default(); + let wat = r#" + (module $from_name_section + (func (export "run") (nop)) + ) + "#; + + let module = Module::new(&store, wat)?; + assert_eq!(module.name(), Some("from_name_section")); + + let module = Module::new_with_name(&store, wat, "override")?; + assert_eq!(module.name(), Some("override")); + + Ok(()) +} diff --git a/crates/api/tests/traps.rs b/crates/api/tests/traps.rs new file mode 100644 index 0000000000..5ef3bf2644 --- /dev/null +++ b/crates/api/tests/traps.rs @@ -0,0 +1,404 @@ +use anyhow::Result; +use std::panic::{self, AssertUnwindSafe}; +use std::rc::Rc; +use wasmtime::*; + +#[test] +fn test_trap_return() -> Result<()> { + struct HelloCallback; + + impl Callable for HelloCallback { + fn call(&self, _params: &[Val], _results: &mut [Val]) -> Result<(), Trap> { + Err(Trap::new("test 123")) + } + } + + let store = Store::default(); + let wat = r#" + (module + (func $hello (import "" "hello")) + (func (export "run") (call $hello)) + ) + "#; + + let module = Module::new(&store, wat)?; + let hello_type = FuncType::new(Box::new([]), Box::new([])); + let hello_func = Func::new(&store, hello_type, Rc::new(HelloCallback)); + + let instance = Instance::new(&module, &[hello_func.into()])?; + let run_func = instance.exports()[0] + .func() + .expect("expected function export"); + + let e = run_func.call(&[]).err().expect("error calling function"); + + assert_eq!(e.message(), "test 123"); + + Ok(()) +} + +#[test] +fn test_trap_trace() -> Result<()> { + let store = Store::default(); + let wat = r#" + (module $hello_mod + (func (export "run") (call $hello)) + (func $hello (unreachable)) + ) + "#; + + let module = Module::new(&store, wat)?; + let instance = Instance::new(&module, &[])?; + let run_func = instance.exports()[0] + .func() + .expect("expected function export"); + + let e = run_func.call(&[]).err().expect("error calling function"); + + let trace = e.trace(); + assert_eq!(trace.len(), 2); + assert_eq!(trace[0].module_name().unwrap(), "hello_mod"); + assert_eq!(trace[0].func_index(), 1); + assert_eq!(trace[0].func_name(), Some("hello")); + assert_eq!(trace[1].module_name().unwrap(), "hello_mod"); + assert_eq!(trace[1].func_index(), 0); + assert_eq!(trace[1].func_name(), None); + assert!( + e.message().contains("unreachable"), + "wrong message: {}", + e.message() + ); + + Ok(()) +} + +#[test] +fn test_trap_trace_cb() -> Result<()> { + struct ThrowCallback; + + impl Callable for ThrowCallback { + fn call(&self, _params: &[Val], _results: &mut [Val]) -> Result<(), Trap> { + Err(Trap::new("cb throw")) + } + } + + let store = Store::default(); + let wat = r#" + (module $hello_mod + (import "" "throw" (func $throw)) + (func (export "run") (call $hello)) + (func $hello (call $throw)) + ) + "#; + + let fn_type = FuncType::new(Box::new([]), Box::new([])); + let fn_func = Func::new(&store, fn_type, Rc::new(ThrowCallback)); + + let module = Module::new(&store, wat)?; + let instance = Instance::new(&module, &[fn_func.into()])?; + let run_func = instance.exports()[0] + .func() + .expect("expected function export"); + + let e = run_func.call(&[]).err().expect("error calling function"); + + let trace = e.trace(); + assert_eq!(trace.len(), 2); + assert_eq!(trace[0].module_name().unwrap(), "hello_mod"); + assert_eq!(trace[0].func_index(), 2); + assert_eq!(trace[1].module_name().unwrap(), "hello_mod"); + assert_eq!(trace[1].func_index(), 1); + assert_eq!(e.message(), "cb throw"); + + Ok(()) +} + +#[test] +fn test_trap_stack_overflow() -> Result<()> { + let store = Store::default(); + let wat = r#" + (module $rec_mod + (func $run (export "run") (call $run)) + ) + "#; + + let module = Module::new(&store, wat)?; + let instance = Instance::new(&module, &[])?; + let run_func = instance.exports()[0] + .func() + .expect("expected function export"); + + let e = run_func.call(&[]).err().expect("error calling function"); + + let trace = e.trace(); + assert!(trace.len() >= 32); + for i in 0..trace.len() { + assert_eq!(trace[i].module_name().unwrap(), "rec_mod"); + assert_eq!(trace[i].func_index(), 0); + assert_eq!(trace[i].func_name(), Some("run")); + } + assert!(e.message().contains("call stack exhausted")); + + Ok(()) +} + +#[test] +fn trap_display_pretty() -> Result<()> { + let store = Store::default(); + let wat = r#" + (module $m + (func $die unreachable) + (func call $die) + (func $foo call 1) + (func (export "bar") call $foo) + ) + "#; + + let module = Module::new(&store, wat)?; + let instance = Instance::new(&module, &[])?; + let run_func = instance.exports()[0] + .func() + .expect("expected function export"); + + let e = run_func.call(&[]).err().expect("error calling function"); + assert_eq!( + e.to_string(), + "\ +wasm trap: unreachable, source location: @0023 +wasm backtrace: + 0: m!die + 1: m! + 2: m!foo + 3: m! +" + ); + Ok(()) +} + +#[test] +fn trap_display_multi_module() -> Result<()> { + let store = Store::default(); + let wat = r#" + (module $a + (func $die unreachable) + (func call $die) + (func $foo call 1) + (func (export "bar") call $foo) + ) + "#; + + let module = Module::new(&store, wat)?; + let instance = Instance::new(&module, &[])?; + let bar = instance.exports()[0].clone(); + + let wat = r#" + (module $b + (import "" "" (func $bar)) + (func $middle call $bar) + (func (export "bar2") call $middle) + ) + "#; + let module = Module::new(&store, wat)?; + let instance = Instance::new(&module, &[bar])?; + let bar2 = instance.exports()[0] + .func() + .expect("expected function export"); + + let e = bar2.call(&[]).err().expect("error calling function"); + assert_eq!( + e.to_string(), + "\ +wasm trap: unreachable, source location: @0023 +wasm backtrace: + 0: a!die + 1: a! + 2: a!foo + 3: a! + 4: b!middle + 5: b! +" + ); + Ok(()) +} + +#[test] +fn trap_start_function_import() -> Result<()> { + struct ReturnTrap; + + impl Callable for ReturnTrap { + fn call(&self, _params: &[Val], _results: &mut [Val]) -> Result<(), Trap> { + Err(Trap::new("user trap")) + } + } + + let store = Store::default(); + let binary = wat::parse_str( + r#" + (module $a + (import "" "" (func $foo)) + (start $foo) + ) + "#, + )?; + + let module = Module::new(&store, &binary)?; + let sig = FuncType::new(Box::new([]), Box::new([])); + let func = Func::new(&store, sig, Rc::new(ReturnTrap)); + let err = Instance::new(&module, &[func.into()]).err().unwrap(); + assert_eq!(err.downcast_ref::().unwrap().message(), "user trap"); + Ok(()) +} + +#[test] +fn rust_panic_import() -> Result<()> { + struct Panic; + + impl Callable for Panic { + fn call(&self, _params: &[Val], _results: &mut [Val]) -> Result<(), Trap> { + panic!("this is a panic"); + } + } + + let store = Store::default(); + let binary = wat::parse_str( + r#" + (module $a + (import "" "" (func $foo)) + (import "" "" (func $bar)) + (func (export "foo") call $foo) + (func (export "bar") call $bar) + ) + "#, + )?; + + let module = Module::new(&store, &binary)?; + let sig = FuncType::new(Box::new([]), Box::new([])); + let func = Func::new(&store, sig, Rc::new(Panic)); + let instance = Instance::new( + &module, + &[ + func.into(), + Func::wrap0(&store, || panic!("this is another panic")).into(), + ], + )?; + let func = instance.exports()[0].func().unwrap().clone(); + let err = panic::catch_unwind(AssertUnwindSafe(|| { + drop(func.call(&[])); + })) + .unwrap_err(); + assert_eq!(err.downcast_ref::<&'static str>(), Some(&"this is a panic")); + + let func = instance.exports()[1].func().unwrap().clone(); + let err = panic::catch_unwind(AssertUnwindSafe(|| { + drop(func.call(&[])); + })) + .unwrap_err(); + assert_eq!( + err.downcast_ref::<&'static str>(), + Some(&"this is another panic") + ); + Ok(()) +} + +#[test] +fn rust_panic_start_function() -> Result<()> { + struct Panic; + + impl Callable for Panic { + fn call(&self, _params: &[Val], _results: &mut [Val]) -> Result<(), Trap> { + panic!("this is a panic"); + } + } + + let store = Store::default(); + let binary = wat::parse_str( + r#" + (module $a + (import "" "" (func $foo)) + (start $foo) + ) + "#, + )?; + + let module = Module::new(&store, &binary)?; + let sig = FuncType::new(Box::new([]), Box::new([])); + let func = Func::new(&store, sig, Rc::new(Panic)); + let err = panic::catch_unwind(AssertUnwindSafe(|| { + drop(Instance::new(&module, &[func.into()])); + })) + .unwrap_err(); + assert_eq!(err.downcast_ref::<&'static str>(), Some(&"this is a panic")); + + let func = Func::wrap0(&store, || panic!("this is another panic")); + let err = panic::catch_unwind(AssertUnwindSafe(|| { + drop(Instance::new(&module, &[func.into()])); + })) + .unwrap_err(); + assert_eq!( + err.downcast_ref::<&'static str>(), + Some(&"this is another panic") + ); + Ok(()) +} + +#[test] +fn mismatched_arguments() -> Result<()> { + let store = Store::default(); + let binary = wat::parse_str( + r#" + (module $a + (func (export "foo") (param i32)) + ) + "#, + )?; + + let module = Module::new(&store, &binary)?; + let instance = Instance::new(&module, &[])?; + let func = instance.exports()[0].func().unwrap().clone(); + assert_eq!( + func.call(&[]).unwrap_err().message(), + "expected 1 arguments, got 0" + ); + assert_eq!( + func.call(&[Val::F32(0)]).unwrap_err().message(), + "argument type mismatch", + ); + assert_eq!( + func.call(&[Val::I32(0), Val::I32(1)]) + .unwrap_err() + .message(), + "expected 1 arguments, got 2" + ); + Ok(()) +} + +#[test] +fn call_signature_mismatch() -> Result<()> { + let store = Store::default(); + let binary = wat::parse_str( + r#" + (module $a + (func $foo + i32.const 0 + call_indirect) + (func $bar (param i32)) + (start $foo) + + (table 1 anyfunc) + (elem (i32.const 0) 1) + ) + "#, + )?; + + let module = Module::new(&store, &binary)?; + let err = Instance::new(&module, &[]) + .err() + .unwrap() + .downcast::() + .unwrap(); + assert_eq!( + err.message(), + "wasm trap: indirect call type mismatch, source location: @0030" + ); + Ok(()) +} diff --git a/crates/c-api/Cargo.toml b/crates/c-api/Cargo.toml new file mode 100644 index 0000000000..3b17fcce03 --- /dev/null +++ b/crates/c-api/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "wasmtime-c-api" +version = "0.12.0" +authors = ["The Wasmtime Project Developers"] +description = "C API to expose the Wasmtime runtime" +license = "Apache-2.0 WITH LLVM-exception" +repository = "https://github.com/bytecodealliance/wasmtime" +readme = "README.md" +edition = "2018" +publish = false + +[lib] +name = "wasmtime" +crate-type = ["staticlib", "cdylib"] +doc = false +test = false +doctest = false + +[dependencies] +wasmtime = { path = "../api" } +wasi-common = { path = "../wasi-common" } +wasmtime-wasi = { path = "../wasi" } diff --git a/crates/c-api/LICENSE b/crates/c-api/LICENSE new file mode 100644 index 0000000000..f9d81955f4 --- /dev/null +++ b/crates/c-api/LICENSE @@ -0,0 +1,220 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + diff --git a/crates/c-api/README.md b/crates/c-api/README.md new file mode 100644 index 0000000000..b0edded9c2 --- /dev/null +++ b/crates/c-api/README.md @@ -0,0 +1,3 @@ +# Implementation of wasm-c-api in Rust + +https://github.com/WebAssembly/wasm-c-api diff --git a/crates/c-api/examples/Makefile b/crates/c-api/examples/Makefile new file mode 100644 index 0000000000..b309e80484 --- /dev/null +++ b/crates/c-api/examples/Makefile @@ -0,0 +1,171 @@ +############################################################################### +# Configuration + +# Inherited from wasm-c-api/Makefile to just run C examples + +WASM_FLAGS = -DWASM_API_DEBUG # -DWASM_API_DEBUG_LOG +C_FLAGS = ${WASM_FLAGS} -Wall -Werror -ggdb -O -fsanitize=address +CC_FLAGS = -std=c++11 ${C_FLAGS} +LD_FLAGS = -fsanitize-memory-track-origins -fsanitize-memory-use-after-dtor + +C_COMP = clang + +WASMTIME_API_MODE = debug + + +# Base directories +WASMTIME_API_DIR = .. +WASM_DIR = wasm-c-api +EXAMPLE_DIR = ${WASM_DIR}/example +OUT_DIR = ${WASM_DIR}/out + +# Example config +EXAMPLE_OUT = ${OUT_DIR}/example +EXAMPLES = \ + hello \ + callback \ + trap \ + start \ + reflect \ + global \ + table \ + memory \ + hostref \ + finalize \ + serialize \ + threads \ + # multi \ + +# Wasm config +WASM_INCLUDE = ${WASM_DIR}/include +WASM_SRC = ${WASM_DIR}/src +WASM_OUT = ${OUT_DIR} +WASM_C_LIBS = wasm-bin wasm-rust-api +WASM_CC_LIBS = $(error unsupported C++) + + +# Compiler config +ifeq (${WASMTIME_API_MODE},release) + CARGO_BUILD_FLAGS = --release +else + CARGO_BUILD_FLAGS = +endif + +ifeq (${C_COMP},clang) + CC_COMP = clang++ + LD_GROUP_START = + LD_GROUP_END = +else ifeq (${C_COMP},gcc) + CC_COMP = g++ + LD_GROUP_START = -Wl,--start-group + LD_GROUP_END = -Wl,--end-group +else + $(error C_COMP set to unknown compiler, must be clang or gcc) +endif + +WASMTIME_BIN_DIR = ${WASMTIME_API_DIR}/../../target/${WASMTIME_API_MODE} +WASMTIME_C_LIB_DIR = ${WASMTIME_BIN_DIR} +WASMTIME_C_LIBS = wasmtime +WASMTIME_CC_LIBS = $(error unsupported c++) + +WASMTIME_C_BINS = ${WASMTIME_C_LIBS:%=${WASMTIME_C_LIB_DIR}/lib%.a} + +############################################################################### +# Examples +# +# To build Wasm APIs and run all examples: +# make all +# +# To run only C examples: +# make c +# +# To run only C++ examples: +# make cc +# +# To run individual C example (e.g. hello): +# make run-hello-c +# +# To run individual C++ example (e.g. hello): +# make run-hello-cc +# + +.PHONY: all cc c +all: cc c +cc: ${EXAMPLES:%=run-%-cc} +c: ${EXAMPLES:%=run-%-c} + +# Running a C / C++ example +run-%-c: ${EXAMPLE_OUT}/%-c ${EXAMPLE_OUT}/%.wasm + @echo ==== C ${@:run-%-c=%} ====; \ + cd ${EXAMPLE_OUT}; ./${@:run-%=%} + @echo ==== Done ==== + +run-%-cc: ${EXAMPLE_OUT}/%-cc ${EXAMPLE_OUT}/%.wasm + @echo ==== C++ ${@:run-%-cc=%} ====; \ + cd ${EXAMPLE_OUT}; ./${@:run-%=%} + @echo ==== Done ==== + +# Compiling C / C++ example +${EXAMPLE_OUT}/%-c.o: ${EXAMPLE_DIR}/%.c ${WASM_INCLUDE}/wasm.h + mkdir -p ${EXAMPLE_OUT} + ${C_COMP} -c ${C_FLAGS} -I. -I${WASM_INCLUDE} $< -o $@ + +${EXAMPLE_OUT}/%-cc.o: ${EXAMPLE_DIR}/%.cc ${WASM_INCLUDE}/wasm.hh + mkdir -p ${EXAMPLE_OUT} + ${CC_COMP} -c ${CC_FLAGS} -I. -I${WASM_INCLUDE} $< -o $@ + +# Linking C / C++ example +.PRECIOUS: ${EXAMPLES:%=${EXAMPLE_OUT}/%-c} +${EXAMPLE_OUT}/%-c: ${EXAMPLE_OUT}/%-c.o ${WASMTIME_C_BINS} + ${CC_COMP} ${CC_FLAGS} ${LD_FLAGS} $< -o $@ \ + ${LD_GROUP_START} \ + ${WASMTIME_C_BINS} \ + ${LD_GROUP_END} \ + -ldl -pthread + +# Installing Wasm binaries +.PRECIOUS: ${EXAMPLES:%=${EXAMPLE_OUT}/%.wasm} +${EXAMPLE_OUT}/%.wasm: ${EXAMPLE_DIR}/%.wasm + cp $< $@ + +############################################################################### +# Wasm C / C++ API +# +# To build both C / C++ APIs: +# make wasm + +.PHONY: wasm wasm-c wasm-cc +wasm: wasm-c wasm-cc +wasm-c: ${WASMTIME_C_BIN} +wasm-cc: ${WASMTIME_CC_BIN} + +${WASMTIME_C_BINS}: CARGO_RUN + cd ${WASMTIME_API_DIR}; cargo build --lib ${CARGO_BUILD_FLAGS} + +.PHONY: CARGO_RUN +CARGO_RUN: + + +############################################################################### +# Clean-up + +.PHONY: clean +clean: + rm -rf ${OUT_DIR} + +############################################################################### +# Other examples + +WASM_EXT_INCLUDE = ${WASMTIME_API_DIR}/include + +run-config-debug-c: ${EXAMPLE_OUT}/config-debug-c ${EXAMPLE_OUT}/fib-wasm.wasm + @echo ==== C config ====; \ + cd ${EXAMPLE_OUT}; ./config-debug-c + @echo ==== Done ==== + +${EXAMPLE_OUT}/fib-wasm.wasm: fib-wasm.wasm + cp $< $@ + +${EXAMPLE_OUT}/config-debug-c.o: config-debug.c ${WASM_INCLUDE}/wasm.h ${WASM_EXT_INCLUDE}/wasmtime.h + mkdir -p ${EXAMPLE_OUT} + ${C_COMP} -c ${C_FLAGS} -I. -I${WASM_INCLUDE} -I${WASM_EXT_INCLUDE} $< -o $@ diff --git a/crates/c-api/examples/config-debug.c b/crates/c-api/examples/config-debug.c new file mode 100644 index 0000000000..650d9645c9 --- /dev/null +++ b/crates/c-api/examples/config-debug.c @@ -0,0 +1,99 @@ +#include +#include +#include +#include + +#include +#include "wasmtime.h" + +#define own + +int main(int argc, const char* argv[]) { + // Configuring engine to support generating of DWARF info. + // lldb can be used to attach to the program and observe + // original fib-wasm.c source code and variables. + wasm_config_t* config = wasm_config_new(); + wasmtime_config_debug_info_set(config, true); + + // Initialize. + printf("Initializing...\n"); + wasm_engine_t* engine = wasm_engine_new_with_config(config); + wasm_store_t* store = wasm_store_new(engine); + + // Load binary. + printf("Loading binary...\n"); + FILE* file = fopen("fib-wasm.wasm", "r"); + if (!file) { + printf("> Error loading module!\n"); + return 1; + } + fseek(file, 0L, SEEK_END); + size_t file_size = ftell(file); + fseek(file, 0L, SEEK_SET); + wasm_byte_vec_t binary; + wasm_byte_vec_new_uninitialized(&binary, file_size); + if (fread(binary.data, file_size, 1, file) != 1) { + printf("> Error loading module!\n"); + return 1; + } + fclose(file); + + // Compile. + printf("Compiling module...\n"); + own wasm_module_t* module = wasm_module_new(store, &binary); + if (!module) { + printf("> Error compiling module!\n"); + return 1; + } + + wasm_byte_vec_delete(&binary); + + // Instantiate. + printf("Instantiating module...\n"); + own wasm_instance_t* instance = + wasm_instance_new(store, module, NULL, NULL); + if (!instance) { + printf("> Error instantiating module!\n"); + return 1; + } + + // Extract export. + printf("Extracting export...\n"); + own wasm_extern_vec_t exports; + wasm_instance_exports(instance, &exports); + if (exports.size == 0) { + printf("> Error accessing exports!\n"); + return 1; + } + // Getting second export (first is memory). + const wasm_func_t* run_func = wasm_extern_as_func(exports.data[1]); + if (run_func == NULL) { + printf("> Error accessing export!\n"); + return 1; + } + + wasm_module_delete(module); + wasm_instance_delete(instance); + + // Call. + printf("Calling fib...\n"); + wasm_val_t params[1] = { {.kind = WASM_I32, .of = {.i32 = 6}} }; + wasm_val_t results[1]; + if (wasm_func_call(run_func, params, results)) { + printf("> Error calling function!\n"); + return 1; + } + + wasm_extern_vec_delete(&exports); + + printf("> fib(6) = %d\n", results[0].of.i32); + + // Shut down. + printf("Shutting down...\n"); + wasm_store_delete(store); + wasm_engine_delete(engine); + + // All done. + printf("Done.\n"); + return 0; +} diff --git a/crates/c-api/examples/fib-wasm.c b/crates/c-api/examples/fib-wasm.c new file mode 100644 index 0000000000..20c06f5efa --- /dev/null +++ b/crates/c-api/examples/fib-wasm.c @@ -0,0 +1,13 @@ +// Compile with: +// clang --target=wasm32 fib-wasm.c -o fib-wasm.wasm -g \ +// -Wl,--no-entry,--export=fib -nostdlib -fdebug-prefix-map=$PWD=. + +int fib(int n) { + int i, t, a = 0, b = 1; + for (i = 0; i < n; i++) { + t = a; + a = b; + b += t; + } + return b; +} diff --git a/crates/c-api/examples/fib-wasm.wasm b/crates/c-api/examples/fib-wasm.wasm new file mode 100755 index 0000000000..0a1ebac429 Binary files /dev/null and b/crates/c-api/examples/fib-wasm.wasm differ diff --git a/crates/c-api/examples/wasm-c-api b/crates/c-api/examples/wasm-c-api new file mode 160000 index 0000000000..d9a80099d4 --- /dev/null +++ b/crates/c-api/examples/wasm-c-api @@ -0,0 +1 @@ +Subproject commit d9a80099d496b5cdba6f3fe8fc77586e0e505ddc diff --git a/crates/c-api/include/wasi.h b/crates/c-api/include/wasi.h new file mode 100644 index 0000000000..e1a7ffc5dc --- /dev/null +++ b/crates/c-api/include/wasi.h @@ -0,0 +1,70 @@ +// WASI C API + +#ifndef WASI_H +#define WASI_H + +#include "wasm.h" + +#ifndef WASI_API_EXTERN +#ifdef _WIN32 +#define WASI_API_EXTERN __declspec(dllimport) +#else +#define WASI_API_EXTERN +#endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#define own + +#define WASI_DECLARE_OWN(name) \ + typedef struct wasi_##name##_t wasi_##name##_t; \ + WASI_API_EXTERN void wasi_##name##_delete(own wasi_##name##_t*); + +// WASI config + +WASI_DECLARE_OWN(config) + +WASI_API_EXTERN own wasi_config_t* wasi_config_new(); + +WASI_API_EXTERN void wasi_config_set_argv(wasi_config_t* config, int argc, const char* argv[]); +WASI_API_EXTERN void wasi_config_inherit_argv(wasi_config_t* config); + +WASI_API_EXTERN void wasi_config_set_env(wasi_config_t* config, int envc, const char* names[], const char* values[]); +WASI_API_EXTERN void wasi_config_inherit_env(wasi_config_t* config); + +WASI_API_EXTERN bool wasi_config_set_stdin_file(wasi_config_t* config, const char* path); +WASI_API_EXTERN void wasi_config_inherit_stdin(wasi_config_t* config); + +WASI_API_EXTERN bool wasi_config_set_stdout_file(wasi_config_t* config, const char* path); +WASI_API_EXTERN void wasi_config_inherit_stdout(wasi_config_t* config); + +WASI_API_EXTERN bool wasi_config_set_stderr_file(wasi_config_t* config, const char* path); +WASI_API_EXTERN void wasi_config_inherit_stderr(wasi_config_t* config); + +WASI_API_EXTERN bool wasi_config_preopen_dir(wasi_config_t* config, const char* path, const char* guest_path); + +// WASI instance + +WASI_DECLARE_OWN(instance) + +WASI_API_EXTERN own wasi_instance_t* wasi_instance_new( + wasm_store_t* store, + own wasi_config_t* config, + own wasm_trap_t** trap +); + +WASI_API_EXTERN const wasm_extern_t* wasi_instance_bind_import( + const wasi_instance_t* instance, + const wasm_importtype_t* import +); + +#undef own + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // #ifdef WASI_H \ No newline at end of file diff --git a/crates/c-api/include/wasmtime.h b/crates/c-api/include/wasmtime.h new file mode 100644 index 0000000000..16b463bb5a --- /dev/null +++ b/crates/c-api/include/wasmtime.h @@ -0,0 +1,45 @@ +// WebAssembly C API extension for Wasmtime + +#ifndef WASMTIME_API_H +#define WASMTIME_API_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef uint8_t wasmtime_strategy_t; +enum wasmtime_strategy_enum { // Strategy + WASMTIME_STRATEGY_AUTO, + WASMTIME_STRATEGY_CRANELIFT, + WASMTIME_STRATEGY_LIGHTBEAM, +}; + +typedef uint8_t wasmtime_opt_level_t; +enum wasmtime_opt_level_enum { // OptLevel + WASMTIME_OPT_LEVEL_NONE, + WASMTIME_OPT_LEVEL_SPEED, + WASMTIME_OPT_LEVEL_SPEED_AND_SIZE, +}; + +#define WASMTIME_CONFIG_PROP(name, ty) \ + WASM_API_EXTERN void wasmtime_config_##name##_set(wasm_config_t*, ty); + +WASMTIME_CONFIG_PROP(debug_info, bool) +WASMTIME_CONFIG_PROP(wasm_threads, bool) +WASMTIME_CONFIG_PROP(wasm_reference_types, bool) +WASMTIME_CONFIG_PROP(wasm_simd, bool) +WASMTIME_CONFIG_PROP(wasm_bulk_memory, bool) +WASMTIME_CONFIG_PROP(wasm_multi_value, bool) +WASMTIME_CONFIG_PROP(strategy, wasmtime_strategy_t) +WASMTIME_CONFIG_PROP(cranelift_debug_verifier, bool) +WASMTIME_CONFIG_PROP(cranelift_opt_level, wasmtime_opt_level_t) + +/////////////////////////////////////////////////////////////////////////////// + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // WASMTIME_API_H diff --git a/crates/c-api/src/ext.rs b/crates/c-api/src/ext.rs new file mode 100644 index 0000000000..549bb1e604 --- /dev/null +++ b/crates/c-api/src/ext.rs @@ -0,0 +1,88 @@ +//! This file defines the extern "C" API extension, which are specific +//! to the wasmtime implementation. + +use crate::wasm_config_t; +use wasmtime::{OptLevel, Strategy}; + +#[repr(u8)] +#[derive(Clone)] +pub enum wasmtime_strategy_t { + WASMTIME_STRATEGY_AUTO, + WASMTIME_STRATEGY_CRANELIFT, + WASMTIME_STRATEGY_LIGHTBEAM, +} + +#[repr(u8)] +#[derive(Clone)] +pub enum wasmtime_opt_level_t { + WASMTIME_OPT_LEVEL_NONE, + WASMTIME_OPT_LEVEL_SPEED, + WASMTIME_OPT_LEVEL_SPEED_AND_SIZE, +} + +#[no_mangle] +pub unsafe extern "C" fn wasmtime_config_debug_info_set(c: *mut wasm_config_t, enable: bool) { + (*c).config.debug_info(enable); +} + +#[no_mangle] +pub unsafe extern "C" fn wasmtime_config_wasm_threads_set(c: *mut wasm_config_t, enable: bool) { + (*c).config.wasm_threads(enable); +} + +#[no_mangle] +pub unsafe extern "C" fn wasmtime_config_wasm_reference_types_set( + c: *mut wasm_config_t, + enable: bool, +) { + (*c).config.wasm_reference_types(enable); +} + +#[no_mangle] +pub unsafe extern "C" fn wasmtime_config_wasm_simd_set(c: *mut wasm_config_t, enable: bool) { + (*c).config.wasm_simd(enable); +} + +#[no_mangle] +pub unsafe extern "C" fn wasmtime_config_wasm_bulk_memory_set(c: *mut wasm_config_t, enable: bool) { + (*c).config.wasm_bulk_memory(enable); +} + +#[no_mangle] +pub unsafe extern "C" fn wasmtime_config_wasm_multi_value_set(c: *mut wasm_config_t, enable: bool) { + (*c).config.wasm_multi_value(enable); +} + +#[no_mangle] +pub unsafe extern "C" fn wasmtime_config_strategy_set( + c: *mut wasm_config_t, + strategy: wasmtime_strategy_t, +) { + use wasmtime_strategy_t::*; + drop((*c).config.strategy(match strategy { + WASMTIME_STRATEGY_AUTO => Strategy::Auto, + WASMTIME_STRATEGY_CRANELIFT => Strategy::Cranelift, + WASMTIME_STRATEGY_LIGHTBEAM => Strategy::Lightbeam, + })); +} + +#[no_mangle] +pub unsafe extern "C" fn wasmtime_config_cranelift_debug_verifier_set( + c: *mut wasm_config_t, + enable: bool, +) { + (*c).config.cranelift_debug_verifier(enable); +} + +#[no_mangle] +pub unsafe extern "C" fn wasmtime_config_cranelift_opt_level_set( + c: *mut wasm_config_t, + opt_level: wasmtime_opt_level_t, +) { + use wasmtime_opt_level_t::*; + (*c).config.cranelift_opt_level(match opt_level { + WASMTIME_OPT_LEVEL_NONE => OptLevel::None, + WASMTIME_OPT_LEVEL_SPEED => OptLevel::Speed, + WASMTIME_OPT_LEVEL_SPEED_AND_SIZE => OptLevel::SpeedAndSize, + }); +} diff --git a/crates/c-api/src/lib.rs b/crates/c-api/src/lib.rs new file mode 100644 index 0000000000..52ec33af4e --- /dev/null +++ b/crates/c-api/src/lib.rs @@ -0,0 +1,1785 @@ +//! This file defines the extern "C" API, which is compatible with the +//! [Wasm C API](https://github.com/WebAssembly/wasm-c-api). + +#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals)] + +// TODO complete the C API + +use std::cell::RefCell; +use std::panic::{self, AssertUnwindSafe}; +use std::rc::Rc; +use std::{mem, ptr, slice}; +use wasmtime::{ + AnyRef, Callable, Config, Engine, ExportType, Extern, ExternType, Func, FuncType, Global, + GlobalType, HostInfo, HostRef, ImportType, Instance, Limits, Memory, MemoryType, Module, Store, + Table, TableType, Trap, Val, ValType, +}; + +mod ext; +mod wasi; + +pub use crate::ext::*; +pub use crate::wasi::*; + +macro_rules! declare_vec { + ($name:ident, $elem_ty:path) => { + #[repr(C)] + #[derive(Clone)] + pub struct $name { + pub size: usize, + pub data: *mut $elem_ty, + } + + impl $name { + #[allow(dead_code)] + fn set_from_slice(&mut self, source: &[$elem_ty]) { + let mut buffer = Vec::with_capacity(source.len()); + buffer.extend_from_slice(source); + assert_eq!(buffer.len(), buffer.capacity()); + self.size = buffer.len(); + self.data = buffer.as_mut_ptr(); + mem::forget(buffer); + } + + #[allow(dead_code)] + fn set_buffer(&mut self, mut buffer: Vec<$elem_ty>) { + assert_eq!(buffer.len(), buffer.capacity()); + self.size = buffer.len(); + self.data = buffer.as_mut_ptr(); + mem::forget(buffer); + } + + #[allow(dead_code)] + fn set_uninitialized(&mut self, size: usize) { + let mut buffer = vec![Default::default(); size]; + self.size = size; + self.data = buffer.as_mut_ptr(); + mem::forget(buffer); + } + + #[allow(dead_code)] + fn uninitialize(&mut self) { + let _ = unsafe { Vec::from_raw_parts(self.data, self.size, self.size) }; + } + + #[allow(dead_code)] + fn as_slice(&self) -> &[$elem_ty] { + unsafe { slice::from_raw_parts(self.data, self.size) } + } + } + + impl From> for $name { + fn from(mut vec: Vec<$elem_ty>) -> Self { + assert_eq!(vec.len(), vec.capacity()); + let result = $name { + size: vec.len(), + data: vec.as_mut_ptr(), + }; + mem::forget(vec); + result + } + } + + impl Drop for $name { + fn drop(&mut self) { + self.uninitialize(); + } + } + }; + + ($name:ident, *mut $elem_ty:path) => { + #[repr(C)] + #[derive(Clone)] + pub struct $name { + pub size: usize, + pub data: *mut *mut $elem_ty, + } + + impl $name { + #[allow(dead_code)] + fn set_from_slice(&mut self, source: &[*mut $elem_ty]) { + let mut buffer = Vec::with_capacity(source.len()); + buffer.extend_from_slice(source); + assert_eq!(buffer.len(), buffer.capacity()); + self.size = buffer.len(); + self.data = buffer.as_mut_ptr(); + mem::forget(buffer); + } + + #[allow(dead_code)] + fn set_buffer(&mut self, mut buffer: Vec<*mut $elem_ty>) { + assert_eq!(buffer.len(), buffer.capacity()); + self.size = buffer.len(); + self.data = buffer.as_mut_ptr(); + mem::forget(buffer); + } + + #[allow(dead_code)] + fn set_uninitialized(&mut self, size: usize) { + let mut buffer = vec![ptr::null_mut(); size]; + self.size = size; + self.data = buffer.as_mut_ptr(); + mem::forget(buffer); + } + + #[allow(dead_code)] + fn uninitialize(&mut self) { + for element in unsafe { Vec::from_raw_parts(self.data, self.size, self.size) } { + let _ = unsafe { Box::from_raw(element) }; + } + } + + #[allow(dead_code)] + fn as_slice(&self) -> &[*mut $elem_ty] { + unsafe { slice::from_raw_parts(self.data, self.size) } + } + } + + impl From> for $name { + fn from(mut vec: Vec<*mut $elem_ty>) -> Self { + assert_eq!(vec.len(), vec.capacity()); + let result = $name { + size: vec.len(), + data: vec.as_mut_ptr(), + }; + mem::forget(vec); + result + } + } + + impl Drop for $name { + fn drop(&mut self) { + self.uninitialize(); + } + } + }; +} + +pub type float32_t = f32; +pub type float64_t = f64; +pub type wasm_byte_t = u8; + +declare_vec!(wasm_byte_vec_t, wasm_byte_t); + +pub type wasm_name_t = wasm_byte_vec_t; +#[repr(C)] +#[derive(Clone)] +pub struct wasm_config_t { + pub(crate) config: Config, +} +#[repr(C)] +#[derive(Clone)] +pub struct wasm_engine_t { + engine: HostRef, +} +#[repr(C)] +#[derive(Clone)] +pub struct wasm_store_t { + store: HostRef, +} +#[doc = ""] +pub type wasm_mutability_t = u8; +#[repr(C)] +#[derive(Clone)] +pub struct wasm_limits_t { + pub min: u32, + pub max: u32, +} +#[repr(C)] +#[derive(Clone)] +pub struct wasm_valtype_t { + ty: ValType, +} + +declare_vec!(wasm_valtype_vec_t, *mut wasm_valtype_t); + +pub type wasm_valkind_t = u8; +#[repr(C)] +#[derive(Clone)] +pub struct wasm_functype_t { + functype: FuncType, + params_cache: Option, + returns_cache: Option, +} + +declare_vec!(wasm_functype_vec_t, *mut wasm_functype_t); + +#[repr(C)] +#[derive(Clone)] +pub struct wasm_globaltype_t { + globaltype: GlobalType, + content_cache: Option, +} + +declare_vec!(wasm_globaltype_vec_t, *mut wasm_globaltype_t); + +#[repr(C)] +#[derive(Clone)] +pub struct wasm_tabletype_t { + tabletype: TableType, + element_cache: Option, + limits_cache: Option, +} + +declare_vec!(wasm_tabletype_vec_t, *mut wasm_tabletype_t); + +#[repr(C)] +#[derive(Clone)] +pub struct wasm_memorytype_t { + memorytype: MemoryType, + limits_cache: Option, +} + +declare_vec!(wasm_memorytype_vec_t, *mut wasm_memorytype_t); + +#[repr(C)] +#[derive(Clone)] +pub struct wasm_externtype_t { + ty: ExternType, + cache: wasm_externtype_t_type_cache, +} + +#[derive(Clone)] +enum wasm_externtype_t_type_cache { + Empty, + Func(wasm_functype_t), + Global(wasm_globaltype_t), + Memory(wasm_memorytype_t), + Table(wasm_tabletype_t), +} + +declare_vec!(wasm_externtype_vec_t, *mut wasm_externtype_t); + +pub type wasm_externkind_t = u8; + +const WASM_EXTERN_FUNC: wasm_externkind_t = 0; +const WASM_EXTERN_GLOBAL: wasm_externkind_t = 1; +const WASM_EXTERN_TABLE: wasm_externkind_t = 2; +const WASM_EXTERN_MEMORY: wasm_externkind_t = 3; + +#[repr(C)] +#[derive(Clone)] +pub struct wasm_importtype_t { + ty: ImportType, + module_cache: Option, + name_cache: Option, + type_cache: Option, +} + +declare_vec!(wasm_importtype_vec_t, *mut wasm_importtype_t); + +#[repr(C)] +#[derive(Clone)] +pub struct wasm_exporttype_t { + ty: ExportType, + name_cache: Option, + type_cache: Option, +} + +declare_vec!(wasm_exporttype_vec_t, *mut wasm_exporttype_t); + +#[doc = ""] +#[repr(C)] +#[derive(Clone)] +pub struct wasm_ref_t { + r: AnyRef, +} +#[repr(C)] +#[derive(Copy, Clone)] +pub struct wasm_val_t { + pub kind: wasm_valkind_t, + pub of: wasm_val_t__bindgen_ty_1, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub union wasm_val_t__bindgen_ty_1 { + pub i32: i32, + pub i64: i64, + pub u32: u32, + pub u64: u64, + pub f32: float32_t, + pub f64: float64_t, + pub ref_: *mut wasm_ref_t, + _bindgen_union_align: u64, +} + +impl Default for wasm_val_t { + fn default() -> Self { + wasm_val_t { + kind: 0, + of: wasm_val_t__bindgen_ty_1 { i32: 0 }, + } + } +} + +declare_vec!(wasm_val_vec_t, wasm_val_t); + +#[repr(C)] +#[derive(Clone)] +pub struct wasm_frame_t { + _unused: [u8; 0], +} + +declare_vec!(wasm_frame_vec_t, *mut wasm_frame_t); + +#[repr(C)] +#[derive(Clone)] +pub struct wasm_instance_t { + instance: HostRef, + exports_cache: RefCell>>, +} +pub type wasm_message_t = wasm_name_t; +#[repr(C)] +#[derive(Clone)] +pub struct wasm_trap_t { + trap: HostRef, +} +#[repr(C)] +#[derive(Clone)] +pub struct wasm_foreign_t { + _unused: [u8; 0], +} +#[repr(C)] +#[derive(Clone)] +pub struct wasm_module_t { + module: HostRef, + imports: Vec, + exports: Vec, +} +#[repr(C)] +#[derive(Clone)] +pub struct wasm_shared_module_t { + _unused: [u8; 0], +} + +#[derive(Clone)] +#[repr(transparent)] +pub struct wasm_func_t { + ext: wasm_extern_t, +} + +impl wasm_func_t { + fn func(&self) -> &HostRef { + match &self.ext.which { + ExternHost::Func(f) => f, + _ => unsafe { std::hint::unreachable_unchecked() }, + } + } +} + +pub type wasm_func_callback_t = std::option::Option< + unsafe extern "C" fn(args: *const wasm_val_t, results: *mut wasm_val_t) -> *mut wasm_trap_t, +>; +pub type wasm_func_callback_with_env_t = std::option::Option< + unsafe extern "C" fn( + env: *mut std::ffi::c_void, + args: *const wasm_val_t, + results: *mut wasm_val_t, + ) -> *mut wasm_trap_t, +>; + +#[derive(Clone)] +#[repr(transparent)] +pub struct wasm_global_t { + ext: wasm_extern_t, +} + +impl wasm_global_t { + fn global(&self) -> &HostRef { + match &self.ext.which { + ExternHost::Global(g) => g, + _ => unsafe { std::hint::unreachable_unchecked() }, + } + } +} + +#[derive(Clone)] +#[repr(transparent)] +pub struct wasm_table_t { + ext: wasm_extern_t, +} + +impl wasm_table_t { + fn table(&self) -> &HostRef
{ + match &self.ext.which { + ExternHost::Table(t) => t, + _ => unsafe { std::hint::unreachable_unchecked() }, + } + } +} + +pub type wasm_table_size_t = u32; + +#[derive(Clone)] +#[repr(transparent)] +pub struct wasm_memory_t { + ext: wasm_extern_t, +} + +impl wasm_memory_t { + fn memory(&self) -> &HostRef { + match &self.ext.which { + ExternHost::Memory(m) => m, + _ => unsafe { std::hint::unreachable_unchecked() }, + } + } +} + +pub type wasm_memory_pages_t = u32; + +#[derive(Clone)] +pub struct wasm_extern_t { + which: ExternHost, +} + +#[derive(Clone)] +enum ExternHost { + Func(HostRef), + Global(HostRef), + Memory(HostRef), + Table(HostRef
), +} + +declare_vec!(wasm_extern_vec_t, *mut wasm_extern_t); + +#[no_mangle] +pub unsafe extern "C" fn wasm_byte_vec_delete(v: *mut wasm_byte_vec_t) { + (*v).uninitialize(); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_byte_vec_new_uninitialized(out: *mut wasm_byte_vec_t, size: usize) { + (*out).set_uninitialized(size); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_engine_delete(engine: *mut wasm_engine_t) { + let _ = Box::from_raw(engine); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_config_new() -> *mut wasm_config_t { + let config = Box::new(wasm_config_t { + config: Config::default(), + }); + Box::into_raw(config) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_engine_new() -> *mut wasm_engine_t { + let engine = Box::new(wasm_engine_t { + engine: HostRef::new(Engine::default()), + }); + Box::into_raw(engine) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_engine_new_with_config(c: *mut wasm_config_t) -> *mut wasm_engine_t { + let config = Box::from_raw(c).config; + let engine = Box::new(wasm_engine_t { + engine: HostRef::new(Engine::new(&config)), + }); + Box::into_raw(engine) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_extern_as_func(e: *mut wasm_extern_t) -> *mut wasm_func_t { + match &(*e).which { + ExternHost::Func(_) => e.cast(), + _ => ptr::null_mut(), + } +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_extern_vec_delete(v: *mut wasm_extern_vec_t) { + (*v).uninitialize(); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_func_as_extern(f: *mut wasm_func_t) -> *mut wasm_extern_t { + &mut (*f).ext +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_func_call( + func: *const wasm_func_t, + args: *const wasm_val_t, + results: *mut wasm_val_t, +) -> *mut wasm_trap_t { + let func = (*func).func().borrow(); + let mut params = Vec::with_capacity(func.param_arity()); + for i in 0..func.param_arity() { + let val = &(*args.add(i)); + params.push(val.val()); + } + + // We're calling arbitrary code here most of the time, and we in general + // want to try to insulate callers against bugs in wasmtime/wasi/etc if we + // can. As a result we catch panics here and transform them to traps to + // allow the caller to have any insulation possible against Rust panics. + let result = panic::catch_unwind(AssertUnwindSafe(|| func.call(¶ms))); + match result { + Ok(Ok(out)) => { + for i in 0..func.result_arity() { + let val = &mut (*results.add(i)); + *val = wasm_val_t::from_val(&out[i]); + } + ptr::null_mut() + } + Ok(Err(trap)) => { + let trap = Box::new(wasm_trap_t { + trap: HostRef::new(trap), + }); + Box::into_raw(trap) + } + Err(panic) => { + let trap = if let Some(msg) = panic.downcast_ref::() { + Trap::new(msg) + } else if let Some(msg) = panic.downcast_ref::<&'static str>() { + Trap::new(*msg) + } else { + Trap::new("rust panic happened") + }; + let trap = Box::new(wasm_trap_t { + trap: HostRef::new(trap), + }); + Box::into_raw(trap) + } + } +} + +impl wasm_val_t { + fn default() -> wasm_val_t { + wasm_val_t { + kind: 0, + of: wasm_val_t__bindgen_ty_1 { i32: 0 }, + } + } + + fn set(&mut self, val: Val) { + match val { + Val::I32(i) => { + self.kind = from_valtype(&ValType::I32); + self.of = wasm_val_t__bindgen_ty_1 { i32: i }; + } + Val::I64(i) => { + self.kind = from_valtype(&ValType::I64); + self.of = wasm_val_t__bindgen_ty_1 { i64: i }; + } + Val::F32(f) => { + self.kind = from_valtype(&ValType::F32); + self.of = wasm_val_t__bindgen_ty_1 { u32: f }; + } + Val::F64(f) => { + self.kind = from_valtype(&ValType::F64); + self.of = wasm_val_t__bindgen_ty_1 { u64: f }; + } + _ => unimplemented!("wasm_val_t::from_val {:?}", val), + } + } + + fn from_val(val: &Val) -> wasm_val_t { + match val { + Val::I32(i) => wasm_val_t { + kind: from_valtype(&ValType::I32), + of: wasm_val_t__bindgen_ty_1 { i32: *i }, + }, + Val::I64(i) => wasm_val_t { + kind: from_valtype(&ValType::I64), + of: wasm_val_t__bindgen_ty_1 { i64: *i }, + }, + Val::F32(f) => wasm_val_t { + kind: from_valtype(&ValType::F32), + of: wasm_val_t__bindgen_ty_1 { u32: *f }, + }, + Val::F64(f) => wasm_val_t { + kind: from_valtype(&ValType::F64), + of: wasm_val_t__bindgen_ty_1 { u64: *f }, + }, + _ => unimplemented!("wasm_val_t::from_val {:?}", val), + } + } + + fn val(&self) -> Val { + match into_valtype(self.kind) { + ValType::I32 => Val::from(unsafe { self.of.i32 }), + ValType::I64 => Val::from(unsafe { self.of.i64 }), + ValType::F32 => Val::from(unsafe { self.of.f32 }), + ValType::F64 => Val::from(unsafe { self.of.f64 }), + _ => unimplemented!("wasm_val_t::val {:?}", self.kind), + } + } +} + +struct Callback { + callback: wasm_func_callback_t, +} + +impl Callable for Callback { + fn call(&self, params: &[Val], results: &mut [Val]) -> Result<(), Trap> { + let params = params + .iter() + .map(|p| wasm_val_t::from_val(p)) + .collect::>(); + let mut out_results = vec![wasm_val_t::default(); results.len()]; + let func = self.callback.expect("wasm_func_callback_t fn"); + let out = unsafe { func(params.as_ptr(), out_results.as_mut_ptr()) }; + if !out.is_null() { + let trap: Box = unsafe { Box::from_raw(out) }; + return Err(trap.trap.borrow().clone()); + } + for i in 0..results.len() { + results[i] = out_results[i].val(); + } + Ok(()) + } +} + +struct CallbackWithEnv { + callback: wasm_func_callback_with_env_t, + env: *mut std::ffi::c_void, + finalizer: std::option::Option, +} + +impl Callable for CallbackWithEnv { + fn call(&self, params: &[Val], results: &mut [Val]) -> Result<(), Trap> { + let params = params + .iter() + .map(|p| wasm_val_t::from_val(p)) + .collect::>(); + let mut out_results = vec![wasm_val_t::default(); results.len()]; + let func = self.callback.expect("wasm_func_callback_with_env_t fn"); + let out = unsafe { func(self.env, params.as_ptr(), out_results.as_mut_ptr()) }; + if !out.is_null() { + let trap: Box = unsafe { Box::from_raw(out) }; + return Err(trap.trap.borrow().clone()); + } + for i in 0..results.len() { + results[i] = out_results[i].val(); + } + Ok(()) + } +} + +impl Drop for CallbackWithEnv { + fn drop(&mut self) { + if let Some(finalizer) = self.finalizer { + unsafe { + finalizer(self.env); + } + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_func_new( + store: *mut wasm_store_t, + ty: *const wasm_functype_t, + callback: wasm_func_callback_t, +) -> *mut wasm_func_t { + let store = &(*store).store.borrow(); + let ty = (*ty).functype.clone(); + let callback = Rc::new(Callback { callback }); + let func = Box::new(wasm_func_t { + ext: wasm_extern_t { + which: ExternHost::Func(HostRef::new(Func::new(store, ty, callback))), + }, + }); + Box::into_raw(func) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_func_delete(f: *mut wasm_func_t) { + let _ = Box::from_raw(f); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_functype_new( + params: *mut wasm_valtype_vec_t, + results: *mut wasm_valtype_vec_t, +) -> *mut wasm_functype_t { + let params = Vec::from_raw_parts((*params).data, (*params).size, (*params).size) + .into_iter() + .map(|vt| (*vt).ty.clone()) + .collect::>(); + let results = Vec::from_raw_parts((*results).data, (*results).size, (*results).size) + .into_iter() + .map(|vt| (*vt).ty.clone()) + .collect::>(); + let functype = FuncType::new(params.into_boxed_slice(), results.into_boxed_slice()); + let functype = Box::new(wasm_functype_t { + functype, + params_cache: None, // TODO get from args? + returns_cache: None, // TODO get from args? + }); + Box::into_raw(functype) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_functype_delete(ft: *mut wasm_functype_t) { + let _ = Box::from_raw(ft); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_instance_delete(instance: *mut wasm_instance_t) { + let _ = Box::from_raw(instance); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_instance_new( + store: *mut wasm_store_t, + module: *const wasm_module_t, + imports: *const *const wasm_extern_t, + result: *mut *mut wasm_trap_t, +) -> *mut wasm_instance_t { + let mut externs: Vec = Vec::with_capacity((*module).imports.len()); + for i in 0..(*module).imports.len() { + let import = *imports.add(i); + externs.push(match &(*import).which { + ExternHost::Func(e) => Extern::Func(e.borrow().clone()), + ExternHost::Table(e) => Extern::Table(e.borrow().clone()), + ExternHost::Global(e) => Extern::Global(e.borrow().clone()), + ExternHost::Memory(e) => Extern::Memory(e.borrow().clone()), + }); + } + let store = &(*store).store.borrow(); + let module = &(*module).module.borrow(); + // FIXME(WebAssembly/wasm-c-api#126) what else can we do with the `store` + // argument? + if !Store::same(&store, module.store()) { + if !result.is_null() { + let trap = Trap::new("wasm_store_t must match store in wasm_module_t"); + let trap = Box::new(wasm_trap_t { + trap: HostRef::new(trap), + }); + (*result) = Box::into_raw(trap); + } + return ptr::null_mut(); + } + match Instance::new(module, &externs) { + Ok(instance) => { + let instance = Box::new(wasm_instance_t { + instance: HostRef::new(instance), + exports_cache: RefCell::new(None), + }); + if !result.is_null() { + (*result) = ptr::null_mut(); + } + Box::into_raw(instance) + } + Err(trap) => { + if !result.is_null() { + let trap = match trap.downcast::() { + Ok(trap) => trap, + Err(e) => Trap::new(format!("{:?}", e)), + }; + let trap = Box::new(wasm_trap_t { + trap: HostRef::new(trap), + }); + (*result) = Box::into_raw(trap); + } + ptr::null_mut() + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_instance_exports( + instance: *const wasm_instance_t, + out: *mut wasm_extern_vec_t, +) { + let mut cache = (*instance).exports_cache.borrow_mut(); + let exports = cache.get_or_insert_with(|| { + let instance = &(*instance).instance.borrow(); + instance + .exports() + .iter() + .map(|e| match e { + Extern::Func(f) => ExternHost::Func(HostRef::new(f.clone())), + Extern::Global(f) => ExternHost::Global(HostRef::new(f.clone())), + Extern::Memory(f) => ExternHost::Memory(HostRef::new(f.clone())), + Extern::Table(f) => ExternHost::Table(HostRef::new(f.clone())), + }) + .collect() + }); + let mut buffer = Vec::with_capacity(exports.len()); + for e in exports { + let ext = Box::new(wasm_extern_t { which: e.clone() }); + buffer.push(Box::into_raw(ext)); + } + (*out).set_buffer(buffer); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_module_delete(module: *mut wasm_module_t) { + let _ = Box::from_raw(module); +} + +impl wasm_name_t { + fn from_name(name: &str) -> wasm_name_t { + name.to_string().into_bytes().into() + } +} + +/// Note that this function does not perform validation on the wasm +/// binary. To perform validation, use `wasm_module_validate`. +#[no_mangle] +pub unsafe extern "C" fn wasm_module_new( + store: *mut wasm_store_t, + binary: *const wasm_byte_vec_t, +) -> *mut wasm_module_t { + let binary = (*binary).as_slice(); + let store = &(*store).store.borrow(); + let module = match Module::from_binary(store, binary) { + Ok(module) => module, + Err(_) => return ptr::null_mut(), + }; + let imports = module + .imports() + .iter() + .map(|i| wasm_importtype_t { + ty: i.clone(), + module_cache: None, + name_cache: None, + type_cache: None, + }) + .collect::>(); + let exports = module + .exports() + .iter() + .map(|e| wasm_exporttype_t { + ty: e.clone(), + name_cache: None, + type_cache: None, + }) + .collect::>(); + let module = Box::new(wasm_module_t { + module: HostRef::new(module), + imports, + exports, + }); + Box::into_raw(module) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_module_validate( + store: *mut wasm_store_t, + binary: *const wasm_byte_vec_t, +) -> bool { + let binary = (*binary).as_slice(); + let store = &(*store).store.borrow(); + Module::validate(store, binary).is_ok() +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_store_delete(store: *mut wasm_store_t) { + let _ = Box::from_raw(store); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_store_new(engine: *mut wasm_engine_t) -> *mut wasm_store_t { + let engine = &(*engine).engine; + let store = Box::new(wasm_store_t { + store: HostRef::new(Store::new(&engine.borrow())), + }); + Box::into_raw(store) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_valtype_vec_new_empty(out: *mut wasm_valtype_vec_t) { + (*out).set_uninitialized(0); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_valtype_vec_new( + out: *mut wasm_valtype_vec_t, + size: usize, + data: *const *mut wasm_valtype_t, +) { + let slice = slice::from_raw_parts(data, size); + (*out).set_from_slice(slice); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_valtype_vec_new_uninitialized( + out: *mut wasm_valtype_vec_t, + size: usize, +) { + (*out).set_uninitialized(size); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_func_new_with_env( + store: *mut wasm_store_t, + ty: *const wasm_functype_t, + callback: wasm_func_callback_with_env_t, + env: *mut std::ffi::c_void, + finalizer: std::option::Option, +) -> *mut wasm_func_t { + let store = &(*store).store.borrow(); + let ty = (*ty).functype.clone(); + let callback = Rc::new(CallbackWithEnv { + callback, + env, + finalizer, + }); + let func = Box::new(wasm_func_t { + ext: wasm_extern_t { + which: ExternHost::Func(HostRef::new(Func::new(store, ty, callback))), + }, + }); + Box::into_raw(func) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_val_copy(out: *mut wasm_val_t, source: *const wasm_val_t) { + *out = match into_valtype((*source).kind) { + ValType::I32 | ValType::I64 | ValType::F32 | ValType::F64 => *source, + _ => unimplemented!("wasm_val_copy arg"), + }; +} + +fn into_valtype(kind: wasm_valkind_t) -> ValType { + match kind { + 0 => ValType::I32, + 1 => ValType::I64, + 2 => ValType::F32, + 3 => ValType::F64, + 128 => ValType::AnyRef, + 129 => ValType::FuncRef, + _ => panic!("unexpected kind: {}", kind), + } +} + +fn from_valtype(ty: &ValType) -> wasm_valkind_t { + match ty { + ValType::I32 => 0, + ValType::I64 => 1, + ValType::F32 => 2, + ValType::F64 => 3, + ValType::AnyRef => 128, + ValType::FuncRef => 129, + _ => panic!("wasm_valkind_t has no known conversion for {:?}", ty), + } +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_valtype_new(kind: wasm_valkind_t) -> *mut wasm_valtype_t { + let ty = Box::new(wasm_valtype_t { + ty: into_valtype(kind), + }); + Box::into_raw(ty) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_valtype_delete(vt: *mut wasm_valtype_t) { + drop(Box::from_raw(vt)); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_byte_vec_new( + out: *mut wasm_byte_vec_t, + size: usize, + data: *const wasm_byte_t, +) { + let slice = slice::from_raw_parts(data, size); + (*out).set_from_slice(slice); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_frame_delete(_arg1: *mut wasm_frame_t) { + unimplemented!("wasm_frame_delete") +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_frame_func_index(_arg1: *const wasm_frame_t) -> u32 { + unimplemented!("wasm_frame_func_index") +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_frame_func_offset(_arg1: *const wasm_frame_t) -> usize { + unimplemented!("wasm_frame_func_offset") +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_frame_instance(_arg1: *const wasm_frame_t) -> *mut wasm_instance_t { + unimplemented!("wasm_frame_instance") +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_frame_module_offset(_arg1: *const wasm_frame_t) -> usize { + unimplemented!("wasm_frame_module_offset") +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_frame_vec_delete(frames: *mut wasm_frame_vec_t) { + (*frames).uninitialize(); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_trap_delete(trap: *mut wasm_trap_t) { + let _ = Box::from_raw(trap); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_trap_new( + _store: *mut wasm_store_t, + message: *const wasm_message_t, +) -> *mut wasm_trap_t { + let message = (*message).as_slice(); + if message[message.len() - 1] != 0 { + panic!("wasm_trap_new message stringz expected"); + } + let message = String::from_utf8_lossy(&message[..message.len() - 1]); + let trap = Box::new(wasm_trap_t { + trap: HostRef::new(Trap::new(message)), + }); + Box::into_raw(trap) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_trap_message(trap: *const wasm_trap_t, out: *mut wasm_message_t) { + let mut buffer = Vec::new(); + buffer.extend_from_slice((*trap).trap.borrow().message().as_bytes()); + buffer.reserve_exact(1); + buffer.push(0); + (*out).set_buffer(buffer); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_trap_origin(_trap: *const wasm_trap_t) -> *mut wasm_frame_t { + ptr::null_mut() +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_trap_trace(_trap: *const wasm_trap_t, out: *mut wasm_frame_vec_t) { + (*out).set_uninitialized(0); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_importtype_module( + it: *const wasm_importtype_t, +) -> *const wasm_name_t { + if (*it).module_cache.is_none() { + let it = (it as *mut wasm_importtype_t).as_mut().unwrap(); + it.module_cache = Some(wasm_name_t::from_name(&it.ty.module())); + } + (*it).module_cache.as_ref().unwrap() +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_importtype_name(it: *const wasm_importtype_t) -> *const wasm_name_t { + if (*it).name_cache.is_none() { + let it = (it as *mut wasm_importtype_t).as_mut().unwrap(); + it.name_cache = Some(wasm_name_t::from_name(&it.ty.name())); + } + (*it).name_cache.as_ref().unwrap() +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_importtype_type( + it: *const wasm_importtype_t, +) -> *const wasm_externtype_t { + if (*it).type_cache.is_none() { + let it = (it as *mut wasm_importtype_t).as_mut().unwrap(); + it.type_cache = Some(wasm_externtype_t { + ty: (*it).ty.ty().clone(), + cache: wasm_externtype_t_type_cache::Empty, + }); + } + (*it).type_cache.as_ref().unwrap() +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_importtype_vec_delete(vec: *mut wasm_importtype_vec_t) { + (*vec).uninitialize(); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_exporttype_name(et: *const wasm_exporttype_t) -> *const wasm_name_t { + if (*et).name_cache.is_none() { + let et = (et as *mut wasm_exporttype_t).as_mut().unwrap(); + et.name_cache = Some(wasm_name_t::from_name(&et.ty.name())); + } + (*et).name_cache.as_ref().unwrap() +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_exporttype_type( + et: *const wasm_exporttype_t, +) -> *const wasm_externtype_t { + if (*et).type_cache.is_none() { + let et = (et as *mut wasm_exporttype_t).as_mut().unwrap(); + et.type_cache = Some(wasm_externtype_t { + ty: (*et).ty.ty().clone(), + cache: wasm_externtype_t_type_cache::Empty, + }); + } + (*et).type_cache.as_ref().unwrap() +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_exporttype_vec_delete(et: *mut wasm_exporttype_vec_t) { + (*et).uninitialize(); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_extern_kind(e: *const wasm_extern_t) -> wasm_externkind_t { + match (*e).which { + ExternHost::Func(_) => WASM_EXTERN_FUNC, + ExternHost::Global(_) => WASM_EXTERN_GLOBAL, + ExternHost::Table(_) => WASM_EXTERN_TABLE, + ExternHost::Memory(_) => WASM_EXTERN_MEMORY, + } +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_extern_type(e: *const wasm_extern_t) -> *mut wasm_externtype_t { + let et = Box::new(wasm_externtype_t { + ty: match &(*e).which { + ExternHost::Func(f) => ExternType::Func(f.borrow().ty().clone()), + ExternHost::Global(f) => ExternType::Global(f.borrow().ty().clone()), + ExternHost::Table(f) => ExternType::Table(f.borrow().ty().clone()), + ExternHost::Memory(f) => ExternType::Memory(f.borrow().ty().clone()), + }, + cache: wasm_externtype_t_type_cache::Empty, + }); + Box::into_raw(et) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_externtype_as_functype_const( + et: *const wasm_externtype_t, +) -> *const wasm_functype_t { + if let wasm_externtype_t_type_cache::Empty = (*et).cache { + let functype = (*et).ty.unwrap_func().clone(); + let f = wasm_functype_t { + functype, + params_cache: None, + returns_cache: None, + }; + let et = (et as *mut wasm_externtype_t).as_mut().unwrap(); + et.cache = wasm_externtype_t_type_cache::Func(f); + } + match &(*et).cache { + wasm_externtype_t_type_cache::Func(f) => f, + _ => panic!("wasm_externtype_as_functype_const"), + } +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_externtype_as_globaltype_const( + et: *const wasm_externtype_t, +) -> *const wasm_globaltype_t { + if let wasm_externtype_t_type_cache::Empty = (*et).cache { + let globaltype = (*et).ty.unwrap_global().clone(); + let g = wasm_globaltype_t { + globaltype, + content_cache: None, + }; + let et = (et as *mut wasm_externtype_t).as_mut().unwrap(); + et.cache = wasm_externtype_t_type_cache::Global(g); + } + match &(*et).cache { + wasm_externtype_t_type_cache::Global(g) => g, + _ => panic!("wasm_externtype_as_globaltype_const"), + } +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_externtype_as_tabletype_const( + et: *const wasm_externtype_t, +) -> *const wasm_tabletype_t { + if let wasm_externtype_t_type_cache::Empty = (*et).cache { + let tabletype = (*et).ty.unwrap_table().clone(); + let t = wasm_tabletype_t { + tabletype, + element_cache: None, + limits_cache: None, + }; + let et = (et as *mut wasm_externtype_t).as_mut().unwrap(); + et.cache = wasm_externtype_t_type_cache::Table(t); + } + match &(*et).cache { + wasm_externtype_t_type_cache::Table(t) => t, + _ => panic!("wasm_externtype_as_tabletype_const"), + } +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_externtype_as_memorytype_const( + et: *const wasm_externtype_t, +) -> *const wasm_memorytype_t { + if let wasm_externtype_t_type_cache::Empty = (*et).cache { + let memorytype = (*et).ty.unwrap_memory().clone(); + let m = wasm_memorytype_t { + memorytype, + limits_cache: None, + }; + let et = (et as *mut wasm_externtype_t).as_mut().unwrap(); + et.cache = wasm_externtype_t_type_cache::Memory(m); + } + match &(*et).cache { + wasm_externtype_t_type_cache::Memory(m) => m, + _ => panic!("wasm_externtype_as_memorytype_const"), + } +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_externtype_delete(et: *mut wasm_externtype_t) { + let _ = Box::from_raw(et); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_externtype_kind(et: *const wasm_externtype_t) -> wasm_externkind_t { + match &(*et).ty { + ExternType::Func(_) => WASM_EXTERN_FUNC, + ExternType::Table(_) => WASM_EXTERN_TABLE, + ExternType::Global(_) => WASM_EXTERN_GLOBAL, + ExternType::Memory(_) => WASM_EXTERN_MEMORY, + } +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_func_type(f: *const wasm_func_t) -> *mut wasm_functype_t { + let ft = Box::new(wasm_functype_t { + functype: (*f).func().borrow().ty().clone(), + params_cache: None, + returns_cache: None, + }); + Box::into_raw(ft) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_func_param_arity(f: *const wasm_func_t) -> usize { + (*f).func().borrow().param_arity() +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_func_result_arity(f: *const wasm_func_t) -> usize { + (*f).func().borrow().result_arity() +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_functype_params( + ft: *const wasm_functype_t, +) -> *const wasm_valtype_vec_t { + if (*ft).params_cache.is_none() { + let ft = (ft as *mut wasm_functype_t).as_mut().unwrap(); + let buffer = ft + .functype + .params() + .iter() + .map(|p| { + let ty = Box::new(wasm_valtype_t { ty: p.clone() }); + Box::into_raw(ty) + }) + .collect::>(); + ft.params_cache = Some(buffer.into()); + } + (*ft).params_cache.as_ref().unwrap() +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_functype_results( + ft: *const wasm_functype_t, +) -> *const wasm_valtype_vec_t { + if (*ft).returns_cache.is_none() { + let ft = (ft as *mut wasm_functype_t).as_mut().unwrap(); + let buffer = ft + .functype + .results() + .iter() + .map(|p| { + let ty = Box::new(wasm_valtype_t { ty: p.clone() }); + Box::into_raw(ty) + }) + .collect::>(); + ft.returns_cache = Some(buffer.into()); + } + (*ft).returns_cache.as_ref().unwrap() +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_globaltype_content( + gt: *const wasm_globaltype_t, +) -> *const wasm_valtype_t { + if (*gt).content_cache.is_none() { + let gt = (gt as *mut wasm_globaltype_t).as_mut().unwrap(); + gt.content_cache = Some(wasm_valtype_t { + ty: (*gt).globaltype.content().clone(), + }); + } + (*gt).content_cache.as_ref().unwrap() +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_globaltype_mutability( + gt: *const wasm_globaltype_t, +) -> wasm_mutability_t { + use wasmtime::Mutability::*; + match (*gt).globaltype.mutability() { + Const => 0, + Var => 1, + } +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_memorytype_limits( + mt: *const wasm_memorytype_t, +) -> *const wasm_limits_t { + if (*mt).limits_cache.is_none() { + let mt = (mt as *mut wasm_memorytype_t).as_mut().unwrap(); + let limits = (*mt).memorytype.limits(); + mt.limits_cache = Some(wasm_limits_t { + min: limits.min(), + max: limits.max().unwrap_or(u32::max_value()), + }); + } + (*mt).limits_cache.as_ref().unwrap() +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_module_exports( + module: *const wasm_module_t, + out: *mut wasm_exporttype_vec_t, +) { + let buffer = (*module) + .exports + .iter() + .map(|et| { + let et = Box::new(et.clone()); + Box::into_raw(et) + }) + .collect::>(); + (*out).set_buffer(buffer); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_module_imports( + module: *const wasm_module_t, + out: *mut wasm_importtype_vec_t, +) { + let buffer = (*module) + .imports + .iter() + .map(|it| { + let it = Box::new(it.clone()); + Box::into_raw(it) + }) + .collect::>(); + (*out).set_buffer(buffer); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_tabletype_element( + tt: *const wasm_tabletype_t, +) -> *const wasm_valtype_t { + if (*tt).element_cache.is_none() { + let tt = (tt as *mut wasm_tabletype_t).as_mut().unwrap(); + tt.element_cache = Some(wasm_valtype_t { + ty: (*tt).tabletype.element().clone(), + }); + } + (*tt).element_cache.as_ref().unwrap() +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_tabletype_limits( + tt: *const wasm_tabletype_t, +) -> *const wasm_limits_t { + if (*tt).limits_cache.is_none() { + let tt = (tt as *mut wasm_tabletype_t).as_mut().unwrap(); + let limits = (*tt).tabletype.limits(); + tt.limits_cache = Some(wasm_limits_t { + min: limits.min(), + max: limits.max().unwrap_or(u32::max_value()), + }); + } + (*tt).limits_cache.as_ref().unwrap() +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_valtype_kind(vt: *const wasm_valtype_t) -> wasm_valkind_t { + from_valtype(&(*vt).ty) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_extern_as_global(e: *mut wasm_extern_t) -> *mut wasm_global_t { + match &(*e).which { + ExternHost::Global(_) => e.cast(), + _ => ptr::null_mut(), + } +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_global_as_extern(g: *mut wasm_global_t) -> *mut wasm_extern_t { + &mut (*g).ext +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_global_delete(g: *mut wasm_global_t) { + let _ = Box::from_raw(g); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_global_copy(g: *const wasm_global_t) -> *mut wasm_global_t { + Box::into_raw(Box::new((*g).clone())) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_global_same( + g1: *const wasm_global_t, + g2: *const wasm_global_t, +) -> bool { + (*g1).global().ptr_eq(&(*g2).global()) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_global_new( + store: *mut wasm_store_t, + gt: *const wasm_globaltype_t, + val: *const wasm_val_t, +) -> *mut wasm_global_t { + let global = HostRef::new( + match Global::new( + &(*store).store.borrow(), + (*gt).globaltype.clone(), + (*val).val(), + ) { + Ok(g) => g, + Err(_) => return ptr::null_mut(), + }, + ); + let g = Box::new(wasm_global_t { + ext: wasm_extern_t { + which: ExternHost::Global(global), + }, + }); + Box::into_raw(g) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_global_type(g: *const wasm_global_t) -> *mut wasm_globaltype_t { + let globaltype = (*g).global().borrow().ty().clone(); + let g = Box::new(wasm_globaltype_t { + globaltype, + content_cache: None, + }); + Box::into_raw(g) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_global_get(g: *const wasm_global_t, out: *mut wasm_val_t) { + (*out).set((*g).global().borrow().get()); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_global_set(g: *mut wasm_global_t, val: *const wasm_val_t) { + let result = (*g).global().borrow().set((*val).val()); + drop(result); // TODO: should communicate this via the api somehow? +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_globaltype_delete(gt: *mut wasm_globaltype_t) { + let _ = Box::from_raw(gt); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_globaltype_new( + ty: *mut wasm_valtype_t, + mutability: wasm_mutability_t, +) -> *mut wasm_globaltype_t { + use wasmtime::Mutability::*; + let ty = Box::from_raw(ty); + let mutability = match mutability { + 0 => Const, + 1 => Var, + _ => panic!("mutability out-of-range"), + }; + let globaltype = GlobalType::new(ty.ty.clone(), mutability); + let gt = Box::new(wasm_globaltype_t { + globaltype, + content_cache: Some(*ty), + }); + Box::into_raw(gt) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_extern_as_memory(e: *mut wasm_extern_t) -> *mut wasm_memory_t { + match &(*e).which { + ExternHost::Memory(_) => e.cast(), + _ => ptr::null_mut(), + } +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_memory_as_extern(m: *mut wasm_memory_t) -> *mut wasm_extern_t { + &mut (*m).ext +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_memory_delete(m: *mut wasm_memory_t) { + let _ = Box::from_raw(m); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_memory_copy(m: *const wasm_memory_t) -> *mut wasm_memory_t { + Box::into_raw(Box::new((*m).clone())) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_memory_same( + m1: *const wasm_memory_t, + m2: *const wasm_memory_t, +) -> bool { + (*m1).memory().ptr_eq(&(*m2).memory()) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_memory_data(m: *mut wasm_memory_t) -> *mut u8 { + (*m).memory().borrow().data_ptr() +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_memory_data_size(m: *const wasm_memory_t) -> usize { + (*m).memory().borrow().data_size() +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_memory_size(m: *const wasm_memory_t) -> wasm_memory_pages_t { + (*m).memory().borrow().size() +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_memory_grow( + m: *mut wasm_memory_t, + delta: wasm_memory_pages_t, +) -> bool { + (*m).memory().borrow().grow(delta).is_ok() +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_memory_new( + store: *mut wasm_store_t, + mt: *const wasm_memorytype_t, +) -> *mut wasm_memory_t { + let memory = HostRef::new(Memory::new( + &(*store).store.borrow(), + (*mt).memorytype.clone(), + )); + let m = Box::new(wasm_memory_t { + ext: wasm_extern_t { + which: ExternHost::Memory(memory), + }, + }); + Box::into_raw(m) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_memorytype_delete(mt: *mut wasm_memorytype_t) { + let _ = Box::from_raw(mt); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_memorytype_new( + limits: *const wasm_limits_t, +) -> *mut wasm_memorytype_t { + let max = if (*limits).max == u32::max_value() { + None + } else { + Some((*limits).max) + }; + let limits = Limits::new((*limits).min, max); + let mt = Box::new(wasm_memorytype_t { + memorytype: MemoryType::new(limits), + limits_cache: None, + }); + Box::into_raw(mt) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_extern_as_table(e: *mut wasm_extern_t) -> *mut wasm_table_t { + match &(*e).which { + ExternHost::Table(_) => e.cast(), + _ => ptr::null_mut(), + } +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_table_as_extern(t: *mut wasm_table_t) -> *mut wasm_extern_t { + &mut (*t).ext +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_func_as_ref(f: *mut wasm_func_t) -> *mut wasm_ref_t { + let r = (*f).func().anyref(); + let f = Box::new(wasm_ref_t { r }); + Box::into_raw(f) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_ref_delete(r: *mut wasm_ref_t) { + if !r.is_null() { + let _ = Box::from_raw(r); + } +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_table_delete(t: *mut wasm_table_t) { + let _ = Box::from_raw(t); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_table_copy(t: *const wasm_table_t) -> *mut wasm_table_t { + Box::into_raw(Box::new((*t).clone())) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_table_new( + store: *mut wasm_store_t, + tt: *const wasm_tabletype_t, + init: *mut wasm_ref_t, +) -> *mut wasm_table_t { + let init: Val = if !init.is_null() { + Box::from_raw(init).r.into() + } else { + Val::AnyRef(AnyRef::Null) + }; + let table = match Table::new(&(*store).store.borrow(), (*tt).tabletype.clone(), init) { + Ok(table) => table, + Err(_) => return ptr::null_mut(), + }; + let t = Box::new(wasm_table_t { + ext: wasm_extern_t { + which: ExternHost::Table(HostRef::new(table)), + }, + }); + Box::into_raw(t) +} + +unsafe fn into_funcref(val: Val) -> *mut wasm_ref_t { + if let Val::AnyRef(AnyRef::Null) = val { + return ptr::null_mut(); + } + let anyref = match val.anyref() { + Some(anyref) => anyref, + None => return ptr::null_mut(), + }; + let r = Box::new(wasm_ref_t { r: anyref }); + Box::into_raw(r) +} + +unsafe fn from_funcref(r: *mut wasm_ref_t) -> Val { + if !r.is_null() { + Box::from_raw(r).r.into() + } else { + Val::AnyRef(AnyRef::Null) + } +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_table_get( + t: *const wasm_table_t, + index: wasm_table_size_t, +) -> *mut wasm_ref_t { + match (*t).table().borrow().get(index) { + Some(val) => into_funcref(val), + None => into_funcref(Val::AnyRef(AnyRef::Null)), + } +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_table_set( + t: *mut wasm_table_t, + index: wasm_table_size_t, + r: *mut wasm_ref_t, +) -> bool { + let val = from_funcref(r); + (*t).table().borrow().set(index, val).is_ok() +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_table_size(t: *const wasm_table_t) -> wasm_table_size_t { + (*t).table().borrow().size() +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_table_grow( + t: *mut wasm_table_t, + delta: wasm_table_size_t, + init: *mut wasm_ref_t, +) -> bool { + let init = from_funcref(init); + (*t).table().borrow().grow(delta, init).is_ok() +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_table_same(t1: *const wasm_table_t, t2: *const wasm_table_t) -> bool { + (*t1).table().ptr_eq((*t2).table()) +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_tabletype_delete(tt: *mut wasm_tabletype_t) { + let _ = Box::from_raw(tt); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_tabletype_new( + ty: *mut wasm_valtype_t, + limits: *const wasm_limits_t, +) -> *mut wasm_tabletype_t { + let ty = Box::from_raw(ty).ty; + let max = if (*limits).max == u32::max_value() { + None + } else { + Some((*limits).max) + }; + let limits = Limits::new((*limits).min, max); + let tt = Box::new(wasm_tabletype_t { + tabletype: TableType::new(ty, limits), + element_cache: None, + limits_cache: None, + }); + Box::into_raw(tt) +} + +struct HostInfoState { + info: *mut std::ffi::c_void, + finalizer: std::option::Option, +} + +impl HostInfo for HostInfoState { + fn finalize(&mut self) { + if let Some(f) = &self.finalizer { + unsafe { + f(self.info); + } + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_instance_set_host_info_with_finalizer( + instance: *mut wasm_instance_t, + info: *mut std::ffi::c_void, + finalizer: std::option::Option, +) { + let info = if info.is_null() && finalizer.is_none() { + None + } else { + let b: Box = Box::new(HostInfoState { info, finalizer }); + Some(b) + }; + (*instance).instance.anyref().set_host_info(info); +} + +#[no_mangle] +pub unsafe extern "C" fn wasm_valtype_vec_copy( + out: *mut wasm_valtype_vec_t, + src: *mut wasm_valtype_vec_t, +) { + let slice = slice::from_raw_parts((*src).data, (*src).size); + (*out).set_from_slice(slice); +} diff --git a/crates/c-api/src/wasi.rs b/crates/c-api/src/wasi.rs new file mode 100644 index 0000000000..5aeeb75e54 --- /dev/null +++ b/crates/c-api/src/wasi.rs @@ -0,0 +1,239 @@ +//! The WASI embedding API definitions for Wasmtime. +use crate::{wasm_extern_t, wasm_importtype_t, wasm_store_t, wasm_trap_t, ExternHost, ExternType}; +use std::collections::HashMap; +use std::ffi::CStr; +use std::fs::File; +use std::os::raw::{c_char, c_int}; +use std::path::Path; +use std::slice; +use wasi_common::{preopen_dir, WasiCtxBuilder}; +use wasmtime::{HostRef, Trap}; +use wasmtime_wasi::Wasi; + +unsafe fn cstr_to_path<'a>(path: *const c_char) -> Option<&'a Path> { + CStr::from_ptr(path).to_str().map(Path::new).ok() +} + +unsafe fn open_file(path: *const c_char) -> Option { + File::open(cstr_to_path(path)?).ok() +} + +unsafe fn create_file(path: *const c_char) -> Option { + File::create(cstr_to_path(path)?).ok() +} + +#[repr(C)] +pub struct wasi_config_t { + builder: WasiCtxBuilder, +} + +impl wasi_config_t {} + +#[no_mangle] +pub unsafe extern "C" fn wasi_config_new() -> *mut wasi_config_t { + Box::into_raw(Box::new(wasi_config_t { + builder: WasiCtxBuilder::new(), + })) +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_config_delete(config: *mut wasi_config_t) { + drop(Box::from_raw(config)); +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_config_set_argv( + config: *mut wasi_config_t, + argc: c_int, + argv: *const *const c_char, +) { + (*config).builder.args( + slice::from_raw_parts(argv, argc as usize) + .iter() + .map(|a| slice::from_raw_parts(*a as *const u8, CStr::from_ptr(*a).to_bytes().len())), + ); +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_config_inherit_argv(config: *mut wasi_config_t) { + (*config).builder.inherit_args(); +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_config_set_env( + config: *mut wasi_config_t, + envc: c_int, + names: *const *const c_char, + values: *const *const c_char, +) { + let names = slice::from_raw_parts(names, envc as usize); + let values = slice::from_raw_parts(values, envc as usize); + + (*config).builder.envs( + names + .iter() + .map(|p| CStr::from_ptr(*p).to_bytes()) + .zip(values.iter().map(|p| CStr::from_ptr(*p).to_bytes())), + ); +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_config_inherit_env(config: *mut wasi_config_t) { + (*config).builder.inherit_env(); +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_config_set_stdin_file( + config: *mut wasi_config_t, + path: *const c_char, +) -> bool { + let file = match open_file(path) { + Some(f) => f, + None => return false, + }; + + (*config).builder.stdin(file); + + true +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_config_inherit_stdin(config: *mut wasi_config_t) { + (*config).builder.inherit_stdin(); +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_config_set_stdout_file( + config: *mut wasi_config_t, + path: *const c_char, +) -> bool { + let file = match create_file(path) { + Some(f) => f, + None => return false, + }; + + (*config).builder.stdout(file); + + true +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_config_inherit_stdout(config: *mut wasi_config_t) { + (*config).builder.inherit_stdout(); +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_config_set_stderr_file( + config: *mut wasi_config_t, + path: *const c_char, +) -> bool { + let file = match create_file(path) { + Some(f) => f, + None => return false, + }; + + (*config).builder.stderr(file); + + true +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_config_inherit_stderr(config: *mut wasi_config_t) { + (*config).builder.inherit_stderr(); +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_config_preopen_dir( + config: *mut wasi_config_t, + path: *const c_char, + guest_path: *const c_char, +) -> bool { + let guest_path = match cstr_to_path(guest_path) { + Some(p) => p, + None => return false, + }; + + let dir = match cstr_to_path(path) { + Some(p) => match preopen_dir(p) { + Ok(d) => d, + Err(_) => return false, + }, + None => return false, + }; + + (*config).builder.preopened_dir(dir, guest_path); + + true +} + +#[repr(C)] +pub struct wasi_instance_t { + wasi: Wasi, + export_cache: HashMap>, +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_instance_new( + store: *mut wasm_store_t, + config: *mut wasi_config_t, + trap: *mut *mut wasm_trap_t, +) -> *mut wasi_instance_t { + let store = &(*store).store.borrow(); + let mut config = Box::from_raw(config); + + match config.builder.build() { + Ok(ctx) => Box::into_raw(Box::new(wasi_instance_t { + wasi: Wasi::new(store, ctx), + export_cache: HashMap::new(), + })), + Err(e) => { + (*trap) = Box::into_raw(Box::new(wasm_trap_t { + trap: HostRef::new(Trap::new(e.to_string())), + })); + + std::ptr::null_mut() + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_instance_delete(instance: *mut wasi_instance_t) { + drop(Box::from_raw(instance)); +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_instance_bind_import( + instance: *mut wasi_instance_t, + import: *const wasm_importtype_t, +) -> *const wasm_extern_t { + // TODO: support previous versions? + if (*import).ty.module() != "wasi_snapshot_preview1" { + return std::ptr::null_mut(); + } + + // The import should be a function (WASI only exports functions) + let func_type = match (*import).ty.ty() { + ExternType::Func(f) => f, + _ => return std::ptr::null_mut(), + }; + + let name = (*import).ty.name(); + + match (*instance).wasi.get_export(name) { + Some(export) => { + if export.ty() != func_type { + return std::ptr::null_mut(); + } + + &**(*instance) + .export_cache + .entry(name.to_string()) + .or_insert_with(|| { + Box::new(wasm_extern_t { + which: ExternHost::Func(HostRef::new(export.clone())), + }) + }) as *const wasm_extern_t + } + None => std::ptr::null_mut(), + } +} diff --git a/crates/c-api/tests/wasm-c-examples.rs b/crates/c-api/tests/wasm-c-examples.rs new file mode 100644 index 0000000000..66efa08c5d --- /dev/null +++ b/crates/c-api/tests/wasm-c-examples.rs @@ -0,0 +1,198 @@ +use std::env; +use std::path::PathBuf; +use std::process::Command; + +fn run_c_example(name: &'static str, expected_out: &[u8]) { + let cargo = env::var("MAKE").unwrap_or("make".to_string()); + let pkg_dir = env!("CARGO_MANIFEST_DIR"); + let examples_dir = PathBuf::from(pkg_dir).join("examples"); + let make_arg = format!("run-{}-c", name); + let output = Command::new(cargo) + .current_dir(examples_dir) + .args(&["-s", &make_arg]) + .output() + .expect("success"); + assert!( + output.status.success(), + "failed to execute the C example '{}': {}", + name, + String::from_utf8_lossy(&output.stderr), + ); + assert_eq!( + output.stdout.as_slice(), + expected_out, + "unexpected stdout from example: {}", + String::from_utf8_lossy(&output.stdout), + ); +} + +#[test] +fn test_run_hello_example() { + run_c_example( + "hello", + br#"==== C hello ==== +Initializing... +Loading binary... +Compiling module... +Creating callback... +Instantiating module... +Extracting export... +Calling export... +Calling back... +> Hello World! +Shutting down... +Done. +==== Done ==== +"#, + ); +} + +#[test] +fn test_run_memory_example() { + run_c_example( + "memory", + br#"==== C memory ==== +Initializing... +Loading binary... +Compiling module... +Instantiating module... +Extracting exports... +Checking memory... +Mutating memory... +Growing memory... +Creating stand-alone memory... +Shutting down... +Done. +==== Done ==== +"#, + ); +} + +#[test] +fn test_run_global_example() { + run_c_example( + "global", + br#"==== C global ==== +Initializing... +Loading binary... +Compiling module... +Creating globals... +Instantiating module... +Extracting exports... +Accessing globals... +Shutting down... +Done. +==== Done ==== +"#, + ); +} + +#[test] +fn test_run_callback_example() { + run_c_example( + "callback", + br#"==== C callback ==== +Initializing... +Loading binary... +Compiling module... +Creating callback... +Instantiating module... +Extracting export... +Calling export... +Calling back... +> 7 +Calling back closure... +> 42 +Printing result... +> 49 +Shutting down... +Done. +==== Done ==== +"#, + ); +} + +#[test] +fn test_run_reflect_example() { + run_c_example( + "reflect", + br#"==== C reflect ==== +Initializing... +Loading binary... +Compiling module... +Instantiating module... +Extracting export... +> export 0 "func" +>> initial: func i32 f64 f32 -> i32 +>> current: func i32 f64 f32 -> i32 +>> in-arity: 3, out-arity: 1 +> export 1 "global" +>> initial: global const f64 +>> current: global const f64 +> export 2 "table" +>> initial: table 0d 50d funcref +>> current: table 0d 50d funcref +> export 3 "memory" +>> initial: memory 1d +>> current: memory 1d +Shutting down... +Done. +==== Done ==== +"#, + ); +} + +#[test] +fn test_run_start_example() { + run_c_example( + "start", + br#"==== C start ==== +Initializing... +Loading binary... +Compiling module... +Instantiating module... +Printing message... +> wasm trap: unreachable, source location: @002e +Printing origin... +> Empty origin. +Printing trace... +> Empty trace. +Shutting down... +Done. +==== Done ==== +"#, + ); +} + +#[test] +fn test_run_trap_example() { + run_c_example( + "trap", + br#"==== C trap ==== +Initializing... +Loading binary... +Compiling module... +Creating callback... +Instantiating module... +Extracting exports... +Calling export 0... +Calling back... +Printing message... +> callback abort +Printing origin... +> Empty origin. +Printing trace... +> Empty trace. +Calling export 1... +Printing message... +> wasm trap: unreachable, source location: @0065 +Printing origin... +> Empty origin. +Printing trace... +> Empty trace. +Shutting down... +Done. +==== Done ==== +"#, + ); +} diff --git a/crates/debug/.gitignore b/crates/debug/.gitignore new file mode 100644 index 0000000000..4308d82204 --- /dev/null +++ b/crates/debug/.gitignore @@ -0,0 +1,3 @@ +target/ +**/*.rs.bk +Cargo.lock diff --git a/crates/debug/Cargo.toml b/crates/debug/Cargo.toml new file mode 100644 index 0000000000..0f02660553 --- /dev/null +++ b/crates/debug/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "wasmtime-debug" +version = "0.12.0" +authors = ["The Wasmtime Project Developers"] +description = "Debug utils for WebAsssembly code in Cranelift" +license = "Apache-2.0 WITH LLVM-exception" +repository = "https://github.com/bytecodealliance/wasmtime" +documentation = "https://docs.rs/wasmtime-debug/" +categories = ["wasm"] +keywords = ["webassembly", "wasm", "debuginfo"] +readme = "README.md" +edition = "2018" + +[dependencies] +gimli = "0.20.0" +wasmparser = "0.51.2" +faerie = "0.14.0" +wasmtime-environ = { path = "../environ", version = "0.12.0" } +target-lexicon = { version = "0.10.0", default-features = false } +anyhow = "1.0" +thiserror = "1.0.4" +more-asserts = "0.2.1" + +[badges] +maintenance = { status = "actively-developed" } diff --git a/crates/debug/LICENSE b/crates/debug/LICENSE new file mode 100644 index 0000000000..f9d81955f4 --- /dev/null +++ b/crates/debug/LICENSE @@ -0,0 +1,220 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + diff --git a/crates/debug/README.md b/crates/debug/README.md new file mode 100644 index 0000000000..d102c74114 --- /dev/null +++ b/crates/debug/README.md @@ -0,0 +1,4 @@ +This is the `wasmtime-debug` crate, which provides functionality to +read, transform, and write DWARF section. + +[`wasmtime-debug`]: https://crates.io/crates/wasmtime-debug diff --git a/crates/debug/src/gc.rs b/crates/debug/src/gc.rs new file mode 100644 index 0000000000..9ac8ef7605 --- /dev/null +++ b/crates/debug/src/gc.rs @@ -0,0 +1,231 @@ +use crate::transform::AddressTransform; +use gimli::constants; +use gimli::read; +use gimli::{Reader, UnitSectionOffset}; +use std::collections::{HashMap, HashSet}; + +#[derive(Debug)] +pub struct Dependencies { + edges: HashMap>, + roots: HashSet, +} + +impl Dependencies { + fn new() -> Dependencies { + Dependencies { + edges: HashMap::new(), + roots: HashSet::new(), + } + } + + fn add_edge(&mut self, a: UnitSectionOffset, b: UnitSectionOffset) { + use std::collections::hash_map::Entry; + match self.edges.entry(a) { + Entry::Occupied(mut o) => { + o.get_mut().insert(b); + } + Entry::Vacant(v) => { + let mut set = HashSet::new(); + set.insert(b); + v.insert(set); + } + } + } + + fn add_root(&mut self, root: UnitSectionOffset) { + self.roots.insert(root); + } + + pub fn get_reachable(&self) -> HashSet { + let mut reachable = self.roots.clone(); + let mut queue = Vec::new(); + for i in self.roots.iter() { + if let Some(deps) = self.edges.get(i) { + for j in deps { + if reachable.contains(j) { + continue; + } + reachable.insert(*j); + queue.push(*j); + } + } + } + while let Some(i) = queue.pop() { + if let Some(deps) = self.edges.get(&i) { + for j in deps { + if reachable.contains(j) { + continue; + } + reachable.insert(*j); + queue.push(*j); + } + } + } + reachable + } +} + +pub fn build_dependencies>( + dwarf: &read::Dwarf, + at: &AddressTransform, +) -> read::Result { + let mut deps = Dependencies::new(); + let mut units = dwarf.units(); + while let Some(unit) = units.next()? { + build_unit_dependencies(unit, dwarf, at, &mut deps)?; + } + Ok(deps) +} + +fn build_unit_dependencies>( + header: read::CompilationUnitHeader, + dwarf: &read::Dwarf, + at: &AddressTransform, + deps: &mut Dependencies, +) -> read::Result<()> { + let unit = dwarf.unit(header)?; + let mut tree = unit.entries_tree(None)?; + let root = tree.root()?; + build_die_dependencies(root, dwarf, &unit, at, deps)?; + Ok(()) +} + +fn has_die_back_edge>(die: &read::DebuggingInformationEntry) -> bool { + match die.tag() { + constants::DW_TAG_variable + | constants::DW_TAG_constant + | constants::DW_TAG_inlined_subroutine + | constants::DW_TAG_lexical_block + | constants::DW_TAG_label + | constants::DW_TAG_with_stmt + | constants::DW_TAG_try_block + | constants::DW_TAG_catch_block + | constants::DW_TAG_template_type_parameter + | constants::DW_TAG_member + | constants::DW_TAG_formal_parameter => true, + _ => false, + } +} + +fn has_valid_code_range>( + die: &read::DebuggingInformationEntry, + dwarf: &read::Dwarf, + unit: &read::Unit, + at: &AddressTransform, +) -> read::Result { + match die.tag() { + constants::DW_TAG_subprogram => { + if let Some(ranges_attr) = die.attr_value(constants::DW_AT_ranges)? { + let offset = match ranges_attr { + read::AttributeValue::RangeListsRef(val) => val, + read::AttributeValue::DebugRngListsIndex(index) => { + dwarf.ranges_offset(unit, index)? + } + _ => return Ok(false), + }; + let mut has_valid_base = if let Some(read::AttributeValue::Addr(low_pc)) = + die.attr_value(constants::DW_AT_low_pc)? + { + Some(at.can_translate_address(low_pc)) + } else { + None + }; + let mut it = dwarf.ranges.raw_ranges(offset, unit.encoding())?; + while let Some(range) = it.next()? { + // If at least one of the range addresses can be converted, + // declaring code range as valid. + match range { + read::RawRngListEntry::AddressOrOffsetPair { .. } + if has_valid_base.is_some() => + { + if has_valid_base.unwrap() { + return Ok(true); + } + } + read::RawRngListEntry::StartEnd { begin, .. } + | read::RawRngListEntry::StartLength { begin, .. } + | read::RawRngListEntry::AddressOrOffsetPair { begin, .. } => { + if at.can_translate_address(begin) { + return Ok(true); + } + } + read::RawRngListEntry::StartxEndx { begin, .. } + | read::RawRngListEntry::StartxLength { begin, .. } => { + let addr = dwarf.address(unit, begin)?; + if at.can_translate_address(addr) { + return Ok(true); + } + } + read::RawRngListEntry::BaseAddress { addr } => { + has_valid_base = Some(at.can_translate_address(addr)); + } + read::RawRngListEntry::BaseAddressx { addr } => { + let addr = dwarf.address(unit, addr)?; + has_valid_base = Some(at.can_translate_address(addr)); + } + read::RawRngListEntry::OffsetPair { .. } => (), + } + } + return Ok(false); + } else if let Some(low_pc) = die.attr_value(constants::DW_AT_low_pc)? { + if let read::AttributeValue::Addr(a) = low_pc { + return Ok(at.can_translate_address(a)); + } + } + } + _ => (), + } + Ok(false) +} + +fn build_die_dependencies>( + die: read::EntriesTreeNode, + dwarf: &read::Dwarf, + unit: &read::Unit, + at: &AddressTransform, + deps: &mut Dependencies, +) -> read::Result<()> { + let entry = die.entry(); + let offset = entry.offset().to_unit_section_offset(unit); + let mut attrs = entry.attrs(); + while let Some(attr) = attrs.next()? { + build_attr_dependencies(&attr, offset, dwarf, unit, at, deps)?; + } + + let mut children = die.children(); + while let Some(child) = children.next()? { + let child_entry = child.entry(); + let child_offset = child_entry.offset().to_unit_section_offset(unit); + deps.add_edge(child_offset, offset); + if has_die_back_edge(child_entry) { + deps.add_edge(offset, child_offset); + } + if has_valid_code_range(child_entry, dwarf, unit, at)? { + deps.add_root(child_offset); + } + build_die_dependencies(child, dwarf, unit, at, deps)?; + } + Ok(()) +} + +fn build_attr_dependencies>( + attr: &read::Attribute, + offset: UnitSectionOffset, + _dwarf: &read::Dwarf, + unit: &read::Unit, + _at: &AddressTransform, + deps: &mut Dependencies, +) -> read::Result<()> { + match attr.value() { + read::AttributeValue::UnitRef(val) => { + let ref_offset = val.to_unit_section_offset(unit); + deps.add_edge(offset, ref_offset); + } + read::AttributeValue::DebugInfoRef(val) => { + let ref_offset = UnitSectionOffset::DebugInfoOffset(val); + deps.add_edge(offset, ref_offset); + } + _ => (), + } + Ok(()) +} diff --git a/crates/debug/src/lib.rs b/crates/debug/src/lib.rs new file mode 100644 index 0000000000..67a7bf0460 --- /dev/null +++ b/crates/debug/src/lib.rs @@ -0,0 +1,184 @@ +//! Debug utils for WebAssembly using Cranelift. + +#![allow(clippy::cast_ptr_alignment)] + +use anyhow::Error; +use faerie::{Artifact, Decl}; +use more_asserts::assert_gt; +use target_lexicon::{BinaryFormat, Triple}; +use wasmtime_environ::isa::TargetFrontendConfig; +use wasmtime_environ::{ModuleAddressMap, ModuleVmctxInfo, ValueLabelsRanges}; + +pub use crate::read_debuginfo::{read_debuginfo, DebugInfoData, WasmFileInfo}; +pub use crate::transform::transform_dwarf; +pub use crate::write_debuginfo::{emit_dwarf, ResolvedSymbol, SymbolResolver}; + +mod gc; +mod read_debuginfo; +mod transform; +mod write_debuginfo; + +struct FunctionRelocResolver {} +impl SymbolResolver for FunctionRelocResolver { + fn resolve_symbol(&self, symbol: usize, addend: i64) -> ResolvedSymbol { + let name = format!("_wasm_function_{}", symbol); + ResolvedSymbol::Reloc { name, addend } + } +} + +pub fn emit_debugsections( + obj: &mut Artifact, + vmctx_info: &ModuleVmctxInfo, + target_config: TargetFrontendConfig, + debuginfo_data: &DebugInfoData, + at: &ModuleAddressMap, + ranges: &ValueLabelsRanges, +) -> Result<(), Error> { + let resolver = FunctionRelocResolver {}; + let dwarf = transform_dwarf(target_config, debuginfo_data, at, vmctx_info, ranges)?; + emit_dwarf(obj, dwarf, &resolver)?; + Ok(()) +} + +struct ImageRelocResolver<'a> { + func_offsets: &'a Vec, +} + +impl<'a> SymbolResolver for ImageRelocResolver<'a> { + fn resolve_symbol(&self, symbol: usize, addend: i64) -> ResolvedSymbol { + let func_start = self.func_offsets[symbol]; + ResolvedSymbol::PhysicalAddress(func_start + addend as u64) + } +} + +pub fn emit_debugsections_image( + triple: Triple, + target_config: TargetFrontendConfig, + debuginfo_data: &DebugInfoData, + vmctx_info: &ModuleVmctxInfo, + at: &ModuleAddressMap, + ranges: &ValueLabelsRanges, + funcs: &[(*const u8, usize)], +) -> Result, Error> { + let func_offsets = &funcs + .iter() + .map(|(ptr, _)| *ptr as u64) + .collect::>(); + let mut obj = Artifact::new(triple, String::from("module")); + let resolver = ImageRelocResolver { func_offsets }; + let dwarf = transform_dwarf(target_config, debuginfo_data, at, vmctx_info, ranges)?; + + // Assuming all functions in the same code block, looking min/max of its range. + assert_gt!(funcs.len(), 0); + let mut segment_body: (usize, usize) = (!0, 0); + for (body_ptr, body_len) in funcs { + segment_body.0 = std::cmp::min(segment_body.0, *body_ptr as usize); + segment_body.1 = std::cmp::max(segment_body.1, *body_ptr as usize + body_len); + } + let segment_body = (segment_body.0 as *const u8, segment_body.1 - segment_body.0); + + let body = unsafe { std::slice::from_raw_parts(segment_body.0, segment_body.1) }; + obj.declare_with("all", Decl::function(), body.to_vec())?; + + emit_dwarf(&mut obj, dwarf, &resolver)?; + + // LLDB is too "magical" about mach-o, generating elf + let mut bytes = obj.emit_as(BinaryFormat::Elf)?; + // elf is still missing details... + convert_faerie_elf_to_loadable_file(&mut bytes, segment_body.0); + + // let mut file = ::std::fs::File::create(::std::path::Path::new("test.o")).expect("file"); + // ::std::io::Write::write(&mut file, &bytes).expect("write"); + + Ok(bytes) +} + +fn convert_faerie_elf_to_loadable_file(bytes: &mut Vec, code_ptr: *const u8) { + use std::ffi::CStr; + use std::os::raw::c_char; + + assert!( + bytes[0x4] == 2 && bytes[0x5] == 1, + "bits and endianess in .ELF" + ); + let e_phoff = unsafe { *(bytes.as_ptr().offset(0x20) as *const u64) }; + let e_phnum = unsafe { *(bytes.as_ptr().offset(0x38) as *const u16) }; + assert!( + e_phoff == 0 && e_phnum == 0, + "program header table is empty" + ); + let e_phentsize = unsafe { *(bytes.as_ptr().offset(0x36) as *const u16) }; + assert_eq!(e_phentsize, 0x38, "size of ph"); + let e_shentsize = unsafe { *(bytes.as_ptr().offset(0x3A) as *const u16) }; + assert_eq!(e_shentsize, 0x40, "size of sh"); + + let e_shoff = unsafe { *(bytes.as_ptr().offset(0x28) as *const u64) }; + let e_shnum = unsafe { *(bytes.as_ptr().offset(0x3C) as *const u16) }; + let mut shstrtab_off = 0; + let mut segment = None; + for i in 0..e_shnum { + let off = e_shoff as isize + i as isize * e_shentsize as isize; + let sh_type = unsafe { *(bytes.as_ptr().offset(off + 0x4) as *const u32) }; + if sh_type == /* SHT_SYMTAB */ 3 { + shstrtab_off = unsafe { *(bytes.as_ptr().offset(off + 0x18) as *const u64) }; + } + if sh_type != /* SHT_PROGBITS */ 1 { + continue; + } + // It is a SHT_PROGBITS, but we need to check sh_name to ensure it is our function + let sh_name = unsafe { + let sh_name_off = *(bytes.as_ptr().offset(off) as *const u32); + CStr::from_ptr( + bytes + .as_ptr() + .offset((shstrtab_off + sh_name_off as u64) as isize) + as *const c_char, + ) + .to_str() + .expect("name") + }; + if sh_name != ".text.all" { + continue; + } + + assert!(segment.is_none()); + // Functions was added at emit_debugsections_image as .text.all. + // Patch vaddr, and save file location and its size. + unsafe { + *(bytes.as_ptr().offset(off + 0x10) as *mut u64) = code_ptr as u64; + }; + let sh_offset = unsafe { *(bytes.as_ptr().offset(off + 0x18) as *const u64) }; + let sh_size = unsafe { *(bytes.as_ptr().offset(off + 0x20) as *const u64) }; + segment = Some((sh_offset, code_ptr, sh_size)); + // Fix name too: cut it to just ".text" + unsafe { + let sh_name_off = *(bytes.as_ptr().offset(off) as *const u32); + bytes[(shstrtab_off + sh_name_off as u64) as usize + ".text".len()] = 0; + } + } + + // LLDB wants segment with virtual address set, placing them at the end of ELF. + let ph_off = bytes.len(); + if let Some((sh_offset, v_offset, sh_size)) = segment { + let segment = vec![0; 0x38]; + unsafe { + *(segment.as_ptr() as *mut u32) = /* PT_LOAD */ 0x1; + *(segment.as_ptr().offset(0x8) as *mut u64) = sh_offset; + *(segment.as_ptr().offset(0x10) as *mut u64) = v_offset as u64; + *(segment.as_ptr().offset(0x18) as *mut u64) = v_offset as u64; + *(segment.as_ptr().offset(0x20) as *mut u64) = sh_size; + *(segment.as_ptr().offset(0x28) as *mut u64) = sh_size; + } + bytes.extend_from_slice(&segment); + } else { + unreachable!(); + } + + // It is somewhat loadable ELF file at this moment. + // Update e_flags, e_phoff and e_phnum. + unsafe { + *(bytes.as_ptr().offset(0x10) as *mut u16) = /* ET_DYN */ 3; + *(bytes.as_ptr().offset(0x20) as *mut u64) = ph_off as u64; + *(bytes.as_ptr().offset(0x38) as *mut u16) = 1 as u16; + } +} diff --git a/crates/debug/src/read_debuginfo.rs b/crates/debug/src/read_debuginfo.rs new file mode 100644 index 0000000000..25b4e3fbc4 --- /dev/null +++ b/crates/debug/src/read_debuginfo.rs @@ -0,0 +1,245 @@ +use gimli::{ + DebugAbbrev, DebugAddr, DebugInfo, DebugLine, DebugLineStr, DebugLoc, DebugLocLists, + DebugRanges, DebugRngLists, DebugStr, DebugStrOffsets, DebugTypes, EndianSlice, LittleEndian, + LocationLists, RangeLists, +}; +use std::collections::HashMap; +use std::path::PathBuf; +use wasmparser::{self, ModuleReader, SectionCode}; + +trait Reader: gimli::Reader {} + +impl<'input> Reader for gimli::EndianSlice<'input, LittleEndian> {} + +pub use wasmparser::Type as WasmType; + +pub type Dwarf<'input> = gimli::Dwarf>; + +#[derive(Debug)] +pub struct FunctionMetadata { + pub params: Box<[WasmType]>, + pub locals: Box<[(u32, WasmType)]>, +} + +#[derive(Debug)] +pub struct WasmFileInfo { + pub path: Option, + pub code_section_offset: u64, + pub funcs: Box<[FunctionMetadata]>, +} + +#[derive(Debug)] +pub struct NameSection { + pub module_name: Option, + pub func_names: HashMap, + pub locals_names: HashMap>, +} + +#[derive(Debug)] +pub struct DebugInfoData<'a> { + pub dwarf: Dwarf<'a>, + pub name_section: Option, + pub wasm_file: WasmFileInfo, +} + +fn convert_sections<'a>(sections: HashMap<&str, &'a [u8]>) -> Dwarf<'a> { + const EMPTY_SECTION: &[u8] = &[]; + + let endian = LittleEndian; + let debug_str = DebugStr::new(sections.get(".debug_str").unwrap_or(&EMPTY_SECTION), endian); + let debug_abbrev = DebugAbbrev::new( + sections.get(".debug_abbrev").unwrap_or(&EMPTY_SECTION), + endian, + ); + let debug_info = DebugInfo::new( + sections.get(".debug_info").unwrap_or(&EMPTY_SECTION), + endian, + ); + let debug_line = DebugLine::new( + sections.get(".debug_line").unwrap_or(&EMPTY_SECTION), + endian, + ); + + if sections.contains_key(".debug_addr") { + panic!("Unexpected .debug_addr"); + } + + let debug_addr = DebugAddr::from(EndianSlice::new(EMPTY_SECTION, endian)); + + if sections.contains_key(".debug_line_str") { + panic!("Unexpected .debug_line_str"); + } + + let debug_line_str = DebugLineStr::from(EndianSlice::new(EMPTY_SECTION, endian)); + let debug_str_sup = DebugStr::from(EndianSlice::new(EMPTY_SECTION, endian)); + + if sections.contains_key(".debug_rnglists") { + panic!("Unexpected .debug_rnglists"); + } + + let debug_ranges = match sections.get(".debug_ranges") { + Some(section) => DebugRanges::new(section, endian), + None => DebugRanges::new(EMPTY_SECTION, endian), + }; + let debug_rnglists = DebugRngLists::new(EMPTY_SECTION, endian); + let ranges = RangeLists::new(debug_ranges, debug_rnglists); + + if sections.contains_key(".debug_loclists") { + panic!("Unexpected .debug_loclists"); + } + + let debug_loc = match sections.get(".debug_loc") { + Some(section) => DebugLoc::new(section, endian), + None => DebugLoc::new(EMPTY_SECTION, endian), + }; + let debug_loclists = DebugLocLists::new(EMPTY_SECTION, endian); + let locations = LocationLists::new(debug_loc, debug_loclists); + + if sections.contains_key(".debug_str_offsets") { + panic!("Unexpected .debug_str_offsets"); + } + + let debug_str_offsets = DebugStrOffsets::from(EndianSlice::new(EMPTY_SECTION, endian)); + + if sections.contains_key(".debug_types") { + panic!("Unexpected .debug_types"); + } + + let debug_types = DebugTypes::from(EndianSlice::new(EMPTY_SECTION, endian)); + + Dwarf { + debug_abbrev, + debug_addr, + debug_info, + debug_line, + debug_line_str, + debug_str, + debug_str_offsets, + debug_str_sup, + debug_types, + locations, + ranges, + } +} + +fn read_name_section(reader: wasmparser::NameSectionReader) -> wasmparser::Result { + let mut module_name = None; + let mut func_names = HashMap::new(); + let mut locals_names = HashMap::new(); + for i in reader.into_iter() { + match i? { + wasmparser::Name::Module(m) => { + module_name = Some(String::from(m.get_name()?)); + } + wasmparser::Name::Function(f) => { + let mut reader = f.get_map()?; + while let Ok(naming) = reader.read() { + func_names.insert(naming.index, String::from(naming.name)); + } + } + wasmparser::Name::Local(l) => { + let mut reader = l.get_function_local_reader()?; + while let Ok(f) = reader.read() { + let mut names = HashMap::new(); + let mut reader = f.get_map()?; + while let Ok(naming) = reader.read() { + names.insert(naming.index, String::from(naming.name)); + } + locals_names.insert(f.func_index, names); + } + } + } + } + let result = NameSection { + module_name, + func_names, + locals_names, + }; + Ok(result) +} + +pub fn read_debuginfo(data: &[u8]) -> DebugInfoData { + let mut reader = ModuleReader::new(data).expect("reader"); + let mut sections = HashMap::new(); + let mut name_section = None; + let mut code_section_offset = 0; + + let mut signatures_params: Vec> = Vec::new(); + let mut func_params_refs: Vec = Vec::new(); + let mut func_locals: Vec> = Vec::new(); + + while !reader.eof() { + let section = reader.read().expect("section"); + match section.code { + SectionCode::Custom { name, .. } => { + if name.starts_with(".debug_") { + let mut reader = section.get_binary_reader(); + let len = reader.bytes_remaining(); + sections.insert(name, reader.read_bytes(len).expect("bytes")); + } + if name == "name" { + if let Ok(reader) = section.get_name_section_reader() { + if let Ok(section) = read_name_section(reader) { + name_section = Some(section); + } + } + } + } + SectionCode::Type => { + signatures_params = section + .get_type_section_reader() + .expect("type section") + .into_iter() + .map(|ft| ft.expect("type").params) + .collect::>(); + } + SectionCode::Function => { + func_params_refs = section + .get_function_section_reader() + .expect("function section") + .into_iter() + .map(|index| index.expect("func index") as usize) + .collect::>(); + } + SectionCode::Code => { + code_section_offset = section.range().start as u64; + func_locals = section + .get_code_section_reader() + .expect("code section") + .into_iter() + .map(|body| { + let locals = body + .expect("body") + .get_locals_reader() + .expect("locals reader"); + locals + .into_iter() + .collect::, _>>() + .expect("locals data") + .into_boxed_slice() + }) + .collect::>(); + } + _ => (), + } + } + + let func_meta = func_params_refs + .into_iter() + .zip(func_locals.into_iter()) + .map(|(params_index, locals)| FunctionMetadata { + params: signatures_params[params_index].clone(), + locals, + }) + .collect::>(); + + DebugInfoData { + dwarf: convert_sections(sections), + name_section, + wasm_file: WasmFileInfo { + path: None, + code_section_offset, + funcs: func_meta.into_boxed_slice(), + }, + } +} diff --git a/crates/debug/src/transform/address_transform.rs b/crates/debug/src/transform/address_transform.rs new file mode 100644 index 0000000000..9a68e7dee2 --- /dev/null +++ b/crates/debug/src/transform/address_transform.rs @@ -0,0 +1,655 @@ +use crate::WasmFileInfo; +use gimli::write; +use more_asserts::assert_le; +use std::collections::BTreeMap; +use std::collections::HashMap; +use std::iter::FromIterator; +use wasmtime_environ::entity::{EntityRef, PrimaryMap}; +use wasmtime_environ::ir::SourceLoc; +use wasmtime_environ::wasm::DefinedFuncIndex; +use wasmtime_environ::{FunctionAddressMap, ModuleAddressMap}; + +pub type GeneratedAddress = usize; +pub type WasmAddress = u64; + +/// Contains mapping of the generated address to its original +/// source location. +#[derive(Debug)] +pub struct AddressMap { + pub generated: GeneratedAddress, + pub wasm: WasmAddress, +} + +/// Information about generated function code: its body start, +/// length, and instructions addresses. +#[derive(Debug)] +pub struct FunctionMap { + pub offset: GeneratedAddress, + pub len: GeneratedAddress, + pub wasm_start: WasmAddress, + pub wasm_end: WasmAddress, + pub addresses: Box<[AddressMap]>, +} + +/// Mapping of the source location to its generated code range. +#[derive(Debug)] +struct Position { + wasm_pos: WasmAddress, + gen_start: GeneratedAddress, + gen_end: GeneratedAddress, +} + +/// Mapping of continuous range of source location to its generated +/// code. The positions are always in accending order for search. +#[derive(Debug)] +struct Range { + wasm_start: WasmAddress, + wasm_end: WasmAddress, + gen_start: GeneratedAddress, + gen_end: GeneratedAddress, + positions: Box<[Position]>, +} + +/// Helper function address lookup data. Contains ranges start positions +/// index and ranges data. The multiple ranges can include the same +/// original source position. The index (B-Tree) uses range start +/// position as a key. +#[derive(Debug)] +struct FuncLookup { + index: Vec<(WasmAddress, Box<[usize]>)>, + ranges: Box<[Range]>, +} + +/// Mapping of original functions to generated code locations/ranges. +#[derive(Debug)] +struct FuncTransform { + start: WasmAddress, + end: WasmAddress, + index: DefinedFuncIndex, + lookup: FuncLookup, +} + +/// Module functions mapping to generated code. +#[derive(Debug)] +pub struct AddressTransform { + map: PrimaryMap, + func: Vec<(WasmAddress, FuncTransform)>, +} + +/// Returns a wasm bytecode offset in the code section from SourceLoc. +pub fn get_wasm_code_offset(loc: SourceLoc, code_section_offset: u64) -> WasmAddress { + // Code section size <= 4GB, allow wrapped SourceLoc to recover the overflow. + loc.bits().wrapping_sub(code_section_offset as u32) as WasmAddress +} + +fn build_function_lookup( + ft: &FunctionAddressMap, + code_section_offset: u64, +) -> (WasmAddress, WasmAddress, FuncLookup) { + assert_le!(code_section_offset, ft.start_srcloc.bits() as u64); + let fn_start = get_wasm_code_offset(ft.start_srcloc, code_section_offset); + let fn_end = get_wasm_code_offset(ft.end_srcloc, code_section_offset); + assert_le!(fn_start, fn_end); + + // Build ranges of continuous source locations. The new ranges starts when + // non-descending order is interrupted. Assuming the same origin location can + // be present in multiple ranges. + let mut range_wasm_start = fn_start; + let mut range_gen_start = ft.body_offset; + let mut last_wasm_pos = range_wasm_start; + let mut ranges = Vec::new(); + let mut ranges_index = BTreeMap::new(); + let mut current_range = Vec::new(); + for t in &ft.instructions { + if t.srcloc.is_default() { + continue; + } + + let offset = get_wasm_code_offset(t.srcloc, code_section_offset); + assert_le!(fn_start, offset); + assert_le!(offset, fn_end); + + let inst_gen_start = t.code_offset; + let inst_gen_end = t.code_offset + t.code_len; + + if last_wasm_pos > offset { + // Start new range. + ranges_index.insert(range_wasm_start, ranges.len()); + ranges.push(Range { + wasm_start: range_wasm_start, + wasm_end: last_wasm_pos, + gen_start: range_gen_start, + gen_end: inst_gen_start, + positions: current_range.into_boxed_slice(), + }); + range_wasm_start = offset; + range_gen_start = inst_gen_start; + current_range = Vec::new(); + } + // Continue existing range: add new wasm->generated code position. + current_range.push(Position { + wasm_pos: offset, + gen_start: inst_gen_start, + gen_end: inst_gen_end, + }); + last_wasm_pos = offset; + } + let last_gen_addr = ft.body_offset + ft.body_len; + ranges_index.insert(range_wasm_start, ranges.len()); + ranges.push(Range { + wasm_start: range_wasm_start, + wasm_end: fn_end, + gen_start: range_gen_start, + gen_end: last_gen_addr, + positions: current_range.into_boxed_slice(), + }); + + // Making ranges lookup faster by building index: B-tree with every range + // start position that maps into list of active ranges at this position. + let ranges = ranges.into_boxed_slice(); + let mut active_ranges = Vec::new(); + let mut index = BTreeMap::new(); + let mut last_wasm_pos = None; + for (wasm_start, range_index) in ranges_index { + if Some(wasm_start) == last_wasm_pos { + active_ranges.push(range_index); + continue; + } + if let Some(position) = last_wasm_pos { + index.insert(position, active_ranges.clone().into_boxed_slice()); + } + active_ranges.retain(|r| ranges[*r].wasm_end.cmp(&wasm_start) != std::cmp::Ordering::Less); + active_ranges.push(range_index); + last_wasm_pos = Some(wasm_start); + } + index.insert(last_wasm_pos.unwrap(), active_ranges.into_boxed_slice()); + let index = Vec::from_iter(index.into_iter()); + (fn_start, fn_end, FuncLookup { index, ranges }) +} + +fn build_function_addr_map( + at: &ModuleAddressMap, + code_section_offset: u64, +) -> PrimaryMap { + let mut map = PrimaryMap::new(); + for (_, ft) in at { + let mut fn_map = Vec::new(); + for t in &ft.instructions { + if t.srcloc.is_default() { + continue; + } + let offset = get_wasm_code_offset(t.srcloc, code_section_offset); + fn_map.push(AddressMap { + generated: t.code_offset, + wasm: offset, + }); + } + + if cfg!(debug) { + // fn_map is sorted by the generated field -- see FunctionAddressMap::instructions. + for i in 1..fn_map.len() { + assert_le!(fn_map[i - 1].generated, fn_map[i].generated); + } + } + + map.push(FunctionMap { + offset: ft.body_offset, + len: ft.body_len, + wasm_start: get_wasm_code_offset(ft.start_srcloc, code_section_offset), + wasm_end: get_wasm_code_offset(ft.end_srcloc, code_section_offset), + addresses: fn_map.into_boxed_slice(), + }); + } + map +} + +struct TransformRangeIter<'a> { + addr: u64, + indicies: &'a [usize], + ranges: &'a [Range], +} + +impl<'a> TransformRangeIter<'a> { + fn new(func: &'a FuncTransform, addr: u64) -> Self { + let found = match func + .lookup + .index + .binary_search_by(|entry| entry.0.cmp(&addr)) + { + Ok(i) => Some(&func.lookup.index[i].1), + Err(i) => { + if i > 0 { + Some(&func.lookup.index[i - 1].1) + } else { + None + } + } + }; + if let Some(range_indices) = found { + TransformRangeIter { + addr, + indicies: range_indices, + ranges: &func.lookup.ranges, + } + } else { + unreachable!(); + } + } +} +impl<'a> Iterator for TransformRangeIter<'a> { + type Item = (usize, usize); + fn next(&mut self) -> Option { + if let Some((first, tail)) = self.indicies.split_first() { + let range_index = *first; + let range = &self.ranges[range_index]; + self.indicies = tail; + let address = match range + .positions + .binary_search_by(|a| a.wasm_pos.cmp(&self.addr)) + { + Ok(i) => range.positions[i].gen_start, + Err(i) => { + if i == 0 { + range.gen_start + } else { + range.positions[i - 1].gen_end + } + } + }; + Some((address, range_index)) + } else { + None + } + } +} + +struct TransformRangeEndIter<'a> { + addr: u64, + indicies: &'a [usize], + ranges: &'a [Range], +} + +impl<'a> TransformRangeEndIter<'a> { + fn new(func: &'a FuncTransform, addr: u64) -> Self { + let found = match func + .lookup + .index + .binary_search_by(|entry| entry.0.cmp(&addr)) + { + Ok(i) => Some(&func.lookup.index[i].1), + Err(i) => { + if i > 0 { + Some(&func.lookup.index[i - 1].1) + } else { + None + } + } + }; + if let Some(range_indices) = found { + TransformRangeEndIter { + addr, + indicies: range_indices, + ranges: &func.lookup.ranges, + } + } else { + unreachable!(); + } + } +} + +impl<'a> Iterator for TransformRangeEndIter<'a> { + type Item = (usize, usize); + fn next(&mut self) -> Option { + while let Some((first, tail)) = self.indicies.split_first() { + let range_index = *first; + let range = &self.ranges[range_index]; + if range.wasm_start >= self.addr { + continue; + } + self.indicies = tail; + let address = match range + .positions + .binary_search_by(|a| a.wasm_pos.cmp(&self.addr)) + { + Ok(i) => range.positions[i].gen_end, + Err(i) => { + if i == range.positions.len() { + range.gen_end + } else { + range.positions[i].gen_start + } + } + }; + return Some((address, range_index)); + } + None + } +} + +impl AddressTransform { + pub fn new(at: &ModuleAddressMap, wasm_file: &WasmFileInfo) -> Self { + let code_section_offset = wasm_file.code_section_offset; + + let mut func = BTreeMap::new(); + for (i, ft) in at { + let (fn_start, fn_end, lookup) = build_function_lookup(ft, code_section_offset); + + func.insert( + fn_start, + FuncTransform { + start: fn_start, + end: fn_end, + index: i, + lookup, + }, + ); + } + + let map = build_function_addr_map(at, code_section_offset); + let func = Vec::from_iter(func.into_iter()); + AddressTransform { map, func } + } + + fn find_func(&self, addr: u64) -> Option<&FuncTransform> { + // TODO check if we need to include end address + let func = match self.func.binary_search_by(|entry| entry.0.cmp(&addr)) { + Ok(i) => &self.func[i].1, + Err(i) => { + if i > 0 { + &self.func[i - 1].1 + } else { + return None; + } + } + }; + if addr >= func.start { + return Some(func); + } + None + } + + pub fn find_func_index(&self, addr: u64) -> Option { + self.find_func(addr).map(|f| f.index) + } + + pub fn translate_raw(&self, addr: u64) -> Option<(DefinedFuncIndex, GeneratedAddress)> { + if addr == 0 { + // It's normally 0 for debug info without the linked code. + return None; + } + if let Some(func) = self.find_func(addr) { + if addr == func.end { + // Clamp last address to the end to extend translation to the end + // of the function. + let map = &self.map[func.index]; + return Some((func.index, map.len)); + } + let first_result = TransformRangeIter::new(func, addr).next(); + first_result.map(|(address, _)| (func.index, address)) + } else { + // Address was not found: function was not compiled? + None + } + } + + pub fn can_translate_address(&self, addr: u64) -> bool { + self.translate(addr).is_some() + } + + pub fn translate(&self, addr: u64) -> Option { + self.translate_raw(addr) + .map(|(func_index, address)| write::Address::Symbol { + symbol: func_index.index(), + addend: address as i64, + }) + } + + pub fn translate_ranges_raw( + &self, + start: u64, + end: u64, + ) -> Option<(DefinedFuncIndex, Vec<(GeneratedAddress, GeneratedAddress)>)> { + if start == 0 { + // It's normally 0 for debug info without the linked code. + return None; + } + if let Some(func) = self.find_func(start) { + let mut starts: HashMap = + HashMap::from_iter(TransformRangeIter::new(func, start).map(|(a, r)| (r, a))); + let mut result = Vec::new(); + TransformRangeEndIter::new(func, end).for_each(|(a, r)| { + let range_start = if let Some(range_start) = starts.get(&r) { + let range_start = *range_start; + starts.remove(&r); + range_start + } else { + let range = &func.lookup.ranges[r]; + range.gen_start + }; + result.push((range_start, a)); + }); + for (r, range_start) in starts { + let range = &func.lookup.ranges[r]; + result.push((range_start, range.gen_end)); + } + return Some((func.index, result)); + } + // Address was not found: function was not compiled? + None + } + + pub fn translate_ranges(&self, start: u64, end: u64) -> Vec<(write::Address, u64)> { + self.translate_ranges_raw(start, end) + .map_or(vec![], |(func_index, ranges)| { + ranges + .iter() + .map(|(start, end)| { + ( + write::Address::Symbol { + symbol: func_index.index(), + addend: *start as i64, + }, + (*end - *start) as u64, + ) + }) + .collect::>() + }) + } + + pub fn map(&self) -> &PrimaryMap { + &self.map + } + + pub fn func_range(&self, index: DefinedFuncIndex) -> (GeneratedAddress, GeneratedAddress) { + let map = &self.map[index]; + (map.offset, map.offset + map.len) + } + + pub fn func_source_range(&self, index: DefinedFuncIndex) -> (WasmAddress, WasmAddress) { + let map = &self.map[index]; + (map.wasm_start, map.wasm_end) + } + + pub fn convert_to_code_range( + &self, + addr: write::Address, + len: u64, + ) -> (GeneratedAddress, GeneratedAddress) { + let start = if let write::Address::Symbol { addend, .. } = addr { + // TODO subtract self.map[symbol].offset ? + addend as GeneratedAddress + } else { + unreachable!(); + }; + (start, start + len as GeneratedAddress) + } +} + +#[cfg(test)] +mod tests { + use super::{build_function_lookup, get_wasm_code_offset, AddressTransform}; + use crate::read_debuginfo::WasmFileInfo; + use gimli::write::Address; + use std::iter::FromIterator; + use wasmtime_environ::entity::PrimaryMap; + use wasmtime_environ::ir::SourceLoc; + use wasmtime_environ::{FunctionAddressMap, InstructionAddressMap, ModuleAddressMap}; + + #[test] + fn test_get_wasm_code_offset() { + let offset = get_wasm_code_offset(SourceLoc::new(3), 1); + assert_eq!(2, offset); + let offset = get_wasm_code_offset(SourceLoc::new(16), 0xF000_0000); + assert_eq!(0x1000_0010, offset); + let offset = get_wasm_code_offset(SourceLoc::new(1), 0x20_8000_0000); + assert_eq!(0x8000_0001, offset); + } + + fn create_simple_func(wasm_offset: u32) -> FunctionAddressMap { + FunctionAddressMap { + instructions: vec![ + InstructionAddressMap { + srcloc: SourceLoc::new(wasm_offset + 2), + code_offset: 5, + code_len: 3, + }, + InstructionAddressMap { + srcloc: SourceLoc::new(wasm_offset + 7), + code_offset: 15, + code_len: 8, + }, + ], + start_srcloc: SourceLoc::new(wasm_offset), + end_srcloc: SourceLoc::new(wasm_offset + 10), + body_offset: 0, + body_len: 30, + } + } + + fn create_simple_module(func: FunctionAddressMap) -> ModuleAddressMap { + PrimaryMap::from_iter(vec![func]) + } + + #[test] + fn test_build_function_lookup_simple() { + let input = create_simple_func(11); + let (start, end, lookup) = build_function_lookup(&input, 1); + assert_eq!(10, start); + assert_eq!(20, end); + + assert_eq!(1, lookup.index.len()); + let index_entry = lookup.index.into_iter().next().unwrap(); + assert_eq!((10u64, vec![0].into_boxed_slice()), index_entry); + assert_eq!(1, lookup.ranges.len()); + let range = &lookup.ranges[0]; + assert_eq!(10, range.wasm_start); + assert_eq!(20, range.wasm_end); + assert_eq!(0, range.gen_start); + assert_eq!(30, range.gen_end); + let positions = &range.positions; + assert_eq!(2, positions.len()); + assert_eq!(12, positions[0].wasm_pos); + assert_eq!(5, positions[0].gen_start); + assert_eq!(8, positions[0].gen_end); + assert_eq!(17, positions[1].wasm_pos); + assert_eq!(15, positions[1].gen_start); + assert_eq!(23, positions[1].gen_end); + } + + #[test] + fn test_build_function_lookup_two_ranges() { + let mut input = create_simple_func(11); + // append instruction with same srcloc as input.instructions[0] + input.instructions.push(InstructionAddressMap { + srcloc: SourceLoc::new(11 + 2), + code_offset: 23, + code_len: 3, + }); + let (start, end, lookup) = build_function_lookup(&input, 1); + assert_eq!(10, start); + assert_eq!(20, end); + + assert_eq!(2, lookup.index.len()); + let index_entries = Vec::from_iter(lookup.index.into_iter()); + assert_eq!((10u64, vec![0].into_boxed_slice()), index_entries[0]); + assert_eq!((12u64, vec![0, 1].into_boxed_slice()), index_entries[1]); + assert_eq!(2, lookup.ranges.len()); + + let range = &lookup.ranges[0]; + assert_eq!(10, range.wasm_start); + assert_eq!(17, range.wasm_end); + assert_eq!(0, range.gen_start); + assert_eq!(23, range.gen_end); + let positions = &range.positions; + assert_eq!(2, positions.len()); + assert_eq!(12, positions[0].wasm_pos); + assert_eq!(5, positions[0].gen_start); + assert_eq!(8, positions[0].gen_end); + assert_eq!(17, positions[1].wasm_pos); + assert_eq!(15, positions[1].gen_start); + assert_eq!(23, positions[1].gen_end); + + let range = &lookup.ranges[1]; + assert_eq!(12, range.wasm_start); + assert_eq!(20, range.wasm_end); + assert_eq!(23, range.gen_start); + assert_eq!(30, range.gen_end); + let positions = &range.positions; + assert_eq!(1, positions.len()); + assert_eq!(12, positions[0].wasm_pos); + assert_eq!(23, positions[0].gen_start); + assert_eq!(26, positions[0].gen_end); + } + + #[test] + fn test_addr_translate() { + let input = create_simple_module(create_simple_func(11)); + let at = AddressTransform::new( + &input, + &WasmFileInfo { + path: None, + code_section_offset: 1, + funcs: Box::new([]), + }, + ); + + let addr = at.translate(10); + assert_eq!( + Some(Address::Symbol { + symbol: 0, + addend: 0, + }), + addr + ); + + let addr = at.translate(20); + assert_eq!( + Some(Address::Symbol { + symbol: 0, + addend: 30, + }), + addr + ); + + let addr = at.translate(0); + assert_eq!(None, addr); + + let addr = at.translate(12); + assert_eq!( + Some(Address::Symbol { + symbol: 0, + addend: 5, + }), + addr + ); + + let addr = at.translate(18); + assert_eq!( + Some(Address::Symbol { + symbol: 0, + addend: 23, + }), + addr + ); + } +} diff --git a/crates/debug/src/transform/attr.rs b/crates/debug/src/transform/attr.rs new file mode 100644 index 0000000000..bd0d784479 --- /dev/null +++ b/crates/debug/src/transform/attr.rs @@ -0,0 +1,283 @@ +use super::address_transform::AddressTransform; +use super::expression::{compile_expression, CompiledExpression, FunctionFrameInfo}; +use super::range_info_builder::RangeInfoBuilder; +use super::refs::{PendingDebugInfoRefs, PendingUnitRefs}; +use super::{DebugInputContext, Reader, TransformError}; +use anyhow::Error; +use gimli::{write, AttributeValue, DebugLineOffset, DebugStr, DebuggingInformationEntry}; + +pub(crate) enum FileAttributeContext<'a> { + Root(Option), + Children(&'a Vec, Option<&'a CompiledExpression>), +} + +fn is_exprloc_to_loclist_allowed(attr_name: gimli::constants::DwAt) -> bool { + match attr_name { + gimli::DW_AT_location + | gimli::DW_AT_string_length + | gimli::DW_AT_return_addr + | gimli::DW_AT_data_member_location + | gimli::DW_AT_frame_base + | gimli::DW_AT_segment + | gimli::DW_AT_static_link + | gimli::DW_AT_use_location + | gimli::DW_AT_vtable_elem_location => true, + _ => false, + } +} + +pub(crate) fn clone_die_attributes<'a, R>( + entry: &DebuggingInformationEntry, + context: &DebugInputContext, + addr_tr: &'a AddressTransform, + frame_info: Option<&FunctionFrameInfo>, + unit_encoding: gimli::Encoding, + out_unit: &mut write::Unit, + current_scope_id: write::UnitEntryId, + subprogram_range_builder: Option, + scope_ranges: Option<&Vec<(u64, u64)>>, + cu_low_pc: u64, + out_strings: &mut write::StringTable, + pending_die_refs: &mut PendingUnitRefs, + pending_di_refs: &mut PendingDebugInfoRefs, + file_context: FileAttributeContext<'a>, +) -> Result<(), Error> +where + R: Reader, +{ + let _tag = &entry.tag(); + let endian = gimli::RunTimeEndian::Little; + + let range_info = if let Some(subprogram_range_builder) = subprogram_range_builder { + subprogram_range_builder + } else if entry.tag() == gimli::DW_TAG_compile_unit { + // FIXME currently address_transform operate on a single func range, + // once it is fixed we can properly set DW_AT_ranges attribute. + // Using for now DW_AT_low_pc = 0. + RangeInfoBuilder::Position(0) + } else { + RangeInfoBuilder::from(entry, context, unit_encoding, cu_low_pc)? + }; + range_info.build(addr_tr, out_unit, current_scope_id); + + let mut attrs = entry.attrs(); + while let Some(attr) = attrs.next()? { + let attr_value = match attr.value() { + AttributeValue::Addr(_) if attr.name() == gimli::DW_AT_low_pc => { + continue; + } + AttributeValue::Udata(_) if attr.name() == gimli::DW_AT_high_pc => { + continue; + } + AttributeValue::RangeListsRef(_) if attr.name() == gimli::DW_AT_ranges => { + continue; + } + AttributeValue::Exprloc(_) if attr.name() == gimli::DW_AT_frame_base => { + continue; + } + + AttributeValue::Addr(u) => { + let addr = addr_tr.translate(u).unwrap_or(write::Address::Constant(0)); + write::AttributeValue::Address(addr) + } + AttributeValue::Udata(u) => write::AttributeValue::Udata(u), + AttributeValue::Data1(d) => write::AttributeValue::Data1(d), + AttributeValue::Data2(d) => write::AttributeValue::Data2(d), + AttributeValue::Data4(d) => write::AttributeValue::Data4(d), + AttributeValue::Sdata(d) => write::AttributeValue::Sdata(d), + AttributeValue::Flag(f) => write::AttributeValue::Flag(f), + AttributeValue::DebugLineRef(line_program_offset) => { + if let FileAttributeContext::Root(o) = file_context { + if o != Some(line_program_offset) { + return Err(TransformError("invalid debug_line offset").into()); + } + write::AttributeValue::LineProgramRef + } else { + return Err(TransformError("unexpected debug_line index attribute").into()); + } + } + AttributeValue::FileIndex(i) => { + if let FileAttributeContext::Children(file_map, _) = file_context { + write::AttributeValue::FileIndex(Some(file_map[(i - 1) as usize])) + } else { + return Err(TransformError("unexpected file index attribute").into()); + } + } + AttributeValue::DebugStrRef(str_offset) => { + let s = context.debug_str.get_str(str_offset)?.to_slice()?.to_vec(); + write::AttributeValue::StringRef(out_strings.add(s)) + } + AttributeValue::RangeListsRef(r) => { + let range_info = + RangeInfoBuilder::from_ranges_ref(r, context, unit_encoding, cu_low_pc)?; + let range_list_id = range_info.build_ranges(addr_tr, &mut out_unit.ranges); + write::AttributeValue::RangeListRef(range_list_id) + } + AttributeValue::LocationListsRef(r) => { + let low_pc = 0; + let mut locs = context.loclists.locations( + r, + unit_encoding, + low_pc, + &context.debug_addr, + context.debug_addr_base, + )?; + let frame_base = if let FileAttributeContext::Children(_, frame_base) = file_context + { + frame_base + } else { + None + }; + let mut result = None; + while let Some(loc) = locs.next()? { + if let Some(expr) = compile_expression(&loc.data, unit_encoding, frame_base)? { + if result.is_none() { + result = Some(Vec::new()); + } + for (start, len, expr) in expr.build_with_locals( + &[(loc.range.begin, loc.range.end)], + addr_tr, + frame_info, + endian, + ) { + if len == 0 { + // Ignore empty range + continue; + } + result.as_mut().unwrap().push(write::Location::StartLength { + begin: start, + length: len, + data: expr, + }); + } + } else { + // FIXME _expr contains invalid expression + continue; // ignore entry + } + } + if result.is_none() { + continue; // no valid locations + } + let list_id = out_unit.locations.add(write::LocationList(result.unwrap())); + write::AttributeValue::LocationListRef(list_id) + } + AttributeValue::Exprloc(ref expr) => { + let frame_base = if let FileAttributeContext::Children(_, frame_base) = file_context + { + frame_base + } else { + None + }; + if let Some(expr) = compile_expression(expr, unit_encoding, frame_base)? { + if expr.is_simple() { + if let Some(expr) = expr.build() { + write::AttributeValue::Exprloc(expr) + } else { + continue; + } + } else { + // Conversion to loclist is required. + if let Some(scope_ranges) = scope_ranges { + let exprs = + expr.build_with_locals(scope_ranges, addr_tr, frame_info, endian); + if exprs.is_empty() { + continue; + } + let found_single_expr = { + // Micro-optimization all expressions alike, use one exprloc. + let mut found_expr: Option = None; + for (_, _, expr) in &exprs { + if let Some(ref prev_expr) = found_expr { + if expr.0.eq(&prev_expr.0) { + continue; // the same expression + } + found_expr = None; + break; + } + found_expr = Some(expr.clone()) + } + found_expr + }; + if let Some(expr) = found_single_expr { + write::AttributeValue::Exprloc(expr) + } else if is_exprloc_to_loclist_allowed(attr.name()) { + // Converting exprloc to loclist. + let mut locs = Vec::new(); + for (begin, length, data) in exprs { + if length == 0 { + // Ignore empty range + continue; + } + locs.push(write::Location::StartLength { + begin, + length, + data, + }); + } + let list_id = out_unit.locations.add(write::LocationList(locs)); + write::AttributeValue::LocationListRef(list_id) + } else { + continue; + } + } else { + continue; + } + } + } else { + // FIXME _expr contains invalid expression + continue; // ignore attribute + } + } + AttributeValue::Encoding(e) => write::AttributeValue::Encoding(e), + AttributeValue::DecimalSign(e) => write::AttributeValue::DecimalSign(e), + AttributeValue::Endianity(e) => write::AttributeValue::Endianity(e), + AttributeValue::Accessibility(e) => write::AttributeValue::Accessibility(e), + AttributeValue::Visibility(e) => write::AttributeValue::Visibility(e), + AttributeValue::Virtuality(e) => write::AttributeValue::Virtuality(e), + AttributeValue::Language(e) => write::AttributeValue::Language(e), + AttributeValue::AddressClass(e) => write::AttributeValue::AddressClass(e), + AttributeValue::IdentifierCase(e) => write::AttributeValue::IdentifierCase(e), + AttributeValue::CallingConvention(e) => write::AttributeValue::CallingConvention(e), + AttributeValue::Inline(e) => write::AttributeValue::Inline(e), + AttributeValue::Ordering(e) => write::AttributeValue::Ordering(e), + AttributeValue::UnitRef(offset) => { + pending_die_refs.insert(current_scope_id, attr.name(), offset); + continue; + } + AttributeValue::DebugInfoRef(offset) => { + pending_di_refs.insert(current_scope_id, attr.name(), offset); + continue; + } + _ => panic!(), //write::AttributeValue::StringRef(out_strings.add("_")), + }; + let current_scope = out_unit.get_mut(current_scope_id); + current_scope.set(attr.name(), attr_value); + } + Ok(()) +} + +pub(crate) fn clone_attr_string( + attr_value: &AttributeValue, + form: gimli::DwForm, + debug_str: &DebugStr, + out_strings: &mut write::StringTable, +) -> Result +where + R: Reader, +{ + let content = match attr_value { + AttributeValue::DebugStrRef(str_offset) => { + debug_str.get_str(*str_offset)?.to_slice()?.to_vec() + } + AttributeValue::String(b) => b.to_slice()?.to_vec(), + _ => panic!("Unexpected attribute value"), + }; + Ok(match form { + gimli::DW_FORM_strp => { + let id = out_strings.add(content); + write::LineString::StringRef(id) + } + gimli::DW_FORM_string => write::LineString::String(content), + _ => panic!("DW_FORM_line_strp or other not supported"), + }) +} diff --git a/crates/debug/src/transform/expression.rs b/crates/debug/src/transform/expression.rs new file mode 100644 index 0000000000..2c3fb72392 --- /dev/null +++ b/crates/debug/src/transform/expression.rs @@ -0,0 +1,564 @@ +use super::address_transform::AddressTransform; +use anyhow::Error; +use gimli::{self, write, Expression, Operation, Reader, ReaderOffset, Register, X86_64}; +use more_asserts::{assert_le, assert_lt}; +use std::collections::{HashMap, HashSet}; +use wasmtime_environ::entity::EntityRef; +use wasmtime_environ::ir::{StackSlots, ValueLabel, ValueLabelsRanges, ValueLoc}; +use wasmtime_environ::isa::RegUnit; +use wasmtime_environ::wasm::{get_vmctx_value_label, DefinedFuncIndex}; +use wasmtime_environ::ModuleMemoryOffset; + +#[derive(Debug)] +pub struct FunctionFrameInfo<'a> { + pub value_ranges: &'a ValueLabelsRanges, + pub memory_offset: ModuleMemoryOffset, + pub stack_slots: &'a StackSlots, +} + +impl<'a> FunctionFrameInfo<'a> { + fn vmctx_memory_offset(&self) -> Option { + match self.memory_offset { + ModuleMemoryOffset::Defined(x) => Some(x as i64), + ModuleMemoryOffset::Imported(_) => { + // TODO implement memory offset for imported memory + None + } + ModuleMemoryOffset::None => None, + } + } +} + +#[derive(Debug)] +enum CompiledExpressionPart { + Code(Vec), + Local(ValueLabel), + Deref, +} + +#[derive(Debug)] +pub struct CompiledExpression { + parts: Vec, + need_deref: bool, +} + +impl Clone for CompiledExpressionPart { + fn clone(&self) -> Self { + match self { + CompiledExpressionPart::Code(c) => CompiledExpressionPart::Code(c.clone()), + CompiledExpressionPart::Local(i) => CompiledExpressionPart::Local(*i), + CompiledExpressionPart::Deref => CompiledExpressionPart::Deref, + } + } +} + +impl CompiledExpression { + pub fn vmctx() -> CompiledExpression { + CompiledExpression::from_label(get_vmctx_value_label()) + } + + pub fn from_label(label: ValueLabel) -> CompiledExpression { + CompiledExpression { + parts: vec![ + CompiledExpressionPart::Local(label), + CompiledExpressionPart::Code(vec![gimli::constants::DW_OP_stack_value.0 as u8]), + ], + need_deref: false, + } + } +} + +fn map_reg(reg: RegUnit) -> Register { + static mut REG_X86_MAP: Option> = None; + // FIXME lazy initialization? + unsafe { + if REG_X86_MAP.is_none() { + REG_X86_MAP = Some(HashMap::new()); + } + if let Some(val) = REG_X86_MAP.as_mut().unwrap().get(®) { + return *val; + } + let result = match reg { + 0 => X86_64::RAX, + 1 => X86_64::RCX, + 2 => X86_64::RDX, + 3 => X86_64::RBX, + 4 => X86_64::RSP, + 5 => X86_64::RBP, + 6 => X86_64::RSI, + 7 => X86_64::RDI, + 8 => X86_64::R8, + 9 => X86_64::R9, + 10 => X86_64::R10, + 11 => X86_64::R11, + 12 => X86_64::R12, + 13 => X86_64::R13, + 14 => X86_64::R14, + 15 => X86_64::R15, + 16 => X86_64::XMM0, + 17 => X86_64::XMM1, + 18 => X86_64::XMM2, + 19 => X86_64::XMM3, + 20 => X86_64::XMM4, + 21 => X86_64::XMM5, + 22 => X86_64::XMM6, + 23 => X86_64::XMM7, + 24 => X86_64::XMM8, + 25 => X86_64::XMM9, + 26 => X86_64::XMM10, + 27 => X86_64::XMM11, + 28 => X86_64::XMM12, + 29 => X86_64::XMM13, + 30 => X86_64::XMM14, + 31 => X86_64::XMM15, + _ => panic!("unknown x86_64 register {}", reg), + }; + REG_X86_MAP.as_mut().unwrap().insert(reg, result); + result + } +} + +fn translate_loc(loc: ValueLoc, frame_info: Option<&FunctionFrameInfo>) -> Option> { + use gimli::write::Writer; + match loc { + ValueLoc::Reg(reg) => { + let machine_reg = map_reg(reg).0 as u8; + Some(if machine_reg < 32 { + vec![gimli::constants::DW_OP_reg0.0 + machine_reg] + } else { + let endian = gimli::RunTimeEndian::Little; + let mut writer = write::EndianVec::new(endian); + writer + .write_u8(gimli::constants::DW_OP_regx.0 as u8) + .expect("regx"); + writer + .write_uleb128(machine_reg.into()) + .expect("machine_reg"); + writer.into_vec() + }) + } + ValueLoc::Stack(ss) => { + if let Some(frame_info) = frame_info { + if let Some(ss_offset) = frame_info.stack_slots[ss].offset { + let endian = gimli::RunTimeEndian::Little; + let mut writer = write::EndianVec::new(endian); + writer + .write_u8(gimli::constants::DW_OP_breg0.0 + X86_64::RBP.0 as u8) + .expect("bp wr"); + writer.write_sleb128(ss_offset as i64 + 16).expect("ss wr"); + writer + .write_u8(gimli::constants::DW_OP_deref.0 as u8) + .expect("bp wr"); + let buf = writer.into_vec(); + return Some(buf); + } + } + None + } + _ => None, + } +} + +fn append_memory_deref( + buf: &mut Vec, + frame_info: &FunctionFrameInfo, + vmctx_loc: ValueLoc, + endian: gimli::RunTimeEndian, +) -> write::Result { + use gimli::write::Writer; + let mut writer = write::EndianVec::new(endian); + // FIXME for imported memory + match vmctx_loc { + ValueLoc::Reg(vmctx_reg) => { + let reg = map_reg(vmctx_reg); + writer.write_u8(gimli::constants::DW_OP_breg0.0 + reg.0 as u8)?; + let memory_offset = match frame_info.vmctx_memory_offset() { + Some(offset) => offset, + None => { + return Ok(false); + } + }; + writer.write_sleb128(memory_offset)?; + } + ValueLoc::Stack(ss) => { + if let Some(ss_offset) = frame_info.stack_slots[ss].offset { + writer.write_u8(gimli::constants::DW_OP_breg0.0 + X86_64::RBP.0 as u8)?; + writer.write_sleb128(ss_offset as i64 + 16)?; + writer.write_u8(gimli::constants::DW_OP_deref.0 as u8)?; + writer.write_u8(gimli::constants::DW_OP_consts.0 as u8)?; + let memory_offset = match frame_info.vmctx_memory_offset() { + Some(offset) => offset, + None => { + return Ok(false); + } + }; + writer.write_sleb128(memory_offset)?; + writer.write_u8(gimli::constants::DW_OP_plus.0 as u8)?; + } else { + return Ok(false); + } + } + _ => { + return Ok(false); + } + } + writer.write_u8(gimli::constants::DW_OP_deref.0 as u8)?; + writer.write_u8(gimli::constants::DW_OP_swap.0 as u8)?; + writer.write_u8(gimli::constants::DW_OP_stack_value.0 as u8)?; + writer.write_u8(gimli::constants::DW_OP_constu.0 as u8)?; + writer.write_uleb128(0xffff_ffff)?; + writer.write_u8(gimli::constants::DW_OP_and.0 as u8)?; + writer.write_u8(gimli::constants::DW_OP_plus.0 as u8)?; + buf.extend_from_slice(writer.slice()); + Ok(true) +} + +impl CompiledExpression { + pub fn is_simple(&self) -> bool { + if let [CompiledExpressionPart::Code(_)] = self.parts.as_slice() { + true + } else { + self.parts.is_empty() + } + } + + pub fn build(&self) -> Option { + if let [CompiledExpressionPart::Code(code)] = self.parts.as_slice() { + return Some(write::Expression(code.to_vec())); + } + // locals found, not supported + None + } + + pub fn build_with_locals( + &self, + scope: &[(u64, u64)], // wasm ranges + addr_tr: &AddressTransform, + frame_info: Option<&FunctionFrameInfo>, + endian: gimli::RunTimeEndian, + ) -> Vec<(write::Address, u64, write::Expression)> { + if scope.is_empty() { + return vec![]; + } + + if let [CompiledExpressionPart::Code(code)] = self.parts.as_slice() { + let mut result_scope = Vec::new(); + for s in scope { + for (addr, len) in addr_tr.translate_ranges(s.0, s.1) { + result_scope.push((addr, len, write::Expression(code.to_vec()))); + } + } + return result_scope; + } + + let vmctx_label = get_vmctx_value_label(); + + // Some locals are present, preparing and divided ranges based on the scope + // and frame_info data. + let mut ranges_builder = ValueLabelRangesBuilder::new(scope, addr_tr, frame_info); + for p in &self.parts { + match p { + CompiledExpressionPart::Code(_) => (), + CompiledExpressionPart::Local(label) => ranges_builder.process_label(*label), + CompiledExpressionPart::Deref => ranges_builder.process_label(vmctx_label), + } + } + if self.need_deref { + ranges_builder.process_label(vmctx_label); + } + ranges_builder.remove_incomplete_ranges(); + let ranges = ranges_builder.ranges; + + let mut result = Vec::new(); + 'range: for CachedValueLabelRange { + func_index, + start, + end, + label_location, + } in ranges + { + // build expression + let mut code_buf = Vec::new(); + for part in &self.parts { + match part { + CompiledExpressionPart::Code(c) => code_buf.extend_from_slice(c.as_slice()), + CompiledExpressionPart::Local(label) => { + let loc = *label_location.get(&label).expect("loc"); + if let Some(expr) = translate_loc(loc, frame_info) { + code_buf.extend_from_slice(&expr) + } else { + continue 'range; + } + } + CompiledExpressionPart::Deref => { + if let (Some(vmctx_loc), Some(frame_info)) = + (label_location.get(&vmctx_label), frame_info) + { + if !append_memory_deref(&mut code_buf, frame_info, *vmctx_loc, endian) + .expect("append_memory_deref") + { + continue 'range; + } + } else { + continue 'range; + }; + } + } + } + if self.need_deref { + if let (Some(vmctx_loc), Some(frame_info)) = + (label_location.get(&vmctx_label), frame_info) + { + if !append_memory_deref(&mut code_buf, frame_info, *vmctx_loc, endian) + .expect("append_memory_deref") + { + continue 'range; + } + } else { + continue 'range; + }; + } + result.push(( + write::Address::Symbol { + symbol: func_index.index(), + addend: start as i64, + }, + (end - start) as u64, + write::Expression(code_buf), + )); + } + + result + } +} + +fn is_old_expression_format(buf: &[u8]) -> bool { + // Heuristic to detect old variable expression format without DW_OP_fbreg: + // DW_OP_plus_uconst op must be present, but not DW_OP_fbreg. + if buf.contains(&(gimli::constants::DW_OP_fbreg.0 as u8)) { + // Stop check if DW_OP_fbreg exist. + return false; + } + buf.contains(&(gimli::constants::DW_OP_plus_uconst.0 as u8)) +} + +pub fn compile_expression( + expr: &Expression, + encoding: gimli::Encoding, + frame_base: Option<&CompiledExpression>, +) -> Result, Error> +where + R: Reader, +{ + let mut pc = expr.0.clone(); + let buf = expr.0.to_slice()?; + let mut parts = Vec::new(); + let mut need_deref = false; + if is_old_expression_format(&buf) && frame_base.is_some() { + // Still supporting old DWARF variable expressions without fbreg. + parts.extend_from_slice(&frame_base.unwrap().parts); + need_deref = frame_base.unwrap().need_deref; + } + let base_len = parts.len(); + let mut code_chunk = Vec::new(); + while !pc.is_empty() { + let next = buf[pc.offset_from(&expr.0).into_u64() as usize]; + need_deref = true; + if next == 0xED { + // WebAssembly DWARF extension + pc.read_u8()?; + let ty = pc.read_uleb128()?; + assert_eq!(ty, 0); + let index = pc.read_sleb128()?; + pc.read_u8()?; // consume 159 + if !code_chunk.is_empty() { + parts.push(CompiledExpressionPart::Code(code_chunk)); + code_chunk = Vec::new(); + } + let label = ValueLabel::from_u32(index as u32); + parts.push(CompiledExpressionPart::Local(label)); + } else { + let pos = pc.offset_from(&expr.0).into_u64() as usize; + let op = Operation::parse(&mut pc, &expr.0, encoding)?; + match op { + Operation::FrameOffset { offset } => { + // Expand DW_OP_fpreg into frame location and DW_OP_plus_uconst. + use gimli::write::Writer; + if frame_base.is_some() { + // Add frame base expressions. + if !code_chunk.is_empty() { + parts.push(CompiledExpressionPart::Code(code_chunk)); + code_chunk = Vec::new(); + } + parts.extend_from_slice(&frame_base.unwrap().parts); + need_deref = frame_base.unwrap().need_deref; + } + // Append DW_OP_plus_uconst part. + let endian = gimli::RunTimeEndian::Little; + let mut writer = write::EndianVec::new(endian); + writer.write_u8(gimli::constants::DW_OP_plus_uconst.0 as u8)?; + writer.write_uleb128(offset as u64)?; + code_chunk.extend(writer.into_vec()); + continue; + } + Operation::Literal { .. } | Operation::PlusConstant { .. } => (), + Operation::StackValue => { + need_deref = false; + } + Operation::Deref { .. } => { + if !code_chunk.is_empty() { + parts.push(CompiledExpressionPart::Code(code_chunk)); + code_chunk = Vec::new(); + } + parts.push(CompiledExpressionPart::Deref); + } + _ => { + return Ok(None); + } + } + let chunk = &buf[pos..pc.offset_from(&expr.0).into_u64() as usize]; + code_chunk.extend_from_slice(chunk); + } + } + + if !code_chunk.is_empty() { + parts.push(CompiledExpressionPart::Code(code_chunk)); + } + + if base_len > 0 && base_len + 1 < parts.len() { + // see if we can glue two code chunks + if let [CompiledExpressionPart::Code(cc1), CompiledExpressionPart::Code(cc2)] = + &parts[base_len..=base_len] + { + let mut combined = cc1.clone(); + combined.extend_from_slice(cc2); + parts[base_len] = CompiledExpressionPart::Code(combined); + parts.remove(base_len + 1); + } + } + + Ok(Some(CompiledExpression { parts, need_deref })) +} + +#[derive(Debug, Clone)] +struct CachedValueLabelRange { + func_index: DefinedFuncIndex, + start: usize, + end: usize, + label_location: HashMap, +} + +struct ValueLabelRangesBuilder<'a, 'b> { + ranges: Vec, + addr_tr: &'a AddressTransform, + frame_info: Option<&'a FunctionFrameInfo<'b>>, + processed_labels: HashSet, +} + +impl<'a, 'b> ValueLabelRangesBuilder<'a, 'b> { + fn new( + scope: &[(u64, u64)], // wasm ranges + addr_tr: &'a AddressTransform, + frame_info: Option<&'a FunctionFrameInfo<'b>>, + ) -> Self { + let mut ranges = Vec::new(); + for s in scope { + if let Some((func_index, tr)) = addr_tr.translate_ranges_raw(s.0, s.1) { + for (start, end) in tr { + ranges.push(CachedValueLabelRange { + func_index, + start, + end, + label_location: HashMap::new(), + }) + } + } + } + ranges.sort_unstable_by(|a, b| a.start.cmp(&b.start)); + ValueLabelRangesBuilder { + ranges, + addr_tr, + frame_info, + processed_labels: HashSet::new(), + } + } + + fn process_label(&mut self, label: ValueLabel) { + if self.processed_labels.contains(&label) { + return; + } + self.processed_labels.insert(label); + + let value_ranges = if let Some(frame_info) = self.frame_info { + &frame_info.value_ranges + } else { + return; + }; + + let ranges = &mut self.ranges; + if let Some(local_ranges) = value_ranges.get(&label) { + for local_range in local_ranges { + let wasm_start = local_range.start; + let wasm_end = local_range.end; + let loc = local_range.loc; + // Find all native ranges for the value label ranges. + for (addr, len) in self + .addr_tr + .translate_ranges(wasm_start as u64, wasm_end as u64) + { + let (range_start, range_end) = self.addr_tr.convert_to_code_range(addr, len); + if range_start == range_end { + continue; + } + assert_lt!(range_start, range_end); + // Find acceptable scope of ranges to intersect with. + let i = match ranges.binary_search_by(|s| s.start.cmp(&range_start)) { + Ok(i) => i, + Err(i) => { + if i > 0 && range_start < ranges[i - 1].end { + i - 1 + } else { + i + } + } + }; + let j = match ranges.binary_search_by(|s| s.start.cmp(&range_end)) { + Ok(i) | Err(i) => i, + }; + // Starting for the end, intersect (range_start..range_end) with + // self.ranges array. + for i in (i..j).rev() { + if range_end <= ranges[i].start || ranges[i].end <= range_start { + continue; + } + if range_end < ranges[i].end { + // Cutting some of the range from the end. + let mut tail = ranges[i].clone(); + ranges[i].end = range_end; + tail.start = range_end; + ranges.insert(i + 1, tail); + } + assert_le!(ranges[i].end, range_end); + if range_start <= ranges[i].start { + ranges[i].label_location.insert(label, loc); + continue; + } + // Cutting some of the range from the start. + let mut tail = ranges[i].clone(); + ranges[i].end = range_start; + tail.start = range_start; + tail.label_location.insert(label, loc); + ranges.insert(i + 1, tail); + } + } + } + } + } + + fn remove_incomplete_ranges(&mut self) { + // Ranges with not-enough labels are discarded. + let processed_labels_len = self.processed_labels.len(); + self.ranges + .retain(|r| r.label_location.len() == processed_labels_len); + } +} diff --git a/crates/debug/src/transform/line_program.rs b/crates/debug/src/transform/line_program.rs new file mode 100644 index 0000000000..f0e715519f --- /dev/null +++ b/crates/debug/src/transform/line_program.rs @@ -0,0 +1,230 @@ +use super::address_transform::AddressTransform; +use super::attr::clone_attr_string; +use super::{Reader, TransformError}; +use anyhow::Error; +use gimli::{ + write, DebugLine, DebugLineOffset, DebugStr, DebuggingInformationEntry, LineEncoding, Unit, +}; +use more_asserts::assert_le; +use std::collections::BTreeMap; +use std::iter::FromIterator; +use wasmtime_environ::entity::EntityRef; + +#[derive(Debug)] +enum SavedLineProgramRow { + Normal { + address: u64, + op_index: u64, + file_index: u64, + line: u64, + column: u64, + discriminator: u64, + is_stmt: bool, + basic_block: bool, + prologue_end: bool, + epilogue_begin: bool, + isa: u64, + }, + EndOfSequence(u64), +} + +#[derive(Debug, Eq, PartialEq)] +enum ReadLineProgramState { + SequenceEnded, + ReadSequence, + IgnoreSequence, +} + +pub(crate) fn clone_line_program( + unit: &Unit, + root: &DebuggingInformationEntry, + addr_tr: &AddressTransform, + out_encoding: gimli::Encoding, + debug_str: &DebugStr, + debug_line: &DebugLine, + out_strings: &mut write::StringTable, +) -> Result<(write::LineProgram, DebugLineOffset, Vec), Error> +where + R: Reader, +{ + let offset = match root.attr_value(gimli::DW_AT_stmt_list)? { + Some(gimli::AttributeValue::DebugLineRef(offset)) => offset, + _ => { + return Err(TransformError("Debug line offset is not found").into()); + } + }; + let comp_dir = root.attr_value(gimli::DW_AT_comp_dir)?; + let comp_name = root.attr_value(gimli::DW_AT_name)?; + let out_comp_dir = clone_attr_string( + comp_dir.as_ref().expect("comp_dir"), + gimli::DW_FORM_strp, + debug_str, + out_strings, + )?; + let out_comp_name = clone_attr_string( + comp_name.as_ref().expect("comp_name"), + gimli::DW_FORM_strp, + debug_str, + out_strings, + )?; + + let program = debug_line.program( + offset, + unit.header.address_size(), + comp_dir.and_then(|val| val.string_value(&debug_str)), + comp_name.and_then(|val| val.string_value(&debug_str)), + ); + if let Ok(program) = program { + let header = program.header(); + assert_le!(header.version(), 4, "not supported 5"); + let line_encoding = LineEncoding { + minimum_instruction_length: header.minimum_instruction_length(), + maximum_operations_per_instruction: header.maximum_operations_per_instruction(), + default_is_stmt: header.default_is_stmt(), + line_base: header.line_base(), + line_range: header.line_range(), + }; + let mut out_program = write::LineProgram::new( + out_encoding, + line_encoding, + out_comp_dir, + out_comp_name, + None, + ); + let mut dirs = Vec::new(); + dirs.push(out_program.default_directory()); + for dir_attr in header.include_directories() { + let dir_id = out_program.add_directory(clone_attr_string( + dir_attr, + gimli::DW_FORM_string, + debug_str, + out_strings, + )?); + dirs.push(dir_id); + } + let mut files = Vec::new(); + for file_entry in header.file_names() { + let dir_id = dirs[file_entry.directory_index() as usize]; + let file_id = out_program.add_file( + clone_attr_string( + &file_entry.path_name(), + gimli::DW_FORM_string, + debug_str, + out_strings, + )?, + dir_id, + None, + ); + files.push(file_id); + } + + let mut rows = program.rows(); + let mut saved_rows = BTreeMap::new(); + let mut state = ReadLineProgramState::SequenceEnded; + while let Some((_header, row)) = rows.next_row()? { + if state == ReadLineProgramState::IgnoreSequence { + if row.end_sequence() { + state = ReadLineProgramState::SequenceEnded; + } + continue; + } + let saved_row = if row.end_sequence() { + state = ReadLineProgramState::SequenceEnded; + SavedLineProgramRow::EndOfSequence(row.address()) + } else { + if state == ReadLineProgramState::SequenceEnded { + // Discard sequences for non-existent code. + if row.address() == 0 { + state = ReadLineProgramState::IgnoreSequence; + continue; + } + state = ReadLineProgramState::ReadSequence; + } + SavedLineProgramRow::Normal { + address: row.address(), + op_index: row.op_index(), + file_index: row.file_index(), + line: row.line().unwrap_or(0), + column: match row.column() { + gimli::ColumnType::LeftEdge => 0, + gimli::ColumnType::Column(val) => val, + }, + discriminator: row.discriminator(), + is_stmt: row.is_stmt(), + basic_block: row.basic_block(), + prologue_end: row.prologue_end(), + epilogue_begin: row.epilogue_begin(), + isa: row.isa(), + } + }; + saved_rows.insert(row.address(), saved_row); + } + + let saved_rows = Vec::from_iter(saved_rows.into_iter()); + for (i, map) in addr_tr.map() { + if map.len == 0 { + continue; // no code generated + } + let symbol = i.index(); + let base_addr = map.offset; + out_program.begin_sequence(Some(write::Address::Symbol { symbol, addend: 0 })); + // TODO track and place function declaration line here + let mut last_address = None; + for addr_map in map.addresses.iter() { + let saved_row = match saved_rows.binary_search_by_key(&addr_map.wasm, |i| i.0) { + Ok(i) => Some(&saved_rows[i].1), + Err(i) => { + if i > 0 { + Some(&saved_rows[i - 1].1) + } else { + None + } + } + }; + if let Some(SavedLineProgramRow::Normal { + address, + op_index, + file_index, + line, + column, + discriminator, + is_stmt, + basic_block, + prologue_end, + epilogue_begin, + isa, + }) = saved_row + { + // Ignore duplicates + if Some(*address) != last_address { + let address_offset = if last_address.is_none() { + // Extend first entry to the function declaration + // TODO use the function declaration line instead + 0 + } else { + (addr_map.generated - base_addr) as u64 + }; + out_program.row().address_offset = address_offset; + out_program.row().op_index = *op_index; + out_program.row().file = files[(file_index - 1) as usize]; + out_program.row().line = *line; + out_program.row().column = *column; + out_program.row().discriminator = *discriminator; + out_program.row().is_statement = *is_stmt; + out_program.row().basic_block = *basic_block; + out_program.row().prologue_end = *prologue_end; + out_program.row().epilogue_begin = *epilogue_begin; + out_program.row().isa = *isa; + out_program.generate_row(); + last_address = Some(*address); + } + } + } + let end_addr = (map.offset + map.len - 1) as u64; + out_program.end_sequence(end_addr); + } + Ok((out_program, offset, files)) + } else { + Err(TransformError("Valid line program not found").into()) + } +} diff --git a/crates/debug/src/transform/mod.rs b/crates/debug/src/transform/mod.rs new file mode 100644 index 0000000000..592fbe2f10 --- /dev/null +++ b/crates/debug/src/transform/mod.rs @@ -0,0 +1,122 @@ +use self::refs::DebugInfoRefsMap; +use self::simulate::generate_simulated_dwarf; +use self::unit::clone_unit; +use crate::gc::build_dependencies; +use crate::DebugInfoData; +use anyhow::Error; +use gimli::{ + write, DebugAddr, DebugAddrBase, DebugLine, DebugStr, LocationLists, RangeLists, + UnitSectionOffset, +}; +use std::collections::HashSet; +use thiserror::Error; +use wasmtime_environ::isa::TargetFrontendConfig; +use wasmtime_environ::{ModuleAddressMap, ModuleVmctxInfo, ValueLabelsRanges}; + +pub use address_transform::AddressTransform; + +mod address_transform; +mod attr; +mod expression; +mod line_program; +mod range_info_builder; +mod refs; +mod simulate; +mod unit; +mod utils; + +pub(crate) trait Reader: gimli::Reader {} + +impl<'input, Endian> Reader for gimli::EndianSlice<'input, Endian> where Endian: gimli::Endianity {} + +#[derive(Error, Debug)] +#[error("Debug info transform error: {0}")] +pub struct TransformError(&'static str); + +pub(crate) struct DebugInputContext<'a, R> +where + R: Reader, +{ + debug_str: &'a DebugStr, + debug_line: &'a DebugLine, + debug_addr: &'a DebugAddr, + debug_addr_base: DebugAddrBase, + rnglists: &'a RangeLists, + loclists: &'a LocationLists, + reachable: &'a HashSet, +} + +pub fn transform_dwarf( + target_config: TargetFrontendConfig, + di: &DebugInfoData, + at: &ModuleAddressMap, + vmctx_info: &ModuleVmctxInfo, + ranges: &ValueLabelsRanges, +) -> Result { + let addr_tr = AddressTransform::new(at, &di.wasm_file); + let reachable = build_dependencies(&di.dwarf, &addr_tr)?.get_reachable(); + + let context = DebugInputContext { + debug_str: &di.dwarf.debug_str, + debug_line: &di.dwarf.debug_line, + debug_addr: &di.dwarf.debug_addr, + debug_addr_base: DebugAddrBase(0), + rnglists: &di.dwarf.ranges, + loclists: &di.dwarf.locations, + reachable: &reachable, + }; + + let out_encoding = gimli::Encoding { + format: gimli::Format::Dwarf32, + // TODO: this should be configurable + // macOS doesn't seem to support DWARF > 3 + version: 3, + address_size: target_config.pointer_bytes(), + }; + + let mut out_strings = write::StringTable::default(); + let mut out_units = write::UnitTable::default(); + + let out_line_strings = write::LineStringTable::default(); + let mut pending_di_refs = Vec::new(); + let mut di_ref_map = DebugInfoRefsMap::new(); + + let mut translated = HashSet::new(); + let mut iter = di.dwarf.debug_info.units(); + while let Some(header) = iter.next().unwrap_or(None) { + let unit = di.dwarf.unit(header)?; + if let Some((id, ref_map, pending_refs)) = clone_unit( + unit, + &context, + &addr_tr, + &ranges, + out_encoding, + &vmctx_info, + &mut out_units, + &mut out_strings, + &mut translated, + )? { + di_ref_map.insert(&header, id, ref_map); + pending_di_refs.push((id, pending_refs)); + } + } + di_ref_map.patch(pending_di_refs.into_iter(), &mut out_units); + + generate_simulated_dwarf( + &addr_tr, + di, + &vmctx_info, + &ranges, + &translated, + out_encoding, + &mut out_units, + &mut out_strings, + )?; + + Ok(write::Dwarf { + units: out_units, + line_programs: vec![], + line_strings: out_line_strings, + strings: out_strings, + }) +} diff --git a/crates/debug/src/transform/range_info_builder.rs b/crates/debug/src/transform/range_info_builder.rs new file mode 100644 index 0000000000..d10f3e8777 --- /dev/null +++ b/crates/debug/src/transform/range_info_builder.rs @@ -0,0 +1,219 @@ +use super::address_transform::AddressTransform; +use super::{DebugInputContext, Reader}; +use anyhow::Error; +use gimli::{write, AttributeValue, DebuggingInformationEntry, RangeListsOffset}; +use more_asserts::assert_lt; +use wasmtime_environ::entity::EntityRef; +use wasmtime_environ::wasm::DefinedFuncIndex; + +pub(crate) enum RangeInfoBuilder { + Undefined, + Position(u64), + Ranges(Vec<(u64, u64)>), + Function(DefinedFuncIndex), +} + +impl RangeInfoBuilder { + pub(crate) fn from( + entry: &DebuggingInformationEntry, + context: &DebugInputContext, + unit_encoding: gimli::Encoding, + cu_low_pc: u64, + ) -> Result + where + R: Reader, + { + if let Some(AttributeValue::RangeListsRef(r)) = entry.attr_value(gimli::DW_AT_ranges)? { + return RangeInfoBuilder::from_ranges_ref(r, context, unit_encoding, cu_low_pc); + }; + + let low_pc = + if let Some(AttributeValue::Addr(addr)) = entry.attr_value(gimli::DW_AT_low_pc)? { + addr + } else { + return Ok(RangeInfoBuilder::Undefined); + }; + + Ok( + if let Some(AttributeValue::Udata(u)) = entry.attr_value(gimli::DW_AT_high_pc)? { + RangeInfoBuilder::Ranges(vec![(low_pc, low_pc + u)]) + } else { + RangeInfoBuilder::Position(low_pc) + }, + ) + } + + pub(crate) fn from_ranges_ref( + ranges: RangeListsOffset, + context: &DebugInputContext, + unit_encoding: gimli::Encoding, + cu_low_pc: u64, + ) -> Result + where + R: Reader, + { + let mut ranges = context.rnglists.ranges( + ranges, + unit_encoding, + cu_low_pc, + &context.debug_addr, + context.debug_addr_base, + )?; + let mut result = Vec::new(); + while let Some(range) = ranges.next()? { + if range.begin >= range.end { + // ignore empty ranges + } + result.push((range.begin, range.end)); + } + + Ok(if result.is_empty() { + RangeInfoBuilder::Undefined + } else { + RangeInfoBuilder::Ranges(result) + }) + } + + pub(crate) fn from_subprogram_die( + entry: &DebuggingInformationEntry, + context: &DebugInputContext, + unit_encoding: gimli::Encoding, + addr_tr: &AddressTransform, + cu_low_pc: u64, + ) -> Result + where + R: Reader, + { + let addr = + if let Some(AttributeValue::Addr(addr)) = entry.attr_value(gimli::DW_AT_low_pc)? { + addr + } else if let Some(AttributeValue::RangeListsRef(r)) = + entry.attr_value(gimli::DW_AT_ranges)? + { + let mut ranges = context.rnglists.ranges( + r, + unit_encoding, + cu_low_pc, + &context.debug_addr, + context.debug_addr_base, + )?; + if let Some(range) = ranges.next()? { + range.begin + } else { + return Ok(RangeInfoBuilder::Undefined); + } + } else { + return Ok(RangeInfoBuilder::Undefined); + }; + + let index = addr_tr.find_func_index(addr); + if index.is_none() { + return Ok(RangeInfoBuilder::Undefined); + } + Ok(RangeInfoBuilder::Function(index.unwrap())) + } + + pub(crate) fn build( + &self, + addr_tr: &AddressTransform, + out_unit: &mut write::Unit, + current_scope_id: write::UnitEntryId, + ) { + match self { + RangeInfoBuilder::Undefined => (), + RangeInfoBuilder::Position(pc) => { + let addr = addr_tr + .translate(*pc) + .unwrap_or(write::Address::Constant(0)); + let current_scope = out_unit.get_mut(current_scope_id); + current_scope.set(gimli::DW_AT_low_pc, write::AttributeValue::Address(addr)); + } + RangeInfoBuilder::Ranges(ranges) => { + let mut result = Vec::new(); + for (begin, end) in ranges { + for tr in addr_tr.translate_ranges(*begin, *end) { + if tr.1 == 0 { + // Ignore empty range + continue; + } + result.push(tr); + } + } + if result.len() != 1 { + let range_list = result + .iter() + .map(|tr| write::Range::StartLength { + begin: tr.0, + length: tr.1, + }) + .collect::>(); + let range_list_id = out_unit.ranges.add(write::RangeList(range_list)); + let current_scope = out_unit.get_mut(current_scope_id); + current_scope.set( + gimli::DW_AT_ranges, + write::AttributeValue::RangeListRef(range_list_id), + ); + } else { + let current_scope = out_unit.get_mut(current_scope_id); + current_scope.set( + gimli::DW_AT_low_pc, + write::AttributeValue::Address(result[0].0), + ); + current_scope.set( + gimli::DW_AT_high_pc, + write::AttributeValue::Udata(result[0].1), + ); + } + } + RangeInfoBuilder::Function(index) => { + let range = addr_tr.func_range(*index); + let symbol = index.index(); + let addr = write::Address::Symbol { + symbol, + addend: range.0 as i64, + }; + let len = (range.1 - range.0) as u64; + let current_scope = out_unit.get_mut(current_scope_id); + current_scope.set(gimli::DW_AT_low_pc, write::AttributeValue::Address(addr)); + current_scope.set(gimli::DW_AT_high_pc, write::AttributeValue::Udata(len)); + } + } + } + + pub(crate) fn get_ranges(&self, addr_tr: &AddressTransform) -> Vec<(u64, u64)> { + match self { + RangeInfoBuilder::Undefined | RangeInfoBuilder::Position(_) => vec![], + RangeInfoBuilder::Ranges(ranges) => ranges.clone(), + RangeInfoBuilder::Function(index) => { + let range = addr_tr.func_source_range(*index); + vec![(range.0, range.1)] + } + } + } + + pub(crate) fn build_ranges( + &self, + addr_tr: &AddressTransform, + out_range_lists: &mut write::RangeListTable, + ) -> write::RangeListId { + if let RangeInfoBuilder::Ranges(ranges) = self { + let mut range_list = Vec::new(); + for (begin, end) in ranges { + assert_lt!(begin, end); + for tr in addr_tr.translate_ranges(*begin, *end) { + if tr.1 == 0 { + // Ignore empty range + continue; + } + range_list.push(write::Range::StartLength { + begin: tr.0, + length: tr.1, + }); + } + } + out_range_lists.add(write::RangeList(range_list)) + } else { + unreachable!(); + } + } +} diff --git a/crates/debug/src/transform/refs.rs b/crates/debug/src/transform/refs.rs new file mode 100644 index 0000000000..1780f06495 --- /dev/null +++ b/crates/debug/src/transform/refs.rs @@ -0,0 +1,111 @@ +//! Helper utils for tracking and patching intra unit or section references. + +use gimli::write; +use gimli::{CompilationUnitHeader, DebugInfoOffset, Reader, UnitOffset}; +use std::collections::HashMap; + +/// Stores compiled unit references: UnitEntryId+DwAt denotes a patch location +/// and UnitOffset is a location in original DWARF. +pub struct PendingUnitRefs { + refs: Vec<(write::UnitEntryId, gimli::DwAt, UnitOffset)>, +} + +impl PendingUnitRefs { + pub fn new() -> Self { + Self { refs: Vec::new() } + } + pub fn insert(&mut self, entry_id: write::UnitEntryId, attr: gimli::DwAt, offset: UnitOffset) { + self.refs.push((entry_id, attr, offset)); + } +} + +/// Stores .debug_info references: UnitEntryId+DwAt denotes a patch location +/// and DebugInfoOffset is a location in original DWARF. +pub struct PendingDebugInfoRefs { + refs: Vec<(write::UnitEntryId, gimli::DwAt, DebugInfoOffset)>, +} + +impl PendingDebugInfoRefs { + pub fn new() -> Self { + Self { refs: Vec::new() } + } + pub fn insert( + &mut self, + entry_id: write::UnitEntryId, + attr: gimli::DwAt, + offset: DebugInfoOffset, + ) { + self.refs.push((entry_id, attr, offset)); + } +} + +/// Stores map between read and written references of DWARF entries of +/// a compiled unit. +pub struct UnitRefsMap { + map: HashMap, +} + +impl UnitRefsMap { + pub fn new() -> Self { + Self { + map: HashMap::new(), + } + } + pub fn insert(&mut self, offset: UnitOffset, entry_id: write::UnitEntryId) { + self.map.insert(offset, entry_id); + } + pub fn patch(&self, refs: PendingUnitRefs, comp_unit: &mut write::Unit) { + for (die_id, attr_name, offset) in refs.refs { + let die = comp_unit.get_mut(die_id); + if let Some(unit_id) = self.map.get(&offset) { + die.set(attr_name, write::AttributeValue::ThisUnitEntryRef(*unit_id)); + } + } + } +} + +/// Stores map between read and written references of DWARF entries of +/// the entire .debug_info. +pub struct DebugInfoRefsMap { + map: HashMap, +} + +impl DebugInfoRefsMap { + pub fn new() -> Self { + Self { + map: HashMap::new(), + } + } + pub fn insert( + &mut self, + unit: &CompilationUnitHeader, + unit_id: write::UnitId, + unit_map: UnitRefsMap, + ) where + R: Reader, + { + self.map + .extend(unit_map.map.into_iter().map(|(off, entry_id)| { + let off = off.to_debug_info_offset(unit); + (off, (unit_id, entry_id)) + })); + } + pub fn patch( + &self, + refs: impl Iterator, + units: &mut write::UnitTable, + ) { + for (id, refs) in refs { + let unit = units.get_mut(id); + for (die_id, attr_name, offset) in refs.refs { + let die = unit.get_mut(die_id); + if let Some((id, entry_id)) = self.map.get(&offset) { + die.set( + attr_name, + write::AttributeValue::AnyUnitEntryRef((*id, *entry_id)), + ); + } + } + } + } +} diff --git a/crates/debug/src/transform/simulate.rs b/crates/debug/src/transform/simulate.rs new file mode 100644 index 0000000000..b139dead99 --- /dev/null +++ b/crates/debug/src/transform/simulate.rs @@ -0,0 +1,373 @@ +use super::expression::{CompiledExpression, FunctionFrameInfo}; +use super::utils::{add_internal_types, append_vmctx_info, get_function_frame_info}; +use super::AddressTransform; +use crate::read_debuginfo::WasmFileInfo; +use anyhow::Error; +use gimli::write; +use gimli::{self, LineEncoding}; +use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; +use wasmtime_environ::entity::EntityRef; +use wasmtime_environ::wasm::get_vmctx_value_label; +use wasmtime_environ::{ModuleVmctxInfo, ValueLabelsRanges}; + +pub use crate::read_debuginfo::{DebugInfoData, FunctionMetadata, WasmType}; + +const PRODUCER_NAME: &str = "wasmtime"; + +fn generate_line_info( + addr_tr: &AddressTransform, + translated: &HashSet, + out_encoding: gimli::Encoding, + w: &WasmFileInfo, + comp_dir_id: write::StringId, + name_id: write::StringId, + name: &str, +) -> Result { + let out_comp_dir = write::LineString::StringRef(comp_dir_id); + let out_comp_name = write::LineString::StringRef(name_id); + + let line_encoding = LineEncoding::default(); + + let mut out_program = write::LineProgram::new( + out_encoding, + line_encoding, + out_comp_dir, + out_comp_name, + None, + ); + + let file_index = out_program.add_file( + write::LineString::String(name.as_bytes().to_vec()), + out_program.default_directory(), + None, + ); + + for (i, map) in addr_tr.map() { + let symbol = i.index(); + if translated.contains(&(symbol as u32)) { + continue; + } + + let base_addr = map.offset; + out_program.begin_sequence(Some(write::Address::Symbol { symbol, addend: 0 })); + for addr_map in map.addresses.iter() { + let address_offset = (addr_map.generated - base_addr) as u64; + out_program.row().address_offset = address_offset; + out_program.row().op_index = 0; + out_program.row().file = file_index; + let wasm_offset = w.code_section_offset + addr_map.wasm as u64; + out_program.row().line = wasm_offset; + out_program.row().column = 0; + out_program.row().discriminator = 1; + out_program.row().is_statement = true; + out_program.row().basic_block = false; + out_program.row().prologue_end = false; + out_program.row().epilogue_begin = false; + out_program.row().isa = 0; + out_program.generate_row(); + } + let end_addr = (map.offset + map.len - 1) as u64; + out_program.end_sequence(end_addr); + } + + Ok(out_program) +} + +fn autogenerate_dwarf_wasm_path(di: &DebugInfoData) -> PathBuf { + let module_name = di + .name_section + .as_ref() + .and_then(|ns| ns.module_name.to_owned()) + .unwrap_or_else(|| unsafe { + static mut GEN_ID: u32 = 0; + GEN_ID += 1; + format!("", GEN_ID) + }); + let path = format!("//{}.wasm", module_name); + PathBuf::from(path) +} + +struct WasmTypesDieRefs { + vmctx: write::UnitEntryId, + i32: write::UnitEntryId, + i64: write::UnitEntryId, + f32: write::UnitEntryId, + f64: write::UnitEntryId, +} + +fn add_wasm_types( + unit: &mut write::Unit, + root_id: write::UnitEntryId, + out_strings: &mut write::StringTable, + vmctx_info: &ModuleVmctxInfo, +) -> WasmTypesDieRefs { + let (_wp_die_id, vmctx_die_id) = add_internal_types(unit, root_id, out_strings, vmctx_info); + + macro_rules! def_type { + ($id:literal, $size:literal, $enc:path) => {{ + let die_id = unit.add(root_id, gimli::DW_TAG_base_type); + let die = unit.get_mut(die_id); + die.set( + gimli::DW_AT_name, + write::AttributeValue::StringRef(out_strings.add($id)), + ); + die.set(gimli::DW_AT_byte_size, write::AttributeValue::Data1($size)); + die.set(gimli::DW_AT_encoding, write::AttributeValue::Encoding($enc)); + die_id + }}; + } + + let i32_die_id = def_type!("i32", 4, gimli::DW_ATE_signed); + let i64_die_id = def_type!("i64", 8, gimli::DW_ATE_signed); + let f32_die_id = def_type!("f32", 4, gimli::DW_ATE_float); + let f64_die_id = def_type!("f64", 8, gimli::DW_ATE_float); + + WasmTypesDieRefs { + vmctx: vmctx_die_id, + i32: i32_die_id, + i64: i64_die_id, + f32: f32_die_id, + f64: f64_die_id, + } +} + +fn resolve_var_type( + index: usize, + wasm_types: &WasmTypesDieRefs, + func_meta: &FunctionMetadata, +) -> Option<(write::UnitEntryId, bool)> { + let (ty, is_param) = if index < func_meta.params.len() { + (func_meta.params[index], true) + } else { + let mut i = (index - func_meta.params.len()) as u32; + let mut j = 0; + while j < func_meta.locals.len() && i >= func_meta.locals[j].0 { + i -= func_meta.locals[j].0; + j += 1; + } + if j >= func_meta.locals.len() { + // Ignore the var index out of bound. + return None; + } + (func_meta.locals[j].1, false) + }; + let type_die_id = match ty { + WasmType::I32 => wasm_types.i32, + WasmType::I64 => wasm_types.i64, + WasmType::F32 => wasm_types.f32, + WasmType::F64 => wasm_types.f64, + _ => { + // Ignore unsupported types. + return None; + } + }; + Some((type_die_id, is_param)) +} + +fn generate_vars( + unit: &mut write::Unit, + die_id: write::UnitEntryId, + addr_tr: &AddressTransform, + frame_info: &FunctionFrameInfo, + scope_ranges: &[(u64, u64)], + wasm_types: &WasmTypesDieRefs, + func_meta: &FunctionMetadata, + locals_names: Option<&HashMap>, + out_strings: &mut write::StringTable, +) { + let vmctx_label = get_vmctx_value_label(); + + // Normalize order of ValueLabelsRanges keys to have reproducable results. + let mut vars = frame_info.value_ranges.keys().collect::>(); + vars.sort_by(|a, b| a.index().cmp(&b.index())); + + for label in vars { + if label.index() == vmctx_label.index() { + append_vmctx_info( + unit, + die_id, + wasm_types.vmctx, + addr_tr, + Some(frame_info), + scope_ranges, + out_strings, + ) + .expect("append_vmctx_info success"); + } else { + let var_index = label.index(); + let (type_die_id, is_param) = + if let Some(result) = resolve_var_type(var_index, wasm_types, func_meta) { + result + } else { + // Skipping if type of local cannot be detected. + continue; + }; + + let loc_list_id = { + let endian = gimli::RunTimeEndian::Little; + + let expr = CompiledExpression::from_label(*label); + let mut locs = Vec::new(); + for (begin, length, data) in + expr.build_with_locals(scope_ranges, addr_tr, Some(frame_info), endian) + { + locs.push(write::Location::StartLength { + begin, + length, + data, + }); + } + unit.locations.add(write::LocationList(locs)) + }; + + let var_id = unit.add( + die_id, + if is_param { + gimli::DW_TAG_formal_parameter + } else { + gimli::DW_TAG_variable + }, + ); + let var = unit.get_mut(var_id); + + let name_id = match locals_names.and_then(|m| m.get(&(var_index as u32))) { + Some(n) => out_strings.add(n.to_owned()), + None => out_strings.add(format!("var{}", var_index)), + }; + + var.set(gimli::DW_AT_name, write::AttributeValue::StringRef(name_id)); + var.set( + gimli::DW_AT_type, + write::AttributeValue::ThisUnitEntryRef(type_die_id), + ); + var.set( + gimli::DW_AT_location, + write::AttributeValue::LocationListRef(loc_list_id), + ); + } + } +} + +pub fn generate_simulated_dwarf( + addr_tr: &AddressTransform, + di: &DebugInfoData, + vmctx_info: &ModuleVmctxInfo, + ranges: &ValueLabelsRanges, + translated: &HashSet, + out_encoding: gimli::Encoding, + out_units: &mut write::UnitTable, + out_strings: &mut write::StringTable, +) -> Result<(), Error> { + let path = di + .wasm_file + .path + .to_owned() + .unwrap_or_else(|| autogenerate_dwarf_wasm_path(di)); + + let (func_names, locals_names) = if let Some(ref name_section) = di.name_section { + ( + Some(&name_section.func_names), + Some(&name_section.locals_names), + ) + } else { + (None, None) + }; + + let (unit, root_id, name_id) = { + let comp_dir_id = out_strings.add(path.parent().expect("path dir").to_str().unwrap()); + let name = path.file_name().expect("path name").to_str().unwrap(); + let name_id = out_strings.add(name); + + let out_program = generate_line_info( + addr_tr, + translated, + out_encoding, + &di.wasm_file, + comp_dir_id, + name_id, + name, + )?; + + let unit_id = out_units.add(write::Unit::new(out_encoding, out_program)); + let unit = out_units.get_mut(unit_id); + + let root_id = unit.root(); + let root = unit.get_mut(root_id); + + let id = out_strings.add(PRODUCER_NAME); + root.set(gimli::DW_AT_producer, write::AttributeValue::StringRef(id)); + root.set(gimli::DW_AT_name, write::AttributeValue::StringRef(name_id)); + root.set( + gimli::DW_AT_stmt_list, + write::AttributeValue::LineProgramRef, + ); + root.set( + gimli::DW_AT_comp_dir, + write::AttributeValue::StringRef(comp_dir_id), + ); + (unit, root_id, name_id) + }; + + let wasm_types = add_wasm_types(unit, root_id, out_strings, vmctx_info); + + for (i, map) in addr_tr.map().iter() { + let index = i.index(); + if translated.contains(&(index as u32)) { + continue; + } + + let start = map.offset as u64; + let end = start + map.len as u64; + let die_id = unit.add(root_id, gimli::DW_TAG_subprogram); + let die = unit.get_mut(die_id); + die.set( + gimli::DW_AT_low_pc, + write::AttributeValue::Address(write::Address::Symbol { + symbol: index, + addend: start as i64, + }), + ); + die.set( + gimli::DW_AT_high_pc, + write::AttributeValue::Udata((end - start) as u64), + ); + + let id = match func_names.and_then(|m| m.get(&(index as u32))) { + Some(n) => out_strings.add(n.to_owned()), + None => out_strings.add(format!("wasm-function[{}]", index)), + }; + + die.set(gimli::DW_AT_name, write::AttributeValue::StringRef(id)); + + die.set( + gimli::DW_AT_decl_file, + write::AttributeValue::StringRef(name_id), + ); + + let f = addr_tr.map().get(i).unwrap(); + let f_start = f.addresses[0].wasm; + let wasm_offset = di.wasm_file.code_section_offset + f_start as u64; + die.set( + gimli::DW_AT_decl_file, + write::AttributeValue::Udata(wasm_offset), + ); + + if let Some(frame_info) = get_function_frame_info(vmctx_info, i, ranges) { + let source_range = addr_tr.func_source_range(i); + generate_vars( + unit, + die_id, + addr_tr, + &frame_info, + &[(source_range.0, source_range.1)], + &wasm_types, + &di.wasm_file.funcs[index], + locals_names.and_then(|m| m.get(&(index as u32))), + out_strings, + ); + } + } + + Ok(()) +} diff --git a/crates/debug/src/transform/unit.rs b/crates/debug/src/transform/unit.rs new file mode 100644 index 0000000000..dbd14f641c --- /dev/null +++ b/crates/debug/src/transform/unit.rs @@ -0,0 +1,362 @@ +use super::address_transform::AddressTransform; +use super::attr::{clone_die_attributes, FileAttributeContext}; +use super::expression::compile_expression; +use super::line_program::clone_line_program; +use super::range_info_builder::RangeInfoBuilder; +use super::refs::{PendingDebugInfoRefs, PendingUnitRefs, UnitRefsMap}; +use super::utils::{add_internal_types, append_vmctx_info, get_function_frame_info}; +use super::{DebugInputContext, Reader, TransformError}; +use anyhow::Error; +use gimli::write; +use gimli::{AttributeValue, DebuggingInformationEntry, Unit}; +use std::collections::HashSet; +use wasmtime_environ::entity::EntityRef; +use wasmtime_environ::{ModuleVmctxInfo, ValueLabelsRanges}; + +struct InheritedAttr { + stack: Vec<(usize, T)>, +} + +impl InheritedAttr { + fn new() -> Self { + InheritedAttr { stack: Vec::new() } + } + + fn update(&mut self, depth: usize) { + while !self.stack.is_empty() && self.stack.last().unwrap().0 >= depth { + self.stack.pop(); + } + } + + fn push(&mut self, depth: usize, value: T) { + self.stack.push((depth, value)); + } + + fn top(&self) -> Option<&T> { + self.stack.last().map(|entry| &entry.1) + } + + fn is_empty(&self) -> bool { + self.stack.is_empty() + } +} + +fn get_base_type_name( + type_entry: &DebuggingInformationEntry, + unit: &Unit, + context: &DebugInputContext, +) -> Result +where + R: Reader, +{ + // FIXME remove recursion. + if let Some(AttributeValue::UnitRef(ref offset)) = type_entry.attr_value(gimli::DW_AT_type)? { + let mut entries = unit.entries_at_offset(*offset)?; + entries.next_entry()?; + if let Some(die) = entries.current() { + if let Some(AttributeValue::DebugStrRef(str_offset)) = + die.attr_value(gimli::DW_AT_name)? + { + return Ok(String::from( + context.debug_str.get_str(str_offset)?.to_string()?, + )); + } + match die.tag() { + gimli::DW_TAG_const_type => { + return Ok(format!("const {}", get_base_type_name(die, unit, context)?)); + } + gimli::DW_TAG_pointer_type => { + return Ok(format!("{}*", get_base_type_name(die, unit, context)?)); + } + gimli::DW_TAG_reference_type => { + return Ok(format!("{}&", get_base_type_name(die, unit, context)?)); + } + gimli::DW_TAG_array_type => { + return Ok(format!("{}[]", get_base_type_name(die, unit, context)?)); + } + _ => (), + } + } + } + Ok(String::from("??")) +} + +fn replace_pointer_type( + parent_id: write::UnitEntryId, + comp_unit: &mut write::Unit, + wp_die_id: write::UnitEntryId, + entry: &DebuggingInformationEntry, + unit: &Unit, + context: &DebugInputContext, + out_strings: &mut write::StringTable, + pending_die_refs: &mut PendingUnitRefs, +) -> Result +where + R: Reader, +{ + let die_id = comp_unit.add(parent_id, gimli::DW_TAG_structure_type); + let die = comp_unit.get_mut(die_id); + + let name = format!( + "WebAssemblyPtrWrapper<{}>", + get_base_type_name(entry, unit, context)? + ); + die.set( + gimli::DW_AT_name, + write::AttributeValue::StringRef(out_strings.add(name.as_str())), + ); + die.set(gimli::DW_AT_byte_size, write::AttributeValue::Data1(4)); + + let p_die_id = comp_unit.add(die_id, gimli::DW_TAG_template_type_parameter); + let p_die = comp_unit.get_mut(p_die_id); + p_die.set( + gimli::DW_AT_name, + write::AttributeValue::StringRef(out_strings.add("T")), + ); + p_die.set( + gimli::DW_AT_type, + write::AttributeValue::ThisUnitEntryRef(wp_die_id), + ); + if let Some(AttributeValue::UnitRef(ref offset)) = entry.attr_value(gimli::DW_AT_type)? { + pending_die_refs.insert(p_die_id, gimli::DW_AT_type, *offset); + } + + let m_die_id = comp_unit.add(die_id, gimli::DW_TAG_member); + let m_die = comp_unit.get_mut(m_die_id); + m_die.set( + gimli::DW_AT_name, + write::AttributeValue::StringRef(out_strings.add("__ptr")), + ); + m_die.set( + gimli::DW_AT_type, + write::AttributeValue::ThisUnitEntryRef(wp_die_id), + ); + m_die.set( + gimli::DW_AT_data_member_location, + write::AttributeValue::Data1(0), + ); + Ok(die_id) +} + +pub(crate) fn clone_unit<'a, R>( + unit: Unit, + context: &DebugInputContext, + addr_tr: &'a AddressTransform, + value_ranges: &'a ValueLabelsRanges, + out_encoding: gimli::Encoding, + module_info: &ModuleVmctxInfo, + out_units: &mut write::UnitTable, + out_strings: &mut write::StringTable, + translated: &mut HashSet, +) -> Result, Error> +where + R: Reader, +{ + let mut die_ref_map = UnitRefsMap::new(); + let mut pending_die_refs = PendingUnitRefs::new(); + let mut pending_di_refs = PendingDebugInfoRefs::new(); + let mut stack = Vec::new(); + + // Iterate over all of this compilation unit's entries. + let mut entries = unit.entries(); + let (mut comp_unit, unit_id, file_map, cu_low_pc, wp_die_id, vmctx_die_id) = + if let Some((depth_delta, entry)) = entries.next_dfs()? { + assert_eq!(depth_delta, 0); + let (out_line_program, debug_line_offset, file_map) = clone_line_program( + &unit, + entry, + addr_tr, + out_encoding, + context.debug_str, + context.debug_line, + out_strings, + )?; + + if entry.tag() == gimli::DW_TAG_compile_unit { + let unit_id = out_units.add(write::Unit::new(out_encoding, out_line_program)); + let comp_unit = out_units.get_mut(unit_id); + + let root_id = comp_unit.root(); + die_ref_map.insert(entry.offset(), root_id); + + let cu_low_pc = if let Some(AttributeValue::Addr(addr)) = + entry.attr_value(gimli::DW_AT_low_pc)? + { + addr + } else { + // FIXME? return Err(TransformError("No low_pc for unit header").into()); + 0 + }; + + clone_die_attributes( + entry, + context, + addr_tr, + None, + unit.encoding(), + comp_unit, + root_id, + None, + None, + cu_low_pc, + out_strings, + &mut pending_die_refs, + &mut pending_di_refs, + FileAttributeContext::Root(Some(debug_line_offset)), + )?; + + let (wp_die_id, vmctx_die_id) = + add_internal_types(comp_unit, root_id, out_strings, module_info); + + stack.push(root_id); + ( + comp_unit, + unit_id, + file_map, + cu_low_pc, + wp_die_id, + vmctx_die_id, + ) + } else { + return Err(TransformError("Unexpected unit header").into()); + } + } else { + return Ok(None); // empty + }; + let mut skip_at_depth = None; + let mut current_frame_base = InheritedAttr::new(); + let mut current_value_range = InheritedAttr::new(); + let mut current_scope_ranges = InheritedAttr::new(); + while let Some((depth_delta, entry)) = entries.next_dfs()? { + let depth_delta = if let Some((depth, cached)) = skip_at_depth { + let new_depth = depth + depth_delta; + if new_depth > 0 { + skip_at_depth = Some((new_depth, cached)); + continue; + } + skip_at_depth = None; + new_depth + cached + } else { + depth_delta + }; + + if !context + .reachable + .contains(&entry.offset().to_unit_section_offset(&unit)) + { + // entry is not reachable: discarding all its info. + skip_at_depth = Some((0, depth_delta)); + continue; + } + + let new_stack_len = stack.len().wrapping_add(depth_delta as usize); + current_frame_base.update(new_stack_len); + current_scope_ranges.update(new_stack_len); + current_value_range.update(new_stack_len); + let range_builder = if entry.tag() == gimli::DW_TAG_subprogram { + let range_builder = RangeInfoBuilder::from_subprogram_die( + entry, + context, + unit.encoding(), + addr_tr, + cu_low_pc, + )?; + if let RangeInfoBuilder::Function(func_index) = range_builder { + if let Some(frame_info) = + get_function_frame_info(module_info, func_index, value_ranges) + { + current_value_range.push(new_stack_len, frame_info); + } + translated.insert(func_index.index() as u32); + current_scope_ranges.push(new_stack_len, range_builder.get_ranges(addr_tr)); + Some(range_builder) + } else { + // FIXME current_scope_ranges.push() + None + } + } else { + let high_pc = entry.attr_value(gimli::DW_AT_high_pc)?; + let ranges = entry.attr_value(gimli::DW_AT_ranges)?; + if high_pc.is_some() || ranges.is_some() { + let range_builder = + RangeInfoBuilder::from(entry, context, unit.encoding(), cu_low_pc)?; + current_scope_ranges.push(new_stack_len, range_builder.get_ranges(addr_tr)); + Some(range_builder) + } else { + None + } + }; + + if depth_delta <= 0 { + for _ in depth_delta..1 { + stack.pop(); + } + } else { + assert_eq!(depth_delta, 1); + } + + if let Some(AttributeValue::Exprloc(expr)) = entry.attr_value(gimli::DW_AT_frame_base)? { + if let Some(expr) = compile_expression(&expr, unit.encoding(), None)? { + current_frame_base.push(new_stack_len, expr); + } + } + + let parent = stack.last().unwrap(); + + if entry.tag() == gimli::DW_TAG_pointer_type { + // Wrap pointer types. + // TODO reference types? + let die_id = replace_pointer_type( + *parent, + comp_unit, + wp_die_id, + entry, + &unit, + context, + out_strings, + &mut pending_die_refs, + )?; + stack.push(die_id); + assert_eq!(stack.len(), new_stack_len); + die_ref_map.insert(entry.offset(), die_id); + continue; + } + + let die_id = comp_unit.add(*parent, entry.tag()); + + stack.push(die_id); + assert_eq!(stack.len(), new_stack_len); + die_ref_map.insert(entry.offset(), die_id); + + clone_die_attributes( + entry, + context, + addr_tr, + current_value_range.top(), + unit.encoding(), + &mut comp_unit, + die_id, + range_builder, + current_scope_ranges.top(), + cu_low_pc, + out_strings, + &mut pending_die_refs, + &mut pending_di_refs, + FileAttributeContext::Children(&file_map, current_frame_base.top()), + )?; + + if entry.tag() == gimli::DW_TAG_subprogram && !current_scope_ranges.is_empty() { + append_vmctx_info( + comp_unit, + die_id, + vmctx_die_id, + addr_tr, + current_value_range.top(), + current_scope_ranges.top().expect("range"), + out_strings, + )?; + } + } + die_ref_map.patch(pending_die_refs, comp_unit); + Ok(Some((unit_id, die_ref_map, pending_di_refs))) +} diff --git a/crates/debug/src/transform/utils.rs b/crates/debug/src/transform/utils.rs new file mode 100644 index 0000000000..2b921581cb --- /dev/null +++ b/crates/debug/src/transform/utils.rs @@ -0,0 +1,165 @@ +use super::address_transform::AddressTransform; +use super::expression::{CompiledExpression, FunctionFrameInfo}; +use anyhow::Error; +use gimli::write; +use wasmtime_environ::wasm::DefinedFuncIndex; +use wasmtime_environ::{ModuleMemoryOffset, ModuleVmctxInfo, ValueLabelsRanges}; + +pub(crate) fn add_internal_types( + comp_unit: &mut write::Unit, + root_id: write::UnitEntryId, + out_strings: &mut write::StringTable, + module_info: &ModuleVmctxInfo, +) -> (write::UnitEntryId, write::UnitEntryId) { + let wp_die_id = comp_unit.add(root_id, gimli::DW_TAG_base_type); + let wp_die = comp_unit.get_mut(wp_die_id); + wp_die.set( + gimli::DW_AT_name, + write::AttributeValue::StringRef(out_strings.add("WebAssemblyPtr")), + ); + wp_die.set(gimli::DW_AT_byte_size, write::AttributeValue::Data1(4)); + wp_die.set( + gimli::DW_AT_encoding, + write::AttributeValue::Encoding(gimli::DW_ATE_unsigned), + ); + + let memory_byte_die_id = comp_unit.add(root_id, gimli::DW_TAG_base_type); + let memory_byte_die = comp_unit.get_mut(memory_byte_die_id); + memory_byte_die.set( + gimli::DW_AT_name, + write::AttributeValue::StringRef(out_strings.add("u8")), + ); + memory_byte_die.set( + gimli::DW_AT_encoding, + write::AttributeValue::Encoding(gimli::DW_ATE_unsigned), + ); + memory_byte_die.set(gimli::DW_AT_byte_size, write::AttributeValue::Data1(1)); + + let memory_bytes_die_id = comp_unit.add(root_id, gimli::DW_TAG_pointer_type); + let memory_bytes_die = comp_unit.get_mut(memory_bytes_die_id); + memory_bytes_die.set( + gimli::DW_AT_name, + write::AttributeValue::StringRef(out_strings.add("u8*")), + ); + memory_bytes_die.set( + gimli::DW_AT_type, + write::AttributeValue::ThisUnitEntryRef(memory_byte_die_id), + ); + + // Create artificial VMContext type and its reference for convinience viewing + // its fields (such as memory ref) in a debugger. + let vmctx_die_id = comp_unit.add(root_id, gimli::DW_TAG_structure_type); + let vmctx_die = comp_unit.get_mut(vmctx_die_id); + vmctx_die.set( + gimli::DW_AT_name, + write::AttributeValue::StringRef(out_strings.add("WasmtimeVMContext")), + ); + + match module_info.memory_offset { + ModuleMemoryOffset::Defined(memory_offset) => { + // The context has defined memory: extend the WasmtimeVMContext size + // past the "memory" field. + const MEMORY_FIELD_SIZE_PLUS_PADDING: u32 = 8; + vmctx_die.set( + gimli::DW_AT_byte_size, + write::AttributeValue::Data4(memory_offset + MEMORY_FIELD_SIZE_PLUS_PADDING), + ); + + // Define the "memory" field which is a direct pointer to allocated Wasm memory. + let m_die_id = comp_unit.add(vmctx_die_id, gimli::DW_TAG_member); + let m_die = comp_unit.get_mut(m_die_id); + m_die.set( + gimli::DW_AT_name, + write::AttributeValue::StringRef(out_strings.add("memory")), + ); + m_die.set( + gimli::DW_AT_type, + write::AttributeValue::ThisUnitEntryRef(memory_bytes_die_id), + ); + m_die.set( + gimli::DW_AT_data_member_location, + write::AttributeValue::Udata(memory_offset as u64), + ); + } + ModuleMemoryOffset::Imported(_) => { + // TODO implement convinience pointer to and additional types for VMMemoryImport. + } + ModuleMemoryOffset::None => (), + } + + let vmctx_ptr_die_id = comp_unit.add(root_id, gimli::DW_TAG_pointer_type); + let vmctx_ptr_die = comp_unit.get_mut(vmctx_ptr_die_id); + vmctx_ptr_die.set( + gimli::DW_AT_name, + write::AttributeValue::StringRef(out_strings.add("WasmtimeVMContext*")), + ); + vmctx_ptr_die.set( + gimli::DW_AT_type, + write::AttributeValue::ThisUnitEntryRef(vmctx_die_id), + ); + + (wp_die_id, vmctx_ptr_die_id) +} + +pub(crate) fn append_vmctx_info( + comp_unit: &mut write::Unit, + parent_id: write::UnitEntryId, + vmctx_die_id: write::UnitEntryId, + addr_tr: &AddressTransform, + frame_info: Option<&FunctionFrameInfo>, + scope_ranges: &[(u64, u64)], + out_strings: &mut write::StringTable, +) -> Result<(), Error> { + let loc = { + let endian = gimli::RunTimeEndian::Little; + + let expr = CompiledExpression::vmctx(); + let mut locs = Vec::new(); + for (begin, length, data) in + expr.build_with_locals(scope_ranges, addr_tr, frame_info, endian) + { + locs.push(write::Location::StartLength { + begin, + length, + data, + }); + } + let list_id = comp_unit.locations.add(write::LocationList(locs)); + write::AttributeValue::LocationListRef(list_id) + }; + + let var_die_id = comp_unit.add(parent_id, gimli::DW_TAG_variable); + let var_die = comp_unit.get_mut(var_die_id); + var_die.set( + gimli::DW_AT_name, + write::AttributeValue::StringRef(out_strings.add("__vmctx")), + ); + var_die.set( + gimli::DW_AT_type, + write::AttributeValue::ThisUnitEntryRef(vmctx_die_id), + ); + var_die.set(gimli::DW_AT_location, loc); + + Ok(()) +} + +pub(crate) fn get_function_frame_info<'a, 'b, 'c>( + module_info: &'b ModuleVmctxInfo, + func_index: DefinedFuncIndex, + value_ranges: &'c ValueLabelsRanges, +) -> Option> +where + 'b: 'a, + 'c: 'a, +{ + if let Some(value_ranges) = value_ranges.get(func_index) { + let frame_info = FunctionFrameInfo { + value_ranges, + memory_offset: module_info.memory_offset.clone(), + stack_slots: &module_info.stack_slots[func_index], + }; + Some(frame_info) + } else { + None + } +} diff --git a/crates/debug/src/write_debuginfo.rs b/crates/debug/src/write_debuginfo.rs new file mode 100644 index 0000000000..9a62460b4c --- /dev/null +++ b/crates/debug/src/write_debuginfo.rs @@ -0,0 +1,143 @@ +use faerie::artifact::{Decl, SectionKind}; +use faerie::*; +use gimli::write::{Address, Dwarf, EndianVec, Result, Sections, Writer}; +use gimli::{RunTimeEndian, SectionId}; + +#[derive(Clone)] +struct DebugReloc { + offset: u32, + size: u8, + name: String, + addend: i64, +} + +pub enum ResolvedSymbol { + PhysicalAddress(u64), + Reloc { name: String, addend: i64 }, +} + +pub trait SymbolResolver { + fn resolve_symbol(&self, symbol: usize, addend: i64) -> ResolvedSymbol; +} + +pub fn emit_dwarf( + artifact: &mut Artifact, + mut dwarf: Dwarf, + symbol_resolver: &dyn SymbolResolver, +) -> anyhow::Result<()> { + let endian = RunTimeEndian::Little; + + let mut sections = Sections::new(WriterRelocate::new(endian, symbol_resolver)); + dwarf.write(&mut sections)?; + sections.for_each_mut(|id, s| -> anyhow::Result<()> { + artifact.declare_with( + id.name(), + Decl::section(SectionKind::Debug), + s.writer.take(), + ) + })?; + sections.for_each_mut(|id, s| -> anyhow::Result<()> { + for reloc in &s.relocs { + artifact.link_with( + faerie::Link { + from: id.name(), + to: &reloc.name, + at: u64::from(reloc.offset), + }, + faerie::Reloc::Debug { + size: reloc.size, + addend: reloc.addend as i32, + }, + )?; + } + Ok(()) + })?; + Ok(()) +} + +#[derive(Clone)] +pub struct WriterRelocate<'a> { + relocs: Vec, + writer: EndianVec, + symbol_resolver: &'a dyn SymbolResolver, +} + +impl<'a> WriterRelocate<'a> { + pub fn new(endian: RunTimeEndian, symbol_resolver: &'a dyn SymbolResolver) -> Self { + WriterRelocate { + relocs: Vec::new(), + writer: EndianVec::new(endian), + symbol_resolver, + } + } +} + +impl<'a> Writer for WriterRelocate<'a> { + type Endian = RunTimeEndian; + + fn endian(&self) -> Self::Endian { + self.writer.endian() + } + + fn len(&self) -> usize { + self.writer.len() + } + + fn write(&mut self, bytes: &[u8]) -> Result<()> { + self.writer.write(bytes) + } + + fn write_at(&mut self, offset: usize, bytes: &[u8]) -> Result<()> { + self.writer.write_at(offset, bytes) + } + + fn write_address(&mut self, address: Address, size: u8) -> Result<()> { + match address { + Address::Constant(val) => self.write_udata(val, size), + Address::Symbol { symbol, addend } => { + match self.symbol_resolver.resolve_symbol(symbol, addend as i64) { + ResolvedSymbol::PhysicalAddress(addr) => self.write_udata(addr, size), + ResolvedSymbol::Reloc { name, addend } => { + let offset = self.len() as u64; + self.relocs.push(DebugReloc { + offset: offset as u32, + size, + name, + addend, + }); + self.write_udata(addend as u64, size) + } + } + } + } + } + + fn write_offset(&mut self, val: usize, section: SectionId, size: u8) -> Result<()> { + let offset = self.len() as u32; + let name = section.name().to_string(); + self.relocs.push(DebugReloc { + offset, + size, + name, + addend: val as i64, + }); + self.write_udata(val as u64, size) + } + + fn write_offset_at( + &mut self, + offset: usize, + val: usize, + section: SectionId, + size: u8, + ) -> Result<()> { + let name = section.name().to_string(); + self.relocs.push(DebugReloc { + offset: offset as u32, + size, + name, + addend: val as i64, + }); + self.write_udata_at(offset, val as u64, size) + } +} diff --git a/crates/environ/.gitignore b/crates/environ/.gitignore new file mode 100644 index 0000000000..4308d82204 --- /dev/null +++ b/crates/environ/.gitignore @@ -0,0 +1,3 @@ +target/ +**/*.rs.bk +Cargo.lock diff --git a/crates/environ/Cargo.toml b/crates/environ/Cargo.toml new file mode 100644 index 0000000000..66b2c38470 --- /dev/null +++ b/crates/environ/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "wasmtime-environ" +version = "0.12.0" +authors = ["The Wasmtime Project Developers"] +description = "Standalone environment support for WebAsssembly code in Cranelift" +license = "Apache-2.0 WITH LLVM-exception" +repository = "https://github.com/bytecodealliance/wasmtime" +documentation = "https://docs.rs/wasmtime-environ/" +categories = ["wasm"] +keywords = ["webassembly", "wasm"] +readme = "README.md" +edition = "2018" + +[dependencies] +anyhow = "1.0" +cranelift-codegen = { version = "0.59.0", features = ["enable-serde"] } +cranelift-entity = { version = "0.59.0", features = ["enable-serde"] } +cranelift-wasm = { version = "0.59.0", features = ["enable-serde"] } +wasmparser = "0.51.2" +lightbeam = { path = "../lightbeam", optional = true, version = "0.12.0" } +indexmap = "1.0.2" +rayon = "1.2.1" +thiserror = "1.0.4" +directories = "2.0.1" +sha2 = "0.8.0" +base64 = "0.11.0" +serde = { version = "1.0.94", features = ["derive"] } +bincode = "1.1.4" +log = { version = "0.4.8", default-features = false } +zstd = "0.5" +toml = "0.5.5" +file-per-thread-logger = "0.1.1" +more-asserts = "0.2.1" + +[target.'cfg(target_os = "windows")'.dependencies] +winapi = "0.3.7" + +[target.'cfg(not(target_os = "windows"))'.dependencies] +libc = "0.2.60" +errno = "0.2.4" + +[dev-dependencies] +tempfile = "3" +target-lexicon = { version = "0.10.0", default-features = false } +pretty_env_logger = "0.3.0" +rand = { version = "0.7.0", default-features = false, features = ["small_rng"] } +cranelift-codegen = { version = "0.59.0", features = ["enable-serde", "all-arch"] } +filetime = "0.2.7" +lazy_static = "1.3.0" + +[badges] +maintenance = { status = "actively-developed" } diff --git a/crates/environ/LICENSE b/crates/environ/LICENSE new file mode 100644 index 0000000000..f9d81955f4 --- /dev/null +++ b/crates/environ/LICENSE @@ -0,0 +1,220 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + diff --git a/crates/environ/README.md b/crates/environ/README.md new file mode 100644 index 0000000000..0649c5f877 --- /dev/null +++ b/crates/environ/README.md @@ -0,0 +1,6 @@ +This is the `wasmtime-environ` crate, which contains the implementations +of the `ModuleEnvironment` and `FuncEnvironment` traits from +[`cranelift-wasm`](https://crates.io/crates/cranelift-wasm). They effectively +implement an ABI for basic wasm compilation that defines how linear memories +are allocated, how indirect calls work, and other details. They can be used +for JITing, native object files, or other purposes. diff --git a/crates/environ/build.rs b/crates/environ/build.rs new file mode 100644 index 0000000000..1db75f1c11 --- /dev/null +++ b/crates/environ/build.rs @@ -0,0 +1,10 @@ +use std::process::Command; +use std::str; + +fn main() { + let git_rev = match Command::new("git").args(&["rev-parse", "HEAD"]).output() { + Ok(output) => str::from_utf8(&output.stdout).unwrap().trim().to_string(), + Err(_) => env!("CARGO_PKG_VERSION").to_string(), + }; + println!("cargo:rustc-env=GIT_REV={}", git_rev); +} diff --git a/crates/environ/src/address_map.rs b/crates/environ/src/address_map.rs new file mode 100644 index 0000000000..461f550eb2 --- /dev/null +++ b/crates/environ/src/address_map.rs @@ -0,0 +1,70 @@ +//! Data structures to provide transformation of the source +// addresses of a WebAssembly module into the native code. + +use cranelift_codegen::ir; +use cranelift_entity::PrimaryMap; +use cranelift_wasm::DefinedFuncIndex; +use serde::{Deserialize, Serialize}; + +/// Single source location to generated address mapping. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct InstructionAddressMap { + /// Original source location. + pub srcloc: ir::SourceLoc, + + /// Generated instructions offset. + pub code_offset: usize, + + /// Generated instructions length. + pub code_len: usize, +} + +/// Function and its instructions addresses mappings. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct FunctionAddressMap { + /// Instructions maps. + /// The array is sorted by the InstructionAddressMap::code_offset field. + pub instructions: Vec, + + /// Function start source location (normally declaration). + pub start_srcloc: ir::SourceLoc, + + /// Function end source location. + pub end_srcloc: ir::SourceLoc, + + /// Generated function body offset if applicable, otherwise 0. + pub body_offset: usize, + + /// Generated function body length. + pub body_len: usize, +} + +/// Module functions addresses mappings. +pub type ModuleAddressMap = PrimaryMap; + +/// Value ranges for functions. +pub type ValueLabelsRanges = PrimaryMap; + +/// Stack slots for functions. +pub type StackSlots = PrimaryMap; + +/// Memory definition offset in the VMContext structure. +#[derive(Debug, Clone)] +pub enum ModuleMemoryOffset { + /// Not available. + None, + /// Offset to the defined memory. + Defined(u32), + /// Offset to the imported memory. + Imported(u32), +} + +/// Module `vmctx` related info. +#[derive(Debug, Clone)] +pub struct ModuleVmctxInfo { + /// The memory definition offset in the VMContext structure. + pub memory_offset: ModuleMemoryOffset, + + /// The functions stack slots. + pub stack_slots: StackSlots, +} diff --git a/crates/environ/src/cache.rs b/crates/environ/src/cache.rs new file mode 100644 index 0000000000..290a54033f --- /dev/null +++ b/crates/environ/src/cache.rs @@ -0,0 +1,256 @@ +use crate::address_map::{ModuleAddressMap, ValueLabelsRanges}; +use crate::compilation::{Compilation, Relocations, Traps}; +use cranelift_codegen::ir; +use cranelift_entity::PrimaryMap; +use cranelift_wasm::DefinedFuncIndex; +use log::{debug, trace, warn}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::fs; +use std::hash::Hash; +use std::hash::Hasher; +use std::io::Write; +use std::path::{Path, PathBuf}; + +#[macro_use] // for tests +mod config; +mod worker; + +pub use config::{create_new_config, CacheConfig}; +use worker::Worker; + +pub struct ModuleCacheEntry<'config>(Option>); + +struct ModuleCacheEntryInner<'config> { + root_path: PathBuf, + cache_config: &'config CacheConfig, +} + +/// Cached compilation data of a Wasm module. +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +pub struct ModuleCacheData { + compilation: Compilation, + relocations: Relocations, + address_transforms: ModuleAddressMap, + value_ranges: ValueLabelsRanges, + stack_slots: PrimaryMap, + traps: Traps, +} + +/// A type alias over the module cache data as a tuple. +pub type ModuleCacheDataTupleType = ( + Compilation, + Relocations, + ModuleAddressMap, + ValueLabelsRanges, + PrimaryMap, + Traps, +); + +struct Sha256Hasher(Sha256); + +impl<'config> ModuleCacheEntry<'config> { + pub fn new<'data>(compiler_name: &str, cache_config: &'config CacheConfig) -> Self { + if cache_config.enabled() { + Self(Some(ModuleCacheEntryInner::new( + compiler_name, + cache_config, + ))) + } else { + Self(None) + } + } + + #[cfg(test)] + fn from_inner(inner: ModuleCacheEntryInner<'config>) -> Self { + Self(Some(inner)) + } + + pub fn get_data( + &self, + state: T, + compute: fn(T) -> Result, + ) -> Result { + let mut hasher = Sha256Hasher(Sha256::new()); + state.hash(&mut hasher); + let hash: [u8; 32] = hasher.0.result().into(); + // standard encoding uses '/' which can't be used for filename + let hash = base64::encode_config(&hash, base64::URL_SAFE_NO_PAD); + + let inner = match &self.0 { + Some(inner) => inner, + None => return compute(state).map(ModuleCacheData::from_tuple), + }; + + if let Some(cached_val) = inner.get_data(&hash) { + let mod_cache_path = inner.root_path.join(&hash); + inner.cache_config.on_cache_get_async(&mod_cache_path); // call on success + return Ok(cached_val); + } + let val_to_cache = ModuleCacheData::from_tuple(compute(state)?); + if inner.update_data(&hash, &val_to_cache).is_some() { + let mod_cache_path = inner.root_path.join(&hash); + inner.cache_config.on_cache_update_async(&mod_cache_path); // call on success + } + Ok(val_to_cache) + } +} + +impl<'config> ModuleCacheEntryInner<'config> { + fn new<'data>(compiler_name: &str, cache_config: &'config CacheConfig) -> Self { + // If debug assertions are enabled then assume that we're some sort of + // local build. We don't want local builds to stomp over caches between + // builds, so just use a separate cache directory based on the mtime of + // our executable, which should roughly correlate with "you changed the + // source code so you get a different directory". + // + // Otherwise if this is a release build we use the `GIT_REV` env var + // which is either the git rev if installed from git or the crate + // version if installed from crates.io. + let compiler_dir = if cfg!(debug_assertions) { + fn self_mtime() -> Option { + let path = std::env::current_exe().ok()?; + let metadata = path.metadata().ok()?; + let mtime = metadata.modified().ok()?; + Some(match mtime.duration_since(std::time::UNIX_EPOCH) { + Ok(dur) => format!("{}", dur.as_millis()), + Err(err) => format!("m{}", err.duration().as_millis()), + }) + } + let self_mtime = self_mtime().unwrap_or("no-mtime".to_string()); + format!( + "{comp_name}-{comp_ver}-{comp_mtime}", + comp_name = compiler_name, + comp_ver = env!("GIT_REV"), + comp_mtime = self_mtime, + ) + } else { + format!( + "{comp_name}-{comp_ver}", + comp_name = compiler_name, + comp_ver = env!("GIT_REV"), + ) + }; + let root_path = cache_config.directory().join("modules").join(compiler_dir); + + Self { + root_path, + cache_config, + } + } + + fn get_data(&self, hash: &str) -> Option { + let mod_cache_path = self.root_path.join(hash); + trace!("get_data() for path: {}", mod_cache_path.display()); + let compressed_cache_bytes = fs::read(&mod_cache_path).ok()?; + let cache_bytes = zstd::decode_all(&compressed_cache_bytes[..]) + .map_err(|err| warn!("Failed to decompress cached code: {}", err)) + .ok()?; + bincode::deserialize(&cache_bytes[..]) + .map_err(|err| warn!("Failed to deserialize cached code: {}", err)) + .ok() + } + + fn update_data(&self, hash: &str, data: &ModuleCacheData) -> Option<()> { + let mod_cache_path = self.root_path.join(hash); + trace!("update_data() for path: {}", mod_cache_path.display()); + let serialized_data = bincode::serialize(&data) + .map_err(|err| warn!("Failed to serialize cached code: {}", err)) + .ok()?; + let compressed_data = zstd::encode_all( + &serialized_data[..], + self.cache_config.baseline_compression_level(), + ) + .map_err(|err| warn!("Failed to compress cached code: {}", err)) + .ok()?; + + // Optimize syscalls: first, try writing to disk. It should succeed in most cases. + // Otherwise, try creating the cache directory and retry writing to the file. + if fs_write_atomic(&mod_cache_path, "mod", &compressed_data) { + return Some(()); + } + + debug!( + "Attempting to create the cache directory, because \ + failed to write cached code to disk, path: {}", + mod_cache_path.display(), + ); + + let cache_dir = mod_cache_path.parent().unwrap(); + fs::create_dir_all(cache_dir) + .map_err(|err| { + warn!( + "Failed to create cache directory, path: {}, message: {}", + cache_dir.display(), + err + ) + }) + .ok()?; + + if fs_write_atomic(&mod_cache_path, "mod", &compressed_data) { + Some(()) + } else { + None + } + } +} + +impl ModuleCacheData { + pub fn from_tuple(data: ModuleCacheDataTupleType) -> Self { + Self { + compilation: data.0, + relocations: data.1, + address_transforms: data.2, + value_ranges: data.3, + stack_slots: data.4, + traps: data.5, + } + } + + pub fn into_tuple(self) -> ModuleCacheDataTupleType { + ( + self.compilation, + self.relocations, + self.address_transforms, + self.value_ranges, + self.stack_slots, + self.traps, + ) + } +} + +impl Hasher for Sha256Hasher { + fn finish(&self) -> u64 { + panic!("Sha256Hasher doesn't support finish!"); + } + + fn write(&mut self, bytes: &[u8]) { + self.0.input(bytes); + } +} + +// Assumption: path inside cache directory. +// Then, we don't have to use sound OS-specific exclusive file access. +// Note: there's no need to remove temporary file here - cleanup task will do it later. +fn fs_write_atomic(path: &Path, reason: &str, contents: &[u8]) -> bool { + let lock_path = path.with_extension(format!("wip-atomic-write-{}", reason)); + fs::OpenOptions::new() + .create_new(true) // atomic file creation (assumption: no one will open it without this flag) + .write(true) + .open(&lock_path) + .and_then(|mut file| file.write_all(contents)) + // file should go out of scope and be closed at this point + .and_then(|()| fs::rename(&lock_path, &path)) // atomic file rename + .map_err(|err| { + warn!( + "Failed to write file with rename, lock path: {}, target path: {}, err: {}", + lock_path.display(), + path.display(), + err + ) + }) + .is_ok() +} + +#[cfg(test)] +mod tests; diff --git a/crates/environ/src/cache/config.rs b/crates/environ/src/cache/config.rs new file mode 100644 index 0000000000..67fd00f6d8 --- /dev/null +++ b/crates/environ/src/cache/config.rs @@ -0,0 +1,586 @@ +//! Module for configuring the cache system. + +use super::Worker; +use anyhow::{anyhow, bail, Context, Result}; +use directories::ProjectDirs; +use log::{trace, warn}; +use serde::{ + de::{self, Deserializer}, + Deserialize, +}; +use std::fmt::Debug; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; +use std::sync::Arc; +use std::time::Duration; + +// wrapped, so we have named section in config, +// also, for possible future compatibility +#[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] +struct Config { + cache: CacheConfig, +} + +/// Global configuration for how the cache is managed +#[derive(Deserialize, Debug, Clone)] +#[serde(deny_unknown_fields)] +pub struct CacheConfig { + enabled: bool, + directory: Option, + #[serde( + default, + rename = "worker-event-queue-size", + deserialize_with = "deserialize_si_prefix" + )] + worker_event_queue_size: Option, + #[serde(rename = "baseline-compression-level")] + baseline_compression_level: Option, + #[serde(rename = "optimized-compression-level")] + optimized_compression_level: Option, + #[serde( + default, + rename = "optimized-compression-usage-counter-threshold", + deserialize_with = "deserialize_si_prefix" + )] + optimized_compression_usage_counter_threshold: Option, + #[serde( + default, + rename = "cleanup-interval", + deserialize_with = "deserialize_duration" + )] + cleanup_interval: Option, + #[serde( + default, + rename = "optimizing-compression-task-timeout", + deserialize_with = "deserialize_duration" + )] + optimizing_compression_task_timeout: Option, + #[serde( + default, + rename = "allowed-clock-drift-for-files-from-future", + deserialize_with = "deserialize_duration" + )] + allowed_clock_drift_for_files_from_future: Option, + #[serde( + default, + rename = "file-count-soft-limit", + deserialize_with = "deserialize_si_prefix" + )] + file_count_soft_limit: Option, + #[serde( + default, + rename = "files-total-size-soft-limit", + deserialize_with = "deserialize_disk_space" + )] + files_total_size_soft_limit: Option, + #[serde( + default, + rename = "file-count-limit-percent-if-deleting", + deserialize_with = "deserialize_percent" + )] + file_count_limit_percent_if_deleting: Option, + #[serde( + default, + rename = "files-total-size-limit-percent-if-deleting", + deserialize_with = "deserialize_percent" + )] + files_total_size_limit_percent_if_deleting: Option, + + #[serde(skip)] + worker: Option, + #[serde(skip)] + state: Arc, +} + +#[derive(Default, Debug)] +struct CacheState { + hits: AtomicUsize, + misses: AtomicUsize, +} + +/// Creates a new configuration file at specified path, or default path if None is passed. +/// Fails if file already exists. +pub fn create_new_config + Debug>(config_file: Option

) -> Result { + trace!("Creating new config file, path: {:?}", config_file); + + let config_file = match config_file { + Some(path) => path.as_ref().to_path_buf(), + None => default_config_path()?, + }; + + if config_file.exists() { + bail!( + "Configuration file '{}' already exists.", + config_file.display() + ); + } + + let parent_dir = config_file + .parent() + .ok_or_else(|| anyhow!("Invalid cache config path: {}", config_file.display()))?; + + fs::create_dir_all(parent_dir).with_context(|| { + format!( + "Failed to create config directory, config path: {}", + config_file.display(), + ) + })?; + + let content = "\ +# Comment out certain settings to use default values. +# For more settings, please refer to the documentation: +# https://bytecodealliance.github.io/wasmtime/cli-cache.html + +[cache] +enabled = true +"; + + fs::write(&config_file, &content).with_context(|| { + format!( + "Failed to flush config to the disk, path: {}", + config_file.display(), + ) + })?; + + Ok(config_file.to_path_buf()) +} + +// permitted levels from: https://docs.rs/zstd/0.4.28+zstd.1.4.3/zstd/stream/write/struct.Encoder.html +const ZSTD_COMPRESSION_LEVELS: std::ops::RangeInclusive = 0..=21; + +// Default settings, you're welcome to tune them! +// TODO: what do we want to warn users about? + +// At the moment of writing, the modules couldn't depend on anothers, +// so we have at most one module per wasmtime instance +// if changed, update cli-cache.md +const DEFAULT_WORKER_EVENT_QUEUE_SIZE: u64 = 0x10; +const WORKER_EVENT_QUEUE_SIZE_WARNING_TRESHOLD: u64 = 3; +// should be quick and provide good enough compression +// if changed, update cli-cache.md +const DEFAULT_BASELINE_COMPRESSION_LEVEL: i32 = zstd::DEFAULT_COMPRESSION_LEVEL; +// should provide significantly better compression than baseline +// if changed, update cli-cache.md +const DEFAULT_OPTIMIZED_COMPRESSION_LEVEL: i32 = 20; +// shouldn't be to low to avoid recompressing too many files +// if changed, update cli-cache.md +const DEFAULT_OPTIMIZED_COMPRESSION_USAGE_COUNTER_THRESHOLD: u64 = 0x100; +// if changed, update cli-cache.md +const DEFAULT_CLEANUP_INTERVAL: Duration = Duration::from_secs(60 * 60); +// if changed, update cli-cache.md +const DEFAULT_OPTIMIZING_COMPRESSION_TASK_TIMEOUT: Duration = Duration::from_secs(30 * 60); +// the default assumes problems with timezone configuration on network share + some clock drift +// please notice 24 timezones = max 23h difference between some of them +// if changed, update cli-cache.md +const DEFAULT_ALLOWED_CLOCK_DRIFT_FOR_FILES_FROM_FUTURE: Duration = + Duration::from_secs(60 * 60 * 24); +// if changed, update cli-cache.md +const DEFAULT_FILE_COUNT_SOFT_LIMIT: u64 = 0x10_000; +// if changed, update cli-cache.md +const DEFAULT_FILES_TOTAL_SIZE_SOFT_LIMIT: u64 = 1024 * 1024 * 512; +// if changed, update cli-cache.md +const DEFAULT_FILE_COUNT_LIMIT_PERCENT_IF_DELETING: u8 = 70; +// if changed, update cli-cache.md +const DEFAULT_FILES_TOTAL_SIZE_LIMIT_PERCENT_IF_DELETING: u8 = 70; + +fn project_dirs() -> Option { + ProjectDirs::from("", "BytecodeAlliance", "wasmtime") +} + +fn default_config_path() -> Result { + match project_dirs() { + Some(dirs) => Ok(dirs.config_dir().join("config.toml")), + None => bail!("config file not specified and failed to get the default"), + } +} + +// Deserializers of our custom formats +// can be replaced with const generics later +macro_rules! generate_deserializer { + ($name:ident($numname:ident: $numty:ty, $unitname:ident: &str) -> $retty:ty {$body:expr}) => { + fn $name<'de, D>(deserializer: D) -> Result<$retty, D::Error> + where + D: Deserializer<'de>, + { + let text = Option::::deserialize(deserializer)?; + let text = match text { + None => return Ok(None), + Some(text) => text, + }; + let text = text.trim(); + let split_point = text.find(|c: char| !c.is_numeric()); + let (num, unit) = split_point.map_or_else(|| (text, ""), |p| text.split_at(p)); + let deserialized = (|| { + let $numname = num.parse::<$numty>().ok()?; + let $unitname = unit.trim(); + $body + })(); + if deserialized.is_some() { + Ok(deserialized) + } else { + Err(de::Error::custom( + "Invalid value, please refer to the documentation", + )) + } + } + }; +} + +generate_deserializer!(deserialize_duration(num: u64, unit: &str) -> Option { + match unit { + "s" => Some(Duration::from_secs(num)), + "m" => Some(Duration::from_secs(num * 60)), + "h" => Some(Duration::from_secs(num * 60 * 60)), + "d" => Some(Duration::from_secs(num * 60 * 60 * 24)), + _ => None, + } +}); + +generate_deserializer!(deserialize_si_prefix(num: u64, unit: &str) -> Option { + match unit { + "" => Some(num), + "K" => num.checked_mul(1_000), + "M" => num.checked_mul(1_000_000), + "G" => num.checked_mul(1_000_000_000), + "T" => num.checked_mul(1_000_000_000_000), + "P" => num.checked_mul(1_000_000_000_000_000), + _ => None, + } +}); + +generate_deserializer!(deserialize_disk_space(num: u64, unit: &str) -> Option { + match unit { + "" => Some(num), + "K" => num.checked_mul(1_000), + "Ki" => num.checked_mul(1u64 << 10), + "M" => num.checked_mul(1_000_000), + "Mi" => num.checked_mul(1u64 << 20), + "G" => num.checked_mul(1_000_000_000), + "Gi" => num.checked_mul(1u64 << 30), + "T" => num.checked_mul(1_000_000_000_000), + "Ti" => num.checked_mul(1u64 << 40), + "P" => num.checked_mul(1_000_000_000_000_000), + "Pi" => num.checked_mul(1u64 << 50), + _ => None, + } +}); + +generate_deserializer!(deserialize_percent(num: u8, unit: &str) -> Option { + match unit { + "%" => Some(num), + _ => None, + } +}); + +static CACHE_IMPROPER_CONFIG_ERROR_MSG: &str = + "Cache system should be enabled and all settings must be validated or defaulted"; + +macro_rules! generate_setting_getter { + ($setting:ident: $setting_type:ty) => { + /// Returns `$setting`. + /// + /// Panics if the cache is disabled. + pub fn $setting(&self) -> $setting_type { + self + .$setting + .expect(CACHE_IMPROPER_CONFIG_ERROR_MSG) + } + }; +} + +impl CacheConfig { + generate_setting_getter!(worker_event_queue_size: u64); + generate_setting_getter!(baseline_compression_level: i32); + generate_setting_getter!(optimized_compression_level: i32); + generate_setting_getter!(optimized_compression_usage_counter_threshold: u64); + generate_setting_getter!(cleanup_interval: Duration); + generate_setting_getter!(optimizing_compression_task_timeout: Duration); + generate_setting_getter!(allowed_clock_drift_for_files_from_future: Duration); + generate_setting_getter!(file_count_soft_limit: u64); + generate_setting_getter!(files_total_size_soft_limit: u64); + generate_setting_getter!(file_count_limit_percent_if_deleting: u8); + generate_setting_getter!(files_total_size_limit_percent_if_deleting: u8); + + /// Returns true if and only if the cache is enabled. + pub fn enabled(&self) -> bool { + self.enabled + } + + /// Returns path to the cache directory. + /// + /// Panics if the cache is disabled. + pub fn directory(&self) -> &PathBuf { + self.directory + .as_ref() + .expect(CACHE_IMPROPER_CONFIG_ERROR_MSG) + } + + /// Creates a new set of configuration which represents a disabled cache + pub fn new_cache_disabled() -> Self { + Self { + enabled: false, + directory: None, + worker_event_queue_size: None, + baseline_compression_level: None, + optimized_compression_level: None, + optimized_compression_usage_counter_threshold: None, + cleanup_interval: None, + optimizing_compression_task_timeout: None, + allowed_clock_drift_for_files_from_future: None, + file_count_soft_limit: None, + files_total_size_soft_limit: None, + file_count_limit_percent_if_deleting: None, + files_total_size_limit_percent_if_deleting: None, + worker: None, + state: Arc::new(CacheState::default()), + } + } + + fn new_cache_enabled_template() -> Self { + let mut conf = Self::new_cache_disabled(); + conf.enabled = true; + conf + } + + /// Parses cache configuration from the file specified + pub fn from_file(config_file: Option<&Path>) -> Result { + let mut config = Self::load_and_parse_file(config_file)?; + + // validate values and fill in defaults + config.validate_directory_or_default()?; + config.validate_worker_event_queue_size_or_default(); + config.validate_baseline_compression_level_or_default()?; + config.validate_optimized_compression_level_or_default()?; + config.validate_optimized_compression_usage_counter_threshold_or_default(); + config.validate_cleanup_interval_or_default(); + config.validate_optimizing_compression_task_timeout_or_default(); + config.validate_allowed_clock_drift_for_files_from_future_or_default(); + config.validate_file_count_soft_limit_or_default(); + config.validate_files_total_size_soft_limit_or_default(); + config.validate_file_count_limit_percent_if_deleting_or_default()?; + config.validate_files_total_size_limit_percent_if_deleting_or_default()?; + config.spawn_worker(); + + Ok(config) + } + + fn spawn_worker(&mut self) { + if self.enabled { + self.worker = Some(Worker::start_new(self, None)); + } + } + + pub(super) fn worker(&self) -> &Worker { + assert!(self.enabled); + self.worker.as_ref().unwrap() + } + + /// Returns the number of cache hits seen so far + pub fn cache_hits(&self) -> usize { + self.state.hits.load(SeqCst) + } + + /// Returns the number of cache misses seen so far + pub fn cache_misses(&self) -> usize { + self.state.misses.load(SeqCst) + } + + pub(crate) fn on_cache_get_async(&self, path: impl AsRef) { + self.state.hits.fetch_add(1, SeqCst); + self.worker().on_cache_get_async(path) + } + + pub(crate) fn on_cache_update_async(&self, path: impl AsRef) { + self.state.misses.fetch_add(1, SeqCst); + self.worker().on_cache_update_async(path) + } + + fn load_and_parse_file(config_file: Option<&Path>) -> Result { + // get config file path + let (config_file, user_custom_file) = match config_file { + Some(path) => (path.to_path_buf(), true), + None => (default_config_path()?, false), + }; + + // read config, or use default one + let entity_exists = config_file.exists(); + match (entity_exists, user_custom_file) { + (false, false) => Ok(Self::new_cache_enabled_template()), + _ => { + let bytes = fs::read(&config_file).context(format!( + "failed to read config file: {}", + config_file.display() + ))?; + let config = toml::from_slice::(&bytes[..]).context(format!( + "failed to parse config file: {}", + config_file.display() + ))?; + Ok(config.cache) + } + } + } + + fn validate_directory_or_default(&mut self) -> Result<()> { + if self.directory.is_none() { + match project_dirs() { + Some(proj_dirs) => self.directory = Some(proj_dirs.cache_dir().to_path_buf()), + None => { + bail!("Cache directory not specified and failed to get the default"); + } + } + } + + // On Windows, if we want long paths, we need '\\?\' prefix, but it doesn't work + // with relative paths. One way to get absolute path (the only one?) is to use + // fs::canonicalize, but it requires that given path exists. The extra advantage + // of this method is fact that the method prepends '\\?\' on Windows. + let cache_dir = self.directory.as_ref().unwrap(); + + if !cache_dir.is_absolute() { + bail!( + "Cache directory path has to be absolute, path: {}", + cache_dir.display(), + ); + } + + fs::create_dir_all(cache_dir).context(format!( + "failed to create cache directory: {}", + cache_dir.display() + ))?; + let canonical = fs::canonicalize(cache_dir).context(format!( + "failed to canonicalize cache directory: {}", + cache_dir.display() + ))?; + self.directory = Some(canonical); + Ok(()) + } + + fn validate_worker_event_queue_size_or_default(&mut self) { + if self.worker_event_queue_size.is_none() { + self.worker_event_queue_size = Some(DEFAULT_WORKER_EVENT_QUEUE_SIZE); + } + + if self.worker_event_queue_size.unwrap() < WORKER_EVENT_QUEUE_SIZE_WARNING_TRESHOLD { + warn!("Detected small worker event queue size. Some messages might be lost."); + } + } + + fn validate_baseline_compression_level_or_default(&mut self) -> Result<()> { + if self.baseline_compression_level.is_none() { + self.baseline_compression_level = Some(DEFAULT_BASELINE_COMPRESSION_LEVEL); + } + + if !ZSTD_COMPRESSION_LEVELS.contains(&self.baseline_compression_level.unwrap()) { + bail!( + "Invalid baseline compression level: {} not in {:#?}", + self.baseline_compression_level.unwrap(), + ZSTD_COMPRESSION_LEVELS + ); + } + Ok(()) + } + + // assumption: baseline compression level has been verified + fn validate_optimized_compression_level_or_default(&mut self) -> Result<()> { + if self.optimized_compression_level.is_none() { + self.optimized_compression_level = Some(DEFAULT_OPTIMIZED_COMPRESSION_LEVEL); + } + + let opt_lvl = self.optimized_compression_level.unwrap(); + let base_lvl = self.baseline_compression_level.unwrap(); + + if !ZSTD_COMPRESSION_LEVELS.contains(&opt_lvl) { + bail!( + "Invalid optimized compression level: {} not in {:#?}", + opt_lvl, + ZSTD_COMPRESSION_LEVELS + ); + } + + if opt_lvl < base_lvl { + bail!( + "Invalid optimized compression level is lower than baseline: {} < {}", + opt_lvl, + base_lvl + ); + } + Ok(()) + } + + fn validate_optimized_compression_usage_counter_threshold_or_default(&mut self) { + if self.optimized_compression_usage_counter_threshold.is_none() { + self.optimized_compression_usage_counter_threshold = + Some(DEFAULT_OPTIMIZED_COMPRESSION_USAGE_COUNTER_THRESHOLD); + } + } + + fn validate_cleanup_interval_or_default(&mut self) { + if self.cleanup_interval.is_none() { + self.cleanup_interval = Some(DEFAULT_CLEANUP_INTERVAL); + } + } + + fn validate_optimizing_compression_task_timeout_or_default(&mut self) { + if self.optimizing_compression_task_timeout.is_none() { + self.optimizing_compression_task_timeout = + Some(DEFAULT_OPTIMIZING_COMPRESSION_TASK_TIMEOUT); + } + } + + fn validate_allowed_clock_drift_for_files_from_future_or_default(&mut self) { + if self.allowed_clock_drift_for_files_from_future.is_none() { + self.allowed_clock_drift_for_files_from_future = + Some(DEFAULT_ALLOWED_CLOCK_DRIFT_FOR_FILES_FROM_FUTURE); + } + } + + fn validate_file_count_soft_limit_or_default(&mut self) { + if self.file_count_soft_limit.is_none() { + self.file_count_soft_limit = Some(DEFAULT_FILE_COUNT_SOFT_LIMIT); + } + } + + fn validate_files_total_size_soft_limit_or_default(&mut self) { + if self.files_total_size_soft_limit.is_none() { + self.files_total_size_soft_limit = Some(DEFAULT_FILES_TOTAL_SIZE_SOFT_LIMIT); + } + } + + fn validate_file_count_limit_percent_if_deleting_or_default(&mut self) -> Result<()> { + if self.file_count_limit_percent_if_deleting.is_none() { + self.file_count_limit_percent_if_deleting = + Some(DEFAULT_FILE_COUNT_LIMIT_PERCENT_IF_DELETING); + } + + let percent = self.file_count_limit_percent_if_deleting.unwrap(); + if percent > 100 { + bail!( + "Invalid files count limit percent if deleting: {} not in range 0-100%", + percent + ); + } + Ok(()) + } + + fn validate_files_total_size_limit_percent_if_deleting_or_default(&mut self) -> Result<()> { + if self.files_total_size_limit_percent_if_deleting.is_none() { + self.files_total_size_limit_percent_if_deleting = + Some(DEFAULT_FILES_TOTAL_SIZE_LIMIT_PERCENT_IF_DELETING); + } + + let percent = self.files_total_size_limit_percent_if_deleting.unwrap(); + if percent > 100 { + bail!( + "Invalid files total size limit percent if deleting: {} not in range 0-100%", + percent + ); + } + Ok(()) + } +} + +#[cfg(test)] +#[macro_use] +pub mod tests; diff --git a/crates/environ/src/cache/config/tests.rs b/crates/environ/src/cache/config/tests.rs new file mode 100644 index 0000000000..0c1dc873fb --- /dev/null +++ b/crates/environ/src/cache/config/tests.rs @@ -0,0 +1,525 @@ +use super::CacheConfig; +use std::fs; +use std::path::PathBuf; +use std::time::Duration; +use tempfile::{self, TempDir}; + +// note: config loading during validation creates cache directory to canonicalize its path, +// that's why these function and macro always use custom cache directory +// note: tempdir removes directory when being dropped, so we need to return it to the caller, +// so the paths are valid +pub fn test_prolog() -> (TempDir, PathBuf, PathBuf) { + let _ = pretty_env_logger::try_init(); + let temp_dir = tempfile::tempdir().expect("Can't create temporary directory"); + let cache_dir = temp_dir.path().join("cache-dir"); + let config_path = temp_dir.path().join("cache-config.toml"); + (temp_dir, cache_dir, config_path) +} + +macro_rules! load_config { + ($config_path:ident, $content_fmt:expr, $cache_dir:ident) => {{ + let config_path = &$config_path; + let content = format!( + $content_fmt, + cache_dir = toml::to_string_pretty(&format!("{}", $cache_dir.display())).unwrap() + ); + fs::write(config_path, content).expect("Failed to write test config file"); + CacheConfig::from_file(Some(config_path)).unwrap() + }}; +} + +macro_rules! bad_config { + ($config_path:ident, $content_fmt:expr, $cache_dir:ident) => {{ + let config_path = &$config_path; + let content = format!( + $content_fmt, + cache_dir = toml::to_string_pretty(&format!("{}", $cache_dir.display())).unwrap() + ); + fs::write(config_path, content).expect("Failed to write test config file"); + assert!(CacheConfig::from_file(Some(config_path)).is_err()); + }}; +} + +// test without macros to test being disabled +#[test] +fn test_disabled() { + let dir = tempfile::tempdir().expect("Can't create temporary directory"); + let config_path = dir.path().join("cache-config.toml"); + let config_content = "[cache]\n\ + enabled = false\n"; + fs::write(&config_path, config_content).expect("Failed to write test config file"); + let conf = CacheConfig::from_file(Some(&config_path)).unwrap(); + assert!(!conf.enabled()); +} + +#[test] +fn test_unrecognized_settings() { + let (_td, cd, cp) = test_prolog(); + bad_config!( + cp, + "unrecognized-setting = 42\n\ + [cache]\n\ + enabled = true\n\ + directory = {cache_dir}", + cd + ); + + bad_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + unrecognized-setting = 42", + cd + ); +} + +#[test] +fn test_all_settings() { + let (_td, cd, cp) = test_prolog(); + let conf = load_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + worker-event-queue-size = '16'\n\ + baseline-compression-level = 3\n\ + optimized-compression-level = 20\n\ + optimized-compression-usage-counter-threshold = '256'\n\ + cleanup-interval = '1h'\n\ + optimizing-compression-task-timeout = '30m'\n\ + allowed-clock-drift-for-files-from-future = '1d'\n\ + file-count-soft-limit = '65536'\n\ + files-total-size-soft-limit = '512Mi'\n\ + file-count-limit-percent-if-deleting = '70%'\n\ + files-total-size-limit-percent-if-deleting = '70%'", + cd + ); + check_conf(&conf, &cd); + + let conf = load_config!( + cp, + // added some white spaces + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + worker-event-queue-size = ' 16\t'\n\ + baseline-compression-level = 3\n\ + optimized-compression-level =\t 20\n\ + optimized-compression-usage-counter-threshold = '256'\n\ + cleanup-interval = ' 1h'\n\ + optimizing-compression-task-timeout = '30 m'\n\ + allowed-clock-drift-for-files-from-future = '1\td'\n\ + file-count-soft-limit = '\t \t65536\t'\n\ + files-total-size-soft-limit = '512\t\t Mi '\n\ + file-count-limit-percent-if-deleting = '70\t%'\n\ + files-total-size-limit-percent-if-deleting = ' 70 %'", + cd + ); + check_conf(&conf, &cd); + + fn check_conf(conf: &CacheConfig, cd: &PathBuf) { + assert!(conf.enabled()); + assert_eq!( + conf.directory(), + &fs::canonicalize(cd).expect("canonicalize failed") + ); + assert_eq!(conf.worker_event_queue_size(), 0x10); + assert_eq!(conf.baseline_compression_level(), 3); + assert_eq!(conf.optimized_compression_level(), 20); + assert_eq!(conf.optimized_compression_usage_counter_threshold(), 0x100); + assert_eq!(conf.cleanup_interval(), Duration::from_secs(60 * 60)); + assert_eq!( + conf.optimizing_compression_task_timeout(), + Duration::from_secs(30 * 60) + ); + assert_eq!( + conf.allowed_clock_drift_for_files_from_future(), + Duration::from_secs(60 * 60 * 24) + ); + assert_eq!(conf.file_count_soft_limit(), 0x10_000); + assert_eq!(conf.files_total_size_soft_limit(), 512 * (1u64 << 20)); + assert_eq!(conf.file_count_limit_percent_if_deleting(), 70); + assert_eq!(conf.files_total_size_limit_percent_if_deleting(), 70); + } +} + +#[test] +fn test_compression_level_settings() { + let (_td, cd, cp) = test_prolog(); + let conf = load_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + baseline-compression-level = 1\n\ + optimized-compression-level = 21", + cd + ); + assert!(conf.enabled()); + assert_eq!(conf.baseline_compression_level(), 1); + assert_eq!(conf.optimized_compression_level(), 21); + + bad_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + baseline-compression-level = -1\n\ + optimized-compression-level = 21", + cd + ); + + bad_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + baseline-compression-level = 15\n\ + optimized-compression-level = 10", + cd + ); +} + +#[test] +fn test_si_prefix_settings() { + let (_td, cd, cp) = test_prolog(); + let conf = load_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + worker-event-queue-size = '42'\n\ + optimized-compression-usage-counter-threshold = '4K'\n\ + file-count-soft-limit = '3M'", + cd + ); + assert!(conf.enabled()); + assert_eq!(conf.worker_event_queue_size(), 42); + assert_eq!(conf.optimized_compression_usage_counter_threshold(), 4_000); + assert_eq!(conf.file_count_soft_limit(), 3_000_000); + + let conf = load_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + worker-event-queue-size = '2K'\n\ + optimized-compression-usage-counter-threshold = '4444T'\n\ + file-count-soft-limit = '1P'", + cd + ); + assert!(conf.enabled()); + assert_eq!(conf.worker_event_queue_size(), 2_000); + assert_eq!( + conf.optimized_compression_usage_counter_threshold(), + 4_444_000_000_000_000 + ); + assert_eq!(conf.file_count_soft_limit(), 1_000_000_000_000_000); + + // different errors + bad_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + worker-event-queue-size = '2g'", + cd + ); + + bad_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + file-count-soft-limit = 1", + cd + ); + + bad_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + file-count-soft-limit = '-31337'", + cd + ); + + bad_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + file-count-soft-limit = '3.14M'", + cd + ); +} + +#[test] +fn test_disk_space_settings() { + let (_td, cd, cp) = test_prolog(); + let conf = load_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + files-total-size-soft-limit = '76'", + cd + ); + assert!(conf.enabled()); + assert_eq!(conf.files_total_size_soft_limit(), 76); + + let conf = load_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + files-total-size-soft-limit = '42 Mi'", + cd + ); + assert!(conf.enabled()); + assert_eq!(conf.files_total_size_soft_limit(), 42 * (1u64 << 20)); + + let conf = load_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + files-total-size-soft-limit = '2 Gi'", + cd + ); + assert!(conf.enabled()); + assert_eq!(conf.files_total_size_soft_limit(), 2 * (1u64 << 30)); + + let conf = load_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + files-total-size-soft-limit = '31337 Ti'", + cd + ); + assert!(conf.enabled()); + assert_eq!(conf.files_total_size_soft_limit(), 31337 * (1u64 << 40)); + + let conf = load_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + files-total-size-soft-limit = '7 Pi'", + cd + ); + assert!(conf.enabled()); + assert_eq!(conf.files_total_size_soft_limit(), 7 * (1u64 << 50)); + + let conf = load_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + files-total-size-soft-limit = '7M'", + cd + ); + assert!(conf.enabled()); + assert_eq!(conf.files_total_size_soft_limit(), 7_000_000); + + // different errors + bad_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + files-total-size-soft-limit = '7 mi'", + cd + ); + + bad_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + files-total-size-soft-limit = 1", + cd + ); + + bad_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + files-total-size-soft-limit = '-31337'", + cd + ); + + bad_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + files-total-size-soft-limit = '3.14Ki'", + cd + ); +} + +#[test] +fn test_duration_settings() { + let (_td, cd, cp) = test_prolog(); + let conf = load_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + cleanup-interval = '100s'\n\ + optimizing-compression-task-timeout = '3m'\n\ + allowed-clock-drift-for-files-from-future = '4h'", + cd + ); + assert!(conf.enabled()); + assert_eq!(conf.cleanup_interval(), Duration::from_secs(100)); + assert_eq!( + conf.optimizing_compression_task_timeout(), + Duration::from_secs(3 * 60) + ); + assert_eq!( + conf.allowed_clock_drift_for_files_from_future(), + Duration::from_secs(4 * 60 * 60) + ); + + let conf = load_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + cleanup-interval = '2d'\n\ + optimizing-compression-task-timeout = '333 m'", + cd + ); + assert!(conf.enabled()); + assert_eq!( + conf.cleanup_interval(), + Duration::from_secs(2 * 24 * 60 * 60) + ); + assert_eq!( + conf.optimizing_compression_task_timeout(), + Duration::from_secs(333 * 60) + ); + + // different errors + bad_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + optimizing-compression-task-timeout = '333'", + cd + ); + + bad_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + optimizing-compression-task-timeout = 333", + cd + ); + + bad_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + optimizing-compression-task-timeout = '10 M'", + cd + ); + + bad_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + optimizing-compression-task-timeout = '10 min'", + cd + ); + + bad_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + optimizing-compression-task-timeout = '-10s'", + cd + ); + + bad_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + optimizing-compression-task-timeout = '1.5m'", + cd + ); +} + +#[test] +fn test_percent_settings() { + let (_td, cd, cp) = test_prolog(); + let conf = load_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + file-count-limit-percent-if-deleting = '62%'\n\ + files-total-size-limit-percent-if-deleting = '23 %'", + cd + ); + assert!(conf.enabled()); + assert_eq!(conf.file_count_limit_percent_if_deleting(), 62); + assert_eq!(conf.files_total_size_limit_percent_if_deleting(), 23); + + // different errors + bad_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + files-total-size-limit-percent-if-deleting = '23'", + cd + ); + + bad_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + files-total-size-limit-percent-if-deleting = '22.5%'", + cd + ); + + bad_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + files-total-size-limit-percent-if-deleting = '0.5'", + cd + ); + + bad_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + files-total-size-limit-percent-if-deleting = '-1%'", + cd + ); + + bad_config!( + cp, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + files-total-size-limit-percent-if-deleting = '101%'", + cd + ); +} diff --git a/crates/environ/src/cache/tests.rs b/crates/environ/src/cache/tests.rs new file mode 100644 index 0000000000..d454371178 --- /dev/null +++ b/crates/environ/src/cache/tests.rs @@ -0,0 +1,104 @@ +use super::config::tests::test_prolog; +use super::*; +use cranelift_entity::PrimaryMap; +use std::fs; + +// Since cache system is a global thing, each test needs to be run in seperate process. +// So, init() tests are run as integration tests. +// However, caching is a private thing, an implementation detail, and needs to be tested +// from the inside of the module. +// We test init() in exactly one test, rest of the tests doesn't rely on it. + +#[test] +fn test_cache_init() { + let (_tempdir, cache_dir, config_path) = test_prolog(); + let baseline_compression_level = 4; + let config_content = format!( + "[cache]\n\ + enabled = true\n\ + directory = {}\n\ + baseline-compression-level = {}\n", + toml::to_string_pretty(&format!("{}", cache_dir.display())).unwrap(), + baseline_compression_level, + ); + fs::write(&config_path, config_content).expect("Failed to write test config file"); + + let cache_config = CacheConfig::from_file(Some(&config_path)).unwrap(); + + // test if we can use config + assert!(cache_config.enabled()); + // assumption: config init creates cache directory and returns canonicalized path + assert_eq!( + *cache_config.directory(), + fs::canonicalize(cache_dir).unwrap() + ); + assert_eq!( + cache_config.baseline_compression_level(), + baseline_compression_level + ); + + // test if we can use worker + cache_config.worker().on_cache_update_async(config_path); +} + +#[test] +fn test_write_read_cache() { + let (_tempdir, cache_dir, config_path) = test_prolog(); + let cache_config = load_config!( + config_path, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + baseline-compression-level = 3\n", + cache_dir + ); + assert!(cache_config.enabled()); + + // assumption: config load creates cache directory and returns canonicalized path + assert_eq!( + *cache_config.directory(), + fs::canonicalize(cache_dir).unwrap() + ); + + let compiler1 = "test-1"; + let compiler2 = "test-2"; + + let entry1 = ModuleCacheEntry::from_inner(ModuleCacheEntryInner::new(compiler1, &cache_config)); + let entry2 = ModuleCacheEntry::from_inner(ModuleCacheEntryInner::new(compiler2, &cache_config)); + + entry1.get_data(1, |_| new_module_cache_data()).unwrap(); + entry1.get_data::<_, i32>(1, |_| panic!()).unwrap(); + + entry1.get_data(2, |_| new_module_cache_data()).unwrap(); + entry1.get_data::<_, i32>(1, |_| panic!()).unwrap(); + entry1.get_data::<_, i32>(2, |_| panic!()).unwrap(); + + entry1.get_data(3, |_| new_module_cache_data()).unwrap(); + entry1.get_data::<_, i32>(1, |_| panic!()).unwrap(); + entry1.get_data::<_, i32>(2, |_| panic!()).unwrap(); + entry1.get_data::<_, i32>(3, |_| panic!()).unwrap(); + + entry1.get_data(4, |_| new_module_cache_data()).unwrap(); + entry1.get_data::<_, i32>(1, |_| panic!()).unwrap(); + entry1.get_data::<_, i32>(2, |_| panic!()).unwrap(); + entry1.get_data::<_, i32>(3, |_| panic!()).unwrap(); + entry1.get_data::<_, i32>(4, |_| panic!()).unwrap(); + + entry2.get_data(1, |_| new_module_cache_data()).unwrap(); + entry1.get_data::<_, i32>(1, |_| panic!()).unwrap(); + entry1.get_data::<_, i32>(2, |_| panic!()).unwrap(); + entry1.get_data::<_, i32>(3, |_| panic!()).unwrap(); + entry1.get_data::<_, i32>(4, |_| panic!()).unwrap(); + entry2.get_data::<_, i32>(1, |_| panic!()).unwrap(); +} + +fn new_module_cache_data() -> Result { + Ok(( + Compilation::new(PrimaryMap::new()), + PrimaryMap::new(), + PrimaryMap::new(), + PrimaryMap::new(), + PrimaryMap::new(), + PrimaryMap::new(), + )) +} diff --git a/crates/environ/src/cache/worker.rs b/crates/environ/src/cache/worker.rs new file mode 100644 index 0000000000..f17ffa4103 --- /dev/null +++ b/crates/environ/src/cache/worker.rs @@ -0,0 +1,900 @@ +//! Background worker that watches over the cache. +//! +//! It cleans up old cache, updates statistics and optimizes the cache. +//! We allow losing some messages (it doesn't hurt) and some races, +//! but we guarantee eventual consistency and fault tolerancy. +//! Background tasks can be CPU intensive, but the worker thread has low priority. + +use super::{fs_write_atomic, CacheConfig}; +use log::{debug, info, trace, warn}; +use serde::{Deserialize, Serialize}; +use std::cmp; +use std::collections::HashMap; +use std::ffi::OsStr; +use std::fmt; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::mpsc::{sync_channel, Receiver, SyncSender}; +#[cfg(test)] +use std::sync::{Arc, Condvar, Mutex}; +use std::thread; +use std::time::Duration; +#[cfg(not(test))] +use std::time::SystemTime; +#[cfg(test)] +use tests::system_time_stub::SystemTimeStub as SystemTime; + +#[derive(Clone)] +pub(super) struct Worker { + sender: SyncSender, + #[cfg(test)] + stats: Arc<(Mutex, Condvar)>, +} + +struct WorkerThread { + receiver: Receiver, + cache_config: CacheConfig, + #[cfg(test)] + stats: Arc<(Mutex, Condvar)>, +} + +#[cfg(test)] +#[derive(Default)] +struct WorkerStats { + dropped: u32, + sent: u32, + handled: u32, +} + +#[derive(Debug, Clone)] +enum CacheEvent { + OnCacheGet(PathBuf), + OnCacheUpdate(PathBuf), +} + +impl Worker { + pub(super) fn start_new( + cache_config: &CacheConfig, + init_file_per_thread_logger: Option<&'static str>, + ) -> Self { + let queue_size = match cache_config.worker_event_queue_size() { + num if num <= usize::max_value() as u64 => num as usize, + _ => usize::max_value(), + }; + let (tx, rx) = sync_channel(queue_size); + + #[cfg(test)] + let stats = Arc::new((Mutex::new(WorkerStats::default()), Condvar::new())); + + let worker_thread = WorkerThread { + receiver: rx, + cache_config: cache_config.clone(), + #[cfg(test)] + stats: stats.clone(), + }; + + // when self is dropped, sender will be dropped, what will cause the channel + // to hang, and the worker thread to exit -- it happens in the tests + // non-tests binary has only a static worker, so Rust doesn't drop it + thread::spawn(move || worker_thread.run(init_file_per_thread_logger)); + + Self { + sender: tx, + #[cfg(test)] + stats, + } + } + + pub(super) fn on_cache_get_async(&self, path: impl AsRef) { + let event = CacheEvent::OnCacheGet(path.as_ref().to_path_buf()); + self.send_cache_event(event); + } + + pub(super) fn on_cache_update_async(&self, path: impl AsRef) { + let event = CacheEvent::OnCacheUpdate(path.as_ref().to_path_buf()); + self.send_cache_event(event); + } + + #[inline] + fn send_cache_event(&self, event: CacheEvent) { + let sent_event = self.sender.try_send(event.clone()); + + if let Err(ref err) = sent_event { + info!( + "Failed to send asynchronously message to worker thread, \ + event: {:?}, error: {}", + event, err + ); + } + + #[cfg(test)] + { + let mut stats = self + .stats + .0 + .lock() + .expect("Failed to acquire worker stats lock"); + + if sent_event.is_ok() { + stats.sent += 1; + } else { + stats.dropped += 1; + } + } + } + + #[cfg(test)] + pub(super) fn events_dropped(&self) -> u32 { + let stats = self + .stats + .0 + .lock() + .expect("Failed to acquire worker stats lock"); + stats.dropped + } + + #[cfg(test)] + pub(super) fn wait_for_all_events_handled(&self) { + let (stats, condvar) = &*self.stats; + let mut stats = stats.lock().expect("Failed to acquire worker stats lock"); + while stats.handled != stats.sent { + stats = condvar + .wait(stats) + .expect("Failed to reacquire worker stats lock"); + } + } +} + +impl fmt::Debug for Worker { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Worker").finish() + } +} + +#[derive(Serialize, Deserialize)] +struct ModuleCacheStatistics { + pub usages: u64, + #[serde(rename = "optimized-compression")] + pub compression_level: i32, +} + +impl ModuleCacheStatistics { + fn default(cache_config: &CacheConfig) -> Self { + Self { + usages: 0, + compression_level: cache_config.baseline_compression_level(), + } + } +} + +enum CacheEntry { + Recognized { + path: PathBuf, + mtime: SystemTime, + size: u64, + }, + Unrecognized { + path: PathBuf, + is_dir: bool, + }, +} + +macro_rules! unwrap_or_warn { + ($result:expr, $cont:stmt, $err_msg:expr, $path:expr) => { + match $result { + Ok(val) => val, + Err(err) => { + warn!("{}, path: {}, msg: {}", $err_msg, $path.display(), err); + $cont + } + } + }; +} + +impl WorkerThread { + fn run(self, init_file_per_thread_logger: Option<&'static str>) { + if let Some(prefix) = init_file_per_thread_logger { + file_per_thread_logger::initialize(prefix); + } + + debug!("Cache worker thread started."); + + Self::lower_thread_priority(); + + #[cfg(test)] + let (stats, condvar) = &*self.stats; + + for event in self.receiver.iter() { + match event { + CacheEvent::OnCacheGet(path) => self.handle_on_cache_get(path), + CacheEvent::OnCacheUpdate(path) => self.handle_on_cache_update(path), + } + + #[cfg(test)] + { + let mut stats = stats.lock().expect("Failed to acquire worker stats lock"); + stats.handled += 1; + condvar.notify_all(); + } + } + } + + #[cfg(target_os = "windows")] + fn lower_thread_priority() { + use std::convert::TryInto; + use winapi::um::processthreadsapi::{GetCurrentThread, SetThreadPriority}; + use winapi::um::winbase::THREAD_MODE_BACKGROUND_BEGIN; + + // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setthreadpriority + // https://docs.microsoft.com/en-us/windows/win32/procthread/scheduling-priorities + + if unsafe { + SetThreadPriority( + GetCurrentThread(), + THREAD_MODE_BACKGROUND_BEGIN.try_into().unwrap(), + ) + } == 0 + { + warn!( + "Failed to lower worker thread priority. It might affect application performance." + ); + } + } + + #[cfg(not(target_os = "windows"))] + fn lower_thread_priority() { + // http://man7.org/linux/man-pages/man7/sched.7.html + + const NICE_DELTA_FOR_BACKGROUND_TASKS: i32 = 3; + + errno::set_errno(errno::Errno(0)); + let current_nice = unsafe { libc::nice(NICE_DELTA_FOR_BACKGROUND_TASKS) }; + let errno_val = errno::errno().0; + + if errno_val != 0 { + warn!( + "Failed to lower worker thread priority. It might affect application performance. \ + errno: {}", + errno_val + ); + } else { + debug!("New nice value of worker thread: {}", current_nice); + } + } + + /// Increases the usage counter and recompresses the file + /// if the usage counter reached configurable treshold. + fn handle_on_cache_get(&self, path: PathBuf) { + trace!("handle_on_cache_get() for path: {}", path.display()); + + // construct .stats file path + let filename = path.file_name().unwrap().to_str().unwrap(); + let stats_path = path.with_file_name(format!("{}.stats", filename)); + + // load .stats file (default if none or error) + let mut stats = read_stats_file(stats_path.as_ref()) + .unwrap_or_else(|| ModuleCacheStatistics::default(&self.cache_config)); + + // step 1: update the usage counter & write to the disk + // it's racy, but it's fine (the counter will be just smaller, + // sometimes will retrigger recompression) + stats.usages += 1; + if !write_stats_file(stats_path.as_ref(), &stats) { + return; + } + + // step 2: recompress if there's a need + let opt_compr_lvl = self.cache_config.optimized_compression_level(); + if stats.compression_level >= opt_compr_lvl + || stats.usages + < self + .cache_config + .optimized_compression_usage_counter_threshold() + { + return; + } + + let lock_path = if let Some(p) = acquire_task_fs_lock( + path.as_ref(), + self.cache_config.optimizing_compression_task_timeout(), + self.cache_config + .allowed_clock_drift_for_files_from_future(), + ) { + p + } else { + return; + }; + + trace!("Trying to recompress file: {}", path.display()); + + // recompress, write to other file, rename (it's atomic file content exchange) + // and update the stats file + let compressed_cache_bytes = unwrap_or_warn!( + fs::read(&path), + return, + "Failed to read old cache file", + path + ); + + let cache_bytes = unwrap_or_warn!( + zstd::decode_all(&compressed_cache_bytes[..]), + return, + "Failed to decompress cached code", + path + ); + + let recompressed_cache_bytes = unwrap_or_warn!( + zstd::encode_all(&cache_bytes[..], opt_compr_lvl), + return, + "Failed to compress cached code", + path + ); + + unwrap_or_warn!( + fs::write(&lock_path, &recompressed_cache_bytes), + return, + "Failed to write recompressed cache", + lock_path + ); + + unwrap_or_warn!( + fs::rename(&lock_path, &path), + { + if let Err(error) = fs::remove_file(&lock_path) { + warn!( + "Failed to clean up (remove) recompressed cache, path {}, err: {}", + lock_path.display(), + error + ); + } + + return; + }, + "Failed to rename recompressed cache", + lock_path + ); + + // update stats file (reload it! recompression can take some time) + if let Some(mut new_stats) = read_stats_file(stats_path.as_ref()) { + if new_stats.compression_level >= opt_compr_lvl { + // Rare race: + // two instances with different opt_compr_lvl: we don't know in which order they updated + // the cache file and the stats file (they are not updated together atomically) + // Possible solution is to use directories per cache entry, but it complicates the system + // and is not worth it. + debug!( + "DETECTED task did more than once (or race with new file): \ + recompression of {}. Note: if optimized compression level setting \ + has changed in the meantine, the stats file might contain \ + inconsistent compression level due to race.", + path.display() + ); + } else { + new_stats.compression_level = opt_compr_lvl; + let _ = write_stats_file(stats_path.as_ref(), &new_stats); + } + + if new_stats.usages < stats.usages { + debug!( + "DETECTED lower usage count (new file or race with counter \ + increasing): file {}", + path.display() + ); + } + } else { + debug!( + "Can't read stats file again to update compression level (it might got \ + cleaned up): file {}", + stats_path.display() + ); + } + + trace!("Task finished: recompress file: {}", path.display()); + } + + fn handle_on_cache_update(&self, path: PathBuf) { + trace!("handle_on_cache_update() for path: {}", path.display()); + + // ---------------------- step 1: create .stats file + + // construct .stats file path + let filename = path + .file_name() + .expect("Expected valid cache file name") + .to_str() + .expect("Expected valid cache file name"); + let stats_path = path.with_file_name(format!("{}.stats", filename)); + + // create and write stats file + let mut stats = ModuleCacheStatistics::default(&self.cache_config); + stats.usages += 1; + write_stats_file(&stats_path, &stats); + + // ---------------------- step 2: perform cleanup task if needed + + // acquire lock for cleanup task + // Lock is a proof of recent cleanup task, so we don't want to delete them. + // Expired locks will be deleted by the cleanup task. + let cleanup_file = self.cache_config.directory().join(".cleanup"); // some non existing marker file + if acquire_task_fs_lock( + &cleanup_file, + self.cache_config.cleanup_interval(), + self.cache_config + .allowed_clock_drift_for_files_from_future(), + ) + .is_none() + { + return; + } + + trace!("Trying to clean up cache"); + + let mut cache_index = self.list_cache_contents(); + let future_tolerance = SystemTime::now() + .checked_add( + self.cache_config + .allowed_clock_drift_for_files_from_future(), + ) + .expect("Brace your cache, the next Big Bang is coming (time overflow)"); + cache_index.sort_unstable_by(|lhs, rhs| { + // sort by age + use CacheEntry::*; + match (lhs, rhs) { + (Recognized { mtime: lhs_mt, .. }, Recognized { mtime: rhs_mt, .. }) => { + match (*lhs_mt > future_tolerance, *rhs_mt > future_tolerance) { + // later == younger + (false, false) => rhs_mt.cmp(lhs_mt), + // files from far future are treated as oldest recognized files + // we want to delete them, so the cache keeps track of recent files + // however, we don't delete them uncodintionally, + // because .stats file can be overwritten with a meaningful mtime + (true, false) => cmp::Ordering::Greater, + (false, true) => cmp::Ordering::Less, + (true, true) => cmp::Ordering::Equal, + } + } + // unrecognized is kind of infinity + (Recognized { .. }, Unrecognized { .. }) => cmp::Ordering::Less, + (Unrecognized { .. }, Recognized { .. }) => cmp::Ordering::Greater, + (Unrecognized { .. }, Unrecognized { .. }) => cmp::Ordering::Equal, + } + }); + + // find "cut" boundary: + // - remove unrecognized files anyway, + // - remove some cache files if some quota has been exceeded + let mut total_size = 0u64; + let mut start_delete_idx = None; + let mut start_delete_idx_if_deleting_recognized_items: Option = None; + + let total_size_limit = self.cache_config.files_total_size_soft_limit(); + let file_count_limit = self.cache_config.file_count_soft_limit(); + let tsl_if_deleting = total_size_limit + .checked_mul( + self.cache_config + .files_total_size_limit_percent_if_deleting() as u64, + ) + .unwrap() + / 100; + let fcl_if_deleting = file_count_limit + .checked_mul(self.cache_config.file_count_limit_percent_if_deleting() as u64) + .unwrap() + / 100; + + for (idx, item) in cache_index.iter().enumerate() { + let size = if let CacheEntry::Recognized { size, .. } = item { + size + } else { + start_delete_idx = Some(idx); + break; + }; + + total_size += size; + if start_delete_idx_if_deleting_recognized_items.is_none() + && (total_size > tsl_if_deleting || (idx + 1) as u64 > fcl_if_deleting) + { + start_delete_idx_if_deleting_recognized_items = Some(idx); + } + + if total_size > total_size_limit || (idx + 1) as u64 > file_count_limit { + start_delete_idx = start_delete_idx_if_deleting_recognized_items; + break; + } + } + + if let Some(idx) = start_delete_idx { + for item in &cache_index[idx..] { + let (result, path, entity) = match item { + CacheEntry::Recognized { path, .. } + | CacheEntry::Unrecognized { + path, + is_dir: false, + } => (fs::remove_file(path), path, "file"), + CacheEntry::Unrecognized { path, is_dir: true } => { + (fs::remove_dir_all(path), path, "directory") + } + }; + if let Err(err) = result { + warn!( + "Failed to remove {} during cleanup, path: {}, err: {}", + entity, + path.display(), + err + ); + } + } + } + + trace!("Task finished: clean up cache"); + } + + // Be fault tolerant: list as much as you can, and ignore the rest + fn list_cache_contents(&self) -> Vec { + fn enter_dir( + vec: &mut Vec, + dir_path: &Path, + level: u8, + cache_config: &CacheConfig, + ) { + macro_rules! add_unrecognized { + (file: $path:expr) => { + add_unrecognized!(false, $path) + }; + (dir: $path:expr) => { + add_unrecognized!(true, $path) + }; + ($is_dir:expr, $path:expr) => { + vec.push(CacheEntry::Unrecognized { + path: $path.to_path_buf(), + is_dir: $is_dir, + }); + }; + } + macro_rules! add_unrecognized_and { + ([ $( $ty:ident: $path:expr ),* ], $cont:stmt) => {{ + $( add_unrecognized!($ty: $path); )* + $cont + }}; + } + + macro_rules! unwrap_or { + ($result:expr, $cont:stmt, $err_msg:expr) => { + unwrap_or!($result, $cont, $err_msg, dir_path) + }; + ($result:expr, $cont:stmt, $err_msg:expr, $path:expr) => { + unwrap_or_warn!( + $result, + $cont, + format!("{}, level: {}", $err_msg, level), + $path + ) + }; + } + + // If we fail to list a directory, something bad is happening anyway + // (something touches our cache or we have disk failure) + // Try to delete it, so we can stay within soft limits of the cache size. + // This comment applies later in this function, too. + let it = unwrap_or!( + fs::read_dir(dir_path), + add_unrecognized_and!([dir: dir_path], return), + "Failed to list cache directory, deleting it" + ); + + let mut cache_files = HashMap::new(); + for entry in it { + // read_dir() returns an iterator over results - in case some of them are errors + // we don't know their names, so we can't delete them. We don't want to delete + // the whole directory with good entries too, so we just ignore the erroneous entries. + let entry = unwrap_or!( + entry, + continue, + "Failed to read a cache dir entry (NOT deleting it, it still occupies space)" + ); + let path = entry.path(); + match (level, path.is_dir()) { + (0..=1, true) => enter_dir(vec, &path, level + 1, cache_config), + (0..=1, false) => { + if level == 0 + && path.file_stem() == Some(OsStr::new(".cleanup")) + && path.extension().is_some() + // assume it's cleanup lock + && !is_fs_lock_expired( + Some(&entry), + &path, + cache_config.cleanup_interval(), + cache_config.allowed_clock_drift_for_files_from_future(), + ) + { + continue; // skip active lock + } + add_unrecognized!(file: path); + } + (2, false) => { + match path.extension().and_then(OsStr::to_str) { + // mod or stats file + None | Some("stats") => { + cache_files.insert(path, entry); + } + + Some(ext) => { + // check if valid lock + let recognized = ext.starts_with("wip-") + && !is_fs_lock_expired( + Some(&entry), + &path, + cache_config.optimizing_compression_task_timeout(), + cache_config.allowed_clock_drift_for_files_from_future(), + ); + + if !recognized { + add_unrecognized!(file: path); + } + } + } + } + (_, is_dir) => add_unrecognized!(is_dir, path), + } + } + + // associate module with its stats & handle them + // assumption: just mods and stats + for (path, entry) in cache_files.iter() { + let path_buf: PathBuf; + let (mod_, stats_, is_mod) = match path.extension() { + Some(_) => { + path_buf = path.with_extension(""); + ( + cache_files.get(&path_buf).map(|v| (&path_buf, v)), + Some((path, entry)), + false, + ) + } + None => { + path_buf = path.with_extension("stats"); + ( + Some((path, entry)), + cache_files.get(&path_buf).map(|v| (&path_buf, v)), + true, + ) + } + }; + + // construct a cache entry + match (mod_, stats_, is_mod) { + (Some((mod_path, mod_entry)), Some((stats_path, stats_entry)), true) => { + let mod_metadata = unwrap_or!( + mod_entry.metadata(), + add_unrecognized_and!([file: stats_path, file: mod_path], continue), + "Failed to get metadata, deleting BOTH module cache and stats files", + mod_path + ); + let stats_mtime = unwrap_or!( + stats_entry.metadata().and_then(|m| m.modified()), + add_unrecognized_and!( + [file: stats_path], + unwrap_or!( + mod_metadata.modified(), + add_unrecognized_and!( + [file: stats_path, file: mod_path], + continue + ), + "Failed to get mtime, deleting BOTH module cache and stats \ + files", + mod_path + ) + ), + "Failed to get metadata/mtime, deleting the file", + stats_path + ); + // .into() called for the SystemTimeStub if cfg(test) + #[allow(clippy::identity_conversion)] + vec.push(CacheEntry::Recognized { + path: mod_path.to_path_buf(), + mtime: stats_mtime.into(), + size: mod_metadata.len(), + }) + } + (Some(_), Some(_), false) => (), // was or will be handled by previous branch + (Some((mod_path, mod_entry)), None, _) => { + let (mod_metadata, mod_mtime) = unwrap_or!( + mod_entry + .metadata() + .and_then(|md| md.modified().map(|mt| (md, mt))), + add_unrecognized_and!([file: mod_path], continue), + "Failed to get metadata/mtime, deleting the file", + mod_path + ); + // .into() called for the SystemTimeStub if cfg(test) + #[allow(clippy::identity_conversion)] + vec.push(CacheEntry::Recognized { + path: mod_path.to_path_buf(), + mtime: mod_mtime.into(), + size: mod_metadata.len(), + }) + } + (None, Some((stats_path, _stats_entry)), _) => { + debug!("Found orphaned stats file: {}", stats_path.display()); + add_unrecognized!(file: stats_path); + } + _ => unreachable!(), + } + } + } + + let mut vec = Vec::new(); + enter_dir( + &mut vec, + self.cache_config.directory(), + 0, + &self.cache_config, + ); + vec + } +} + +fn read_stats_file(path: &Path) -> Option { + fs::read(path) + .map_err(|err| { + trace!( + "Failed to read stats file, path: {}, err: {}", + path.display(), + err + ) + }) + .and_then(|bytes| { + toml::from_slice::(&bytes[..]).map_err(|err| { + trace!( + "Failed to parse stats file, path: {}, err: {}", + path.display(), + err, + ) + }) + }) + .ok() +} + +fn write_stats_file(path: &Path, stats: &ModuleCacheStatistics) -> bool { + toml::to_string_pretty(&stats) + .map_err(|err| { + warn!( + "Failed to serialize stats file, path: {}, err: {}", + path.display(), + err + ) + }) + .and_then(|serialized| { + if fs_write_atomic(path, "stats", serialized.as_bytes()) { + Ok(()) + } else { + Err(()) + } + }) + .is_ok() +} + +/// Tries to acquire a lock for specific task. +/// +/// Returns Some(path) to the lock if succeeds. The task path must not +/// contain any extension and have file stem. +/// +/// To release a lock you need either manually rename or remove it, +/// or wait until it expires and cleanup task removes it. +/// +/// Note: this function is racy. Main idea is: be fault tolerant and +/// never block some task. The price is that we rarely do some task +/// more than once. +fn acquire_task_fs_lock( + task_path: &Path, + timeout: Duration, + allowed_future_drift: Duration, +) -> Option { + assert!(task_path.extension().is_none()); + assert!(task_path.file_stem().is_some()); + + // list directory + let dir_path = task_path.parent()?; + let it = fs::read_dir(dir_path) + .map_err(|err| { + warn!( + "Failed to list cache directory, path: {}, err: {}", + dir_path.display(), + err + ) + }) + .ok()?; + + // look for existing locks + for entry in it { + let entry = entry + .map_err(|err| { + warn!( + "Failed to list cache directory, path: {}, err: {}", + dir_path.display(), + err + ) + }) + .ok()?; + + let path = entry.path(); + if path.is_dir() || path.file_stem() != task_path.file_stem() { + continue; + } + + // check extension and mtime + match path.extension() { + None => continue, + Some(ext) => { + if let Some(ext_str) = ext.to_str() { + // if it's None, i.e. not valid UTF-8 string, then that's not our lock for sure + if ext_str.starts_with("wip-") + && !is_fs_lock_expired(Some(&entry), &path, timeout, allowed_future_drift) + { + return None; + } + } + } + } + } + + // create the lock + let lock_path = task_path.with_extension(format!("wip-{}", std::process::id())); + let _file = fs::OpenOptions::new() + .create_new(true) + .write(true) + .open(&lock_path) + .map_err(|err| { + warn!( + "Failed to create lock file (note: it shouldn't exists): path: {}, err: {}", + lock_path.display(), + err + ) + }) + .ok()?; + + Some(lock_path) +} + +// we have either both, or just path; dir entry is desirable since on some platforms we can get +// metadata without extra syscalls +// futhermore: it's better to get a path if we have it instead of allocating a new one from the dir entry +fn is_fs_lock_expired( + entry: Option<&fs::DirEntry>, + path: &PathBuf, + threshold: Duration, + allowed_future_drift: Duration, +) -> bool { + let mtime = match entry + .map_or_else(|| path.metadata(), |e| e.metadata()) + .and_then(|metadata| metadata.modified()) + { + Ok(mt) => mt, + Err(err) => { + warn!( + "Failed to get metadata/mtime, treating as an expired lock, path: {}, err: {}", + path.display(), + err + ); + return true; // can't read mtime, treat as expired, so this task will not be starved + } + }; + + // DON'T use: mtime.elapsed() -- we must call SystemTime directly for the tests to be deterministic + match SystemTime::now().duration_since(mtime) { + Ok(elapsed) => elapsed >= threshold, + Err(err) => { + trace!( + "Found mtime in the future, treating as a not expired lock, path: {}, err: {}", + path.display(), + err + ); + // the lock is expired if the time is too far in the future + // it is fine to have network share and not synchronized clocks, + // but it's not good when user changes time in their system clock + err.duration() > allowed_future_drift + } + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/environ/src/cache/worker/tests.rs b/crates/environ/src/cache/worker/tests.rs new file mode 100644 index 0000000000..43f0c0671a --- /dev/null +++ b/crates/environ/src/cache/worker/tests.rs @@ -0,0 +1,768 @@ +use super::*; +use crate::cache::config::tests::test_prolog; +use more_asserts::{assert_ge, assert_gt, assert_lt}; +use std::iter::repeat; +use std::process; +// load_config! comes from crate::cache(::config::tests); + +// when doing anything with the tests, make sure they are DETERMINISTIC +// -- the result shouldn't rely on system time! +pub mod system_time_stub; + +#[test] +fn test_on_get_create_stats_file() { + let (_tempdir, cache_dir, config_path) = test_prolog(); + let cache_config = load_config!( + config_path, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}", + cache_dir + ); + assert!(cache_config.enabled()); + let worker = Worker::start_new(&cache_config, None); + + let mod_file = cache_dir.join("some-mod"); + worker.on_cache_get_async(mod_file); + worker.wait_for_all_events_handled(); + assert_eq!(worker.events_dropped(), 0); + + let stats_file = cache_dir.join("some-mod.stats"); + let stats = read_stats_file(&stats_file).expect("Failed to read stats file"); + assert_eq!(stats.usages, 1); + assert_eq!( + stats.compression_level, + cache_config.baseline_compression_level() + ); +} + +#[test] +fn test_on_get_update_usage_counter() { + let (_tempdir, cache_dir, config_path) = test_prolog(); + let cache_config = load_config!( + config_path, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + worker-event-queue-size = '16'", + cache_dir + ); + assert!(cache_config.enabled()); + let worker = Worker::start_new(&cache_config, None); + + let mod_file = cache_dir.join("some-mod"); + let stats_file = cache_dir.join("some-mod.stats"); + let default_stats = ModuleCacheStatistics::default(&cache_config); + assert!(write_stats_file(&stats_file, &default_stats)); + + let mut usages = 0; + for times_used in &[4, 7, 2] { + for _ in 0..*times_used { + worker.on_cache_get_async(mod_file.clone()); + usages += 1; + } + + worker.wait_for_all_events_handled(); + assert_eq!(worker.events_dropped(), 0); + + let stats = read_stats_file(&stats_file).expect("Failed to read stats file"); + assert_eq!(stats.usages, usages); + } +} + +#[test] +fn test_on_get_recompress_no_mod_file() { + let (_tempdir, cache_dir, config_path) = test_prolog(); + let cache_config = load_config!( + config_path, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + worker-event-queue-size = '16'\n\ + baseline-compression-level = 3\n\ + optimized-compression-level = 7\n\ + optimized-compression-usage-counter-threshold = '256'", + cache_dir + ); + assert!(cache_config.enabled()); + let worker = Worker::start_new(&cache_config, None); + + let mod_file = cache_dir.join("some-mod"); + let stats_file = cache_dir.join("some-mod.stats"); + let mut start_stats = ModuleCacheStatistics::default(&cache_config); + start_stats.usages = 250; + assert!(write_stats_file(&stats_file, &start_stats)); + + let mut usages = start_stats.usages; + for times_used in &[4, 7, 2] { + for _ in 0..*times_used { + worker.on_cache_get_async(mod_file.clone()); + usages += 1; + } + + worker.wait_for_all_events_handled(); + assert_eq!(worker.events_dropped(), 0); + + let stats = read_stats_file(&stats_file).expect("Failed to read stats file"); + assert_eq!(stats.usages, usages); + assert_eq!( + stats.compression_level, + cache_config.baseline_compression_level() + ); + } +} + +#[test] +fn test_on_get_recompress_with_mod_file() { + let (_tempdir, cache_dir, config_path) = test_prolog(); + let cache_config = load_config!( + config_path, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + worker-event-queue-size = '16'\n\ + baseline-compression-level = 3\n\ + optimized-compression-level = 7\n\ + optimized-compression-usage-counter-threshold = '256'", + cache_dir + ); + assert!(cache_config.enabled()); + let worker = Worker::start_new(&cache_config, None); + + let mod_file = cache_dir.join("some-mod"); + let mod_data = "some test data to be compressed"; + let data = zstd::encode_all( + mod_data.as_bytes(), + cache_config.baseline_compression_level(), + ) + .expect("Failed to compress sample mod file"); + fs::write(&mod_file, &data).expect("Failed to write sample mod file"); + + let stats_file = cache_dir.join("some-mod.stats"); + let mut start_stats = ModuleCacheStatistics::default(&cache_config); + start_stats.usages = 250; + assert!(write_stats_file(&stats_file, &start_stats)); + + // scenarios: + // 1. Shouldn't be recompressed + // 2. Should be recompressed + // 3. After lowering compression level, should be recompressed + let scenarios = [(4, false), (7, true), (2, false)]; + + let mut usages = start_stats.usages; + assert_lt!( + usages, + cache_config.optimized_compression_usage_counter_threshold() + ); + let mut tested_higher_opt_compr_lvl = false; + for (times_used, lower_compr_lvl) in &scenarios { + for _ in 0..*times_used { + worker.on_cache_get_async(mod_file.clone()); + usages += 1; + } + + worker.wait_for_all_events_handled(); + assert_eq!(worker.events_dropped(), 0); + + let mut stats = read_stats_file(&stats_file).expect("Failed to read stats file"); + assert_eq!(stats.usages, usages); + assert_eq!( + stats.compression_level, + if usages < cache_config.optimized_compression_usage_counter_threshold() { + cache_config.baseline_compression_level() + } else { + cache_config.optimized_compression_level() + } + ); + let compressed_data = fs::read(&mod_file).expect("Failed to read mod file"); + let decoded_data = + zstd::decode_all(&compressed_data[..]).expect("Failed to decompress mod file"); + assert_eq!(decoded_data, mod_data.as_bytes()); + + if *lower_compr_lvl { + assert_ge!( + usages, + cache_config.optimized_compression_usage_counter_threshold() + ); + tested_higher_opt_compr_lvl = true; + stats.compression_level -= 1; + assert!(write_stats_file(&stats_file, &stats)); + } + } + assert_ge!( + usages, + cache_config.optimized_compression_usage_counter_threshold() + ); + assert!(tested_higher_opt_compr_lvl); +} + +#[test] +fn test_on_get_recompress_lock() { + let (_tempdir, cache_dir, config_path) = test_prolog(); + let cache_config = load_config!( + config_path, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + worker-event-queue-size = '16'\n\ + baseline-compression-level = 3\n\ + optimized-compression-level = 7\n\ + optimized-compression-usage-counter-threshold = '256'\n\ + optimizing-compression-task-timeout = '30m'\n\ + allowed-clock-drift-for-files-from-future = '1d'", + cache_dir + ); + assert!(cache_config.enabled()); + let worker = Worker::start_new(&cache_config, None); + + let mod_file = cache_dir.join("some-mod"); + let mod_data = "some test data to be compressed"; + let data = zstd::encode_all( + mod_data.as_bytes(), + cache_config.baseline_compression_level(), + ) + .expect("Failed to compress sample mod file"); + fs::write(&mod_file, &data).expect("Failed to write sample mod file"); + + let stats_file = cache_dir.join("some-mod.stats"); + let mut start_stats = ModuleCacheStatistics::default(&cache_config); + start_stats.usages = 255; + + let lock_file = cache_dir.join("some-mod.wip-lock"); + + let scenarios = [ + // valid lock + (true, "past", Duration::from_secs(30 * 60 - 1)), + // valid future lock + (true, "future", Duration::from_secs(24 * 60 * 60)), + // expired lock + (false, "past", Duration::from_secs(30 * 60)), + // expired future lock + (false, "future", Duration::from_secs(24 * 60 * 60 + 1)), + ]; + + for (lock_valid, duration_sign, duration) in &scenarios { + assert!(write_stats_file(&stats_file, &start_stats)); // restore usage & compression level + create_file_with_mtime(&lock_file, "", duration_sign, &duration); + + worker.on_cache_get_async(mod_file.clone()); + worker.wait_for_all_events_handled(); + assert_eq!(worker.events_dropped(), 0); + + let stats = read_stats_file(&stats_file).expect("Failed to read stats file"); + assert_eq!(stats.usages, start_stats.usages + 1); + assert_eq!( + stats.compression_level, + if *lock_valid { + cache_config.baseline_compression_level() + } else { + cache_config.optimized_compression_level() + } + ); + let compressed_data = fs::read(&mod_file).expect("Failed to read mod file"); + let decoded_data = + zstd::decode_all(&compressed_data[..]).expect("Failed to decompress mod file"); + assert_eq!(decoded_data, mod_data.as_bytes()); + } +} + +#[test] +fn test_on_update_fresh_stats_file() { + let (_tempdir, cache_dir, config_path) = test_prolog(); + let cache_config = load_config!( + config_path, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + worker-event-queue-size = '16'\n\ + baseline-compression-level = 3\n\ + optimized-compression-level = 7\n\ + cleanup-interval = '1h'", + cache_dir + ); + assert!(cache_config.enabled()); + let worker = Worker::start_new(&cache_config, None); + + let mod_file = cache_dir.join("some-mod"); + let stats_file = cache_dir.join("some-mod.stats"); + let cleanup_certificate = cache_dir.join(".cleanup.wip-done"); + create_file_with_mtime(&cleanup_certificate, "", "future", &Duration::from_secs(0)); + // the below created by the worker if it cleans up + let worker_lock_file = cache_dir.join(format!(".cleanup.wip-{}", process::id())); + + // scenarios: + // 1. Create new stats file + // 2. Overwrite existing file + for update_file in &[true, false] { + worker.on_cache_update_async(mod_file.clone()); + worker.wait_for_all_events_handled(); + assert_eq!(worker.events_dropped(), 0); + + let mut stats = read_stats_file(&stats_file).expect("Failed to read stats file"); + assert_eq!(stats.usages, 1); + assert_eq!( + stats.compression_level, + cache_config.baseline_compression_level() + ); + + if *update_file { + stats.usages += 42; + stats.compression_level += 1; + assert!(write_stats_file(&stats_file, &stats)); + } + + assert!(!worker_lock_file.exists()); + } +} + +#[test] +fn test_on_update_cleanup_limits_trash_locks() { + let (_tempdir, cache_dir, config_path) = test_prolog(); + let cache_config = load_config!( + config_path, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + worker-event-queue-size = '16'\n\ + cleanup-interval = '30m'\n\ + optimizing-compression-task-timeout = '30m'\n\ + allowed-clock-drift-for-files-from-future = '1d'\n\ + file-count-soft-limit = '5'\n\ + files-total-size-soft-limit = '30K'\n\ + file-count-limit-percent-if-deleting = '70%'\n\ + files-total-size-limit-percent-if-deleting = '70%' + ", + cache_dir + ); + assert!(cache_config.enabled()); + let worker = Worker::start_new(&cache_config, None); + let content_1k = "a".repeat(1_000); + let content_10k = "a".repeat(10_000); + + let mods_files_dir = cache_dir.join("target-triple").join("compiler-version"); + let mod_with_stats = mods_files_dir.join("mod-with-stats"); + let trash_dirs = [ + mods_files_dir.join("trash"), + mods_files_dir.join("trash").join("trash"), + ]; + let trash_files = [ + cache_dir.join("trash-file"), + cache_dir.join("trash-file.wip-lock"), + cache_dir.join("target-triple").join("trash.txt"), + cache_dir.join("target-triple").join("trash.txt.wip-lock"), + mods_files_dir.join("trash.ogg"), + mods_files_dir.join("trash").join("trash.doc"), + mods_files_dir.join("trash").join("trash.doc.wip-lock"), + mods_files_dir.join("trash").join("trash").join("trash.xls"), + mods_files_dir + .join("trash") + .join("trash") + .join("trash.xls.wip-lock"), + ]; + let mod_locks = [ + // valid lock + ( + mods_files_dir.join("mod0.wip-lock"), + true, + "past", + Duration::from_secs(30 * 60 - 1), + ), + // valid future lock + ( + mods_files_dir.join("mod1.wip-lock"), + true, + "future", + Duration::from_secs(24 * 60 * 60), + ), + // expired lock + ( + mods_files_dir.join("mod2.wip-lock"), + false, + "past", + Duration::from_secs(30 * 60), + ), + // expired future lock + ( + mods_files_dir.join("mod3.wip-lock"), + false, + "future", + Duration::from_secs(24 * 60 * 60 + 1), + ), + ]; + // the below created by the worker if it cleans up + let worker_lock_file = cache_dir.join(format!(".cleanup.wip-{}", process::id())); + + let scenarios = [ + // Close to limits, but not reached, only trash deleted + (2, 2, 4), + // File count limit exceeded + (1, 10, 3), + // Total size limit exceeded + (4, 0, 2), + // Both limits exceeded + (3, 5, 3), + ]; + + for (files_10k, files_1k, remaining_files) in &scenarios { + let mut secs_ago = 100; + + for d in &trash_dirs { + fs::create_dir_all(d).expect("Failed to create directories"); + } + for f in &trash_files { + create_file_with_mtime(f, "", "past", &Duration::from_secs(0)); + } + for (f, _, sign, duration) in &mod_locks { + create_file_with_mtime(f, "", sign, &duration); + } + + let mut mods_paths = vec![]; + for content in repeat(&content_10k) + .take(*files_10k) + .chain(repeat(&content_1k).take(*files_1k)) + { + mods_paths.push(mods_files_dir.join(format!("test-mod-{}", mods_paths.len()))); + create_file_with_mtime( + mods_paths.last().unwrap(), + content, + "past", + &Duration::from_secs(secs_ago), + ); + assert_gt!(secs_ago, 0); + secs_ago -= 1; + } + + // creating .stats file updates mtime what affects test results + // so we use a separate nonexistent module here (orphaned .stats will be removed anyway) + worker.on_cache_update_async(mod_with_stats.clone()); + worker.wait_for_all_events_handled(); + assert_eq!(worker.events_dropped(), 0); + + for ent in trash_dirs.iter().chain(trash_files.iter()) { + assert!(!ent.exists()); + } + for (f, valid, ..) in &mod_locks { + assert_eq!(f.exists(), *valid); + } + for (idx, path) in mods_paths.iter().enumerate() { + let should_exist = idx >= mods_paths.len() - *remaining_files; + assert_eq!(path.exists(), should_exist); + if should_exist { + // cleanup before next iteration + fs::remove_file(path).expect("Failed to remove a file"); + } + } + fs::remove_file(&worker_lock_file).expect("Failed to remove lock file"); + } +} + +#[test] +fn test_on_update_cleanup_lru_policy() { + let (_tempdir, cache_dir, config_path) = test_prolog(); + let cache_config = load_config!( + config_path, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + worker-event-queue-size = '16'\n\ + file-count-soft-limit = '5'\n\ + files-total-size-soft-limit = '30K'\n\ + file-count-limit-percent-if-deleting = '80%'\n\ + files-total-size-limit-percent-if-deleting = '70%'", + cache_dir + ); + assert!(cache_config.enabled()); + let worker = Worker::start_new(&cache_config, None); + let content_1k = "a".repeat(1_000); + let content_5k = "a".repeat(5_000); + let content_10k = "a".repeat(10_000); + + let mods_files_dir = cache_dir.join("target-triple").join("compiler-version"); + fs::create_dir_all(&mods_files_dir).expect("Failed to create directories"); + let nonexistent_mod_file = cache_dir.join("nonexistent-mod"); + let orphaned_stats_file = cache_dir.join("orphaned-mod.stats"); + let worker_lock_file = cache_dir.join(format!(".cleanup.wip-{}", process::id())); + + // content, how long ago created, how long ago stats created (if created), should be alive + let scenarios = [ + &[ + (&content_10k, 29, None, false), + (&content_10k, 28, None, false), + (&content_10k, 27, None, false), + (&content_1k, 26, None, true), + (&content_10k, 25, None, true), + (&content_1k, 24, None, true), + ], + &[ + (&content_10k, 29, None, false), + (&content_10k, 28, None, false), + (&content_10k, 27, None, true), + (&content_1k, 26, None, true), + (&content_5k, 25, None, true), + (&content_1k, 24, None, true), + ], + &[ + (&content_10k, 29, Some(19), true), + (&content_10k, 28, None, false), + (&content_10k, 27, None, false), + (&content_1k, 26, Some(18), true), + (&content_5k, 25, None, true), + (&content_1k, 24, None, true), + ], + &[ + (&content_10k, 29, Some(19), true), + (&content_10k, 28, Some(18), true), + (&content_10k, 27, None, false), + (&content_1k, 26, Some(17), true), + (&content_5k, 25, None, false), + (&content_1k, 24, None, false), + ], + &[ + (&content_10k, 29, Some(19), true), + (&content_10k, 28, None, false), + (&content_1k, 27, None, false), + (&content_5k, 26, Some(18), true), + (&content_1k, 25, None, false), + (&content_10k, 24, None, false), + ], + ]; + + for mods in &scenarios { + let filenames = (0..mods.len()) + .map(|i| { + ( + mods_files_dir.join(format!("mod-{}", i)), + mods_files_dir.join(format!("mod-{}.stats", i)), + ) + }) + .collect::>(); + + for ((content, mod_secs_ago, create_stats, _), (mod_filename, stats_filename)) in + mods.iter().zip(filenames.iter()) + { + create_file_with_mtime( + mod_filename, + content, + "past", + &Duration::from_secs(*mod_secs_ago), + ); + if let Some(stats_secs_ago) = create_stats { + create_file_with_mtime( + stats_filename, + "cleanup doesn't care", + "past", + &Duration::from_secs(*stats_secs_ago), + ); + } + } + create_file_with_mtime( + &orphaned_stats_file, + "cleanup doesn't care", + "past", + &Duration::from_secs(0), + ); + + worker.on_cache_update_async(nonexistent_mod_file.clone()); + worker.wait_for_all_events_handled(); + assert_eq!(worker.events_dropped(), 0); + + assert!(!orphaned_stats_file.exists()); + for ((_, _, create_stats, alive), (mod_filename, stats_filename)) in + mods.iter().zip(filenames.iter()) + { + assert_eq!(mod_filename.exists(), *alive); + assert_eq!(stats_filename.exists(), *alive && create_stats.is_some()); + + // cleanup for next iteration + if *alive { + fs::remove_file(&mod_filename).expect("Failed to remove a file"); + if create_stats.is_some() { + fs::remove_file(&stats_filename).expect("Failed to remove a file"); + } + } + } + + fs::remove_file(&worker_lock_file).expect("Failed to remove lock file"); + } +} + +// clock drift should be applied to mod cache & stats, too +// however, postpone deleting files to as late as possible +#[test] +fn test_on_update_cleanup_future_files() { + let (_tempdir, cache_dir, config_path) = test_prolog(); + let cache_config = load_config!( + config_path, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + worker-event-queue-size = '16'\n\ + allowed-clock-drift-for-files-from-future = '1d'\n\ + file-count-soft-limit = '3'\n\ + files-total-size-soft-limit = '1M'\n\ + file-count-limit-percent-if-deleting = '70%'\n\ + files-total-size-limit-percent-if-deleting = '70%'", + cache_dir + ); + assert!(cache_config.enabled()); + let worker = Worker::start_new(&cache_config, None); + let content_1k = "a".repeat(1_000); + + let mods_files_dir = cache_dir.join("target-triple").join("compiler-version"); + fs::create_dir_all(&mods_files_dir).expect("Failed to create directories"); + let nonexistent_mod_file = cache_dir.join("nonexistent-mod"); + // the below created by the worker if it cleans up + let worker_lock_file = cache_dir.join(format!(".cleanup.wip-{}", process::id())); + + let scenarios: [&[_]; 5] = [ + // NOT cleaning up, everythings ok + &[ + (Duration::from_secs(0), None, true), + (Duration::from_secs(24 * 60 * 60), None, true), + ], + // NOT cleaning up, everythings ok + &[ + (Duration::from_secs(0), None, true), + (Duration::from_secs(24 * 60 * 60 + 1), None, true), + ], + // cleaning up, removing files from oldest + &[ + (Duration::from_secs(0), None, false), + (Duration::from_secs(24 * 60 * 60), None, true), + (Duration::from_secs(1), None, false), + (Duration::from_secs(2), None, true), + ], + // cleaning up, removing files from oldest; deleting file from far future + &[ + (Duration::from_secs(0), None, false), + (Duration::from_secs(1), None, true), + (Duration::from_secs(24 * 60 * 60 + 1), None, false), + (Duration::from_secs(2), None, true), + ], + // cleaning up, removing files from oldest; file from far future should have .stats from +-now => it's a legitimate file + &[ + (Duration::from_secs(0), None, false), + (Duration::from_secs(1), None, false), + ( + Duration::from_secs(24 * 60 * 60 + 1), + Some(Duration::from_secs(3)), + true, + ), + (Duration::from_secs(2), None, true), + ], + ]; + + for mods in &scenarios { + let filenames = (0..mods.len()) + .map(|i| { + ( + mods_files_dir.join(format!("mod-{}", i)), + mods_files_dir.join(format!("mod-{}.stats", i)), + ) + }) + .collect::>(); + + for ((duration, opt_stats_duration, _), (mod_filename, stats_filename)) in + mods.iter().zip(filenames.iter()) + { + create_file_with_mtime(mod_filename, &content_1k, "future", duration); + if let Some(stats_duration) = opt_stats_duration { + create_file_with_mtime(stats_filename, "", "future", stats_duration); + } + } + + worker.on_cache_update_async(nonexistent_mod_file.clone()); + worker.wait_for_all_events_handled(); + assert_eq!(worker.events_dropped(), 0); + + for ((_, opt_stats_duration, alive), (mod_filename, stats_filename)) in + mods.iter().zip(filenames.iter()) + { + assert_eq!(mod_filename.exists(), *alive); + assert_eq!( + stats_filename.exists(), + *alive && opt_stats_duration.is_some() + ); + if *alive { + fs::remove_file(mod_filename).expect("Failed to remove a file"); + if opt_stats_duration.is_some() { + fs::remove_file(stats_filename).expect("Failed to remove a file"); + } + } + } + + fs::remove_file(&worker_lock_file).expect("Failed to remove lock file"); + } +} + +// this tests if worker triggered cleanup or not when some cleanup lock/certificate was out there +#[test] +fn test_on_update_cleanup_self_lock() { + let (_tempdir, cache_dir, config_path) = test_prolog(); + let cache_config = load_config!( + config_path, + "[cache]\n\ + enabled = true\n\ + directory = {cache_dir}\n\ + worker-event-queue-size = '16'\n\ + cleanup-interval = '30m'\n\ + allowed-clock-drift-for-files-from-future = '1d'", + cache_dir + ); + assert!(cache_config.enabled()); + let worker = Worker::start_new(&cache_config, None); + + let mod_file = cache_dir.join("some-mod"); + let trash_file = cache_dir.join("trash-file.txt"); + + let lock_file = cache_dir.join(".cleanup.wip-lock"); + // the below created by the worker if it cleans up + let worker_lock_file = cache_dir.join(format!(".cleanup.wip-{}", process::id())); + + let scenarios = [ + // valid lock + (true, "past", Duration::from_secs(30 * 60 - 1)), + // valid future lock + (true, "future", Duration::from_secs(24 * 60 * 60)), + // expired lock + (false, "past", Duration::from_secs(30 * 60)), + // expired future lock + (false, "future", Duration::from_secs(24 * 60 * 60 + 1)), + ]; + + for (lock_valid, duration_sign, duration) in &scenarios { + create_file_with_mtime( + &trash_file, + "with trash content", + "future", + &Duration::from_secs(0), + ); + create_file_with_mtime(&lock_file, "", duration_sign, &duration); + + worker.on_cache_update_async(mod_file.clone()); + worker.wait_for_all_events_handled(); + assert_eq!(worker.events_dropped(), 0); + + assert_eq!(trash_file.exists(), *lock_valid); + assert_eq!(lock_file.exists(), *lock_valid); + if *lock_valid { + assert!(!worker_lock_file.exists()); + } else { + fs::remove_file(&worker_lock_file).expect("Failed to remove lock file"); + } + } +} + +fn create_file_with_mtime(filename: &Path, contents: &str, offset_sign: &str, offset: &Duration) { + fs::write(filename, contents).expect("Failed to create a file"); + let mtime = match offset_sign { + "past" => system_time_stub::NOW + .checked_sub(*offset) + .expect("Failed to calculate new mtime"), + "future" => system_time_stub::NOW + .checked_add(*offset) + .expect("Failed to calculate new mtime"), + _ => unreachable!(), + }; + filetime::set_file_mtime(filename, mtime.into()).expect("Failed to set mtime"); +} diff --git a/crates/environ/src/cache/worker/tests/system_time_stub.rs b/crates/environ/src/cache/worker/tests/system_time_stub.rs new file mode 100644 index 0000000000..5e457d63fc --- /dev/null +++ b/crates/environ/src/cache/worker/tests/system_time_stub.rs @@ -0,0 +1,29 @@ +use lazy_static::lazy_static; +use std::time::{Duration, SystemTime, SystemTimeError}; + +lazy_static! { + pub static ref NOW: SystemTime = SystemTime::now(); // no need for RefCell and set_now() for now +} + +#[derive(PartialOrd, PartialEq, Ord, Eq)] +pub struct SystemTimeStub(SystemTime); + +impl SystemTimeStub { + pub fn now() -> Self { + Self(*NOW) + } + + pub fn checked_add(&self, duration: Duration) -> Option { + self.0.checked_add(duration).map(|t| t.into()) + } + + pub fn duration_since(&self, earlier: SystemTime) -> Result { + self.0.duration_since(earlier) + } +} + +impl From for SystemTimeStub { + fn from(time: SystemTime) -> Self { + Self(time) + } +} diff --git a/crates/environ/src/compilation.rs b/crates/environ/src/compilation.rs new file mode 100644 index 0000000000..0a896316e1 --- /dev/null +++ b/crates/environ/src/compilation.rs @@ -0,0 +1,298 @@ +//! A `Compilation` contains the compiled function bodies for a WebAssembly +//! module. + +use crate::cache::ModuleCacheDataTupleType; +use crate::module; +use crate::module_environ::FunctionBodyData; +use crate::CacheConfig; +use cranelift_codegen::{binemit, ir, isa, Context}; +use cranelift_entity::PrimaryMap; +use cranelift_wasm::{DefinedFuncIndex, FuncIndex, ModuleTranslationState, WasmError}; +use serde::{Deserialize, Serialize}; +use std::ops::Range; +use thiserror::Error; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct FDERelocEntry(pub i64, pub usize, pub u8); + +/// Relocation entry for unwind info. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct CompiledFunctionUnwindInfoReloc { + /// Entry offest in the code block. + pub offset: u32, + /// Entry addend relative to the code block. + pub addend: u32, +} + +/// Compiled function unwind information. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub enum CompiledFunctionUnwindInfo { + /// No info. + None, + /// Windows UNWIND_INFO. + Windows(Vec), + /// Frame layout info. + FrameLayout(Vec, usize, Vec), +} + +impl CompiledFunctionUnwindInfo { + /// Constructs unwind info object. + pub fn new(isa: &dyn isa::TargetIsa, context: &Context) -> Self { + use cranelift_codegen::binemit::{ + FrameUnwindKind, FrameUnwindOffset, FrameUnwindSink, Reloc, + }; + use cranelift_codegen::isa::CallConv; + + struct Sink(Vec, usize, Vec); + impl FrameUnwindSink for Sink { + fn len(&self) -> FrameUnwindOffset { + self.0.len() + } + fn bytes(&mut self, b: &[u8]) { + self.0.extend_from_slice(b); + } + fn reserve(&mut self, len: usize) { + self.0.reserve(len) + } + fn reloc(&mut self, r: Reloc, off: FrameUnwindOffset) { + self.2.push(FDERelocEntry( + 0, + off, + match r { + Reloc::Abs4 => 4, + Reloc::Abs8 => 8, + _ => { + panic!("unexpected reloc type"); + } + }, + )) + } + fn set_entry_offset(&mut self, off: FrameUnwindOffset) { + self.1 = off; + } + } + + let kind = match context.func.signature.call_conv { + CallConv::SystemV | CallConv::Fast | CallConv::Cold => FrameUnwindKind::Libunwind, + CallConv::WindowsFastcall => FrameUnwindKind::Fastcall, + _ => { + return CompiledFunctionUnwindInfo::None; + } + }; + + let mut sink = Sink(Vec::new(), 0, Vec::new()); + context.emit_unwind_info(isa, kind, &mut sink); + + let Sink(data, offset, relocs) = sink; + if data.is_empty() { + return CompiledFunctionUnwindInfo::None; + } + + match kind { + FrameUnwindKind::Fastcall => CompiledFunctionUnwindInfo::Windows(data), + FrameUnwindKind::Libunwind => { + CompiledFunctionUnwindInfo::FrameLayout(data, offset, relocs) + } + } + } + + /// Retuns true is no unwind info data. + pub fn is_empty(&self) -> bool { + match self { + CompiledFunctionUnwindInfo::None => true, + CompiledFunctionUnwindInfo::Windows(d) => d.is_empty(), + CompiledFunctionUnwindInfo::FrameLayout(c, _, _) => c.is_empty(), + } + } + + /// Returns size of serilized unwind info. + pub fn len(&self) -> usize { + match self { + CompiledFunctionUnwindInfo::None => 0, + CompiledFunctionUnwindInfo::Windows(d) => d.len(), + CompiledFunctionUnwindInfo::FrameLayout(c, _, _) => c.len(), + } + } + + /// Serializes data into byte array. + pub fn serialize(&self, dest: &mut [u8], relocs: &mut Vec) { + match self { + CompiledFunctionUnwindInfo::None => (), + CompiledFunctionUnwindInfo::Windows(d) => { + dest.copy_from_slice(d); + } + CompiledFunctionUnwindInfo::FrameLayout(code, _fde_offset, r) => { + dest.copy_from_slice(code); + r.iter().for_each(move |r| { + assert_eq!(r.2, 8); + relocs.push(CompiledFunctionUnwindInfoReloc { + offset: r.1 as u32, + addend: r.0 as u32, + }) + }); + } + } + } +} + +/// Compiled function: machine code body, jump table offsets, and unwind information. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct CompiledFunction { + /// The function body. + pub body: Vec, + + /// The jump tables offsets (in the body). + pub jt_offsets: ir::JumpTableOffsets, + + /// The unwind information. + pub unwind_info: CompiledFunctionUnwindInfo, +} + +type Functions = PrimaryMap; + +/// The result of compiling a WebAssembly module's functions. +#[derive(Deserialize, Serialize, Debug, PartialEq, Eq)] +pub struct Compilation { + /// Compiled machine code for the function bodies. + functions: Functions, +} + +impl Compilation { + /// Creates a compilation artifact from a contiguous function buffer and a set of ranges + pub fn new(functions: Functions) -> Self { + Self { functions } + } + + /// Allocates the compilation result with the given function bodies. + pub fn from_buffer( + buffer: Vec, + functions: impl IntoIterator, ir::JumpTableOffsets, Range)>, + ) -> Self { + Self::new( + functions + .into_iter() + .map(|(body_range, jt_offsets, unwind_range)| CompiledFunction { + body: buffer[body_range].to_vec(), + jt_offsets, + unwind_info: CompiledFunctionUnwindInfo::Windows(buffer[unwind_range].to_vec()), + }) + .collect(), + ) + } + + /// Gets the bytes of a single function + pub fn get(&self, func: DefinedFuncIndex) -> &CompiledFunction { + &self.functions[func] + } + + /// Gets the number of functions defined. + pub fn len(&self) -> usize { + self.functions.len() + } + + /// Returns whether there are no functions defined. + pub fn is_empty(&self) -> bool { + self.functions.is_empty() + } + + /// Gets functions jump table offsets. + pub fn get_jt_offsets(&self) -> PrimaryMap { + self.functions + .iter() + .map(|(_, func)| func.jt_offsets.clone()) + .collect::>() + } +} + +impl<'a> IntoIterator for &'a Compilation { + type IntoIter = Iter<'a>; + type Item = ::Item; + + fn into_iter(self) -> Self::IntoIter { + Iter { + iterator: self.functions.iter(), + } + } +} + +pub struct Iter<'a> { + iterator: <&'a Functions as IntoIterator>::IntoIter, +} + +impl<'a> Iterator for Iter<'a> { + type Item = &'a CompiledFunction; + + fn next(&mut self) -> Option { + self.iterator.next().map(|(_, b)| b) + } +} + +/// A record of a relocation to perform. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct Relocation { + /// The relocation code. + pub reloc: binemit::Reloc, + /// Relocation target. + pub reloc_target: RelocationTarget, + /// The offset where to apply the relocation. + pub offset: binemit::CodeOffset, + /// The addend to add to the relocation value. + pub addend: binemit::Addend, +} + +/// Destination function. Can be either user function or some special one, like `memory.grow`. +#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq)] +pub enum RelocationTarget { + /// The user function index. + UserFunc(FuncIndex), + /// A compiler-generated libcall. + LibCall(ir::LibCall), + /// Jump table index. + JumpTable(FuncIndex, ir::JumpTable), +} + +/// Relocations to apply to function bodies. +pub type Relocations = PrimaryMap>; + +/// Information about trap. +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +pub struct TrapInformation { + /// The offset of the trapping instruction in native code. It is relative to the beginning of the function. + pub code_offset: binemit::CodeOffset, + /// Location of trapping instruction in WebAssembly binary module. + pub source_loc: ir::SourceLoc, + /// Code of the trap. + pub trap_code: ir::TrapCode, +} + +/// Information about traps associated with the functions where the traps are placed. +pub type Traps = PrimaryMap>; + +/// An error while compiling WebAssembly to machine code. +#[derive(Error, Debug)] +pub enum CompileError { + /// A wasm translation error occured. + #[error("WebAssembly translation error")] + Wasm(#[from] WasmError), + + /// A compilation error occured. + #[error("Compilation error: {0}")] + Codegen(String), + + /// A compilation error occured. + #[error("Debug info is not supported with this configuration")] + DebugInfoNotSupported, +} + +/// An implementation of a compiler from parsed WebAssembly module to native code. +pub trait Compiler { + /// Compile a parsed module with the given `TargetIsa`. + fn compile_module<'data, 'module>( + module: &'module module::Module, + module_translation: &ModuleTranslationState, + function_body_inputs: PrimaryMap>, + isa: &dyn isa::TargetIsa, + generate_debug_info: bool, + cache_config: &CacheConfig, + ) -> Result; +} diff --git a/crates/environ/src/cranelift.rs b/crates/environ/src/cranelift.rs new file mode 100644 index 0000000000..da1dbe151c --- /dev/null +++ b/crates/environ/src/cranelift.rs @@ -0,0 +1,351 @@ +//! Support for compiling with Cranelift. + +use crate::address_map::{FunctionAddressMap, InstructionAddressMap}; +use crate::cache::{ModuleCacheDataTupleType, ModuleCacheEntry}; +use crate::compilation::{ + Compilation, CompileError, CompiledFunction, CompiledFunctionUnwindInfo, Relocation, + RelocationTarget, TrapInformation, +}; +use crate::func_environ::{get_func_name, FuncEnvironment}; +use crate::module::{Module, ModuleLocal}; +use crate::module_environ::FunctionBodyData; +use crate::CacheConfig; +use cranelift_codegen::ir::{self, ExternalName}; +use cranelift_codegen::print_errors::pretty_error; +use cranelift_codegen::{binemit, isa, Context}; +use cranelift_entity::PrimaryMap; +use cranelift_wasm::{DefinedFuncIndex, FuncIndex, FuncTranslator, ModuleTranslationState}; +use rayon::prelude::{IntoParallelRefIterator, ParallelIterator}; +use std::hash::{Hash, Hasher}; + +/// Implementation of a relocation sink that just saves all the information for later +pub struct RelocSink { + /// Current function index. + func_index: FuncIndex, + + /// Relocations recorded for the function. + pub func_relocs: Vec, +} + +impl binemit::RelocSink for RelocSink { + fn reloc_block( + &mut self, + _offset: binemit::CodeOffset, + _reloc: binemit::Reloc, + _block_offset: binemit::CodeOffset, + ) { + // This should use the `offsets` field of `ir::Function`. + panic!("block headers not yet implemented"); + } + fn reloc_external( + &mut self, + offset: binemit::CodeOffset, + reloc: binemit::Reloc, + name: &ExternalName, + addend: binemit::Addend, + ) { + let reloc_target = if let ExternalName::User { namespace, index } = *name { + debug_assert_eq!(namespace, 0); + RelocationTarget::UserFunc(FuncIndex::from_u32(index)) + } else if let ExternalName::LibCall(libcall) = *name { + RelocationTarget::LibCall(libcall) + } else { + panic!("unrecognized external name") + }; + self.func_relocs.push(Relocation { + reloc, + reloc_target, + offset, + addend, + }); + } + + fn reloc_constant( + &mut self, + _code_offset: binemit::CodeOffset, + _reloc: binemit::Reloc, + _constant_offset: ir::ConstantOffset, + ) { + // Do nothing for now: cranelift emits constant data after the function code and also emits + // function code with correct relative offsets to the constant data. + } + + fn reloc_jt(&mut self, offset: binemit::CodeOffset, reloc: binemit::Reloc, jt: ir::JumpTable) { + self.func_relocs.push(Relocation { + reloc, + reloc_target: RelocationTarget::JumpTable(self.func_index, jt), + offset, + addend: 0, + }); + } +} + +impl RelocSink { + /// Return a new `RelocSink` instance. + pub fn new(func_index: FuncIndex) -> Self { + Self { + func_index, + func_relocs: Vec::new(), + } + } +} + +struct TrapSink { + pub traps: Vec, +} + +impl TrapSink { + fn new() -> Self { + Self { traps: Vec::new() } + } +} + +impl binemit::TrapSink for TrapSink { + fn trap( + &mut self, + code_offset: binemit::CodeOffset, + source_loc: ir::SourceLoc, + trap_code: ir::TrapCode, + ) { + self.traps.push(TrapInformation { + code_offset, + source_loc, + trap_code, + }); + } +} + +fn get_function_address_map<'data>( + context: &Context, + data: &FunctionBodyData<'data>, + body_len: usize, + isa: &dyn isa::TargetIsa, +) -> FunctionAddressMap { + let mut instructions = Vec::new(); + + let func = &context.func; + let mut blocks = func.layout.blocks().collect::>(); + blocks.sort_by_key(|block| func.offsets[*block]); // Ensure inst offsets always increase + + let encinfo = isa.encoding_info(); + for block in blocks { + for (offset, inst, size) in func.inst_offsets(block, &encinfo) { + let srcloc = func.srclocs[inst]; + instructions.push(InstructionAddressMap { + srcloc, + code_offset: offset as usize, + code_len: size as usize, + }); + } + } + + // Generate artificial srcloc for function start/end to identify boundary + // within module. Similar to FuncTranslator::cur_srcloc(): it will wrap around + // if byte code is larger than 4 GB. + let start_srcloc = ir::SourceLoc::new(data.module_offset as u32); + let end_srcloc = ir::SourceLoc::new((data.module_offset + data.data.len()) as u32); + + FunctionAddressMap { + instructions, + start_srcloc, + end_srcloc, + body_offset: 0, + body_len, + } +} + +/// A compiler that compiles a WebAssembly module with Cranelift, translating the Wasm to Cranelift IR, +/// optimizing it and then translating to assembly. +pub struct Cranelift; + +impl crate::compilation::Compiler for Cranelift { + /// Compile the module using Cranelift, producing a compilation result with + /// associated relocations. + fn compile_module( + module: &Module, + module_translation: &ModuleTranslationState, + function_body_inputs: PrimaryMap>, + isa: &dyn isa::TargetIsa, + generate_debug_info: bool, + cache_config: &CacheConfig, + ) -> Result { + let cache_entry = ModuleCacheEntry::new("cranelift", cache_config); + + let data = cache_entry.get_data( + ( + &module.local, + HashedModuleTranslationState(module_translation), + function_body_inputs, + Isa(isa), + generate_debug_info, + ), + compile, + )?; + Ok(data.into_tuple()) + } +} + +fn compile( + ( + module, + HashedModuleTranslationState(module_translation), + function_body_inputs, + Isa(isa), + generate_debug_info, + ): ( + &ModuleLocal, + HashedModuleTranslationState<'_>, + PrimaryMap>, + Isa<'_, '_>, + bool, + ), +) -> Result { + let mut functions = PrimaryMap::with_capacity(function_body_inputs.len()); + let mut relocations = PrimaryMap::with_capacity(function_body_inputs.len()); + let mut address_transforms = PrimaryMap::with_capacity(function_body_inputs.len()); + let mut value_ranges = PrimaryMap::with_capacity(function_body_inputs.len()); + let mut stack_slots = PrimaryMap::with_capacity(function_body_inputs.len()); + let mut traps = PrimaryMap::with_capacity(function_body_inputs.len()); + + function_body_inputs + .into_iter() + .collect::)>>() + .par_iter() + .map_init(FuncTranslator::new, |func_translator, (i, input)| { + let func_index = module.func_index(*i); + let mut context = Context::new(); + context.func.name = get_func_name(func_index); + context.func.signature = module.signatures[module.functions[func_index]].clone(); + context.func.collect_frame_layout_info(); + if generate_debug_info { + context.func.collect_debug_info(); + } + + func_translator.translate( + module_translation, + input.data, + input.module_offset, + &mut context.func, + &mut FuncEnvironment::new(isa.frontend_config(), module), + )?; + + let mut code_buf: Vec = Vec::new(); + let mut reloc_sink = RelocSink::new(func_index); + let mut trap_sink = TrapSink::new(); + let mut stackmap_sink = binemit::NullStackmapSink {}; + context + .compile_and_emit( + isa, + &mut code_buf, + &mut reloc_sink, + &mut trap_sink, + &mut stackmap_sink, + ) + .map_err(|error| { + CompileError::Codegen(pretty_error(&context.func, Some(isa), error)) + })?; + + let unwind_info = CompiledFunctionUnwindInfo::new(isa, &context); + + let address_transform = if generate_debug_info { + let body_len = code_buf.len(); + Some(get_function_address_map(&context, input, body_len, isa)) + } else { + None + }; + + let ranges = if generate_debug_info { + let ranges = context.build_value_labels_ranges(isa).map_err(|error| { + CompileError::Codegen(pretty_error(&context.func, Some(isa), error)) + })?; + Some(ranges) + } else { + None + }; + + Ok(( + code_buf, + context.func.jt_offsets, + reloc_sink.func_relocs, + address_transform, + ranges, + context.func.stack_slots, + trap_sink.traps, + unwind_info, + )) + }) + .collect::, CompileError>>()? + .into_iter() + .for_each( + |( + function, + func_jt_offsets, + relocs, + address_transform, + ranges, + sss, + function_traps, + unwind_info, + )| { + functions.push(CompiledFunction { + body: function, + jt_offsets: func_jt_offsets, + unwind_info, + }); + relocations.push(relocs); + if let Some(address_transform) = address_transform { + address_transforms.push(address_transform); + } + value_ranges.push(ranges.unwrap_or_default()); + stack_slots.push(sss); + traps.push(function_traps); + }, + ); + + // TODO: Reorganize where we create the Vec for the resolved imports. + + Ok(( + Compilation::new(functions), + relocations, + address_transforms, + value_ranges, + stack_slots, + traps, + )) +} + +/// This is a wrapper struct to hash the specific bits of `TargetIsa` that +/// affect the output we care about. The trait itself can't implement `Hash` +/// (it's not object safe) so we have to implement our own hashing. +struct Isa<'a, 'b>(&'a (dyn isa::TargetIsa + 'b)); + +impl Hash for Isa<'_, '_> { + fn hash(&self, hasher: &mut H) { + self.0.triple().hash(hasher); + + // TODO: if this `to_string()` is too expensive then we should upstream + // a native hashing ability of flags into cranelift itself, but + // compilation and/or cache loading is relatively expensive so seems + // unlikely. + self.0.flags().to_string().hash(hasher); + + // TODO: ... and should we hash anything else? There's a lot of stuff in + // `TargetIsa`, like registers/encodings/etc. Should we be hashing that + // too? It seems like wasmtime doesn't configure it too too much, but + // this may become an issue at some point. + } +} + +/// A wrapper struct around cranelift's `ModuleTranslationState` to implement +/// `Hash` since it's not `Hash` upstream yet. +/// +/// TODO: we should upstream a `Hash` implementation, it would be very small! At +/// this moment though based on the definition it should be fine to not hash it +/// since we'll re-hash the signatures later. +struct HashedModuleTranslationState<'a>(&'a ModuleTranslationState); + +impl Hash for HashedModuleTranslationState<'_> { + fn hash(&self, _hasher: &mut H) { + // nothing to hash right now + } +} diff --git a/crates/environ/src/data_structures.rs b/crates/environ/src/data_structures.rs new file mode 100755 index 0000000000..e12dee3ca7 --- /dev/null +++ b/crates/environ/src/data_structures.rs @@ -0,0 +1,29 @@ +#![doc(hidden)] + +pub mod ir { + pub use cranelift_codegen::ir::{ + types, AbiParam, ArgumentPurpose, Signature, SourceLoc, StackSlots, TrapCode, Type, + ValueLabel, ValueLoc, + }; + pub use cranelift_codegen::ValueLabelsRanges; +} + +pub mod settings { + pub use cranelift_codegen::settings::{builder, Builder, Configurable, Flags}; +} + +pub mod isa { + pub use cranelift_codegen::isa::{CallConv, RegUnit, TargetFrontendConfig, TargetIsa}; +} + +pub mod entity { + pub use cranelift_entity::{packed_option, BoxedSlice, EntityRef, PrimaryMap}; +} + +pub mod wasm { + pub use cranelift_wasm::{ + get_vmctx_value_label, DefinedFuncIndex, DefinedGlobalIndex, DefinedMemoryIndex, + DefinedTableIndex, FuncIndex, Global, GlobalIndex, GlobalInit, Memory, MemoryIndex, + PassiveElemIndex, SignatureIndex, Table, TableElementType, TableIndex, + }; +} diff --git a/crates/environ/src/func_environ.rs b/crates/environ/src/func_environ.rs new file mode 100644 index 0000000000..0626703b6e --- /dev/null +++ b/crates/environ/src/func_environ.rs @@ -0,0 +1,1198 @@ +use crate::module::{MemoryPlan, MemoryStyle, ModuleLocal, TableStyle}; +use crate::vmoffsets::VMOffsets; +use crate::WASM_PAGE_SIZE; +use cranelift_codegen::cursor::{Cursor, FuncCursor}; +use cranelift_codegen::ir; +use cranelift_codegen::ir::condcodes::*; +use cranelift_codegen::ir::immediates::{Offset32, Uimm64}; +use cranelift_codegen::ir::types::*; +use cranelift_codegen::ir::{AbiParam, ArgumentPurpose, Function, InstBuilder, Signature}; +use cranelift_codegen::isa::TargetFrontendConfig; +use cranelift_entity::EntityRef; +use cranelift_wasm::{ + self, FuncIndex, GlobalIndex, GlobalVariable, MemoryIndex, SignatureIndex, TableIndex, + TargetEnvironment, WasmError, WasmResult, +}; +#[cfg(feature = "lightbeam")] +use cranelift_wasm::{DefinedFuncIndex, DefinedGlobalIndex, DefinedMemoryIndex, DefinedTableIndex}; +use std::convert::TryFrom; + +/// Compute an `ir::ExternalName` for a given wasm function index. +pub fn get_func_name(func_index: FuncIndex) -> ir::ExternalName { + ir::ExternalName::user(0, func_index.as_u32()) +} + +/// An index type for builtin functions. +#[derive(Copy, Clone, Debug)] +pub struct BuiltinFunctionIndex(u32); + +impl BuiltinFunctionIndex { + /// Returns an index for wasm's `memory.grow` builtin function. + pub const fn get_memory32_grow_index() -> Self { + Self(0) + } + /// Returns an index for wasm's imported `memory.grow` builtin function. + pub const fn get_imported_memory32_grow_index() -> Self { + Self(1) + } + /// Returns an index for wasm's `memory.size` builtin function. + pub const fn get_memory32_size_index() -> Self { + Self(2) + } + /// Returns an index for wasm's imported `memory.size` builtin function. + pub const fn get_imported_memory32_size_index() -> Self { + Self(3) + } + /// Returns an index for wasm's `table.copy` when both tables are locally + /// defined. + pub const fn get_table_copy_index() -> Self { + Self(4) + } + /// Returns an index for wasm's `table.init`. + pub const fn get_table_init_index() -> Self { + Self(5) + } + /// Returns an index for wasm's `elem.drop`. + pub const fn get_elem_drop_index() -> Self { + Self(6) + } + /// Returns an index for wasm's `memory.copy` for locally defined memories. + pub const fn get_defined_memory_copy_index() -> Self { + Self(7) + } + /// Returns an index for wasm's `memory.copy` for imported memories. + pub const fn get_imported_memory_copy_index() -> Self { + Self(8) + } + /// Returns an index for wasm's `memory.fill` for locally defined memories. + pub const fn get_memory_fill_index() -> Self { + Self(9) + } + /// Returns an index for wasm's `memory.fill` for imported memories. + pub const fn get_imported_memory_fill_index() -> Self { + Self(10) + } + /// Returns the total number of builtin functions. + pub const fn builtin_functions_total_number() -> u32 { + 11 + } + + /// Return the index as an u32 number. + pub const fn index(&self) -> u32 { + self.0 + } +} + +/// The `FuncEnvironment` implementation for use by the `ModuleEnvironment`. +pub struct FuncEnvironment<'module_environment> { + /// Target-specified configuration. + target_config: TargetFrontendConfig, + + /// The module-level environment which this function-level environment belongs to. + module: &'module_environment ModuleLocal, + + /// The Cranelift global holding the vmctx address. + vmctx: Option, + + /// The external function signature for implementing wasm's `memory.size` + /// for locally-defined 32-bit memories. + memory32_size_sig: Option, + + /// The external function signature for implementing wasm's `memory.grow` + /// for locally-defined memories. + memory_grow_sig: Option, + + /// The external function signature for implementing wasm's `table.copy` + /// (it's the same for both local and imported tables). + table_copy_sig: Option, + + /// The external function signature for implementing wasm's `table.init`. + table_init_sig: Option, + + /// The external function signature for implementing wasm's `elem.drop`. + elem_drop_sig: Option, + + /// The external function signature for implementing wasm's `memory.copy` + /// (it's the same for both local and imported memories). + memory_copy_sig: Option, + + /// The external function signature for implementing wasm's `memory.fill` + /// (it's the same for both local and imported memories). + memory_fill_sig: Option, + + /// Offsets to struct fields accessed by JIT code. + offsets: VMOffsets, +} + +impl<'module_environment> FuncEnvironment<'module_environment> { + pub fn new( + target_config: TargetFrontendConfig, + module: &'module_environment ModuleLocal, + ) -> Self { + Self { + target_config, + module, + vmctx: None, + memory32_size_sig: None, + memory_grow_sig: None, + table_copy_sig: None, + table_init_sig: None, + elem_drop_sig: None, + memory_copy_sig: None, + memory_fill_sig: None, + offsets: VMOffsets::new(target_config.pointer_bytes(), module), + } + } + + fn pointer_type(&self) -> ir::Type { + self.target_config.pointer_type() + } + + fn vmctx(&mut self, func: &mut Function) -> ir::GlobalValue { + self.vmctx.unwrap_or_else(|| { + let vmctx = func.create_global_value(ir::GlobalValueData::VMContext); + self.vmctx = Some(vmctx); + vmctx + }) + } + + fn get_memory_grow_sig(&mut self, func: &mut Function) -> ir::SigRef { + let sig = self.memory_grow_sig.unwrap_or_else(|| { + func.import_signature(Signature { + params: vec![ + AbiParam::special(self.pointer_type(), ArgumentPurpose::VMContext), + AbiParam::new(I32), + AbiParam::new(I32), + ], + returns: vec![AbiParam::new(I32)], + call_conv: self.target_config.default_call_conv, + }) + }); + self.memory_grow_sig = Some(sig); + sig + } + + /// Return the memory.grow function signature to call for the given index, along with the + /// translated index value to pass to it and its index in `VMBuiltinFunctionsArray`. + fn get_memory_grow_func( + &mut self, + func: &mut Function, + index: MemoryIndex, + ) -> (ir::SigRef, usize, BuiltinFunctionIndex) { + if self.module.is_imported_memory(index) { + ( + self.get_memory_grow_sig(func), + index.index(), + BuiltinFunctionIndex::get_imported_memory32_grow_index(), + ) + } else { + ( + self.get_memory_grow_sig(func), + self.module.defined_memory_index(index).unwrap().index(), + BuiltinFunctionIndex::get_memory32_grow_index(), + ) + } + } + + fn get_memory32_size_sig(&mut self, func: &mut Function) -> ir::SigRef { + let sig = self.memory32_size_sig.unwrap_or_else(|| { + func.import_signature(Signature { + params: vec![ + AbiParam::special(self.pointer_type(), ArgumentPurpose::VMContext), + AbiParam::new(I32), + ], + returns: vec![AbiParam::new(I32)], + call_conv: self.target_config.default_call_conv, + }) + }); + self.memory32_size_sig = Some(sig); + sig + } + + /// Return the memory.size function signature to call for the given index, along with the + /// translated index value to pass to it and its index in `VMBuiltinFunctionsArray`. + fn get_memory_size_func( + &mut self, + func: &mut Function, + index: MemoryIndex, + ) -> (ir::SigRef, usize, BuiltinFunctionIndex) { + if self.module.is_imported_memory(index) { + ( + self.get_memory32_size_sig(func), + index.index(), + BuiltinFunctionIndex::get_imported_memory32_size_index(), + ) + } else { + ( + self.get_memory32_size_sig(func), + self.module.defined_memory_index(index).unwrap().index(), + BuiltinFunctionIndex::get_memory32_size_index(), + ) + } + } + + fn get_table_copy_sig(&mut self, func: &mut Function) -> ir::SigRef { + let sig = self.table_copy_sig.unwrap_or_else(|| { + func.import_signature(Signature { + params: vec![ + AbiParam::special(self.pointer_type(), ArgumentPurpose::VMContext), + // Destination table index. + AbiParam::new(I32), + // Source table index. + AbiParam::new(I32), + // Index within destination table. + AbiParam::new(I32), + // Index within source table. + AbiParam::new(I32), + // Number of elements to copy. + AbiParam::new(I32), + // Source location. + AbiParam::new(I32), + ], + returns: vec![], + call_conv: self.target_config.default_call_conv, + }) + }); + self.table_copy_sig = Some(sig); + sig + } + + fn get_table_copy_func( + &mut self, + func: &mut Function, + dst_table_index: TableIndex, + src_table_index: TableIndex, + ) -> (ir::SigRef, usize, usize, BuiltinFunctionIndex) { + let sig = self.get_table_copy_sig(func); + ( + sig, + dst_table_index.as_u32() as usize, + src_table_index.as_u32() as usize, + BuiltinFunctionIndex::get_table_copy_index(), + ) + } + + fn get_table_init_sig(&mut self, func: &mut Function) -> ir::SigRef { + let sig = self.table_init_sig.unwrap_or_else(|| { + func.import_signature(Signature { + params: vec![ + AbiParam::special(self.pointer_type(), ArgumentPurpose::VMContext), + // Table index. + AbiParam::new(I32), + // Segment index. + AbiParam::new(I32), + // Destination index within table. + AbiParam::new(I32), + // Source index within segment. + AbiParam::new(I32), + // Number of elements to initialize. + AbiParam::new(I32), + // Source location. + AbiParam::new(I32), + ], + returns: vec![], + call_conv: self.target_config.default_call_conv, + }) + }); + self.table_init_sig = Some(sig); + sig + } + + fn get_table_init_func( + &mut self, + func: &mut Function, + table_index: TableIndex, + ) -> (ir::SigRef, usize, BuiltinFunctionIndex) { + let sig = self.get_table_init_sig(func); + let table_index = table_index.as_u32() as usize; + ( + sig, + table_index, + BuiltinFunctionIndex::get_table_init_index(), + ) + } + + fn get_elem_drop_sig(&mut self, func: &mut Function) -> ir::SigRef { + let sig = self.elem_drop_sig.unwrap_or_else(|| { + func.import_signature(Signature { + params: vec![ + AbiParam::special(self.pointer_type(), ArgumentPurpose::VMContext), + // Element index. + AbiParam::new(I32), + ], + returns: vec![], + call_conv: self.target_config.default_call_conv, + }) + }); + self.elem_drop_sig = Some(sig); + sig + } + + fn get_elem_drop_func(&mut self, func: &mut Function) -> (ir::SigRef, BuiltinFunctionIndex) { + let sig = self.get_elem_drop_sig(func); + (sig, BuiltinFunctionIndex::get_elem_drop_index()) + } + + fn get_memory_copy_sig(&mut self, func: &mut Function) -> ir::SigRef { + let sig = self.memory_copy_sig.unwrap_or_else(|| { + func.import_signature(Signature { + params: vec![ + AbiParam::special(self.pointer_type(), ArgumentPurpose::VMContext), + // Memory index. + AbiParam::new(I32), + // Destination address. + AbiParam::new(I32), + // Source address. + AbiParam::new(I32), + // Length. + AbiParam::new(I32), + // Source location. + AbiParam::new(I32), + ], + returns: vec![], + call_conv: self.target_config.default_call_conv, + }) + }); + self.memory_copy_sig = Some(sig); + sig + } + + fn get_memory_copy_func( + &mut self, + func: &mut Function, + memory_index: MemoryIndex, + ) -> (ir::SigRef, usize, BuiltinFunctionIndex) { + let sig = self.get_memory_copy_sig(func); + if let Some(defined_memory_index) = self.module.defined_memory_index(memory_index) { + ( + sig, + defined_memory_index.index(), + BuiltinFunctionIndex::get_defined_memory_copy_index(), + ) + } else { + ( + sig, + memory_index.index(), + BuiltinFunctionIndex::get_imported_memory_copy_index(), + ) + } + } + + fn get_memory_fill_sig(&mut self, func: &mut Function) -> ir::SigRef { + let sig = self.memory_fill_sig.unwrap_or_else(|| { + func.import_signature(Signature { + params: vec![ + AbiParam::special(self.pointer_type(), ArgumentPurpose::VMContext), + // Memory index. + AbiParam::new(I32), + // Destination address. + AbiParam::new(I32), + // Value. + AbiParam::new(I32), + // Length. + AbiParam::new(I32), + // Source location. + AbiParam::new(I32), + ], + returns: vec![], + call_conv: self.target_config.default_call_conv, + }) + }); + self.memory_fill_sig = Some(sig); + sig + } + + fn get_memory_fill_func( + &mut self, + func: &mut Function, + memory_index: MemoryIndex, + ) -> (ir::SigRef, usize, BuiltinFunctionIndex) { + let sig = self.get_memory_fill_sig(func); + if let Some(defined_memory_index) = self.module.defined_memory_index(memory_index) { + ( + sig, + defined_memory_index.index(), + BuiltinFunctionIndex::get_memory_fill_index(), + ) + } else { + ( + sig, + memory_index.index(), + BuiltinFunctionIndex::get_imported_memory_fill_index(), + ) + } + } + + /// Translates load of builtin function and returns a pair of values `vmctx` + /// and address of the loaded function. + fn translate_load_builtin_function_address( + &mut self, + pos: &mut FuncCursor<'_>, + callee_func_idx: BuiltinFunctionIndex, + ) -> (ir::Value, ir::Value) { + // We use an indirect call so that we don't have to patch the code at runtime. + let pointer_type = self.pointer_type(); + let vmctx = self.vmctx(&mut pos.func); + let base = pos.ins().global_value(pointer_type, vmctx); + + let mut mem_flags = ir::MemFlags::trusted(); + mem_flags.set_readonly(); + + // Load the callee address. + let body_offset = + i32::try_from(self.offsets.vmctx_builtin_function(callee_func_idx)).unwrap(); + let func_addr = pos.ins().load(pointer_type, mem_flags, base, body_offset); + + (base, func_addr) + } +} + +#[cfg(feature = "lightbeam")] +impl lightbeam::ModuleContext for FuncEnvironment<'_> { + type Signature = ir::Signature; + type GlobalType = ir::Type; + + fn func_index(&self, defined_func_index: u32) -> u32 { + self.module + .func_index(DefinedFuncIndex::from_u32(defined_func_index)) + .as_u32() + } + + fn defined_func_index(&self, func_index: u32) -> Option { + self.module + .defined_func_index(FuncIndex::from_u32(func_index)) + .map(DefinedFuncIndex::as_u32) + } + + fn defined_global_index(&self, global_index: u32) -> Option { + self.module + .defined_global_index(GlobalIndex::from_u32(global_index)) + .map(DefinedGlobalIndex::as_u32) + } + + fn global_type(&self, global_index: u32) -> &Self::GlobalType { + &self.module.globals[GlobalIndex::from_u32(global_index)].ty + } + + fn func_type_index(&self, func_idx: u32) -> u32 { + self.module.functions[FuncIndex::from_u32(func_idx)].as_u32() + } + + fn signature(&self, index: u32) -> &Self::Signature { + &self.module.signatures[SignatureIndex::from_u32(index)] + } + + fn defined_table_index(&self, table_index: u32) -> Option { + self.module + .defined_table_index(TableIndex::from_u32(table_index)) + .map(DefinedTableIndex::as_u32) + } + + fn defined_memory_index(&self, memory_index: u32) -> Option { + self.module + .defined_memory_index(MemoryIndex::from_u32(memory_index)) + .map(DefinedMemoryIndex::as_u32) + } + + fn vmctx_vmfunction_import_body(&self, func_index: u32) -> u32 { + self.offsets + .vmctx_vmfunction_import_body(FuncIndex::from_u32(func_index)) + } + fn vmctx_vmfunction_import_vmctx(&self, func_index: u32) -> u32 { + self.offsets + .vmctx_vmfunction_import_vmctx(FuncIndex::from_u32(func_index)) + } + + fn vmctx_vmglobal_import_from(&self, global_index: u32) -> u32 { + self.offsets + .vmctx_vmglobal_import_from(GlobalIndex::from_u32(global_index)) + } + fn vmctx_vmglobal_definition(&self, defined_global_index: u32) -> u32 { + self.offsets + .vmctx_vmglobal_definition(DefinedGlobalIndex::from_u32(defined_global_index)) + } + fn vmctx_vmmemory_import_from(&self, memory_index: u32) -> u32 { + self.offsets + .vmctx_vmmemory_import_from(MemoryIndex::from_u32(memory_index)) + } + fn vmctx_vmmemory_definition(&self, defined_memory_index: u32) -> u32 { + self.offsets + .vmctx_vmmemory_definition(DefinedMemoryIndex::from_u32(defined_memory_index)) + } + fn vmctx_vmmemory_definition_base(&self, defined_memory_index: u32) -> u32 { + self.offsets + .vmctx_vmmemory_definition_base(DefinedMemoryIndex::from_u32(defined_memory_index)) + } + fn vmctx_vmmemory_definition_current_length(&self, defined_memory_index: u32) -> u32 { + self.offsets + .vmctx_vmmemory_definition_current_length(DefinedMemoryIndex::from_u32( + defined_memory_index, + )) + } + fn vmmemory_definition_base(&self) -> u8 { + self.offsets.vmmemory_definition_base() + } + fn vmmemory_definition_current_length(&self) -> u8 { + self.offsets.vmmemory_definition_current_length() + } + fn vmctx_vmtable_import_from(&self, table_index: u32) -> u32 { + self.offsets + .vmctx_vmtable_import_from(TableIndex::from_u32(table_index)) + } + fn vmctx_vmtable_definition(&self, defined_table_index: u32) -> u32 { + self.offsets + .vmctx_vmtable_definition(DefinedTableIndex::from_u32(defined_table_index)) + } + fn vmctx_vmtable_definition_base(&self, defined_table_index: u32) -> u32 { + self.offsets + .vmctx_vmtable_definition_base(DefinedTableIndex::from_u32(defined_table_index)) + } + fn vmctx_vmtable_definition_current_elements(&self, defined_table_index: u32) -> u32 { + self.offsets + .vmctx_vmtable_definition_current_elements(DefinedTableIndex::from_u32( + defined_table_index, + )) + } + fn vmtable_definition_base(&self) -> u8 { + self.offsets.vmtable_definition_base() + } + fn vmtable_definition_current_elements(&self) -> u8 { + self.offsets.vmtable_definition_current_elements() + } + fn vmcaller_checked_anyfunc_type_index(&self) -> u8 { + self.offsets.vmcaller_checked_anyfunc_type_index() + } + fn vmcaller_checked_anyfunc_func_ptr(&self) -> u8 { + self.offsets.vmcaller_checked_anyfunc_func_ptr() + } + fn vmcaller_checked_anyfunc_vmctx(&self) -> u8 { + self.offsets.vmcaller_checked_anyfunc_vmctx() + } + fn size_of_vmcaller_checked_anyfunc(&self) -> u8 { + self.offsets.size_of_vmcaller_checked_anyfunc() + } + fn vmctx_vmshared_signature_id(&self, signature_idx: u32) -> u32 { + self.offsets + .vmctx_vmshared_signature_id(SignatureIndex::from_u32(signature_idx)) + } + + // TODO: type of a global +} + +impl<'module_environment> TargetEnvironment for FuncEnvironment<'module_environment> { + fn target_config(&self) -> TargetFrontendConfig { + self.target_config + } +} + +impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'module_environment> { + fn is_wasm_parameter(&self, _signature: &ir::Signature, index: usize) -> bool { + // The first two parameters are the vmctx and caller vmctx. The rest are + // the wasm parameters. + index >= 2 + } + + fn make_table(&mut self, func: &mut ir::Function, index: TableIndex) -> WasmResult { + let pointer_type = self.pointer_type(); + + let (ptr, base_offset, current_elements_offset) = { + let vmctx = self.vmctx(func); + if let Some(def_index) = self.module.defined_table_index(index) { + let base_offset = + i32::try_from(self.offsets.vmctx_vmtable_definition_base(def_index)).unwrap(); + let current_elements_offset = i32::try_from( + self.offsets + .vmctx_vmtable_definition_current_elements(def_index), + ) + .unwrap(); + (vmctx, base_offset, current_elements_offset) + } else { + let from_offset = self.offsets.vmctx_vmtable_import_from(index); + let table = func.create_global_value(ir::GlobalValueData::Load { + base: vmctx, + offset: Offset32::new(i32::try_from(from_offset).unwrap()), + global_type: pointer_type, + readonly: true, + }); + let base_offset = i32::from(self.offsets.vmtable_definition_base()); + let current_elements_offset = + i32::from(self.offsets.vmtable_definition_current_elements()); + (table, base_offset, current_elements_offset) + } + }; + + let base_gv = func.create_global_value(ir::GlobalValueData::Load { + base: ptr, + offset: Offset32::new(base_offset), + global_type: pointer_type, + readonly: false, + }); + let bound_gv = func.create_global_value(ir::GlobalValueData::Load { + base: ptr, + offset: Offset32::new(current_elements_offset), + global_type: self.offsets.type_of_vmtable_definition_current_elements(), + readonly: false, + }); + + let element_size = match self.module.table_plans[index].style { + TableStyle::CallerChecksSignature => { + u64::from(self.offsets.size_of_vmcaller_checked_anyfunc()) + } + }; + + Ok(func.create_table(ir::TableData { + base_gv, + min_size: Uimm64::new(0), + bound_gv, + element_size: Uimm64::new(element_size), + index_type: I32, + })) + } + + fn translate_table_grow( + &mut self, + _: cranelift_codegen::cursor::FuncCursor<'_>, + _: u32, + _: ir::Value, + _: ir::Value, + ) -> WasmResult { + Err(WasmError::Unsupported( + "the `table.grow` instruction is not supported yet".into(), + )) + } + + fn translate_table_get( + &mut self, + _: cranelift_codegen::cursor::FuncCursor<'_>, + _: u32, + _: ir::Value, + ) -> WasmResult { + Err(WasmError::Unsupported( + "the `table.get` instruction is not supported yet".into(), + )) + } + + fn translate_table_set( + &mut self, + _: cranelift_codegen::cursor::FuncCursor<'_>, + _: u32, + _: ir::Value, + _: ir::Value, + ) -> WasmResult<()> { + Err(WasmError::Unsupported( + "the `table.set` instruction is not supported yet".into(), + )) + } + + fn translate_table_fill( + &mut self, + _: cranelift_codegen::cursor::FuncCursor<'_>, + _: u32, + _: ir::Value, + _: ir::Value, + _: ir::Value, + ) -> WasmResult<()> { + Err(WasmError::Unsupported( + "the `table.fill` instruction is not supported yet".into(), + )) + } + + fn translate_ref_func( + &mut self, + _: cranelift_codegen::cursor::FuncCursor<'_>, + _: u32, + ) -> WasmResult { + Err(WasmError::Unsupported( + "the `ref.func` instruction is not supported yet".into(), + )) + } + + fn translate_custom_global_get( + &mut self, + _: cranelift_codegen::cursor::FuncCursor<'_>, + _: cranelift_wasm::GlobalIndex, + ) -> WasmResult { + unreachable!("we don't make any custom globals") + } + + fn translate_custom_global_set( + &mut self, + _: cranelift_codegen::cursor::FuncCursor<'_>, + _: cranelift_wasm::GlobalIndex, + _: ir::Value, + ) -> WasmResult<()> { + unreachable!("we don't make any custom globals") + } + + fn make_heap(&mut self, func: &mut ir::Function, index: MemoryIndex) -> WasmResult { + let pointer_type = self.pointer_type(); + + let (ptr, base_offset, current_length_offset) = { + let vmctx = self.vmctx(func); + if let Some(def_index) = self.module.defined_memory_index(index) { + let base_offset = + i32::try_from(self.offsets.vmctx_vmmemory_definition_base(def_index)).unwrap(); + let current_length_offset = i32::try_from( + self.offsets + .vmctx_vmmemory_definition_current_length(def_index), + ) + .unwrap(); + (vmctx, base_offset, current_length_offset) + } else { + let from_offset = self.offsets.vmctx_vmmemory_import_from(index); + let memory = func.create_global_value(ir::GlobalValueData::Load { + base: vmctx, + offset: Offset32::new(i32::try_from(from_offset).unwrap()), + global_type: pointer_type, + readonly: true, + }); + let base_offset = i32::from(self.offsets.vmmemory_definition_base()); + let current_length_offset = + i32::from(self.offsets.vmmemory_definition_current_length()); + (memory, base_offset, current_length_offset) + } + }; + + // If we have a declared maximum, we can make this a "static" heap, which is + // allocated up front and never moved. + let (offset_guard_size, heap_style, readonly_base) = match self.module.memory_plans[index] { + MemoryPlan { + style: MemoryStyle::Dynamic, + offset_guard_size, + memory: _, + } => { + let heap_bound = func.create_global_value(ir::GlobalValueData::Load { + base: ptr, + offset: Offset32::new(current_length_offset), + global_type: self.offsets.type_of_vmmemory_definition_current_length(), + readonly: false, + }); + ( + Uimm64::new(offset_guard_size), + ir::HeapStyle::Dynamic { + bound_gv: heap_bound, + }, + false, + ) + } + MemoryPlan { + style: MemoryStyle::Static { bound }, + offset_guard_size, + memory: _, + } => ( + Uimm64::new(offset_guard_size), + ir::HeapStyle::Static { + bound: Uimm64::new(u64::from(bound) * u64::from(WASM_PAGE_SIZE)), + }, + true, + ), + }; + + let heap_base = func.create_global_value(ir::GlobalValueData::Load { + base: ptr, + offset: Offset32::new(base_offset), + global_type: pointer_type, + readonly: readonly_base, + }); + Ok(func.create_heap(ir::HeapData { + base: heap_base, + min_size: 0.into(), + offset_guard_size, + style: heap_style, + index_type: I32, + })) + } + + fn make_global( + &mut self, + func: &mut ir::Function, + index: GlobalIndex, + ) -> WasmResult { + let pointer_type = self.pointer_type(); + + let (ptr, offset) = { + let vmctx = self.vmctx(func); + if let Some(def_index) = self.module.defined_global_index(index) { + let offset = + i32::try_from(self.offsets.vmctx_vmglobal_definition(def_index)).unwrap(); + (vmctx, offset) + } else { + let from_offset = self.offsets.vmctx_vmglobal_import_from(index); + let global = func.create_global_value(ir::GlobalValueData::Load { + base: vmctx, + offset: Offset32::new(i32::try_from(from_offset).unwrap()), + global_type: pointer_type, + readonly: true, + }); + (global, 0) + } + }; + + Ok(GlobalVariable::Memory { + gv: ptr, + offset: offset.into(), + ty: self.module.globals[index].ty, + }) + } + + fn make_indirect_sig( + &mut self, + func: &mut ir::Function, + index: SignatureIndex, + ) -> WasmResult { + Ok(func.import_signature(self.module.signatures[index].clone())) + } + + fn make_direct_func( + &mut self, + func: &mut ir::Function, + index: FuncIndex, + ) -> WasmResult { + let sigidx = self.module.functions[index]; + let signature = func.import_signature(self.module.signatures[sigidx].clone()); + let name = get_func_name(index); + Ok(func.import_function(ir::ExtFuncData { + name, + signature, + // We currently allocate all code segments independently, so nothing + // is colocated. + colocated: false, + })) + } + + fn translate_call_indirect( + &mut self, + mut pos: FuncCursor<'_>, + table_index: TableIndex, + table: ir::Table, + sig_index: SignatureIndex, + sig_ref: ir::SigRef, + callee: ir::Value, + call_args: &[ir::Value], + ) -> WasmResult { + let pointer_type = self.pointer_type(); + + let table_entry_addr = pos.ins().table_addr(pointer_type, table, callee, 0); + + // Dereference table_entry_addr to get the function address. + let mem_flags = ir::MemFlags::trusted(); + let func_addr = pos.ins().load( + pointer_type, + mem_flags, + table_entry_addr, + i32::from(self.offsets.vmcaller_checked_anyfunc_func_ptr()), + ); + + // Check whether `func_addr` is null. + pos.ins().trapz(func_addr, ir::TrapCode::IndirectCallToNull); + + // If necessary, check the signature. + match self.module.table_plans[table_index].style { + TableStyle::CallerChecksSignature => { + let sig_id_size = self.offsets.size_of_vmshared_signature_index(); + let sig_id_type = Type::int(u16::from(sig_id_size) * 8).unwrap(); + let vmctx = self.vmctx(pos.func); + let base = pos.ins().global_value(pointer_type, vmctx); + let offset = + i32::try_from(self.offsets.vmctx_vmshared_signature_id(sig_index)).unwrap(); + + // Load the caller ID. + let mut mem_flags = ir::MemFlags::trusted(); + mem_flags.set_readonly(); + let caller_sig_id = pos.ins().load(sig_id_type, mem_flags, base, offset); + + // Load the callee ID. + let mem_flags = ir::MemFlags::trusted(); + let callee_sig_id = pos.ins().load( + sig_id_type, + mem_flags, + table_entry_addr, + i32::from(self.offsets.vmcaller_checked_anyfunc_type_index()), + ); + + // Check that they match. + let cmp = pos.ins().icmp(IntCC::Equal, callee_sig_id, caller_sig_id); + pos.ins().trapz(cmp, ir::TrapCode::BadSignature); + } + } + + let mut real_call_args = Vec::with_capacity(call_args.len() + 2); + let caller_vmctx = pos.func.special_param(ArgumentPurpose::VMContext).unwrap(); + + // First append the callee vmctx address. + let vmctx = pos.ins().load( + pointer_type, + mem_flags, + table_entry_addr, + i32::from(self.offsets.vmcaller_checked_anyfunc_vmctx()), + ); + real_call_args.push(vmctx); + real_call_args.push(caller_vmctx); + + // Then append the regular call arguments. + real_call_args.extend_from_slice(call_args); + + Ok(pos.ins().call_indirect(sig_ref, func_addr, &real_call_args)) + } + + fn translate_call( + &mut self, + mut pos: FuncCursor<'_>, + callee_index: FuncIndex, + callee: ir::FuncRef, + call_args: &[ir::Value], + ) -> WasmResult { + let mut real_call_args = Vec::with_capacity(call_args.len() + 2); + let caller_vmctx = pos.func.special_param(ArgumentPurpose::VMContext).unwrap(); + + // Handle direct calls to locally-defined functions. + if !self.module.is_imported_function(callee_index) { + // First append the callee vmctx address, which is the same as the caller vmctx in + // this case. + real_call_args.push(caller_vmctx); + + // Then append the caller vmctx address. + real_call_args.push(caller_vmctx); + + // Then append the regular call arguments. + real_call_args.extend_from_slice(call_args); + + return Ok(pos.ins().call(callee, &real_call_args)); + } + + // Handle direct calls to imported functions. We use an indirect call + // so that we don't have to patch the code at runtime. + let pointer_type = self.pointer_type(); + let sig_ref = pos.func.dfg.ext_funcs[callee].signature; + let vmctx = self.vmctx(&mut pos.func); + let base = pos.ins().global_value(pointer_type, vmctx); + + let mem_flags = ir::MemFlags::trusted(); + + // Load the callee address. + let body_offset = + i32::try_from(self.offsets.vmctx_vmfunction_import_body(callee_index)).unwrap(); + let func_addr = pos.ins().load(pointer_type, mem_flags, base, body_offset); + + // First append the callee vmctx address. + let vmctx_offset = + i32::try_from(self.offsets.vmctx_vmfunction_import_vmctx(callee_index)).unwrap(); + let vmctx = pos.ins().load(pointer_type, mem_flags, base, vmctx_offset); + real_call_args.push(vmctx); + real_call_args.push(caller_vmctx); + + // Then append the regular call arguments. + real_call_args.extend_from_slice(call_args); + + Ok(pos.ins().call_indirect(sig_ref, func_addr, &real_call_args)) + } + + fn translate_memory_grow( + &mut self, + mut pos: FuncCursor<'_>, + index: MemoryIndex, + _heap: ir::Heap, + val: ir::Value, + ) -> WasmResult { + let (func_sig, index_arg, func_idx) = self.get_memory_grow_func(&mut pos.func, index); + let memory_index = pos.ins().iconst(I32, index_arg as i64); + let (vmctx, func_addr) = self.translate_load_builtin_function_address(&mut pos, func_idx); + let call_inst = pos + .ins() + .call_indirect(func_sig, func_addr, &[vmctx, val, memory_index]); + Ok(*pos.func.dfg.inst_results(call_inst).first().unwrap()) + } + + fn translate_memory_size( + &mut self, + mut pos: FuncCursor<'_>, + index: MemoryIndex, + _heap: ir::Heap, + ) -> WasmResult { + let (func_sig, index_arg, func_idx) = self.get_memory_size_func(&mut pos.func, index); + let memory_index = pos.ins().iconst(I32, index_arg as i64); + let (vmctx, func_addr) = self.translate_load_builtin_function_address(&mut pos, func_idx); + let call_inst = pos + .ins() + .call_indirect(func_sig, func_addr, &[vmctx, memory_index]); + Ok(*pos.func.dfg.inst_results(call_inst).first().unwrap()) + } + + fn translate_memory_copy( + &mut self, + mut pos: FuncCursor, + memory_index: MemoryIndex, + _heap: ir::Heap, + dst: ir::Value, + src: ir::Value, + len: ir::Value, + ) -> WasmResult<()> { + let (func_sig, memory_index, func_idx) = + self.get_memory_copy_func(&mut pos.func, memory_index); + + let memory_index_arg = pos.ins().iconst(I32, memory_index as i64); + + let (vmctx, func_addr) = self.translate_load_builtin_function_address(&mut pos, func_idx); + + let src_loc = pos.srcloc(); + let src_loc_arg = pos.ins().iconst(I32, src_loc.bits() as i64); + + pos.ins().call_indirect( + func_sig, + func_addr, + &[vmctx, memory_index_arg, dst, src, len, src_loc_arg], + ); + + Ok(()) + } + + fn translate_memory_fill( + &mut self, + mut pos: FuncCursor, + memory_index: MemoryIndex, + _heap: ir::Heap, + dst: ir::Value, + val: ir::Value, + len: ir::Value, + ) -> WasmResult<()> { + let (func_sig, memory_index, func_idx) = + self.get_memory_fill_func(&mut pos.func, memory_index); + + let memory_index_arg = pos.ins().iconst(I32, memory_index as i64); + + let (vmctx, func_addr) = self.translate_load_builtin_function_address(&mut pos, func_idx); + + let src_loc = pos.srcloc(); + let src_loc_arg = pos.ins().iconst(I32, src_loc.bits() as i64); + + pos.ins().call_indirect( + func_sig, + func_addr, + &[vmctx, memory_index_arg, dst, val, len, src_loc_arg], + ); + + Ok(()) + } + + fn translate_memory_init( + &mut self, + _pos: FuncCursor, + _index: MemoryIndex, + _heap: ir::Heap, + _seg_index: u32, + _dst: ir::Value, + _src: ir::Value, + _len: ir::Value, + ) -> WasmResult<()> { + Err(WasmError::Unsupported( + "bulk memory: `memory.init`".to_string(), + )) + } + + fn translate_data_drop(&mut self, _pos: FuncCursor, _seg_index: u32) -> WasmResult<()> { + Err(WasmError::Unsupported( + "bulk memory: `data.drop`".to_string(), + )) + } + + fn translate_table_size( + &mut self, + _pos: FuncCursor, + _index: TableIndex, + _table: ir::Table, + ) -> WasmResult { + Err(WasmError::Unsupported( + "bulk memory: `table.size`".to_string(), + )) + } + + fn translate_table_copy( + &mut self, + mut pos: FuncCursor, + dst_table_index: TableIndex, + _dst_table: ir::Table, + src_table_index: TableIndex, + _src_table: ir::Table, + dst: ir::Value, + src: ir::Value, + len: ir::Value, + ) -> WasmResult<()> { + let (func_sig, dst_table_index_arg, src_table_index_arg, func_idx) = + self.get_table_copy_func(&mut pos.func, dst_table_index, src_table_index); + + let dst_table_index_arg = pos.ins().iconst(I32, dst_table_index_arg as i64); + let src_table_index_arg = pos.ins().iconst(I32, src_table_index_arg as i64); + + let src_loc = pos.srcloc(); + let src_loc_arg = pos.ins().iconst(I32, src_loc.bits() as i64); + + let (vmctx, func_addr) = self.translate_load_builtin_function_address(&mut pos, func_idx); + + pos.ins().call_indirect( + func_sig, + func_addr, + &[ + vmctx, + dst_table_index_arg, + src_table_index_arg, + dst, + src, + len, + src_loc_arg, + ], + ); + + Ok(()) + } + + fn translate_table_init( + &mut self, + mut pos: FuncCursor, + seg_index: u32, + table_index: TableIndex, + _table: ir::Table, + dst: ir::Value, + src: ir::Value, + len: ir::Value, + ) -> WasmResult<()> { + let (func_sig, table_index_arg, func_idx) = + self.get_table_init_func(&mut pos.func, table_index); + + let table_index_arg = pos.ins().iconst(I32, table_index_arg as i64); + let seg_index_arg = pos.ins().iconst(I32, seg_index as i64); + + let src_loc = pos.srcloc(); + let src_loc_arg = pos.ins().iconst(I32, src_loc.bits() as i64); + + let (vmctx, func_addr) = self.translate_load_builtin_function_address(&mut pos, func_idx); + + pos.ins().call_indirect( + func_sig, + func_addr, + &[ + vmctx, + table_index_arg, + seg_index_arg, + dst, + src, + len, + src_loc_arg, + ], + ); + + Ok(()) + } + + fn translate_elem_drop(&mut self, mut pos: FuncCursor, elem_index: u32) -> WasmResult<()> { + let (func_sig, func_idx) = self.get_elem_drop_func(&mut pos.func); + + let elem_index_arg = pos.ins().iconst(I32, elem_index as i64); + + let (vmctx, func_addr) = self.translate_load_builtin_function_address(&mut pos, func_idx); + + pos.ins() + .call_indirect(func_sig, func_addr, &[vmctx, elem_index_arg]); + + Ok(()) + } +} diff --git a/crates/environ/src/lib.rs b/crates/environ/src/lib.rs new file mode 100644 index 0000000000..ba849b54fe --- /dev/null +++ b/crates/environ/src/lib.rs @@ -0,0 +1,75 @@ +//! Standalone environment for WebAssembly using Cranelift. Provides functions to translate +//! `get_global`, `set_global`, `memory.size`, `memory.grow`, `call_indirect` that hardcode in +//! the translation the base addresses of regions of memory that will hold the globals, tables and +//! linear memories. + +#![deny(missing_docs, trivial_numeric_casts, unused_extern_crates)] +#![warn(unused_import_braces)] +#![cfg_attr(feature = "clippy", plugin(clippy(conf_file = "../../clippy.toml")))] +#![cfg_attr( + feature = "cargo-clippy", + allow(clippy::new_without_default, clippy::new_without_default) +)] +#![cfg_attr( + feature = "cargo-clippy", + warn( + clippy::float_arithmetic, + clippy::mut_mut, + clippy::nonminimal_bool, + clippy::option_map_unwrap_or, + clippy::option_map_unwrap_or_else, + clippy::print_stdout, + clippy::unicode_not_nfc, + clippy::use_self + ) +)] + +mod address_map; +mod compilation; +mod data_structures; +mod func_environ; +mod module; +mod module_environ; +mod tunables; +mod vmoffsets; + +mod cache; + +pub mod cranelift; +#[cfg(feature = "lightbeam")] +pub mod lightbeam; + +pub use crate::address_map::{ + FunctionAddressMap, InstructionAddressMap, ModuleAddressMap, ModuleMemoryOffset, + ModuleVmctxInfo, ValueLabelsRanges, +}; +pub use crate::cache::create_new_config as cache_create_new_config; +pub use crate::cache::CacheConfig; +pub use crate::compilation::{ + Compilation, CompileError, CompiledFunction, CompiledFunctionUnwindInfo, + CompiledFunctionUnwindInfoReloc, Compiler, Relocation, RelocationTarget, Relocations, + TrapInformation, Traps, +}; +pub use crate::cranelift::Cranelift; +pub use crate::data_structures::*; +pub use crate::func_environ::BuiltinFunctionIndex; +#[cfg(feature = "lightbeam")] +pub use crate::lightbeam::Lightbeam; +pub use crate::module::{ + Export, MemoryPlan, MemoryStyle, Module, ModuleLocal, TableElements, TablePlan, TableStyle, +}; +pub use crate::module_environ::{ + translate_signature, DataInitializer, DataInitializerLocation, FunctionBodyData, + ModuleEnvironment, ModuleTranslation, +}; +pub use crate::tunables::Tunables; +pub use crate::vmoffsets::{TargetSharedSignatureIndex, VMOffsets}; + +/// WebAssembly page sizes are defined to be 64KiB. +pub const WASM_PAGE_SIZE: u32 = 0x10000; + +/// The number of pages we can have before we run out of byte index space. +pub const WASM_MAX_PAGES: u32 = 0x10000; + +/// Version number of this crate. +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/crates/environ/src/lightbeam.rs b/crates/environ/src/lightbeam.rs new file mode 100644 index 0000000000..3abf98301f --- /dev/null +++ b/crates/environ/src/lightbeam.rs @@ -0,0 +1,75 @@ +//! Support for compiling with Lightbeam. + +use crate::cache::ModuleCacheDataTupleType; +use crate::compilation::{Compilation, CompileError, Traps}; +use crate::func_environ::FuncEnvironment; +use crate::module::Module; +use crate::module_environ::FunctionBodyData; +// TODO: Put this in `compilation` +use crate::address_map::{ModuleAddressMap, ValueLabelsRanges}; +use crate::cranelift::RelocSink; +use crate::CacheConfig; +use cranelift_codegen::isa; +use cranelift_entity::{PrimaryMap, SecondaryMap}; +use cranelift_wasm::{DefinedFuncIndex, ModuleTranslationState}; + +/// A compiler that compiles a WebAssembly module with Lightbeam, directly translating the Wasm file. +pub struct Lightbeam; + +impl crate::compilation::Compiler for Lightbeam { + /// Compile the module using Lightbeam, producing a compilation result with + /// associated relocations. + fn compile_module<'data, 'module>( + module: &'module Module, + _module_translation: &ModuleTranslationState, + function_body_inputs: PrimaryMap>, + isa: &dyn isa::TargetIsa, + // TODO + generate_debug_info: bool, + _cache_config: &CacheConfig, + ) -> Result { + if generate_debug_info { + return Err(CompileError::DebugInfoNotSupported); + } + + let env = FuncEnvironment::new(isa.frontend_config(), &module.local); + let mut relocations = PrimaryMap::new(); + let mut codegen_session: lightbeam::CodeGenSession<_> = + lightbeam::CodeGenSession::new(function_body_inputs.len() as u32, &env); + + for (i, function_body) in &function_body_inputs { + let func_index = module.local.func_index(i); + let mut reloc_sink = RelocSink::new(func_index); + + lightbeam::translate_function( + &mut codegen_session, + &mut reloc_sink, + i.as_u32(), + &wasmparser::FunctionBody::new(0, function_body.data), + ) + .map_err(|e| CompileError::Codegen(format!("Failed to translate function: {}", e)))?; + relocations.push(reloc_sink.func_relocs); + } + + let code_section = codegen_session + .into_translated_code_section() + .map_err(|e| CompileError::Codegen(format!("Failed to generate output code: {}", e)))?; + + // TODO pass jump table offsets to Compilation::from_buffer() when they + // are implemented in lightbeam -- using empty set of offsets for now. + // TODO: pass an empty range for the unwind information until lightbeam emits it + let code_section_ranges_and_jt = code_section + .funcs() + .into_iter() + .map(|r| (r, SecondaryMap::new(), 0..0)); + + Ok(( + Compilation::from_buffer(code_section.buffer().to_vec(), code_section_ranges_and_jt), + relocations, + ModuleAddressMap::new(), + ValueLabelsRanges::new(), + PrimaryMap::new(), + Traps::new(), + )) + } +} diff --git a/crates/environ/src/module.rs b/crates/environ/src/module.rs new file mode 100644 index 0000000000..aacc6fe747 --- /dev/null +++ b/crates/environ/src/module.rs @@ -0,0 +1,335 @@ +//! Data structures for representing decoded wasm modules. + +use crate::tunables::Tunables; +use crate::WASM_MAX_PAGES; +use cranelift_codegen::ir; +use cranelift_entity::{EntityRef, PrimaryMap}; +use cranelift_wasm::{ + DefinedFuncIndex, DefinedGlobalIndex, DefinedMemoryIndex, DefinedTableIndex, FuncIndex, Global, + GlobalIndex, Memory, MemoryIndex, PassiveElemIndex, SignatureIndex, Table, TableIndex, +}; +use indexmap::IndexMap; +use more_asserts::assert_ge; +use std::collections::HashMap; +use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; + +/// A WebAssembly table initializer. +#[derive(Clone, Debug, Hash)] +pub struct TableElements { + /// The index of a table to initialize. + pub table_index: TableIndex, + /// Optionally, a global variable giving a base index. + pub base: Option, + /// The offset to add to the base. + pub offset: usize, + /// The values to write into the table elements. + pub elements: Box<[FuncIndex]>, +} + +/// An entity to export. +#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub enum Export { + /// Function export. + Function(FuncIndex), + /// Table export. + Table(TableIndex), + /// Memory export. + Memory(MemoryIndex), + /// Global export. + Global(GlobalIndex), +} + +/// Implemenation styles for WebAssembly linear memory. +#[derive(Debug, Clone, Hash)] +pub enum MemoryStyle { + /// The actual memory can be resized and moved. + Dynamic, + /// Addresss space is allocated up front. + Static { + /// The number of mapped and unmapped pages. + bound: u32, + }, +} + +impl MemoryStyle { + /// Decide on an implementation style for the given `Memory`. + pub fn for_memory(memory: Memory, tunables: &Tunables) -> (Self, u64) { + // A heap with a maximum that doesn't exceed the static memory bound specified by the + // tunables make it static. + // + // If the module doesn't declare an explicit maximum treat it as 4GiB. + let maximum = memory.maximum.unwrap_or(WASM_MAX_PAGES); + if maximum <= tunables.static_memory_bound { + assert_ge!(tunables.static_memory_bound, memory.minimum); + return ( + Self::Static { + bound: tunables.static_memory_bound, + }, + tunables.static_memory_offset_guard_size, + ); + } + + // Otherwise, make it dynamic. + (Self::Dynamic, tunables.dynamic_memory_offset_guard_size) + } +} + +/// A WebAssembly linear memory description along with our chosen style for +/// implementing it. +#[derive(Debug, Clone, Hash)] +pub struct MemoryPlan { + /// The WebAssembly linear memory description. + pub memory: Memory, + /// Our chosen implementation style. + pub style: MemoryStyle, + /// Our chosen offset-guard size. + pub offset_guard_size: u64, +} + +impl MemoryPlan { + /// Draw up a plan for implementing a `Memory`. + pub fn for_memory(memory: Memory, tunables: &Tunables) -> Self { + let (style, offset_guard_size) = MemoryStyle::for_memory(memory, tunables); + Self { + memory, + style, + offset_guard_size, + } + } +} + +/// Implemenation styles for WebAssembly tables. +#[derive(Debug, Clone, Hash)] +pub enum TableStyle { + /// Signatures are stored in the table and checked in the caller. + CallerChecksSignature, +} + +impl TableStyle { + /// Decide on an implementation style for the given `Table`. + pub fn for_table(_table: Table, _tunables: &Tunables) -> Self { + Self::CallerChecksSignature + } +} + +/// A WebAssembly table description along with our chosen style for +/// implementing it. +#[derive(Debug, Clone, Hash)] +pub struct TablePlan { + /// The WebAssembly table description. + pub table: cranelift_wasm::Table, + /// Our chosen implementation style. + pub style: TableStyle, +} + +impl TablePlan { + /// Draw up a plan for implementing a `Table`. + pub fn for_table(table: Table, tunables: &Tunables) -> Self { + let style = TableStyle::for_table(table, tunables); + Self { table, style } + } +} + +/// A translated WebAssembly module, excluding the function bodies and +/// memory initializers. +#[derive(Debug)] +pub struct Module { + /// A unique identifier (within this process) for this module. + pub id: usize, + + /// Local information about a module which is the bare minimum necessary to + /// translate a function body. This is derived as `Hash` whereas this module + /// isn't, since it contains too much information needed to translate a + /// function. + pub local: ModuleLocal, + + /// Names of imported functions, as well as the index of the import that + /// performed this import. + pub imported_funcs: PrimaryMap, + + /// Names of imported tables. + pub imported_tables: PrimaryMap, + + /// Names of imported memories. + pub imported_memories: PrimaryMap, + + /// Names of imported globals. + pub imported_globals: PrimaryMap, + + /// Exported entities. + pub exports: IndexMap, + + /// The module "start" function, if present. + pub start_func: Option, + + /// WebAssembly table initializers. + pub table_elements: Vec, + + /// WebAssembly passive elements. + pub passive_elements: HashMap>, + + /// WebAssembly table initializers. + pub func_names: HashMap, +} + +/// Local information known about a wasm module, the bare minimum necessary to +/// translate function bodies. +/// +/// This is stored within a `Module` and it implements `Hash`, unlike `Module`, +/// and is used as part of the cache key when we load compiled modules from the +/// global cache. +#[derive(Debug, Hash)] +pub struct ModuleLocal { + /// Unprocessed signatures exactly as provided by `declare_signature()`. + pub signatures: PrimaryMap, + + /// Number of imported functions in the module. + pub num_imported_funcs: usize, + + /// Number of imported tables in the module. + pub num_imported_tables: usize, + + /// Number of imported memories in the module. + pub num_imported_memories: usize, + + /// Number of imported globals in the module. + pub num_imported_globals: usize, + + /// Types of functions, imported and local. + pub functions: PrimaryMap, + + /// WebAssembly tables. + pub table_plans: PrimaryMap, + + /// WebAssembly linear memory plans. + pub memory_plans: PrimaryMap, + + /// WebAssembly global variables. + pub globals: PrimaryMap, +} + +impl Module { + /// Allocates the module data structures. + pub fn new() -> Self { + static NEXT_ID: AtomicUsize = AtomicUsize::new(0); + + Self { + id: NEXT_ID.fetch_add(1, SeqCst), + imported_funcs: PrimaryMap::new(), + imported_tables: PrimaryMap::new(), + imported_memories: PrimaryMap::new(), + imported_globals: PrimaryMap::new(), + exports: IndexMap::new(), + start_func: None, + table_elements: Vec::new(), + passive_elements: HashMap::new(), + func_names: HashMap::new(), + local: ModuleLocal { + num_imported_funcs: 0, + num_imported_tables: 0, + num_imported_memories: 0, + num_imported_globals: 0, + signatures: PrimaryMap::new(), + functions: PrimaryMap::new(), + table_plans: PrimaryMap::new(), + memory_plans: PrimaryMap::new(), + globals: PrimaryMap::new(), + }, + } + } + + /// Get the given passive element, if it exists. + pub fn get_passive_element(&self, index: PassiveElemIndex) -> Option<&[FuncIndex]> { + self.passive_elements.get(&index).map(|es| &**es) + } +} + +impl ModuleLocal { + /// Convert a `DefinedFuncIndex` into a `FuncIndex`. + pub fn func_index(&self, defined_func: DefinedFuncIndex) -> FuncIndex { + FuncIndex::new(self.num_imported_funcs + defined_func.index()) + } + + /// Convert a `FuncIndex` into a `DefinedFuncIndex`. Returns None if the + /// index is an imported function. + pub fn defined_func_index(&self, func: FuncIndex) -> Option { + if func.index() < self.num_imported_funcs { + None + } else { + Some(DefinedFuncIndex::new( + func.index() - self.num_imported_funcs, + )) + } + } + + /// Test whether the given function index is for an imported function. + pub fn is_imported_function(&self, index: FuncIndex) -> bool { + index.index() < self.num_imported_funcs + } + + /// Convert a `DefinedTableIndex` into a `TableIndex`. + pub fn table_index(&self, defined_table: DefinedTableIndex) -> TableIndex { + TableIndex::new(self.num_imported_tables + defined_table.index()) + } + + /// Convert a `TableIndex` into a `DefinedTableIndex`. Returns None if the + /// index is an imported table. + pub fn defined_table_index(&self, table: TableIndex) -> Option { + if table.index() < self.num_imported_tables { + None + } else { + Some(DefinedTableIndex::new( + table.index() - self.num_imported_tables, + )) + } + } + + /// Test whether the given table index is for an imported table. + pub fn is_imported_table(&self, index: TableIndex) -> bool { + index.index() < self.num_imported_tables + } + + /// Convert a `DefinedMemoryIndex` into a `MemoryIndex`. + pub fn memory_index(&self, defined_memory: DefinedMemoryIndex) -> MemoryIndex { + MemoryIndex::new(self.num_imported_memories + defined_memory.index()) + } + + /// Convert a `MemoryIndex` into a `DefinedMemoryIndex`. Returns None if the + /// index is an imported memory. + pub fn defined_memory_index(&self, memory: MemoryIndex) -> Option { + if memory.index() < self.num_imported_memories { + None + } else { + Some(DefinedMemoryIndex::new( + memory.index() - self.num_imported_memories, + )) + } + } + + /// Test whether the given memory index is for an imported memory. + pub fn is_imported_memory(&self, index: MemoryIndex) -> bool { + index.index() < self.num_imported_memories + } + + /// Convert a `DefinedGlobalIndex` into a `GlobalIndex`. + pub fn global_index(&self, defined_global: DefinedGlobalIndex) -> GlobalIndex { + GlobalIndex::new(self.num_imported_globals + defined_global.index()) + } + + /// Convert a `GlobalIndex` into a `DefinedGlobalIndex`. Returns None if the + /// index is an imported global. + pub fn defined_global_index(&self, global: GlobalIndex) -> Option { + if global.index() < self.num_imported_globals { + None + } else { + Some(DefinedGlobalIndex::new( + global.index() - self.num_imported_globals, + )) + } + } + + /// Test whether the given global index is for an imported global. + pub fn is_imported_global(&self, index: GlobalIndex) -> bool { + index.index() < self.num_imported_globals + } +} diff --git a/crates/environ/src/module_environ.rs b/crates/environ/src/module_environ.rs new file mode 100644 index 0000000000..7839bdd643 --- /dev/null +++ b/crates/environ/src/module_environ.rs @@ -0,0 +1,450 @@ +use crate::func_environ::FuncEnvironment; +use crate::module::{Export, MemoryPlan, Module, TableElements, TablePlan}; +use crate::tunables::Tunables; +use cranelift_codegen::ir; +use cranelift_codegen::ir::{AbiParam, ArgumentPurpose}; +use cranelift_codegen::isa::TargetFrontendConfig; +use cranelift_entity::PrimaryMap; +use cranelift_wasm::{ + self, translate_module, DefinedFuncIndex, FuncIndex, Global, GlobalIndex, Memory, MemoryIndex, + ModuleTranslationState, PassiveDataIndex, PassiveElemIndex, SignatureIndex, Table, TableIndex, + TargetEnvironment, WasmError, WasmResult, +}; +use std::convert::TryFrom; + +/// Contains function data: byte code and its offset in the module. +#[derive(Hash)] +pub struct FunctionBodyData<'a> { + /// Body byte code. + pub data: &'a [u8], + + /// Body offset in the module file. + pub module_offset: usize, +} + +/// The result of translating via `ModuleEnvironment`. Function bodies are not +/// yet translated, and data initializers have not yet been copied out of the +/// original buffer. +pub struct ModuleTranslation<'data> { + /// Compilation setting flags. + pub target_config: TargetFrontendConfig, + + /// Module information. + pub module: Module, + + /// References to the function bodies. + pub function_body_inputs: PrimaryMap>, + + /// References to the data initializers. + pub data_initializers: Vec>, + + /// Tunable parameters. + pub tunables: Tunables, + + /// The decoded Wasm types for the module. + pub module_translation: Option, +} + +impl<'data> ModuleTranslation<'data> { + /// Return a new `FuncEnvironment` for translating a function. + pub fn func_env(&self) -> FuncEnvironment<'_> { + FuncEnvironment::new(self.target_config, &self.module.local) + } +} + +/// Object containing the standalone environment information. +pub struct ModuleEnvironment<'data> { + /// The result to be filled in. + result: ModuleTranslation<'data>, + imports: u32, +} + +impl<'data> ModuleEnvironment<'data> { + /// Allocates the environment data structures. + pub fn new(target_config: TargetFrontendConfig, tunables: Tunables) -> Self { + Self { + result: ModuleTranslation { + target_config, + module: Module::new(), + function_body_inputs: PrimaryMap::new(), + data_initializers: Vec::new(), + tunables, + module_translation: None, + }, + imports: 0, + } + } + + fn pointer_type(&self) -> ir::Type { + self.result.target_config.pointer_type() + } + + /// Translate a wasm module using this environment. This consumes the + /// `ModuleEnvironment` and produces a `ModuleTranslation`. + pub fn translate(mut self, data: &'data [u8]) -> WasmResult> { + assert!(self.result.module_translation.is_none()); + let module_translation = translate_module(data, &mut self)?; + self.result.module_translation = Some(module_translation); + Ok(self.result) + } + + fn declare_export(&mut self, export: Export, name: &str) -> WasmResult<()> { + self.result + .module + .exports + .insert(String::from(name), export); + Ok(()) + } +} + +impl<'data> TargetEnvironment for ModuleEnvironment<'data> { + fn target_config(&self) -> TargetFrontendConfig { + self.result.target_config + } +} + +/// This trait is useful for `translate_module` because it tells how to translate +/// environment-dependent wasm instructions. These functions should not be called by the user. +impl<'data> cranelift_wasm::ModuleEnvironment<'data> for ModuleEnvironment<'data> { + fn reserve_signatures(&mut self, num: u32) -> WasmResult<()> { + self.result + .module + .local + .signatures + .reserve_exact(usize::try_from(num).unwrap()); + Ok(()) + } + + fn declare_signature(&mut self, sig: ir::Signature) -> WasmResult<()> { + let sig = translate_signature(sig, self.pointer_type()); + // TODO: Deduplicate signatures. + self.result.module.local.signatures.push(sig); + Ok(()) + } + + fn declare_func_import( + &mut self, + sig_index: SignatureIndex, + module: &str, + field: &str, + ) -> WasmResult<()> { + debug_assert_eq!( + self.result.module.local.functions.len(), + self.result.module.imported_funcs.len(), + "Imported functions must be declared first" + ); + self.result.module.local.functions.push(sig_index); + + self.result.module.imported_funcs.push(( + String::from(module), + String::from(field), + self.imports, + )); + self.result.module.local.num_imported_funcs += 1; + self.imports += 1; + Ok(()) + } + + fn declare_table_import(&mut self, table: Table, module: &str, field: &str) -> WasmResult<()> { + debug_assert_eq!( + self.result.module.local.table_plans.len(), + self.result.module.imported_tables.len(), + "Imported tables must be declared first" + ); + let plan = TablePlan::for_table(table, &self.result.tunables); + self.result.module.local.table_plans.push(plan); + + self.result.module.imported_tables.push(( + String::from(module), + String::from(field), + self.imports, + )); + self.result.module.local.num_imported_tables += 1; + self.imports += 1; + Ok(()) + } + + fn declare_memory_import( + &mut self, + memory: Memory, + module: &str, + field: &str, + ) -> WasmResult<()> { + debug_assert_eq!( + self.result.module.local.memory_plans.len(), + self.result.module.imported_memories.len(), + "Imported memories must be declared first" + ); + let plan = MemoryPlan::for_memory(memory, &self.result.tunables); + self.result.module.local.memory_plans.push(plan); + + self.result.module.imported_memories.push(( + String::from(module), + String::from(field), + self.imports, + )); + self.result.module.local.num_imported_memories += 1; + self.imports += 1; + Ok(()) + } + + fn declare_global_import( + &mut self, + global: Global, + module: &str, + field: &str, + ) -> WasmResult<()> { + debug_assert_eq!( + self.result.module.local.globals.len(), + self.result.module.imported_globals.len(), + "Imported globals must be declared first" + ); + self.result.module.local.globals.push(global); + + self.result.module.imported_globals.push(( + String::from(module), + String::from(field), + self.imports, + )); + self.result.module.local.num_imported_globals += 1; + self.imports += 1; + Ok(()) + } + + fn finish_imports(&mut self) -> WasmResult<()> { + self.result.module.imported_funcs.shrink_to_fit(); + self.result.module.imported_tables.shrink_to_fit(); + self.result.module.imported_memories.shrink_to_fit(); + self.result.module.imported_globals.shrink_to_fit(); + Ok(()) + } + + fn reserve_func_types(&mut self, num: u32) -> WasmResult<()> { + self.result + .module + .local + .functions + .reserve_exact(usize::try_from(num).unwrap()); + self.result + .function_body_inputs + .reserve_exact(usize::try_from(num).unwrap()); + Ok(()) + } + + fn declare_func_type(&mut self, sig_index: SignatureIndex) -> WasmResult<()> { + self.result.module.local.functions.push(sig_index); + Ok(()) + } + + fn reserve_tables(&mut self, num: u32) -> WasmResult<()> { + self.result + .module + .local + .table_plans + .reserve_exact(usize::try_from(num).unwrap()); + Ok(()) + } + + fn declare_table(&mut self, table: Table) -> WasmResult<()> { + let plan = TablePlan::for_table(table, &self.result.tunables); + self.result.module.local.table_plans.push(plan); + Ok(()) + } + + fn reserve_memories(&mut self, num: u32) -> WasmResult<()> { + self.result + .module + .local + .memory_plans + .reserve_exact(usize::try_from(num).unwrap()); + Ok(()) + } + + fn declare_memory(&mut self, memory: Memory) -> WasmResult<()> { + let plan = MemoryPlan::for_memory(memory, &self.result.tunables); + self.result.module.local.memory_plans.push(plan); + Ok(()) + } + + fn reserve_globals(&mut self, num: u32) -> WasmResult<()> { + self.result + .module + .local + .globals + .reserve_exact(usize::try_from(num).unwrap()); + Ok(()) + } + + fn declare_global(&mut self, global: Global) -> WasmResult<()> { + self.result.module.local.globals.push(global); + Ok(()) + } + + fn reserve_exports(&mut self, num: u32) -> WasmResult<()> { + self.result + .module + .exports + .reserve(usize::try_from(num).unwrap()); + Ok(()) + } + + fn declare_func_export(&mut self, func_index: FuncIndex, name: &str) -> WasmResult<()> { + self.declare_export(Export::Function(func_index), name) + } + + fn declare_table_export(&mut self, table_index: TableIndex, name: &str) -> WasmResult<()> { + self.declare_export(Export::Table(table_index), name) + } + + fn declare_memory_export(&mut self, memory_index: MemoryIndex, name: &str) -> WasmResult<()> { + self.declare_export(Export::Memory(memory_index), name) + } + + fn declare_global_export(&mut self, global_index: GlobalIndex, name: &str) -> WasmResult<()> { + self.declare_export(Export::Global(global_index), name) + } + + fn declare_start_func(&mut self, func_index: FuncIndex) -> WasmResult<()> { + debug_assert!(self.result.module.start_func.is_none()); + self.result.module.start_func = Some(func_index); + Ok(()) + } + + fn reserve_table_elements(&mut self, num: u32) -> WasmResult<()> { + self.result + .module + .table_elements + .reserve_exact(usize::try_from(num).unwrap()); + Ok(()) + } + + fn declare_table_elements( + &mut self, + table_index: TableIndex, + base: Option, + offset: usize, + elements: Box<[FuncIndex]>, + ) -> WasmResult<()> { + self.result.module.table_elements.push(TableElements { + table_index, + base, + offset, + elements, + }); + Ok(()) + } + + fn declare_passive_element( + &mut self, + elem_index: PassiveElemIndex, + segments: Box<[FuncIndex]>, + ) -> WasmResult<()> { + let old = self + .result + .module + .passive_elements + .insert(elem_index, segments); + debug_assert!( + old.is_none(), + "should never get duplicate element indices, that would be a bug in `cranelift_wasm`'s \ + translation" + ); + Ok(()) + } + + fn define_function_body( + &mut self, + _module_translation: &ModuleTranslationState, + body_bytes: &'data [u8], + body_offset: usize, + ) -> WasmResult<()> { + self.result.function_body_inputs.push(FunctionBodyData { + data: body_bytes, + module_offset: body_offset, + }); + Ok(()) + } + + fn reserve_data_initializers(&mut self, num: u32) -> WasmResult<()> { + self.result + .data_initializers + .reserve_exact(usize::try_from(num).unwrap()); + Ok(()) + } + + fn declare_data_initialization( + &mut self, + memory_index: MemoryIndex, + base: Option, + offset: usize, + data: &'data [u8], + ) -> WasmResult<()> { + self.result.data_initializers.push(DataInitializer { + location: DataInitializerLocation { + memory_index, + base, + offset, + }, + data, + }); + Ok(()) + } + + fn reserve_passive_data(&mut self, count: u32) -> WasmResult<()> { + self.result.module.passive_elements.reserve(count as usize); + Ok(()) + } + + fn declare_passive_data( + &mut self, + _data_index: PassiveDataIndex, + _data: &'data [u8], + ) -> WasmResult<()> { + Err(WasmError::Unsupported( + "bulk memory: passive data".to_string(), + )) + } + + fn declare_func_name(&mut self, func_index: FuncIndex, name: &'data str) -> WasmResult<()> { + self.result + .module + .func_names + .insert(func_index, name.to_string()); + Ok(()) + } +} + +/// Add environment-specific function parameters. +pub fn translate_signature(mut sig: ir::Signature, pointer_type: ir::Type) -> ir::Signature { + // Prepend the vmctx argument. + sig.params.insert( + 0, + AbiParam::special(pointer_type, ArgumentPurpose::VMContext), + ); + // Prepend the caller vmctx argument. + sig.params.insert(1, AbiParam::new(pointer_type)); + sig +} + +/// A memory index and offset within that memory where a data initialization +/// should is to be performed. +#[derive(Clone)] +pub struct DataInitializerLocation { + /// The index of the memory to initialize. + pub memory_index: MemoryIndex, + + /// Optionally a globalvar base to initialize at. + pub base: Option, + + /// A constant offset to initialize at. + pub offset: usize, +} + +/// A data initializer for linear memory. +pub struct DataInitializer<'data> { + /// The location where the initialization is to be performed. + pub location: DataInitializerLocation, + + /// The initialization data. + pub data: &'data [u8], +} diff --git a/crates/environ/src/tunables.rs b/crates/environ/src/tunables.rs new file mode 100644 index 0000000000..64032f1ddc --- /dev/null +++ b/crates/environ/src/tunables.rs @@ -0,0 +1,44 @@ +/// Tunable parameters for WebAssembly compilation. +#[derive(Clone)] +pub struct Tunables { + /// For static heaps, the size in wasm pages of the heap protected by bounds checking. + pub static_memory_bound: u32, + + /// The size in bytes of the offset guard for static heaps. + pub static_memory_offset_guard_size: u64, + + /// The size in bytes of the offset guard for dynamic heaps. + pub dynamic_memory_offset_guard_size: u64, +} + +impl Default for Tunables { + fn default() -> Self { + Self { + #[cfg(target_pointer_width = "32")] + /// Size in wasm pages of the bound for static memories. + static_memory_bound: 0x4000, + #[cfg(target_pointer_width = "64")] + /// Size in wasm pages of the bound for static memories. + /// + /// When we allocate 4 GiB of address space, we can avoid the + /// need for explicit bounds checks. + static_memory_bound: 0x1_0000, + + #[cfg(target_pointer_width = "32")] + /// Size in bytes of the offset guard for static memories. + static_memory_offset_guard_size: 0x1_0000, + #[cfg(target_pointer_width = "64")] + /// Size in bytes of the offset guard for static memories. + /// + /// Allocating 2 GiB of address space lets us translate wasm + /// offsets into x86 offsets as aggressively as we can. + static_memory_offset_guard_size: 0x8000_0000, + + /// Size in bytes of the offset guard for dynamic memories. + /// + /// Allocate a small guard to optimize common cases but without + /// wasting too much memor. + dynamic_memory_offset_guard_size: 0x1_0000, + } + } +} diff --git a/crates/environ/src/vmoffsets.rs b/crates/environ/src/vmoffsets.rs new file mode 100644 index 0000000000..13c2b90d67 --- /dev/null +++ b/crates/environ/src/vmoffsets.rs @@ -0,0 +1,581 @@ +//! Offsets and sizes of various structs in wasmtime-runtime's vmcontext +//! module. + +use crate::module::ModuleLocal; +use crate::BuiltinFunctionIndex; +use cranelift_codegen::ir; +use cranelift_wasm::{ + DefinedGlobalIndex, DefinedMemoryIndex, DefinedTableIndex, FuncIndex, GlobalIndex, MemoryIndex, + SignatureIndex, TableIndex, +}; +use more_asserts::assert_lt; +use std::convert::TryFrom; + +#[cfg(target_pointer_width = "32")] +fn cast_to_u32(sz: usize) -> u32 { + u32::try_from(sz).unwrap() +} +#[cfg(target_pointer_width = "64")] +fn cast_to_u32(sz: usize) -> u32 { + u32::try_from(sz).expect("overflow in cast from usize to u32") +} + +/// Align an offset used in this module to a specific byte-width by rounding up +fn align(offset: u32, width: u32) -> u32 { + (offset + (width - 1)) / width * width +} + +/// This class computes offsets to fields within `VMContext` and other +/// related structs that JIT code accesses directly. +pub struct VMOffsets { + /// The size in bytes of a pointer on the target. + pub pointer_size: u8, + /// The number of signature declarations in the module. + pub num_signature_ids: u32, + /// The number of imported functions in the module. + pub num_imported_functions: u32, + /// The number of imported tables in the module. + pub num_imported_tables: u32, + /// The number of imported memories in the module. + pub num_imported_memories: u32, + /// The number of imported globals in the module. + pub num_imported_globals: u32, + /// The number of defined tables in the module. + pub num_defined_tables: u32, + /// The number of defined memories in the module. + pub num_defined_memories: u32, + /// The number of defined globals in the module. + pub num_defined_globals: u32, +} + +impl VMOffsets { + /// Return a new `VMOffsets` instance, for a given pointer size. + pub fn new(pointer_size: u8, module: &ModuleLocal) -> Self { + Self { + pointer_size, + num_signature_ids: cast_to_u32(module.signatures.len()), + num_imported_functions: cast_to_u32(module.num_imported_funcs), + num_imported_tables: cast_to_u32(module.num_imported_tables), + num_imported_memories: cast_to_u32(module.num_imported_memories), + num_imported_globals: cast_to_u32(module.num_imported_globals), + num_defined_tables: cast_to_u32(module.table_plans.len()), + num_defined_memories: cast_to_u32(module.memory_plans.len()), + num_defined_globals: cast_to_u32(module.globals.len()), + } + } +} + +/// Offsets for `VMFunctionImport`. +impl VMOffsets { + /// The offset of the `body` field. + #[allow(clippy::erasing_op)] + pub fn vmfunction_import_body(&self) -> u8 { + 0 * self.pointer_size + } + + /// The offset of the `vmctx` field. + #[allow(clippy::identity_op)] + pub fn vmfunction_import_vmctx(&self) -> u8 { + 1 * self.pointer_size + } + + /// Return the size of `VMFunctionImport`. + pub fn size_of_vmfunction_import(&self) -> u8 { + 2 * self.pointer_size + } +} + +/// Offsets for `*const VMFunctionBody`. +impl VMOffsets { + /// The size of the `current_elements` field. + #[allow(clippy::identity_op)] + pub fn size_of_vmfunction_body_ptr(&self) -> u8 { + 1 * self.pointer_size + } +} + +/// Offsets for `VMTableImport`. +impl VMOffsets { + /// The offset of the `from` field. + #[allow(clippy::erasing_op)] + pub fn vmtable_import_from(&self) -> u8 { + 0 * self.pointer_size + } + + /// The offset of the `vmctx` field. + #[allow(clippy::identity_op)] + pub fn vmtable_import_vmctx(&self) -> u8 { + 1 * self.pointer_size + } + + /// Return the size of `VMTableImport`. + pub fn size_of_vmtable_import(&self) -> u8 { + 2 * self.pointer_size + } +} + +/// Offsets for `VMTableDefinition`. +impl VMOffsets { + /// The offset of the `base` field. + #[allow(clippy::erasing_op)] + pub fn vmtable_definition_base(&self) -> u8 { + 0 * self.pointer_size + } + + /// The offset of the `current_elements` field. + #[allow(clippy::identity_op)] + pub fn vmtable_definition_current_elements(&self) -> u8 { + 1 * self.pointer_size + } + + /// The size of the `current_elements` field. + pub fn size_of_vmtable_definition_current_elements(&self) -> u8 { + 4 + } + + /// Return the size of `VMTableDefinition`. + pub fn size_of_vmtable_definition(&self) -> u8 { + 2 * self.pointer_size + } + + /// The type of the `current_elements` field. + pub fn type_of_vmtable_definition_current_elements(&self) -> ir::Type { + ir::Type::int(u16::from(self.size_of_vmtable_definition_current_elements()) * 8).unwrap() + } +} + +/// Offsets for `VMMemoryImport`. +impl VMOffsets { + /// The offset of the `from` field. + #[allow(clippy::erasing_op)] + pub fn vmmemory_import_from(&self) -> u8 { + 0 * self.pointer_size + } + + /// The offset of the `vmctx` field. + #[allow(clippy::identity_op)] + pub fn vmmemory_import_vmctx(&self) -> u8 { + 1 * self.pointer_size + } + + /// Return the size of `VMMemoryImport`. + pub fn size_of_vmmemory_import(&self) -> u8 { + 2 * self.pointer_size + } +} + +/// Offsets for `VMMemoryDefinition`. +impl VMOffsets { + /// The offset of the `base` field. + #[allow(clippy::erasing_op)] + pub fn vmmemory_definition_base(&self) -> u8 { + 0 * self.pointer_size + } + + /// The offset of the `current_length` field. + #[allow(clippy::identity_op)] + pub fn vmmemory_definition_current_length(&self) -> u8 { + 1 * self.pointer_size + } + + /// The size of the `current_length` field. + pub fn size_of_vmmemory_definition_current_length(&self) -> u8 { + 4 + } + + /// Return the size of `VMMemoryDefinition`. + pub fn size_of_vmmemory_definition(&self) -> u8 { + 2 * self.pointer_size + } + + /// The type of the `current_length` field. + pub fn type_of_vmmemory_definition_current_length(&self) -> ir::Type { + ir::Type::int(u16::from(self.size_of_vmmemory_definition_current_length()) * 8).unwrap() + } +} + +/// Offsets for `VMGlobalImport`. +impl VMOffsets { + /// The offset of the `from` field. + #[allow(clippy::erasing_op)] + pub fn vmglobal_import_from(&self) -> u8 { + 0 * self.pointer_size + } + + /// Return the size of `VMGlobalImport`. + #[allow(clippy::identity_op)] + pub fn size_of_vmglobal_import(&self) -> u8 { + 1 * self.pointer_size + } +} + +/// Offsets for `VMGlobalDefinition`. +impl VMOffsets { + /// Return the size of `VMGlobalDefinition`; this is the size of the largest value type (i.e. a + /// V128). + pub fn size_of_vmglobal_definition(&self) -> u8 { + 16 + } +} + +/// Offsets for `VMSharedSignatureIndex`. +impl VMOffsets { + /// Return the size of `VMSharedSignatureIndex`. + pub fn size_of_vmshared_signature_index(&self) -> u8 { + 4 + } +} + +/// Offsets for `VMCallerCheckedAnyfunc`. +impl VMOffsets { + /// The offset of the `func_ptr` field. + #[allow(clippy::erasing_op)] + pub fn vmcaller_checked_anyfunc_func_ptr(&self) -> u8 { + 0 * self.pointer_size + } + + /// The offset of the `type_index` field. + #[allow(clippy::identity_op)] + pub fn vmcaller_checked_anyfunc_type_index(&self) -> u8 { + 1 * self.pointer_size + } + + /// The offset of the `vmctx` field. + pub fn vmcaller_checked_anyfunc_vmctx(&self) -> u8 { + 2 * self.pointer_size + } + + /// Return the size of `VMCallerCheckedAnyfunc`. + pub fn size_of_vmcaller_checked_anyfunc(&self) -> u8 { + 3 * self.pointer_size + } +} + +/// Offsets for `VMContext`. +impl VMOffsets { + /// The offset of the `signature_ids` array. + pub fn vmctx_signature_ids_begin(&self) -> u32 { + 0 + } + + /// The offset of the `tables` array. + #[allow(clippy::erasing_op)] + pub fn vmctx_imported_functions_begin(&self) -> u32 { + self.vmctx_signature_ids_begin() + .checked_add( + self.num_signature_ids + .checked_mul(u32::from(self.size_of_vmshared_signature_index())) + .unwrap(), + ) + .unwrap() + } + + /// The offset of the `tables` array. + #[allow(clippy::identity_op)] + pub fn vmctx_imported_tables_begin(&self) -> u32 { + self.vmctx_imported_functions_begin() + .checked_add( + self.num_imported_functions + .checked_mul(u32::from(self.size_of_vmfunction_import())) + .unwrap(), + ) + .unwrap() + } + + /// The offset of the `memories` array. + pub fn vmctx_imported_memories_begin(&self) -> u32 { + self.vmctx_imported_tables_begin() + .checked_add( + self.num_imported_tables + .checked_mul(u32::from(self.size_of_vmtable_import())) + .unwrap(), + ) + .unwrap() + } + + /// The offset of the `globals` array. + pub fn vmctx_imported_globals_begin(&self) -> u32 { + self.vmctx_imported_memories_begin() + .checked_add( + self.num_imported_memories + .checked_mul(u32::from(self.size_of_vmmemory_import())) + .unwrap(), + ) + .unwrap() + } + + /// The offset of the `tables` array. + pub fn vmctx_tables_begin(&self) -> u32 { + self.vmctx_imported_globals_begin() + .checked_add( + self.num_imported_globals + .checked_mul(u32::from(self.size_of_vmglobal_import())) + .unwrap(), + ) + .unwrap() + } + + /// The offset of the `memories` array. + pub fn vmctx_memories_begin(&self) -> u32 { + self.vmctx_tables_begin() + .checked_add( + self.num_defined_tables + .checked_mul(u32::from(self.size_of_vmtable_definition())) + .unwrap(), + ) + .unwrap() + } + + /// The offset of the `globals` array. + pub fn vmctx_globals_begin(&self) -> u32 { + let offset = self + .vmctx_memories_begin() + .checked_add( + self.num_defined_memories + .checked_mul(u32::from(self.size_of_vmmemory_definition())) + .unwrap(), + ) + .unwrap(); + align(offset, 16) + } + + /// The offset of the builtin functions array. + pub fn vmctx_builtin_functions_begin(&self) -> u32 { + self.vmctx_globals_begin() + .checked_add( + self.num_defined_globals + .checked_mul(u32::from(self.size_of_vmglobal_definition())) + .unwrap(), + ) + .unwrap() + } + + /// Return the size of the `VMContext` allocation. + pub fn size_of_vmctx(&self) -> u32 { + self.vmctx_builtin_functions_begin() + .checked_add( + BuiltinFunctionIndex::builtin_functions_total_number() + .checked_mul(u32::from(self.pointer_size)) + .unwrap(), + ) + .unwrap() + } + + /// Return the offset to `VMSharedSignatureId` index `index`. + pub fn vmctx_vmshared_signature_id(&self, index: SignatureIndex) -> u32 { + assert_lt!(index.as_u32(), self.num_signature_ids); + self.vmctx_signature_ids_begin() + .checked_add( + index + .as_u32() + .checked_mul(u32::from(self.size_of_vmshared_signature_index())) + .unwrap(), + ) + .unwrap() + } + + /// Return the offset to `VMFunctionImport` index `index`. + pub fn vmctx_vmfunction_import(&self, index: FuncIndex) -> u32 { + assert_lt!(index.as_u32(), self.num_imported_functions); + self.vmctx_imported_functions_begin() + .checked_add( + index + .as_u32() + .checked_mul(u32::from(self.size_of_vmfunction_import())) + .unwrap(), + ) + .unwrap() + } + + /// Return the offset to `VMTableImport` index `index`. + pub fn vmctx_vmtable_import(&self, index: TableIndex) -> u32 { + assert_lt!(index.as_u32(), self.num_imported_tables); + self.vmctx_imported_tables_begin() + .checked_add( + index + .as_u32() + .checked_mul(u32::from(self.size_of_vmtable_import())) + .unwrap(), + ) + .unwrap() + } + + /// Return the offset to `VMMemoryImport` index `index`. + pub fn vmctx_vmmemory_import(&self, index: MemoryIndex) -> u32 { + assert_lt!(index.as_u32(), self.num_imported_memories); + self.vmctx_imported_memories_begin() + .checked_add( + index + .as_u32() + .checked_mul(u32::from(self.size_of_vmmemory_import())) + .unwrap(), + ) + .unwrap() + } + + /// Return the offset to `VMGlobalImport` index `index`. + pub fn vmctx_vmglobal_import(&self, index: GlobalIndex) -> u32 { + assert_lt!(index.as_u32(), self.num_imported_globals); + self.vmctx_imported_globals_begin() + .checked_add( + index + .as_u32() + .checked_mul(u32::from(self.size_of_vmglobal_import())) + .unwrap(), + ) + .unwrap() + } + + /// Return the offset to `VMTableDefinition` index `index`. + pub fn vmctx_vmtable_definition(&self, index: DefinedTableIndex) -> u32 { + assert_lt!(index.as_u32(), self.num_defined_tables); + self.vmctx_tables_begin() + .checked_add( + index + .as_u32() + .checked_mul(u32::from(self.size_of_vmtable_definition())) + .unwrap(), + ) + .unwrap() + } + + /// Return the offset to `VMMemoryDefinition` index `index`. + pub fn vmctx_vmmemory_definition(&self, index: DefinedMemoryIndex) -> u32 { + assert_lt!(index.as_u32(), self.num_defined_memories); + self.vmctx_memories_begin() + .checked_add( + index + .as_u32() + .checked_mul(u32::from(self.size_of_vmmemory_definition())) + .unwrap(), + ) + .unwrap() + } + + /// Return the offset to the `VMGlobalDefinition` index `index`. + pub fn vmctx_vmglobal_definition(&self, index: DefinedGlobalIndex) -> u32 { + assert_lt!(index.as_u32(), self.num_defined_globals); + self.vmctx_globals_begin() + .checked_add( + index + .as_u32() + .checked_mul(u32::from(self.size_of_vmglobal_definition())) + .unwrap(), + ) + .unwrap() + } + + /// Return the offset to the `body` field in `*const VMFunctionBody` index `index`. + pub fn vmctx_vmfunction_import_body(&self, index: FuncIndex) -> u32 { + self.vmctx_vmfunction_import(index) + .checked_add(u32::from(self.vmfunction_import_body())) + .unwrap() + } + + /// Return the offset to the `vmctx` field in `*const VMFunctionBody` index `index`. + pub fn vmctx_vmfunction_import_vmctx(&self, index: FuncIndex) -> u32 { + self.vmctx_vmfunction_import(index) + .checked_add(u32::from(self.vmfunction_import_vmctx())) + .unwrap() + } + + /// Return the offset to the `from` field in `VMTableImport` index `index`. + pub fn vmctx_vmtable_import_from(&self, index: TableIndex) -> u32 { + self.vmctx_vmtable_import(index) + .checked_add(u32::from(self.vmtable_import_from())) + .unwrap() + } + + /// Return the offset to the `base` field in `VMTableDefinition` index `index`. + pub fn vmctx_vmtable_definition_base(&self, index: DefinedTableIndex) -> u32 { + self.vmctx_vmtable_definition(index) + .checked_add(u32::from(self.vmtable_definition_base())) + .unwrap() + } + + /// Return the offset to the `current_elements` field in `VMTableDefinition` index `index`. + pub fn vmctx_vmtable_definition_current_elements(&self, index: DefinedTableIndex) -> u32 { + self.vmctx_vmtable_definition(index) + .checked_add(u32::from(self.vmtable_definition_current_elements())) + .unwrap() + } + + /// Return the offset to the `from` field in `VMMemoryImport` index `index`. + pub fn vmctx_vmmemory_import_from(&self, index: MemoryIndex) -> u32 { + self.vmctx_vmmemory_import(index) + .checked_add(u32::from(self.vmmemory_import_from())) + .unwrap() + } + + /// Return the offset to the `vmctx` field in `VMMemoryImport` index `index`. + pub fn vmctx_vmmemory_import_vmctx(&self, index: MemoryIndex) -> u32 { + self.vmctx_vmmemory_import(index) + .checked_add(u32::from(self.vmmemory_import_vmctx())) + .unwrap() + } + + /// Return the offset to the `base` field in `VMMemoryDefinition` index `index`. + pub fn vmctx_vmmemory_definition_base(&self, index: DefinedMemoryIndex) -> u32 { + self.vmctx_vmmemory_definition(index) + .checked_add(u32::from(self.vmmemory_definition_base())) + .unwrap() + } + + /// Return the offset to the `current_length` field in `VMMemoryDefinition` index `index`. + pub fn vmctx_vmmemory_definition_current_length(&self, index: DefinedMemoryIndex) -> u32 { + self.vmctx_vmmemory_definition(index) + .checked_add(u32::from(self.vmmemory_definition_current_length())) + .unwrap() + } + + /// Return the offset to the `from` field in `VMGlobalImport` index `index`. + pub fn vmctx_vmglobal_import_from(&self, index: GlobalIndex) -> u32 { + self.vmctx_vmglobal_import(index) + .checked_add(u32::from(self.vmglobal_import_from())) + .unwrap() + } + + /// Return the offset to builtin function in `VMBuiltinFunctionsArray` index `index`. + pub fn vmctx_builtin_function(&self, index: BuiltinFunctionIndex) -> u32 { + self.vmctx_builtin_functions_begin() + .checked_add( + index + .index() + .checked_mul(u32::from(self.pointer_size)) + .unwrap(), + ) + .unwrap() + } +} + +/// Target specific type for shared signature index. +#[derive(Debug, Copy, Clone)] +pub struct TargetSharedSignatureIndex(u32); + +impl TargetSharedSignatureIndex { + /// Constructs `TargetSharedSignatureIndex`. + pub fn new(value: u32) -> Self { + Self(value) + } + + /// Returns index value. + pub fn index(self) -> u32 { + self.0 + } +} + +#[cfg(test)] +mod tests { + use crate::vmoffsets::align; + + #[test] + fn alignment() { + fn is_aligned(x: u32) -> bool { + x % 16 == 0 + } + assert!(is_aligned(align(0, 16))); + assert!(is_aligned(align(32, 16))); + assert!(is_aligned(align(33, 16))); + assert!(is_aligned(align(31, 16))); + } +} diff --git a/crates/environ/tests/cache_write_default_config.rs b/crates/environ/tests/cache_write_default_config.rs new file mode 100644 index 0000000000..1f1592fdb5 --- /dev/null +++ b/crates/environ/tests/cache_write_default_config.rs @@ -0,0 +1,13 @@ +use tempfile; +use wasmtime_environ::cache_create_new_config; + +#[test] +fn test_cache_write_default_config() { + let dir = tempfile::tempdir().expect("Can't create temporary directory"); + let config_path = dir.path().join("cache-config.toml"); + + let result = cache_create_new_config(Some(&config_path)); + assert!(result.is_ok()); + assert!(config_path.exists()); + assert_eq!(config_path, result.unwrap()); +} diff --git a/crates/fuzzing/Cargo.toml b/crates/fuzzing/Cargo.toml new file mode 100644 index 0000000000..74eec7c213 --- /dev/null +++ b/crates/fuzzing/Cargo.toml @@ -0,0 +1,20 @@ +[package] +authors = ["The Wasmtime Project Developers"] +description = "Fuzzing infrastructure for Wasmtime" +edition = "2018" +name = "wasmtime-fuzzing" +publish = false +version = "0.12.0" + +[dependencies] +anyhow = "1.0.22" +arbitrary = { version = "0.3.2", features = ["derive"] } +binaryen = "0.10.0" +env_logger = "0.7.1" +log = "0.4.8" +wasmparser = "0.51.2" +wasmprinter = "0.2.1" +wasmtime = { path = "../api", version = "0.12.0" } + +[dev-dependencies] +wat = "1.0.10" diff --git a/crates/fuzzing/README.md b/crates/fuzzing/README.md new file mode 100644 index 0000000000..12c5852cf5 --- /dev/null +++ b/crates/fuzzing/README.md @@ -0,0 +1,14 @@ +# Fuzzing Infrastructure for Wasmtime + +This crate provides test case generators and oracles for use with fuzzing. + +These generators and oracles are generally independent of the fuzzing engine +that might be using them and driving the whole fuzzing process (e.g. libFuzzer +or AFL). As such, this crate does *not* contain any actual fuzz targets +itself. Those are generally just a couple lines of glue code that plug raw input +from (for example) `libFuzzer` into a generator, and then run one or more +oracles on the generated test case. + +If you're looking for the actual fuzz target definitions we currently have, they +live in `wasmtime/fuzz/fuzz_targets/*` and are driven by `cargo fuzz` and +`libFuzzer`. diff --git a/crates/fuzzing/src/generators.rs b/crates/fuzzing/src/generators.rs new file mode 100644 index 0000000000..c3813a1b6e --- /dev/null +++ b/crates/fuzzing/src/generators.rs @@ -0,0 +1,85 @@ +//! Test case generators. +//! +//! Test case generators take raw, unstructured input from a fuzzer +//! (e.g. libFuzzer) and translate that into a structured test case (e.g. a +//! valid Wasm binary). +//! +//! These are generally implementations of the `Arbitrary` trait, or some +//! wrapper over an external tool, such that the wrapper implements the +//! `Arbitrary` trait for the wrapped external tool. + +pub mod api; + +use arbitrary::{Arbitrary, Unstructured}; +use std::fmt; + +/// A Wasm test case generator that is powered by Binaryen's `wasm-opt -ttf`. +#[derive(Clone)] +pub struct WasmOptTtf { + /// The raw, encoded Wasm bytes. + pub wasm: Vec, +} + +impl fmt::Debug for WasmOptTtf { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "WasmOptTtf {{ wasm: wat::parse_str(r###\"\n{}\n\"###).unwrap() }}", + wasmprinter::print_bytes(&self.wasm).expect("valid wasm should always disassemble") + ) + } +} + +impl Arbitrary for WasmOptTtf { + fn arbitrary(input: &mut Unstructured) -> arbitrary::Result { + let seed: Vec = Arbitrary::arbitrary(input)?; + let module = binaryen::tools::translate_to_fuzz_mvp(&seed); + let wasm = module.write(); + Ok(WasmOptTtf { wasm }) + } + + fn arbitrary_take_rest(input: Unstructured) -> arbitrary::Result { + let seed: Vec = Arbitrary::arbitrary_take_rest(input)?; + let module = binaryen::tools::translate_to_fuzz_mvp(&seed); + let wasm = module.write(); + Ok(WasmOptTtf { wasm }) + } +} + +/// A description of configuration options that we should do differential +/// testing between. +#[derive(Arbitrary, Clone, Debug, PartialEq, Eq, Hash)] +pub struct DifferentialConfig { + strategy: DifferentialStrategy, + opt_level: DifferentialOptLevel, +} + +impl DifferentialConfig { + /// Convert this differential fuzzing config into a `wasmtime::Config`. + pub fn to_wasmtime_config(&self) -> anyhow::Result { + let mut config = wasmtime::Config::new(); + config.strategy(match self.strategy { + DifferentialStrategy::Cranelift => wasmtime::Strategy::Cranelift, + DifferentialStrategy::Lightbeam => wasmtime::Strategy::Lightbeam, + })?; + config.cranelift_opt_level(match self.opt_level { + DifferentialOptLevel::None => wasmtime::OptLevel::None, + DifferentialOptLevel::Speed => wasmtime::OptLevel::Speed, + DifferentialOptLevel::SpeedAndSize => wasmtime::OptLevel::SpeedAndSize, + }); + Ok(config) + } +} + +#[derive(Arbitrary, Clone, Debug, PartialEq, Eq, Hash)] +enum DifferentialStrategy { + Cranelift, + Lightbeam, +} + +#[derive(Arbitrary, Clone, Debug, PartialEq, Eq, Hash)] +enum DifferentialOptLevel { + None, + Speed, + SpeedAndSize, +} diff --git a/crates/fuzzing/src/generators/api.rs b/crates/fuzzing/src/generators/api.rs new file mode 100644 index 0000000000..9ffe8c5e26 --- /dev/null +++ b/crates/fuzzing/src/generators/api.rs @@ -0,0 +1,204 @@ +//! Generating sequences of Wasmtime API calls. +//! +//! We only generate *valid* sequences of API calls. To do this, we keep track +//! of what objects we've already created in earlier API calls via the `Scope` +//! struct. +//! +//! To generate even-more-pathological sequences of API calls, we use [swarm +//! testing]: +//! +//! > In swarm testing, the usual practice of potentially including all features +//! > in every test case is abandoned. Rather, a large “swarm” of randomly +//! > generated configurations, each of which omits some features, is used, with +//! > configurations receiving equal resources. +//! +//! [swarm testing]: https://www.cs.utah.edu/~regehr/papers/swarm12.pdf + +use arbitrary::{Arbitrary, Unstructured}; +use std::collections::BTreeMap; +use std::mem; +use wasmparser::*; + +#[derive(Arbitrary, Debug)] +struct Swarm { + config_debug_info: bool, + module_new: bool, + module_drop: bool, + instance_new: bool, + instance_drop: bool, + call_exported_func: bool, +} + +/// A call to one of Wasmtime's public APIs. +#[derive(Arbitrary, Clone, Debug)] +#[allow(missing_docs)] +pub enum ApiCall { + ConfigNew, + ConfigDebugInfo(bool), + EngineNew, + StoreNew, + ModuleNew { id: usize, wasm: super::WasmOptTtf }, + ModuleDrop { id: usize }, + InstanceNew { id: usize, module: usize }, + InstanceDrop { id: usize }, + CallExportedFunc { instance: usize, nth: usize }, +} +use ApiCall::*; + +#[derive(Default)] +struct Scope { + id_counter: usize, + predicted_rss: usize, + /// Map from a module id to the predicted amount of rss it will take to + /// instantiate. + modules: BTreeMap, + /// Map from an instance id to the amount of rss it's expected to be using. + instances: BTreeMap, +} + +impl Scope { + fn next_id(&mut self) -> usize { + let id = self.id_counter; + self.id_counter = id + 1; + id + } +} + +/// A sequence of API calls. +#[derive(Debug)] +pub struct ApiCalls { + /// The API calls. + pub calls: Vec, +} + +impl Arbitrary for ApiCalls { + fn arbitrary(input: &mut Unstructured) -> arbitrary::Result { + let swarm = Swarm::arbitrary(input)?; + let mut calls = vec![]; + + arbitrary_config(input, &swarm, &mut calls)?; + calls.push(EngineNew); + calls.push(StoreNew); + + let mut scope = Scope::default(); + let max_rss = 1 << 30; // 1GB + + for _ in 0..input.arbitrary_len::()? { + let mut choices: Vec arbitrary::Result> = vec![]; + + if swarm.module_new { + choices.push(|input, scope| { + let id = scope.next_id(); + let wasm = super::WasmOptTtf::arbitrary(input)?; + let predicted_rss = predict_rss(&wasm.wasm).unwrap_or(0); + scope.modules.insert(id, predicted_rss); + Ok(ModuleNew { id, wasm }) + }); + } + if swarm.module_drop && !scope.modules.is_empty() { + choices.push(|input, scope| { + let modules: Vec<_> = scope.modules.keys().collect(); + let id = **input.choose(&modules)?; + scope.modules.remove(&id); + Ok(ModuleDrop { id }) + }); + } + if swarm.instance_new && !scope.modules.is_empty() && scope.predicted_rss < max_rss { + choices.push(|input, scope| { + let modules: Vec<_> = scope.modules.iter().collect(); + let (&module, &predicted_rss) = *input.choose(&modules)?; + let id = scope.next_id(); + scope.instances.insert(id, predicted_rss); + scope.predicted_rss += predicted_rss; + Ok(InstanceNew { id, module }) + }); + } + if swarm.instance_drop && !scope.instances.is_empty() { + choices.push(|input, scope| { + let instances: Vec<_> = scope.instances.iter().collect(); + let (&id, &rss) = *input.choose(&instances)?; + scope.instances.remove(&id); + scope.predicted_rss -= rss; + Ok(InstanceDrop { id }) + }); + } + if swarm.call_exported_func && !scope.instances.is_empty() { + choices.push(|input, scope| { + let instances: Vec<_> = scope.instances.keys().collect(); + let instance = **input.choose(&instances)?; + let nth = usize::arbitrary(input)?; + Ok(CallExportedFunc { instance, nth }) + }); + } + + if choices.is_empty() { + break; + } + let c = input.choose(&choices)?; + calls.push(c(input, &mut scope)?); + } + + Ok(ApiCalls { calls }) + } +} + +fn arbitrary_config( + input: &mut Unstructured, + swarm: &Swarm, + calls: &mut Vec, +) -> arbitrary::Result<()> { + calls.push(ConfigNew); + + if swarm.config_debug_info && bool::arbitrary(input)? { + calls.push(ConfigDebugInfo(bool::arbitrary(input)?)); + } + + // TODO: flags, features, and compilation strategy. + + Ok(()) +} + +/// Attempt to heuristically predict how much rss instantiating the `wasm` +/// provided will take in wasmtime. +/// +/// The intention of this function is to prevent out-of-memory situations from +/// trivially instantiating a bunch of modules. We're basically taking any +/// random sequence of fuzz inputs and generating API calls, but if we +/// instantiate a million things we'd reasonably expect that to exceed the fuzz +/// limit of 2GB because, well, instantiation does take a bit of memory. +/// +/// This prediction will prevent new instances from being created once we've +/// created a bunch of instances. Once instances start being dropped, though, +/// it'll free up new slots to start making new instances. +fn predict_rss(wasm: &[u8]) -> Result { + let mut prediction = 0; + let mut reader = ModuleReader::new(wasm)?; + while !reader.eof() { + let section = reader.read()?; + match section.code { + // For each declared memory we'll have to map that all in, so add in + // the minimum amount of memory to our predicted rss. + SectionCode::Memory => { + for entry in section.get_memory_section_reader()? { + let initial = entry?.limits.initial as usize; + prediction += initial * 64 * 1024; + } + } + + // We'll need to allocate tables and space for table elements, and + // currently this is 3 pointers per table entry. + SectionCode::Table => { + for entry in section.get_table_section_reader()? { + let initial = entry?.limits.initial as usize; + prediction += initial * 3 * mem::size_of::(); + } + } + + // ... and for now nothing else is counted. If we run into issues + // with the fuzzers though we can always try to take into account + // more things + _ => {} + } + } + Ok(prediction) +} diff --git a/crates/fuzzing/src/lib.rs b/crates/fuzzing/src/lib.rs new file mode 100644 index 0000000000..f036efe617 --- /dev/null +++ b/crates/fuzzing/src/lib.rs @@ -0,0 +1,6 @@ +//! Fuzzing infrastructure for Wasmtime. + +#![deny(missing_docs, missing_debug_implementations)] + +pub mod generators; +pub mod oracles; diff --git a/crates/fuzzing/src/oracles.rs b/crates/fuzzing/src/oracles.rs new file mode 100644 index 0000000000..3af5ea8fa0 --- /dev/null +++ b/crates/fuzzing/src/oracles.rs @@ -0,0 +1,404 @@ +//! Oracles. +//! +//! Oracles take a test case and determine whether we have a bug. For example, +//! one of the simplest oracles is to take a Wasm binary as our input test case, +//! validate and instantiate it, and (implicitly) check that no assertions +//! failed or segfaults happened. A more complicated oracle might compare the +//! result of executing a Wasm file with and without optimizations enabled, and +//! make sure that the two executions are observably identical. +//! +//! When an oracle finds a bug, it should report it to the fuzzing engine by +//! panicking. + +pub mod dummy; + +use dummy::{dummy_imports, dummy_values}; +use std::collections::{HashMap, HashSet}; +use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; +use wasmtime::*; + +fn fuzz_default_config(strategy: Strategy) -> Config { + drop(env_logger::try_init()); + let mut config = Config::new(); + config + .cranelift_debug_verifier(true) + .wasm_multi_value(true) + .strategy(strategy) + .expect("failed to enable lightbeam"); + return config; +} + +fn log_wasm(wasm: &[u8]) { + static CNT: AtomicUsize = AtomicUsize::new(0); + if !log::log_enabled!(log::Level::Debug) { + return; + } + + let i = CNT.fetch_add(1, SeqCst); + let name = format!("testcase{}.wasm", i); + std::fs::write(&name, wasm).expect("failed to write wasm file"); + log::debug!("wrote wasm file to `{}`", name); + if let Ok(s) = wasmprinter::print_bytes(wasm) { + let name = format!("testcase{}.wat", i); + std::fs::write(&name, s).expect("failed to write wat file"); + } +} + +/// Instantiate the Wasm buffer, and implicitly fail if we have an unexpected +/// panic or segfault or anything else that can be detected "passively". +/// +/// Performs initial validation, and returns early if the Wasm is invalid. +/// +/// You can control which compiler is used via passing a `Strategy`. +pub fn instantiate(wasm: &[u8], strategy: Strategy) { + instantiate_with_config(wasm, fuzz_default_config(strategy)); +} + +/// Instantiate the Wasm buffer, and implicitly fail if we have an unexpected +/// panic or segfault or anything else that can be detected "passively". +/// +/// The engine will be configured using provided config. +/// +/// See also `instantiate` functions. +pub fn instantiate_with_config(wasm: &[u8], config: Config) { + let engine = Engine::new(&config); + let store = Store::new(&engine); + + log_wasm(wasm); + let module = match Module::new(&store, wasm) { + Ok(module) => module, + Err(_) => return, + }; + + let imports = match dummy_imports(&store, module.imports()) { + Ok(imps) => imps, + Err(_) => { + // There are some value types that we can't synthesize a + // dummy value for (e.g. anyrefs) and for modules that + // import things of these types we skip instantiation. + return; + } + }; + + // Don't unwrap this: there can be instantiation-/link-time errors that + // aren't caught during validation or compilation. For example, an imported + // table might not have room for an element segment that we want to + // initialize into it. + let _result = Instance::new(&module, &imports); +} + +/// Compile the Wasm buffer, and implicitly fail if we have an unexpected +/// panic or segfault or anything else that can be detected "passively". +/// +/// Performs initial validation, and returns early if the Wasm is invalid. +/// +/// You can control which compiler is used via passing a `Strategy`. +pub fn compile(wasm: &[u8], strategy: Strategy) { + let engine = Engine::new(&fuzz_default_config(strategy)); + let store = Store::new(&engine); + log_wasm(wasm); + let _ = Module::new(&store, wasm); +} + +/// Instantiate the given Wasm module with each `Config` and call all of its +/// exports. Modulo OOM, non-canonical NaNs, and usage of Wasm features that are +/// or aren't enabled for different configs, we should get the same results when +/// we call the exported functions for all of our different configs. +pub fn differential_execution( + ttf: &crate::generators::WasmOptTtf, + configs: &[crate::generators::DifferentialConfig], +) { + drop(env_logger::try_init()); + // We need at least two configs. + if configs.len() < 2 + // And all the configs should be unique. + || configs.iter().collect::>().len() != configs.len() + { + return; + } + + let configs: Vec<_> = match configs.iter().map(|c| c.to_wasmtime_config()).collect() { + Ok(cs) => cs, + // If the config is trying to use something that was turned off at + // compile time, eg lightbeam, just continue to the next fuzz input. + Err(_) => return, + }; + + let mut export_func_results: HashMap, Trap>> = Default::default(); + log_wasm(&ttf.wasm); + + for config in &configs { + let engine = Engine::new(config); + let store = Store::new(&engine); + + let module = match Module::new(&store, &ttf.wasm) { + Ok(module) => module, + // The module might rely on some feature that our config didn't + // enable or something like that. + Err(e) => { + eprintln!("Warning: failed to compile `wasm-opt -ttf` module: {}", e); + continue; + } + }; + + // TODO: we should implement tracing versions of these dummy imports + // that record a trace of the order that imported functions were called + // in and with what values. Like the results of exported functions, + // calls to imports should also yield the same values for each + // configuration, and we should assert that. + let imports = match dummy_imports(&store, module.imports()) { + Ok(imps) => imps, + Err(e) => { + // There are some value types that we can't synthesize a + // dummy value for (e.g. anyrefs) and for modules that + // import things of these types we skip instantiation. + eprintln!("Warning: failed to synthesize dummy imports: {}", e); + continue; + } + }; + + // Don't unwrap this: there can be instantiation-/link-time errors that + // aren't caught during validation or compilation. For example, an imported + // table might not have room for an element segment that we want to + // initialize into it. + let instance = match Instance::new(&module, &imports) { + Ok(instance) => instance, + Err(e) => { + eprintln!( + "Warning: failed to instantiate `wasm-opt -ttf` module: {}", + e + ); + continue; + } + }; + + let funcs = module + .exports() + .iter() + .filter_map(|e| { + if let ExternType::Func(_) = e.ty() { + Some(e.name()) + } else { + None + } + }) + .collect::>(); + + for name in funcs { + // Always call the hang limit initializer first, so that we don't + // infinite loop when calling another export. + init_hang_limit(&instance); + + let f = match instance + .get_export(&name) + .expect("instance should have export from module") + { + Extern::Func(f) => f.clone(), + _ => panic!("export should be a function"), + }; + + let ty = f.ty(); + let params = match dummy_values(ty.params()) { + Ok(p) => p, + Err(_) => continue, + }; + let this_result = f.call(¶ms); + + let existing_result = export_func_results + .entry(name.to_string()) + .or_insert_with(|| this_result.clone()); + assert_same_export_func_result(&existing_result, &this_result, name); + } + } +} + +fn init_hang_limit(instance: &Instance) { + match instance.get_export("hangLimitInitializer") { + None => return, + Some(Extern::Func(f)) => { + f.call(&[]) + .expect("initializing the hang limit should not fail"); + } + Some(_) => panic!("unexpected hangLimitInitializer export"), + } +} + +fn assert_same_export_func_result( + lhs: &Result, Trap>, + rhs: &Result, Trap>, + func_name: &str, +) { + let fail = || { + panic!( + "differential fuzzing failed: exported func {} returned two \ + different results: {:?} != {:?}", + func_name, lhs, rhs + ) + }; + + match (lhs, rhs) { + (Err(_), Err(_)) => {} + (Ok(lhs), Ok(rhs)) => { + if lhs.len() != rhs.len() { + fail(); + } + for (lhs, rhs) in lhs.iter().zip(rhs.iter()) { + match (lhs, rhs) { + (Val::I32(lhs), Val::I32(rhs)) if lhs == rhs => continue, + (Val::I64(lhs), Val::I64(rhs)) if lhs == rhs => continue, + (Val::V128(lhs), Val::V128(rhs)) if lhs == rhs => continue, + (Val::F32(lhs), Val::F32(rhs)) => { + let lhs = f32::from_bits(*lhs); + let rhs = f32::from_bits(*rhs); + if lhs == rhs || (lhs.is_nan() && rhs.is_nan()) { + continue; + } else { + fail() + } + } + (Val::F64(lhs), Val::F64(rhs)) => { + let lhs = f64::from_bits(*lhs); + let rhs = f64::from_bits(*rhs); + if lhs == rhs || (lhs.is_nan() && rhs.is_nan()) { + continue; + } else { + fail() + } + } + (Val::AnyRef(_), Val::AnyRef(_)) | (Val::FuncRef(_), Val::FuncRef(_)) => { + continue + } + _ => fail(), + } + } + } + _ => fail(), + } +} + +/// Invoke the given API calls. +pub fn make_api_calls(api: crate::generators::api::ApiCalls) { + use crate::generators::api::ApiCall; + + drop(env_logger::try_init()); + + let mut config: Option = None; + let mut engine: Option = None; + let mut store: Option = None; + let mut modules: HashMap = Default::default(); + let mut instances: HashMap = Default::default(); + + for call in api.calls { + match call { + ApiCall::ConfigNew => { + log::trace!("creating config"); + assert!(config.is_none()); + let mut cfg = Config::new(); + cfg.cranelift_debug_verifier(true); + config = Some(cfg); + } + + ApiCall::ConfigDebugInfo(b) => { + log::trace!("enabling debuginfo"); + config.as_mut().unwrap().debug_info(b); + } + + ApiCall::EngineNew => { + log::trace!("creating engine"); + assert!(engine.is_none()); + engine = Some(Engine::new(config.as_ref().unwrap())); + } + + ApiCall::StoreNew => { + log::trace!("creating store"); + assert!(store.is_none()); + store = Some(Store::new(engine.as_ref().unwrap())); + } + + ApiCall::ModuleNew { id, wasm } => { + log::debug!("creating module: {}", id); + log_wasm(&wasm.wasm); + let module = match Module::new(store.as_ref().unwrap(), &wasm.wasm) { + Ok(m) => m, + Err(_) => continue, + }; + let old = modules.insert(id, module); + assert!(old.is_none()); + } + + ApiCall::ModuleDrop { id } => { + log::trace!("dropping module: {}", id); + drop(modules.remove(&id)); + } + + ApiCall::InstanceNew { id, module } => { + log::trace!("instantiating module {} as {}", module, id); + let module = match modules.get(&module) { + Some(m) => m, + None => continue, + }; + + let imports = match dummy_imports(store.as_ref().unwrap(), module.imports()) { + Ok(imps) => imps, + Err(_) => { + // There are some value types that we can't synthesize a + // dummy value for (e.g. anyrefs) and for modules that + // import things of these types we skip instantiation. + continue; + } + }; + + // Don't unwrap this: there can be instantiation-/link-time errors that + // aren't caught during validation or compilation. For example, an imported + // table might not have room for an element segment that we want to + // initialize into it. + if let Ok(instance) = Instance::new(&module, &imports) { + instances.insert(id, instance); + } + } + + ApiCall::InstanceDrop { id } => { + log::trace!("dropping instance {}", id); + drop(instances.remove(&id)); + } + + ApiCall::CallExportedFunc { instance, nth } => { + log::trace!("calling instance export {} / {}", instance, nth); + let instance = match instances.get(&instance) { + Some(i) => i, + None => { + // Note that we aren't guaranteed to instantiate valid + // modules, see comments in `InstanceNew` for details on + // that. But the API call generator can't know if + // instantiation failed, so we might not actually have + // this instance. When that's the case, just skip the + // API call and keep going. + continue; + } + }; + + let funcs = instance + .exports() + .iter() + .filter_map(|e| match e { + Extern::Func(f) => Some(f.clone()), + _ => None, + }) + .collect::>(); + + if funcs.is_empty() { + continue; + } + + let nth = nth % funcs.len(); + let f = &funcs[nth]; + let ty = f.ty(); + let params = match dummy_values(ty.params()) { + Ok(p) => p, + Err(_) => continue, + }; + let _ = f.call(¶ms); + } + } + } +} diff --git a/crates/fuzzing/src/oracles/dummy.rs b/crates/fuzzing/src/oracles/dummy.rs new file mode 100644 index 0000000000..b06297d376 --- /dev/null +++ b/crates/fuzzing/src/oracles/dummy.rs @@ -0,0 +1,93 @@ +//! Dummy implementations of things that a Wasm module can import. + +use std::rc::Rc; +use wasmtime::{ + Callable, Extern, ExternType, Func, FuncType, Global, GlobalType, ImportType, Memory, + MemoryType, Store, Table, TableType, Trap, Val, ValType, +}; + +/// Create a set of dummy functions/globals/etc for the given imports. +pub fn dummy_imports(store: &Store, import_tys: &[ImportType]) -> Result, Trap> { + let mut imports = Vec::with_capacity(import_tys.len()); + for imp in import_tys { + imports.push(match imp.ty() { + ExternType::Func(func_ty) => Extern::Func(DummyFunc::new(&store, func_ty.clone())), + ExternType::Global(global_ty) => { + Extern::Global(dummy_global(&store, global_ty.clone())?) + } + ExternType::Table(table_ty) => Extern::Table(dummy_table(&store, table_ty.clone())?), + ExternType::Memory(mem_ty) => Extern::Memory(dummy_memory(&store, mem_ty.clone())), + }); + } + Ok(imports) +} + +/// A function that doesn't do anything but return the default (zero) value for +/// the function's type. +#[derive(Debug)] +pub struct DummyFunc(FuncType); + +impl DummyFunc { + /// Construct a new dummy `Func`. + pub fn new(store: &Store, ty: FuncType) -> Func { + let callable = DummyFunc(ty.clone()); + Func::new(store, ty, Rc::new(callable) as _) + } +} + +impl Callable for DummyFunc { + fn call(&self, _params: &[Val], results: &mut [Val]) -> Result<(), Trap> { + for (ret_ty, result) in self.0.results().iter().zip(results) { + *result = dummy_value(ret_ty)?; + } + + Ok(()) + } +} + +/// Construct a dummy value for the given value type. +pub fn dummy_value(val_ty: &ValType) -> Result { + Ok(match val_ty { + ValType::I32 => Val::I32(0), + ValType::I64 => Val::I64(0), + ValType::F32 => Val::F32(0), + ValType::F64 => Val::F64(0), + ValType::V128 => { + return Err(Trap::new( + "dummy_value: unsupported function return type: v128".to_string(), + )) + } + ValType::AnyRef => { + return Err(Trap::new( + "dummy_value: unsupported function return type: anyref".to_string(), + )) + } + ValType::FuncRef => { + return Err(Trap::new( + "dummy_value: unsupported function return type: funcref".to_string(), + )) + } + }) +} + +/// Construct a sequence of dummy values for the given types. +pub fn dummy_values(val_tys: &[ValType]) -> Result, Trap> { + val_tys.iter().map(dummy_value).collect() +} + +/// Construct a dummy global for the given global type. +pub fn dummy_global(store: &Store, ty: GlobalType) -> Result { + let val = dummy_value(ty.content())?; + Ok(Global::new(store, ty, val).unwrap()) +} + +/// Construct a dummy table for the given table type. +pub fn dummy_table(store: &Store, ty: TableType) -> Result { + let init_val = dummy_value(&ty.element())?; + Ok(Table::new(store, ty, init_val).unwrap()) +} + +/// Construct a dummy memory for the given memory type. +pub fn dummy_memory(store: &Store, ty: MemoryType) -> Memory { + Memory::new(store, ty) +} diff --git a/crates/fuzzing/tests/regressions.rs b/crates/fuzzing/tests/regressions.rs new file mode 100644 index 0000000000..de29c718a0 --- /dev/null +++ b/crates/fuzzing/tests/regressions.rs @@ -0,0 +1,29 @@ +//! Regression tests for bugs found via fuzzing. +//! +//! The `#[test]` goes in here, the Wasm binary goes in +//! `./regressions/some-descriptive-name.wasm`, and then the `#[test]` should +//! use the Wasm binary by including it via +//! `include_bytes!("./regressions/some-descriptive-name.wasm")`. + +use wasmtime::{Config, Strategy}; +use wasmtime_fuzzing::oracles; + +#[test] +fn instantiate_empty_module() { + let data = wat::parse_str(include_str!("./regressions/empty.wat")).unwrap(); + oracles::instantiate(&data, Strategy::Auto); +} + +#[test] +fn instantiate_empty_module_with_memory() { + let data = wat::parse_str(include_str!("./regressions/empty_with_memory.wat")).unwrap(); + oracles::instantiate(&data, Strategy::Auto); +} + +#[test] +fn instantiate_module_that_compiled_to_x64_has_register_32() { + let mut config = Config::new(); + config.debug_info(true); + let data = wat::parse_str(include_str!("./regressions/issue694.wat")).unwrap(); + oracles::instantiate_with_config(&data, config); +} diff --git a/crates/fuzzing/tests/regressions/README.md b/crates/fuzzing/tests/regressions/README.md new file mode 100644 index 0000000000..3a1630279f --- /dev/null +++ b/crates/fuzzing/tests/regressions/README.md @@ -0,0 +1,2 @@ +This directory contains `.wasm` binaries generated during fuzzing that uncovered +a bug, and which we now use as regression tests in `../regressions.rs`. diff --git a/crates/fuzzing/tests/regressions/empty.wat b/crates/fuzzing/tests/regressions/empty.wat new file mode 100644 index 0000000000..3af8f25454 --- /dev/null +++ b/crates/fuzzing/tests/regressions/empty.wat @@ -0,0 +1 @@ +(module) diff --git a/crates/fuzzing/tests/regressions/empty_with_memory.wat b/crates/fuzzing/tests/regressions/empty_with_memory.wat new file mode 100644 index 0000000000..4503196d52 --- /dev/null +++ b/crates/fuzzing/tests/regressions/empty_with_memory.wat @@ -0,0 +1 @@ +(module (memory 1)) diff --git a/crates/fuzzing/tests/regressions/issue694.wat b/crates/fuzzing/tests/regressions/issue694.wat new file mode 100644 index 0000000000..7e92aad07f --- /dev/null +++ b/crates/fuzzing/tests/regressions/issue694.wat @@ -0,0 +1,49 @@ +(module + (type (;0;) (func)) + (type (;1;) (func (param i64))) + (func (;0;) (type 0)) + (func (;1;) (type 0)) + (func (;2;) (type 0)) + (func (;3;) (type 0)) + (func (;4;) (type 1) (param i64) + (local f32 f32 f32) + loop (result i64) ;; label = @1 + global.get 0 + if ;; label = @2 + local.get 1 + return + end + block (result i64) ;; label = @2 + loop ;; label = @3 + block ;; label = @4 + global.get 0 + if ;; label = @5 + i32.const 5 + if (result f32) ;; label = @6 + block (result f32) ;; label = @7 + call 0 + i32.const 7 + if (result f32) ;; label = @8 + local.get 2 + else + f32.const 0x1p+0 (;=1;) + end + end + else + f32.const 0x1p+0 (;=1;) + end + local.tee 1 + local.set 3 + end + end + end + i32.const 8 + br_if 1 (;@1;) + i64.const 4 + end + end + return) + (memory (;0;) 1) + (global (;0;) i32 (i32.const 0)) +) + diff --git a/crates/interface-types/Cargo.toml b/crates/interface-types/Cargo.toml new file mode 100644 index 0000000000..1ba6fc4094 --- /dev/null +++ b/crates/interface-types/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "wasmtime-interface-types" +version = "0.12.0" +authors = ["The Wasmtime Project Developers"] +description = "Support for wasm interface types with wasmtime" +license = "Apache-2.0 WITH LLVM-exception" +categories = ["wasm"] +keywords = ["webassembly", "wasm"] +repository = "https://github.com/bytecodealliance/wasmtime" +readme = "README.md" +edition = "2018" + +[dependencies] +anyhow = "1.0.19" +walrus = "0.15" +wasmparser = "0.51.2" +wasm-webidl-bindings = "0.8" +wasmtime = { path = "../api", version = "0.12.0" } +wasmtime-jit = { path = "../jit", version = "0.12.0" } +wasmtime-environ = { path = "../environ", version = "0.12.0" } +wasmtime-runtime = { path = "../runtime", version = "0.12.0" } +wasmtime-wasi = { path = "../wasi", version = "0.12.0" } + +[badges] +maintenance = { status = "actively-developed" } diff --git a/crates/interface-types/README.md b/crates/interface-types/README.md new file mode 100644 index 0000000000..a8350586f1 --- /dev/null +++ b/crates/interface-types/README.md @@ -0,0 +1,4 @@ +This crate implements a prototype of the wasm [interface types] proposal +for Wasmtime. + +[interface types]: https://github.com/WebAssembly/interface-types diff --git a/crates/interface-types/src/lib.rs b/crates/interface-types/src/lib.rs new file mode 100644 index 0000000000..4e650b13c0 --- /dev/null +++ b/crates/interface-types/src/lib.rs @@ -0,0 +1,490 @@ +//! A small crate to handle WebAssembly interface types in wasmtime. +//! +//! Note that this is intended to follow the [official proposal][proposal] and +//! is highly susceptible to change/breakage/etc. +//! +//! [proposal]: https://github.com/webassembly/webidl-bindings + +#![deny(missing_docs)] + +use anyhow::{bail, format_err, Result}; +use std::convert::TryFrom; +use std::str; +use wasm_webidl_bindings::ast; +use wasmtime::Val; +use wasmtime_environ::ir; +use wasmtime_runtime::{Export, InstanceHandle}; + +mod value; +pub use value::Value; + +/// A data structure intended to hold a parsed representation of the wasm +/// interface types of a module. +/// +/// The expected usage pattern is to create this next to wasmtime data +/// structures and then use this to process arguments into wasm arguments as +/// appropriate for bound functions. +pub struct ModuleData { + inner: Option, + wasi_module_name: Option, +} + +struct Inner { + module: walrus::Module, +} + +/// Representation of a binding of an exported function. +/// +/// Can be used to learn about binding expressions and/or binding types. +pub struct ExportBinding<'a> { + kind: ExportBindingKind<'a>, +} + +enum ExportBindingKind<'a> { + Rich { + section: &'a ast::WebidlBindings, + binding: &'a ast::ExportBinding, + }, + Raw(ir::Signature), +} + +impl ModuleData { + /// Parses a raw binary wasm file, extracting information about wasm + /// interface types. + /// + /// Returns an error if the wasm file is malformed. + pub fn new(wasm: &[u8]) -> Result { + // Perform a fast search through the module for the right custom + // section. Actually parsing out the interface types data is currently a + // pretty expensive operation so we want to only do that if we actually + // find the right section. + let mut reader = wasmparser::ModuleReader::new(wasm)?; + let mut found = false; + let mut wasi_module_name = None; + while !reader.eof() { + let section = reader.read()?; + + match section.code { + wasmparser::SectionCode::Custom { name, .. } => { + if name == "webidl-bindings" { + found = true; + break; + } + } + + // If we see the import section then see if we can find a wasi + // module import which we can later use to register the wasi + // implementation automatically. + wasmparser::SectionCode::Import => { + let section = section.get_import_section_reader()?; + for import in section { + let import = import?; + if wasmtime_wasi::is_wasi_module(import.module) { + wasi_module_name = Some(import.module.to_string()); + } + } + } + _ => {} + } + } + if !found { + return Ok(ModuleData { + inner: None, + wasi_module_name, + }); + } + + // Ok, perform the more expensive parsing. WebAssembly interface types + // are super experimental and under development. To get something + // quickly up and running we're using the same crate as `wasm-bindgen`, + // a producer of wasm interface types, the `wasm-webidl-bindings` crate. + // This crate relies on `walrus` which has its own IR for a wasm module. + // Ideally we'd do all this during cranelift's own parsing of the wasm + // module and we wouldn't have to reparse here purely for this one use + // case. + // + // For now though this is "fast enough" and good enough for some demos, + // but for full-on production quality engines we'll want to integrate + // this much more tightly with the rest of wasmtime. + let module = walrus::ModuleConfig::new() + .on_parse(wasm_webidl_bindings::binary::on_parse) + .parse(wasm)?; + + Ok(ModuleData { + inner: Some(Inner { module }), + wasi_module_name, + }) + } + + /// Detects if WASI support is needed: returns module name that is requested. + pub fn find_wasi_module_name(&self) -> Option { + self.wasi_module_name.clone() + } + + /// Invokes wasmtime function with a `&[Value]` list. `Value` the set of + /// wasm interface types. + pub fn invoke_export( + &self, + instance: &wasmtime::Instance, + export: &str, + args: &[Value], + ) -> Result> { + let mut handle = instance.handle().clone(); + + let binding = self.binding_for_export(&mut handle, export)?; + let incoming = binding.param_bindings()?; + let outgoing = binding.result_bindings()?; + + let f = instance + .get_export(export) + .ok_or_else(|| format_err!("failed to find export `{}`", export))? + .func() + .ok_or_else(|| format_err!("`{}` is not a function", export))? + .clone(); + + let mut cx = InstanceTranslateContext(instance.clone()); + let wasm_args = translate_incoming(&mut cx, &incoming, args)? + .into_iter() + .map(|rv| rv.into()) + .collect::>(); + let wasm_results = f.call(&wasm_args)?; + translate_outgoing(&mut cx, &outgoing, &wasm_results) + } + + /// Returns an appropriate binding for the `name` export in this module + /// which has also been instantiated as `instance` provided here. + /// + /// Returns an error if `name` is not present in the module. + pub fn binding_for_export( + &self, + instance: &mut InstanceHandle, + name: &str, + ) -> Result> { + if let Some(binding) = self.interface_binding_for_export(name) { + return Ok(binding); + } + let signature = match instance.lookup(name) { + Some(Export::Function { signature, .. }) => signature, + Some(_) => bail!("`{}` is not a function", name), + None => bail!("failed to find export `{}`", name), + }; + Ok(ExportBinding { + kind: ExportBindingKind::Raw(signature), + }) + } + + fn interface_binding_for_export(&self, name: &str) -> Option> { + let inner = self.inner.as_ref()?; + let bindings = inner.module.customs.get_typed::()?; + let export = inner.module.exports.iter().find(|e| e.name == name)?; + let id = match export.item { + walrus::ExportItem::Function(f) => f, + _ => panic!(), + }; + let (_, bind) = bindings.binds.iter().find(|(_, b)| b.func == id)?; + let binding = bindings.bindings.get(bind.binding)?; + let binding = match binding { + ast::FunctionBinding::Export(export) => export, + ast::FunctionBinding::Import(_) => return None, + }; + Some(ExportBinding { + kind: ExportBindingKind::Rich { + binding, + section: bindings, + }, + }) + } +} + +impl ExportBinding<'_> { + /// Returns the list of binding expressions used to create the parameters + /// for this binding. + pub fn param_bindings(&self) -> Result> { + match &self.kind { + ExportBindingKind::Rich { binding, .. } => Ok(binding.params.bindings.clone()), + ExportBindingKind::Raw(sig) => sig + .params + .iter() + .skip(2) // skip the VMContext arguments + .enumerate() + .map(|(i, param)| default_incoming(i, param)) + .collect(), + } + } + + /// Returns the list of scalar types used for this binding + pub fn param_types(&self) -> Result> { + match &self.kind { + ExportBindingKind::Rich { + binding, section, .. + } => { + let id = match binding.webidl_ty { + ast::WebidlTypeRef::Id(id) => id, + ast::WebidlTypeRef::Scalar(_) => { + bail!("webidl types for functions cannot be scalar") + } + }; + let ty = section + .types + .get::(id) + .ok_or_else(|| format_err!("invalid webidl custom section"))?; + let func = match ty { + ast::WebidlCompoundType::Function(f) => f, + _ => bail!("webidl type for function must be of function type"), + }; + func.params + .iter() + .map(|param| match param { + ast::WebidlTypeRef::Id(_) => bail!("function arguments cannot be compound"), + ast::WebidlTypeRef::Scalar(s) => Ok(*s), + }) + .collect() + } + ExportBindingKind::Raw(sig) => sig.params.iter().skip(2).map(abi2ast).collect(), + } + } + + /// Returns the list of binding expressions used to extract the return + /// values of this binding. + pub fn result_bindings(&self) -> Result> { + match &self.kind { + ExportBindingKind::Rich { binding, .. } => Ok(binding.result.bindings.clone()), + ExportBindingKind::Raw(sig) => sig + .returns + .iter() + .enumerate() + .map(|(i, param)| default_outgoing(i, param)) + .collect(), + } + } +} + +fn default_incoming(idx: usize, param: &ir::AbiParam) -> Result { + let get = ast::IncomingBindingExpressionGet { idx: idx as u32 }; + let ty = if param.value_type == ir::types::I32 { + walrus::ValType::I32 + } else if param.value_type == ir::types::I64 { + walrus::ValType::I64 + } else if param.value_type == ir::types::F32 { + walrus::ValType::F32 + } else if param.value_type == ir::types::F64 { + walrus::ValType::F64 + } else { + bail!("unsupported type {:?}", param.value_type) + }; + Ok(ast::IncomingBindingExpressionAs { + ty, + expr: Box::new(get.into()), + } + .into()) +} + +fn default_outgoing(idx: usize, param: &ir::AbiParam) -> Result { + let ty = abi2ast(param)?; + Ok(ast::OutgoingBindingExpressionAs { + ty: ty.into(), + idx: idx as u32, + } + .into()) +} + +fn abi2ast(param: &ir::AbiParam) -> Result { + Ok(if param.value_type == ir::types::I32 { + ast::WebidlScalarType::Long + } else if param.value_type == ir::types::I64 { + ast::WebidlScalarType::LongLong + } else if param.value_type == ir::types::F32 { + ast::WebidlScalarType::UnrestrictedFloat + } else if param.value_type == ir::types::F64 { + ast::WebidlScalarType::UnrestrictedDouble + } else { + bail!("unsupported type {:?}", param.value_type) + }) +} + +trait TranslateContext { + fn invoke_alloc(&mut self, alloc_func_name: &str, len: i32) -> Result; + unsafe fn get_memory(&mut self) -> Result<&mut [u8]>; +} + +struct InstanceTranslateContext(pub wasmtime::Instance); + +impl TranslateContext for InstanceTranslateContext { + fn invoke_alloc(&mut self, alloc_func_name: &str, len: i32) -> Result { + let alloc = self + .0 + .get_export(alloc_func_name) + .ok_or_else(|| format_err!("failed to find alloc function `{}`", alloc_func_name))? + .func() + .ok_or_else(|| format_err!("`{}` is not a (alloc) function", alloc_func_name))? + .clone(); + let alloc_args = vec![wasmtime::Val::I32(len)]; + let results = alloc.call(&alloc_args)?; + if results.len() != 1 { + bail!("allocator function wrong number of results"); + } + Ok(match results[0] { + wasmtime::Val::I32(i) => i, + _ => bail!("allocator function bad return type"), + }) + } + unsafe fn get_memory(&mut self) -> Result<&mut [u8]> { + let memory = self + .0 + .get_export("memory") + .ok_or_else(|| format_err!("failed to find `memory` export"))? + .memory() + .ok_or_else(|| format_err!("`memory` is not a memory"))? + .clone(); + let ptr = memory.data_ptr(); + let len = memory.data_size(); + Ok(std::slice::from_raw_parts_mut(ptr, len)) + } +} + +fn translate_incoming( + cx: &mut dyn TranslateContext, + bindings: &[ast::IncomingBindingExpression], + args: &[Value], +) -> Result> { + let get = |expr: &ast::IncomingBindingExpression| match expr { + ast::IncomingBindingExpression::Get(g) => args + .get(g.idx as usize) + .ok_or_else(|| format_err!("argument index out of bounds: {}", g.idx)), + _ => bail!("unsupported incoming binding expr {:?}", expr), + }; + + let mut copy = |alloc_func_name: &str, bytes: &[u8]| -> Result<(i32, i32)> { + let len = i32::try_from(bytes.len()).map_err(|_| format_err!("length overflow"))?; + let ptr = cx.invoke_alloc(alloc_func_name, len)?; + unsafe { + let raw = cx.get_memory()?; + raw[ptr as usize..][..bytes.len()].copy_from_slice(bytes) + } + + Ok((ptr, len)) + }; + + let mut wasm = Vec::new(); + + for expr in bindings { + match expr { + ast::IncomingBindingExpression::AllocUtf8Str(g) => { + let val = match get(&g.expr)? { + Value::String(s) => s, + _ => bail!("expected a string"), + }; + let (ptr, len) = copy(&g.alloc_func_name, val.as_bytes())?; + wasm.push(Val::I32(ptr)); + wasm.push(Val::I32(len)); + } + ast::IncomingBindingExpression::As(g) => { + let val = get(&g.expr)?; + match g.ty { + walrus::ValType::I32 => match val { + Value::I32(i) => wasm.push(Val::I32(*i)), + Value::U32(i) => wasm.push(Val::I32(*i as i32)), + _ => bail!("cannot convert {:?} to `i32`", val), + }, + walrus::ValType::I64 => match val { + Value::I32(i) => wasm.push(Val::I64((*i).into())), + Value::U32(i) => wasm.push(Val::I64((*i).into())), + Value::I64(i) => wasm.push(Val::I64(*i)), + Value::U64(i) => wasm.push(Val::I64(*i as i64)), + _ => bail!("cannot convert {:?} to `i64`", val), + }, + walrus::ValType::F32 => match val { + Value::F32(i) => wasm.push(Val::F32(i.to_bits())), + _ => bail!("cannot convert {:?} to `f32`", val), + }, + walrus::ValType::F64 => match val { + Value::F32(i) => wasm.push(Val::F64((*i as f64).to_bits())), + Value::F64(i) => wasm.push(Val::F64(i.to_bits())), + _ => bail!("cannot convert {:?} to `f64`", val), + }, + walrus::ValType::V128 | walrus::ValType::Anyref => { + bail!("unsupported `as` type {:?}", g.ty); + } + } + } + _ => bail!("unsupported incoming binding expr {:?}", expr), + } + } + + Ok(wasm) +} + +fn translate_outgoing( + cx: &mut dyn TranslateContext, + bindings: &[ast::OutgoingBindingExpression], + args: &[Val], +) -> Result> { + let mut values = Vec::new(); + + let get = |idx: u32| { + args.get(idx as usize) + .cloned() + .ok_or_else(|| format_err!("argument index out of bounds: {}", idx)) + }; + + for expr in bindings { + match expr { + ast::OutgoingBindingExpression::As(a) => { + let arg = get(a.idx)?; + match a.ty { + ast::WebidlTypeRef::Scalar(ast::WebidlScalarType::UnsignedLong) => match arg { + Val::I32(a) => values.push(Value::U32(a as u32)), + _ => bail!("can't convert {:?} to unsigned long", arg), + }, + ast::WebidlTypeRef::Scalar(ast::WebidlScalarType::Long) => match arg { + Val::I32(a) => values.push(Value::I32(a)), + _ => bail!("can't convert {:?} to long", arg), + }, + ast::WebidlTypeRef::Scalar(ast::WebidlScalarType::LongLong) => match arg { + Val::I32(a) => values.push(Value::I64(a as i64)), + Val::I64(a) => values.push(Value::I64(a)), + _ => bail!("can't convert {:?} to long long", arg), + }, + ast::WebidlTypeRef::Scalar(ast::WebidlScalarType::UnsignedLongLong) => { + match arg { + Val::I32(a) => values.push(Value::U64(a as u64)), + Val::I64(a) => values.push(Value::U64(a as u64)), + _ => bail!("can't convert {:?} to unsigned long long", arg), + } + } + ast::WebidlTypeRef::Scalar(ast::WebidlScalarType::Float) => match arg { + Val::F32(a) => values.push(Value::F32(f32::from_bits(a))), + _ => bail!("can't convert {:?} to float", arg), + }, + ast::WebidlTypeRef::Scalar(ast::WebidlScalarType::Double) => match arg { + Val::F32(a) => values.push(Value::F64(f32::from_bits(a) as f64)), + Val::F64(a) => values.push(Value::F64(f64::from_bits(a))), + _ => bail!("can't convert {:?} to double", arg), + }, + _ => bail!("unsupported outgoing binding expr {:?}", expr), + } + } + ast::OutgoingBindingExpression::Utf8Str(e) => { + if e.ty != ast::WebidlScalarType::DomString.into() { + bail!("utf-8 strings must go into dom-string") + } + let offset = match get(e.offset)? { + Val::I32(a) => a, + _ => bail!("offset must be an i32"), + }; + let length = match get(e.length)? { + Val::I32(a) => a, + _ => bail!("length must be an i32"), + }; + let bytes = unsafe { &cx.get_memory()?[offset as usize..][..length as usize] }; + values.push(Value::String(str::from_utf8(bytes).unwrap().to_string())); + } + _ => { + drop(cx); + bail!("unsupported outgoing binding expr {:?}", expr); + } + } + } + + Ok(values) +} diff --git a/crates/interface-types/src/value.rs b/crates/interface-types/src/value.rs new file mode 100644 index 0000000000..84fc081196 --- /dev/null +++ b/crates/interface-types/src/value.rs @@ -0,0 +1,66 @@ +use std::convert::TryFrom; +use std::fmt; + +/// The set of all possible WebAssembly Interface Types +#[derive(Debug, Clone)] +#[allow(missing_docs)] +pub enum Value { + String(String), + I32(i32), + U32(u32), + I64(i64), + U64(u64), + F32(f32), + F64(f64), +} + +macro_rules! from { + ($($a:ident => $b:ident,)*) => ($( + impl From<$a> for Value { + fn from(val: $a) -> Value { + Value::$b(val) + } + } + + impl TryFrom for $a { + type Error = anyhow::Error; + + fn try_from(val: Value) -> Result<$a, Self::Error> { + match val { + Value::$b(v) => Ok(v), + v => anyhow::bail!("cannot convert {:?} to {}", v, stringify!($a)), + } + } + } + )*) +} + +from! { + String => String, + i32 => I32, + u32 => U32, + i64 => I64, + u64 => U64, + f32 => F32, + f64 => F64, +} + +impl<'a> From<&'a str> for Value { + fn from(x: &'a str) -> Value { + x.to_string().into() + } +} + +impl fmt::Display for Value { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Value::String(s) => s.fmt(f), + Value::I32(s) => s.fmt(f), + Value::U32(s) => s.fmt(f), + Value::I64(s) => s.fmt(f), + Value::U64(s) => s.fmt(f), + Value::F32(s) => s.fmt(f), + Value::F64(s) => s.fmt(f), + } + } +} diff --git a/crates/jit/Cargo.toml b/crates/jit/Cargo.toml new file mode 100644 index 0000000000..f7c50e66f2 --- /dev/null +++ b/crates/jit/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "wasmtime-jit" +version = "0.12.0" +authors = ["The Wasmtime Project Developers"] +description = "JIT-style execution for WebAsssembly code in Cranelift" +license = "Apache-2.0 WITH LLVM-exception" +categories = ["wasm"] +keywords = ["webassembly", "wasm"] +repository = "https://github.com/bytecodealliance/wasmtime" +readme = "README.md" +edition = "2018" + +[dependencies] +cranelift-codegen = { version = "0.59.0", features = ["enable-serde"] } +cranelift-entity = { version = "0.59.0", features = ["enable-serde"] } +cranelift-wasm = { version = "0.59.0", features = ["enable-serde"] } +cranelift-native = "0.59.0" +cranelift-frontend = "0.59.0" +wasmtime-environ = { path = "../environ", version = "0.12.0" } +wasmtime-runtime = { path = "../runtime", version = "0.12.0" } +wasmtime-debug = { path = "../debug", version = "0.12.0" } +wasmtime-profiling = { path = "../profiling", version = "0.12.0" } +region = "2.0.0" +thiserror = "1.0.4" +target-lexicon = { version = "0.10.0", default-features = false } +wasmparser = "0.51.2" +more-asserts = "0.2.1" +anyhow = "1.0" +cfg-if = "0.1.9" + +[target.'cfg(target_os = "windows")'.dependencies] +winapi = { version = "0.3.7", features = ["winnt", "impl-default"] } + +[features] +lightbeam = ["wasmtime-environ/lightbeam"] + +[badges] +maintenance = { status = "actively-developed" } diff --git a/crates/jit/LICENSE b/crates/jit/LICENSE new file mode 100644 index 0000000000..f9d81955f4 --- /dev/null +++ b/crates/jit/LICENSE @@ -0,0 +1,220 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + diff --git a/crates/jit/README.md b/crates/jit/README.md new file mode 100644 index 0000000000..cbf255d893 --- /dev/null +++ b/crates/jit/README.md @@ -0,0 +1,6 @@ +This is the `wasmtime-jit` crate, which contains JIT-based execution +for wasm, using the wasm ABI defined by [`wasmtime-environ`] and the +runtime support provided by [`wasmtime-runtime`]. + +[`wasmtime-environ`]: https://crates.io/crates/wasmtime-environ +[`wasmtime-runtime`]: https://crates.io/crates/wasmtime-runtime diff --git a/crates/jit/src/code_memory.rs b/crates/jit/src/code_memory.rs new file mode 100644 index 0000000000..4670f40dfc --- /dev/null +++ b/crates/jit/src/code_memory.rs @@ -0,0 +1,252 @@ +//! Memory management for executable code. + +use crate::function_table::FunctionTable; +use region; +use std::mem::ManuallyDrop; +use std::{cmp, mem}; +use wasmtime_environ::{Compilation, CompiledFunction}; +use wasmtime_profiling::ProfilingAgent; +use wasmtime_runtime::{Mmap, VMFunctionBody}; + +struct CodeMemoryEntry { + mmap: ManuallyDrop, + table: ManuallyDrop, +} + +impl CodeMemoryEntry { + fn new() -> Self { + Self { + mmap: ManuallyDrop::new(Mmap::new()), + table: ManuallyDrop::new(FunctionTable::new()), + } + } + fn with_capacity(cap: usize) -> Result { + Ok(Self { + mmap: ManuallyDrop::new(Mmap::with_at_least(cap)?), + table: ManuallyDrop::new(FunctionTable::new()), + }) + } +} + +impl Drop for CodeMemoryEntry { + fn drop(&mut self) { + unsafe { + // Table needs to be freed before mmap. + ManuallyDrop::drop(&mut self.table); + ManuallyDrop::drop(&mut self.mmap); + } + } +} + +/// Memory manager for executable code. +pub struct CodeMemory { + current: CodeMemoryEntry, + entries: Vec, + position: usize, + published: usize, +} + +fn _assert() { + fn _assert_send_sync() {} + _assert_send_sync::(); +} + +impl CodeMemory { + /// Create a new `CodeMemory` instance. + pub fn new() -> Self { + Self { + current: CodeMemoryEntry::new(), + entries: Vec::new(), + position: 0, + published: 0, + } + } + + /// Allocate a continuous memory block for a single compiled function. + /// TODO: Reorganize the code that calls this to emit code directly into the + /// mmap region rather than into a Vec that we need to copy in. + pub fn allocate_for_function( + &mut self, + func: &CompiledFunction, + ) -> Result<&mut [VMFunctionBody], String> { + let size = Self::function_allocation_size(func); + + let start = self.position as u32; + let (buf, table) = self.allocate(size)?; + + let (_, _, _, vmfunc) = Self::copy_function(func, start, buf, table); + + Ok(vmfunc) + } + + /// Allocate a continuous memory block for a compilation. + /// + /// Allocates memory for both the function bodies as well as function unwind data. + pub fn allocate_for_compilation( + &mut self, + compilation: &Compilation, + ) -> Result, String> { + let total_len = compilation + .into_iter() + .fold(0, |acc, func| acc + Self::function_allocation_size(func)); + + let mut start = self.position as u32; + let (mut buf, mut table) = self.allocate(total_len)?; + let mut result = Vec::with_capacity(compilation.len()); + + for func in compilation.into_iter() { + let (next_start, next_buf, next_table, vmfunc) = + Self::copy_function(func, start, buf, table); + + result.push(vmfunc); + + start = next_start; + buf = next_buf; + table = next_table; + } + + Ok(result.into_boxed_slice()) + } + + /// Make all allocated memory executable. + pub fn publish(&mut self) { + self.push_current(0) + .expect("failed to push current memory map"); + + for CodeMemoryEntry { mmap: m, table: t } in &mut self.entries[self.published..] { + // Remove write access to the pages due to the relocation fixups. + t.publish(m.as_ptr() as u64) + .expect("failed to publish function table"); + + if !m.is_empty() { + unsafe { + region::protect(m.as_mut_ptr(), m.len(), region::Protection::ReadExecute) + } + .expect("unable to make memory readonly and executable"); + } + } + + self.published = self.entries.len(); + } + + /// Allocate `size` bytes of memory which can be made executable later by + /// calling `publish()`. Note that we allocate the memory as writeable so + /// that it can be written to and patched, though we make it readonly before + /// actually executing from it. + /// + /// TODO: Add an alignment flag. + fn allocate(&mut self, size: usize) -> Result<(&mut [u8], &mut FunctionTable), String> { + if self.current.mmap.len() - self.position < size { + self.push_current(cmp::max(0x10000, size))?; + } + + let old_position = self.position; + self.position += size; + + Ok(( + &mut self.current.mmap.as_mut_slice()[old_position..self.position], + &mut self.current.table, + )) + } + + /// Calculates the allocation size of the given compiled function. + fn function_allocation_size(func: &CompiledFunction) -> usize { + if func.unwind_info.is_empty() { + func.body.len() + } else { + // Account for necessary unwind information alignment padding (32-bit) + ((func.body.len() + 3) & !3) + func.unwind_info.len() + } + } + + /// Copies the data of the compiled function to the given buffer. + /// + /// This will also add the function to the current function table. + fn copy_function<'a>( + func: &CompiledFunction, + func_start: u32, + buf: &'a mut [u8], + table: &'a mut FunctionTable, + ) -> ( + u32, + &'a mut [u8], + &'a mut FunctionTable, + &'a mut [VMFunctionBody], + ) { + let func_end = func_start + (func.body.len() as u32); + + let (body, remainder) = buf.split_at_mut(func.body.len()); + body.copy_from_slice(&func.body); + let vmfunc = Self::view_as_mut_vmfunc_slice(body); + + if func.unwind_info.is_empty() { + return (func_end, remainder, table, vmfunc); + } + + // Keep unwind information 32-bit aligned (round up to the nearest 4 byte boundary) + let padding = ((func.body.len() + 3) & !3) - func.body.len(); + let (unwind, remainder) = remainder.split_at_mut(padding + func.unwind_info.len()); + let mut relocs = Vec::new(); + func.unwind_info + .serialize(&mut unwind[padding..], &mut relocs); + + let unwind_start = func_end + (padding as u32); + let unwind_end = unwind_start + (func.unwind_info.len() as u32); + + relocs.iter_mut().for_each(move |r| { + r.offset += unwind_start; + r.addend += func_start; + }); + + table.add_function(func_start, func_end, unwind_start, &relocs); + + (unwind_end, remainder, table, vmfunc) + } + + /// Convert mut a slice from u8 to VMFunctionBody. + fn view_as_mut_vmfunc_slice(slice: &mut [u8]) -> &mut [VMFunctionBody] { + let byte_ptr: *mut [u8] = slice; + let body_ptr = byte_ptr as *mut [VMFunctionBody]; + unsafe { &mut *body_ptr } + } + + /// Pushes the current Mmap (and function table) and allocates a new Mmap of the given size. + fn push_current(&mut self, new_size: usize) -> Result<(), String> { + let previous = mem::replace( + &mut self.current, + if new_size == 0 { + CodeMemoryEntry::new() + } else { + CodeMemoryEntry::with_capacity(cmp::max(0x10000, new_size))? + }, + ); + + if !previous.mmap.is_empty() { + self.entries.push(previous); + } else { + assert_eq!(previous.table.len(), 0); + } + + self.position = 0; + + Ok(()) + } + + /// Calls the module_load for a given ProfilerAgent. Includes + /// all memory address and length for the given module. + /// TODO: Properly handle the possibilities of multiple mmapped regions + /// which may, amongst other things, influence being more specific about + /// the module name. + pub fn profiler_module_load( + &mut self, + profiler: &mut Box, + module_name: &str, + dbg_image: Option<&[u8]>, + ) -> () { + for CodeMemoryEntry { mmap: m, table: _t } in &mut self.entries { + if m.len() > 0 { + profiler.module_load(module_name, m.as_ptr(), m.len(), dbg_image); + } + } + } +} diff --git a/crates/jit/src/compiler.rs b/crates/jit/src/compiler.rs new file mode 100644 index 0000000000..31af40531a --- /dev/null +++ b/crates/jit/src/compiler.rs @@ -0,0 +1,452 @@ +//! JIT compilation. + +use crate::code_memory::CodeMemory; +use crate::instantiate::SetupError; +use crate::target_tunables::target_tunables; +use cranelift_codegen::ir::InstBuilder; +use cranelift_codegen::print_errors::pretty_error; +use cranelift_codegen::Context; +use cranelift_codegen::{binemit, ir}; +use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext}; +use cranelift_wasm::ModuleTranslationState; +use std::collections::HashMap; +use std::convert::TryFrom; +use wasmtime_debug::{emit_debugsections_image, DebugInfoData}; +use wasmtime_environ::entity::{EntityRef, PrimaryMap}; +use wasmtime_environ::isa::{TargetFrontendConfig, TargetIsa}; +use wasmtime_environ::wasm::{DefinedFuncIndex, DefinedMemoryIndex, MemoryIndex}; +use wasmtime_environ::{ + CacheConfig, Compilation, CompileError, CompiledFunction, CompiledFunctionUnwindInfo, + Compiler as _C, FunctionBodyData, Module, ModuleMemoryOffset, ModuleVmctxInfo, Relocations, + Traps, Tunables, VMOffsets, +}; +use wasmtime_profiling::ProfilingAgent; +use wasmtime_runtime::{ + InstantiationError, SignatureRegistry, TrapRegistration, TrapRegistry, VMFunctionBody, + VMSharedSignatureIndex, +}; + +/// Select which kind of compilation to use. +#[derive(Copy, Clone, Debug)] +pub enum CompilationStrategy { + /// Let Wasmtime pick the strategy. + Auto, + + /// Compile all functions with Cranelift. + Cranelift, + + /// Compile all functions with Lightbeam. + #[cfg(feature = "lightbeam")] + Lightbeam, +} + +/// A WebAssembly code JIT compiler. +/// +/// A `Compiler` instance owns the executable memory that it allocates. +/// +/// TODO: Evolve this to support streaming rather than requiring a `&[u8]` +/// containing a whole wasm module at once. +/// +/// TODO: Consider using cranelift-module. +pub struct Compiler { + isa: Box, + + code_memory: CodeMemory, + trap_registry: TrapRegistry, + trampoline_park: HashMap, + signatures: SignatureRegistry, + strategy: CompilationStrategy, + cache_config: CacheConfig, + + /// The `FunctionBuilderContext`, shared between trampline function compilations. + fn_builder_ctx: FunctionBuilderContext, +} + +impl Compiler { + /// Construct a new `Compiler`. + pub fn new( + isa: Box, + strategy: CompilationStrategy, + cache_config: CacheConfig, + ) -> Self { + Self { + isa, + code_memory: CodeMemory::new(), + trampoline_park: HashMap::new(), + signatures: SignatureRegistry::new(), + fn_builder_ctx: FunctionBuilderContext::new(), + strategy, + trap_registry: TrapRegistry::default(), + cache_config, + } + } +} + +impl Compiler { + /// Return the target's frontend configuration settings. + pub fn frontend_config(&self) -> TargetFrontendConfig { + self.isa.frontend_config() + } + + /// Return the tunables in use by this engine. + pub fn tunables(&self) -> Tunables { + target_tunables(self.isa.triple()) + } + + /// Compile the given function bodies. + pub(crate) fn compile<'data>( + &mut self, + module: &Module, + module_translation: &ModuleTranslationState, + function_body_inputs: PrimaryMap>, + debug_data: Option, + ) -> Result< + ( + PrimaryMap, + PrimaryMap, + Relocations, + Option>, + TrapRegistration, + ), + SetupError, + > { + let (compilation, relocations, address_transform, value_ranges, stack_slots, traps) = + match self.strategy { + // For now, interpret `Auto` as `Cranelift` since that's the most stable + // implementation. + CompilationStrategy::Auto | CompilationStrategy::Cranelift => { + wasmtime_environ::cranelift::Cranelift::compile_module( + module, + module_translation, + function_body_inputs, + &*self.isa, + debug_data.is_some(), + &self.cache_config, + ) + } + #[cfg(feature = "lightbeam")] + CompilationStrategy::Lightbeam => { + wasmtime_environ::lightbeam::Lightbeam::compile_module( + module, + module_translation, + function_body_inputs, + &*self.isa, + debug_data.is_some(), + &self.cache_config, + ) + } + } + .map_err(SetupError::Compile)?; + + let allocated_functions = + allocate_functions(&mut self.code_memory, &compilation).map_err(|message| { + SetupError::Instantiate(InstantiationError::Resource(format!( + "failed to allocate memory for functions: {}", + message + ))) + })?; + + let trap_registration = register_traps(&allocated_functions, &traps, &self.trap_registry); + + // Translate debug info (DWARF) only if at least one function is present. + let dbg = if debug_data.is_some() && !allocated_functions.is_empty() { + let target_config = self.isa.frontend_config(); + let ofs = VMOffsets::new(target_config.pointer_bytes(), &module.local); + + let mut funcs = Vec::new(); + for (i, allocated) in allocated_functions.into_iter() { + let ptr = (*allocated) as *const u8; + let body_len = compilation.get(i).body.len(); + funcs.push((ptr, body_len)); + } + let module_vmctx_info = { + ModuleVmctxInfo { + memory_offset: if ofs.num_imported_memories > 0 { + ModuleMemoryOffset::Imported(ofs.vmctx_vmmemory_import(MemoryIndex::new(0))) + } else if ofs.num_defined_memories > 0 { + ModuleMemoryOffset::Defined( + ofs.vmctx_vmmemory_definition_base(DefinedMemoryIndex::new(0)), + ) + } else { + ModuleMemoryOffset::None + }, + stack_slots, + } + }; + let bytes = emit_debugsections_image( + self.isa.triple().clone(), + target_config, + debug_data.as_ref().unwrap(), + &module_vmctx_info, + &address_transform, + &value_ranges, + &funcs, + ) + .map_err(SetupError::DebugInfo)?; + Some(bytes) + } else { + None + }; + + let jt_offsets = compilation.get_jt_offsets(); + + Ok(( + allocated_functions, + jt_offsets, + relocations, + dbg, + trap_registration, + )) + } + + /// Create a trampoline for invoking a function. + pub(crate) fn get_trampoline( + &mut self, + signature: &ir::Signature, + value_size: usize, + ) -> Result<*const VMFunctionBody, SetupError> { + let index = self.signatures.register(signature); + if let Some(trampoline) = self.trampoline_park.get(&index) { + return Ok(*trampoline); + } + let body = make_trampoline( + &*self.isa, + &mut self.code_memory, + &mut self.fn_builder_ctx, + signature, + value_size, + )?; + self.trampoline_park.insert(index, body); + return Ok(body); + } + + /// Create and publish a trampoline for invoking a function. + pub fn get_published_trampoline( + &mut self, + signature: &ir::Signature, + value_size: usize, + ) -> Result<*const VMFunctionBody, SetupError> { + let result = self.get_trampoline(signature, value_size)?; + self.publish_compiled_code(); + Ok(result) + } + + /// Make memory containing compiled code executable. + pub(crate) fn publish_compiled_code(&mut self) { + self.code_memory.publish(); + } + + pub(crate) fn profiler_module_load( + &mut self, + profiler: &mut Box, + module_name: &str, + dbg_image: Option<&[u8]>, + ) -> () { + self.code_memory + .profiler_module_load(profiler, module_name, dbg_image); + } + + /// Shared signature registry. + pub fn signatures(&self) -> &SignatureRegistry { + &self.signatures + } + + /// Shared registration of trap information + pub fn trap_registry(&self) -> &TrapRegistry { + &self.trap_registry + } +} + +/// Create a trampoline for invoking a function. +fn make_trampoline( + isa: &dyn TargetIsa, + code_memory: &mut CodeMemory, + fn_builder_ctx: &mut FunctionBuilderContext, + signature: &ir::Signature, + value_size: usize, +) -> Result<*const VMFunctionBody, SetupError> { + let pointer_type = isa.pointer_type(); + let mut wrapper_sig = ir::Signature::new(isa.frontend_config().default_call_conv); + + // Add the callee `vmctx` parameter. + wrapper_sig.params.push(ir::AbiParam::special( + pointer_type, + ir::ArgumentPurpose::VMContext, + )); + + // Add the caller `vmctx` parameter. + wrapper_sig.params.push(ir::AbiParam::new(pointer_type)); + + // Add the `callee_address` parameter. + wrapper_sig.params.push(ir::AbiParam::new(pointer_type)); + + // Add the `values_vec` parameter. + wrapper_sig.params.push(ir::AbiParam::new(pointer_type)); + + let mut context = Context::new(); + context.func = ir::Function::with_name_signature(ir::ExternalName::user(0, 0), wrapper_sig); + context.func.collect_frame_layout_info(); + + { + let mut builder = FunctionBuilder::new(&mut context.func, fn_builder_ctx); + let block0 = builder.create_block(); + + builder.append_block_params_for_function_params(block0); + builder.switch_to_block(block0); + builder.seal_block(block0); + + let (vmctx_ptr_val, caller_vmctx_ptr_val, callee_value, values_vec_ptr_val) = { + let params = builder.func.dfg.block_params(block0); + (params[0], params[1], params[2], params[3]) + }; + + // Load the argument values out of `values_vec`. + let mflags = ir::MemFlags::trusted(); + let callee_args = signature + .params + .iter() + .enumerate() + .map(|(i, r)| { + match i { + 0 => vmctx_ptr_val, + 1 => caller_vmctx_ptr_val, + _ => + // i - 2 because vmctx and caller vmctx aren't passed through `values_vec`. + { + builder.ins().load( + r.value_type, + mflags, + values_vec_ptr_val, + ((i - 2) * value_size) as i32, + ) + } + } + }) + .collect::>(); + + let new_sig = builder.import_signature(signature.clone()); + + let call = builder + .ins() + .call_indirect(new_sig, callee_value, &callee_args); + + let results = builder.func.dfg.inst_results(call).to_vec(); + + // Store the return values into `values_vec`. + let mflags = ir::MemFlags::trusted(); + for (i, r) in results.iter().enumerate() { + builder + .ins() + .store(mflags, *r, values_vec_ptr_val, (i * value_size) as i32); + } + + builder.ins().return_(&[]); + builder.finalize() + } + + let mut code_buf = Vec::new(); + let mut reloc_sink = RelocSink {}; + let mut trap_sink = binemit::NullTrapSink {}; + let mut stackmap_sink = binemit::NullStackmapSink {}; + context + .compile_and_emit( + isa, + &mut code_buf, + &mut reloc_sink, + &mut trap_sink, + &mut stackmap_sink, + ) + .map_err(|error| { + SetupError::Compile(CompileError::Codegen(pretty_error( + &context.func, + Some(isa), + error, + ))) + })?; + + let unwind_info = CompiledFunctionUnwindInfo::new(isa, &context); + + Ok(code_memory + .allocate_for_function(&CompiledFunction { + body: code_buf, + jt_offsets: context.func.jt_offsets, + unwind_info, + }) + .map_err(|message| SetupError::Instantiate(InstantiationError::Resource(message)))? + .as_ptr()) +} + +fn allocate_functions( + code_memory: &mut CodeMemory, + compilation: &Compilation, +) -> Result, String> { + let fat_ptrs = code_memory.allocate_for_compilation(compilation)?; + + // Second, create a PrimaryMap from result vector of pointers. + let mut result = PrimaryMap::with_capacity(compilation.len()); + for i in 0..fat_ptrs.len() { + let fat_ptr: *mut [VMFunctionBody] = fat_ptrs[i]; + result.push(fat_ptr); + } + Ok(result) +} + +fn register_traps( + allocated_functions: &PrimaryMap, + traps: &Traps, + registry: &TrapRegistry, +) -> TrapRegistration { + let traps = + allocated_functions + .values() + .zip(traps.values()) + .flat_map(|(func_addr, func_traps)| { + func_traps.iter().map(move |trap_desc| { + let func_addr = *func_addr as *const u8 as usize; + let offset = usize::try_from(trap_desc.code_offset).unwrap(); + let trap_addr = func_addr + offset; + (trap_addr, trap_desc.source_loc, trap_desc.trap_code) + }) + }); + registry.register_traps(traps) +} + +/// We don't expect trampoline compilation to produce any relocations, so +/// this `RelocSink` just asserts that it doesn't recieve any. +struct RelocSink {} + +impl binemit::RelocSink for RelocSink { + fn reloc_block( + &mut self, + _offset: binemit::CodeOffset, + _reloc: binemit::Reloc, + _block_offset: binemit::CodeOffset, + ) { + panic!("trampoline compilation should not produce block relocs"); + } + fn reloc_external( + &mut self, + _offset: binemit::CodeOffset, + _reloc: binemit::Reloc, + _name: &ir::ExternalName, + _addend: binemit::Addend, + ) { + panic!("trampoline compilation should not produce external symbol relocs"); + } + fn reloc_constant( + &mut self, + _code_offset: binemit::CodeOffset, + _reloc: binemit::Reloc, + _constant_offset: ir::ConstantOffset, + ) { + panic!("trampoline compilation should not produce constant relocs"); + } + fn reloc_jt( + &mut self, + _offset: binemit::CodeOffset, + _reloc: binemit::Reloc, + _jt: ir::JumpTable, + ) { + panic!("trampoline compilation should not produce jump table relocs"); + } +} diff --git a/crates/jit/src/function_table.rs b/crates/jit/src/function_table.rs new file mode 100644 index 0000000000..ed982cf7a2 --- /dev/null +++ b/crates/jit/src/function_table.rs @@ -0,0 +1,224 @@ +//! Runtime function table. +//! +//! This module is primarily used to track JIT functions on Windows for stack walking and unwind. + +type FunctionTableReloc = wasmtime_environ::CompiledFunctionUnwindInfoReloc; + +/// Represents a runtime function table. +/// +/// This is used to register JIT code with the operating system to enable stack walking and unwinding. +#[cfg(all(target_os = "windows", target_arch = "x86_64"))] +pub(crate) struct FunctionTable { + functions: Vec, + published: bool, +} + +#[cfg(all(target_os = "windows", target_arch = "x86_64"))] +impl FunctionTable { + /// Creates a new function table. + pub fn new() -> Self { + Self { + functions: Vec::new(), + published: false, + } + } + + /// Returns the number of functions in the table, also referred to as its 'length'. + pub fn len(&self) -> usize { + self.functions.len() + } + + /// Adds a function to the table based off of the start offset, end offset, and unwind offset. + /// + /// The offsets are from the "module base", which is provided when the table is published. + pub fn add_function( + &mut self, + start: u32, + end: u32, + unwind: u32, + _relocs: &[FunctionTableReloc], + ) { + assert_eq!(_relocs.len(), 0); + use winapi::um::winnt; + + assert!(!self.published, "table has already been published"); + + let mut entry = winnt::RUNTIME_FUNCTION::default(); + + entry.BeginAddress = start; + entry.EndAddress = end; + + unsafe { + *entry.u.UnwindInfoAddress_mut() = unwind; + } + + self.functions.push(entry); + } + + /// Publishes the function table using the given base address. + /// + /// A published function table will automatically be deleted when it is dropped. + pub fn publish(&mut self, base_address: u64) -> Result<(), String> { + use winapi::um::winnt; + + if self.published { + return Err("function table was already published".into()); + } + + self.published = true; + + if self.functions.is_empty() { + return Ok(()); + } + + unsafe { + // Windows heap allocations are 32-bit aligned, but assert just in case + assert_eq!( + (self.functions.as_mut_ptr() as u64) % 4, + 0, + "function table allocation was not aligned" + ); + + if winnt::RtlAddFunctionTable( + self.functions.as_mut_ptr(), + self.functions.len() as u32, + base_address, + ) == 0 + { + return Err("failed to add function table".into()); + } + } + + Ok(()) + } +} + +#[cfg(target_os = "windows")] +impl Drop for FunctionTable { + fn drop(&mut self) { + use winapi::um::winnt; + + if self.published { + unsafe { + winnt::RtlDeleteFunctionTable(self.functions.as_mut_ptr()); + } + } + } +} + +/// Represents a runtime function table. +/// +/// This is used to register JIT code with the operating system to enable stack walking and unwinding. +#[cfg(unix)] +pub(crate) struct FunctionTable { + functions: Vec, + relocs: Vec, + published: Option>, +} + +#[cfg(unix)] +impl FunctionTable { + /// Creates a new function table. + pub fn new() -> Self { + Self { + functions: Vec::new(), + relocs: Vec::new(), + published: None, + } + } + + /// Returns the number of functions in the table, also referred to as its 'length'. + pub fn len(&self) -> usize { + self.functions.len() + } + + /// Adds a function to the table based off of the start offset, end offset, and unwind offset. + /// + /// The offsets are from the "module base", which is provided when the table is published. + pub fn add_function( + &mut self, + _start: u32, + _end: u32, + unwind: u32, + relocs: &[FunctionTableReloc], + ) { + assert!(self.published.is_none(), "table has already been published"); + self.functions.push(unwind); + self.relocs.extend_from_slice(relocs); + } + + /// Publishes the function table using the given base address. + /// + /// A published function table will automatically be deleted when it is dropped. + pub fn publish(&mut self, base_address: u64) -> Result<(), String> { + if self.published.is_some() { + return Err("function table was already published".into()); + } + + if self.functions.is_empty() { + assert_eq!(self.relocs.len(), 0); + self.published = Some(vec![]); + return Ok(()); + } + + extern "C" { + // libunwind import + fn __register_frame(fde: *const u8); + } + + for reloc in self.relocs.iter() { + let addr = base_address + (reloc.offset as u64); + let target = base_address + (reloc.addend as u64); + unsafe { + std::ptr::write(addr as *mut u64, target); + } + } + + let mut fdes = Vec::with_capacity(self.functions.len()); + for unwind_offset in self.functions.iter() { + let addr = base_address + (*unwind_offset as u64); + let off = unsafe { std::ptr::read::(addr as *const u32) } as usize + 4; + + let fde = (addr + off as u64) as usize; + unsafe { + __register_frame(fde as *const _); + } + fdes.push(fde); + } + + self.published = Some(fdes); + Ok(()) + } +} + +#[cfg(unix)] +impl Drop for FunctionTable { + fn drop(&mut self) { + extern "C" { + // libunwind import + fn __deregister_frame(fde: *const u8); + } + + if let Some(published) = &self.published { + unsafe { + // I'm not really sure why, but it appears to be way faster to + // unregister frames in reverse order rather than in-order. This + // way we're deregistering in LIFO order, and maybe there's some + // vec shifting or something like that in libgcc? + // + // Locally on Ubuntu 18.04 a wasm module with 40k empty + // functions takes 0.1s to compile and drop with reverse + // iteration. With forward iteration it takes 3s to compile and + // drop! + // + // Poking around libgcc sources seems to indicate that some sort + // of linked list is being traversed... We may need to figure + // out something else for backtraces in the future since this + // API may not be long-lived to keep calling. + for fde in published.iter().rev() { + __deregister_frame(*fde as *const _); + } + } + } + } +} diff --git a/crates/jit/src/imports.rs b/crates/jit/src/imports.rs new file mode 100644 index 0000000000..c2151ec94e --- /dev/null +++ b/crates/jit/src/imports.rs @@ -0,0 +1,289 @@ +//! Module imports resolving logic. + +use crate::resolver::Resolver; +use more_asserts::assert_ge; +use std::collections::HashSet; +use wasmtime_environ::entity::PrimaryMap; +use wasmtime_environ::wasm::{Global, GlobalInit, Memory, Table, TableElementType}; +use wasmtime_environ::{MemoryPlan, MemoryStyle, Module, TablePlan}; +use wasmtime_runtime::{ + Export, Imports, InstanceHandle, LinkError, VMFunctionImport, VMGlobalImport, VMMemoryImport, + VMTableImport, +}; + +/// This function allows to match all imports of a `Module` with concrete definitions provided by +/// a `Resolver`. +/// +/// If all imports are satisfied returns an `Imports` instance required for a module instantiation. +pub fn resolve_imports(module: &Module, resolver: &mut dyn Resolver) -> Result { + let mut dependencies = HashSet::new(); + + let mut function_imports = PrimaryMap::with_capacity(module.imported_funcs.len()); + for (index, (module_name, field, import_idx)) in module.imported_funcs.iter() { + match resolver.resolve(*import_idx, module_name, field) { + Some(export_value) => match export_value { + Export::Function { + address, + signature, + vmctx, + } => { + let import_signature = &module.local.signatures[module.local.functions[index]]; + if signature != *import_signature { + // TODO: If the difference is in the calling convention, + // we could emit a wrapper function to fix it up. + return Err(LinkError(format!( + "{}/{}: incompatible import type: exported function with signature {} \ + incompatible with function import with signature {}", + module_name, field, signature, import_signature + ))); + } + dependencies.insert(unsafe { InstanceHandle::from_vmctx(vmctx) }); + function_imports.push(VMFunctionImport { + body: address, + vmctx, + }); + } + Export::Table { .. } | Export::Memory { .. } | Export::Global { .. } => { + return Err(LinkError(format!( + "{}/{}: incompatible import type: export incompatible with function import", + module_name, field + ))); + } + }, + None => { + return Err(LinkError(format!( + "{}/{}: unknown import function: function not provided", + module_name, field + ))); + } + } + } + + let mut table_imports = PrimaryMap::with_capacity(module.imported_tables.len()); + for (index, (module_name, field, import_idx)) in module.imported_tables.iter() { + match resolver.resolve(*import_idx, module_name, field) { + Some(export_value) => match export_value { + Export::Table { + definition, + vmctx, + table, + } => { + let import_table = &module.local.table_plans[index]; + if !is_table_compatible(&table, import_table) { + return Err(LinkError(format!( + "{}/{}: incompatible import type: exported table incompatible with \ + table import", + module_name, field, + ))); + } + dependencies.insert(unsafe { InstanceHandle::from_vmctx(vmctx) }); + table_imports.push(VMTableImport { + from: definition, + vmctx, + }); + } + Export::Global { .. } | Export::Memory { .. } | Export::Function { .. } => { + return Err(LinkError(format!( + "{}/{}: incompatible import type: export incompatible with table import", + module_name, field + ))); + } + }, + None => { + return Err(LinkError(format!( + "unknown import: no provided import table for {}/{}", + module_name, field + ))); + } + } + } + + let mut memory_imports = PrimaryMap::with_capacity(module.imported_memories.len()); + for (index, (module_name, field, import_idx)) in module.imported_memories.iter() { + match resolver.resolve(*import_idx, module_name, field) { + Some(export_value) => match export_value { + Export::Memory { + definition, + vmctx, + memory, + } => { + let import_memory = &module.local.memory_plans[index]; + if !is_memory_compatible(&memory, import_memory) { + return Err(LinkError(format!( + "{}/{}: incompatible import type: exported memory incompatible with \ + memory import", + module_name, field + ))); + } + + // Sanity-check: Ensure that the imported memory has at least + // guard-page protections the importing module expects it to have. + if let ( + MemoryStyle::Static { bound }, + MemoryStyle::Static { + bound: import_bound, + }, + ) = (memory.style, &import_memory.style) + { + assert_ge!(bound, *import_bound); + } + assert_ge!(memory.offset_guard_size, import_memory.offset_guard_size); + + dependencies.insert(unsafe { InstanceHandle::from_vmctx(vmctx) }); + memory_imports.push(VMMemoryImport { + from: definition, + vmctx, + }); + } + Export::Table { .. } | Export::Global { .. } | Export::Function { .. } => { + return Err(LinkError(format!( + "{}/{}: incompatible import type: export incompatible with memory import", + module_name, field + ))); + } + }, + None => { + return Err(LinkError(format!( + "unknown import: no provided import memory for {}/{}", + module_name, field + ))); + } + } + } + + let mut global_imports = PrimaryMap::with_capacity(module.imported_globals.len()); + for (index, (module_name, field, import_idx)) in module.imported_globals.iter() { + match resolver.resolve(*import_idx, module_name, field) { + Some(export_value) => match export_value { + Export::Table { .. } | Export::Memory { .. } | Export::Function { .. } => { + return Err(LinkError(format!( + "{}/{}: incompatible import type: exported global incompatible with \ + global import", + module_name, field + ))); + } + Export::Global { + definition, + vmctx, + global, + } => { + let imported_global = module.local.globals[index]; + if !is_global_compatible(&global, &imported_global) { + return Err(LinkError(format!( + "{}/{}: incompatible import type: exported global incompatible with \ + global import", + module_name, field + ))); + } + dependencies.insert(unsafe { InstanceHandle::from_vmctx(vmctx) }); + global_imports.push(VMGlobalImport { from: definition }); + } + }, + None => { + return Err(LinkError(format!( + "unknown import: no provided import global for {}/{}", + module_name, field + ))); + } + } + } + + Ok(Imports::new( + dependencies, + function_imports, + table_imports, + memory_imports, + global_imports, + )) +} + +fn is_global_compatible(exported: &Global, imported: &Global) -> bool { + match imported.initializer { + GlobalInit::Import => (), + _ => panic!("imported Global should have an Imported initializer"), + } + + let Global { + ty: exported_ty, + mutability: exported_mutability, + initializer: _exported_initializer, + } = exported; + let Global { + ty: imported_ty, + mutability: imported_mutability, + initializer: _imported_initializer, + } = imported; + exported_ty == imported_ty && imported_mutability == exported_mutability +} + +fn is_table_element_type_compatible( + exported_type: TableElementType, + imported_type: TableElementType, +) -> bool { + match exported_type { + TableElementType::Func => match imported_type { + TableElementType::Func => true, + _ => false, + }, + TableElementType::Val(exported_val_ty) => match imported_type { + TableElementType::Val(imported_val_ty) => exported_val_ty == imported_val_ty, + _ => false, + }, + } +} + +fn is_table_compatible(exported: &TablePlan, imported: &TablePlan) -> bool { + let TablePlan { + table: + Table { + ty: exported_ty, + minimum: exported_minimum, + maximum: exported_maximum, + }, + style: _exported_style, + } = exported; + let TablePlan { + table: + Table { + ty: imported_ty, + minimum: imported_minimum, + maximum: imported_maximum, + }, + style: _imported_style, + } = imported; + + is_table_element_type_compatible(*exported_ty, *imported_ty) + && imported_minimum <= exported_minimum + && (imported_maximum.is_none() + || (!exported_maximum.is_none() + && imported_maximum.unwrap() >= exported_maximum.unwrap())) +} + +fn is_memory_compatible(exported: &MemoryPlan, imported: &MemoryPlan) -> bool { + let MemoryPlan { + memory: + Memory { + minimum: exported_minimum, + maximum: exported_maximum, + shared: exported_shared, + }, + style: _exported_style, + offset_guard_size: _exported_offset_guard_size, + } = exported; + let MemoryPlan { + memory: + Memory { + minimum: imported_minimum, + maximum: imported_maximum, + shared: imported_shared, + }, + style: _imported_style, + offset_guard_size: _imported_offset_guard_size, + } = imported; + + imported_minimum <= exported_minimum + && (imported_maximum.is_none() + || (!exported_maximum.is_none() + && imported_maximum.unwrap() >= exported_maximum.unwrap())) + && exported_shared == imported_shared +} diff --git a/crates/jit/src/instantiate.rs b/crates/jit/src/instantiate.rs new file mode 100644 index 0000000000..957f1c4099 --- /dev/null +++ b/crates/jit/src/instantiate.rs @@ -0,0 +1,288 @@ +//! Define the `instantiate` function, which takes a byte array containing an +//! encoded wasm module and returns a live wasm instance. Also, define +//! `CompiledModule` to allow compiling and instantiating to be done as separate +//! steps. + +use crate::compiler::Compiler; +use crate::imports::resolve_imports; +use crate::link::link_module; +use crate::resolver::Resolver; +use std::io::Write; +use std::rc::Rc; +use std::sync::{Arc, Mutex}; +use thiserror::Error; +use wasmtime_debug::read_debuginfo; +use wasmtime_environ::entity::{BoxedSlice, PrimaryMap}; +use wasmtime_environ::wasm::{DefinedFuncIndex, SignatureIndex}; +use wasmtime_environ::{ + CompileError, DataInitializer, DataInitializerLocation, Module, ModuleEnvironment, +}; +use wasmtime_profiling::ProfilingAgent; +use wasmtime_runtime::{ + GdbJitImageRegistration, InstanceHandle, InstantiationError, TrapRegistration, VMFunctionBody, + VMSharedSignatureIndex, +}; + +/// An error condition while setting up a wasm instance, be it validation, +/// compilation, or instantiation. +#[derive(Error, Debug)] +pub enum SetupError { + /// The module did not pass validation. + #[error("Validation error: {0}")] + Validate(String), + + /// A wasm translation error occured. + #[error("WebAssembly failed to compile")] + Compile(#[from] CompileError), + + /// Some runtime resource was unavailable or insufficient, or the start function + /// trapped. + #[error("Instantiation failed during setup")] + Instantiate(#[from] InstantiationError), + + /// Debug information generation error occured. + #[error("Debug information error")] + DebugInfo(#[from] anyhow::Error), +} + +/// This is similar to `CompiledModule`, but references the data initializers +/// from the wasm buffer rather than holding its own copy. +struct RawCompiledModule<'data> { + module: Module, + finished_functions: BoxedSlice, + data_initializers: Box<[DataInitializer<'data>]>, + signatures: BoxedSlice, + dbg_jit_registration: Option, + trap_registration: TrapRegistration, +} + +impl<'data> RawCompiledModule<'data> { + /// Create a new `RawCompiledModule` by compiling the wasm module in `data` and instatiating it. + fn new( + compiler: &mut Compiler, + data: &'data [u8], + debug_info: bool, + profiler: Option<&Arc>>>, + ) -> Result { + let environ = ModuleEnvironment::new(compiler.frontend_config(), compiler.tunables()); + + let translation = environ + .translate(data) + .map_err(|error| SetupError::Compile(CompileError::Wasm(error)))?; + + let debug_data = if debug_info { + Some(read_debuginfo(&data)) + } else { + None + }; + + let (finished_functions, jt_offsets, relocations, dbg_image, trap_registration) = compiler + .compile( + &translation.module, + translation.module_translation.as_ref().unwrap(), + translation.function_body_inputs, + debug_data, + )?; + + link_module( + &translation.module, + &finished_functions, + &jt_offsets, + relocations, + ); + + // Compute indices into the shared signature table. + let signatures = { + let signature_registry = compiler.signatures(); + translation + .module + .local + .signatures + .values() + .map(|sig| signature_registry.register(sig)) + .collect::>() + }; + + // Make all code compiled thus far executable. + compiler.publish_compiled_code(); + + // Initialize profiler and load the wasm module + match profiler { + Some(_) => { + let region_name = String::from("wasm_module"); + let mut profiler = profiler.unwrap().lock().unwrap(); + match &dbg_image { + Some(dbg) => { + compiler.profiler_module_load(&mut profiler, ®ion_name, Some(&dbg)) + } + _ => compiler.profiler_module_load(&mut profiler, ®ion_name, None), + }; + } + _ => (), + }; + + let dbg_jit_registration = if let Some(img) = dbg_image { + let mut bytes = Vec::new(); + bytes.write_all(&img).expect("all written"); + let reg = GdbJitImageRegistration::register(bytes); + Some(reg) + } else { + None + }; + + Ok(Self { + module: translation.module, + finished_functions: finished_functions.into_boxed_slice(), + data_initializers: translation.data_initializers.into_boxed_slice(), + signatures: signatures.into_boxed_slice(), + dbg_jit_registration, + trap_registration, + }) + } +} + +/// A compiled wasm module, ready to be instantiated. +pub struct CompiledModule { + module: Arc, + finished_functions: BoxedSlice, + data_initializers: Box<[OwnedDataInitializer]>, + signatures: BoxedSlice, + dbg_jit_registration: Option>, + trap_registration: TrapRegistration, +} + +impl CompiledModule { + /// Compile a data buffer into a `CompiledModule`, which may then be instantiated. + pub fn new<'data>( + compiler: &mut Compiler, + data: &'data [u8], + debug_info: bool, + profiler: Option<&Arc>>>, + ) -> Result { + let raw = RawCompiledModule::<'data>::new(compiler, data, debug_info, profiler)?; + + Ok(Self::from_parts( + raw.module, + raw.finished_functions, + raw.data_initializers + .iter() + .map(OwnedDataInitializer::new) + .collect::>() + .into_boxed_slice(), + raw.signatures.clone(), + raw.dbg_jit_registration, + raw.trap_registration, + )) + } + + /// Construct a `CompiledModule` from component parts. + pub fn from_parts( + module: Module, + finished_functions: BoxedSlice, + data_initializers: Box<[OwnedDataInitializer]>, + signatures: BoxedSlice, + dbg_jit_registration: Option, + trap_registration: TrapRegistration, + ) -> Self { + Self { + module: Arc::new(module), + finished_functions, + data_initializers, + signatures, + dbg_jit_registration: dbg_jit_registration.map(Rc::new), + trap_registration, + } + } + + /// Crate an `Instance` from this `CompiledModule`. + /// + /// Note that if only one instance of this module is needed, it may be more + /// efficient to call the top-level `instantiate`, since that avoids copying + /// the data initializers. + /// + /// # Unsafety + /// + /// See `InstanceHandle::new` + pub unsafe fn instantiate( + &self, + is_bulk_memory: bool, + resolver: &mut dyn Resolver, + ) -> Result { + let data_initializers = self + .data_initializers + .iter() + .map(|init| DataInitializer { + location: init.location.clone(), + data: &*init.data, + }) + .collect::>(); + let imports = resolve_imports(&self.module, resolver)?; + InstanceHandle::new( + Arc::clone(&self.module), + self.trap_registration.clone(), + self.finished_functions.clone(), + imports, + &data_initializers, + self.signatures.clone(), + self.dbg_jit_registration.as_ref().map(|r| Rc::clone(&r)), + is_bulk_memory, + Box::new(()), + ) + } + + /// Return a reference-counting pointer to a module. + pub fn module(&self) -> &Arc { + &self.module + } + + /// Return a reference to a module. + pub fn module_ref(&self) -> &Module { + &self.module + } + + /// Returns the map of all finished JIT functions compiled for this module + pub fn finished_functions(&self) -> &BoxedSlice { + &self.finished_functions + } +} + +/// Similar to `DataInitializer`, but owns its own copy of the data rather +/// than holding a slice of the original module. +pub struct OwnedDataInitializer { + /// The location where the initialization is to be performed. + location: DataInitializerLocation, + + /// The initialization data. + data: Box<[u8]>, +} + +impl OwnedDataInitializer { + fn new(borrowed: &DataInitializer<'_>) -> Self { + Self { + location: borrowed.location.clone(), + data: borrowed.data.to_vec().into_boxed_slice(), + } + } +} + +/// Create a new wasm instance by compiling the wasm module in `data` and instatiating it. +/// +/// This is equivalent to creating a `CompiledModule` and calling `instantiate()` on it, +/// but avoids creating an intermediate copy of the data initializers. +/// +/// # Unsafety +/// +/// See `InstanceHandle::new` +#[allow(clippy::implicit_hasher)] +pub unsafe fn instantiate( + compiler: &mut Compiler, + data: &[u8], + resolver: &mut dyn Resolver, + debug_info: bool, + is_bulk_memory: bool, + profiler: Option<&Arc>>>, +) -> Result { + let instance = CompiledModule::new(compiler, data, debug_info, profiler)? + .instantiate(is_bulk_memory, resolver)?; + Ok(instance) +} diff --git a/crates/jit/src/lib.rs b/crates/jit/src/lib.rs new file mode 100644 index 0000000000..0c56c58a6f --- /dev/null +++ b/crates/jit/src/lib.rs @@ -0,0 +1,44 @@ +//! JIT-style runtime for WebAssembly using Cranelift. + +#![deny(missing_docs, trivial_numeric_casts, unused_extern_crates)] +#![warn(unused_import_braces)] +#![cfg_attr(feature = "clippy", plugin(clippy(conf_file = "../../clippy.toml")))] +#![cfg_attr( + feature = "cargo-clippy", + allow(clippy::new_without_default, clippy::new_without_default) +)] +#![cfg_attr( + feature = "cargo-clippy", + warn( + clippy::float_arithmetic, + clippy::mut_mut, + clippy::nonminimal_bool, + clippy::option_map_unwrap_or, + clippy::option_map_unwrap_or_else, + clippy::print_stdout, + clippy::unicode_not_nfc, + clippy::use_self + ) +)] + +mod code_memory; +mod compiler; +mod function_table; +mod imports; +mod instantiate; +mod link; +mod resolver; +mod target_tunables; + +pub mod native; +pub mod trampoline; + +pub use crate::code_memory::CodeMemory; +pub use crate::compiler::{CompilationStrategy, Compiler}; +pub use crate::instantiate::{instantiate, CompiledModule, SetupError}; +pub use crate::link::link_module; +pub use crate::resolver::{NullResolver, Resolver}; +pub use crate::target_tunables::target_tunables; + +/// Version number of this crate. +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/crates/jit/src/link.rs b/crates/jit/src/link.rs new file mode 100644 index 0000000000..fcd715589a --- /dev/null +++ b/crates/jit/src/link.rs @@ -0,0 +1,126 @@ +//! Linking for JIT-compiled code. + +use cranelift_codegen::binemit::Reloc; +use cranelift_codegen::ir::JumpTableOffsets; +use std::ptr::write_unaligned; +use wasmtime_environ::entity::PrimaryMap; +use wasmtime_environ::wasm::DefinedFuncIndex; +use wasmtime_environ::{Module, RelocationTarget, Relocations}; +use wasmtime_runtime::libcalls; +use wasmtime_runtime::VMFunctionBody; + +/// Links a module that has been compiled with `compiled_module` in `wasmtime-environ`. +/// +/// Performs all required relocations inside the function code, provided the necessary metadata. +pub fn link_module( + module: &Module, + allocated_functions: &PrimaryMap, + jt_offsets: &PrimaryMap, + relocations: Relocations, +) { + for (i, function_relocs) in relocations.into_iter() { + for r in function_relocs { + use self::libcalls::*; + let target_func_address: usize = match r.reloc_target { + RelocationTarget::UserFunc(index) => match module.local.defined_func_index(index) { + Some(f) => { + let fatptr: *const [VMFunctionBody] = allocated_functions[f]; + fatptr as *const VMFunctionBody as usize + } + None => panic!("direct call to import"), + }, + RelocationTarget::LibCall(libcall) => { + use cranelift_codegen::ir::LibCall::*; + match libcall { + CeilF32 => wasmtime_f32_ceil as usize, + FloorF32 => wasmtime_f32_floor as usize, + TruncF32 => wasmtime_f32_trunc as usize, + NearestF32 => wasmtime_f32_nearest as usize, + CeilF64 => wasmtime_f64_ceil as usize, + FloorF64 => wasmtime_f64_floor as usize, + TruncF64 => wasmtime_f64_trunc as usize, + NearestF64 => wasmtime_f64_nearest as usize, + Probestack => PROBESTACK as usize, + other => panic!("unexpected libcall: {}", other), + } + } + RelocationTarget::JumpTable(func_index, jt) => { + match module.local.defined_func_index(func_index) { + Some(f) => { + let offset = *jt_offsets + .get(f) + .and_then(|ofs| ofs.get(jt)) + .expect("func jump table"); + let fatptr: *const [VMFunctionBody] = allocated_functions[f]; + fatptr as *const VMFunctionBody as usize + offset as usize + } + None => panic!("func index of jump table"), + } + } + }; + + let fatptr: *const [VMFunctionBody] = allocated_functions[i]; + let body = fatptr as *const VMFunctionBody; + match r.reloc { + #[cfg(target_pointer_width = "64")] + Reloc::Abs8 => unsafe { + let reloc_address = body.add(r.offset as usize) as usize; + let reloc_addend = r.addend as isize; + let reloc_abs = (target_func_address as u64) + .checked_add(reloc_addend as u64) + .unwrap(); + write_unaligned(reloc_address as *mut u64, reloc_abs); + }, + #[cfg(target_pointer_width = "32")] + Reloc::X86PCRel4 => unsafe { + let reloc_address = body.add(r.offset as usize) as usize; + let reloc_addend = r.addend as isize; + let reloc_delta_u32 = (target_func_address as u32) + .wrapping_sub(reloc_address as u32) + .checked_add(reloc_addend as u32) + .unwrap(); + write_unaligned(reloc_address as *mut u32, reloc_delta_u32); + }, + #[cfg(target_pointer_width = "32")] + Reloc::X86CallPCRel4 => { + // ignore + } + Reloc::X86PCRelRodata4 => { + // ignore + } + _ => panic!("unsupported reloc kind"), + } + } + } +} + +// A declaration for the stack probe function in Rust's standard library, for +// catching callstack overflow. +cfg_if::cfg_if! { + if #[cfg(any( + target_arch="aarch64", + all( + target_os = "windows", + target_env = "msvc", + target_pointer_width = "64" + ) + ))] { + extern "C" { + pub fn __chkstk(); + } + const PROBESTACK: unsafe extern "C" fn() = __chkstk; + } else if #[cfg(all(target_os = "windows", target_env = "gnu"))] { + extern "C" { + // ___chkstk (note the triple underscore) is implemented in compiler-builtins/src/x86_64.rs + // by the Rust compiler for the MinGW target + #[cfg(all(target_os = "windows", target_env = "gnu"))] + pub fn ___chkstk(); + } + const PROBESTACK: unsafe extern "C" fn() = ___chkstk; + } else { + extern "C" { + pub fn __rust_probestack(); + } + static PROBESTACK: unsafe extern "C" fn() = __rust_probestack; + } +} diff --git a/crates/jit/src/native.rs b/crates/jit/src/native.rs new file mode 100644 index 0000000000..9d1fdd7b66 --- /dev/null +++ b/crates/jit/src/native.rs @@ -0,0 +1,14 @@ +#![allow(missing_docs)] + +use cranelift_codegen; + +pub fn builder() -> cranelift_codegen::isa::Builder { + cranelift_native::builder().expect("host machine is not a supported target") +} + +pub fn call_conv() -> cranelift_codegen::isa::CallConv { + use target_lexicon::HOST; + cranelift_codegen::isa::CallConv::triple_default(&HOST) +} + +pub use cranelift_codegen::isa::lookup; diff --git a/crates/jit/src/resolver.rs b/crates/jit/src/resolver.rs new file mode 100644 index 0000000000..34f052c45a --- /dev/null +++ b/crates/jit/src/resolver.rs @@ -0,0 +1,26 @@ +//! Define the `Resolver` trait, allowing custom resolution for external +//! references. + +use wasmtime_runtime::Export; + +/// Import resolver connects imports with available exported values. +pub trait Resolver { + /// Resolves an import a WebAssembly module to an export it's hooked up to. + /// + /// The `index` provided is the index of the import in the wasm module + /// that's being resolved. For example 1 means that it's the second import + /// listed in the wasm module. + /// + /// The `module` and `field` arguments provided are the module/field names + /// listed on the import itself. + fn resolve(&mut self, index: u32, module: &str, field: &str) -> Option; +} + +/// `Resolver` implementation that always resolves to `None`. +pub struct NullResolver {} + +impl Resolver for NullResolver { + fn resolve(&mut self, _idx: u32, _module: &str, _field: &str) -> Option { + None + } +} diff --git a/crates/jit/src/target_tunables.rs b/crates/jit/src/target_tunables.rs new file mode 100644 index 0000000000..2fd5b4848f --- /dev/null +++ b/crates/jit/src/target_tunables.rs @@ -0,0 +1,22 @@ +use std::cmp::min; +use target_lexicon::{OperatingSystem, Triple}; +use wasmtime_environ::Tunables; + +/// Return a `Tunables` instance tuned for the given target platform. +pub fn target_tunables(triple: &Triple) -> Tunables { + let mut result = Tunables::default(); + + match triple.operating_system { + OperatingSystem::Windows => { + // For now, use a smaller footprint on Windows so that we don't + // don't outstrip the paging file. + // TODO: Make this configurable. + result.static_memory_bound = min(result.static_memory_bound, 0x100); + result.static_memory_offset_guard_size = + min(result.static_memory_offset_guard_size, 0x10000); + } + _ => {} + } + + result +} diff --git a/crates/jit/src/trampoline.rs b/crates/jit/src/trampoline.rs new file mode 100644 index 0000000000..8d32cd714d --- /dev/null +++ b/crates/jit/src/trampoline.rs @@ -0,0 +1,57 @@ +#![allow(missing_docs)] + +pub mod ir { + pub use cranelift_codegen::ir::{ + ExternalName, Function, InstBuilder, MemFlags, StackSlotData, StackSlotKind, + }; +} +pub use cranelift_codegen::print_errors::pretty_error; +pub use cranelift_codegen::Context; +pub use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext}; + +pub mod binemit { + pub use cranelift_codegen::binemit::NullTrapSink; + pub use cranelift_codegen::binemit::{CodeOffset, NullStackmapSink, TrapSink}; + + use cranelift_codegen::{binemit, ir}; + + /// We don't expect trampoline compilation to produce any relocations, so + /// this `RelocSink` just asserts that it doesn't recieve any. + pub struct TrampolineRelocSink {} + + impl binemit::RelocSink for TrampolineRelocSink { + fn reloc_block( + &mut self, + _offset: binemit::CodeOffset, + _reloc: binemit::Reloc, + _block_offset: binemit::CodeOffset, + ) { + panic!("trampoline compilation should not produce block relocs"); + } + fn reloc_external( + &mut self, + _offset: binemit::CodeOffset, + _reloc: binemit::Reloc, + _name: &ir::ExternalName, + _addend: binemit::Addend, + ) { + panic!("trampoline compilation should not produce external symbol relocs"); + } + fn reloc_constant( + &mut self, + _code_offset: binemit::CodeOffset, + _reloc: binemit::Reloc, + _constant_offset: ir::ConstantOffset, + ) { + panic!("trampoline compilation should not produce constant relocs"); + } + fn reloc_jt( + &mut self, + _offset: binemit::CodeOffset, + _reloc: binemit::Reloc, + _jt: ir::JumpTable, + ) { + panic!("trampoline compilation should not produce jump table relocs"); + } + } +} diff --git a/crates/lightbeam/Cargo.toml b/crates/lightbeam/Cargo.toml new file mode 100644 index 0000000000..f51889b9b2 --- /dev/null +++ b/crates/lightbeam/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "lightbeam" +version = "0.12.0" +authors = ["The Lightbeam Project Developers"] +description = "An optimising one-pass streaming compiler for WebAssembly" +license = "Apache-2.0 WITH LLVM-exception" +repository = "https://github.com/bytecodealliance/wasmtime" +readme = "README.md" +categories = ["wasm"] +keywords = ["webassembly", "wasm", "compile", "compiler", "jit"] +edition = "2018" + +[dependencies] +smallvec = "1.0.0" +dynasm = "0.5.2" +dynasmrt = "0.5.2" +wasmparser = "0.51.2" +memoffset = "0.5.3" +itertools = "0.8.2" +capstone = "0.6.0" +thiserror = "1.0.9" +cranelift-codegen = "0.59.0" +multi_mut = "0.1" +either = "1.5" +typemap = "0.3" +more-asserts = "0.2.1" + +[dev-dependencies] +lazy_static = "1.2" +wat = "1.0.9" +quickcheck = "0.9.0" +anyhow = "1.0" + +[badges] +maintenance = { status = "experimental" } diff --git a/crates/lightbeam/LICENSE b/crates/lightbeam/LICENSE new file mode 100644 index 0000000000..f9d81955f4 --- /dev/null +++ b/crates/lightbeam/LICENSE @@ -0,0 +1,220 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + diff --git a/crates/lightbeam/README.md b/crates/lightbeam/README.md new file mode 100644 index 0000000000..9d40d55d99 --- /dev/null +++ b/crates/lightbeam/README.md @@ -0,0 +1,168 @@ +# Lightbeam + +Lightbeam is an optimising one-pass streaming compiler for WebAssembly, intended for use in [Wasmtime][wasmtime]. + +[wasmtime]: https://github.com/bytecodealliance/wasmtime + +## Quality of output + +Already - with a very small number of relatively simple optimisation rules - Lightbeam produces surprisingly high-quality output considering how restricted it is. It even produces better code than Cranelift, Firefox or both for some workloads. Here's a very simple example, this recursive fibonacci function in Rust: + +```rust +fn fib(n: i32) -> i32 { + if n == 0 || n == 1 { + 1 + } else { + fib(n - 1) + fib(n - 2) + } +} +``` + +When compiled with optimisations enabled, rustc will produce the following WebAssembly: + +```rust +(module + (func $fib (param $p0 i32) (result i32) + (local $l1 i32) + (set_local $l1 + (i32.const 1)) + (block $B0 + (br_if $B0 + (i32.lt_u + (get_local $p0) + (i32.const 2))) + (set_local $l1 + (i32.const 1)) + (loop $L1 + (set_local $l1 + (i32.add + (call $fib + (i32.add + (get_local $p0) + (i32.const -1))) + (get_local $l1))) + (br_if $L1 + (i32.gt_u + (tee_local $p0 + (i32.add + (get_local $p0) + (i32.const -2))) + (i32.const 1))))) + (get_local $l1))) +``` + +Firefox's optimising compiler produces the following assembly (labels cleaned up somewhat): + +```asm +fib: + sub rsp, 0x18 + cmp qword ptr [r14 + 0x28], rsp + jae stack_overflow + mov dword ptr [rsp + 0xc], edi + cmp edi, 2 + jae .Lelse + mov eax, 1 + mov dword ptr [rsp + 8], eax + jmp .Lreturn +.Lelse: + mov dword ptr [rsp + 0xc], edi + mov eax, 1 + mov dword ptr [rsp + 8], eax +.Lloop: + mov edi, dword ptr [rsp + 0xc] + add edi, -1 + call 0 + mov ecx, dword ptr [rsp + 8] + add ecx, eax + mov dword ptr [rsp + 8], ecx + mov ecx, dword ptr [rsp + 0xc] + add ecx, -2 + mov dword ptr [rsp + 0xc], ecx + cmp ecx, 1 + ja .Lloop +.Lreturn: + mov eax, dword ptr [rsp + 8] + nop + add rsp, 0x18 + ret +``` + +Cranelift with optimisations enabled produces similar: + +```asm +fib: + push rbp + mov rbp, rsp + sub rsp, 0x20 + mov qword ptr [rsp + 0x10], rdi + mov dword ptr [rsp + 0x1c], esi + mov eax, 1 + mov dword ptr [rsp + 0x18], eax + mov eax, dword ptr [rsp + 0x1c] + cmp eax, 2 + jb .Lreturn + movabs rax, 0 + mov qword ptr [rsp + 8], rax +.Lloop: + mov eax, dword ptr [rsp + 0x1c] + add eax, -1 + mov rcx, qword ptr [rsp + 8] + mov rdx, qword ptr [rsp + 0x10] + mov rdi, rdx + mov esi, eax + call rcx + mov ecx, dword ptr [rsp + 0x18] + add eax, ecx + mov dword ptr [rsp + 0x18], eax + mov eax, dword ptr [rsp + 0x1c] + add eax, -2 + mov dword ptr [rsp + 0x1c], eax + mov eax, dword ptr [rsp + 0x1c] + cmp eax, 1 + ja .Lloop +.Lreturn: + mov eax, dword ptr [rsp + 0x18] + add rsp, 0x20 + pop rbp + ret +``` + +Whereas Lightbeam produces smaller code with far fewer memory accesses than both (and fewer blocks than Firefox's output): + +```asm +fib: + cmp esi, 2 + mov eax, 1 + jb .Lreturn + mov eax, 1 +.Lloop: + mov rcx, rsi + add ecx, 0xffffffff + push rsi + push rax + push rax + mov rsi, rcx + call fib + add eax, [rsp + 8] + mov rcx, [rsp + 0x10] + add ecx, 0xfffffffe + cmp ecx, 1 + mov rsi, rcx + lea rsp, [rsp + 0x18] + ja .Lloop +.Lreturn: + ret +``` + +Now obviously I'm not advocating for replacing Firefox's optimising compiler with Lightbeam since the latter can only really produce better code when receiving optimised WebAssembly (and so debug-mode or hand-written WebAssembly may produce much worse output). However, this shows that even with the restrictions of a streaming compiler it's absolutely possible to produce high-quality assembly output. For the assembly above, the Lightbeam output runs within 15% of native speed. This is paramount for one of Lightbeam's intended usecases for real-time systems that want good runtime performance but cannot tolerate compiler bombs. + +## Specification compliance + +Lightbeam passes 100% of the specification test suite, but that doesn't necessarily mean that it's 100% specification-compliant. Hopefully as we run a fuzzer against it we can find any issues and get Lightbeam to a state where it can be used in production. + +## Getting involved + +You can file issues in the [Wasmtime issue tracker][Wasmtime issue tracker]. If you want to get involved jump into the [CraneStation Gitter room][cranestation-gitter] and someone can direct you to the right place. I wish I could say "the most useful thing you can do is play with it and open issues where you find problems" but until it passes the spec suite that won't be very helpful. + +[cranestation-gitter]: https://gitter.im/CraneStation/Lobby +[Wasmtime issue tracker]: https://github.com/bytecodealliance/wasmtime/issues diff --git a/crates/lightbeam/examples/test.rs b/crates/lightbeam/examples/test.rs new file mode 100644 index 0000000000..12d54193ae --- /dev/null +++ b/crates/lightbeam/examples/test.rs @@ -0,0 +1,16 @@ +use lightbeam::translate; + +const WAT: &str = r#" +(module + (func (param i32) (param i32) (result i32) (i32.add (get_local 0) (get_local 1))) +) +"#; + +fn main() -> anyhow::Result<()> { + let data = wat::parse_str(WAT)?; + let translated = translate(&data)?; + let result: u32 = translated.execute_func(0, (5u32, 3u32))?; + println!("f(5, 3) = {}", result); + + Ok(()) +} diff --git a/crates/lightbeam/src/backend.rs b/crates/lightbeam/src/backend.rs new file mode 100644 index 0000000000..4b8f36215c --- /dev/null +++ b/crates/lightbeam/src/backend.rs @@ -0,0 +1,6003 @@ +#![allow(clippy::float_cmp)] + +use self::registers::*; +use crate::error::Error; +use crate::microwasm::{BrTarget, Ieee32, Ieee64, SignlessType, Type, Value, F32, F64, I32, I64}; +use crate::module::ModuleContext; +use cranelift_codegen::{binemit, ir}; +use dynasm::dynasm; +use dynasmrt::x64::Assembler; +use dynasmrt::{AssemblyOffset, DynamicLabel, DynasmApi, DynasmLabelApi, ExecutableBuffer}; +use either::Either; + +use std::{ + any::{Any, TypeId}, + cmp::Ordering, + collections::HashMap, + convert::{TryFrom, TryInto}, + fmt::Display, + iter::{self, FromIterator}, + mem, + ops::RangeInclusive, +}; + +// TODO: Get rid of this! It's a total hack. +mod magic { + use cranelift_codegen::ir; + + /// Compute an `ir::ExternalName` for the `memory.grow` libcall for + /// 32-bit locally-defined memories. + pub fn get_memory32_grow_name() -> ir::ExternalName { + ir::ExternalName::user(1, 0) + } + + /// Compute an `ir::ExternalName` for the `memory.grow` libcall for + /// 32-bit imported memories. + pub fn get_imported_memory32_grow_name() -> ir::ExternalName { + ir::ExternalName::user(1, 1) + } + + /// Compute an `ir::ExternalName` for the `memory.size` libcall for + /// 32-bit locally-defined memories. + pub fn get_memory32_size_name() -> ir::ExternalName { + ir::ExternalName::user(1, 2) + } + + /// Compute an `ir::ExternalName` for the `memory.size` libcall for + /// 32-bit imported memories. + pub fn get_imported_memory32_size_name() -> ir::ExternalName { + ir::ExternalName::user(1, 3) + } +} + +/// Size of a pointer on the target in bytes. +const WORD_SIZE: u32 = 8; + +type RegId = u8; + +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] +pub enum GPR { + Rq(RegId), + Rx(RegId), +} + +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] +pub enum GPRType { + Rq, + Rx, +} + +impl From for GPRType { + fn from(other: SignlessType) -> GPRType { + match other { + I32 | I64 => GPRType::Rq, + F32 | F64 => GPRType::Rx, + } + } +} + +impl From for Option { + fn from(other: SignlessType) -> Self { + Some(other.into()) + } +} + +impl GPR { + fn type_(self) -> GPRType { + match self { + GPR::Rq(_) => GPRType::Rq, + GPR::Rx(_) => GPRType::Rx, + } + } + + fn rq(self) -> Option { + match self { + GPR::Rq(r) => Some(r), + GPR::Rx(_) => None, + } + } + + fn rx(self) -> Option { + match self { + GPR::Rx(r) => Some(r), + GPR::Rq(_) => None, + } + } +} + +pub fn arg_locs(types: impl IntoIterator) -> Result, Error> { + let types = types.into_iter(); + let mut out = Vec::with_capacity(types.size_hint().0); + // TODO: VmCtx is in the first register + let mut int_gpr_iter = INTEGER_ARGS_IN_GPRS.iter(); + let mut float_gpr_iter = FLOAT_ARGS_IN_GPRS.iter(); + let mut stack_idx = 0; + + for ty in types { + match ty { + I32 | I64 => out.push(int_gpr_iter.next().map(|&r| CCLoc::Reg(r)).unwrap_or_else( + || { + let out = CCLoc::Stack(stack_idx); + stack_idx += 1; + out + }, + )), + F32 | F64 => match float_gpr_iter.next() { + None => { + return Err(Error::Microwasm( + "Float args on stack not yet supported".to_string(), + )) + } + Some(val) => out.push(CCLoc::Reg(*val)), + }, + } + } + + Ok(out) +} + +pub fn ret_locs(types: impl IntoIterator) -> Result, Error> { + let types = types.into_iter(); + let mut out = Vec::with_capacity(types.size_hint().0); + // TODO: VmCtx is in the first register + let mut int_gpr_iter = INTEGER_RETURN_GPRS.iter(); + let mut float_gpr_iter = FLOAT_RETURN_GPRS.iter(); + + for ty in types { + match ty { + I32 | I64 => match int_gpr_iter.next() { + None => { + return Err(Error::Microwasm( + "We don't support stack returns yet".to_string(), + )) + } + Some(val) => out.push(CCLoc::Reg(*val)), + }, + F32 | F64 => match float_gpr_iter.next() { + None => { + return Err(Error::Microwasm( + "We don't support stack returns yet".to_string(), + )) + } + Some(val) => out.push(CCLoc::Reg(*val)), + }, + } + } + + Ok(out) +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +struct GPRs { + bits: u16, +} + +impl GPRs { + fn new() -> Self { + Self { bits: 0 } + } +} + +#[allow(dead_code)] +pub mod registers { + use super::{RegId, GPR}; + + pub mod rq { + use super::RegId; + + pub const RAX: RegId = 0; + pub const RCX: RegId = 1; + pub const RDX: RegId = 2; + pub const RBX: RegId = 3; + pub const RSP: RegId = 4; + pub const RBP: RegId = 5; + pub const RSI: RegId = 6; + pub const RDI: RegId = 7; + pub const R8: RegId = 8; + pub const R9: RegId = 9; + pub const R10: RegId = 10; + pub const R11: RegId = 11; + pub const R12: RegId = 12; + pub const R13: RegId = 13; + pub const R14: RegId = 14; + pub const R15: RegId = 15; + } + + pub const RAX: GPR = GPR::Rq(self::rq::RAX); + pub const RCX: GPR = GPR::Rq(self::rq::RCX); + pub const RDX: GPR = GPR::Rq(self::rq::RDX); + pub const RBX: GPR = GPR::Rq(self::rq::RBX); + pub const RSP: GPR = GPR::Rq(self::rq::RSP); + pub const RBP: GPR = GPR::Rq(self::rq::RBP); + pub const RSI: GPR = GPR::Rq(self::rq::RSI); + pub const RDI: GPR = GPR::Rq(self::rq::RDI); + pub const R8: GPR = GPR::Rq(self::rq::R8); + pub const R9: GPR = GPR::Rq(self::rq::R9); + pub const R10: GPR = GPR::Rq(self::rq::R10); + pub const R11: GPR = GPR::Rq(self::rq::R11); + pub const R12: GPR = GPR::Rq(self::rq::R12); + pub const R13: GPR = GPR::Rq(self::rq::R13); + pub const R14: GPR = GPR::Rq(self::rq::R14); + pub const R15: GPR = GPR::Rq(self::rq::R15); + + pub const XMM0: GPR = GPR::Rx(0); + pub const XMM1: GPR = GPR::Rx(1); + pub const XMM2: GPR = GPR::Rx(2); + pub const XMM3: GPR = GPR::Rx(3); + pub const XMM4: GPR = GPR::Rx(4); + pub const XMM5: GPR = GPR::Rx(5); + pub const XMM6: GPR = GPR::Rx(6); + pub const XMM7: GPR = GPR::Rx(7); + pub const XMM8: GPR = GPR::Rx(8); + pub const XMM9: GPR = GPR::Rx(9); + pub const XMM10: GPR = GPR::Rx(10); + pub const XMM11: GPR = GPR::Rx(11); + pub const XMM12: GPR = GPR::Rx(12); + pub const XMM13: GPR = GPR::Rx(13); + pub const XMM14: GPR = GPR::Rx(14); + pub const XMM15: GPR = GPR::Rx(15); + + pub const NUM_GPRS: u8 = 16; +} + +const SIGN_MASK_F64: u64 = 0x8000_0000_0000_0000; +const REST_MASK_F64: u64 = !SIGN_MASK_F64; +const SIGN_MASK_F32: u32 = 0x8000_0000; +const REST_MASK_F32: u32 = !SIGN_MASK_F32; + +impl GPRs { + fn take(&mut self) -> Option { + let lz = self.bits.trailing_zeros(); + if lz < 16 { + let gpr = lz as RegId; + self.mark_used(gpr); + Some(gpr) + } else { + None + } + } + + fn mark_used(&mut self, gpr: RegId) { + self.bits &= !(1 << gpr as u16); + } + + fn release(&mut self, gpr: RegId) { + debug_assert!( + !self.is_free(gpr), + "released register {} was already free", + gpr + ); + self.bits |= 1 << gpr; + } + + fn is_free(self, gpr: RegId) -> bool { + (self.bits & (1 << gpr)) != 0 + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct Registers { + /// Registers at 64 bits and below (al/ah/ax/eax/rax, for example) + scratch_64: (GPRs, [u8; NUM_GPRS as usize]), + /// Registers at 128 bits (xmm0, for example) + scratch_128: (GPRs, [u8; NUM_GPRS as usize]), +} + +impl Default for Registers { + fn default() -> Self { + Self::new() + } +} + +impl Registers { + pub fn new() -> Self { + Self { + scratch_64: (GPRs::new(), [1; NUM_GPRS as _]), + scratch_128: (GPRs::new(), [1; NUM_GPRS as _]), + } + } + + pub fn release_scratch_register(&mut self) -> Result<(), Error> { + // Give ourselves a few scratch registers to work with, for now. + for &scratch in SCRATCH_REGS { + self.release(scratch)?; + } + Ok(()) + } + + fn scratch_counts_mut(&mut self, gpr: GPR) -> (u8, &mut (GPRs, [u8; NUM_GPRS as usize])) { + match gpr { + GPR::Rq(r) => (r, &mut self.scratch_64), + GPR::Rx(r) => (r, &mut self.scratch_128), + } + } + + fn scratch_counts(&self, gpr: GPR) -> (u8, &(GPRs, [u8; NUM_GPRS as usize])) { + match gpr { + GPR::Rq(r) => (r, &self.scratch_64), + GPR::Rx(r) => (r, &self.scratch_128), + } + } + + pub fn mark_used(&mut self, gpr: GPR) { + let (gpr, scratch_counts) = self.scratch_counts_mut(gpr); + scratch_counts.0.mark_used(gpr); + scratch_counts.1[gpr as usize] += 1; + } + + pub fn num_usages(&self, gpr: GPR) -> u8 { + let (gpr, scratch_counts) = self.scratch_counts(gpr); + scratch_counts.1[gpr as usize] + } + + pub fn take(&mut self, ty: impl Into) -> Option { + let (mk_gpr, scratch_counts) = match ty.into() { + GPRType::Rq => (GPR::Rq as fn(_) -> _, &mut self.scratch_64), + GPRType::Rx => (GPR::Rx as fn(_) -> _, &mut self.scratch_128), + }; + + let out = scratch_counts.0.take()?; + scratch_counts.1[out as usize] += 1; + Some(mk_gpr(out)) + } + + pub fn release(&mut self, gpr: GPR) -> Result<(), Error> { + let (gpr, scratch_counts) = self.scratch_counts_mut(gpr); + let c = &mut scratch_counts.1[gpr as usize]; + *c = match c.checked_sub(1) { + Some(e) => e, + None => return Err(Error::Microwasm(format!("Double-freed register: {}", gpr))), + }; + if *c == 0 { + scratch_counts.0.release(gpr); + } + Ok(()) + } + + pub fn is_free(&self, gpr: GPR) -> bool { + let (gpr, scratch_counts) = self.scratch_counts(gpr); + scratch_counts.0.is_free(gpr) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BlockCallingConvention { + pub stack_depth: StackDepth, + pub arguments: Vec, +} + +impl BlockCallingConvention { + pub fn function_start(args: impl IntoIterator) -> Self { + BlockCallingConvention { + // We start and return the function with stack depth 1 since we must + // allow space for the saved return address. + stack_depth: StackDepth(1), + arguments: Vec::from_iter(args), + } + } +} + +// TODO: Combine this with `ValueLocation`? +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum CCLoc { + /// Value exists in a register. + Reg(GPR), + /// Value exists on the stack. + Stack(i32), +} + +impl CCLoc { + fn try_from(other: ValueLocation) -> Option { + match other { + ValueLocation::Reg(reg) => Some(CCLoc::Reg(reg)), + ValueLocation::Stack(offset) => Some(CCLoc::Stack(offset)), + _ => None, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum CondCode { + CF0, + CF1, + ZF0, + ZF1, + CF0AndZF0, + CF1OrZF1, + ZF0AndSFEqOF, + ZF1OrSFNeOF, + SFEqOF, + SFNeOF, +} + +mod cc { + use super::CondCode; + + pub const EQUAL: CondCode = CondCode::ZF0; + pub const NOT_EQUAL: CondCode = CondCode::ZF1; + pub const GE_U: CondCode = CondCode::CF0; + pub const LT_U: CondCode = CondCode::CF1; + pub const GT_U: CondCode = CondCode::CF0AndZF0; + pub const LE_U: CondCode = CondCode::CF1OrZF1; + pub const GE_S: CondCode = CondCode::SFEqOF; + pub const LT_S: CondCode = CondCode::SFNeOF; + pub const GT_S: CondCode = CondCode::ZF0AndSFEqOF; + pub const LE_S: CondCode = CondCode::ZF1OrSFNeOF; +} + +impl std::ops::Not for CondCode { + type Output = Self; + + fn not(self) -> Self { + use CondCode::*; + + match self { + CF0 => CF1, + CF1 => CF0, + ZF0 => ZF1, + ZF1 => ZF0, + CF0AndZF0 => CF1OrZF1, + CF1OrZF1 => CF0AndZF0, + ZF0AndSFEqOF => ZF1OrSFNeOF, + ZF1OrSFNeOF => ZF0AndSFEqOF, + SFEqOF => SFNeOF, + SFNeOF => SFEqOF, + } + } +} + +/// Describes location of a value. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ValueLocation { + /// Value exists in a register. + Reg(GPR), + /// Value exists on the stack. Note that this offset is from the rsp as it + /// was when we entered the function. + Stack(i32), + /// Value is a literal + Immediate(Value), + /// Value is a set condition code + Cond(CondCode), +} + +impl From for ValueLocation { + fn from(other: CCLoc) -> Self { + match other { + CCLoc::Reg(r) => ValueLocation::Reg(r), + CCLoc::Stack(o) => ValueLocation::Stack(o), + } + } +} + +impl ValueLocation { + fn stack(self) -> Option { + match self { + ValueLocation::Stack(o) => Some(o), + _ => None, + } + } + + fn reg(self) -> Option { + match self { + ValueLocation::Reg(r) => Some(r), + _ => None, + } + } + + fn immediate(self) -> Option { + match self { + ValueLocation::Immediate(i) => Some(i), + _ => None, + } + } + + fn imm_i32(self) -> Option { + self.immediate().and_then(Value::as_i32) + } + + fn imm_i64(self) -> Option { + self.immediate().and_then(Value::as_i64) + } + + fn imm_f32(self) -> Option { + self.immediate().and_then(Value::as_f32) + } + + fn imm_f64(self) -> Option { + self.immediate().and_then(Value::as_f64) + } +} + +// TODO: This assumes only system-v calling convention. +// In system-v calling convention the first 6 arguments are passed via registers. +// All rest arguments are passed on the stack. +const INTEGER_ARGS_IN_GPRS: &[GPR] = &[RSI, RDX, RCX, R8, R9]; +const INTEGER_RETURN_GPRS: &[GPR] = &[RAX, RDX]; +const FLOAT_ARGS_IN_GPRS: &[GPR] = &[XMM0, XMM1, XMM2, XMM3, XMM4, XMM5, XMM6, XMM7]; +const FLOAT_RETURN_GPRS: &[GPR] = &[XMM0, XMM1]; +// List of scratch registers taken from https://wiki.osdev.org/System_V_ABI +const SCRATCH_REGS: &[GPR] = &[ + RSI, RDX, RCX, R8, R9, RAX, R10, R11, XMM0, XMM1, XMM2, XMM3, XMM4, XMM5, XMM6, XMM7, XMM8, + XMM9, XMM10, XMM11, XMM12, XMM13, XMM14, XMM15, +]; +const VMCTX: RegId = rq::RDI; + +#[must_use] +#[derive(Debug, Clone)] +pub struct FunctionEnd { + should_generate_epilogue: bool, +} + +pub struct CodeGenSession<'module, M> { + assembler: Assembler, + pub module_context: &'module M, + pub op_offset_map: Vec<(AssemblyOffset, Box)>, + labels: Labels, + func_starts: Vec<(Option, DynamicLabel)>, +} + +impl<'module, M> CodeGenSession<'module, M> { + pub fn new(func_count: u32, module_context: &'module M) -> Self { + let mut assembler = Assembler::new().unwrap(); + let func_starts = iter::repeat_with(|| (None, assembler.new_dynamic_label())) + .take(func_count as usize) + .collect::>(); + + CodeGenSession { + assembler, + op_offset_map: Default::default(), + labels: Default::default(), + func_starts, + module_context, + } + } + + pub fn new_context<'this>( + &'this mut self, + func_idx: u32, + reloc_sink: &'this mut dyn binemit::RelocSink, + ) -> Context<'this, M> { + { + let func_start = &mut self.func_starts[func_idx as usize]; + + // At this point we know the exact start address of this function. Save it + // and define dynamic label at this location. + func_start.0 = Some(self.assembler.offset()); + self.assembler.dynamic_label(func_start.1); + } + + Context { + asm: &mut self.assembler, + current_function: func_idx, + reloc_sink, + func_starts: &self.func_starts, + labels: &mut self.labels, + block_state: Default::default(), + module_context: self.module_context, + } + } + + fn finalize(&mut self) { + let mut values = self.labels.values_mut().collect::>(); + values.sort_unstable_by_key(|(_, align, _)| *align); + for (label, align, func) in values { + if let Some(mut func) = func.take() { + dynasm!(self.assembler + ; .align *align as usize + ); + self.assembler.dynamic_label(label.0); + func(&mut self.assembler); + } + } + } + + pub fn into_translated_code_section(mut self) -> Result { + self.finalize(); + let exec_buf = self + .assembler + .finalize() + .map_err(|_asm| Error::Assembler("assembler error".to_owned()))?; + let func_starts = self + .func_starts + .iter() + .map(|(offset, _)| offset.unwrap()) + .collect::>(); + Ok(TranslatedCodeSection { + exec_buf, + func_starts, + op_offset_map: self.op_offset_map, + // TODO + relocatable_accesses: vec![], + }) + } +} + +#[derive(Debug)] +struct RelocateAddress { + reg: Option, + imm: usize, +} + +#[derive(Debug)] +struct RelocateAccess { + position: AssemblyOffset, + dst_reg: GPR, + address: RelocateAddress, +} + +pub struct TranslatedCodeSection { + exec_buf: ExecutableBuffer, + func_starts: Vec, + #[allow(dead_code)] + relocatable_accesses: Vec, + op_offset_map: Vec<(AssemblyOffset, Box)>, +} + +impl TranslatedCodeSection { + pub fn func_start(&self, idx: usize) -> *const u8 { + let offset = self.func_starts[idx]; + self.exec_buf.ptr(offset) + } + + pub fn func_range(&self, idx: usize) -> std::ops::Range { + let end = self + .func_starts + .get(idx + 1) + .map(|i| i.0) + .unwrap_or_else(|| self.exec_buf.len()); + + self.func_starts[idx].0..end + } + + pub fn funcs<'a>(&'a self) -> impl Iterator> + 'a { + (0..self.func_starts.len()).map(move |i| self.func_range(i)) + } + + pub fn buffer(&self) -> &[u8] { + &*self.exec_buf + } + + pub fn disassemble(&self) { + crate::disassemble::disassemble(&*self.exec_buf, &self.op_offset_map).unwrap(); + } +} + +#[derive(Debug, Default, Clone)] +pub struct BlockState { + pub stack: Stack, + pub depth: StackDepth, + pub regs: Registers, +} + +type Stack = Vec; + +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +enum LabelValue { + I32(i32), + I64(i64), +} + +impl From for LabelValue { + fn from(other: Value) -> LabelValue { + match other { + Value::I32(v) => LabelValue::I32(v), + Value::I64(v) => LabelValue::I64(v), + Value::F32(v) => LabelValue::I32(v.to_bits() as _), + Value::F64(v) => LabelValue::I64(v.to_bits() as _), + } + } +} + +type Labels = HashMap< + (u32, Either)>), + (Label, u32, Option>), +>; + +pub struct Context<'this, M> { + pub asm: &'this mut Assembler, + reloc_sink: &'this mut dyn binemit::RelocSink, + module_context: &'this M, + current_function: u32, + func_starts: &'this Vec<(Option, DynamicLabel)>, + /// Each push and pop on the value stack increments or decrements this value by 1 respectively. + pub block_state: BlockState, + labels: &'this mut Labels, +} + +/// Label in code. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct Label(DynamicLabel); + +/// Offset from starting value of SP counted in words. +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +pub struct StackDepth(u32); + +impl StackDepth { + pub fn reserve(&mut self, slots: u32) { + self.0 = self.0.checked_add(slots).unwrap(); + } + + pub fn free(&mut self, slots: u32) { + self.0 = self.0.checked_sub(slots).unwrap(); + } +} + +macro_rules! int_div { + ($full_div_s:ident, $full_div_u:ident, $div_u:ident, $div_s:ident, $rem_u:ident, $rem_s:ident, $imm_fn:ident, $signed_ty:ty, $unsigned_ty:ty, $reg_ty:tt, $pointer_ty:tt) => { + // TODO: Fast div using mul for constant divisor? It looks like LLVM doesn't do that for us when + // emitting Wasm. + pub fn $div_u(&mut self) -> Result<(), Error>{ + let divisor = self.pop()?; + let dividend = self.pop()?; + + if let (Some(dividend), Some(divisor)) = (dividend.$imm_fn(), divisor.$imm_fn()) { + if divisor == 0 { + self.trap(); + self.push(ValueLocation::Immediate((0 as $unsigned_ty).into()))?; + } else { + self.push(ValueLocation::Immediate( + <$unsigned_ty>::wrapping_div(dividend as _, divisor as _).into(), + ))?; + } + + return Ok(()) + } + + let (div, rem, saved) = self.$full_div_u(divisor, dividend)?; + + self.free_value(rem)?; + + let div = match div { + ValueLocation::Reg(div) => { + if saved.clone().any(|dst| dst == div) { + let new = self.take_reg(I32).unwrap(); + dynasm!(self.asm + ; mov Rq(new.rq().unwrap()), Rq(div.rq().unwrap()) + ); + self.block_state.regs.release(div)?; + ValueLocation::Reg(new) + } else { + ValueLocation::Reg(div) + } + } + _ => div, + }; + + self.cleanup_gprs(saved); + + self.push(div)?; + Ok(()) + } + + // TODO: Fast div using mul for constant divisor? It looks like LLVM doesn't do that for us when + // emitting Wasm. + pub fn $div_s(&mut self) -> Result<(), Error>{ + let divisor = self.pop()?; + let dividend = self.pop()?; + + if let (Some(dividend), Some(divisor)) = (dividend.$imm_fn(), divisor.$imm_fn()) { + if divisor == 0 { + self.trap(); + self.push(ValueLocation::Immediate((0 as $signed_ty).into()))?; + } else { + self.push(ValueLocation::Immediate( + <$signed_ty>::wrapping_div(dividend, divisor).into(), + ))?; + } + + return Ok(()) + } + + let (div, rem, saved) = self.$full_div_s(divisor, dividend)?; + + self.free_value(rem)?; + + let div = match div { + ValueLocation::Reg(div) => { + if saved.clone().any(|dst| dst == div) { + let new = self.take_reg(I32).unwrap(); + dynasm!(self.asm + ; mov Rq(new.rq().unwrap()), Rq(div.rq().unwrap()) + ); + self.block_state.regs.release(div)?; + ValueLocation::Reg(new) + } else { + ValueLocation::Reg(div) + } + } + _ => div, + }; + + self.cleanup_gprs(saved); + + self.push(div)?; + Ok(()) + } + + pub fn $rem_u(&mut self) -> Result<(), Error>{ + let divisor = self.pop()?; + let dividend = self.pop()?; + + if let (Some(dividend), Some(divisor)) = (dividend.$imm_fn(), divisor.$imm_fn()) { + if divisor == 0 { + self.trap(); + self.push(ValueLocation::Immediate((0 as $unsigned_ty).into()))?; + } else { + self.push(ValueLocation::Immediate( + (dividend as $unsigned_ty % divisor as $unsigned_ty).into(), + ))?; + } + return Ok(()); + } + + let (div, rem, saved) = self.$full_div_u(divisor, dividend)?; + + self.free_value(div)?; + + let rem = match rem { + ValueLocation::Reg(rem) => { + if saved.clone().any(|dst| dst == rem) { + let new = self.take_reg(I32).unwrap(); + dynasm!(self.asm + ; mov Rq(new.rq().unwrap()), Rq(rem.rq().unwrap()) + ); + self.block_state.regs.release(rem)?; + ValueLocation::Reg(new) + } else { + ValueLocation::Reg(rem) + } + } + _ => rem, + }; + + self.cleanup_gprs(saved); + + self.push(rem)?; + Ok(()) + } + + pub fn $rem_s(&mut self) -> Result<(), Error>{ + let mut divisor = self.pop()?; + let dividend = self.pop()?; + + if let (Some(dividend), Some(divisor)) = (dividend.$imm_fn(), divisor.$imm_fn()) { + if divisor == 0 { + self.trap(); + self.push(ValueLocation::Immediate((0 as $signed_ty).into()))?; + } else { + self.push(ValueLocation::Immediate((dividend % divisor).into()))?; + } + return Ok(()); + } + + let is_neg1 = self.create_label(); + + let current_depth = self.block_state.depth.clone(); + + // TODO: This could cause segfaults because of implicit push/pop + let gen_neg1_case = match divisor { + ValueLocation::Immediate(_) => { + if divisor.$imm_fn().unwrap() == -1 { + self.push(ValueLocation::Immediate((-1 as $signed_ty).into()))?; + self.free_value(dividend)?; + return Ok(()); + } + + false + } + ValueLocation::Reg(_) => { + let reg = match self.put_into_register(GPRType::Rq, &mut divisor) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + dynasm!(self.asm + ; cmp $reg_ty(reg.rq().unwrap()), -1 + ); + // TODO: We could choose `current_depth` as the depth here instead but we currently + // don't for simplicity + self.set_stack_depth(current_depth.clone())?; + dynasm!(self.asm + ; je =>is_neg1.0 + ); + + true + } + ValueLocation::Stack(offset) => { + let offset = self.adjusted_offset(offset); + dynasm!(self.asm + ; cmp $pointer_ty [rsp + offset], -1 + ); + self.set_stack_depth(current_depth.clone())?; + dynasm!(self.asm + ; je =>is_neg1.0 + ); + + true + } + ValueLocation::Cond(_) => { + // `cc` can never be `-1`, only `0` and `1` + false + } + }; + + let (div, rem, saved) = self.$full_div_s(divisor, dividend)?; + + self.free_value(div)?; + + let rem = match rem { + ValueLocation::Reg(rem) => { + if saved.clone().any(|dst| dst == rem) { + let new = self.take_reg(I32).unwrap(); + dynasm!(self.asm + ; mov Rq(new.rq().unwrap()), Rq(rem.rq().unwrap()) + ); + self.block_state.regs.release(rem)?; + ValueLocation::Reg(new) + } else { + ValueLocation::Reg(rem) + } + } + _ => rem, + }; + + self.cleanup_gprs(saved); + + if gen_neg1_case { + let ret = self.create_label(); + self.set_stack_depth(current_depth.clone())?; + dynasm!(self.asm + ; jmp =>ret.0 + ); + self.define_label(is_neg1); + + let dst_ccloc = match CCLoc::try_from(rem) { + None => { + return Err(Error::Microwasm( + "$rem_s Programmer error".to_string(), + )) + } + Some(o) => o, + }; + + self.copy_value( + ValueLocation::Immediate((0 as $signed_ty).into()), + dst_ccloc + )?; + + self.set_stack_depth(current_depth.clone())?; + self.define_label(ret); + } + + self.push(rem)?; + Ok(()) + } + } +} + +macro_rules! unop { + ($name:ident, $instr:ident, $reg_ty:tt, $typ:ty, $const_fallback:expr) => { + pub fn $name(&mut self) -> Result<(), Error>{ + let mut val = self.pop()?; + + let out_val = match val { + ValueLocation::Immediate(imm) => + ValueLocation::Immediate( + ($const_fallback(imm.as_int().unwrap() as $typ) as $typ).into() + ), + ValueLocation::Stack(offset) => { + let offset = self.adjusted_offset(offset); + let temp = self.take_reg(Type::for_::<$typ>()).unwrap(); + dynasm!(self.asm + ; $instr $reg_ty(temp.rq().unwrap()), [rsp + offset] + ); + ValueLocation::Reg(temp) + } + ValueLocation::Reg(_) | ValueLocation::Cond(_) => { + let reg = match self.put_into_register(GPRType::Rq, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + let temp = self.take_reg(Type::for_::<$typ>()).unwrap(); + dynasm!(self.asm + ; $instr $reg_ty(temp.rq().unwrap()), $reg_ty(reg.rq().unwrap()) + ); + ValueLocation::Reg(temp) + } + }; + + self.free_value(val)?; + self.push(out_val)?; + Ok(()) + } + } +} + +macro_rules! conversion { + ( + $name:ident, + $instr:ident, + $in_reg_ty:tt, + $in_reg_fn:ident, + $out_reg_ty:tt, + $out_reg_fn:ident, + $in_typ:ty, + $out_typ:ty, + $const_ty_fn:ident, + $const_fallback:expr + ) => { + pub fn $name(&mut self) -> Result<(), Error>{ + let mut val = self.pop()?; + + let out_val = match val { + ValueLocation::Immediate(imm) => + ValueLocation::Immediate( + $const_fallback(imm.$const_ty_fn().unwrap()).into() + ), + ValueLocation::Stack(offset) => { + let offset = self.adjusted_offset(offset); + let temp = self.take_reg(Type::for_::<$out_typ>()).unwrap(); + dynasm!(self.asm + ; $instr $out_reg_ty(temp.$out_reg_fn().unwrap()), [rsp + offset] + ); + + ValueLocation::Reg(temp) + } + ValueLocation::Reg(_) | ValueLocation::Cond(_) => { + let reg = match self.put_into_register(Type::for_::<$in_typ>(), &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + let temp = self.take_reg(Type::for_::<$out_typ>()).unwrap(); + + dynasm!(self.asm + ; $instr $out_reg_ty(temp.$out_reg_fn().unwrap()), $in_reg_ty(reg.$in_reg_fn().unwrap()) + ); + + ValueLocation::Reg(temp) + } + }; + + self.free_value(val)?; + + self.push(out_val)?; + Ok(()) + } + } +} + +// TODO: Support immediate `count` parameters +macro_rules! shift { + ($name:ident, $reg_ty:tt, $instr:ident, $const_fallback:expr, $ty:expr) => { + pub fn $name(&mut self) -> Result<(), Error>{ + let mut count = self.pop()?; + let mut val = self.pop()?; + + if let Some(imm) = count.immediate() { + if let Some(imm) = imm.as_int() { + if let Ok(imm) = i8::try_from(imm) { + let reg = match self.put_into_temp_register($ty, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + dynasm!(self.asm + ; $instr $reg_ty(reg.rq().unwrap()), imm + ); + self.push(ValueLocation::Reg(reg))?; + return Ok(()); + } + } + } + + if val == ValueLocation::Reg(RCX) { + let new = self.take_reg($ty).unwrap(); + self.copy_value(val, CCLoc::Reg(new))?; + self.free_value(val)?; + val = ValueLocation::Reg(new); + } + + // TODO: Maybe allocate `RCX`, write `count` to it and then free `count`. + // Once we've implemented refcounting this will do the right thing + // for free. + let temp_rcx = match count { + ValueLocation::Reg(RCX) => {None} + other => { + let out = if self.block_state.regs.is_free(RCX) { + None + } else { + let new_reg = self.take_reg(I32).unwrap(); + dynasm!(self.asm + ; mov Rq(new_reg.rq().unwrap()), rcx + ); + Some(new_reg) + }; + + match other { + ValueLocation::Reg(_) | ValueLocation::Cond(_) => { + let gpr = match self.put_into_register(I32, &mut count) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + dynasm!(self.asm + ; mov cl, Rb(gpr.rq().unwrap()) + ); + } + ValueLocation::Stack(offset) => { + let offset = self.adjusted_offset(offset); + dynasm!(self.asm + ; mov cl, [rsp + offset] + ); + } + ValueLocation::Immediate(imm) => { + dynasm!(self.asm + ; mov cl, imm.as_int().unwrap() as i8 + ); + } + } + + out + } + }; + + self.free_value(count)?; + self.block_state.regs.mark_used(RCX); + count = ValueLocation::Reg(RCX); + + let reg = match self.put_into_temp_register($ty, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + dynasm!(self.asm + ; $instr $reg_ty(reg.rq().unwrap()), cl + ); + + self.free_value(count)?; + + if let Some(gpr) = temp_rcx { + dynasm!(self.asm + ; mov rcx, Rq(gpr.rq().unwrap()) + ); + self.block_state.regs.release(gpr)?; + } + + self.push(val)?; + Ok(()) + } + } +} + +macro_rules! cmp_i32 { + ($name:ident, $flags:expr, $reverse_flags:expr, $const_fallback:expr) => { + pub fn $name(&mut self) -> Result<(), Error>{ + let mut right = self.pop()?; + let mut left = self.pop()?; + + let out = if let Some(i) = left.imm_i32() { + match right { + ValueLocation::Stack(offset) => { + let offset = self.adjusted_offset(offset); + + dynasm!(self.asm + ; cmp DWORD [rsp + offset], i + ); + ValueLocation::Cond($reverse_flags) + } + ValueLocation::Reg(_) | ValueLocation::Cond(_) => { + let rreg = match self.put_into_register(I32, &mut right) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + dynasm!(self.asm + ; cmp Rd(rreg.rq().unwrap()), i + ); + ValueLocation::Cond($reverse_flags) + } + ValueLocation::Immediate(right) => { + ValueLocation::Immediate( + (if $const_fallback(i, right.as_i32().unwrap()) { + 1i32 + } else { + 0i32 + }).into() + ) + } + } + } else { + let lreg = match self.put_into_register(I32, &mut left) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + match right { + ValueLocation::Stack(offset) => { + let offset = self.adjusted_offset(offset); + dynasm!(self.asm + ; cmp Rd(lreg.rq().unwrap()), [rsp + offset] + ); + } + ValueLocation::Reg(_) | ValueLocation::Cond(_) => { + let rreg = match self.put_into_register(I32, &mut right) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + dynasm!(self.asm + ; cmp Rd(lreg.rq().unwrap()), Rd(rreg.rq().unwrap()) + ); + } + ValueLocation::Immediate(i) => { + dynasm!(self.asm + ; cmp Rd(lreg.rq().unwrap()), i.as_i32().unwrap() + ); + } + } + + ValueLocation::Cond($flags) + }; + + self.free_value(left)?; + self.free_value(right)?; + + self.push(out)?; + Ok(()) + } + } +} + +macro_rules! cmp_i64 { + ($name:ident, $flags:expr, $reverse_flags:expr, $const_fallback:expr) => { + pub fn $name(&mut self) -> Result<(), Error> { + let mut right = self.pop()?; + let mut left = self.pop()?; + + let out = if let Some(i) = left.imm_i64() { + match right { + ValueLocation::Stack(offset) => { + let offset = self.adjusted_offset(offset); + if let Some(i) = i.try_into().ok() { + dynasm!(self.asm + ; cmp QWORD [rsp + offset], i + ); + } else { + let lreg = match self.put_into_register(I32, &mut left) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + dynasm!(self.asm + ; cmp QWORD [rsp + offset], Rq(lreg.rq().unwrap()) + ); + } + ValueLocation::Cond($reverse_flags) + } + ValueLocation::Reg(_) | ValueLocation::Cond(_) => { + let rreg = match self.put_into_register(I32, &mut right) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + if let Some(i) = i.try_into().ok() { + dynasm!(self.asm + ; cmp Rq(rreg.rq().unwrap()), i + ); + } else { + let lreg = match self.put_into_register(I32, &mut left) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + dynasm!(self.asm + ; cmp Rq(rreg.rq().unwrap()), Rq(lreg.rq().unwrap()) + ); + } + ValueLocation::Cond($reverse_flags) + } + ValueLocation::Immediate(right) => { + ValueLocation::Immediate( + (if $const_fallback(i, right.as_i64().unwrap()) { + 1i32 + } else { + 0i32 + }).into() + ) + } + } + } else { + let lreg = match self.put_into_register(I64, &mut left) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + match right { + ValueLocation::Stack(offset) => { + let offset = self.adjusted_offset(offset); + dynasm!(self.asm + ; cmp Rq(lreg.rq().unwrap()), [rsp + offset] + ); + } + ValueLocation::Reg(_) | ValueLocation::Cond(_) => { + let rreg = match self.put_into_register(I32, &mut right) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + dynasm!(self.asm + ; cmp Rq(lreg.rq().unwrap()), Rq(rreg.rq().unwrap()) + ); + } + ValueLocation::Immediate(i) => { + let i = i.as_i64().unwrap(); + if let Some(i) = i.try_into().ok() { + dynasm!(self.asm + ; cmp Rq(lreg.rq().unwrap()), i + ); + } else { + let rreg = match self.put_into_register(I32, &mut right) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + dynasm!(self.asm + ; cmp Rq(lreg.rq().unwrap()), Rq(rreg.rq().unwrap()) + ); + } + } + } + + ValueLocation::Cond($flags) + }; + + self.free_value(left)?; + self.free_value(right)?; + self.push(out)?; + Ok(()) + } + } +} + +macro_rules! cmp_f32 { + ($name:ident, $reverse_name:ident, $instr:ident, $const_fallback:expr) => { + cmp_float!( + comiss, + f32, + imm_f32, + $name, + $reverse_name, + $instr, + $const_fallback + ); + }; +} + +macro_rules! eq_float { + ($name:ident, $instr:ident, $imm_fn:ident, $const_fallback:expr) => { + pub fn $name(&mut self) -> Result<(), Error>{ + let right = self.pop()?; + let left = self.pop()?; + + if let Some(right) = right.immediate() { + if let Some(left) = left.immediate() { + self.push(ValueLocation::Immediate( + if $const_fallback(left.$imm_fn().unwrap(), right.$imm_fn().unwrap()) { + 1u32 + } else { + 0 + }.into() + ))?; + return Ok(()); + } + } + + let (mut left, mut right) = match left { + ValueLocation::Reg(r) if self.block_state.regs.num_usages(r) <= 1 => (left, right), + _ => (right, left) + }; + + let lreg = match self.put_into_temp_register(GPRType::Rx, &mut left) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + let rreg = match self.put_into_register(GPRType::Rx, &mut right) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + let out = self.take_reg(I32).unwrap(); + + dynasm!(self.asm + ; $instr Rx(lreg.rx().unwrap()), Rx(rreg.rx().unwrap()) + ; movd Rd(out.rq().unwrap()), Rx(lreg.rx().unwrap()) + ; and Rd(out.rq().unwrap()), 1 + ); + + self.push(ValueLocation::Reg(out))?; + self.free_value(left)?; + self.free_value(right)?; + Ok(()) + } + + } +} + +macro_rules! minmax_float { + ( + $name:ident, + $instr:ident, + $cmpinstr:ident, + $addinstr:ident, + $combineinstr:ident, + $imm_fn:ident, + $const_fallback:expr + ) => { + pub fn $name(&mut self) -> Result<(), Error>{ + let right = self.pop()?; + let left = self.pop()?; + + if let Some(right) = right.immediate() { + if let Some(left) = left.immediate() { + self.push(ValueLocation::Immediate( + $const_fallback(left.$imm_fn().unwrap(), right.$imm_fn().unwrap()).into() + ))?; + return Ok(()); + } + } + + let (mut left, mut right) = match left { + ValueLocation::Reg(r) if self.block_state.regs.num_usages(r) <= 1 => (left, right), + _ => (right, left) + }; + + let lreg = match self.put_into_temp_register(GPRType::Rx, &mut left) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + let rreg = match self.put_into_register(GPRType::Rx, &mut right) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + dynasm!(self.asm + ; $cmpinstr Rx(lreg.rx().unwrap()), Rx(rreg.rx().unwrap()) + ; je >equal + ; $instr Rx(lreg.rx().unwrap()), Rx(rreg.rx().unwrap()) + ; jmp >ret + ; equal: + ; jnp >equal_but_not_parity + ; $addinstr Rx(lreg.rx().unwrap()), Rx(rreg.rx().unwrap()) + ; jmp >ret + ; equal_but_not_parity: + ; $combineinstr Rx(lreg.rx().unwrap()), Rx(rreg.rx().unwrap()) + ; ret: + ); + + self.push(left)?; + self.free_value(right)?; + Ok(()) + } + + } +} + +macro_rules! cmp_f64 { + ($name:ident, $reverse_name:ident, $instr:ident, $const_fallback:expr) => { + cmp_float!( + comisd, + f64, + imm_f64, + $name, + $reverse_name, + $instr, + $const_fallback + ); + }; +} + +macro_rules! cmp_float { + (@helper $cmp_instr:ident, $ty:ty, $imm_fn:ident, $self:expr, $left:expr, $right:expr, $instr:ident, $const_fallback:expr) => {{ + let (left, right, this) = ($left, $right, $self); + if let (Some(left), Some(right)) = (left.$imm_fn(), right.$imm_fn()) { + if $const_fallback(<$ty>::from_bits(left.to_bits()), <$ty>::from_bits(right.to_bits())) { + ValueLocation::Immediate(1i32.into()) + } else { + ValueLocation::Immediate(0i32.into()) + } + } else { + let lreg = match this.put_into_register(GPRType::Rx, left) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + let result = this.take_reg(I32).unwrap(); + + match right { + ValueLocation::Stack(offset) => { + let offset = this.adjusted_offset(*offset); + + dynasm!(this.asm + ; xor Rq(result.rq().unwrap()), Rq(result.rq().unwrap()) + ; $cmp_instr Rx(lreg.rx().unwrap()), [rsp + offset] + ; $instr Rb(result.rq().unwrap()) + ); + } + right => { + let rreg = match this.put_into_register(GPRType::Rx, right) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + dynasm!(this.asm + ; xor Rq(result.rq().unwrap()), Rq(result.rq().unwrap()) + ; $cmp_instr Rx(lreg.rx().unwrap()), Rx(rreg.rx().unwrap()) + ; $instr Rb(result.rq().unwrap()) + ); + } + } + + ValueLocation::Reg(result) + } + }}; + ($cmp_instr:ident, $ty:ty, $imm_fn:ident, $name:ident, $reverse_name:ident, $instr:ident, $const_fallback:expr) => { + pub fn $name(&mut self) -> Result<(), Error> { + let mut right = self.pop()?; + let mut left = self.pop()?; + + let out = cmp_float!(@helper + $cmp_instr, + $ty, + $imm_fn, + &mut *self, + &mut left, + &mut right, + $instr, + $const_fallback + ); + + self.free_value(left)?; + self.free_value(right)?; + + self.push(out)?; + Ok(()) + } + + pub fn $reverse_name(&mut self) -> Result<(), Error> { + let mut right = self.pop()?; + let mut left = self.pop()?; + + let out = cmp_float!(@helper + $cmp_instr, + $ty, + $imm_fn, + &mut *self, + &mut right, + &mut left, + $instr, + $const_fallback + ); + + self.free_value(left)?; + self.free_value(right)?; + + self.push(out)?; + Ok(()) + } + }; +} + +macro_rules! binop_i32 { + ($name:ident, $instr:ident, $const_fallback:expr) => { + binop!( + $name, + $instr, + $const_fallback, + Rd, + rq, + I32, + imm_i32, + |this: &mut Context<_>, op1: GPR, i| dynasm!(this.asm + ; $instr Rd(op1.rq().unwrap()), i + ) + ); + }; +} + +macro_rules! commutative_binop_i32 { + ($name:ident, $instr:ident, $const_fallback:expr) => { + commutative_binop!( + $name, + $instr, + $const_fallback, + Rd, + rq, + I32, + imm_i32, + |this: &mut Context<_>, op1: GPR, i| dynasm!(this.asm + ; $instr Rd(op1.rq().unwrap()), i + ) + ); + }; +} + +macro_rules! binop_i64 { + ($name:ident, $instr:ident, $const_fallback:expr) => { + binop!( + $name, + $instr, + $const_fallback, + Rq, + rq, + I64, + imm_i64, + |this: &mut Context<_>, op1: GPR, i| dynasm!(this.asm + ; $instr Rq(op1.rq().unwrap()), i + ) + ); + }; +} + +macro_rules! commutative_binop_i64 { + ($name:ident, $instr:ident, $const_fallback:expr) => { + commutative_binop!( + $name, + $instr, + $const_fallback, + Rq, + rq, + I64, + imm_i64, + |this: &mut Context<_>, op1: GPR, i| dynasm!(this.asm + ; $instr Rq(op1.rq().unwrap()), i + ) + ); + }; +} + +macro_rules! binop_f32 { + ($name:ident, $instr:ident, $const_fallback:expr) => { + binop!( + $name, + $instr, + |a: Ieee32, b: Ieee32| Ieee32::from_bits( + $const_fallback(f32::from_bits(a.to_bits()), f32::from_bits(b.to_bits())).to_bits() + ), + Rx, + rx, + F32, + imm_f32, + |_, _, _: i32| unreachable!() + ); + }; +} + +macro_rules! commutative_binop_f32 { + ($name:ident, $instr:ident, $const_fallback:expr) => { + commutative_binop!( + $name, + $instr, + |a: Ieee32, b: Ieee32| Ieee32::from_bits( + $const_fallback(f32::from_bits(a.to_bits()), f32::from_bits(b.to_bits())).to_bits() + ), + Rx, + rx, + F32, + imm_f32, + |_, _, _: i32| unreachable!() + ); + }; +} + +macro_rules! binop_f64 { + ($name:ident, $instr:ident, $const_fallback:expr) => { + binop!( + $name, + $instr, + |a: Ieee64, b: Ieee64| Ieee64::from_bits( + $const_fallback(f64::from_bits(a.to_bits()), f64::from_bits(b.to_bits())).to_bits() + ), + Rx, + rx, + F64, + imm_f64, + |_, _, _: i32| unreachable!() + ); + }; +} + +macro_rules! commutative_binop_f64 { + ($name:ident, $instr:ident, $const_fallback:expr) => { + commutative_binop!( + $name, + $instr, + |a: Ieee64, b: Ieee64| Ieee64::from_bits( + $const_fallback(f64::from_bits(a.to_bits()), f64::from_bits(b.to_bits())).to_bits() + ), + Rx, + rx, + F64, + imm_f64, + |_, _, _: i32| unreachable!() + ); + }; +} +macro_rules! commutative_binop { + ($name:ident, $instr:ident, $const_fallback:expr, $reg_ty:tt, $reg_fn:ident, $ty:expr, $imm_fn:ident, $direct_imm:expr) => { + binop!( + $name, + $instr, + $const_fallback, + $reg_ty, + $reg_fn, + $ty, + $imm_fn, + $direct_imm, + |op1: ValueLocation, op0: ValueLocation| match op1 { + ValueLocation::Reg(_) => (op1, op0), + _ => { + if op0.immediate().is_some() { + (op1, op0) + } else { + (op0, op1) + } + } + } + ); + }; +} + +macro_rules! binop { + ($name:ident, $instr:ident, $const_fallback:expr, $reg_ty:tt, $reg_fn:ident, $ty:expr, $imm_fn:ident, $direct_imm:expr) => { + binop!($name, $instr, $const_fallback, $reg_ty, $reg_fn, $ty, $imm_fn, $direct_imm, |a, b| (a, b)); + }; + ($name:ident, $instr:ident, $const_fallback:expr, $reg_ty:tt, $reg_fn:ident, $ty:expr, $imm_fn:ident, $direct_imm:expr, $map_op:expr) => { + pub fn $name(&mut self) -> Result<(), Error> { + let right = self.pop()?; + let left = self.pop()?; + + if let Some(i1) = left.$imm_fn() { + if let Some(i0) = right.$imm_fn() { + self.block_state.stack.push(ValueLocation::Immediate($const_fallback(i1, i0).into())); + return Ok(()); + } + } + + let (mut left, mut right) = $map_op(left, right); + let lreg = match self.put_into_temp_register($ty, &mut left) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + match right { + ValueLocation::Reg(_) | ValueLocation::Cond(_) => { + // This handles the case where we (for example) have a float in an `Rq` reg + let right_reg = match self.put_into_register($ty, &mut right) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + dynasm!(self.asm + ; $instr $reg_ty(lreg.$reg_fn().unwrap()), $reg_ty(right_reg.$reg_fn().unwrap()) + ); + } + ValueLocation::Stack(offset) => { + let offset = self.adjusted_offset(offset); + dynasm!(self.asm + ; $instr $reg_ty(lreg.$reg_fn().unwrap()), [rsp + offset] + ); + } + ValueLocation::Immediate(i) => { + if let Some(i) = i.as_int().and_then(|i| i.try_into().ok()) { + $direct_imm(&mut *self, lreg, i); + } else { + let scratch = self.take_reg($ty).unwrap(); + self.immediate_to_reg(scratch, i); + + dynasm!(self.asm + ; $instr $reg_ty(lreg.$reg_fn().unwrap()), $reg_ty(scratch.$reg_fn().unwrap()) + ); + + self.block_state.regs.release(scratch)?; + } + } + } + + self.free_value(right)?; + self.push(left)?; + Ok(()) + } + } +} + +macro_rules! load { + (@inner $name:ident, $rtype:expr, $reg_ty:tt, $emit_fn:expr) => { + pub fn $name(&mut self, offset: u32) -> Result<(), Error> { + fn load_to_reg<_M: ModuleContext>( + ctx: &mut Context<_M>, + dst: GPR, + (offset, runtime_offset): (i32, Result) + ) -> Result<(), Error> { + let mem_index = 0; + let reg_offset = ctx.module_context + .defined_memory_index(mem_index) + .map(|index| ( + None, + ctx.module_context.vmctx_vmmemory_definition(index) as i32 + )); + let (reg, mem_offset) = reg_offset.unwrap_or_else(|| { + let reg = ctx.take_reg(I64).unwrap(); + + dynasm!(ctx.asm + ; mov Rq(reg.rq().unwrap()), [ + Rq(VMCTX) + ctx.module_context.vmctx_vmmemory_import_from(mem_index) as i32 + ] + ); + + (Some(reg), 0) + }); + + let vmctx = GPR::Rq(VMCTX); + + if ctx.module_context.emit_memory_bounds_check() { + let trap_label = ctx.trap_label(); + let addr_reg = match runtime_offset { + Ok(imm) => { + let addr_reg = ctx.take_reg(I64).unwrap(); + dynasm!(ctx.asm + ; mov Rq(addr_reg.rq().unwrap()), QWORD imm as i64 + offset as i64 + ); + addr_reg + } + Err(gpr) => { + if offset == 0 { + match ctx.clone_to_register(I32, ValueLocation::Reg(gpr)) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + } + + } else if offset > 0 { + let addr_reg = ctx.take_reg(I64).unwrap(); + dynasm!(ctx.asm + ; lea Rq(addr_reg.rq().unwrap()), [Rq(gpr.rq().unwrap()) + offset] + ); + addr_reg + } else { + let addr_reg = ctx.take_reg(I64).unwrap(); + let offset_reg = ctx.take_reg(I64).unwrap(); + dynasm!(ctx.asm + ; mov Rd(offset_reg.rq().unwrap()), offset + ; mov Rq(addr_reg.rq().unwrap()), Rq(gpr.rq().unwrap()) + ; add Rq(addr_reg.rq().unwrap()), Rq(offset_reg.rq().unwrap()) + ); + ctx.block_state.regs.release(offset_reg)?; + addr_reg + } + } + }; + dynasm!(ctx.asm + ; cmp [ + Rq(reg.unwrap_or(vmctx).rq().unwrap()) + + mem_offset + + ctx.module_context.vmmemory_definition_current_length() as i32 + ], Rq(addr_reg.rq().unwrap()) + ; jna =>trap_label.0 + ); + ctx.block_state.regs.release(addr_reg)?; + } + + let mem_ptr_reg = ctx.take_reg(I64).unwrap(); + dynasm!(ctx.asm + ; mov Rq(mem_ptr_reg.rq().unwrap()), [ + Rq(reg.unwrap_or(vmctx).rq().unwrap()) + + mem_offset + + ctx.module_context.vmmemory_definition_base() as i32 + ] + ); + if let Some(reg) = reg { + ctx.block_state.regs.release(reg)?; + } + $emit_fn(ctx, dst, mem_ptr_reg, runtime_offset, offset)?; + ctx.block_state.regs.release(mem_ptr_reg)?; + Ok(()) + } + + let base = self.pop()?; + + let temp = self.take_reg($rtype).unwrap(); + + match base { + ValueLocation::Immediate(i) => { + load_to_reg(self, temp, (offset as _, Ok(i.as_i32().unwrap())))?; + } + mut base => { + let gpr = match self.put_into_register(I32, &mut base) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + load_to_reg(self, temp, (offset as _, Err(gpr)))?; + self.free_value(base)?; + } + } + + self.push(ValueLocation::Reg(temp))?; + Ok(()) + } + }; + ($name:ident, $rtype:expr, $reg_ty:tt, NONE, $rq_instr:ident, $ty:ident) => { + load!(@inner + $name, + $rtype, + $reg_ty, + |ctx: &mut Context<_>, dst: GPR, mem_ptr_reg: GPR, runtime_offset: Result, offset: i32| -> Result<(), Error> { + match runtime_offset { + Ok(imm) => { + dynasm!(ctx.asm + ; $rq_instr $reg_ty(dst.rq().unwrap()), $ty [Rq(mem_ptr_reg.rq().unwrap()) + offset + imm] + ); + Ok(()) + } + Err(offset_reg) => { + dynasm!(ctx.asm + ; $rq_instr $reg_ty(dst.rq().unwrap()), $ty [Rq(mem_ptr_reg.rq().unwrap()) + Rq(offset_reg.rq().unwrap()) + offset] + ); + Ok(()) + } + } + } + ); + }; + ($name:ident, $rtype:expr, $reg_ty:tt, $xmm_instr:ident, $rq_instr:ident, $ty:ident) => { + load!(@inner + $name, + $rtype, + $reg_ty, + |ctx: &mut Context<_>, dst: GPR, mem_ptr_reg: GPR, runtime_offset: Result, offset: i32| -> Result<(), Error> { + match (dst, runtime_offset) { + (GPR::Rq(r), Ok(imm)) => { + dynasm!(ctx.asm + ; $rq_instr $reg_ty(r), $ty [Rq(mem_ptr_reg.rq().unwrap()) + offset + imm] + ); + Ok(()) + } + (GPR::Rx(r), Ok(imm)) => { + if let Some(combined) = offset.checked_add(imm) { + dynasm!(ctx.asm + ; $xmm_instr Rx(r), $ty [Rq(mem_ptr_reg.rq().unwrap()) + combined] + ); + Ok(()) + } else { + let offset_reg = ctx.take_reg(GPRType::Rq).unwrap(); + dynasm!(ctx.asm + ; mov Rq(offset_reg.rq().unwrap()), offset + ; $xmm_instr Rx(r), $ty [ + Rq(mem_ptr_reg.rq().unwrap()) + + Rq(offset_reg.rq().unwrap()) + + imm + ] + ); + ctx.block_state.regs.release(offset_reg)?; + Ok(()) + } + } + (GPR::Rq(r), Err(offset_reg)) => { + dynasm!(ctx.asm + ; $rq_instr $reg_ty(r), $ty [Rq(mem_ptr_reg.rq().unwrap()) + Rq(offset_reg.rq().unwrap()) + offset] + ); + Ok(()) + } + (GPR::Rx(r), Err(offset_reg)) => { + dynasm!(ctx.asm + ; $xmm_instr Rx(r), $ty [Rq(mem_ptr_reg.rq().unwrap()) + Rq(offset_reg.rq().unwrap()) + offset] + ); + Ok(()) + } + } + } + ); + }; +} + +macro_rules! store { + (@inner $name:ident, $int_reg_ty:tt, $match_offset:expr, $size:ident) => { + pub fn $name(&mut self, offset: u32) -> Result<(), Error>{ + fn store_from_reg<_M: ModuleContext>( + ctx: &mut Context<_M>, + src: GPR, + (offset, runtime_offset): (i32, Result) + ) -> Result<(), Error> { + let mem_index = 0; + let reg_offset = ctx.module_context + .defined_memory_index(mem_index) + .map(|index| ( + None, + ctx.module_context.vmctx_vmmemory_definition(index) as i32 + )); + let (reg, mem_offset) = reg_offset.unwrap_or_else(|| { + let reg = ctx.take_reg(I64).unwrap(); + + dynasm!(ctx.asm + ; mov Rq(reg.rq().unwrap()), [ + Rq(VMCTX) + ctx.module_context.vmctx_vmmemory_import_from(mem_index) as i32 + ] + ); + + (Some(reg), 0) + }); + + let vmctx = GPR::Rq(VMCTX); + + if ctx.module_context.emit_memory_bounds_check() { + let trap_label = ctx.trap_label(); + let addr_reg = match runtime_offset { + Ok(imm) => { + let addr_reg = ctx.take_reg(I64).unwrap(); + dynasm!(ctx.asm + ; mov Rq(addr_reg.rq().unwrap()), QWORD imm as i64 + offset as i64 + ); + addr_reg + } + Err(gpr) => { + if offset == 0 { + match ctx.clone_to_register(I32, ValueLocation::Reg(gpr)) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + } + + } else if offset > 0 { + let addr_reg = ctx.take_reg(I64).unwrap(); + dynasm!(ctx.asm + ; lea Rq(addr_reg.rq().unwrap()), [Rq(gpr.rq().unwrap()) + offset] + ); + addr_reg + } else { + let addr_reg = ctx.take_reg(I64).unwrap(); + let offset_reg = ctx.take_reg(I64).unwrap(); + dynasm!(ctx.asm + ; mov Rd(offset_reg.rq().unwrap()), offset + ; mov Rq(addr_reg.rq().unwrap()), Rq(gpr.rq().unwrap()) + ; add Rq(addr_reg.rq().unwrap()), Rq(offset_reg.rq().unwrap()) + ); + ctx.block_state.regs.release(offset_reg)?; + addr_reg + } + } + }; + dynasm!(ctx.asm + ; cmp Rq(addr_reg.rq().unwrap()), [ + Rq(reg.unwrap_or(vmctx).rq().unwrap()) + + mem_offset + + ctx.module_context.vmmemory_definition_current_length() as i32 + ] + ; jae =>trap_label.0 + ); + ctx.block_state.regs.release(addr_reg)?; + } + + let mem_ptr_reg = ctx.take_reg(I64).unwrap(); + dynasm!(ctx.asm + ; mov Rq(mem_ptr_reg.rq().unwrap()), [ + Rq(reg.unwrap_or(vmctx).rq().unwrap()) + + mem_offset + + ctx.module_context.vmmemory_definition_base() as i32 + ] + ); + if let Some(reg) = reg { + ctx.block_state.regs.release(reg)?; + } + let src = $match_offset(ctx, mem_ptr_reg, runtime_offset, offset, src)?; + ctx.block_state.regs.release(mem_ptr_reg)?; + ctx.block_state.regs.release(src)?; + Ok(()) + } + + if !(offset <= i32::max_value() as u32) { + return Err(Error::Microwasm(format!("store: offset value too big {}", offset))) + } + + let mut src = self.pop()?; + let base = self.pop()?; + + // `store_from_reg` frees `src` + // TODO: Would it be better to free it outside `store_from_reg`? + let src_reg = match self.put_into_register(None, &mut src) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + + match base { + ValueLocation::Immediate(i) => { + store_from_reg(self, src_reg, (offset as i32, Ok(i.as_i32().unwrap())))? + } + mut base => { + let gpr = match self.put_into_register(I32, &mut base) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + store_from_reg(self, src_reg, (offset as i32, Err(gpr)))?; + self.free_value(base)?; + } + } + Ok(()) + } + }; + ($name:ident, $int_reg_ty:tt, NONE, $size:ident) => { + store!(@inner + $name, + $int_reg_ty, + |ctx: &mut Context<_>, mem_ptr_reg: GPR, runtime_offset: Result, offset: i32, src| -> Result { + let src_reg = match ctx.put_into_temp_register(GPRType::Rq, &mut ValueLocation::Reg(src)) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + match runtime_offset { + Ok(imm) => { + dynasm!(ctx.asm + ; mov [Rq(mem_ptr_reg.rq().unwrap()) + offset + imm], $int_reg_ty(src_reg.rq().unwrap()) + ); + } + Err(offset_reg) => { + dynasm!(ctx.asm + ; mov [Rq(mem_ptr_reg.rq().unwrap()) + Rq(offset_reg.rq().unwrap()) + offset], $int_reg_ty(src_reg.rq().unwrap()) + ); + } + } + + Ok(src_reg) + }, + $size + ); + }; + ($name:ident, $int_reg_ty:tt, $xmm_instr:ident, $size:ident) => { + store!(@inner + $name, + $int_reg_ty, + |ctx: &mut Context<_>, mem_ptr_reg: GPR, runtime_offset: Result, offset: i32, src| -> Result { + match (runtime_offset, src) { + (Ok(imm), GPR::Rq(r)) => { + dynasm!(ctx.asm + ; mov [Rq(mem_ptr_reg.rq().unwrap()) + offset + imm], $int_reg_ty(r) + ); + } + (Ok(imm), GPR::Rx(r)) => { + dynasm!(ctx.asm + ; $xmm_instr [Rq(mem_ptr_reg.rq().unwrap()) + offset + imm], Rx(r) + ); + } + (Err(offset_reg), GPR::Rq(r)) => { + dynasm!(ctx.asm + ; mov [Rq(mem_ptr_reg.rq().unwrap()) + Rq(offset_reg.rq().unwrap()) + offset], $int_reg_ty(r) + ); + } + (Err(offset_reg), GPR::Rx(r)) => { + dynasm!(ctx.asm + ; $xmm_instr [Rq(mem_ptr_reg.rq().unwrap()) + Rq(offset_reg.rq().unwrap()) + offset], Rx(r) + ); + } + } + + Ok(src) + }, + $size + ); + }; +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct VirtualCallingConvention { + pub stack: Stack, + pub depth: StackDepth, +} + +impl<'this, M: ModuleContext> Context<'this, M> { + fn free_reg(&mut self, type_: GPRType) -> Result { + let pos = if let Some(pos) = self + .block_state + .stack + .iter() + .position(|r| r.reg().map(|reg| reg.type_() == type_).unwrap_or(false)) + { + pos + } else { + return Ok(false); + }; + + let old_loc = self.block_state.stack[pos]; + let new_loc = self.push_physical(old_loc)?; + self.block_state.stack[pos] = new_loc; + + let reg = old_loc.reg().unwrap(); + + for elem in &mut self.block_state.stack[pos + 1..] { + if *elem == old_loc { + *elem = new_loc; + self.block_state.regs.release(reg)?; + } + } + + Ok(true) + } + + fn take_reg(&mut self, r: impl Into) -> Option { + let r = r.into(); + loop { + if let Some(gpr) = self.block_state.regs.take(r) { + break Some(gpr); + } + + if self.free_reg(r) == Ok(false) { + break None; + } + } + } + + pub fn virtual_calling_convention(&self) -> VirtualCallingConvention { + VirtualCallingConvention { + stack: self.block_state.stack.clone(), + depth: self.block_state.depth, + } + } + + /// Create a new undefined label. + pub fn create_label(&mut self) -> Label { + Label(self.asm.new_dynamic_label()) + } + + pub fn define_host_fn(&mut self, host_fn: *const u8) { + dynasm!(self.asm + ; mov rax, QWORD host_fn as i64 + ; call rax + ; ret + ); + } + + fn adjusted_offset(&self, offset: i32) -> i32 { + (self.block_state.depth.0 as i32 + offset) * WORD_SIZE as i32 + } + + cmp_i32!(i32_eq, cc::EQUAL, cc::EQUAL, |a, b| a == b); + cmp_i32!(i32_neq, cc::NOT_EQUAL, cc::NOT_EQUAL, |a, b| a != b); + // `dynasm-rs` inexplicably doesn't support setb but `setnae` (and `setc`) are synonymous + cmp_i32!(i32_lt_u, cc::LT_U, cc::GT_U, |a, b| (a as u32) < (b as u32)); + cmp_i32!(i32_le_u, cc::LE_U, cc::GE_U, |a, b| (a as u32) + <= (b as u32)); + cmp_i32!(i32_gt_u, cc::GT_U, cc::LT_U, |a, b| (a as u32) > (b as u32)); + cmp_i32!(i32_ge_u, cc::GE_U, cc::LE_U, |a, b| (a as u32) + >= (b as u32)); + cmp_i32!(i32_lt_s, cc::LT_S, cc::GT_S, |a, b| a < b); + cmp_i32!(i32_le_s, cc::LE_S, cc::GE_S, |a, b| a <= b); + cmp_i32!(i32_gt_s, cc::GT_S, cc::LT_S, |a, b| a > b); + cmp_i32!(i32_ge_s, cc::GE_S, cc::LE_S, |a, b| a >= b); + + cmp_i64!(i64_eq, cc::EQUAL, cc::EQUAL, |a, b| a == b); + cmp_i64!(i64_neq, cc::NOT_EQUAL, cc::NOT_EQUAL, |a, b| a != b); + // `dynasm-rs` inexplicably doesn't support setb but `setnae` (and `setc`) are synonymous + cmp_i64!(i64_lt_u, cc::LT_U, cc::GT_U, |a, b| (a as u64) < (b as u64)); + cmp_i64!(i64_le_u, cc::LE_U, cc::GE_U, |a, b| (a as u64) + <= (b as u64)); + cmp_i64!(i64_gt_u, cc::GT_U, cc::LT_U, |a, b| (a as u64) > (b as u64)); + cmp_i64!(i64_ge_u, cc::GE_U, cc::LE_U, |a, b| (a as u64) + >= (b as u64)); + cmp_i64!(i64_lt_s, cc::LT_S, cc::GT_S, |a, b| a < b); + cmp_i64!(i64_le_s, cc::LE_S, cc::GE_S, |a, b| a <= b); + cmp_i64!(i64_gt_s, cc::GT_S, cc::LT_S, |a, b| a > b); + cmp_i64!(i64_ge_s, cc::GE_S, cc::LE_S, |a, b| a >= b); + + cmp_f32!(f32_gt, f32_lt, seta, |a, b| a > b); + cmp_f32!(f32_ge, f32_le, setnc, |a, b| a >= b); + eq_float!( + f32_eq, + cmpeqss, + as_f32, + |a: Ieee32, b: Ieee32| f32::from_bits(a.to_bits()) == f32::from_bits(b.to_bits()) + ); + eq_float!( + f32_ne, + cmpneqss, + as_f32, + |a: Ieee32, b: Ieee32| f32::from_bits(a.to_bits()) != f32::from_bits(b.to_bits()) + ); + + cmp_f64!(f64_gt, f64_lt, seta, |a, b| a > b); + cmp_f64!(f64_ge, f64_le, setnc, |a, b| a >= b); + eq_float!( + f64_eq, + cmpeqsd, + as_f64, + |a: Ieee64, b: Ieee64| f64::from_bits(a.to_bits()) == f64::from_bits(b.to_bits()) + ); + eq_float!( + f64_ne, + cmpneqsd, + as_f64, + |a: Ieee64, b: Ieee64| f64::from_bits(a.to_bits()) != f64::from_bits(b.to_bits()) + ); + + // TODO: Should we do this logic in `eq` and just have this delegate to `eq`? + // That would mean that `eqz` and `eq` with a const 0 argument don't + // result in different code. It would also allow us to generate better + // code for `neq` and `gt_u` with const 0 operand + pub fn i32_eqz(&mut self) -> Result<(), Error> { + let mut val = self.pop()?; + + if let ValueLocation::Immediate(Value::I32(i)) = val { + self.push(ValueLocation::Immediate( + (if i == 0 { 1i32 } else { 0 }).into(), + ))?; + return Ok(()); + } + + if let ValueLocation::Cond(loc) = val { + self.push(ValueLocation::Cond(!loc))?; + return Ok(()); + } + + let reg = match self.put_into_register(I32, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + let out = self.take_reg(I32).unwrap(); + + dynasm!(self.asm + ; xor Rd(out.rq().unwrap()), Rd(out.rq().unwrap()) + ; test Rd(reg.rq().unwrap()), Rd(reg.rq().unwrap()) + ; setz Rb(out.rq().unwrap()) + ); + + self.free_value(val)?; + + self.push(ValueLocation::Reg(out))?; + Ok(()) + } + + pub fn i64_eqz(&mut self) -> Result<(), Error> { + let mut val = self.pop()?; + + if let ValueLocation::Immediate(Value::I64(i)) = val { + self.push(ValueLocation::Immediate( + (if i == 0 { 1i32 } else { 0 }).into(), + ))?; + return Ok(()); + } + + if let ValueLocation::Cond(loc) = val { + self.push(ValueLocation::Cond(!loc))?; + return Ok(()); + } + + let reg = match self.put_into_register(I64, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + let out = self.take_reg(I64).unwrap(); + + dynasm!(self.asm + ; xor Rd(out.rq().unwrap()), Rd(out.rq().unwrap()) + ; test Rq(reg.rq().unwrap()), Rq(reg.rq().unwrap()) + ; setz Rb(out.rq().unwrap()) + ); + + self.free_value(val)?; + + self.push(ValueLocation::Reg(out))?; + Ok(()) + } + + fn br_on_cond_code(&mut self, label: Label, cond: CondCode) { + match cond { + cc::EQUAL => dynasm!(self.asm + ; je =>label.0 + ), + cc::NOT_EQUAL => dynasm!(self.asm + ; jne =>label.0 + ), + cc::GT_U => dynasm!(self.asm + ; ja =>label.0 + ), + cc::GE_U => dynasm!(self.asm + ; jae =>label.0 + ), + cc::LT_U => dynasm!(self.asm + ; jb =>label.0 + ), + cc::LE_U => dynasm!(self.asm + ; jbe =>label.0 + ), + cc::GT_S => dynasm!(self.asm + ; jg =>label.0 + ), + cc::GE_S => dynasm!(self.asm + ; jge =>label.0 + ), + cc::LT_S => dynasm!(self.asm + ; jl =>label.0 + ), + cc::LE_S => dynasm!(self.asm + ; jle =>label.0 + ), + } + } + + /// Pops i32 predicate and branches to the specified label + /// if the predicate is equal to zero. + pub fn br_if_false( + &mut self, + target: impl Into>, + pass_args: impl FnOnce(&mut Self) -> Result<(), Error>, + ) -> Result<(), Error> { + let mut val = self.pop()?; + let label = target + .into() + .label() + .copied() + .unwrap_or_else(|| self.ret_label()); + + let cond = match val { + ValueLocation::Cond(cc) => !cc, + _ => { + let predicate = match self.put_into_register(I32, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + dynasm!(self.asm + ; test Rd(predicate.rq().unwrap()), Rd(predicate.rq().unwrap()) + ); + + CondCode::ZF0 + } + }; + + self.free_value(val)?; + + pass_args(self)?; + + self.br_on_cond_code(label, cond); + Ok(()) + } + + /// Pops i32 predicate and branches to the specified label + /// if the predicate is not equal to zero. + pub fn br_if_true( + &mut self, + target: impl Into>, + pass_args: impl FnOnce(&mut Self) -> Result<(), Error>, + ) -> Result<(), Error> { + let mut val = self.pop()?; + let label = target + .into() + .label() + .copied() + .unwrap_or_else(|| self.ret_label()); + + let cond = match val { + ValueLocation::Cond(cc) => cc, + _ => { + let predicate = match self.put_into_register(I32, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + dynasm!(self.asm + ; test Rd(predicate.rq().unwrap()), Rd(predicate.rq().unwrap()) + ); + + CondCode::ZF1 + } + }; + + self.free_value(val)?; + + pass_args(self)?; + + self.br_on_cond_code(label, cond); + Ok(()) + } + + /// Branch unconditionally to the specified label. + pub fn br(&mut self, label: impl Into>) { + match label.into() { + BrTarget::Return => self.ret(), + BrTarget::Label(label) => dynasm!(self.asm + ; jmp =>label.0 + ), + } + } + + /// If `default` is `None` then the default is just continuing execution + pub fn br_table( + &mut self, + targets: I, + default: Option>, + pass_args: impl FnOnce(&mut Self) -> Result<(), Error>, + ) -> Result<(), Error> + where + I: IntoIterator>>, + I::IntoIter: ExactSizeIterator, + { + let mut targets = targets.into_iter(); + let count = targets.len(); + + let mut selector = self.pop()?; + + pass_args(self)?; + + if let Some(imm) = selector.imm_i32() { + if let Some(target) = targets.nth(imm as _).or(Some(default)).and_then(|a| a) { + match target { + BrTarget::Label(label) => self.br(label), + BrTarget::Return => { + dynasm!(self.asm + ; ret + ); + } + } + } + } else { + let end_label = self.create_label(); + + if count > 0 { + let temp = match self.put_into_temp_register(GPRType::Rq, &mut selector) { + Err(e) => return Err(e), + Ok(o) => match o { + Some(r) => Ok((r, false)), + None => { + self.push_physical(ValueLocation::Reg(RAX))?; + self.block_state.regs.mark_used(RAX); + Ok((RAX, true)) + } + }, + }; + + let (selector_reg, pop_selector) = match temp { + Err(e) => return Err(e), + Ok(a) => a, + }; + + let (tmp, pop_tmp) = if let Some(reg) = self.take_reg(I64) { + (reg, false) + } else { + let out_reg = if selector_reg == RAX { RCX } else { RAX }; + + self.push_physical(ValueLocation::Reg(out_reg))?; + self.block_state.regs.mark_used(out_reg); + + (out_reg, true) + }; + + self.immediate_to_reg(tmp, (count as u32).into()); + dynasm!(self.asm + ; cmp Rq(selector_reg.rq().unwrap()), Rq(tmp.rq().unwrap()) + ; cmova Rq(selector_reg.rq().unwrap()), Rq(tmp.rq().unwrap()) + ; lea Rq(tmp.rq().unwrap()), [>start_label] + ; lea Rq(selector_reg.rq().unwrap()), [ + Rq(selector_reg.rq().unwrap()) * 5 + ] + ; add Rq(selector_reg.rq().unwrap()), Rq(tmp.rq().unwrap()) + ); + + if pop_tmp { + dynasm!(self.asm + ; pop Rq(tmp.rq().unwrap()) + ); + } else { + self.block_state.regs.release(tmp)?; + } + + if pop_selector { + dynasm!(self.asm + ; pop Rq(selector_reg.rq().unwrap()) + ); + } + + dynasm!(self.asm + ; jmp Rq(selector_reg.rq().unwrap()) + ; start_label: + ); + + for target in targets { + let label = target + .map(|target| self.target_to_label(target)) + .unwrap_or(end_label); + dynasm!(self.asm + ; jmp =>label.0 + ); + } + } + + if let Some(def) = default { + match def { + BrTarget::Label(label) => dynasm!(self.asm + ; jmp =>label.0 + ), + BrTarget::Return => dynasm!(self.asm + ; ret + ), + } + } + + self.define_label(end_label); + } + + self.free_value(selector)?; + Ok(()) + } + + fn set_stack_depth(&mut self, depth: StackDepth) -> Result<(), Error> { + if self.block_state.depth.0 != depth.0 { + let diff = depth.0 as i32 - self.block_state.depth.0 as i32; + let emit_lea = if diff.abs() != 1 { + true + } else { + match self.block_state.depth.0.cmp(&depth.0) { + Ordering::Less => { + for _ in 0..diff { + dynasm!(self.asm + ; push rax + ); + } + false + } + Ordering::Greater => { + if let Some(trash) = self.take_reg(I64) { + for _ in 0..self.block_state.depth.0 - depth.0 { + dynasm!(self.asm + ; pop Rq(trash.rq().unwrap()) + ); + } + self.block_state.regs.release(trash)?; + false + } else { + true + } + } + Ordering::Equal => false, + } + }; + if emit_lea { + dynasm!(self.asm + ; lea rsp, [rsp + (self.block_state.depth.0 as i32 - depth.0 as i32) * WORD_SIZE as i32] + ); + } + self.block_state.depth = depth; + } + Ok(()) + } + + fn do_pass_block_args(&mut self, cc: &BlockCallingConvention) -> Result<(), Error> { + let args = &cc.arguments; + for &dst in args.iter().rev().take(self.block_state.stack.len()) { + if let CCLoc::Reg(r) = dst { + if !self.block_state.regs.is_free(r) + && *self.block_state.stack.last().unwrap() != ValueLocation::Reg(r) + { + // TODO: This would be made simpler and more efficient with a proper SSE + // representation. + self.save_regs(&[r], ..)?; + } + + self.block_state.regs.mark_used(r); + } + self.pop_into(dst)?; + } + Ok(()) + } + + pub fn pass_block_args(&mut self, cc: &BlockCallingConvention) -> Result<(), Error> { + self.do_pass_block_args(cc)?; + self.set_stack_depth(cc.stack_depth)?; + Ok(()) + } + + pub fn serialize_block_args( + &mut self, + cc: &BlockCallingConvention, + params: u32, + ) -> Result { + self.do_pass_block_args(cc)?; + + let mut out_args = cc.arguments.clone(); + + out_args.reverse(); + + while out_args.len() < params as usize { + let mut val = self.pop()?; + + // TODO: We can use stack slots for values already on the stack but we + // don't refcount stack slots right now + let ccloc = self.put_into_temp_location(None, &mut val)?; + out_args.push(ccloc); + } + + out_args.reverse(); + + self.set_stack_depth(cc.stack_depth)?; + + Ok(BlockCallingConvention { + stack_depth: cc.stack_depth, + arguments: out_args, + }) + } + + /// Puts all stack values into "real" locations so that they can i.e. be set to different + /// values on different iterations of a loop + pub fn serialize_args(&mut self, count: u32) -> Result { + let mut out = Vec::with_capacity(count as _); + + // TODO: We can make this more efficient now that `pop` isn't so complicated + for _ in 0..count { + let mut val = self.pop()?; + // TODO: We can use stack slots for values already on the stack but we + // don't refcount stack slots right now + let loc = self.put_into_temp_location(None, &mut val)?; + + out.push(loc); + } + + out.reverse(); + + Ok(BlockCallingConvention { + stack_depth: self.block_state.depth, + arguments: out, + }) + } + + pub fn get_global(&mut self, global_idx: u32) -> Result<(), Error> { + let (reg, offset) = self + .module_context + .defined_global_index(global_idx) + .map(|defined_global_index| { + ( + None, + self.module_context + .vmctx_vmglobal_definition(defined_global_index), + ) + }) + .unwrap_or_else(|| { + let reg = self.take_reg(I64).unwrap(); + + dynasm!(self.asm + ; mov Rq(reg.rq().unwrap()), [ + Rq(VMCTX) + + self.module_context.vmctx_vmglobal_import_from(global_idx) as i32 + ] + ); + + (Some(reg), 0) + }); + + let out = self.take_reg(GPRType::Rq).unwrap(); + let vmctx = GPR::Rq(VMCTX); + + // TODO: Are globals necessarily aligned to 128 bits? We can load directly to an XMM reg if so + dynasm!(self.asm + ; mov Rq(out.rq().unwrap()), [Rq(reg.unwrap_or(vmctx).rq().unwrap()) + offset as i32] + ); + + if let Some(reg) = reg { + self.block_state.regs.release(reg)?; + } + + self.push(ValueLocation::Reg(out))?; + Ok(()) + } + + pub fn set_global(&mut self, global_idx: u32) -> Result<(), Error> { + let mut val = self.pop()?; + let (reg, offset) = self + .module_context + .defined_global_index(global_idx) + .map(|defined_global_index| { + ( + None, + self.module_context + .vmctx_vmglobal_definition(defined_global_index), + ) + }) + .unwrap_or_else(|| { + let reg = self.take_reg(I64).unwrap(); + + dynasm!(self.asm + ; mov Rq(reg.rq().unwrap()), [ + Rq(VMCTX) + + self.module_context.vmctx_vmglobal_import_from(global_idx) as i32 + ] + ); + + (Some(reg), 0) + }); + + let val_reg = match self.put_into_register(GPRType::Rq, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + let vmctx = GPR::Rq(VMCTX); + + // We always use `Rq` (even for floats) since the globals are not necessarily aligned to 128 bits + dynasm!(self.asm + ; mov [ + Rq(reg.unwrap_or(vmctx).rq().unwrap()) + offset as i32 + ], Rq(val_reg.rq().unwrap()) + ); + + if let Some(reg) = reg { + self.block_state.regs.release(reg)?; + } + + self.free_value(val)?; + Ok(()) + } + + fn immediate_to_reg(&mut self, reg: GPR, val: Value) { + match reg { + GPR::Rq(r) => { + let val = val.as_bytes(); + if (val as u64) <= u32::max_value() as u64 { + dynasm!(self.asm + ; mov Rd(r), val as i32 + ); + } else { + dynasm!(self.asm + ; mov Rq(r), QWORD val + ); + } + } + GPR::Rx(r) => { + let label = self.aligned_label(16, LabelValue::from(val)); + dynasm!(self.asm + ; movq Rx(r), [=>label.0] + ); + } + } + } + + // The `&` and `&mut` aren't necessary (`ValueLocation` is copy) but it ensures that we don't get + // the arguments the wrong way around. In the future we want to have a `ReadLocation` and `WriteLocation` + // so we statically can't write to a literal so this will become a non-issue. + fn copy_value(&mut self, src: ValueLocation, dst: CCLoc) -> Result<(), Error> { + match (src, dst) { + (ValueLocation::Cond(cond), CCLoc::Stack(o)) => { + let offset = self.adjusted_offset(o); + + dynasm!(self.asm + ; mov QWORD [rsp + offset], DWORD 0 + ); + + match cond { + cc::EQUAL => dynasm!(self.asm + ; sete [rsp + offset] + ), + cc::NOT_EQUAL => dynasm!(self.asm + ; setne [rsp + offset] + ), + cc::GT_U => dynasm!(self.asm + ; seta [rsp + offset] + ), + cc::GE_U => dynasm!(self.asm + ; setae [rsp + offset] + ), + cc::LT_U => dynasm!(self.asm + ; setb [rsp + offset] + ), + cc::LE_U => dynasm!(self.asm + ; setbe [rsp + offset] + ), + cc::GT_S => dynasm!(self.asm + ; setg [rsp + offset] + ), + cc::GE_S => dynasm!(self.asm + ; setge [rsp + offset] + ), + cc::LT_S => dynasm!(self.asm + ; setl [rsp + offset] + ), + cc::LE_S => dynasm!(self.asm + ; setle [rsp + offset] + ), + } + } + (ValueLocation::Cond(cond), CCLoc::Reg(reg)) => match reg { + GPR::Rq(r) => { + dynasm!(self.asm + ; mov Rq(r), 0 + ); + + match cond { + cc::EQUAL => dynasm!(self.asm + ; sete Rb(r) + ), + cc::NOT_EQUAL => dynasm!(self.asm + ; setne Rb(r) + ), + cc::GT_U => dynasm!(self.asm + ; seta Rb(r) + ), + cc::GE_U => dynasm!(self.asm + ; setae Rb(r) + ), + cc::LT_U => dynasm!(self.asm + ; setb Rb(r) + ), + cc::LE_U => dynasm!(self.asm + ; setbe Rb(r) + ), + cc::GT_S => dynasm!(self.asm + ; setg Rb(r) + ), + cc::GE_S => dynasm!(self.asm + ; setge Rb(r) + ), + cc::LT_S => dynasm!(self.asm + ; setl Rb(r) + ), + cc::LE_S => dynasm!(self.asm + ; setle Rb(r) + ), + } + } + GPR::Rx(_) => { + let temp = CCLoc::Reg(self.take_reg(I32).unwrap()); + self.copy_value(src, temp)?; + let temp = temp.into(); + self.copy_value(temp, dst)?; + self.free_value(temp)?; + } + }, + (ValueLocation::Stack(in_offset), CCLoc::Stack(out_offset)) => { + let in_offset = self.adjusted_offset(in_offset); + let out_offset = self.adjusted_offset(out_offset); + if in_offset != out_offset { + if let Some(gpr) = self.take_reg(I64) { + dynasm!(self.asm + ; mov Rq(gpr.rq().unwrap()), [rsp + in_offset] + ; mov [rsp + out_offset], Rq(gpr.rq().unwrap()) + ); + self.block_state.regs.release(gpr)?; + } else { + dynasm!(self.asm + ; push rax + ; mov rax, [rsp + in_offset + WORD_SIZE as i32] + ; mov [rsp + out_offset + WORD_SIZE as i32], rax + ; pop rax + ); + } + } + } + // TODO: XMM registers + (ValueLocation::Reg(in_reg), CCLoc::Stack(out_offset)) => { + let out_offset = self.adjusted_offset(out_offset); + match in_reg { + GPR::Rq(in_reg) => { + // We can always use `Rq` here for now because stack slots are in multiples of + // 8 bytes + dynasm!(self.asm + ; mov [rsp + out_offset], Rq(in_reg) + ); + } + GPR::Rx(in_reg) => { + // We can always use `movq` here for now because stack slots are in multiples of + // 8 bytes + dynasm!(self.asm + ; movq [rsp + out_offset], Rx(in_reg) + ); + } + } + } + (ValueLocation::Immediate(i), CCLoc::Stack(out_offset)) => { + // TODO: Floats + let i = i.as_bytes(); + let out_offset = self.adjusted_offset(out_offset); + if (i as u64) <= u32::max_value() as u64 { + dynasm!(self.asm + ; mov DWORD [rsp + out_offset], i as i32 + ); + } else if let Some(scratch) = self.take_reg(I64) { + dynasm!(self.asm + ; mov Rq(scratch.rq().unwrap()), QWORD i + ; mov [rsp + out_offset], Rq(scratch.rq().unwrap()) + ); + + self.block_state.regs.release(scratch)?; + } else { + dynasm!(self.asm + ; push rax + ; mov rax, QWORD i + ; mov [rsp + out_offset + WORD_SIZE as i32], rax + ; pop rax + ); + } + } + (ValueLocation::Stack(in_offset), CCLoc::Reg(out_reg)) => { + let in_offset = self.adjusted_offset(in_offset); + match out_reg { + GPR::Rq(out_reg) => { + // We can always use `Rq` here for now because stack slots are in multiples of + // 8 bytes + dynasm!(self.asm + ; mov Rq(out_reg), [rsp + in_offset] + ); + } + GPR::Rx(out_reg) => { + // We can always use `movq` here for now because stack slots are in multiples of + // 8 bytes + dynasm!(self.asm + ; movq Rx(out_reg), [rsp + in_offset] + ); + } + } + } + (ValueLocation::Reg(in_reg), CCLoc::Reg(out_reg)) => { + if in_reg != out_reg { + match (in_reg, out_reg) { + (GPR::Rq(in_reg), GPR::Rq(out_reg)) => { + dynasm!(self.asm + ; mov Rq(out_reg), Rq(in_reg) + ); + } + (GPR::Rx(in_reg), GPR::Rq(out_reg)) => { + dynasm!(self.asm + ; movq Rq(out_reg), Rx(in_reg) + ); + } + (GPR::Rq(in_reg), GPR::Rx(out_reg)) => { + dynasm!(self.asm + ; movq Rx(out_reg), Rq(in_reg) + ); + } + (GPR::Rx(in_reg), GPR::Rx(out_reg)) => { + dynasm!(self.asm + ; movapd Rx(out_reg), Rx(in_reg) + ); + } + } + } + } + (ValueLocation::Immediate(i), CCLoc::Reg(out_reg)) => { + // TODO: Floats + self.immediate_to_reg(out_reg, i); + } + } + Ok(()) + } + + /// Define the given label at the current position. + /// + /// Multiple labels can be defined at the same position. However, a label + /// can be defined only once. + pub fn define_label(&mut self, label: Label) { + self.asm.dynamic_label(label.0); + } + + pub fn set_state(&mut self, state: VirtualCallingConvention) -> Result<(), Error> { + self.block_state.regs = Registers::new(); + self.block_state.regs.release_scratch_register()?; + for elem in &state.stack { + if let ValueLocation::Reg(r) = elem { + self.block_state.regs.mark_used(*r); + } + } + self.block_state.stack = state.stack; + self.block_state.depth = state.depth; + Ok(()) + } + + pub fn apply_cc(&mut self, cc: &BlockCallingConvention) -> Result<(), Error> { + let stack = cc.arguments.iter(); + + self.block_state.stack = Vec::with_capacity(stack.size_hint().0); + self.block_state.regs = Registers::new(); + self.block_state.regs.release_scratch_register()?; + + for &elem in stack { + if let CCLoc::Reg(r) = elem { + self.block_state.regs.mark_used(r); + } + + self.block_state.stack.push(elem.into()); + } + + self.block_state.depth = cc.stack_depth; + Ok(()) + } + + load!(i32_load, GPRType::Rq, Rd, movd, mov, DWORD); + load!(i64_load, GPRType::Rq, Rq, movq, mov, QWORD); + load!(f32_load, GPRType::Rx, Rd, movd, mov, DWORD); + load!(f64_load, GPRType::Rx, Rq, movq, mov, QWORD); + + load!(i32_load8_u, GPRType::Rq, Rd, NONE, movzx, BYTE); + load!(i32_load8_s, GPRType::Rq, Rd, NONE, movsx, BYTE); + load!(i32_load16_u, GPRType::Rq, Rd, NONE, movzx, WORD); + load!(i32_load16_s, GPRType::Rq, Rd, NONE, movsx, WORD); + + load!(i64_load8_u, GPRType::Rq, Rq, NONE, movzx, BYTE); + load!(i64_load8_s, GPRType::Rq, Rq, NONE, movsx, BYTE); + load!(i64_load16_u, GPRType::Rq, Rq, NONE, movzx, WORD); + load!(i64_load16_s, GPRType::Rq, Rq, NONE, movsx, WORD); + load!(i64_load32_u, GPRType::Rq, Rd, movd, mov, DWORD); + load!(i64_load32_s, GPRType::Rq, Rq, NONE, movsxd, DWORD); + + store!(store8, Rb, NONE, DWORD); + store!(store16, Rw, NONE, QWORD); + store!(store32, Rd, movd, DWORD); + store!(store64, Rq, movq, QWORD); + + fn push_physical(&mut self, mut value: ValueLocation) -> Result { + let out_offset = -(self.block_state.depth.0 as i32 + 1); + match value { + ValueLocation::Reg(_) | ValueLocation::Immediate(_) | ValueLocation::Cond(_) => { + if let Some(gpr) = self.put_into_register(GPRType::Rq, &mut value)? { + dynasm!(self.asm + ; push Rq(gpr.rq().unwrap()) + ); + } else { + dynasm!(self.asm + ; push rax + ); + + self.copy_value(value, CCLoc::Stack(out_offset))?; + } + + self.free_value(value)?; + } + ValueLocation::Stack(o) => { + let offset = self.adjusted_offset(o); + dynasm!(self.asm + ; push QWORD [rsp + offset] + ); + } + } + + self.block_state.depth.reserve(1); + + Ok(ValueLocation::Stack(out_offset)) + } + + fn push(&mut self, value: ValueLocation) -> Result<(), Error> { + if let Some(mut top) = self.block_state.stack.pop() { + if let ValueLocation::Cond(_) = top { + match self.put_into_register(I32, &mut top) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + } + + self.block_state.stack.push(top); + } + + self.block_state.stack.push(value); + Ok(()) + } + + fn pop(&mut self) -> Result { + match self.block_state.stack.pop() { + Some(v) => Ok(v), + None => Err(Error::Microwasm( + "Stack is empty - pop impossible".to_string(), + )), + } + } + + pub fn drop(&mut self, range: RangeInclusive) -> Result<(), Error> { + let mut repush = Vec::with_capacity(*range.start() as _); + + for _ in 0..*range.start() { + let v = self.pop()?; + repush.push(v); + } + + for _ in range { + let val = self.pop()?; + self.free_value(val)?; + } + + for v in repush.into_iter().rev() { + self.push(v)?; + } + Ok(()) + } + + fn pop_into(&mut self, dst: CCLoc) -> Result<(), Error> { + let val = self.pop()?; + self.copy_value(val, dst)?; + self.free_value(val)?; + Ok(()) + } + + fn free_value(&mut self, val: ValueLocation) -> Result<(), Error> { + if let ValueLocation::Reg(r) = val { + self.block_state.regs.release(r)?; + } + Ok(()) + } + + /// Puts this value into a register so that it can be efficiently read + fn put_into_register( + &mut self, + ty: impl Into>, + val: &mut ValueLocation, + ) -> Result, Error> { + if let Some(out) = self.clone_to_register(ty, *val)? { + self.free_value(*val)?; + *val = ValueLocation::Reg(out); + Ok(Some(out)) + } else { + Ok(None) + } + } + + /// Clones this value into a register so that it can be efficiently read + fn clone_to_register( + &mut self, + ty: impl Into>, + val: ValueLocation, + ) -> Result, Error> { + let ty = ty.into(); + match val { + ValueLocation::Reg(r) if ty.map(|t| t == r.type_()).unwrap_or(true) => { + self.block_state.regs.mark_used(r); + Ok(Some(r)) + } + val => match self.take_reg(ty.unwrap_or(GPRType::Rq)) { + Some(scratch) => { + self.copy_value(val, CCLoc::Reg(scratch))?; + Ok(Some(scratch)) + } + None => Ok(None), + }, + } + } + + /// Puts this value into a temporary register so that operations + /// on that register don't write to a local. + fn put_into_temp_register( + &mut self, + ty: impl Into>, + val: &mut ValueLocation, + ) -> Result, Error> { + let out = self.clone_to_temp_register(ty, *val)?; + if let Some(o) = out { + self.free_value(*val)?; + *val = ValueLocation::Reg(o); + Ok(Some(o)) + } else { + Ok(None) + } + } + + fn put_into_temp_location( + &mut self, + ty: impl Into>, + val: &mut ValueLocation, + ) -> Result { + match val { + _ => { + if let Some(gpr) = self.put_into_temp_register(ty, val)? { + Ok(CCLoc::Reg(gpr)) + } else { + let out = CCLoc::Stack(self.push_physical(*val)?.stack().unwrap()); + *val = out.into(); + Ok(out) + } + } + } + } + + /// Clones this value into a temporary register so that operations + /// on that register don't write to a local. + + fn clone_to_temp_register( + &mut self, + ty: impl Into>, + val: ValueLocation, + ) -> Result, Error> { + // If we have `None` as the type then it always matches (`.unwrap_or(true)`) + match val { + ValueLocation::Reg(r) => { + let ty = ty.into(); + let type_matches = ty.map(|t| t == r.type_()).unwrap_or(true); + + if self.block_state.regs.num_usages(r) <= 1 && type_matches { + self.block_state.regs.mark_used(r); + Ok(Some(r)) + } else if let Some(scratch) = self.take_reg(ty.unwrap_or(GPRType::Rq)) { + self.copy_value(val, CCLoc::Reg(scratch))?; + Ok(Some(scratch)) + } else { + Ok(None) + } + } + val => self.clone_to_register(ty, val), + } + } + + pub fn f32_neg(&mut self) -> Result<(), Error> { + let mut val = self.pop()?; + + let out = if let Some(i) = val.imm_f32() { + ValueLocation::Immediate( + Ieee32::from_bits((-f32::from_bits(i.to_bits())).to_bits()).into(), + ) + } else { + let reg = match self.put_into_temp_register(GPRType::Rx, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + let const_label = self.aligned_label(16, LabelValue::I32(SIGN_MASK_F32 as i32)); + + dynasm!(self.asm + ; xorps Rx(reg.rx().unwrap()), [=>const_label.0] + ); + + val + }; + + self.push(out)?; + Ok(()) + } + + pub fn f64_neg(&mut self) -> Result<(), Error> { + let mut val = self.pop()?; + + let out = if let Some(i) = val.imm_f64() { + ValueLocation::Immediate( + Ieee64::from_bits((-f64::from_bits(i.to_bits())).to_bits()).into(), + ) + } else { + let reg = match self.put_into_temp_register(GPRType::Rx, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + let const_label = self.aligned_label(16, LabelValue::I64(SIGN_MASK_F64 as i64)); + + dynasm!(self.asm + ; xorpd Rx(reg.rx().unwrap()), [=>const_label.0] + ); + + val + }; + + self.push(out)?; + Ok(()) + } + + pub fn f32_abs(&mut self) -> Result<(), Error> { + let mut val = self.pop()?; + + let out = if let Some(i) = val.imm_f32() { + ValueLocation::Immediate( + Ieee32::from_bits(f32::from_bits(i.to_bits()).abs().to_bits()).into(), + ) + } else { + let reg = match self.put_into_temp_register(GPRType::Rx, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + let const_label = self.aligned_label(16, LabelValue::I32(REST_MASK_F32 as i32)); + + dynasm!(self.asm + ; andps Rx(reg.rx().unwrap()), [=>const_label.0] + ); + + val + }; + + self.push(out)?; + Ok(()) + } + + pub fn f64_abs(&mut self) -> Result<(), Error> { + let mut val = self.pop()?; + + let out = if let Some(i) = val.imm_f64() { + ValueLocation::Immediate( + Ieee64::from_bits(f64::from_bits(i.to_bits()).abs().to_bits()).into(), + ) + } else { + let reg = match self.put_into_temp_register(GPRType::Rx, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + let const_label = self.aligned_label(16, LabelValue::I64(REST_MASK_F64 as i64)); + + dynasm!(self.asm + ; andps Rx(reg.rx().unwrap()), [=>const_label.0] + ); + + val + }; + + self.push(out)?; + Ok(()) + } + + pub fn f32_sqrt(&mut self) -> Result<(), Error> { + let mut val = self.pop()?; + + let out = if let Some(i) = val.imm_f32() { + ValueLocation::Immediate( + Ieee32::from_bits(f32::from_bits(i.to_bits()).sqrt().to_bits()).into(), + ) + } else { + let reg = match self.put_into_temp_register(GPRType::Rx, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + dynasm!(self.asm + ; sqrtss Rx(reg.rx().unwrap()), Rx(reg.rx().unwrap()) + ); + + val + }; + + self.push(out)?; + Ok(()) + } + + pub fn f64_sqrt(&mut self) -> Result<(), Error> { + let mut val = self.pop()?; + + let out = if let Some(i) = val.imm_f64() { + ValueLocation::Immediate( + Ieee64::from_bits(f64::from_bits(i.to_bits()).sqrt().to_bits()).into(), + ) + } else { + let reg = match self.put_into_temp_register(GPRType::Rx, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + dynasm!(self.asm + ; sqrtsd Rx(reg.rx().unwrap()), Rx(reg.rx().unwrap()) + ); + + ValueLocation::Reg(reg) + }; + + self.push(out)?; + Ok(()) + } + + pub fn f32_copysign(&mut self) -> Result<(), Error> { + let mut right = self.pop()?; + let mut left = self.pop()?; + + let out = if let (Some(left), Some(right)) = (left.imm_f32(), right.imm_f32()) { + ValueLocation::Immediate( + Ieee32::from_bits( + (left.to_bits() & REST_MASK_F32) | (right.to_bits() & SIGN_MASK_F32), + ) + .into(), + ) + } else { + let lreg = match self.put_into_temp_register(GPRType::Rx, &mut left) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + let rreg = match self.put_into_register(GPRType::Rx, &mut right) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + let sign_mask = self.aligned_label(16, LabelValue::I32(SIGN_MASK_F32 as i32)); + let rest_mask = self.aligned_label(16, LabelValue::I32(REST_MASK_F32 as i32)); + + dynasm!(self.asm + ; andps Rx(rreg.rx().unwrap()), [=>sign_mask.0] + ; andps Rx(lreg.rx().unwrap()), [=>rest_mask.0] + ; orps Rx(lreg.rx().unwrap()), Rx(rreg.rx().unwrap()) + ); + + self.free_value(right)?; + + left + }; + + self.push(out)?; + Ok(()) + } + + pub fn f64_copysign(&mut self) -> Result<(), Error> { + let mut right = self.pop()?; + let mut left = self.pop()?; + + let out = if let (Some(left), Some(right)) = (left.imm_f64(), right.imm_f64()) { + ValueLocation::Immediate( + Ieee64::from_bits( + (left.to_bits() & REST_MASK_F64) | (right.to_bits() & SIGN_MASK_F64), + ) + .into(), + ) + } else { + let lreg = match self.put_into_temp_register(GPRType::Rx, &mut left) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + let rreg = match self.put_into_register(GPRType::Rx, &mut right) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + let sign_mask = self.aligned_label(16, LabelValue::I64(SIGN_MASK_F64 as i64)); + let rest_mask = self.aligned_label(16, LabelValue::I64(REST_MASK_F64 as i64)); + + dynasm!(self.asm + ; andpd Rx(rreg.rx().unwrap()), [=>sign_mask.0] + ; andpd Rx(lreg.rx().unwrap()), [=>rest_mask.0] + ; orpd Rx(lreg.rx().unwrap()), Rx(rreg.rx().unwrap()) + ); + + self.free_value(right)?; + + left + }; + + self.push(out)?; + Ok(()) + } + + pub fn i32_clz(&mut self) -> Result<(), Error> { + let mut val = self.pop()?; + + let out_val = match val { + ValueLocation::Immediate(imm) => { + ValueLocation::Immediate(imm.as_i32().unwrap().leading_zeros().into()) + } + ValueLocation::Stack(offset) => { + let offset = self.adjusted_offset(offset); + let temp = self.take_reg(I32).unwrap(); + + if is_x86_feature_detected!("lzcnt") { + dynasm!(self.asm + ; lzcnt Rd(temp.rq().unwrap()), [rsp + offset] + ); + ValueLocation::Reg(temp) + } else { + let temp_2 = self.take_reg(I32).unwrap(); + + dynasm!(self.asm + ; bsr Rd(temp.rq().unwrap()), [rsp + offset] + ; mov Rd(temp_2.rq().unwrap()), DWORD 0x3fu64 as _ + ; cmove Rd(temp.rq().unwrap()), Rd(temp_2.rq().unwrap()) + ; mov Rd(temp_2.rq().unwrap()), DWORD 0x1fu64 as _ + ; xor Rd(temp.rq().unwrap()), Rd(temp_2.rq().unwrap()) + ); + self.free_value(ValueLocation::Reg(temp_2))?; + ValueLocation::Reg(temp) + } + } + ValueLocation::Reg(_) | ValueLocation::Cond(_) => { + let reg = match self.put_into_register(GPRType::Rq, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + let temp = self.take_reg(I32).unwrap(); + + if is_x86_feature_detected!("lzcnt") { + dynasm!(self.asm + ; lzcnt Rd(temp.rq().unwrap()), Rd(reg.rq().unwrap()) + ); + ValueLocation::Reg(temp) + } else { + dynasm!(self.asm + ; bsr Rd(temp.rq().unwrap()), Rd(reg.rq().unwrap()) + ; mov Rd(reg.rq().unwrap()), DWORD 0x3fu64 as _ + ; cmove Rd(temp.rq().unwrap()), Rd(reg.rq().unwrap()) + ; mov Rd(reg.rq().unwrap()), DWORD 0x1fu64 as _ + ; xor Rd(temp.rq().unwrap()), Rd(reg.rq().unwrap()) + ); + ValueLocation::Reg(temp) + } + } + }; + + self.free_value(val)?; + self.push(out_val)?; + Ok(()) + } + + pub fn i64_clz(&mut self) -> Result<(), Error> { + let mut val = self.pop()?; + + let out_val = match val { + ValueLocation::Immediate(imm) => { + ValueLocation::Immediate((imm.as_i64().unwrap().leading_zeros() as u64).into()) + } + ValueLocation::Stack(offset) => { + let offset = self.adjusted_offset(offset); + let temp = self.take_reg(I64).unwrap(); + + if is_x86_feature_detected!("lzcnt") { + dynasm!(self.asm + ; lzcnt Rq(temp.rq().unwrap()), [rsp + offset] + ); + ValueLocation::Reg(temp) + } else { + let temp_2 = self.take_reg(I64).unwrap(); + + dynasm!(self.asm + ; bsr Rq(temp.rq().unwrap()), [rsp + offset] + ; mov Rq(temp_2.rq().unwrap()), QWORD 0x7fu64 as _ + ; cmove Rq(temp.rq().unwrap()), Rq(temp_2.rq().unwrap()) + ; mov Rq(temp_2.rq().unwrap()), QWORD 0x3fu64 as _ + ; xor Rq(temp.rq().unwrap()), Rq(temp_2.rq().unwrap()) + ); + self.free_value(ValueLocation::Reg(temp_2))?; + ValueLocation::Reg(temp) + } + } + ValueLocation::Reg(_) | ValueLocation::Cond(_) => { + let reg = match self.put_into_register(GPRType::Rq, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + let temp = self.take_reg(I64).unwrap(); + + if is_x86_feature_detected!("lzcnt") { + dynasm!(self.asm + ; lzcnt Rq(temp.rq().unwrap()), Rq(reg.rq().unwrap()) + ); + ValueLocation::Reg(temp) + } else { + dynasm!(self.asm + ; bsr Rq(temp.rq().unwrap()), Rq(reg.rq().unwrap()) + ; mov Rq(reg.rq().unwrap()), QWORD 0x7fu64 as _ + ; cmove Rq(temp.rq().unwrap()), Rq(reg.rq().unwrap()) + ; mov Rq(reg.rq().unwrap()), QWORD 0x3fu64 as _ + ; xor Rq(temp.rq().unwrap()), Rq(reg.rq().unwrap()) + ); + ValueLocation::Reg(temp) + } + } + }; + + self.free_value(val)?; + self.push(out_val)?; + Ok(()) + } + + pub fn i32_ctz(&mut self) -> Result<(), Error> { + let mut val = self.pop()?; + + let out_val = match val { + ValueLocation::Immediate(imm) => { + ValueLocation::Immediate(imm.as_i32().unwrap().trailing_zeros().into()) + } + ValueLocation::Stack(offset) => { + let offset = self.adjusted_offset(offset); + let temp = self.take_reg(I32).unwrap(); + + if is_x86_feature_detected!("lzcnt") { + dynasm!(self.asm + ; tzcnt Rd(temp.rq().unwrap()), [rsp + offset] + ); + ValueLocation::Reg(temp) + } else { + let temp_zero_val = self.take_reg(I32).unwrap(); + + dynasm!(self.asm + ; bsf Rd(temp.rq().unwrap()), [rsp + offset] + ; mov Rd(temp_zero_val.rq().unwrap()), DWORD 0x20u32 as _ + ; cmove Rd(temp.rq().unwrap()), Rd(temp_zero_val.rq().unwrap()) + ); + self.free_value(ValueLocation::Reg(temp_zero_val))?; + ValueLocation::Reg(temp) + } + } + ValueLocation::Reg(_) | ValueLocation::Cond(_) => { + let reg = match self.put_into_register(GPRType::Rq, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + let temp = self.take_reg(I32).unwrap(); + + if is_x86_feature_detected!("lzcnt") { + dynasm!(self.asm + ; tzcnt Rd(temp.rq().unwrap()), Rd(reg.rq().unwrap()) + ); + ValueLocation::Reg(temp) + } else { + dynasm!(self.asm + ; bsf Rd(temp.rq().unwrap()), Rd(reg.rq().unwrap()) + ; mov Rd(reg.rq().unwrap()), DWORD 0x20u32 as _ + ; cmove Rd(temp.rq().unwrap()), Rd(reg.rq().unwrap()) + ); + ValueLocation::Reg(temp) + } + } + }; + + self.free_value(val)?; + self.push(out_val)?; + Ok(()) + } + + pub fn i64_ctz(&mut self) -> Result<(), Error> { + let mut val = self.pop()?; + + let out_val = match val { + ValueLocation::Immediate(imm) => { + ValueLocation::Immediate((imm.as_i64().unwrap().trailing_zeros() as u64).into()) + } + ValueLocation::Stack(offset) => { + let offset = self.adjusted_offset(offset); + let temp = self.take_reg(I64).unwrap(); + + if is_x86_feature_detected!("lzcnt") { + dynasm!(self.asm + ; tzcnt Rq(temp.rq().unwrap()), [rsp + offset] + ); + ValueLocation::Reg(temp) + } else { + let temp_zero_val = self.take_reg(I64).unwrap(); + + dynasm!(self.asm + ; bsf Rq(temp.rq().unwrap()), [rsp + offset] + ; mov Rq(temp_zero_val.rq().unwrap()), QWORD 0x40u64 as _ + ; cmove Rq(temp.rq().unwrap()), Rq(temp_zero_val.rq().unwrap()) + ); + self.free_value(ValueLocation::Reg(temp_zero_val))?; + ValueLocation::Reg(temp) + } + } + ValueLocation::Reg(_) | ValueLocation::Cond(_) => { + let reg = match self.put_into_register(GPRType::Rq, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + let temp = self.take_reg(I64).unwrap(); + + dynasm!(self.asm + ; bsf Rq(temp.rq().unwrap()), Rq(reg.rq().unwrap()) + ; mov Rq(reg.rq().unwrap()), QWORD 0x40u64 as _ + ; cmove Rq(temp.rq().unwrap()), Rq(reg.rq().unwrap()) + ); + ValueLocation::Reg(temp) + } + }; + + self.free_value(val)?; + self.push(out_val)?; + Ok(()) + } + + pub fn i32_extend_u(&mut self) -> Result<(), Error> { + let val = self.pop()?; + + let out = if let ValueLocation::Immediate(imm) = val { + ValueLocation::Immediate((imm.as_i32().unwrap() as u32 as u64).into()) + } else { + let new_reg = self.take_reg(I64).unwrap(); + + // TODO: Track set-ness of bits - we can make this a no-op in most cases + // but we have to make this unconditional just in case this value + // came from a truncate. + match val { + ValueLocation::Reg(GPR::Rx(rxreg)) => { + dynasm!(self.asm + ; movd Rd(new_reg.rq().unwrap()), Rx(rxreg) + ); + } + ValueLocation::Reg(GPR::Rq(rqreg)) => { + dynasm!(self.asm + ; mov Rd(new_reg.rq().unwrap()), Rd(rqreg) + ); + } + ValueLocation::Stack(offset) => { + let offset = self.adjusted_offset(offset); + + dynasm!(self.asm + ; mov Rd(new_reg.rq().unwrap()), [rsp + offset] + ); + } + ValueLocation::Cond(_) => self.copy_value(val, CCLoc::Reg(new_reg))?, + ValueLocation::Immediate(_) => { + return Err(Error::Microwasm( + "i32_extend_u unreachable code".to_string(), + )) + } + } + + ValueLocation::Reg(new_reg) + }; + + self.free_value(val)?; + + self.push(out)?; + Ok(()) + } + + pub fn i32_extend_s(&mut self) -> Result<(), Error> { + let val = self.pop()?; + + self.free_value(val)?; + let new_reg = self.take_reg(I64).unwrap(); + + let out = if let ValueLocation::Immediate(imm) = val { + self.block_state.regs.release(new_reg)?; + ValueLocation::Immediate((imm.as_i32().unwrap() as i64).into()) + } else { + match val { + ValueLocation::Reg(GPR::Rx(rxreg)) => { + dynasm!(self.asm + ; movd Rd(new_reg.rq().unwrap()), Rx(rxreg) + ; movsxd Rq(new_reg.rq().unwrap()), Rd(new_reg.rq().unwrap()) + ); + } + ValueLocation::Reg(GPR::Rq(rqreg)) => { + dynasm!(self.asm + ; movsxd Rq(new_reg.rq().unwrap()), Rd(rqreg) + ); + } + ValueLocation::Stack(offset) => { + let offset = self.adjusted_offset(offset); + + dynasm!(self.asm + ; movsxd Rq(new_reg.rq().unwrap()), DWORD [rsp + offset] + ); + } + _ => { + return Err(Error::Microwasm( + "i32_extend_s unreachable code".to_string(), + )) + } + } + + ValueLocation::Reg(new_reg) + }; + + self.push(out)?; + Ok(()) + } + + unop!(i32_popcnt, popcnt, Rd, u32, u32::count_ones); + conversion!( + f64_from_f32, + cvtss2sd, + Rx, + rx, + Rx, + rx, + f32, + f64, + as_f32, + |a: Ieee32| Ieee64::from_bits((f32::from_bits(a.to_bits()) as f64).to_bits()) + ); + conversion!( + f32_from_f64, + cvtsd2ss, + Rx, + rx, + Rx, + rx, + f64, + f32, + as_f64, + |a: Ieee64| Ieee32::from_bits((f64::from_bits(a.to_bits()) as f32).to_bits()) + ); + pub fn i32_truncate_f32_s(&mut self) -> Result<(), Error> { + let mut val = self.pop()?; + + let out_val = match val { + ValueLocation::Immediate(imm) => ValueLocation::Immediate( + (f32::from_bits(imm.as_f32().unwrap().to_bits()) as i32).into(), + ), + _ => { + let reg = match self.put_into_register(F32, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + let temp = self.take_reg(I32).unwrap(); + + let sign_mask = self.aligned_label(4, LabelValue::I32(SIGN_MASK_F32 as i32)); + let float_cmp_mask = + self.aligned_label(16, LabelValue::I32(0xCF00_0000_u32 as i32)); + let zero = self.aligned_label(16, LabelValue::I32(0)); + let trap_label = self.trap_label(); + + dynasm!(self.asm + ; cvttss2si Rd(temp.rq().unwrap()), Rx(reg.rx().unwrap()) + ; cmp Rd(temp.rq().unwrap()), [=>sign_mask.0] + ; jne >ret + ; ucomiss Rx(reg.rx().unwrap()), Rx(reg.rx().unwrap()) + ; jp =>trap_label.0 + ; ucomiss Rx(reg.rx().unwrap()), [=>float_cmp_mask.0] + ; jnae =>trap_label.0 + ; ucomiss Rx(reg.rx().unwrap()), [=>zero.0] + ; jnb =>trap_label.0 + ; ret: + ); + + ValueLocation::Reg(temp) + } + }; + + self.free_value(val)?; + + self.push(out_val)?; + Ok(()) + } + + pub fn i32_truncate_f32_u(&mut self) -> Result<(), Error> { + let mut val = self.pop()?; + + let out_val = match val { + ValueLocation::Immediate(imm) => ValueLocation::Immediate( + (f32::from_bits(imm.as_f32().unwrap().to_bits()) as i32).into(), + ), + _ => { + let reg = match self.put_into_temp_register(F32, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + let temp = self.take_reg(I32).unwrap(); + + let sign_mask = self.aligned_label(4, LabelValue::I32(SIGN_MASK_F32 as i32)); + let float_cmp_mask = + self.aligned_label(16, LabelValue::I32(0x4F00_0000_u32 as i32)); + let trap_label = self.trap_label(); + + dynasm!(self.asm + ; ucomiss Rx(reg.rx().unwrap()), [=>float_cmp_mask.0] + ; jae >else_ + ; jp =>trap_label.0 + ; cvttss2si Rd(temp.rq().unwrap()), Rx(reg.rx().unwrap()) + ; test Rd(temp.rq().unwrap()), Rd(temp.rq().unwrap()) + ; js =>trap_label.0 + ; jmp >ret + ; else_: + ; subss Rx(reg.rx().unwrap()), [=>float_cmp_mask.0] + ; cvttss2si Rd(temp.rq().unwrap()), Rx(reg.rx().unwrap()) + ; test Rd(temp.rq().unwrap()), Rd(temp.rq().unwrap()) + ; js =>trap_label.0 + ; add Rq(temp.rq().unwrap()), [=>sign_mask.0] + ; ret: + ); + + ValueLocation::Reg(temp) + } + }; + + self.free_value(val)?; + + self.push(out_val)?; + Ok(()) + } + + pub fn i32_truncate_f64_s(&mut self) -> Result<(), Error> { + let mut val = self.pop()?; + + let out_val = match val { + ValueLocation::Immediate(imm) => ValueLocation::Immediate( + (f64::from_bits(imm.as_f64().unwrap().to_bits()) as i32).into(), + ), + _ => { + let reg = match self.put_into_register(F32, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + let temp = self.take_reg(I32).unwrap(); + + let sign_mask = self.aligned_label(4, LabelValue::I32(SIGN_MASK_F32 as i32)); + let float_cmp_mask = + self.aligned_label(16, LabelValue::I64(0xC1E0_0000_0020_0000_u64 as i64)); + let zero = self.aligned_label(16, LabelValue::I64(0)); + let trap_label = self.trap_label(); + + dynasm!(self.asm + ; cvttsd2si Rd(temp.rq().unwrap()), Rx(reg.rx().unwrap()) + ; cmp Rd(temp.rq().unwrap()), [=>sign_mask.0] + ; jne >ret + ; ucomisd Rx(reg.rx().unwrap()), Rx(reg.rx().unwrap()) + ; jp =>trap_label.0 + ; ucomisd Rx(reg.rx().unwrap()), [=>float_cmp_mask.0] + ; jna =>trap_label.0 + ; ucomisd Rx(reg.rx().unwrap()), [=>zero.0] + ; jnb =>trap_label.0 + ; ret: + ); + + ValueLocation::Reg(temp) + } + }; + + self.free_value(val)?; + + self.push(out_val)?; + Ok(()) + } + + pub fn i32_truncate_f64_u(&mut self) -> Result<(), Error> { + let mut val = self.pop()?; + + let out_val = match val { + ValueLocation::Immediate(imm) => ValueLocation::Immediate( + (f64::from_bits(imm.as_f64().unwrap().to_bits()) as u32).into(), + ), + _ => { + let reg = match self.put_into_temp_register(F32, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + let temp = self.take_reg(I32).unwrap(); + + let sign_mask = self.aligned_label(4, LabelValue::I32(SIGN_MASK_F32 as i32)); + let float_cmp_mask = + self.aligned_label(16, LabelValue::I64(0x41E0_0000_0000_0000_u64 as i64)); + let trap_label = self.trap_label(); + + dynasm!(self.asm + ; ucomisd Rx(reg.rx().unwrap()), [=>float_cmp_mask.0] + ; jae >else_ + ; jp =>trap_label.0 + ; cvttsd2si Rd(temp.rq().unwrap()), Rx(reg.rx().unwrap()) + ; test Rd(temp.rq().unwrap()), Rd(temp.rq().unwrap()) + ; js =>trap_label.0 + ; jmp >ret + ; else_: + ; subsd Rx(reg.rx().unwrap()), [=>float_cmp_mask.0] + ; cvttsd2si Rd(temp.rq().unwrap()), Rx(reg.rx().unwrap()) + ; test Rd(temp.rq().unwrap()), Rd(temp.rq().unwrap()) + ; js =>trap_label.0 + ; add Rq(temp.rq().unwrap()), [=>sign_mask.0] + ; ret: + ); + + ValueLocation::Reg(temp) + } + }; + + self.free_value(val)?; + + self.push(out_val)?; + Ok(()) + } + + conversion!( + f32_convert_from_i32_s, + cvtsi2ss, + Rd, + rq, + Rx, + rx, + i32, + f32, + as_i32, + |a| Ieee32::from_bits((a as f32).to_bits()) + ); + conversion!( + f64_convert_from_i32_s, + cvtsi2sd, + Rd, + rq, + Rx, + rx, + i32, + f64, + as_i32, + |a| Ieee64::from_bits((a as f64).to_bits()) + ); + conversion!( + f32_convert_from_i64_s, + cvtsi2ss, + Rq, + rq, + Rx, + rx, + i64, + f32, + as_i64, + |a| Ieee32::from_bits((a as f32).to_bits()) + ); + conversion!( + f64_convert_from_i64_s, + cvtsi2sd, + Rq, + rq, + Rx, + rx, + i64, + f64, + as_i64, + |a| Ieee64::from_bits((a as f64).to_bits()) + ); + + pub fn i64_truncate_f32_s(&mut self) -> Result<(), Error> { + let mut val = self.pop()?; + + let out_val = match val { + ValueLocation::Immediate(imm) => ValueLocation::Immediate( + (f32::from_bits(imm.as_f32().unwrap().to_bits()) as i64).into(), + ), + _ => { + let reg = match self.put_into_temp_register(F32, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + let temp = self.take_reg(I32).unwrap(); + + let sign_mask = self.aligned_label(16, LabelValue::I64(SIGN_MASK_F64 as i64)); + let float_cmp_mask = + self.aligned_label(16, LabelValue::I32(0xDF00_0000_u32 as i32)); + let zero = self.aligned_label(16, LabelValue::I64(0)); + let trap_label = self.trap_label(); + + dynasm!(self.asm + ; cvttss2si Rq(temp.rq().unwrap()), Rx(reg.rx().unwrap()) + ; cmp Rq(temp.rq().unwrap()), [=>sign_mask.0] + ; jne >ret + ; ucomiss Rx(reg.rx().unwrap()), Rx(reg.rx().unwrap()) + ; jp =>trap_label.0 + ; ucomiss Rx(reg.rx().unwrap()), [=>float_cmp_mask.0] + ; jnae =>trap_label.0 + ; ucomiss Rx(reg.rx().unwrap()), [=>zero.0] + ; jnb =>trap_label.0 + ; ret: + ); + + ValueLocation::Reg(temp) + } + }; + + self.free_value(val)?; + + self.push(out_val)?; + Ok(()) + } + + pub fn i64_truncate_f64_s(&mut self) -> Result<(), Error> { + let mut val = self.pop()?; + + let out_val = match val { + ValueLocation::Immediate(imm) => ValueLocation::Immediate( + (f64::from_bits(imm.as_f64().unwrap().to_bits()) as i64).into(), + ), + _ => { + let reg = match self.put_into_register(F32, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + let temp = self.take_reg(I32).unwrap(); + + let sign_mask = self.aligned_label(8, LabelValue::I64(SIGN_MASK_F64 as i64)); + let float_cmp_mask = + self.aligned_label(16, LabelValue::I64(0xC3E0_0000_0000_0000_u64 as i64)); + let zero = self.aligned_label(16, LabelValue::I64(0)); + let trap_label = self.trap_label(); + + dynasm!(self.asm + ; cvttsd2si Rq(temp.rq().unwrap()), Rx(reg.rx().unwrap()) + ; cmp Rq(temp.rq().unwrap()), [=>sign_mask.0] + ; jne >ret + ; ucomisd Rx(reg.rx().unwrap()), Rx(reg.rx().unwrap()) + ; jp =>trap_label.0 + ; ucomisd Rx(reg.rx().unwrap()), [=>float_cmp_mask.0] + ; jnae =>trap_label.0 + ; ucomisd Rx(reg.rx().unwrap()), [=>zero.0] + ; jnb =>trap_label.0 + ; ret: + ); + + ValueLocation::Reg(temp) + } + }; + + self.free_value(val)?; + + self.push(out_val)?; + Ok(()) + } + + pub fn i64_truncate_f32_u(&mut self) -> Result<(), Error> { + let mut val = self.pop()?; + + let out_val = match val { + ValueLocation::Immediate(imm) => ValueLocation::Immediate( + (f32::from_bits(imm.as_f32().unwrap().to_bits()) as u64).into(), + ), + _ => { + let reg = match self.put_into_register(F32, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + let temp = self.take_reg(I64).unwrap(); + let sign_mask = self.aligned_label(16, LabelValue::I64(SIGN_MASK_F64 as i64)); + let u64_trunc_f32_const = self.aligned_label(16, LabelValue::I32(0x5F00_0000_i32)); + let trap_label = self.trap_label(); + + dynasm!(self.asm + ; comiss Rx(reg.rx().unwrap()), [=>u64_trunc_f32_const.0] + ; jae >large + ; jp =>trap_label.0 + ; cvttss2si Rq(temp.rq().unwrap()), Rx(reg.rx().unwrap()) + ; test Rq(temp.rq().unwrap()), Rq(temp.rq().unwrap()) + ; js =>trap_label.0 + ; jmp >cont + ; large: + ; subss Rx(reg.rx().unwrap()), [=>u64_trunc_f32_const.0] + ; cvttss2si Rq(temp.rq().unwrap()), Rx(reg.rx().unwrap()) + ; test Rq(temp.rq().unwrap()), Rq(temp.rq().unwrap()) + ; js =>trap_label.0 + ; add Rq(temp.rq().unwrap()), [=>sign_mask.0] + ; cont: + ); + + ValueLocation::Reg(temp) + } + }; + + self.free_value(val)?; + + self.push(out_val)?; + Ok(()) + } + + pub fn i64_truncate_f64_u(&mut self) -> Result<(), Error> { + let mut val = self.pop()?; + + let out_val = match val { + ValueLocation::Immediate(imm) => ValueLocation::Immediate( + (f64::from_bits(imm.as_f64().unwrap().to_bits()) as u64).into(), + ), + _ => { + let reg = match self.put_into_register(F64, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + let temp = self.take_reg(I64).unwrap(); + + let sign_mask = self.aligned_label(16, LabelValue::I64(SIGN_MASK_F64 as i64)); + let u64_trunc_f64_const = + self.aligned_label(16, LabelValue::I64(0x43E0_0000_0000_0000_i64)); + let trap_label = self.trap_label(); + + dynasm!(self.asm + ; comisd Rx(reg.rx().unwrap()), [=>u64_trunc_f64_const.0] + ; jnb >large + ; jp =>trap_label.0 + ; cvttsd2si Rq(temp.rq().unwrap()), Rx(reg.rx().unwrap()) + ; cmp Rq(temp.rq().unwrap()), 0 + ; jge >cont + ; jmp =>trap_label.0 + ; large: + ; subsd Rx(reg.rx().unwrap()), [=>u64_trunc_f64_const.0] + ; cvttsd2si Rq(temp.rq().unwrap()), Rx(reg.rx().unwrap()) + ; cmp Rq(temp.rq().unwrap()), 0 + ; jnge =>trap_label.0 + ; add Rq(temp.rq().unwrap()), [=>sign_mask.0] + ; cont: + ); + + ValueLocation::Reg(temp) + } + }; + + self.free_value(val)?; + + self.push(out_val)?; + Ok(()) + } + + pub fn f32_convert_from_i32_u(&mut self) -> Result<(), Error> { + let mut val = self.pop()?; + + let out_val = match val { + ValueLocation::Immediate(imm) => ValueLocation::Immediate( + Ieee32::from_bits((imm.as_i32().unwrap() as u32 as f32).to_bits()).into(), + ), + _ => { + let reg = match self.put_into_register(I32, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + let temp = self.take_reg(F32).unwrap(); + + dynasm!(self.asm + ; mov Rd(reg.rq().unwrap()), Rd(reg.rq().unwrap()) + ; cvtsi2ss Rx(temp.rx().unwrap()), Rq(reg.rq().unwrap()) + ); + + ValueLocation::Reg(temp) + } + }; + + self.free_value(val)?; + + self.push(out_val)?; + Ok(()) + } + + pub fn f64_convert_from_i32_u(&mut self) -> Result<(), Error> { + let mut val = self.pop()?; + + let out_val = match val { + ValueLocation::Immediate(imm) => ValueLocation::Immediate( + Ieee64::from_bits((imm.as_i32().unwrap() as u32 as f64).to_bits()).into(), + ), + _ => { + let reg = match self.put_into_register(I32, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + let temp = self.take_reg(F64).unwrap(); + + dynasm!(self.asm + ; mov Rd(reg.rq().unwrap()), Rd(reg.rq().unwrap()) + ; cvtsi2sd Rx(temp.rx().unwrap()), Rq(reg.rq().unwrap()) + ); + + ValueLocation::Reg(temp) + } + }; + + self.free_value(val)?; + + self.push(out_val)?; + Ok(()) + } + + pub fn f32_convert_from_i64_u(&mut self) -> Result<(), Error> { + let mut val = self.pop()?; + + let out_val = match val { + ValueLocation::Immediate(imm) => ValueLocation::Immediate( + Ieee32::from_bits((imm.as_i64().unwrap() as u64 as f32).to_bits()).into(), + ), + _ => { + let reg = match self.put_into_register(I64, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + let out = self.take_reg(F32).unwrap(); + let temp = self.take_reg(I64).unwrap(); + + dynasm!(self.asm + ; test Rq(reg.rq().unwrap()), Rq(reg.rq().unwrap()) + ; js >negative + ; cvtsi2ss Rx(out.rx().unwrap()), Rq(reg.rq().unwrap()) + ; jmp >ret + ; negative: + ; mov Rq(temp.rq().unwrap()), Rq(reg.rq().unwrap()) + ; shr Rq(temp.rq().unwrap()), 1 + ; and Rq(reg.rq().unwrap()), 1 + ; or Rq(reg.rq().unwrap()), Rq(temp.rq().unwrap()) + ; cvtsi2ss Rx(out.rx().unwrap()), Rq(reg.rq().unwrap()) + ; addss Rx(out.rx().unwrap()), Rx(out.rx().unwrap()) + ; ret: + ); + + self.free_value(ValueLocation::Reg(temp))?; + + ValueLocation::Reg(out) + } + }; + + self.free_value(val)?; + + self.push(out_val)?; + Ok(()) + } + + pub fn f64_convert_from_i64_u(&mut self) -> Result<(), Error> { + let mut val = self.pop()?; + + let out_val = match val { + ValueLocation::Immediate(imm) => ValueLocation::Immediate( + Ieee64::from_bits((imm.as_i64().unwrap() as u64 as f64).to_bits()).into(), + ), + _ => { + let reg = match self.put_into_register(I64, &mut val) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + let out = self.take_reg(F32).unwrap(); + let temp = self.take_reg(I64).unwrap(); + + dynasm!(self.asm + ; test Rq(reg.rq().unwrap()), Rq(reg.rq().unwrap()) + ; js >negative + ; cvtsi2sd Rx(out.rx().unwrap()), Rq(reg.rq().unwrap()) + ; jmp >ret + ; negative: + ; mov Rq(temp.rq().unwrap()), Rq(reg.rq().unwrap()) + ; shr Rq(temp.rq().unwrap()), 1 + ; and Rq(reg.rq().unwrap()), 1 + ; or Rq(reg.rq().unwrap()), Rq(temp.rq().unwrap()) + ; cvtsi2sd Rx(out.rx().unwrap()), Rq(reg.rq().unwrap()) + ; addsd Rx(out.rx().unwrap()), Rx(out.rx().unwrap()) + ; ret: + ); + + self.free_value(ValueLocation::Reg(temp))?; + + ValueLocation::Reg(out) + } + }; + + self.free_value(val)?; + + self.push(out_val)?; + Ok(()) + } + + pub fn i32_wrap_from_i64(&mut self) -> Result<(), Error> { + let val = self.pop()?; + + let out = match val { + ValueLocation::Immediate(imm) => { + ValueLocation::Immediate((imm.as_i64().unwrap() as u64 as u32).into()) + } + val => val, + }; + + self.push(out)?; + Ok(()) + } + + pub fn i32_reinterpret_from_f32(&mut self) -> Result<(), Error> { + let val = self.pop()?; + + let out = match val { + ValueLocation::Immediate(imm) => { + ValueLocation::Immediate(imm.as_f32().unwrap().to_bits().into()) + } + val => val, + }; + + self.push(out)?; + Ok(()) + } + + pub fn i64_reinterpret_from_f64(&mut self) -> Result<(), Error> { + let val = self.pop()?; + + let out = match val { + ValueLocation::Immediate(imm) => { + ValueLocation::Immediate(imm.as_f64().unwrap().to_bits().into()) + } + val => val, + }; + + self.push(out)?; + Ok(()) + } + + pub fn f32_reinterpret_from_i32(&mut self) -> Result<(), Error> { + let val = self.pop()?; + + let out = match val { + ValueLocation::Immediate(imm) => { + ValueLocation::Immediate(Ieee32::from_bits(imm.as_i32().unwrap() as _).into()) + } + val => val, + }; + + self.push(out)?; + Ok(()) + } + + pub fn f64_reinterpret_from_i64(&mut self) -> Result<(), Error> { + let val = self.pop()?; + + let out = match val { + ValueLocation::Immediate(imm) => { + ValueLocation::Immediate(Ieee64::from_bits(imm.as_i64().unwrap() as _).into()) + } + val => val, + }; + + self.push(out)?; + Ok(()) + } + + unop!(i64_popcnt, popcnt, Rq, u64, |a: u64| a.count_ones() as u64); + + // TODO: Use `lea` when the LHS operand isn't a temporary but both of the operands + // are in registers. + commutative_binop_i32!(i32_add, add, i32::wrapping_add); + commutative_binop_i32!(i32_and, and, |a, b| a & b); + commutative_binop_i32!(i32_or, or, |a, b| a | b); + commutative_binop_i32!(i32_xor, xor, |a, b| a ^ b); + binop_i32!(i32_sub, sub, i32::wrapping_sub); + + commutative_binop_i64!(i64_add, add, i64::wrapping_add); + commutative_binop_i64!(i64_and, and, |a, b| a & b); + commutative_binop_i64!(i64_or, or, |a, b| a | b); + commutative_binop_i64!(i64_xor, xor, |a, b| a ^ b); + binop_i64!(i64_sub, sub, i64::wrapping_sub); + + commutative_binop_f32!(f32_add, addss, |a, b| a + b); + commutative_binop_f32!(f32_mul, mulss, |a, b| a * b); + minmax_float!( + f32_min, + minss, + ucomiss, + addss, + orps, + as_f32, + |a: Ieee32, b: Ieee32| Ieee32::from_bits( + f32::from_bits(a.to_bits()) + .min(f32::from_bits(b.to_bits())) + .to_bits() + ) + ); + minmax_float!( + f32_max, + maxss, + ucomiss, + addss, + andps, + as_f32, + |a: Ieee32, b: Ieee32| Ieee32::from_bits( + f32::from_bits(a.to_bits()) + .max(f32::from_bits(b.to_bits())) + .to_bits() + ) + ); + binop_f32!(f32_sub, subss, |a, b| a - b); + binop_f32!(f32_div, divss, |a, b| a / b); + + pub fn f32_ceil(&mut self) -> Result<(), Error> { + self.relocated_function_call( + &ir::ExternalName::LibCall(ir::LibCall::CeilF32), + iter::once(F32), + iter::once(F32), + true, + )?; + Ok(()) + } + + pub fn f32_floor(&mut self) -> Result<(), Error> { + self.relocated_function_call( + &ir::ExternalName::LibCall(ir::LibCall::FloorF32), + iter::once(F32), + iter::once(F32), + true, + )?; + Ok(()) + } + + pub fn f32_nearest(&mut self) -> Result<(), Error> { + self.relocated_function_call( + &ir::ExternalName::LibCall(ir::LibCall::NearestF32), + iter::once(F32), + iter::once(F32), + true, + )?; + Ok(()) + } + + pub fn f32_trunc(&mut self) -> Result<(), Error> { + self.relocated_function_call( + &ir::ExternalName::LibCall(ir::LibCall::TruncF32), + iter::once(F32), + iter::once(F32), + true, + )?; + Ok(()) + } + + commutative_binop_f64!(f64_add, addsd, |a, b| a + b); + commutative_binop_f64!(f64_mul, mulsd, |a, b| a * b); + minmax_float!( + f64_min, + minsd, + ucomisd, + addsd, + orpd, + as_f64, + |a: Ieee64, b: Ieee64| Ieee64::from_bits( + f64::from_bits(a.to_bits()) + .min(f64::from_bits(b.to_bits())) + .to_bits() + ) + ); + minmax_float!( + f64_max, + maxsd, + ucomisd, + addsd, + andpd, + as_f64, + |a: Ieee64, b: Ieee64| Ieee64::from_bits( + f64::from_bits(a.to_bits()) + .max(f64::from_bits(b.to_bits())) + .to_bits() + ) + ); + binop_f64!(f64_sub, subsd, |a, b| a - b); + binop_f64!(f64_div, divsd, |a, b| a / b); + + pub fn f64_ceil(&mut self) -> Result<(), Error> { + self.relocated_function_call( + &ir::ExternalName::LibCall(ir::LibCall::CeilF64), + iter::once(F64), + iter::once(F64), + true, + )?; + Ok(()) + } + + pub fn f64_floor(&mut self) -> Result<(), Error> { + self.relocated_function_call( + &ir::ExternalName::LibCall(ir::LibCall::FloorF64), + iter::once(F64), + iter::once(F64), + true, + )?; + Ok(()) + } + + pub fn f64_nearest(&mut self) -> Result<(), Error> { + self.relocated_function_call( + &ir::ExternalName::LibCall(ir::LibCall::NearestF64), + iter::once(F64), + iter::once(F64), + true, + )?; + Ok(()) + } + + pub fn f64_trunc(&mut self) -> Result<(), Error> { + self.relocated_function_call( + &ir::ExternalName::LibCall(ir::LibCall::TruncF64), + iter::once(F64), + iter::once(F64), + true, + )?; + Ok(()) + } + + shift!( + i32_shl, + Rd, + shl, + |a, b| (a as i32).wrapping_shl(b as _), + I32 + ); + shift!( + i32_shr_s, + Rd, + sar, + |a, b| (a as i32).wrapping_shr(b as _), + I32 + ); + shift!( + i32_shr_u, + Rd, + shr, + |a, b| (a as u32).wrapping_shr(b as _), + I32 + ); + shift!( + i32_rotl, + Rd, + rol, + |a, b| (a as u32).rotate_left(b as _), + I32 + ); + shift!( + i32_rotr, + Rd, + ror, + |a, b| (a as u32).rotate_right(b as _), + I32 + ); + + shift!( + i64_shl, + Rq, + shl, + |a, b| (a as i64).wrapping_shl(b as _), + I64 + ); + shift!( + i64_shr_s, + Rq, + sar, + |a, b| (a as i64).wrapping_shr(b as _), + I64 + ); + shift!( + i64_shr_u, + Rq, + shr, + |a, b| (a as u64).wrapping_shr(b as _), + I64 + ); + shift!( + i64_rotl, + Rq, + rol, + |a, b| (a as u64).rotate_left(b as _), + I64 + ); + shift!( + i64_rotr, + Rq, + ror, + |a, b| (a as u64).rotate_right(b as _), + I64 + ); + + // TODO: Do this without emitting `mov` + fn cleanup_gprs(&mut self, gprs: impl Iterator) { + for gpr in gprs { + dynasm!(self.asm + ; pop Rq(gpr.rq().unwrap()) + ); + self.block_state.depth.free(1); + // DON'T MARK IT USED HERE! See comment in `full_div` + } + } + + int_div!( + i32_full_div_s, + i32_full_div_u, + i32_div_u, + i32_div_s, + i32_rem_u, + i32_rem_s, + imm_i32, + i32, + u32, + Rd, + DWORD + ); + int_div!( + i64_full_div_s, + i64_full_div_u, + i64_div_u, + i64_div_s, + i64_rem_u, + i64_rem_s, + imm_i64, + i64, + u64, + Rq, + QWORD + ); + + // TODO: With a proper SSE-like "Value" system we could do this way better (we wouldn't have + // to move `RAX`/`RDX` back afterwards). + fn full_div( + &mut self, + mut divisor: ValueLocation, + dividend: ValueLocation, + do_div: impl FnOnce(&mut Self, &mut ValueLocation) -> Result<(), Error>, + ) -> Result< + ( + ValueLocation, + ValueLocation, + impl Iterator + Clone + 'this, + ), + Error, + > { + // To stop `take_reg` from allocating either of these necessary registers + self.block_state.regs.mark_used(RAX); + self.block_state.regs.mark_used(RDX); + if divisor == ValueLocation::Reg(RAX) || divisor == ValueLocation::Reg(RDX) { + let new_reg = self.take_reg(GPRType::Rq).unwrap(); + self.copy_value(divisor, CCLoc::Reg(new_reg))?; + self.free_value(divisor)?; + + divisor = ValueLocation::Reg(new_reg); + } + self.block_state.regs.release(RAX)?; + self.block_state.regs.release(RDX)?; + + let saved_rax = if self.block_state.regs.is_free(RAX) { + None + } else { + dynasm!(self.asm + ; push rax + ); + self.block_state.depth.reserve(1); + // DON'T FREE THIS REGISTER HERE - since we don't + // remove it from the stack freeing the register + // here will cause `take_reg` to allocate it. + Some(()) + }; + + let saved_rdx = if self.block_state.regs.is_free(RDX) { + None + } else { + dynasm!(self.asm + ; push rdx + ); + self.block_state.depth.reserve(1); + // DON'T FREE THIS REGISTER HERE - since we don't + // remove it from the stack freeing the register + // here will cause `take_reg` to allocate it. + Some(()) + }; + + let saved = saved_rdx + .map(|_| RDX) + .into_iter() + .chain(saved_rax.map(|_| RAX)); + + self.copy_value(dividend, CCLoc::Reg(RAX))?; + self.block_state.regs.mark_used(RAX); + + self.free_value(dividend)?; + // To stop `take_reg` from allocating either of these necessary registers + self.block_state.regs.mark_used(RDX); + + do_div(self, &mut divisor)?; + self.free_value(divisor)?; + + if self.block_state.regs.is_free(RAX) { + return Err(Error::Microwasm("full_div: RAX is not free".to_string())); + } + if self.block_state.regs.is_free(RDX) { + return Err(Error::Microwasm("full_div: RDX is not free".to_string())); + } + + Ok((ValueLocation::Reg(RAX), ValueLocation::Reg(RDX), saved)) + } + + fn i32_full_div_u( + &mut self, + divisor: ValueLocation, + dividend: ValueLocation, + ) -> Result< + ( + ValueLocation, + ValueLocation, + impl Iterator + Clone + 'this, + ), + Error, + > { + self.full_div(divisor, dividend, |this, divisor| match divisor { + ValueLocation::Stack(offset) => { + let offset = this.adjusted_offset(*offset); + dynasm!(this.asm + ; xor edx, edx + ; div DWORD [rsp + offset] + ); + Ok(()) + } + ValueLocation::Immediate(_) | ValueLocation::Reg(_) | ValueLocation::Cond(_) => { + let r = match this.put_into_register(I32, divisor) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + dynasm!(this.asm + ; xor edx, edx + ; div Rd(r.rq().unwrap()) + ); + Ok(()) + } + }) + } + + fn i32_full_div_s( + &mut self, + divisor: ValueLocation, + dividend: ValueLocation, + ) -> Result< + ( + ValueLocation, + ValueLocation, + impl Iterator + Clone + 'this, + ), + Error, + > { + self.full_div(divisor, dividend, |this, divisor| match divisor { + ValueLocation::Stack(offset) => { + let offset = this.adjusted_offset(*offset); + dynasm!(this.asm + ; cdq + ; idiv DWORD [rsp + offset] + ); + Ok(()) + } + ValueLocation::Immediate(_) | ValueLocation::Reg(_) | ValueLocation::Cond(_) => { + let r = match this.put_into_register(I32, divisor) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + dynasm!(this.asm + ; cdq + ; idiv Rd(r.rq().unwrap()) + ); + Ok(()) + } + }) + } + + fn i64_full_div_u( + &mut self, + divisor: ValueLocation, + dividend: ValueLocation, + ) -> Result< + ( + ValueLocation, + ValueLocation, + impl Iterator + Clone + 'this, + ), + Error, + > { + self.full_div(divisor, dividend, |this, divisor| match divisor { + ValueLocation::Stack(offset) => { + let offset = this.adjusted_offset(*offset); + dynasm!(this.asm + ; xor rdx, rdx + ; div QWORD [rsp + offset] + ); + Ok(()) + } + ValueLocation::Immediate(_) | ValueLocation::Reg(_) | ValueLocation::Cond(_) => { + let r = match this.put_into_register(I64, divisor) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + dynasm!(this.asm + ; xor rdx, rdx + ; div Rq(r.rq().unwrap()) + ); + Ok(()) + } + }) + } + + fn i64_full_div_s( + &mut self, + divisor: ValueLocation, + dividend: ValueLocation, + ) -> Result< + ( + ValueLocation, + ValueLocation, + impl Iterator + Clone + 'this, + ), + Error, + > { + self.full_div(divisor, dividend, |this, divisor| match divisor { + ValueLocation::Stack(offset) => { + let offset = this.adjusted_offset(*offset); + dynasm!(this.asm + ; cqo + ; idiv QWORD [rsp + offset] + ); + Ok(()) + } + ValueLocation::Immediate(_) | ValueLocation::Reg(_) | ValueLocation::Cond(_) => { + let r = match this.put_into_register(I64, divisor) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + dynasm!(this.asm + ; cqo + ; idiv Rq(r.rq().unwrap()) + ); + Ok(()) + } + }) + } + + // `i32_mul` needs to be separate because the immediate form of the instruction + // has a different syntax to the immediate form of the other instructions. + pub fn i32_mul(&mut self) -> Result<(), Error> { + let right = self.pop()?; + let left = self.pop()?; + + if let Some(right) = right.immediate() { + if let Some(left) = left.immediate() { + self.push(ValueLocation::Immediate( + i32::wrapping_mul(right.as_i32().unwrap(), left.as_i32().unwrap()).into(), + ))?; + return Ok(()); + } + } + + let (mut left, mut right) = match left { + ValueLocation::Reg(_) => (left, right), + _ => { + if right.immediate().is_some() { + (left, right) + } else { + (right, left) + } + } + }; + + let out = match right { + ValueLocation::Reg(_) | ValueLocation::Cond(_) => { + let rreg = match self.put_into_register(I32, &mut right) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + let lreg = match self.put_into_temp_register(I32, &mut left) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + dynasm!(self.asm + ; imul Rd(lreg.rq().unwrap()), Rd(rreg.rq().unwrap()) + ); + left + } + ValueLocation::Stack(offset) => { + let offset = self.adjusted_offset(offset); + + let lreg = match self.put_into_temp_register(I32, &mut left) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + dynasm!(self.asm + ; imul Rd(lreg.rq().unwrap()), [rsp + offset] + ); + left + } + ValueLocation::Immediate(i) => { + let lreg = match self.put_into_register(I32, &mut left) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + let new_reg = self.take_reg(I32).unwrap(); + dynasm!(self.asm + ; imul Rd(new_reg.rq().unwrap()), Rd(lreg.rq().unwrap()), i.as_i32().unwrap() + ); + self.free_value(left)?; + ValueLocation::Reg(new_reg) + } + }; + + self.push(out)?; + self.free_value(right)?; + Ok(()) + } + + // `i64_mul` needs to be separate because the immediate form of the instruction + // has a different syntax to the immediate form of the other instructions. + pub fn i64_mul(&mut self) -> Result<(), Error> { + let right = self.pop()?; + let left = self.pop()?; + + if let Some(right) = right.immediate() { + if let Some(left) = left.immediate() { + self.push(ValueLocation::Immediate( + i64::wrapping_mul(right.as_i64().unwrap(), left.as_i64().unwrap()).into(), + ))?; + return Ok(()); + } + } + + let (mut left, mut right) = match left { + ValueLocation::Reg(_) => (left, right), + _ => { + if right.immediate().is_some() { + (left, right) + } else { + (right, left) + } + } + }; + + let out = match right { + ValueLocation::Reg(_) | ValueLocation::Cond(_) => { + let rreg = match self.put_into_register(I64, &mut right) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + let lreg = match self.put_into_temp_register(I64, &mut left) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + dynasm!(self.asm + ; imul Rq(lreg.rq().unwrap()), Rq(rreg.rq().unwrap()) + ); + left + } + ValueLocation::Stack(offset) => { + let offset = self.adjusted_offset(offset); + + let lreg = match self.put_into_temp_register(I64, &mut left) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + dynasm!(self.asm + ; imul Rq(lreg.rq().unwrap()), [rsp + offset] + ); + left + } + ValueLocation::Immediate(i) => { + let i = i.as_i64().unwrap(); + if let Ok(i) = i.try_into() { + let new_reg = self.take_reg(I64).unwrap(); + + let lreg = match self.put_into_register(I64, &mut left) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + dynasm!(self.asm + ; imul Rq(new_reg.rq().unwrap()), Rq(lreg.rq().unwrap()), i + ); + + self.free_value(left)?; + + ValueLocation::Reg(new_reg) + } else { + let rreg = match self.put_into_register(I64, &mut right) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + let lreg = match self.put_into_temp_register(I64, &mut left) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + dynasm!(self.asm + ; imul Rq(lreg.rq().unwrap()), Rq(rreg.rq().unwrap()) + ); + left + } + } + }; + + self.push(out)?; + self.free_value(right)?; + Ok(()) + } + + fn cmov(&mut self, cond_code: CondCode, dst: GPR, src: CCLoc) { + match src { + CCLoc::Reg(reg) => match cond_code { + cc::EQUAL => { + dynasm!(self.asm + ; cmove Rq(dst.rq().unwrap()), Rq(reg.rq().unwrap()) + ); + } + cc::NOT_EQUAL => { + dynasm!(self.asm + ; cmovne Rq(dst.rq().unwrap()), Rq(reg.rq().unwrap()) + ); + } + cc::GE_U => { + dynasm!(self.asm + ; cmovae Rq(dst.rq().unwrap()), Rq(reg.rq().unwrap()) + ); + } + cc::LT_U => { + dynasm!(self.asm + ; cmovb Rq(dst.rq().unwrap()), Rq(reg.rq().unwrap()) + ); + } + cc::GT_U => { + dynasm!(self.asm + ; cmova Rq(dst.rq().unwrap()), Rq(reg.rq().unwrap()) + ); + } + cc::LE_U => { + dynasm!(self.asm + ; cmovbe Rq(dst.rq().unwrap()), Rq(reg.rq().unwrap()) + ); + } + cc::GE_S => { + dynasm!(self.asm + ; cmovge Rq(dst.rq().unwrap()), Rq(reg.rq().unwrap()) + ); + } + cc::LT_S => { + dynasm!(self.asm + ; cmovl Rq(dst.rq().unwrap()), Rq(reg.rq().unwrap()) + ); + } + cc::GT_S => { + dynasm!(self.asm + ; cmovg Rq(dst.rq().unwrap()), Rq(reg.rq().unwrap()) + ); + } + cc::LE_S => { + dynasm!(self.asm + ; cmovle Rq(dst.rq().unwrap()), Rq(reg.rq().unwrap()) + ); + } + }, + CCLoc::Stack(offset) => { + let offset = self.adjusted_offset(offset); + + match cond_code { + cc::EQUAL => { + dynasm!(self.asm + ; cmove Rq(dst.rq().unwrap()), [rsp + offset] + ); + } + cc::NOT_EQUAL => { + dynasm!(self.asm + ; cmovne Rq(dst.rq().unwrap()), [rsp + offset] + ); + } + cc::GE_U => { + dynasm!(self.asm + ; cmovae Rq(dst.rq().unwrap()), [rsp + offset] + ); + } + cc::LT_U => { + dynasm!(self.asm + ; cmovb Rq(dst.rq().unwrap()), [rsp + offset] + ); + } + cc::GT_U => { + dynasm!(self.asm + ; cmova Rq(dst.rq().unwrap()), [rsp + offset] + ); + } + cc::LE_U => { + dynasm!(self.asm + ; cmovbe Rq(dst.rq().unwrap()), [rsp + offset] + ); + } + cc::GE_S => { + dynasm!(self.asm + ; cmovge Rq(dst.rq().unwrap()), [rsp + offset] + ); + } + cc::LT_S => { + dynasm!(self.asm + ; cmovl Rq(dst.rq().unwrap()), [rsp + offset] + ); + } + cc::GT_S => { + dynasm!(self.asm + ; cmovg Rq(dst.rq().unwrap()), [rsp + offset] + ); + } + cc::LE_S => { + dynasm!(self.asm + ; cmovle Rq(dst.rq().unwrap()), [rsp + offset] + ); + } + } + } + } + } + + pub fn select(&mut self) -> Result<(), Error> { + let mut cond = self.pop()?; + let mut else_ = self.pop()?; + let mut then = self.pop()?; + + if let ValueLocation::Immediate(i) = cond { + if i.as_i32().unwrap() == 0 { + self.free_value(then)?; + self.push(else_)?; + } else { + self.free_value(else_)?; + self.push(then)?; + } + + return Ok(()); + } + + let cond_code = match cond { + ValueLocation::Cond(cc) => cc, + _ => { + let cond_reg = match self.put_into_register(I32, &mut cond) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + dynasm!(self.asm + ; test Rd(cond_reg.rq().unwrap()), Rd(cond_reg.rq().unwrap()) + ); + self.free_value(cond)?; + + cc::NOT_EQUAL + } + }; + + let else_ = if let ValueLocation::Stack(offset) = else_ { + CCLoc::Stack(offset) + } else { + let gpr = match self.put_into_register(I32, &mut else_) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + CCLoc::Reg(gpr) + }; + + let then = if let ValueLocation::Stack(offset) = then { + CCLoc::Stack(offset) + } else { + let gpr = match self.put_into_register(I32, &mut then) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + CCLoc::Reg(gpr) + }; + + let out_gpr = match (then, else_) { + (CCLoc::Reg(then_reg), else_) if self.block_state.regs.num_usages(then_reg) <= 1 => { + self.cmov(!cond_code, then_reg, else_); + self.free_value(else_.into())?; + + then_reg + } + (then, CCLoc::Reg(else_reg)) if self.block_state.regs.num_usages(else_reg) <= 1 => { + self.cmov(cond_code, else_reg, then); + self.free_value(then.into())?; + + else_reg + } + (then, else_) => { + let out = self.take_reg(GPRType::Rq).unwrap(); + self.copy_value(else_.into(), CCLoc::Reg(out))?; + self.cmov(cond_code, out, then); + + self.free_value(then.into())?; + self.free_value(else_.into())?; + + out + } + }; + + self.push(ValueLocation::Reg(out_gpr))?; + Ok(()) + } + + pub fn pick(&mut self, depth: u32) { + let idx = self.block_state.stack.len() - 1 - depth as usize; + let v = self.block_state.stack[idx]; + if let ValueLocation::Reg(r) = v { + self.block_state.regs.mark_used(r); + } + self.block_state.stack.push(v); + } + + pub fn const_(&mut self, imm: Value) -> Result<(), Error> { + self.push(ValueLocation::Immediate(imm))?; + Ok(()) + } + + fn relocated_function_call( + &mut self, + name: &cranelift_codegen::ir::ExternalName, + args: impl IntoIterator, + rets: impl IntoIterator, + preserve_vmctx: bool, + ) -> Result<(), Error> { + let locs = arg_locs(args)?; + + self.save_volatile(..locs.len())?; + + if preserve_vmctx { + dynasm!(self.asm + ; push Rq(VMCTX) + ); + self.block_state.depth.reserve(1); + } + + let depth = self.block_state.depth; + + self.pass_outgoing_args(&locs)?; + // 2 bytes for the 64-bit `mov` opcode + register ident, the rest is the immediate + self.reloc_sink.reloc_external( + (self.asm.offset().0 + - self.func_starts[self.current_function as usize] + .0 + .unwrap() + .0) as u32 + + 2, + binemit::Reloc::Abs8, + name, + 0, + ); + let temp = self.take_reg(I64).unwrap(); + dynasm!(self.asm + ; mov Rq(temp.rq().unwrap()), QWORD 0xDEAD_BEEF_DEAD_BEEF_u64 as i64 + ; call Rq(temp.rq().unwrap()) + ); + self.block_state.regs.release(temp)?; + + for i in locs { + self.free_value(i.into())?; + } + + self.push_function_returns(rets)?; + + if preserve_vmctx { + self.set_stack_depth(depth)?; + + dynasm!(self.asm + ; pop Rq(VMCTX) + ); + self.block_state.depth.free(1); + } + Ok(()) + } + + // TODO: Other memory indices + pub fn memory_size(&mut self) -> Result<(), Error> { + let memory_index = 0; + if let Some(defined_memory_index) = self.module_context.defined_memory_index(memory_index) { + self.push(ValueLocation::Immediate(defined_memory_index.into()))?; + self.relocated_function_call( + &magic::get_memory32_size_name(), + iter::once(I32), + iter::once(I32), + true, + )?; + } else { + self.push(ValueLocation::Immediate(memory_index.into()))?; + self.relocated_function_call( + &magic::get_imported_memory32_size_name(), + iter::once(I32), + iter::once(I32), + true, + )?; + } + Ok(()) + } + + // TODO: Other memory indices + pub fn memory_grow(&mut self) -> Result<(), Error> { + let memory_index = 0; + if let Some(defined_memory_index) = self.module_context.defined_memory_index(memory_index) { + self.push(ValueLocation::Immediate(defined_memory_index.into()))?; + self.relocated_function_call( + &magic::get_memory32_grow_name(), + iter::once(I32).chain(iter::once(I32)), + iter::once(I32), + true, + )?; + } else { + self.push(ValueLocation::Immediate(memory_index.into()))?; + self.relocated_function_call( + &magic::get_imported_memory32_grow_name(), + iter::once(I32).chain(iter::once(I32)), + iter::once(I32), + true, + )?; + } + Ok(()) + } + + // TODO: Use `ArrayVec`? + // TODO: This inefficiently duplicates registers but it's not really possible + // to double up stack space right now. + /// Saves volatile (i.e. caller-saved) registers before a function call, if they are used. + fn save_volatile(&mut self, _bounds: impl std::ops::RangeBounds) -> Result<(), Error> { + self.save_regs(SCRATCH_REGS, ..)?; + Ok(()) + } + + fn save_regs( + &mut self, + regs: &I, + bounds: impl std::ops::RangeBounds, + ) -> Result<(), Error> + where + for<'a> &'a I: IntoIterator, + I: ?Sized, + { + use std::ops::Bound::*; + + let mut stack = mem::replace(&mut self.block_state.stack, vec![]); + let (start, end) = ( + match bounds.end_bound() { + Unbounded => 0, + Included(v) => stack.len().saturating_sub(1 + v), + Excluded(v) => stack.len().saturating_sub(*v), + }, + match bounds.start_bound() { + Unbounded => stack.len(), + Included(v) => stack.len().saturating_sub(*v), + Excluded(v) => stack.len().saturating_sub(1 + v), + }, + ); + + let mut slice = &mut stack[start..end]; + + while let Some((first, rest)) = slice.split_first_mut() { + if let ValueLocation::Reg(vreg) = *first { + if regs.into_iter().any(|r| *r == vreg) { + let old = *first; + *first = self.push_physical(old)?; + for val in &mut *rest { + if *val == old { + self.free_value(*val)?; + *val = *first; + } + } + } + } + + slice = rest; + } + + mem::replace(&mut self.block_state.stack, stack); + Ok(()) + } + + /// Write the arguments to the callee to the registers and the stack using the SystemV + /// calling convention. + fn pass_outgoing_args(&mut self, out_locs: &[CCLoc]) -> Result<(), Error> { + // TODO: Do alignment here + let total_stack_space = out_locs + .iter() + .flat_map(|&l| { + if let CCLoc::Stack(offset) = l { + if offset > 0 { + Some(offset as u32) + } else { + None + } + } else { + None + } + }) + .max() + .unwrap_or(0); + let mut depth = self.block_state.depth.0 + total_stack_space; + + if depth & 1 != 0 { + self.set_stack_depth(StackDepth(self.block_state.depth.0 + 1))?; + depth += 1; + } + + let mut pending = Vec::<(ValueLocation, CCLoc)>::with_capacity(out_locs.len()); + + for &loc in out_locs.iter().rev() { + let val = self.pop()?; + + pending.push((val, loc)); + } + + while !pending.is_empty() { + let start_len = pending.len(); + + for (src, dst) in mem::replace(&mut pending, vec![]) { + if src != ValueLocation::from(dst) { + if let CCLoc::Reg(r) = dst { + if !self.block_state.regs.is_free(r) { + pending.push((src, dst)); + continue; + } + + self.block_state.regs.mark_used(r); + } + + self.copy_value(src, dst)?; + self.free_value(src)?; + } + } + + if pending.len() == start_len { + let src = match pending + .iter() + .filter_map(|(src, _)| { + if let ValueLocation::Reg(reg) = src { + Some(reg) + } else { + None + } + }) + .next() + { + None => { + return Err(Error::Microwasm( + "Programmer error: We shouldn't need to push \ + intermediate args if we don't have any argument sources in registers" + .to_string(), + )) + } + Some(val) => *val, + }; + let new_src = self.push_physical(ValueLocation::Reg(src))?; + for (old_src, _) in pending.iter_mut() { + if *old_src == ValueLocation::Reg(src) { + *old_src = new_src; + } + } + } + } + + self.set_stack_depth(StackDepth(depth))?; + Ok(()) + } + + fn push_function_returns( + &mut self, + returns: impl IntoIterator, + ) -> Result<(), Error> { + for loc in ret_locs(returns)? { + if let CCLoc::Reg(reg) = loc { + self.block_state.regs.mark_used(reg); + } + + self.push(loc.into())?; + } + Ok(()) + } + + pub fn call_indirect( + &mut self, + type_id: u32, + arg_types: impl IntoIterator, + return_types: impl IntoIterator, + ) -> Result<(), Error> { + let locs = arg_locs(arg_types)?; + + for &loc in &locs { + if let CCLoc::Reg(r) = loc { + self.block_state.regs.mark_used(r); + } + } + + let mut callee = self.pop()?; + let callee_reg = match self.put_into_temp_register(I32, &mut callee) { + Err(e) => return Err(e), + Ok(o) => o.unwrap(), + }; + + for &loc in &locs { + if let CCLoc::Reg(r) = loc { + self.block_state.regs.release(r)?; + } + } + + self.save_volatile(..locs.len())?; + dynasm!(self.asm + ; push Rq(VMCTX) + ); + self.block_state.depth.reserve(1); + let depth = self.block_state.depth; + + self.pass_outgoing_args(&locs)?; + + let fail = self.trap_label().0; + let table_index = 0; + let reg_offset = self + .module_context + .defined_table_index(table_index) + .map(|index| { + ( + None, + self.module_context.vmctx_vmtable_definition(index) as i32, + ) + }); + + let vmctx = GPR::Rq(VMCTX); + let (reg, offset) = reg_offset.unwrap_or_else(|| { + let reg = self.take_reg(I64).unwrap(); + + dynasm!(self.asm + ; mov Rq(reg.rq().unwrap()), [ + Rq(VMCTX) + self.module_context.vmctx_vmtable_import_from(table_index) as i32 + ] + ); + + (Some(reg), 0) + }); + + let temp0 = self.take_reg(I64).unwrap(); + + dynasm!(self.asm + ; cmp Rd(callee_reg.rq().unwrap()), [ + Rq(reg.unwrap_or(vmctx).rq().unwrap()) + + offset + + self.module_context.vmtable_definition_current_elements() as i32 + ] + ; jae =>fail + ; imul + Rd(callee_reg.rq().unwrap()), + Rd(callee_reg.rq().unwrap()), + self.module_context.size_of_vmcaller_checked_anyfunc() as i32 + ; mov Rq(temp0.rq().unwrap()), [ + Rq(reg.unwrap_or(vmctx).rq().unwrap()) + + offset + + self.module_context.vmtable_definition_base() as i32 + ] + ); + + if let Some(reg) = reg { + self.block_state.regs.release(reg)?; + } + + let temp1 = self.take_reg(I64).unwrap(); + + dynasm!(self.asm + ; mov Rd(temp1.rq().unwrap()), [ + Rq(VMCTX) + + self.module_context + .vmctx_vmshared_signature_id(type_id) as i32 + ] + ; cmp DWORD [ + Rq(temp0.rq().unwrap()) + + Rq(callee_reg.rq().unwrap()) + + self.module_context.vmcaller_checked_anyfunc_type_index() as i32 + ], Rd(temp1.rq().unwrap()) + ; jne =>fail + ; mov Rq(VMCTX), [ + Rq(temp0.rq().unwrap()) + + Rq(callee_reg.rq().unwrap()) + + self.module_context.vmcaller_checked_anyfunc_vmctx() as i32 + ] + ; call QWORD [ + Rq(temp0.rq().unwrap()) + + Rq(callee_reg.rq().unwrap()) + + self.module_context.vmcaller_checked_anyfunc_func_ptr() as i32 + ] + ); + + self.block_state.regs.release(temp0)?; + self.block_state.regs.release(temp1)?; + self.free_value(callee)?; + + for i in locs { + self.free_value(i.into())?; + } + + self.push_function_returns(return_types)?; + + self.set_stack_depth(depth)?; + dynasm!(self.asm + ; pop Rq(VMCTX) + ); + self.block_state.depth.free(1); + Ok(()) + } + + pub fn swap(&mut self, depth: u32) { + let last = self.block_state.stack.len() - 1; + self.block_state.stack.swap(last, last - depth as usize); + } + + /// Call a function with the given index + pub fn call_direct( + &mut self, + index: u32, + arg_types: impl IntoIterator, + return_types: impl IntoIterator, + ) -> Result<(), Error> { + self.relocated_function_call( + &ir::ExternalName::user(0, index), + arg_types, + return_types, + false, + )?; + Ok(()) + } + + /// Call a function with the given index + pub fn call_direct_self( + &mut self, + defined_index: u32, + arg_types: impl IntoIterator, + return_types: impl IntoIterator, + ) -> Result<(), Error> { + let locs = arg_locs(arg_types)?; + + self.save_volatile(..locs.len())?; + + let (_, label) = self.func_starts[defined_index as usize]; + + self.pass_outgoing_args(&locs)?; + dynasm!(self.asm + ; call =>label + ); + + for i in locs { + self.free_value(i.into())?; + } + + self.push_function_returns(return_types)?; + Ok(()) + } + + /// Call a function with the given index + pub fn call_direct_imported( + &mut self, + index: u32, + arg_types: impl IntoIterator, + return_types: impl IntoIterator, + ) -> Result<(), Error> { + let locs = arg_locs(arg_types)?; + + dynasm!(self.asm + ; push Rq(VMCTX) + ); + self.block_state.depth.reserve(1); + let depth = self.block_state.depth; + + self.save_volatile(..locs.len())?; + self.pass_outgoing_args(&locs)?; + + let callee = self.take_reg(I64).unwrap(); + + dynasm!(self.asm + ; mov Rq(callee.rq().unwrap()), [ + Rq(VMCTX) + self.module_context.vmctx_vmfunction_import_body(index) as i32 + ] + ; mov Rq(VMCTX), [ + Rq(VMCTX) + self.module_context.vmctx_vmfunction_import_vmctx(index) as i32 + ] + ; call Rq(callee.rq().unwrap()) + ); + + self.block_state.regs.release(callee)?; + + for i in locs { + self.free_value(i.into())?; + } + + self.push_function_returns(return_types)?; + + self.set_stack_depth(depth)?; + dynasm!(self.asm + ; pop Rq(VMCTX) + ); + self.block_state.depth.free(1); + Ok(()) + } + + // TODO: Reserve space to store RBX, RBP, and R12..R15 so we can use them + // as scratch registers + /// Writes the function prologue and stores the arguments as locals + pub fn start_function( + &mut self, + params: impl IntoIterator, + ) -> Result<(), Error> { + let i_locs = arg_locs(params)?; + let locs = Vec::from_iter(i_locs); + + self.apply_cc(&BlockCallingConvention::function_start(locs))?; + Ok(()) + } + + pub fn ret(&mut self) { + dynasm!(self.asm + ; ret + ); + } + + pub fn epilogue(&mut self) {} + + pub fn trap(&mut self) { + let trap_label = self.trap_label(); + dynasm!(self.asm + ; jmp =>trap_label.0 + ); + } + + pub fn trap_label(&mut self) -> Label { + self.label(|asm: &mut Assembler| { + dynasm!(asm + ; ud2 + ); + }) + } + + pub fn ret_label(&mut self) -> Label { + self.label(|asm: &mut Assembler| { + dynasm!(asm + ; ret + ); + }) + } + + fn label(&mut self, fun: F) -> Label + where + F: IntoLabel, + { + self.aligned_label(1, fun) + } + + fn aligned_label(&mut self, align: u32, fun: F) -> Label + where + F: IntoLabel, + { + let key = fun.key(); + if let Some((label, _, _)) = self.labels.get(&(align, key)) { + return *label; + } + + let label = self.create_label(); + self.labels + .insert((align, key), (label, align, Some(fun.callback()))); + + label + } + + fn target_to_label(&mut self, target: BrTarget

+ /// Represents an abstract host binding. + /// + internal abstract class Binding + { + public abstract SafeHandle Bind(Store store, IHost host); + + public static WasmtimeException CreateBindingException(Import import, MemberInfo member, string message) + { + return new WasmtimeException($"Unable to bind '{member.DeclaringType.Name}.{member.Name}' to WebAssembly import '{import}': {message}."); + } + + public static List GetImportBindings(Module module, Wasi wasi = null, IHost host = null) + { + if (module is null) + { + throw new ArgumentNullException(nameof(module)); + } + + var bindings = new List(); + var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; + var type = host?.GetType(); + var methods = type?.GetMethods(flags).Where(m => !m.IsSpecialName && Attribute.IsDefined(m, typeof(ImportAttribute))); + var fields = type?.GetFields(flags).Where(m => !m.IsSpecialName && Attribute.IsDefined(m, typeof(ImportAttribute))); + + foreach (var import in module.Imports.All) + { + var wasiBinding = wasi?.Bind(import); + if (!(wasiBinding is null)) + { + bindings.Add(wasiBinding); + continue; + } + + switch (import) + { + case FunctionImport func: + bindings.Add(BindFunction(func, methods)); + break; + + case GlobalImport global: + bindings.Add(BindGlobal(global, fields)); + break; + + case MemoryImport memory: + bindings.Add(BindMemory(memory, fields)); + break; + + default: + throw new NotSupportedException("Unsupported import binding type."); + } + } + + return bindings; + } + + private static FunctionBinding BindFunction(FunctionImport import, IEnumerable methods) + { + var method = methods?.Where(m => + { + var attribute = (ImportAttribute)m.GetCustomAttribute(typeof(ImportAttribute)); + if (attribute is null) + { + return false; + } + + return attribute.Name == import.Name && + ((string.IsNullOrEmpty(attribute.Module) && + string.IsNullOrEmpty(import.ModuleName)) || + attribute.Module == import.ModuleName); + } + ).FirstOrDefault(); + + if (method is null) + { + throw new WasmtimeException($"Failed to bind function import '{import}': the host does not contain a method with a matching 'Import' attribute."); + } + + return new FunctionBinding(import, method); + } + + private static GlobalBinding BindGlobal(GlobalImport import, IEnumerable fields) + { + var field = fields?.Where(f => + { + var attribute = (ImportAttribute)f.GetCustomAttribute(typeof(ImportAttribute)); + return attribute.Name == import.Name && + ((string.IsNullOrEmpty(attribute.Module) && + string.IsNullOrEmpty(import.ModuleName)) || + attribute.Module == import.ModuleName); + } + ).FirstOrDefault(); + + if (field is null) + { + throw new WasmtimeException($"Failed to bind global import '{import}': the host does not contain a global field with a matching 'Import' attribute."); + } + + return new GlobalBinding(import, field); + } + + private static MemoryBinding BindMemory(MemoryImport import, IEnumerable fields) + { + var field = fields?.Where(f => + { + var attribute = (ImportAttribute)f.GetCustomAttribute(typeof(ImportAttribute)); + return attribute.Name == import.Name && + ((string.IsNullOrEmpty(attribute.Module) && + string.IsNullOrEmpty(import.ModuleName)) || + attribute.Module == import.ModuleName); + } + ).FirstOrDefault(); + + if (field is null) + { + throw new WasmtimeException($"Failed to bind memory import '{import}': the host does not contain a memory field with a matching 'Import' attribute."); + } + + return new MemoryBinding(import, field); + } + } +} diff --git a/crates/misc/dotnet/src/Bindings/FunctionBinding.cs b/crates/misc/dotnet/src/Bindings/FunctionBinding.cs new file mode 100644 index 0000000000..5d5d781750 --- /dev/null +++ b/crates/misc/dotnet/src/Bindings/FunctionBinding.cs @@ -0,0 +1,339 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using Wasmtime.Imports; + +namespace Wasmtime.Bindings +{ + /// + /// Represents a host function binding. + /// + internal class FunctionBinding : Binding + { + /// + /// Constructs a new function binding. + /// + /// The function import of the binding. + /// The method the import is bound to. + public FunctionBinding(FunctionImport import, MethodInfo method) + { + if (import is null) + { + throw new ArgumentNullException(nameof(import)); + } + + if (method is null) + { + throw new ArgumentNullException(nameof(method)); + } + + Import = import; + Method = method; + + Validate(); + } + + /// + /// The function import of the binding. + /// + public FunctionImport Import { get; private set; } + + /// + /// The method the import is bound to. + /// + public MethodInfo Method { get; private set; } + + public override SafeHandle Bind(Store store, IHost host) + { + unsafe + { + var parameters = Interop.ToValueTypeVec(Import.Parameters); + var results = Interop.ToValueTypeVec(Import.Results); + using var funcType = Interop.wasm_functype_new(ref parameters, ref results); + var callback = CreateCallback(store, host); + var func = Interop.wasm_func_new(store.Handle, funcType, callback); + // Store the callback with the safe handle to keep the delegate GC reachable + func.Callback = callback; + return func; + } + } + + private void Validate() + { + if (Method.IsStatic) + { + throw CreateBindingException(Import, Method, "method cannot be static"); + } + + if (Method.IsGenericMethod) + { + throw CreateBindingException(Import, Method, "method cannot be generic"); + } + + if (Method.IsConstructor) + { + throw CreateBindingException(Import, Method, "method cannot be a constructor"); + } + + ValidateParameters(); + + ValidateReturnType(); + } + + private void ValidateParameters() + { + var parameters = Method.GetParameters(); + if (parameters.Length != Import.Parameters.Count) + { + throw CreateBindingException( + Import, + Method, + $"parameter mismatch: import requires {Import.Parameters.Count} but the method has {parameters.Length}"); + } + + for (int i = 0; i < parameters.Length; ++i) + { + var parameter = parameters[i]; + if (parameter.ParameterType.IsByRef) + { + if (parameter.IsOut) + { + throw CreateBindingException(Import, Method, $"parameter '{parameter.Name}' cannot be an 'out' parameter"); + } + else + { + throw CreateBindingException(Import, Method, $"parameter '{parameter.Name}' cannot be a 'ref' parameter"); + } + } + + var expected = Import.Parameters[i]; + if (!Interop.TryGetValueKind(parameter.ParameterType, out var kind) || !Interop.IsMatchingKind(kind, expected)) + { + throw CreateBindingException(Import, Method, $"method parameter '{parameter.Name}' is expected to be of type '{Interop.ToString(expected)}'"); + } + } + } + + private void ValidateReturnType() + { + int resultsCount = Import.Results.Count(); + if (resultsCount == 0) + { + if (Method.ReturnType != typeof(void)) + { + throw CreateBindingException(Import, Method, "method must return void"); + } + } + else if (resultsCount == 1) + { + var expected = Import.Results[0]; + if (!Interop.TryGetValueKind(Method.ReturnType, out var kind) || !Interop.IsMatchingKind(kind, expected)) + { + throw CreateBindingException(Import, Method, $"return type is expected to be '{Interop.ToString(expected)}'"); + } + } + else + { + if (!IsTupleOfSize(Method.ReturnType, resultsCount)) + { + throw CreateBindingException(Import, Method, $"return type is expected to be a tuple of size {resultsCount}"); + } + + var typeArguments = + Method.ReturnType.GetGenericArguments().SelectMany(type => + { + if (type.IsConstructedGenericType) + { + return type.GenericTypeArguments; + } + return Enumerable.Repeat(type, 1); + }); + + int i = 0; + foreach (var typeArgument in typeArguments) + { + var expected = Import.Results[i]; + if (!Interop.TryGetValueKind(typeArgument, out var kind) || !Interop.IsMatchingKind(kind, expected)) + { + throw CreateBindingException(Import, Method, $"return tuple item #{i} is expected to be of type '{Interop.ToString(expected)}'"); + } + + ++i; + } + } + } + + private static bool IsTupleOfSize(Type type, int size) + { + if (!type.IsConstructedGenericType) + { + return false; + } + + var definition = type.GetGenericTypeDefinition(); + + if (size == 0) + { + return definition == typeof(ValueTuple); + } + + if (size == 1) + { + return definition == typeof(ValueTuple<>); + } + + if (size == 2) + { + return definition == typeof(ValueTuple<,>); + } + + if (size == 3) + { + return definition == typeof(ValueTuple<,,>); + } + + if (size == 4) + { + return definition == typeof(ValueTuple<,,,>); + } + + if (size == 5) + { + return definition == typeof(ValueTuple<,,,,>); + } + + if (size == 6) + { + return definition == typeof(ValueTuple<,,,,,>); + } + + if (size == 7) + { + return definition == typeof(ValueTuple<,,,,,,>); + } + + if (definition != typeof(ValueTuple<,,,,,,,>)) + { + return false; + } + + return IsTupleOfSize(type.GetGenericArguments().Last(), size - 7); + } + + private unsafe Interop.WasmFuncCallback CreateCallback(Store store, IHost host) + { + var args = new object[Import.Parameters.Count]; + bool hasReturn = Method.ReturnType != typeof(void); + var storeHandle = store.Handle; + + Interop.WasmFuncCallback callback = (arguments, results) => + { + try + { + SetArgs(arguments, args); + + var result = Method.Invoke(host, BindingFlags.DoNotWrapExceptions, null, args, null); + + if (hasReturn) + { + SetResults(result, results); + } + return IntPtr.Zero; + } + catch (Exception ex) + { + var bytes = Encoding.UTF8.GetBytes(ex.Message + "\0" /* exception messages need a null */); + + fixed (byte* ptr = bytes) + { + Interop.wasm_byte_vec_t message = new Interop.wasm_byte_vec_t(); + message.size = (UIntPtr)bytes.Length; + message.data = ptr; + + return Interop.wasm_trap_new(storeHandle, ref message); + } + } + }; + + return callback; + } + + private static unsafe void SetArgs(Interop.wasm_val_t* arguments, object[] args) + { + for (int i = 0; i < args.Length; ++i) + { + var arg = arguments[i]; + + switch (arg.kind) + { + case Interop.wasm_valkind_t.WASM_I32: + args[i] = arg.of.i32; + break; + + case Interop.wasm_valkind_t.WASM_I64: + args[i] = arg.of.i64; + break; + + case Interop.wasm_valkind_t.WASM_F32: + args[i] = arg.of.f32; + break; + + case Interop.wasm_valkind_t.WASM_F64: + args[i] = arg.of.f64; + break; + + default: + throw new NotSupportedException("Unsupported value type."); + } + } + } + + private static unsafe void SetResults(object value, Interop.wasm_val_t* results) + { + var tuple = value as ITuple; + if (tuple is null) + { + SetResult(value, &results[0]); + } + else + { + for (int i = 0; i < tuple.Length; ++i) + { + SetResults(tuple[i], &results[i]); + } + } + } + + private static unsafe void SetResult(object value, Interop.wasm_val_t* result) + { + switch (value) + { + case int i: + result->kind = Interop.wasm_valkind_t.WASM_I32; + result->of.i32 = i; + break; + + case long l: + result->kind = Interop.wasm_valkind_t.WASM_I64; + result->of.i64 = l; + break; + + case float f: + result->kind = Interop.wasm_valkind_t.WASM_F32; + result->of.f32 = f; + break; + + case double d: + result->kind = Interop.wasm_valkind_t.WASM_F64; + result->of.f64 = d; + break; + + default: + throw new NotSupportedException("Unsupported return value type."); + } + } + } +} diff --git a/crates/misc/dotnet/src/Bindings/GlobalBinding.cs b/crates/misc/dotnet/src/Bindings/GlobalBinding.cs new file mode 100644 index 0000000000..da75e8148b --- /dev/null +++ b/crates/misc/dotnet/src/Bindings/GlobalBinding.cs @@ -0,0 +1,125 @@ +using System; +using System.Reflection; +using System.Runtime.InteropServices; +using Wasmtime.Imports; + +namespace Wasmtime.Bindings +{ + /// + /// Represents a host global binding. + /// + internal class GlobalBinding : Binding + { + /// + /// Constructs a new global binding. + /// + /// The global import of the binding. + /// The field the import is bound to. + public GlobalBinding(GlobalImport import, FieldInfo field) + { + if (import is null) + { + throw new ArgumentNullException(nameof(import)); + } + + if (field is null) + { + throw new ArgumentNullException(nameof(field)); + } + + Import = import; + Field = field; + + Validate(); + } + + /// + /// The global import of the binding. + /// + public GlobalImport Import { get; private set; } + + /// + /// The field the import is bound to. + /// + public FieldInfo Field { get; private set; } + + public override SafeHandle Bind(Store store, IHost host) + { + unsafe + { + dynamic global = Field.GetValue(host); + if (!(global.Handle is null)) + { + throw new InvalidOperationException("Cannot bind more than once."); + } + + var v = Interop.ToValue((object)global.InitialValue, Import.Kind); + + var valueType = Interop.wasm_valtype_new(v.kind); + var valueTypeHandle = valueType.DangerousGetHandle(); + valueType.SetHandleAsInvalid(); + + using var globalType = Interop.wasm_globaltype_new( + valueTypeHandle, + Import.IsMutable ? Interop.wasm_mutability_t.WASM_VAR : Interop.wasm_mutability_t.WASM_CONST + ); + + var handle = Interop.wasm_global_new(store.Handle, globalType, &v); + global.Handle = handle; + return handle; + } + } + + private void Validate() + { + if (Field.IsStatic) + { + throw CreateBindingException(Import, Field, "field cannot be static"); + } + + if (!Field.IsInitOnly) + { + throw CreateBindingException(Import, Field, "field must be readonly"); + } + + if (!Field.FieldType.IsGenericType) + { + throw CreateBindingException(Import, Field, "field is expected to be of type 'Global'"); + } + + var definition = Field.FieldType.GetGenericTypeDefinition(); + if (definition == typeof(Global<>)) + { + if (Import.IsMutable) + { + throw CreateBindingException(Import, Field, "the import is mutable (use the 'MutableGlobal' type)"); + } + } + else if (definition == typeof(MutableGlobal<>)) + { + if (!Import.IsMutable) + { + throw CreateBindingException(Import, Field, "the import is constant (use the 'Global' type)"); + } + } + else + { + throw CreateBindingException(Import, Field, "field is expected to be of type 'Global' or 'MutableGlobal'"); + } + + var arg = Field.FieldType.GetGenericArguments()[0]; + + if (Interop.TryGetValueKind(arg, out var kind)) + { + if (!Interop.IsMatchingKind(kind, Import.Kind)) + { + throw CreateBindingException(Import, Field, $"global type argument is expected to be of type '{Interop.ToString(Import.Kind)}'"); + } + } + else + { + throw CreateBindingException(Import, Field, $"'{arg}' is not a valid global type"); + } + } + } +} diff --git a/crates/misc/dotnet/src/Bindings/MemoryBinding.cs b/crates/misc/dotnet/src/Bindings/MemoryBinding.cs new file mode 100644 index 0000000000..30e9f18cde --- /dev/null +++ b/crates/misc/dotnet/src/Bindings/MemoryBinding.cs @@ -0,0 +1,97 @@ +using System; +using System.Reflection; +using System.Runtime.InteropServices; +using Wasmtime.Imports; + +namespace Wasmtime.Bindings +{ + /// + /// Represents a host memory binding. + /// + internal class MemoryBinding : Binding + { + /// + /// Constructs a new memory binding. + /// + /// The memory import of the binding. + /// The field the import is bound to. + public MemoryBinding(MemoryImport import, FieldInfo field) + { + if (import is null) + { + throw new ArgumentNullException(nameof(import)); + } + + if (field is null) + { + throw new ArgumentNullException(nameof(field)); + } + + Import = import; + Field = field; + + Validate(); + } + + /// + /// The memory import of the binding. + /// + public MemoryImport Import { get; private set; } + + /// + /// The field the import is bound to. + /// + public FieldInfo Field { get; private set; } + + public override SafeHandle Bind(Store store, IHost host) + { + Memory memory = (Memory)Field.GetValue(host); + if (!(memory.Handle is null)) + { + throw new InvalidOperationException("Cannot bind more than once."); + } + + uint min = memory.Minimum; + uint max = memory.Maximum; + + if (min != Import.Minimum) + { + throw CreateBindingException(Import, Field, $"Memory does not have the expected minimum of {Import.Minimum} page(s)"); + } + if (max != Import.Maximum) + { + throw CreateBindingException(Import, Field, $"Memory does not have the expected maximum of {Import.Maximum} page(s)"); + } + + unsafe + { + Interop.wasm_limits_t limits = new Interop.wasm_limits_t(); + limits.min = min; + limits.max = max; + + using var memoryType = Interop.wasm_memorytype_new(&limits); + var handle = Interop.wasm_memory_new(store.Handle, memoryType); + memory.Handle = handle; + return handle; + } + } + + private void Validate() + { + if (Field.IsStatic) + { + throw CreateBindingException(Import, Field, "field cannot be static"); + } + + if (!Field.IsInitOnly) + { + throw CreateBindingException(Import, Field, "field must be readonly"); + } + + if (Field.FieldType != typeof(Memory)) + { + throw CreateBindingException(Import, Field, "field is expected to be of type 'Memory'"); + } + } + } +} diff --git a/crates/misc/dotnet/src/Bindings/WasiBinding.cs b/crates/misc/dotnet/src/Bindings/WasiBinding.cs new file mode 100644 index 0000000000..4541f808c7 --- /dev/null +++ b/crates/misc/dotnet/src/Bindings/WasiBinding.cs @@ -0,0 +1,23 @@ +using System; +using System.Runtime.InteropServices; + +namespace Wasmtime.Bindings +{ + /// + /// Represents a binding to a WASI export. + /// + internal class WasiBinding : Binding + { + public WasiBinding(IntPtr handle) + { + _handle = handle; + } + + public override SafeHandle Bind(Store store, IHost host) + { + return new Interop.WasiExportHandle(_handle); + } + + private IntPtr _handle; + } +} diff --git a/crates/misc/dotnet/src/Engine.cs b/crates/misc/dotnet/src/Engine.cs new file mode 100644 index 0000000000..575e94e9bd --- /dev/null +++ b/crates/misc/dotnet/src/Engine.cs @@ -0,0 +1,55 @@ +using System; + +namespace Wasmtime +{ + /// + /// Represents the Wasmtime engine. + /// + public class Engine : IDisposable + { + /// + /// Constructs a new . + /// + public Engine() + { + Handle = Interop.wasm_engine_new(); + + if (Handle.IsInvalid) + { + throw new WasmtimeException("Failed to create Wasmtime engine."); + } + } + + internal Engine(Interop.WasmConfigHandle config) + { + Handle = Interop.wasm_engine_new_with_config(config); + config.SetHandleAsInvalid(); + + if (Handle.IsInvalid) + { + throw new WasmtimeException("Failed to create Wasmtime engine."); + } + } + + /// + /// Creates a new Wasmtime . + /// + /// Returns the new . + public Store CreateStore() + { + return new Store(this); + } + + /// + public void Dispose() + { + if (!Handle.IsInvalid) + { + Handle.Dispose(); + Handle.SetHandleAsInvalid(); + } + } + + internal Interop.EngineHandle Handle { get; private set; } + } +} diff --git a/crates/misc/dotnet/src/EngineBuilder.cs b/crates/misc/dotnet/src/EngineBuilder.cs new file mode 100644 index 0000000000..c9983c9b21 --- /dev/null +++ b/crates/misc/dotnet/src/EngineBuilder.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Wasmtime +{ + /// + /// Represents the Wasmtime compiler strategy. + /// + public enum CompilerStrategy + { + /// + /// Automatically pick the compiler strategy. + /// + Auto, + /// + /// Use the Cranelift compiler. + /// + Cranelift, + /// + /// Use the Lightbeam compiler. + /// + Lightbeam + } + + /// + /// Represents the Wasmtime optimization level. + /// + public enum OptimizationLevel + { + /// + /// Disable optimizations. + /// + None, + /// + /// Optimize for speed. + /// + Speed, + /// + /// Optimize for speed and size. + /// + SpeedAndSize + } + + /// + /// Represents a builder of instances. + /// + public class EngineBuilder + { + /// + /// Constructs a new . + /// + public EngineBuilder() + { + } + + /// + /// Sets whether or not to enable debug information. + /// + /// True to enable debug information or false to disable. + /// Returns the current builder. + public EngineBuilder WithDebugInfo(bool enable) + { + _enableDebugInfo = enable; + return this; + } + + /// + /// Sets whether or not enable WebAssembly threads support. + /// + /// True to enable WebAssembly threads support or false to disable. + /// Returns the current builder. + public EngineBuilder WithWasmThreads(bool enable) + { + _enableWasmThreads = enable; + return this; + } + + /// + /// Sets whether or not enable WebAssembly reference types support. + /// + /// True to enable WebAssembly reference types support or false to disable. + /// Returns the current builder. + public EngineBuilder WithReferenceTypes(bool enable) + { + _enableReferenceTypes = enable; + return this; + } + + /// + /// Sets whether or not enable WebAssembly SIMD support. + /// + /// True to enable WebAssembly SIMD support or false to disable. + /// Returns the current builder. + public EngineBuilder WithSIMD(bool enable) + { + _enableSIMD = enable; + return this; + } + + /// + /// Sets whether or not enable WebAssembly multi-value support. + /// + /// True to enable WebAssembly multi-value support or false to disable. + /// Returns the current builder. + public EngineBuilder WithMultiValue(bool enable) + { + _enableMultiValue = enable; + return this; + } + + /// + /// Sets whether or not enable WebAssembly bulk memory support. + /// + /// True to enable WebAssembly bulk memory support or false to disable. + /// Returns the current builder. + public EngineBuilder WithBulkMemory(bool enable) + { + _enableBulkMemory = enable; + return this; + } + + /// + /// Sets the compiler strategy to use. + /// + /// The compiler strategy to use. + /// Returns the current builder. + public EngineBuilder WithCompilerStrategy(CompilerStrategy strategy) + { + switch (strategy) + { + case CompilerStrategy.Auto: + _strategy = Interop.wasmtime_strategy_t.WASMTIME_STRATEGY_AUTO; + break; + + case CompilerStrategy.Cranelift: + _strategy = Interop.wasmtime_strategy_t.WASMTIME_STRATEGY_CRANELIFT; + break; + + case CompilerStrategy.Lightbeam: + _strategy = Interop.wasmtime_strategy_t.WASMTIME_STRATEGY_LIGHTBEAM; + break; + + default: + throw new ArgumentOutOfRangeException(nameof(strategy)); + } + return this; + } + + /// + /// Sets whether or not enable the Cranelift debug verifier. + /// + /// True to enable the Cranelift debug verifier or false to disable. + /// Returns the current builder. + public EngineBuilder WithCraneliftDebugVerifier(bool enable) + { + _enableCraneliftDebugVerifier = enable; + return this; + } + + /// + /// Sets the optimization level to use. + /// + /// The optimization level to use. + /// Returns the current builder. + public EngineBuilder WithOptimizationLevel(OptimizationLevel level) + { + switch (level) + { + case OptimizationLevel.None: + _optLevel = Interop.wasmtime_opt_level_t.WASMTIME_OPT_LEVEL_NONE; + break; + + case OptimizationLevel.Speed: + _optLevel = Interop.wasmtime_opt_level_t.WASMTIME_OPT_LEVEL_SPEED; + break; + + case OptimizationLevel.SpeedAndSize: + _optLevel = Interop.wasmtime_opt_level_t.WASMTIME_OPT_LEVEL_SPEED_AND_SIZE; + break; + + default: + throw new ArgumentOutOfRangeException(nameof(level)); + } + return this; + } + + /// + /// Builds the instance. + /// + /// Returns the new instance. + public Engine Build() + { + var config = Interop.wasm_config_new(); + + if (_enableDebugInfo.HasValue) + { + Interop.wasmtime_config_debug_info_set(config, _enableDebugInfo.Value); + } + + if (_enableWasmThreads.HasValue) + { + Interop.wasmtime_config_wasm_threads_set(config, _enableWasmThreads.Value); + } + + if (_enableReferenceTypes.HasValue) + { + Interop.wasmtime_config_wasm_reference_types_set(config, _enableReferenceTypes.Value); + } + + if (_enableSIMD.HasValue) + { + Interop.wasmtime_config_wasm_simd_set(config, _enableSIMD.Value); + } + + if (_enableBulkMemory.HasValue) + { + Interop.wasmtime_config_wasm_bulk_memory_set(config, _enableBulkMemory.Value); + } + + if (_enableMultiValue.HasValue) + { + Interop.wasmtime_config_wasm_multi_value_set(config, _enableMultiValue.Value); + } + + if (_strategy.HasValue) + { + Interop.wasmtime_config_strategy_set(config, _strategy.Value); + } + + if (_enableCraneliftDebugVerifier.HasValue) + { + Interop.wasmtime_config_cranelift_debug_verifier_set(config, _enableCraneliftDebugVerifier.Value); + } + + if (_optLevel.HasValue) + { + Interop.wasmtime_config_cranelift_opt_level_set(config, _optLevel.Value); + } + + return new Engine(config); + } + + private bool? _enableDebugInfo; + private bool? _enableWasmThreads; + private bool? _enableReferenceTypes; + private bool? _enableSIMD; + private bool? _enableBulkMemory; + private bool? _enableMultiValue; + private Interop.wasmtime_strategy_t? _strategy; + private bool? _enableCraneliftDebugVerifier; + private Interop.wasmtime_opt_level_t? _optLevel; + } +} diff --git a/crates/misc/dotnet/src/Exports/Export.cs b/crates/misc/dotnet/src/Exports/Export.cs new file mode 100644 index 0000000000..047ad7025e --- /dev/null +++ b/crates/misc/dotnet/src/Exports/Export.cs @@ -0,0 +1,31 @@ +using System; +using System.Runtime.InteropServices; + +namespace Wasmtime.Exports +{ + /// + /// Represents an export of a WebAssembly module. + /// + public abstract class Export + { + internal Export(IntPtr exportType) + { + unsafe + { + var name = Interop.wasm_exporttype_name(exportType); + Name = Marshal.PtrToStringUTF8((IntPtr)name->data, (int)name->size); + } + } + + /// + /// The name of the export. + /// + public string Name { get; private set; } + + /// + public override string ToString() + { + return Name; + } + } +} diff --git a/crates/misc/dotnet/src/Exports/Exports.cs b/crates/misc/dotnet/src/Exports/Exports.cs new file mode 100644 index 0000000000..52d50c3c48 --- /dev/null +++ b/crates/misc/dotnet/src/Exports/Exports.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; + +namespace Wasmtime.Exports +{ + /// + /// Represents the exports of a WebAssembly module. + /// + public class Exports + { + internal Exports(Module module) + { + Interop.wasm_exporttype_vec_t exports; + Interop.wasm_module_exports(module.Handle, out exports); + + try + { + var all = new List((int)exports.size); + var functions = new List(); + var globals = new List(); + var tables = new List(); + var memories = new List(); + + for (int i = 0; i < (int)exports.size; ++i) + { + unsafe + { + var exportType = exports.data[i]; + var externType = Interop.wasm_exporttype_type(exportType); + + switch (Interop.wasm_externtype_kind(externType)) + { + case Interop.wasm_externkind_t.WASM_EXTERN_FUNC: + var function = new FunctionExport(exportType, externType); + functions.Add(function); + all.Add(function); + break; + + case Interop.wasm_externkind_t.WASM_EXTERN_GLOBAL: + var global = new GlobalExport(exportType, externType); + globals.Add(global); + all.Add(global); + break; + + case Interop.wasm_externkind_t.WASM_EXTERN_TABLE: + var table = new TableExport(exportType, externType); + tables.Add(table); + all.Add(table); + break; + + case Interop.wasm_externkind_t.WASM_EXTERN_MEMORY: + var memory = new MemoryExport(exportType, externType); + memories.Add(memory); + all.Add(memory); + break; + + default: + throw new NotSupportedException("Unsupported export extern type."); + } + } + } + + Functions = functions; + Globals = globals; + Tables = tables; + Memories = memories; + All = all; + } + finally + { + Interop.wasm_exporttype_vec_delete(ref exports); + } + } + + /// + /// The exported functions of a WebAssembly module. + /// + public IReadOnlyList Functions { get; private set; } + + /// + /// The exported globals of a WebAssembly module. + /// + public IReadOnlyList Globals { get; private set; } + + /// + /// The exported tables of a WebAssembly module. + /// + public IReadOnlyList Tables { get; private set; } + + /// + /// The exported memories of a WebAssembly module. + /// + public IReadOnlyList Memories { get; private set; } + + internal List All { get; private set; } + } +} diff --git a/crates/misc/dotnet/src/Exports/FunctionExport.cs b/crates/misc/dotnet/src/Exports/FunctionExport.cs new file mode 100644 index 0000000000..1b57c65cb3 --- /dev/null +++ b/crates/misc/dotnet/src/Exports/FunctionExport.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Wasmtime.Exports +{ + /// + /// Represents a function exported from a WebAssembly module. + /// + public class FunctionExport : Export + { + internal FunctionExport(IntPtr exportType, IntPtr externType) : base(exportType) + { + Debug.Assert(Interop.wasm_externtype_kind(externType) == Interop.wasm_externkind_t.WASM_EXTERN_FUNC); + + unsafe + { + var funcType = Interop.wasm_externtype_as_functype_const(externType); + Parameters = Interop.ToValueKindList(Interop.wasm_functype_params(funcType)); + Results = Interop.ToValueKindList(Interop.wasm_functype_results(funcType)); + } + } + + /// + /// The parameter of the exported WebAssembly function. + /// + public IReadOnlyList Parameters { get; private set; } + + /// + /// The results of the exported WebAssembly function. + /// + public IReadOnlyList Results { get; private set; } + } +} diff --git a/crates/misc/dotnet/src/Exports/GlobalExport.cs b/crates/misc/dotnet/src/Exports/GlobalExport.cs new file mode 100644 index 0000000000..3dc76be560 --- /dev/null +++ b/crates/misc/dotnet/src/Exports/GlobalExport.cs @@ -0,0 +1,30 @@ +using System; +using System.Diagnostics; + +namespace Wasmtime.Exports +{ + /// + /// Represents a global variable exported from a WebAssembly module. + /// + public class GlobalExport : Export + { + internal GlobalExport(IntPtr exportType, IntPtr externType) : base(exportType) + { + Debug.Assert(Interop.wasm_externtype_kind(externType) == Interop.wasm_externkind_t.WASM_EXTERN_GLOBAL); + + var globalType = Interop.wasm_externtype_as_globaltype_const(externType); + Kind = Interop.wasm_valtype_kind(Interop.wasm_globaltype_content(globalType)); + IsMutable = Interop.wasm_globaltype_mutability(globalType) == Interop.wasm_mutability_t.WASM_VAR; + } + + /// + /// The kind of value for the global variable. + /// + public ValueKind Kind { get; private set; } + + /// + /// Determines whether or not the global variable is mutable. + /// + public bool IsMutable { get; private set; } + } +} diff --git a/crates/misc/dotnet/src/Exports/MemoryExport.cs b/crates/misc/dotnet/src/Exports/MemoryExport.cs new file mode 100644 index 0000000000..f5f200e39a --- /dev/null +++ b/crates/misc/dotnet/src/Exports/MemoryExport.cs @@ -0,0 +1,35 @@ +using System; +using System.Diagnostics; + +namespace Wasmtime.Exports +{ + /// + /// Represents a memory exported from a WebAssembly module. + /// + public class MemoryExport : Export + { + internal MemoryExport(IntPtr exportType, IntPtr externType) : base(exportType) + { + Debug.Assert(Interop.wasm_externtype_kind(externType) == Interop.wasm_externkind_t.WASM_EXTERN_MEMORY); + + var memoryType = Interop.wasm_externtype_as_memorytype_const(externType); + + unsafe + { + var limits = Interop.wasm_memorytype_limits(memoryType); + Minimum = limits->min; + Maximum = limits->max; + } + } + + /// + /// The minimum memory size (in WebAssembly page units). + /// + public uint Minimum { get; private set; } + + /// + /// The maximum memory size (in WebAssembly page units). + /// + public uint Maximum { get; private set; } + } +} diff --git a/crates/misc/dotnet/src/Exports/TableExport.cs b/crates/misc/dotnet/src/Exports/TableExport.cs new file mode 100644 index 0000000000..1643aee68d --- /dev/null +++ b/crates/misc/dotnet/src/Exports/TableExport.cs @@ -0,0 +1,42 @@ +using System; +using System.Diagnostics; + +namespace Wasmtime.Exports +{ + /// + /// Represents a table exported from a WebAssembly module. + /// + public class TableExport : Export + { + internal TableExport(IntPtr exportType, IntPtr externType) : base(exportType) + { + Debug.Assert(Interop.wasm_externtype_kind(externType) == Interop.wasm_externkind_t.WASM_EXTERN_TABLE); + + var tableType = Interop.wasm_externtype_as_tabletype_const(externType); + + Kind = Interop.wasm_valtype_kind(Interop.wasm_tabletype_element(tableType)); + + unsafe + { + var limits = Interop.wasm_tabletype_limits(tableType); + Minimum = limits->min; + Maximum = limits->max; + } + } + + /// + /// The value kind of the table. + /// + public ValueKind Kind { get; private set; } + + /// + /// The minimum number of elements in the table. + /// + public uint Minimum { get; private set; } + + /// + /// The maximum number of elements in the table. + /// + public uint Maximum { get; private set; } + } +} diff --git a/crates/misc/dotnet/src/Externs/ExternFunction.cs b/crates/misc/dotnet/src/Externs/ExternFunction.cs new file mode 100644 index 0000000000..462f3b9ffe --- /dev/null +++ b/crates/misc/dotnet/src/Externs/ExternFunction.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using Wasmtime.Exports; + +namespace Wasmtime.Externs +{ + /// + /// Represents an external (instantiated) WebAssembly function. + /// + public class ExternFunction + { + internal ExternFunction(FunctionExport export, IntPtr func) + { + _export = export; + _func = func; + } + + /// + /// The name of the WebAssembly function. + /// + public string Name => _export.Name; + + /// + /// The parameters of the WebAssembly function. + /// + public IReadOnlyList Parameters => _export.Parameters; + + /// + /// The results of the WebAssembly function. + /// + public IReadOnlyList Results => _export.Results; + + /// + /// Invokes the WebAssembly function. + /// + /// The array of arguments to pass to the function. + /// + /// Returns null if the function has no return value. + /// Returns the value if the function returns a single value. + /// Returns an array of values if the function returns more than one value. + /// + public object Invoke(params object[] arguments) + { + if (arguments.Length != Parameters.Count) + { + throw new WasmtimeException($"Argument mismatch when invoking function '{Name}': requires {Parameters.Count} but was given {arguments.Length}."); + } + + unsafe + { + Interop.wasm_val_t* args = stackalloc Interop.wasm_val_t[Parameters.Count]; + Interop.wasm_val_t* results = stackalloc Interop.wasm_val_t[Results.Count]; + + for (int i = 0; i < arguments.Length; ++i) + { + args[i] = Interop.ToValue(arguments[i], Parameters[i]); + } + + var trap = Interop.wasm_func_call(_func, args, results); + if (trap != IntPtr.Zero) + { + throw TrapException.FromOwnedTrap(trap); + } + + if (Results.Count == 0) + { + return null; + } + + if (Results.Count == 1) + { + return Interop.ToObject(&results[0]); + } + + var ret = new object[Results.Count]; + for (int i = 0; i < Results.Count; ++i) + { + ret[i] = Interop.ToObject(&results[i]); + } + return ret; + } + } + + private FunctionExport _export; + private IntPtr _func; + } +} diff --git a/crates/misc/dotnet/src/Externs/ExternGlobal.cs b/crates/misc/dotnet/src/Externs/ExternGlobal.cs new file mode 100644 index 0000000000..087260058c --- /dev/null +++ b/crates/misc/dotnet/src/Externs/ExternGlobal.cs @@ -0,0 +1,62 @@ +using System; +using Wasmtime.Exports; + +namespace Wasmtime.Externs +{ + /// + /// Represents an external (instantiated) WebAssembly global. + /// + public class ExternGlobal + { + internal ExternGlobal(GlobalExport export, IntPtr global) + { + _export = export; + _global = global; + } + + /// + /// The name of the WebAssembly global. + /// + public string Name => _export.Name; + + /// + /// The kind of value for the global variable. + /// + public ValueKind Kind => _export.Kind; + + /// + /// Determines whether or not the global variable is mutable. + /// + public bool IsMutable => _export.IsMutable; + + public object Value + { + get + { + unsafe + { + var v = stackalloc Interop.wasm_val_t[1]; + Interop.wasm_global_get(_global, v); + return Interop.ToObject(v); + } + } + set + { + if (!IsMutable) + { + throw new InvalidOperationException($"The value of global '{Name}' cannot be modified."); + } + + var v = Interop.ToValue(value, Kind); + + unsafe + { + Interop.wasm_global_set(_global, &v); + } + } + } + + private GlobalExport _export; + private IntPtr _global; + } +} diff --git a/crates/misc/dotnet/src/Externs/ExternMemory.cs b/crates/misc/dotnet/src/Externs/ExternMemory.cs new file mode 100644 index 0000000000..e86be4f341 --- /dev/null +++ b/crates/misc/dotnet/src/Externs/ExternMemory.cs @@ -0,0 +1,262 @@ +using System; +using System.Buffers.Binary; +using System.Text; +using Wasmtime.Exports; + +namespace Wasmtime.Externs +{ + /// + /// Represents an external (instantiated) WebAssembly memory. + /// + public class ExternMemory + { + internal ExternMemory(MemoryExport export, IntPtr memory) + { + _export = export; + _memory = memory; + } + + /// + /// The name of the WebAssembly memory. + /// + public string Name => _export.Name; + + /// + /// The minimum memory size (in WebAssembly page units). + /// + public uint Minimum => _export.Minimum; + + /// + /// The maximum memory size (in WebAssembly page units). + /// + public uint Maximum => _export.Maximum; + + /// + /// The span of the memory. + /// + /// + /// The span may become invalid if the memory grows. + /// + /// This may happen if the memory is explicitly requested to grow or + /// grows as a result of WebAssembly execution. + /// + /// Therefore, the returned Span should not be stored. + /// + public unsafe Span Span + { + get + { + var data = Interop.wasm_memory_data(_memory); + var size = Convert.ToInt32(Interop.wasm_memory_data_size(_memory).ToUInt32()); + return new Span(data, size); + } + } + + /// + /// Reads a UTF-8 string from memory. + /// + /// The zero-based address to read from. + /// The length of bytes to read. + /// Returns the string read from memory. + public string ReadString(int address, int length) + { + return Encoding.UTF8.GetString(Span.Slice(address, length)); + } + + /// + /// Reads a null-terminated UTF-8 string from memory. + /// + /// The zero-based address to read from. + /// Returns the string read from memory. + public string ReadNullTerminatedString(int address) + { + var slice = Span.Slice(address); + var terminator = slice.IndexOf((byte)0); + if (terminator == -1) + { + throw new InvalidOperationException("string is not null terminated"); + } + + return Encoding.UTF8.GetString(slice.Slice(0, terminator)); + } + + /// + /// Writes a UTF-8 string at the given address. + /// + /// The zero-based address to write to. + /// The string to write. + /// Returns the number of bytes written. + public int WriteString(int address, string value) + { + return Encoding.UTF8.GetBytes(value, Span.Slice(address)); + } + + /// + /// Reads a byte from memory. + /// + /// The zero-based address to read from. + /// Returns the byte read from memory. + public byte ReadByte(int address) + { + return Span[address]; + } + + /// + /// Writes a byte to memory. + /// + /// The zero-based address to write to. + /// The byte to write. + public void WriteByte(int address, byte value) + { + Span[address] = value; + } + + /// + /// Reads a short from memory. + /// + /// The zero-based address to read from. + /// Returns the short read from memory. + public short ReadInt16(int address) + { + return BinaryPrimitives.ReadInt16LittleEndian(Span.Slice(address, 2)); + } + + /// + /// Writes a short to memory. + /// + /// The zero-based address to write to. + /// The short to write. + public void WriteInt16(int address, short value) + { + BinaryPrimitives.WriteInt16LittleEndian(Span.Slice(address, 2), value); + } + + /// + /// Reads an int from memory. + /// + /// The zero-based address to read from. + /// Returns the int read from memory. + public int ReadInt32(int address) + { + return BinaryPrimitives.ReadInt32LittleEndian(Span.Slice(address, 4)); + } + + /// + /// Writes an int to memory. + /// + /// The zero-based address to write to. + /// The int to write. + public void WriteInt32(int address, int value) + { + BinaryPrimitives.WriteInt32LittleEndian(Span.Slice(address, 4), value); + } + + /// + /// Reads a long from memory. + /// + /// The zero-based address to read from. + /// Returns the long read from memory. + public long ReadInt64(int address) + { + return BinaryPrimitives.ReadInt64LittleEndian(Span.Slice(address, 8)); + } + + /// + /// Writes a long to memory. + /// + /// The zero-based address to write to. + /// The long to write. + public void WriteInt64(int address, long value) + { + BinaryPrimitives.WriteInt64LittleEndian(Span.Slice(address, 8), value); + } + + /// + /// Reads an IntPtr from memory. + /// + /// The zero-based address to read from. + /// Returns the IntPtr read from memory. + public IntPtr ReadIntPtr(int address) + { + if (IntPtr.Size == 4) + { + return (IntPtr)ReadInt32(address); + } + return (IntPtr)ReadInt64(address); + } + + /// + /// Writes an IntPtr to memory. + /// + /// The zero-based address to write to. + /// The IntPtr to write. + public void WriteIntPtr(int address, IntPtr value) + { + if (IntPtr.Size == 4) + { + WriteInt32(address, value.ToInt32()); + } + else + { + WriteInt64(address, value.ToInt64()); + } + } + + /// + /// Reads a long from memory. + /// + /// The zero-based address to read from. + /// Returns the long read from memory. + public float ReadSingle(int address) + { + unsafe + { + var i = ReadInt32(address); + return *((float*)&i); + } + } + + /// + /// Writes a single to memory. + /// + /// The zero-based address to write to. + /// The single to write. + public void WriteSingle(int address, float value) + { + unsafe + { + WriteInt32(address, *(int*)&value); + } + } + + /// + /// Reads a double from memory. + /// + /// The zero-based address to read from. + /// Returns the double read from memory. + public double ReadDouble(int address) + { + unsafe + { + var i = ReadInt64(address); + return *((double*)&i); + } + } + + /// + /// Writes a double to memory. + /// + /// The zero-based address to write to. + /// The double to write. + public void WriteDouble(int address, double value) + { + unsafe + { + WriteInt64(address, *(long*)&value); + } + } + + private MemoryExport _export; + private IntPtr _memory; + } +} diff --git a/crates/misc/dotnet/src/Externs/Externs.cs b/crates/misc/dotnet/src/Externs/Externs.cs new file mode 100644 index 0000000000..078f179180 --- /dev/null +++ b/crates/misc/dotnet/src/Externs/Externs.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using Wasmtime.Exports; + +namespace Wasmtime.Externs +{ + /// + /// Represents external (instantiated) WebAssembly functions, globals, tables, and memories. + /// + public class Externs + { + internal Externs(Wasmtime.Exports.Exports exports, Interop.wasm_extern_vec_t externs) + { + var functions = new List(); + var globals = new List(); + var memories = new List(); + + for (int i = 0; i < (int)externs.size; ++i) + { + unsafe + { + var ext = externs.data[i]; + + switch (Interop.wasm_extern_kind(ext)) + { + case Interop.wasm_externkind_t.WASM_EXTERN_FUNC: + var function = new ExternFunction((FunctionExport)exports.All[i], Interop.wasm_extern_as_func(ext)); + functions.Add(function); + break; + + case Interop.wasm_externkind_t.WASM_EXTERN_GLOBAL: + var global = new ExternGlobal((GlobalExport)exports.All[i], Interop.wasm_extern_as_global(ext)); + globals.Add(global); + break; + + case Interop.wasm_externkind_t.WASM_EXTERN_MEMORY: + var memory = new ExternMemory((MemoryExport)exports.All[i], Interop.wasm_extern_as_memory(ext)); + memories.Add(memory); + break; + + default: + throw new NotSupportedException("Unsupported extern type."); + } + } + } + + Functions = functions; + Globals = globals; + Memories = memories; + } + + /// + /// The extern functions from an instantiated WebAssembly module. + /// + public IReadOnlyList Functions { get; private set; } + + /// + /// The extern globals from an instantiated WebAssembly module. + /// + public IReadOnlyList Globals { get; private set; } + + /// + /// The extern memories from an instantiated WebAssembly module. + /// + public IReadOnlyList Memories { get; private set; } + } +} diff --git a/crates/misc/dotnet/src/Global.cs b/crates/misc/dotnet/src/Global.cs new file mode 100644 index 0000000000..13ca0e285e --- /dev/null +++ b/crates/misc/dotnet/src/Global.cs @@ -0,0 +1,50 @@ +using System; + +namespace Wasmtime +{ + /// + /// Represents a constant WebAssembly global value. + /// + public class Global + { + /// + /// Creates a new with the given initial value. + /// + /// The initial value of the global. + public Global(T initialValue) + { + InitialValue = initialValue; + Kind = Interop.ToValueKind(typeof(T)); + } + + /// + /// The value of the global. + /// + public T Value + { + get + { + if (Handle is null) + { + throw new InvalidOperationException("The global cannot be used before it is bound to a module instance."); + } + + unsafe + { + var v = stackalloc Interop.wasm_val_t[1]; + + Interop.wasm_global_get(Handle.DangerousGetHandle(), v); + + // TODO: figure out a way that doesn't box the value + return (T)Interop.ToObject(v); + } + } + } + + internal ValueKind Kind { get; private set; } + + internal Interop.GlobalHandle Handle { get; set; } + + internal T InitialValue { get; private set; } + } +} diff --git a/crates/misc/dotnet/src/IHost.cs b/crates/misc/dotnet/src/IHost.cs new file mode 100644 index 0000000000..1734c2d9f1 --- /dev/null +++ b/crates/misc/dotnet/src/IHost.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using Wasmtime.Bindings; + +namespace Wasmtime +{ + /// + /// The interface implemented by Wasmtime hosts. + /// + public interface IHost + { + /// + /// The that the host is bound to. + /// + /// A host can only bind to one module instance at a time. + Instance Instance { get; set; } + } +} diff --git a/crates/misc/dotnet/src/ImportAttribute.cs b/crates/misc/dotnet/src/ImportAttribute.cs new file mode 100644 index 0000000000..ff06fc4cb3 --- /dev/null +++ b/crates/misc/dotnet/src/ImportAttribute.cs @@ -0,0 +1,31 @@ +using System; + +namespace Wasmtime +{ + /// + /// Used to mark .NET methods and fields as imports to a WebAssembly module. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Field)] + public class ImportAttribute : Attribute + { + /// + /// Constructs a new . + /// + /// The name of the import. + public ImportAttribute(string name) + { + Name = name; + } + + /// + /// The name of the import. + /// + public string Name { get; set; } + + /// + /// The module name of the import. + /// + /// A null or empty module name implies that the import is not scoped to a module. + public string Module { get; set; } + } +} diff --git a/crates/misc/dotnet/src/Imports/FunctionImport.cs b/crates/misc/dotnet/src/Imports/FunctionImport.cs new file mode 100644 index 0000000000..0d963a657e --- /dev/null +++ b/crates/misc/dotnet/src/Imports/FunctionImport.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Wasmtime.Imports +{ + /// + /// Represents a function imported to a WebAssembly module. + /// + public class FunctionImport : Import + { + internal FunctionImport(IntPtr importType, IntPtr externType) : base(importType) + { + Debug.Assert(Interop.wasm_externtype_kind(externType) == Interop.wasm_externkind_t.WASM_EXTERN_FUNC); + + unsafe + { + var funcType = Interop.wasm_externtype_as_functype_const(externType); + Parameters = Interop.ToValueKindList(Interop.wasm_functype_params(funcType)); + Results = Interop.ToValueKindList(Interop.wasm_functype_results(funcType)); + } + } + + /// + /// The parameters of the imported function. + /// + public IReadOnlyList Parameters { get; private set; } + + /// + /// The results of the imported function. + /// + public IReadOnlyList Results { get; private set; } + } +} diff --git a/crates/misc/dotnet/src/Imports/GlobalImport.cs b/crates/misc/dotnet/src/Imports/GlobalImport.cs new file mode 100644 index 0000000000..0f92b40f19 --- /dev/null +++ b/crates/misc/dotnet/src/Imports/GlobalImport.cs @@ -0,0 +1,30 @@ +using System; +using System.Diagnostics; + +namespace Wasmtime.Imports +{ + /// + /// Represents a global variable imported to a WebAssembly module. + /// + public class GlobalImport : Import + { + internal GlobalImport(IntPtr importType, IntPtr externType) : base(importType) + { + Debug.Assert(Interop.wasm_externtype_kind(externType) == Interop.wasm_externkind_t.WASM_EXTERN_GLOBAL); + + var globalType = Interop.wasm_externtype_as_globaltype_const(externType); + Kind = Interop.wasm_valtype_kind(Interop.wasm_globaltype_content(globalType)); + IsMutable = Interop.wasm_globaltype_mutability(globalType) == Interop.wasm_mutability_t.WASM_VAR; + } + + /// + /// The kind of value for the global variable. + /// + public ValueKind Kind { get; private set; } + + /// + /// Determines whether or not the global variable is mutable. + /// + public bool IsMutable { get; private set; } + } +} diff --git a/crates/misc/dotnet/src/Imports/Import.cs b/crates/misc/dotnet/src/Imports/Import.cs new file mode 100644 index 0000000000..09cf354fd0 --- /dev/null +++ b/crates/misc/dotnet/src/Imports/Import.cs @@ -0,0 +1,43 @@ +using System; +using System.Runtime.InteropServices; + +namespace Wasmtime.Imports +{ + /// + /// The base class for import types. + /// + public abstract class Import + { + internal Import(IntPtr importType) + { + unsafe + { + Handle = importType; + + var moduleName = Interop.wasm_importtype_module(Handle); + ModuleName = Marshal.PtrToStringUTF8((IntPtr)moduleName->data, (int)moduleName->size); + + var name = Interop.wasm_importtype_name(Handle); + Name = Marshal.PtrToStringUTF8((IntPtr)name->data, (int)name->size); + } + } + + /// + /// The module name of the import. + /// + public string ModuleName { get; private set; } + + /// + /// The name of the import. + /// + public string Name { get; private set; } + + internal IntPtr Handle { get; private set; } + + /// + public override string ToString() + { + return $"{ModuleName}{(string.IsNullOrEmpty(ModuleName) ? "" : ".")}{Name}"; + } + } +} diff --git a/crates/misc/dotnet/src/Imports/Imports.cs b/crates/misc/dotnet/src/Imports/Imports.cs new file mode 100644 index 0000000000..bda7796e58 --- /dev/null +++ b/crates/misc/dotnet/src/Imports/Imports.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; + +namespace Wasmtime.Imports +{ + /// + /// Represents imported functions, globals, tables, and memories to a WebAssembly module. + /// + public class Imports : IDisposable + { + internal Imports(Module module) + { + Interop.wasm_importtype_vec_t imports; + Interop.wasm_module_imports(module.Handle, out imports); + + var all = new List((int)imports.size); + var functions = new List(); + var globals = new List(); + var tables = new List(); + var memories = new List(); + + for (int i = 0; i < (int)imports.size; ++i) + { + unsafe + { + var importType = imports.data[i]; + var externType = Interop.wasm_importtype_type(importType); + + switch (Interop.wasm_externtype_kind(externType)) + { + case Interop.wasm_externkind_t.WASM_EXTERN_FUNC: + var function = new FunctionImport(importType, externType); + functions.Add(function); + all.Add(function); + break; + + case Interop.wasm_externkind_t.WASM_EXTERN_GLOBAL: + var global = new GlobalImport(importType, externType); + globals.Add(global); + all.Add(global); + break; + + case Interop.wasm_externkind_t.WASM_EXTERN_TABLE: + var table = new TableImport(importType, externType); + tables.Add(table); + all.Add(table); + break; + + case Interop.wasm_externkind_t.WASM_EXTERN_MEMORY: + var memory = new MemoryImport(importType, externType); + memories.Add(memory); + all.Add(memory); + break; + + default: + throw new NotSupportedException("Unsupported import extern type."); + } + } + } + + Functions = functions; + Globals = globals; + Tables = tables; + Memories = memories; + All = all; + } + + /// + public unsafe void Dispose() + { + if (!(_imports.data is null)) + { + Interop.wasm_importtype_vec_delete(ref _imports); + _imports.data = null; + } + } + + /// + /// The imported functions required by a WebAssembly module. + /// + public IReadOnlyList Functions { get; private set; } + + /// + /// The imported globals required by a WebAssembly module. + /// + public IReadOnlyList Globals { get; private set; } + + /// + /// The imported tables required by a WebAssembly module. + /// + public IReadOnlyList Tables { get; private set; } + + /// + /// The imported memories required by a WebAssembly module. + /// + public IReadOnlyList Memories { get; private set; } + + internal IReadOnlyList All { get; private set; } + + private Interop.wasm_importtype_vec_t _imports; + } +} diff --git a/crates/misc/dotnet/src/Imports/MemoryImport.cs b/crates/misc/dotnet/src/Imports/MemoryImport.cs new file mode 100644 index 0000000000..3b6769c888 --- /dev/null +++ b/crates/misc/dotnet/src/Imports/MemoryImport.cs @@ -0,0 +1,35 @@ +using System; +using System.Diagnostics; + +namespace Wasmtime.Imports +{ + /// + /// Represents a memory imported to a WebAssembly module. + /// + public class MemoryImport : Import + { + internal MemoryImport(IntPtr importType, IntPtr externType) : base(importType) + { + Debug.Assert(Interop.wasm_externtype_kind(externType) == Interop.wasm_externkind_t.WASM_EXTERN_MEMORY); + + var memoryType = Interop.wasm_externtype_as_memorytype_const(externType); + + unsafe + { + var limits = Interop.wasm_memorytype_limits(memoryType); + Minimum = limits->min; + Maximum = limits->max; + } + } + + /// + /// The minimum memory size (in WebAssembly page units). + /// + public uint Minimum { get; private set; } + + /// + /// The maximum memory size (in WebAssembly page units). + /// + public uint Maximum { get; private set; } + } +} diff --git a/crates/misc/dotnet/src/Imports/TableImport.cs b/crates/misc/dotnet/src/Imports/TableImport.cs new file mode 100644 index 0000000000..352a4c1047 --- /dev/null +++ b/crates/misc/dotnet/src/Imports/TableImport.cs @@ -0,0 +1,42 @@ +using System; +using System.Diagnostics; + +namespace Wasmtime.Imports +{ + /// + /// Represents a table imported to a WebAssembly module. + /// + public class TableImport : Import + { + internal TableImport(IntPtr importType, IntPtr externType) : base(importType) + { + Debug.Assert(Interop.wasm_externtype_kind(externType) == Interop.wasm_externkind_t.WASM_EXTERN_TABLE); + + var tableType = Interop.wasm_externtype_as_tabletype_const(externType); + + Kind = Interop.wasm_valtype_kind(Interop.wasm_tabletype_element(tableType)); + + unsafe + { + var limits = Interop.wasm_tabletype_limits(tableType); + Minimum = limits->min; + Maximum = limits->max; + } + } + + /// + /// The value kind of the table. + /// + public ValueKind Kind { get; private set; } + + /// + /// The minimum number of elements in the table. + /// + public uint Minimum { get; private set; } + + /// + /// The maximum number of elements in the table. + /// + public uint Maximum { get; private set; } + } +} diff --git a/crates/misc/dotnet/src/Instance.cs b/crates/misc/dotnet/src/Instance.cs new file mode 100644 index 0000000000..f274101d08 --- /dev/null +++ b/crates/misc/dotnet/src/Instance.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Dynamic; +using Wasmtime.Externs; +using Wasmtime.Bindings; + +namespace Wasmtime +{ + /// + /// Represents an instantiated WebAssembly module. + /// + public class Instance : DynamicObject, IDisposable + { + internal Instance(Module module, Wasi wasi = null, IHost host = null) + { + Host = host; + Module = module; + + // Save the bindings to root the objects. + // Otherwise the GC may collect the callback delegates from FunctionHandles for example. + _bindings = Binding.GetImportBindings(module, wasi, host) + .Select(b => b.Bind(module.Store, host)) + .ToArray(); + + unsafe + { + Handle = Interop.wasm_instance_new( + Module.Store.Handle, + Module.Handle, + _bindings.Select(h => ToExtern(h)).ToArray(), + out var trap); + + if (trap != IntPtr.Zero) + { + throw TrapException.FromOwnedTrap(trap); + } + } + + if (Handle.IsInvalid) + { + throw new WasmtimeException($"Failed to instantiate module '{module.Name}'."); + } + + Interop.wasm_instance_exports(Handle, out _externs); + + Externs = new Wasmtime.Externs.Externs(Module.Exports, _externs); + + _functions = Externs.Functions.ToDictionary(f => f.Name); + _globals = Externs.Globals.ToDictionary(g => g.Name); + } + + /// + /// The host associated with this instance. + /// + public IHost Host { get; private set; } + + /// + /// The WebAssembly module associated with the instantiation. + /// + public Module Module { get; private set; } + + /// + /// The external (instantiated) collection of functions, globals, tables, and memories. + /// + public Wasmtime.Externs.Externs Externs { get; private set; } + + /// + public unsafe void Dispose() + { + if (!Handle.IsInvalid) + { + Handle.Dispose(); + Handle.SetHandleAsInvalid(); + } + + if (!(_bindings is null)) + { + foreach (var binding in _bindings) + { + binding.Dispose(); + } + _bindings = null; + } + + if (!(_externs.data is null)) + { + Interop.wasm_extern_vec_delete(ref _externs); + _externs.data = null; + } + } + + /// + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + if (_globals.TryGetValue(binder.Name, out var global)) + { + result = global.Value; + return true; + } + result = null; + return false; + } + + /// + public override bool TrySetMember(SetMemberBinder binder, object value) + { + if (_globals.TryGetValue(binder.Name, out var global)) + { + global.Value = value; + return true; + } + return false; + } + + /// + public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) + { + if (!_functions.TryGetValue(binder.Name, out var func)) + { + result = null; + return false; + } + + result = func.Invoke(args); + return true; + } + + private static unsafe IntPtr ToExtern(SafeHandle handle) + { + switch (handle) + { + case Interop.FunctionHandle f: + return Interop.wasm_func_as_extern(f); + + case Interop.GlobalHandle g: + return Interop.wasm_global_as_extern(g); + + case Interop.MemoryHandle m: + return Interop.wasm_memory_as_extern(m); + + case Interop.WasiExportHandle w: + return w.DangerousGetHandle(); + + default: + throw new NotSupportedException("Unexpected handle type."); + } + } + + internal Interop.InstanceHandle Handle { get; private set; } + private SafeHandle[] _bindings; + private Interop.wasm_extern_vec_t _externs; + private Dictionary _functions; + private Dictionary _globals; + } +} diff --git a/crates/misc/dotnet/src/Interop.cs b/crates/misc/dotnet/src/Interop.cs new file mode 100644 index 0000000000..24baac04e3 --- /dev/null +++ b/crates/misc/dotnet/src/Interop.cs @@ -0,0 +1,978 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; + +namespace Wasmtime +{ + /// + /// Implements the Wasmtime API bindings. + /// + /// See https://github.com/WebAssembly/wasm-c-api/blob/master/include/wasm.h for the C API reference. + internal static class Interop + { + const string LibraryName = "wasmtime"; + + internal class EngineHandle : SafeHandle + { + public EngineHandle() : base(IntPtr.Zero, true) + { + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + Interop.wasm_engine_delete(handle); + return true; + } + } + + internal class StoreHandle : SafeHandle + { + public StoreHandle() : base(IntPtr.Zero, true) + { + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + Interop.wasm_store_delete(handle); + return true; + } + } + + internal class ModuleHandle : SafeHandle + { + public ModuleHandle() : base(IntPtr.Zero, true) + { + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + Interop.wasm_module_delete(handle); + return true; + } + } + + internal class FunctionHandle : SafeHandle + { + public FunctionHandle() : base(IntPtr.Zero, true) + { + } + + public WasmFuncCallback Callback { get; set; } = null; + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + Interop.wasm_func_delete(handle); + return true; + } + } + + internal class GlobalHandle : SafeHandle + { + public GlobalHandle() : base(IntPtr.Zero, true) + { + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + Interop.wasm_global_delete(handle); + return true; + } + } + + internal class MemoryHandle : SafeHandle + { + public MemoryHandle() : base(IntPtr.Zero, true) + { + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + Interop.wasm_memory_delete(handle); + return true; + } + } + + internal class InstanceHandle : SafeHandle + { + public InstanceHandle() : base(IntPtr.Zero, true) + { + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + Interop.wasm_instance_delete(handle); + return true; + } + } + + internal class FuncTypeHandle : SafeHandle + { + public FuncTypeHandle() : base(IntPtr.Zero, true) + { + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + Interop.wasm_functype_delete(handle); + return true; + } + } + + internal class GlobalTypeHandle : SafeHandle + { + public GlobalTypeHandle() : base(IntPtr.Zero, true) + { + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + Interop.wasm_globaltype_delete(handle); + return true; + } + } + + internal class MemoryTypeHandle : SafeHandle + { + public MemoryTypeHandle() : base(IntPtr.Zero, true) + { + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + Interop.wasm_memorytype_delete(handle); + return true; + } + } + + internal class ValueTypeHandle : SafeHandle + { + public ValueTypeHandle() : base(IntPtr.Zero, true) + { + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + Interop.wasm_valtype_delete(handle); + return true; + } + } + + internal class WasmConfigHandle : SafeHandle + { + public WasmConfigHandle() : base(IntPtr.Zero, true) + { + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + Interop.wasm_config_delete(handle); + return true; + } + } + + internal class WasiConfigHandle : SafeHandle + { + public WasiConfigHandle() : base(IntPtr.Zero, true) + { + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + Interop.wasi_config_delete(handle); + return true; + } + } + + internal class WasiInstanceHandle : SafeHandle + { + public WasiInstanceHandle() : base(IntPtr.Zero, true) + { + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + Interop.wasi_instance_delete(handle); + return true; + } + } + + internal class WasiExportHandle : SafeHandle + { + public WasiExportHandle(IntPtr handle) : base(IntPtr.Zero, false /* not owned */) + { + SetHandle(handle); + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + return true; + } + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct wasm_byte_vec_t + { + public UIntPtr size; + public byte* data; + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct wasm_valtype_vec_t + { + public UIntPtr size; + public IntPtr* data; + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct wasm_export_vec_t + { + public UIntPtr size; + public IntPtr* data; + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct wasm_extern_vec_t + { + public UIntPtr size; + public IntPtr* data; + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct wasm_importtype_vec_t + { + public UIntPtr size; + public IntPtr* data; + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct wasm_exporttype_vec_t + { + public UIntPtr size; + public IntPtr* data; + } + + internal enum wasm_valkind_t : byte + { + WASM_I32, + WASM_I64, + WASM_F32, + WASM_F64, + WASM_ANYREF = 128, + WASM_FUNCREF, + } + + internal enum wasmtime_strategy_t : byte + { + WASMTIME_STRATEGY_AUTO, + WASMTIME_STRATEGY_CRANELIFT, + WASMTIME_STRATEGY_LIGHTBEAM + } + + internal enum wasmtime_opt_level_t : byte + { + WASMTIME_OPT_LEVEL_NONE, + WASMTIME_OPT_LEVEL_SPEED, + WASMTIME_OPT_LEVEL_SPEED_AND_SIZE + } + + [StructLayout(LayoutKind.Explicit)] + internal struct wasm_val_union_t + { + [FieldOffset(0)] + public int i32; + + [FieldOffset(0)] + public long i64; + + [FieldOffset(0)] + public float f32; + + [FieldOffset(0)] + public double f64; + + [FieldOffset(0)] + public IntPtr reference; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct wasm_val_t + { + public wasm_valkind_t kind; + public wasm_val_union_t of; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct wasm_limits_t + { + public uint min; + + public uint max; + } + + public static wasm_val_t ToValue(object o, ValueKind kind) + { + wasm_val_t value = new wasm_val_t(); + switch (kind) + { + case ValueKind.Int32: + value.kind = wasm_valkind_t.WASM_I32; + value.of.i32 = (int)Convert.ChangeType(o, TypeCode.Int32); + break; + + case ValueKind.Int64: + value.kind = wasm_valkind_t.WASM_I64; + value.of.i64 = (long)Convert.ChangeType(o, TypeCode.Int64); + break; + + case ValueKind.Float32: + value.kind = wasm_valkind_t.WASM_F32; + value.of.f32 = (float)Convert.ChangeType(o, TypeCode.Single); + break; + + case ValueKind.Float64: + value.kind = wasm_valkind_t.WASM_F64; + value.of.f64 = (double)Convert.ChangeType(o, TypeCode.Double); + break; + + // TODO: support AnyRef + + default: + throw new NotSupportedException("Unsupported value type."); + } + return value; + } + + public static unsafe object ToObject(wasm_val_t* v) + { + switch (v->kind) + { + case Interop.wasm_valkind_t.WASM_I32: + return v->of.i32; + + case Interop.wasm_valkind_t.WASM_I64: + return v->of.i64; + + case Interop.wasm_valkind_t.WASM_F32: + return v->of.f32; + + case Interop.wasm_valkind_t.WASM_F64: + return v->of.f64; + + // TODO: support AnyRef + + default: + throw new NotSupportedException("Unsupported value kind."); + } + } + + public static bool TryGetValueKind(Type type, out ValueKind kind) + { + if (type == typeof(int)) + { + kind = ValueKind.Int32; + return true; + } + + if (type == typeof(long)) + { + kind = ValueKind.Int64; + return true; + } + + if (type == typeof(float)) + { + kind = ValueKind.Float32; + return true; + } + + if (type == typeof(double)) + { + kind = ValueKind.Float64; + return true; + } + + kind = default(ValueKind); + return false; + } + + public static ValueKind ToValueKind(Type type) + { + if (TryGetValueKind(type, out var kind)) + { + return kind; + } + + throw new NotSupportedException($"Type '{type}' is not a supported WebAssembly value type."); + } + + public static string ToString(ValueKind kind) + { + switch (kind) + { + case ValueKind.Int32: + return "int"; + + case ValueKind.Int64: + return "long"; + + case ValueKind.Float32: + return "float"; + + case ValueKind.Float64: + return "double"; + + default: + throw new NotSupportedException("Unsupported value kind."); + } + } + + public static bool IsMatchingKind(ValueKind kind, ValueKind expected) + { + if (kind == expected) + { + return true; + } + + if (expected == ValueKind.AnyRef) + { + return kind == ValueKind.FuncRef; + } + + return false; + } + + internal unsafe delegate IntPtr WasmFuncCallback(wasm_val_t* parameters, wasm_val_t* results); + + internal enum wasm_externkind_t : byte + { + WASM_EXTERN_FUNC, + WASM_EXTERN_GLOBAL, + WASM_EXTERN_TABLE, + WASM_EXTERN_MEMORY, + } + + internal enum wasm_mutability_t : byte + { + WASM_CONST, + WASM_VAR, + } + + internal static unsafe List ToValueKindList(Interop.wasm_valtype_vec_t* vec) + { + var list = new List((int)vec->size); + + for (int i = 0; i < (int)vec->size; ++i) + { + list.Add(Interop.wasm_valtype_kind(vec->data[i])); + } + + return list; + } + + internal static Interop.wasm_valtype_vec_t ToValueTypeVec(IReadOnlyList collection) + { + Interop.wasm_valtype_vec_t vec; + Interop.wasm_valtype_vec_new_uninitialized(out vec, (UIntPtr)collection.Count); + + int i = 0; + foreach (var type in collection) + { + var valType = Interop.wasm_valtype_new((wasm_valkind_t)type); + unsafe + { + vec.data[i++] = valType.DangerousGetHandle(); + } + valType.SetHandleAsInvalid(); + } + + return vec; + } + + internal static unsafe (byte*[], GCHandle[]) ToUTF8PtrArray(IList strings) + { + // Unfortunately .NET cannot currently marshal string[] as UTF-8 + // See: https://github.com/dotnet/runtime/issues/7315 + // Therefore, we need to marshal the strings manually + var handles = new GCHandle[strings.Count]; + var ptrs = new byte*[strings.Count]; + for (int i = 0; i < strings.Count; ++i) + { + handles[i] = GCHandle.Alloc( + Encoding.UTF8.GetBytes(strings[i] + '\0'), + GCHandleType.Pinned + ); + ptrs[i] = (byte*)handles[i].AddrOfPinnedObject(); + } + + return (ptrs, handles); + } + + // Engine imports + + [DllImport(LibraryName)] + public static extern EngineHandle wasm_engine_new(); + + [DllImport(LibraryName)] + public static extern EngineHandle wasm_engine_new_with_config(WasmConfigHandle config); + + [DllImport(LibraryName)] + public static extern void wasm_engine_delete(IntPtr engine); + + // Store imports + + [DllImport(LibraryName)] + public static extern StoreHandle wasm_store_new(EngineHandle engine); + + [DllImport(LibraryName)] + public static extern void wasm_store_delete(IntPtr engine); + + // Byte vec imports + + [DllImport(LibraryName)] + public static extern void wasm_byte_vec_new_empty(out wasm_byte_vec_t vec); + + [DllImport(LibraryName)] + public static extern void wasm_byte_vec_new_uninitialized(out wasm_byte_vec_t vec, UIntPtr length); + + [DllImport(LibraryName)] + public static extern void wasm_byte_vec_new(out wasm_byte_vec_t vec, UIntPtr length, byte[] data); + + [DllImport(LibraryName)] + public static extern void wasm_byte_vec_copy(out wasm_byte_vec_t vec, ref wasm_byte_vec_t src); + + [DllImport(LibraryName)] + public static extern void wasm_byte_vec_delete(ref wasm_byte_vec_t vec); + + // Value type vec imports + + [DllImport(LibraryName)] + public static extern void wasm_valtype_vec_new_empty(out wasm_valtype_vec_t vec); + + [DllImport(LibraryName)] + public static extern void wasm_valtype_vec_new_uninitialized(out wasm_valtype_vec_t vec, UIntPtr length); + + [DllImport(LibraryName)] + public static extern void wasm_valtype_vec_new(out wasm_valtype_vec_t vec, UIntPtr length, IntPtr[] data); + + [DllImport(LibraryName)] + public static extern void wasm_valtype_vec_copy(out wasm_valtype_vec_t vec, ref wasm_valtype_vec_t src); + + [DllImport(LibraryName)] + public static extern void wasm_valtype_vec_delete(ref wasm_valtype_vec_t vec); + + // Extern vec imports + + [DllImport(LibraryName)] + public static extern void wasm_extern_vec_new_empty(out wasm_extern_vec_t vec); + + [DllImport(LibraryName)] + public static extern void wasm_extern_vec_new_uninitialized(out wasm_extern_vec_t vec, UIntPtr length); + + [DllImport(LibraryName)] + public static extern void wasm_extern_vec_new(out wasm_extern_vec_t vec, UIntPtr length, IntPtr[] data); + + [DllImport(LibraryName)] + public static extern void wasm_extern_vec_copy(out wasm_extern_vec_t vec, ref wasm_extern_vec_t src); + + [DllImport(LibraryName)] + public static extern void wasm_extern_vec_delete(ref wasm_extern_vec_t vec); + + // Import type vec imports + + [DllImport(LibraryName)] + public static extern void wasm_importtype_vec_new_empty(out wasm_importtype_vec_t vec); + + [DllImport(LibraryName)] + public static extern void wasm_importtype_vec_new_uninitialized(out wasm_importtype_vec_t vec, UIntPtr length); + + [DllImport(LibraryName)] + public static extern void wasm_importtype_vec_new(out wasm_importtype_vec_t vec, UIntPtr length, IntPtr[] data); + + [DllImport(LibraryName)] + public static extern void wasm_importtype_vec_copy(out wasm_importtype_vec_t vec, ref wasm_importtype_vec_t src); + + [DllImport(LibraryName)] + public static extern void wasm_importtype_vec_delete(ref wasm_importtype_vec_t vec); + + // Export type vec imports + + [DllImport(LibraryName)] + public static extern void wasm_exporttype_vec_new_empty(out wasm_exporttype_vec_t vec); + + [DllImport(LibraryName)] + public static extern void wasm_exporttype_vec_new_uninitialized(out wasm_exporttype_vec_t vec, UIntPtr length); + + [DllImport(LibraryName)] + public static extern void wasm_exporttype_vec_new(out wasm_exporttype_vec_t vec, UIntPtr length, IntPtr[] data); + + [DllImport(LibraryName)] + public static extern void wasm_exporttype_vec_copy(out wasm_exporttype_vec_t vec, ref wasm_exporttype_vec_t src); + + [DllImport(LibraryName)] + public static extern void wasm_exporttype_vec_delete(ref wasm_exporttype_vec_t vec); + + // Import type imports + + [DllImport(LibraryName)] + public static extern unsafe wasm_byte_vec_t* wasm_importtype_module(IntPtr importType); + + [DllImport(LibraryName)] + public static extern unsafe wasm_byte_vec_t* wasm_importtype_name(IntPtr importType); + + [DllImport(LibraryName)] + public static extern unsafe IntPtr wasm_importtype_type(IntPtr importType); + + // Export type imports + + [DllImport(LibraryName)] + public static extern unsafe wasm_byte_vec_t* wasm_exporttype_name(IntPtr exportType); + + [DllImport(LibraryName)] + public static extern unsafe IntPtr wasm_exporttype_type(IntPtr exportType); + + // Module imports + + [DllImport(LibraryName)] + public static extern ModuleHandle wasm_module_new(StoreHandle store, ref wasm_byte_vec_t bytes); + + [DllImport(LibraryName)] + public static extern void wasm_module_imports(ModuleHandle module, out wasm_importtype_vec_t imports); + + [DllImport(LibraryName)] + public static extern void wasm_module_exports(ModuleHandle module, out wasm_exporttype_vec_t exports); + + [DllImport(LibraryName)] + public static extern void wasm_module_delete(IntPtr module); + + // Value type imports + + [DllImport(LibraryName)] + public static extern ValueTypeHandle wasm_valtype_new(wasm_valkind_t kind); + + [DllImport(LibraryName)] + public static extern void wasm_valtype_delete(IntPtr valueType); + + [DllImport(LibraryName)] + public static extern ValueKind wasm_valtype_kind(IntPtr valueType); + + // Extern imports + + [DllImport(LibraryName)] + public static extern wasm_externkind_t wasm_extern_kind(IntPtr ext); + + [DllImport(LibraryName)] + public static extern IntPtr wasm_extern_type(IntPtr ext); + + [DllImport(LibraryName)] + public static extern IntPtr wasm_extern_as_func(IntPtr ext); + + [DllImport(LibraryName)] + public static extern IntPtr wasm_extern_as_global(IntPtr ext); + + [DllImport(LibraryName)] + public static extern IntPtr wasm_extern_as_table(IntPtr ext); + + [DllImport(LibraryName)] + public static extern IntPtr wasm_extern_as_memory(IntPtr ext); + + // Extern type imports + + [DllImport(LibraryName)] + public static extern wasm_externkind_t wasm_externtype_kind(IntPtr externType); + + [DllImport(LibraryName)] + public static extern IntPtr wasm_externtype_as_functype_const(IntPtr externType); + + [DllImport(LibraryName)] + public static extern IntPtr wasm_externtype_as_globaltype_const(IntPtr externType); + + [DllImport(LibraryName)] + public static extern IntPtr wasm_externtype_as_tabletype_const(IntPtr externType); + + [DllImport(LibraryName)] + public static extern IntPtr wasm_externtype_as_memorytype_const(IntPtr externType); + + // Function imports + + [DllImport(LibraryName)] + public static extern FunctionHandle wasm_func_new(StoreHandle store, FuncTypeHandle type, WasmFuncCallback callback); + + [DllImport(LibraryName)] + public static extern void wasm_func_delete(IntPtr function); + + [DllImport(LibraryName)] + public static unsafe extern IntPtr wasm_func_call(IntPtr function, wasm_val_t* args, wasm_val_t* results); + + [DllImport(LibraryName)] + public static extern IntPtr wasm_func_as_extern(FunctionHandle function); + + [DllImport(LibraryName)] + public static extern IntPtr wasm_global_as_extern(GlobalHandle global); + + [DllImport(LibraryName)] + public static extern IntPtr wasm_memory_as_extern(MemoryHandle memory); + + // Function type imports + + [DllImport(LibraryName)] + public static extern unsafe wasm_valtype_vec_t* wasm_functype_params(IntPtr funcType); + + [DllImport(LibraryName)] + public static extern unsafe wasm_valtype_vec_t* wasm_functype_results(IntPtr funcType); + + // Instance imports + + [DllImport(LibraryName)] + public static extern unsafe InstanceHandle wasm_instance_new(StoreHandle store, ModuleHandle module, IntPtr[] imports, out IntPtr trap); + + [DllImport(LibraryName)] + public static extern void wasm_instance_delete(IntPtr ext); + + [DllImport(LibraryName)] + public static extern void wasm_instance_exports(InstanceHandle instance, out wasm_extern_vec_t exports); + + // Function type imports + + [DllImport(LibraryName)] + public static extern FuncTypeHandle wasm_functype_new(ref wasm_valtype_vec_t parameters, ref wasm_valtype_vec_t results); + + [DllImport(LibraryName)] + public static extern void wasm_functype_delete(IntPtr functype); + + // Global type imports + + [DllImport(LibraryName)] + public static extern GlobalTypeHandle wasm_globaltype_new(IntPtr valueType, wasm_mutability_t mutability); + + [DllImport(LibraryName)] + public static extern IntPtr wasm_globaltype_delete(IntPtr globalType); + + [DllImport(LibraryName)] + public static extern IntPtr wasm_globaltype_content(IntPtr globalType); + + [DllImport(LibraryName)] + public static extern wasm_mutability_t wasm_globaltype_mutability(IntPtr globalType); + + // Memory type imports + + [DllImport(LibraryName)] + public static extern unsafe MemoryTypeHandle wasm_memorytype_new(wasm_limits_t* limits); + + [DllImport(LibraryName)] + public static extern IntPtr wasm_memorytype_delete(IntPtr memoryType); + + + [DllImport(LibraryName)] + public static extern unsafe wasm_limits_t* wasm_memorytype_limits(MemoryTypeHandle memoryType); + + // Trap imports + + [DllImport(LibraryName)] + public static extern IntPtr wasm_trap_new(StoreHandle store, ref wasm_byte_vec_t message); + + [DllImport(LibraryName)] + public static extern void wasm_trap_delete(IntPtr trap); + + [DllImport(LibraryName)] + public static extern void wasm_trap_message(IntPtr trap, out wasm_byte_vec_t message); + + // Table type imports + + [DllImport(LibraryName)] + public static extern IntPtr wasm_tabletype_element(IntPtr tableType); + + [DllImport(LibraryName)] + public static unsafe extern wasm_limits_t* wasm_tabletype_limits(IntPtr tableType); + + // Memory type imports + + [DllImport(LibraryName)] + public static unsafe extern wasm_limits_t* wasm_memorytype_limits(IntPtr memoryType); + + // Global imports + + [DllImport(LibraryName)] + public static unsafe extern GlobalHandle wasm_global_new(StoreHandle handle, GlobalTypeHandle globalType, wasm_val_t* initialValue); + + [DllImport(LibraryName)] + public static extern void wasm_global_delete(IntPtr global); + + [DllImport(LibraryName)] + public static extern IntPtr wasm_global_type(IntPtr global); + + [DllImport(LibraryName)] + public static unsafe extern void wasm_global_get(IntPtr global, wasm_val_t* value); + + [DllImport(LibraryName)] + public static unsafe extern void wasm_global_set(IntPtr global, wasm_val_t* value); + + // Memory imports + + [DllImport(LibraryName)] + public static extern MemoryHandle wasm_memory_new(StoreHandle handle, MemoryTypeHandle memoryType); + + [DllImport(LibraryName)] + public static extern void wasm_memory_delete(IntPtr memory); + + [DllImport(LibraryName)] + public static extern IntPtr wasm_memory_type(MemoryHandle memory); + + [DllImport(LibraryName)] + public static unsafe extern byte* wasm_memory_data(IntPtr memory); + + [DllImport(LibraryName)] + public static extern UIntPtr wasm_memory_data_size(IntPtr memory); + + [DllImport(LibraryName)] + public static extern uint wasm_memory_size(MemoryHandle memory); + + [DllImport(LibraryName)] + [return: MarshalAs(UnmanagedType.I1)] + public static extern bool wasm_memory_grow(MemoryHandle memory, uint delta); + + // Wasm config + + [DllImport(LibraryName)] + public static extern WasmConfigHandle wasm_config_new(); + + [DllImport(LibraryName)] + public static extern void wasm_config_delete(IntPtr config); + + // WASI config + + [DllImport(LibraryName)] + public static extern WasiConfigHandle wasi_config_new(); + + [DllImport(LibraryName)] + public static extern void wasi_config_delete(IntPtr config); + + [DllImport(LibraryName)] + public unsafe static extern void wasi_config_set_argv(WasiConfigHandle config, int argc, byte*[] argv); + + [DllImport(LibraryName)] + public static extern void wasi_config_inherit_argv(WasiConfigHandle config); + + [DllImport(LibraryName)] + public static extern unsafe void wasi_config_set_env( + WasiConfigHandle config, + int envc, + byte*[] names, + byte*[] values + ); + + [DllImport(LibraryName)] + public static extern void wasi_config_inherit_env(WasiConfigHandle config); + + [DllImport(LibraryName)] + [return: MarshalAs(UnmanagedType.I1)] + public static extern bool wasi_config_set_stdin_file( + WasiConfigHandle config, + [MarshalAs(UnmanagedType.LPUTF8Str)] string path + ); + + [DllImport(LibraryName)] + public static extern void wasi_config_inherit_stdin(WasiConfigHandle config); + + [DllImport(LibraryName)] + [return: MarshalAs(UnmanagedType.I1)] + public static extern bool wasi_config_set_stdout_file( + WasiConfigHandle config, + [MarshalAs(UnmanagedType.LPUTF8Str)] string path + ); + + [DllImport(LibraryName)] + public static extern void wasi_config_inherit_stdout(WasiConfigHandle config); + + [DllImport(LibraryName)] + [return: MarshalAs(UnmanagedType.I1)] + public static extern bool wasi_config_set_stderr_file( + WasiConfigHandle config, + [MarshalAs(UnmanagedType.LPUTF8Str)] string path + ); + + [DllImport(LibraryName)] + public static extern void wasi_config_inherit_stderr(WasiConfigHandle config); + + [DllImport(LibraryName)] + [return: MarshalAs(UnmanagedType.I1)] + public static extern bool wasi_config_preopen_dir( + WasiConfigHandle config, + [MarshalAs(UnmanagedType.LPUTF8Str)] string path, + [MarshalAs(UnmanagedType.LPUTF8Str)] string guestPath + ); + + // WASI instance + [DllImport(LibraryName)] + public static extern WasiInstanceHandle wasi_instance_new( + StoreHandle store, + WasiConfigHandle config, + out IntPtr trap + ); + + [DllImport(LibraryName)] + public static extern void wasi_instance_delete(IntPtr instance); + + [DllImport(LibraryName)] + public static extern IntPtr wasi_instance_bind_import(WasiInstanceHandle instance, IntPtr importType); + + // Wasmtime config + + [DllImport(LibraryName)] + public static extern void wasmtime_config_debug_info_set(WasmConfigHandle config, [MarshalAs(UnmanagedType.I1)] bool enable); + + [DllImport(LibraryName)] + public static extern void wasmtime_config_wasm_threads_set(WasmConfigHandle config, [MarshalAs(UnmanagedType.I1)] bool enable); + + [DllImport(LibraryName)] + public static extern void wasmtime_config_wasm_reference_types_set(WasmConfigHandle config, [MarshalAs(UnmanagedType.I1)] bool enable); + + [DllImport(LibraryName)] + public static extern IntPtr wasmtime_config_wasm_simd_set(WasmConfigHandle config, [MarshalAs(UnmanagedType.I1)] bool enable); + + [DllImport(LibraryName)] + public static extern void wasmtime_config_wasm_bulk_memory_set(WasmConfigHandle config, [MarshalAs(UnmanagedType.I1)] bool enable); + + [DllImport(LibraryName)] + public static extern void wasmtime_config_wasm_multi_value_set(WasmConfigHandle config, [MarshalAs(UnmanagedType.I1)] bool enable); + + [DllImport(LibraryName)] + public static extern void wasmtime_config_strategy_set(WasmConfigHandle config, wasmtime_strategy_t strategy); + + [DllImport(LibraryName)] + public static extern void wasmtime_config_cranelift_debug_verifier_set(WasmConfigHandle config, [MarshalAs(UnmanagedType.I1)] bool enable); + + [DllImport(LibraryName)] + public static extern void wasmtime_config_cranelift_opt_level_set(WasmConfigHandle config, wasmtime_opt_level_t level); + } +} diff --git a/crates/misc/dotnet/src/Memory.cs b/crates/misc/dotnet/src/Memory.cs new file mode 100644 index 0000000000..2cfe060f81 --- /dev/null +++ b/crates/misc/dotnet/src/Memory.cs @@ -0,0 +1,270 @@ +using System; +using System.Text; +using System.Buffers.Binary; + +namespace Wasmtime +{ + /// + /// Represents a WebAssembly memory. + /// + public class Memory + { + /// + /// The size, in bytes, of a WebAssembly memory page. + /// + public const int PageSize = 65536; + + /// + /// Creates a new memory with the given minimum and maximum page counts. + /// + /// + /// + public Memory(uint minimum = 1, uint maximum = uint.MaxValue) + { + if (minimum == 0) + { + throw new ArgumentException("The minimum cannot be zero..", nameof(minimum)); + } + + if (maximum < minimum) + { + throw new ArgumentException("The maximum cannot be less than the minimum.", nameof(maximum)); + } + + Minimum = minimum; + Maximum = maximum; + } + + /// + /// The minimum memory size (in WebAssembly page units). + /// + public uint Minimum { get; private set; } + + /// + /// The minimum memory size (in WebAssembly page units). + /// + public uint Maximum { get; private set; } + + /// + /// The span of the memory. + /// + /// + /// The span may become invalid if the memory grows. + /// + /// This may happen if the memory is explicitly requested to grow or + /// grows as a result of WebAssembly execution. + /// + /// Therefore, the returned Span should not be stored. + /// + public unsafe Span Span + { + get + { + var data = Interop.wasm_memory_data(_handle.DangerousGetHandle()); + var size = Convert.ToInt32(Interop.wasm_memory_data_size(_handle.DangerousGetHandle()).ToUInt32()); + return new Span(data, size); + } + } + + /// + /// Reads a UTF-8 string from memory. + /// + /// The zero-based address to read from. + /// The length of bytes to read. + /// Returns the string read from memory. + public string ReadString(int address, int length) + { + return Encoding.UTF8.GetString(Span.Slice(address, length)); + } + + /// + /// Writes a UTF-8 string at the given address. + /// + /// The zero-based address to write to. + /// The string to write. + /// Returns the number of bytes written. + public int WriteString(int address, string value) + { + return Encoding.UTF8.GetBytes(value, Span.Slice(address)); + } + + /// + /// Reads a byte from memory. + /// + /// The zero-based address to read from. + /// Returns the byte read from memory. + public byte ReadByte(int address) + { + return Span[address]; + } + + /// + /// Writes a byte to memory. + /// + /// The zero-based address to write to. + /// The byte to write. + public void WriteByte(int address, byte value) + { + Span[address] = value; + } + + /// + /// Reads a short from memory. + /// + /// The zero-based address to read from. + /// Returns the short read from memory. + public short ReadInt16(int address) + { + return BinaryPrimitives.ReadInt16LittleEndian(Span.Slice(address, 2)); + } + + /// + /// Writes a short to memory. + /// + /// The zero-based address to write to. + /// The short to write. + public void WriteInt16(int address, short value) + { + BinaryPrimitives.WriteInt16LittleEndian(Span.Slice(address, 2), value); + } + + /// + /// Reads an int from memory. + /// + /// The zero-based address to read from. + /// Returns the int read from memory. + public int ReadInt32(int address) + { + return BinaryPrimitives.ReadInt32LittleEndian(Span.Slice(address, 4)); + } + + /// + /// Writes an int to memory. + /// + /// The zero-based address to write to. + /// The int to write. + public void WriteInt32(int address, int value) + { + BinaryPrimitives.WriteInt32LittleEndian(Span.Slice(address, 4), value); + } + + /// + /// Reads a long from memory. + /// + /// The zero-based address to read from. + /// Returns the long read from memory. + public long ReadInt64(int address) + { + return BinaryPrimitives.ReadInt64LittleEndian(Span.Slice(address, 8)); + } + + /// + /// Writes a long to memory. + /// + /// The zero-based address to write to. + /// The long to write. + public void WriteInt64(int address, long value) + { + BinaryPrimitives.WriteInt64LittleEndian(Span.Slice(address, 8), value); + } + + /// + /// Reads an IntPtr from memory. + /// + /// The zero-based address to read from. + /// Returns the IntPtr read from memory. + public IntPtr ReadIntPtr(int address) + { + if (IntPtr.Size == 4) + { + return (IntPtr)ReadInt32(address); + } + return (IntPtr)ReadInt64(address); + } + + /// + /// Writes an IntPtr to memory. + /// + /// The zero-based address to write to. + /// The IntPtr to write. + public void WriteIntPtr(int address, IntPtr value) + { + if (IntPtr.Size == 4) + { + WriteInt32(address, value.ToInt32()); + } + else + { + WriteInt64(address, value.ToInt64()); + } + } + + /// + /// Reads a long from memory. + /// + /// The zero-based address to read from. + /// Returns the long read from memory. + public float ReadSingle(int address) + { + unsafe + { + var i = ReadInt32(address); + return *((float*)&i); + } + } + + /// + /// Writes a single to memory. + /// + /// The zero-based address to write to. + /// The single to write. + public void WriteSingle(int address, float value) + { + unsafe + { + WriteInt32(address, *(int*)&value); + } + } + + /// + /// Reads a double from memory. + /// + /// The zero-based address to read from. + /// Returns the double read from memory. + public double ReadDouble(int address) + { + unsafe + { + var i = ReadInt64(address); + return *((double*)&i); + } + } + + /// + /// Writes a double to memory. + /// + /// The zero-based address to write to. + /// The double to write. + public void WriteDouble(int address, double value) + { + unsafe + { + WriteInt64(address, *(long*)&value); + } + } + + internal Interop.MemoryHandle Handle + { + get + { + return _handle; + } + set + { + _handle = value; + } + } + + private Interop.MemoryHandle _handle; + } +} diff --git a/crates/misc/dotnet/src/Module.cs b/crates/misc/dotnet/src/Module.cs new file mode 100644 index 0000000000..203e5f7a6a --- /dev/null +++ b/crates/misc/dotnet/src/Module.cs @@ -0,0 +1,119 @@ +using System; +using System.Runtime.InteropServices; + +namespace Wasmtime +{ + /// + /// Represents a WebAssembly module. + /// + public class Module : IDisposable + { + internal Module(Store store, string name, byte[] bytes) + { + if (store.Handle.IsInvalid) + { + throw new ArgumentNullException(nameof(store)); + } + + var bytesHandle = GCHandle.Alloc(bytes, GCHandleType.Pinned); + + try + { + unsafe + { + Interop.wasm_byte_vec_t vec; + vec.size = (UIntPtr)bytes.Length; + vec.data = (byte*)bytesHandle.AddrOfPinnedObject(); + + Handle = Interop.wasm_module_new(store.Handle, ref vec); + } + + if (Handle.IsInvalid) + { + throw new WasmtimeException($"WebAssembly module '{name}' is not valid."); + } + } + finally + { + bytesHandle.Free(); + } + + Store = store; + Name = name; + Imports = new Wasmtime.Imports.Imports(this); + Exports = new Wasmtime.Exports.Exports(this); + } + + /// + /// Instantiates a WebAssembly module for the given host. + /// + /// The host to use for the WebAssembly module's instance. + /// Returns a new . + public Instance Instantiate(IHost host = null) + { + return Instantiate(null, host); + } + + /// + /// Instantiates a WebAssembly module for the given host. + /// + /// The WASI instance to use for WASI imports. + /// The host to use for the WebAssembly module's instance. + /// Returns a new . + public Instance Instantiate(Wasi wasi, IHost host = null) + { + if (!(host?.Instance is null)) + { + throw new InvalidOperationException("The host has already been associated with an instantiated module."); + } + + var instance = new Instance(this, wasi, host); + + if (!(host is null)) + { + host.Instance = instance; + return instance; + } + + return instance; + } + + /// + /// The associated with the module. + /// + public Store Store { get; private set; } + + /// + /// The name of the module. + /// + public string Name { get; private set; } + + /// + /// The imports of the module. + /// + public Wasmtime.Imports.Imports Imports { get; private set; } + + /// + /// The exports of the module. + /// + /// + public Wasmtime.Exports.Exports Exports { get; private set; } + + /// + public void Dispose() + { + if (!Handle.IsInvalid) + { + Handle.Dispose(); + Handle.SetHandleAsInvalid(); + } + if (!(Imports is null)) + { + Imports.Dispose(); + Imports = null; + } + } + + internal Interop.ModuleHandle Handle { get; private set; } + } +} diff --git a/crates/misc/dotnet/src/MutableGlobal.cs b/crates/misc/dotnet/src/MutableGlobal.cs new file mode 100644 index 0000000000..223c8c5f0c --- /dev/null +++ b/crates/misc/dotnet/src/MutableGlobal.cs @@ -0,0 +1,65 @@ +using System; + +namespace Wasmtime +{ + /// + /// Represents a mutable WebAssembly global value. + /// + public class MutableGlobal + { + /// + /// Creates a new with the given initial value. + /// + /// The initial value of the global. + public MutableGlobal(T initialValue) + { + InitialValue = initialValue; + Kind = Interop.ToValueKind(typeof(T)); + } + + /// + /// The value of the global. + /// + public T Value + { + get + { + if (Handle is null) + { + throw new InvalidOperationException("The global cannot be used before it is instantiated."); + } + + unsafe + { + var v = stackalloc Interop.wasm_val_t[1]; + + Interop.wasm_global_get(Handle.DangerousGetHandle(), v); + + // TODO: figure out a way that doesn't box the value + return (T)Interop.ToObject(v); + } + } + set + { + if (Handle is null) + { + throw new InvalidOperationException("The global cannot be used before it is instantiated."); + } + + // TODO: figure out a way that doesn't box the value + var v = Interop.ToValue(value, Kind); + + unsafe + { + Interop.wasm_global_set(Handle.DangerousGetHandle(), &v); + } + } + } + + internal ValueKind Kind { get; private set; } + + internal Interop.GlobalHandle Handle { get; set; } + + internal T InitialValue { get; private set; } + } +} diff --git a/crates/misc/dotnet/src/Store.cs b/crates/misc/dotnet/src/Store.cs new file mode 100644 index 0000000000..37be522201 --- /dev/null +++ b/crates/misc/dotnet/src/Store.cs @@ -0,0 +1,75 @@ +using System; +using System.IO; + +namespace Wasmtime +{ + /// + /// Represents the Wasmtime store. + /// + public sealed class Store : IDisposable + { + internal Store(Engine engine) + { + Handle = Interop.wasm_store_new(engine.Handle); + + if (Handle.IsInvalid) + { + throw new WasmtimeException("Failed to create Wasmtime store."); + } + } + + /// + /// Create a given the module name and bytes. + /// + /// The name of the module. + /// The bytes of the module. + /// Retuw . + public Module CreateModule(string name, byte[] bytes) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + if (bytes is null) + { + throw new ArgumentNullException(nameof(bytes)); + } + + return new Module(this, name, bytes); + } + + /// + /// Create a given the module name and path to the WebAssembly file. + /// + /// The name of the module. + /// The path to the WebAssembly file. + /// Returns a new . + public Module CreateModule(string name, string path) + { + return CreateModule(name, File.ReadAllBytes(path)); + } + + /// + /// Create a given the path to the WebAssembly file. + /// + /// The path to the WebAssembly file. + /// Returns a new . + public Module CreateModule(string path) + { + return CreateModule(Path.GetFileNameWithoutExtension(path), path); + } + + /// + public void Dispose() + { + if (!Handle.IsInvalid) + { + Handle.Dispose(); + Handle.SetHandleAsInvalid(); + } + } + + internal Interop.StoreHandle Handle { get; private set; } + } +} diff --git a/crates/misc/dotnet/src/TrapException.cs b/crates/misc/dotnet/src/TrapException.cs new file mode 100644 index 0000000000..7446ab6a3d --- /dev/null +++ b/crates/misc/dotnet/src/TrapException.cs @@ -0,0 +1,49 @@ +using System; +using System.Runtime.Serialization; +using System.Text; + +namespace Wasmtime +{ + /// + /// The exception for WebAssembly traps. + /// + [Serializable] + public class TrapException : WasmtimeException + { + /// + public TrapException() { } + + /// + public TrapException(string message) : base(message) { } + + /// + public TrapException(string message, Exception inner) : base(message, inner) { } + + /// + protected TrapException(SerializationInfo info, StreamingContext context) : base(info, context) { } + + internal static TrapException FromOwnedTrap(IntPtr trap) + { + unsafe + { + Interop.wasm_trap_message(trap, out var bytes); + var byteSpan = new ReadOnlySpan(bytes.data, checked((int)bytes.size)); + + int indexOfNull = byteSpan.LastIndexOf((byte)0); + if (indexOfNull != -1) + { + byteSpan = byteSpan.Slice(0, indexOfNull); + } + + var message = Encoding.UTF8.GetString(byteSpan); + Interop.wasm_byte_vec_delete(ref bytes); + + Interop.wasm_trap_delete(trap); + + return new TrapException(message); + } + } + + // TODO: expose trap frames + } +} diff --git a/crates/misc/dotnet/src/ValueKind.cs b/crates/misc/dotnet/src/ValueKind.cs new file mode 100644 index 0000000000..0a6f72f123 --- /dev/null +++ b/crates/misc/dotnet/src/ValueKind.cs @@ -0,0 +1,35 @@ +using System; + +namespace Wasmtime +{ + /// + /// Represents the possible kinds of WebAssembly values. + /// + public enum ValueKind : byte + { + /// + /// The value is a 32-bit integer. + /// + Int32, + /// + /// The value is a 64-bit integer. + /// + Int64, + /// + /// The value is a 32-bit floating point number. + /// + Float32, + /// + /// The value is a 64-bit floating point number. + /// + Float64, + /// + /// The value is a reference. + /// + AnyRef = 128, + /// + /// The value is a function reference. + /// + FuncRef, + } +} diff --git a/crates/misc/dotnet/src/Wasi.cs b/crates/misc/dotnet/src/Wasi.cs new file mode 100644 index 0000000000..e16feaf58b --- /dev/null +++ b/crates/misc/dotnet/src/Wasi.cs @@ -0,0 +1,44 @@ +using System; +using Wasmtime.Bindings; +using Wasmtime.Imports; + +namespace Wasmtime +{ + public class Wasi + { + /// + /// Creates a default instance. + /// + public Wasi(Store store) : + this( + (store ?? throw new ArgumentNullException(nameof(store))).Handle, + Interop.wasi_config_new() + ) + { + } + + internal Wasi(Interop.StoreHandle store, Interop.WasiConfigHandle config) + { + IntPtr trap; + Handle = Interop.wasi_instance_new(store, config, out trap); + config.SetHandleAsInvalid(); + + if (trap != IntPtr.Zero) + { + throw TrapException.FromOwnedTrap(trap); + } + } + + internal WasiBinding Bind(Import import) + { + var export = Interop.wasi_instance_bind_import(Handle, import.Handle); + if (export == IntPtr.Zero) + { + return null; + } + return new WasiBinding(export); + } + + internal Interop.WasiInstanceHandle Handle { get; private set; } + } +} diff --git a/crates/misc/dotnet/src/WasiBuilder.cs b/crates/misc/dotnet/src/WasiBuilder.cs new file mode 100644 index 0000000000..d1a363f3ea --- /dev/null +++ b/crates/misc/dotnet/src/WasiBuilder.cs @@ -0,0 +1,409 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Wasmtime +{ + /// + /// Represents a builder of instances. + /// + public class WasiBuilder + { + /// + /// Constructs a new . + /// + public WasiBuilder() + { + } + + /// + /// Adds a command line argument to the builder. + /// + /// The command line argument to add. + /// Returns the current builder. + public WasiBuilder WithArg(string arg) + { + if (arg is null) + { + throw new ArgumentNullException(nameof(arg)); + } + + if (_inheritArgs) + { + _args.Clear(); + _inheritArgs = false; + } + + _args.Add(arg); + return this; + } + + /// + /// Adds multiple command line arguments to the builder. + /// + /// The command line arguments to add. + /// Returns the current builder. + public WasiBuilder WithArgs(IEnumerable args) + { + if (args is null) + { + throw new ArgumentNullException(nameof(args)); + } + + if (_inheritArgs) + { + _args.Clear(); + _inheritArgs = false; + } + + foreach (var arg in args) + { + _args.Add(arg); + } + return this; + } + + /// + /// Adds multiple command line arguments to the builder. + /// + /// The command line arguments to add. + /// Returns the current builder. + public WasiBuilder WithArgs(params string[] args) + { + return WithArgs((IEnumerable)args); + } + + /// + /// Sets the builder to inherit command line arguments. + /// + /// Any explicitly specified command line arguments will be removed. + /// Returns the current builder. + public WasiBuilder WithInheritedArgs() + { + _inheritArgs = true; + _args.Clear(); + _args.AddRange(Environment.GetCommandLineArgs()); + return this; + } + + /// + /// Adds an environment variable to the builder. + /// + /// The name of the environment variable. + /// The value of the environment variable. + /// Returns the current builder. + public WasiBuilder WithEnvironmentVariable(string name, string value) + { + if (name is null) + { + throw new ArgumentNullException(nameof(name)); + } + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("Environment variable name cannot be empty.", nameof(name)); + } + + _inheritEnv = false; + _vars.Add((name, value)); + return this; + } + + /// + /// Adds multiple environment variables to the builder. + /// + /// The name-value tuples of the environment variables to add. + /// Returns the current builder. + public WasiBuilder WithEnvironmentVariables(IEnumerable<(string,string)> vars) + { + if (vars is null) + { + throw new ArgumentNullException(nameof(vars)); + } + + _inheritEnv = false; + + foreach (var v in vars) + { + _vars.Add(v); + } + + return this; + } + + /// + /// Sets the builder to inherit environment variables. + /// + /// Any explicitly specified environment variables will be removed. + /// Returns the current builder. + public WasiBuilder WithInheritedEnvironment() + { + _inheritEnv = true; + _vars.Clear(); + return this; + } + + /// + /// Sets the builder to use the given file path as stdin. + /// + /// The file to use as stdin. + /// Returns the current builder. + public WasiBuilder WithStandardInput(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentException("The path cannot be null or empty.", nameof(path)); + } + + _inheritStandardInput = false; + _standardInputPath = path; + return this; + } + + /// + /// Sets the builder to inherit stdin. + /// + /// Any explicitly specified stdin file will be removed. + /// Returns the current builder. + public WasiBuilder WithInheritedStandardInput() + { + _inheritStandardInput = true; + _standardInputPath = null; + return this; + } + + /// + /// Sets the builder to use the given file path as stdout. + /// + /// The file to use as stdout. + /// Returns the current builder. + public WasiBuilder WithStandardOutput(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentException("The path cannot be null or empty.", nameof(path)); + } + + _inheritStandardOutput = false; + _standardOutputPath = path; + return this; + } + + /// + /// Sets the builder to inherit stdout. + /// + /// Any explicitly specified stdout file will be removed. + /// Returns the current builder. + public WasiBuilder WithInheritedStandardOutput() + { + _inheritStandardOutput = true; + _standardOutputPath = null; + return this; + } + + /// + /// Sets the builder to use the given file path as stderr. + /// + /// The file to use as stderr. + /// Returns the current builder. + public WasiBuilder WithStandardError(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentException("The path cannot be null or empty.", nameof(path)); + } + + _inheritStandardError = false; + _standardErrorPath = path; + return this; + } + + /// + /// Sets the builder to inherit stderr. + /// + /// Any explicitly specified stderr file will be removed. + /// Returns the current builder. + public WasiBuilder WithInheritedStandardError() + { + _inheritStandardError = true; + _standardErrorPath = null; + return this; + } + + /// + /// Adds a preopen directory to the builder. + /// + /// The path to the directory to add. + /// The path the guest will use to open the directory. + /// Returns the current builder. + public WasiBuilder WithPreopenedDirectory(string path, string guestPath) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentException("The path cannot be null or empty.", nameof(path)); + } + if (string.IsNullOrEmpty(guestPath)) + { + throw new ArgumentException("The guest path cannot be null or empty.", nameof(guestPath)); + } + + _preopenDirs.Add((path, guestPath)); + return this; + } + + /// + /// Builds the instance. + /// + /// The to use. + /// Returns the new instance. + public Wasi Build(Store store) + { + var config = Interop.wasi_config_new(); + + SetConfigArgs(config); + SetEnvironmentVariables(config); + SetStandardIn(config); + SetStandardOut(config); + SetStandardError(config); + SetPreopenDirectories(config); + + return new Wasi(store.Handle, config); + } + + private unsafe void SetConfigArgs(Interop.WasiConfigHandle config) + { + // Don't call wasi_config_inherit_argv as the command line to the .NET program may not be + // the same as the process' command line (e.g. `dotnet foo.dll foo bar baz` => "foo.dll foo bar baz"). + if (_args.Count == 0) + { + return; + } + + var (args, handles) = Interop.ToUTF8PtrArray(_args); + + try + { + Interop.wasi_config_set_argv(config, _args.Count, args); + } + finally + { + foreach (var handle in handles) + { + handle.Free(); + } + } + } + + private unsafe void SetEnvironmentVariables(Interop.WasiConfigHandle config) + { + if (_inheritEnv) + { + Interop.wasi_config_inherit_env(config); + return; + } + + if (_vars.Count == 0) + { + return; + } + + var (names, nameHandles) = Interop.ToUTF8PtrArray(_vars.Select(var => var.Name).ToArray()); + var (values, valueHandles) = Interop.ToUTF8PtrArray(_vars.Select(var => var.Value).ToArray()); + + try + { + Interop.wasi_config_set_env(config, _vars.Count, names, values); + } + finally + { + foreach (var handle in nameHandles) + { + handle.Free(); + } + + foreach (var handle in valueHandles) + { + handle.Free(); + } + } + } + + private void SetStandardIn(Interop.WasiConfigHandle config) + { + if (_inheritStandardInput) + { + Interop.wasi_config_inherit_stdin(config); + return; + } + + if (!string.IsNullOrEmpty(_standardInputPath)) + { + if (!Interop.wasi_config_set_stdin_file(config, _standardInputPath)) + { + throw new InvalidOperationException($"Failed to set stdin to file '{_standardInputPath}'."); + } + } + } + + private void SetStandardOut(Interop.WasiConfigHandle config) + { + if (_inheritStandardOutput) + { + Interop.wasi_config_inherit_stdout(config); + return; + } + + if (!string.IsNullOrEmpty(_standardOutputPath)) + { + if (!Interop.wasi_config_set_stdout_file(config, _standardOutputPath)) + { + throw new InvalidOperationException($"Failed to set stdout to file '{_standardOutputPath}'."); + } + } + } + + private void SetStandardError(Interop.WasiConfigHandle config) + { + if (_inheritStandardError) + { + Interop.wasi_config_inherit_stderr(config); + return; + } + + if (!string.IsNullOrEmpty(_standardErrorPath)) + { + if (!Interop.wasi_config_set_stderr_file(config, _standardErrorPath)) + { + throw new InvalidOperationException($"Failed to set stderr to file '{_standardErrorPath}'."); + } + } + } + + private void SetPreopenDirectories(Interop.WasiConfigHandle config) + { + foreach (var dir in _preopenDirs) + { + if (!Interop.wasi_config_preopen_dir(config, dir.Path, dir.GuestPath)) + { + throw new InvalidOperationException($"Failed to preopen directory '{dir.Path}'."); + } + } + } + + private readonly List _args = new List(); + private readonly List<(string Name, string Value)> _vars = new List<(string, string)>(); + private string _standardInputPath; + private string _standardOutputPath; + private string _standardErrorPath; + private readonly List<(string Path, string GuestPath)> _preopenDirs = new List<(string, string)>(); + private bool _inheritArgs = false; + private bool _inheritEnv = false; + private bool _inheritStandardInput = false; + private bool _inheritStandardOutput = false; + private bool _inheritStandardError = false; + } +} diff --git a/crates/misc/dotnet/src/Wasmtime.csproj b/crates/misc/dotnet/src/Wasmtime.csproj new file mode 100644 index 0000000000..ffb0054e9e --- /dev/null +++ b/crates/misc/dotnet/src/Wasmtime.csproj @@ -0,0 +1,77 @@ + + + + netstandard2.1 + true + + + + + + + + Wasmtime.Dotnet + Wasmtime + $(WasmtimeVersion)-preview1 + Peter Huene + Peter Huene + true + snupkg + https://github.com/bytecodealliance/wasmtime + Initial release of Wasmtime for .NET. + A .NET API for Wasmtime, a standalone WebAssembly runtime + webassembly, .net, wasm, wasmtime + Wasmtime + +A .NET API for Wasmtime. + +Wasmtime is a standalone runtime for WebAssembly, using the Cranelift JIT compiler. + +Wasmtime for .NET enables .NET code to instantiate WebAssembly modules and to interact with them in-process. + + + + + + x86_64 + https://github.com/bytecodealliance/wasmtime/releases/download/v$(WasmtimeVersion)/wasmtime-v$(WasmtimeVersion)-$(WasmtimeArchitecture) + + + + + $(ReleaseURLBase)-linux-c-api.tar.xz + $(IntermediateOutputPath)wasmtime-linux + linux.tar.xz + libwasmtime.so + runtimes/linux-x64/native + + + $(ReleaseURLBase)-macos-c-api.tar.xz + $(IntermediateOutputPath)wasmtime-macos + macos.tar.xz + libwasmtime.dylib + runtimes/osx-x64/native + + + $(ReleaseURLBase)-windows-c-api.zip + $(IntermediateOutputPath)wasmtime-windows + windows.zip + wasmtime.dll + runtimes/win-x64/native + + + + + + + + + + + + %(WasmtimeDownload.PackagePath) + + + + + diff --git a/crates/misc/dotnet/src/WasmtimeException.cs b/crates/misc/dotnet/src/WasmtimeException.cs new file mode 100644 index 0000000000..26f0e6c5e6 --- /dev/null +++ b/crates/misc/dotnet/src/WasmtimeException.cs @@ -0,0 +1,24 @@ +using System; +using System.Runtime.Serialization; + +namespace Wasmtime +{ + /// + /// The base type for Wasmtime exceptions. + /// + [System.Serializable] + public class WasmtimeException : Exception + { + /// + public WasmtimeException() { } + + /// + public WasmtimeException(string message) : base(message) { } + + /// + public WasmtimeException(string message, Exception inner) : base(message, inner) { } + + /// + protected WasmtimeException(SerializationInfo info, StreamingContext context) : base(info, context) { } + } +} diff --git a/crates/misc/dotnet/tests/Fixtures/ModuleFixture.cs b/crates/misc/dotnet/tests/Fixtures/ModuleFixture.cs new file mode 100644 index 0000000000..d3a08a6dce --- /dev/null +++ b/crates/misc/dotnet/tests/Fixtures/ModuleFixture.cs @@ -0,0 +1,46 @@ +using System; +using System.IO; +using Wasmtime; + +namespace Wasmtime.Tests +{ + public abstract class ModuleFixture : IDisposable + { + public ModuleFixture() + { + Engine = new EngineBuilder() + .WithMultiValue(true) + .WithReferenceTypes(true) + .Build(); + Store = Engine.CreateStore(); + Module = Store.CreateModule(Path.Combine("Modules", ModuleFileName)); + } + + public void Dispose() + { + if (!(Module is null)) + { + Module.Dispose(); + Module = null; + } + + if (!(Store is null)) + { + Store.Dispose(); + Store = null; + } + + if (!(Engine is null)) + { + Engine.Dispose(); + Engine = null; + } + } + + public Engine Engine { get; set; } + public Store Store { get; set; } + public Module Module { get; set; } + + protected abstract string ModuleFileName { get; } + } +} diff --git a/crates/misc/dotnet/tests/FunctionExportsTests.cs b/crates/misc/dotnet/tests/FunctionExportsTests.cs new file mode 100644 index 0000000000..01d0929216 --- /dev/null +++ b/crates/misc/dotnet/tests/FunctionExportsTests.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Xunit; + +namespace Wasmtime.Tests +{ + public class FunctionExportsFixture : ModuleFixture + { + protected override string ModuleFileName => "FunctionExports.wasm"; + } + + public class FunctionExportsTests : IClassFixture + { + public FunctionExportsTests(FunctionExportsFixture fixture) + { + Fixture = fixture; + } + + private FunctionExportsFixture Fixture { get; set; } + + [Theory] + [MemberData(nameof(GetFunctionExports))] + public void ItHasTheExpectedFunctionExports(string exportName, ValueKind[] expectedParameters, ValueKind[] expectedResults) + { + var export = Fixture.Module.Exports.Functions.Where(f => f.Name == exportName).FirstOrDefault(); + export.Should().NotBeNull(); + export.Parameters.Should().Equal(expectedParameters); + export.Results.Should().Equal(expectedResults); + } + + [Fact] + public void ItHasTheExpectedNumberOfExportedFunctions() + { + GetFunctionExports().Count().Should().Be(Fixture.Module.Exports.Functions.Count); + } + + public static IEnumerable GetFunctionExports() + { + yield return new object[] { + "no_params_no_results", + Array.Empty(), + Array.Empty() + }; + + yield return new object[] { + "one_i32_param_no_results", + new ValueKind[] { + ValueKind.Int32 + }, + Array.Empty() + }; + + yield return new object[] { + "one_i64_param_no_results", + new ValueKind[] { + ValueKind.Int64 + }, + Array.Empty() + }; + + yield return new object[] { + "one_f32_param_no_results", + new ValueKind[] { + ValueKind.Float32 + }, + Array.Empty() + }; + + yield return new object[] { + "one_f64_param_no_results", + new ValueKind[] { + ValueKind.Float64 + }, + Array.Empty() + }; + + yield return new object[] { + "one_param_of_each_type", + new ValueKind[] { + ValueKind.Int32, + ValueKind.Int64, + ValueKind.Float32, + ValueKind.Float64 + }, + Array.Empty() + }; + + yield return new object[] { + "no_params_one_i32_result", + Array.Empty(), + new ValueKind[] { + ValueKind.Int32, + } + }; + + yield return new object[] { + "no_params_one_i64_result", + Array.Empty(), + new ValueKind[] { + ValueKind.Int64, + } + }; + + yield return new object[] { + "no_params_one_f32_result", + Array.Empty(), + new ValueKind[] { + ValueKind.Float32, + } + }; + + yield return new object[] { + "no_params_one_f64_result", + Array.Empty(), + new ValueKind[] { + ValueKind.Float64, + } + }; + + yield return new object[] { + "one_result_of_each_type", + Array.Empty(), + new ValueKind[] { + ValueKind.Int32, + ValueKind.Int64, + ValueKind.Float32, + ValueKind.Float64, + } + }; + + yield return new object[] { + "one_param_and_result_of_each_type", + new ValueKind[] { + ValueKind.Int32, + ValueKind.Int64, + ValueKind.Float32, + ValueKind.Float64, + }, + new ValueKind[] { + ValueKind.Int32, + ValueKind.Int64, + ValueKind.Float32, + ValueKind.Float64, + } + }; + } + } +} diff --git a/crates/misc/dotnet/tests/FunctionImportsTests.cs b/crates/misc/dotnet/tests/FunctionImportsTests.cs new file mode 100644 index 0000000000..bb2bdff302 --- /dev/null +++ b/crates/misc/dotnet/tests/FunctionImportsTests.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Xunit; + +namespace Wasmtime.Tests +{ + public class FunctionImportsFixture : ModuleFixture + { + protected override string ModuleFileName => "FunctionImports.wasm"; + } + + public class FunctionImportsTests : IClassFixture + { + public FunctionImportsTests(FunctionImportsFixture fixture) + { + Fixture = fixture; + } + + private FunctionImportsFixture Fixture { get; set; } + + [Theory] + [MemberData(nameof(GetFunctionImports))] + public void ItHasTheExpectedFunctionImports(string importModule, string importName, ValueKind[] expectedParameters, ValueKind[] expectedResults) + { + var import = Fixture.Module.Imports.Functions.Where(f => f.ModuleName == importModule && f.Name == importName).FirstOrDefault(); + import.Should().NotBeNull(); + import.Parameters.Should().Equal(expectedParameters); + import.Results.Should().Equal(expectedResults); + } + + [Fact] + public void ItHasTheExpectedNumberOfExportedFunctions() + { + GetFunctionImports().Count().Should().Be(Fixture.Module.Imports.Functions.Count); + } + + public static IEnumerable GetFunctionImports() + { + yield return new object[] { + "", + "no_params_no_results", + Array.Empty(), + Array.Empty() + }; + + yield return new object[] { + "", + "one_i32_param_no_results", + new ValueKind[] { + ValueKind.Int32 + }, + Array.Empty() + }; + + yield return new object[] { + "", + "one_i64_param_no_results", + new ValueKind[] { + ValueKind.Int64 + }, + Array.Empty() + }; + + yield return new object[] { + "", + "one_f32_param_no_results", + new ValueKind[] { + ValueKind.Float32 + }, + Array.Empty() + }; + + yield return new object[] { + "", + "one_f64_param_no_results", + new ValueKind[] { + ValueKind.Float64 + }, + Array.Empty() + }; + + yield return new object[] { + "", + "one_param_of_each_type", + new ValueKind[] { + ValueKind.Int32, + ValueKind.Int64, + ValueKind.Float32, + ValueKind.Float64 + }, + Array.Empty() + }; + + yield return new object[] { + "", + "no_params_one_i32_result", + Array.Empty(), + new ValueKind[] { + ValueKind.Int32, + } + }; + + yield return new object[] { + "", + "no_params_one_i64_result", + Array.Empty(), + new ValueKind[] { + ValueKind.Int64, + } + }; + + yield return new object[] { + "", + "no_params_one_f32_result", + Array.Empty(), + new ValueKind[] { + ValueKind.Float32, + } + }; + + yield return new object[] { + "", + "no_params_one_f64_result", + Array.Empty(), + new ValueKind[] { + ValueKind.Float64, + } + }; + + yield return new object[] { + "", + "one_result_of_each_type", + Array.Empty(), + new ValueKind[] { + ValueKind.Int32, + ValueKind.Int64, + ValueKind.Float32, + ValueKind.Float64, + } + }; + + yield return new object[] { + "", + "one_param_and_result_of_each_type", + new ValueKind[] { + ValueKind.Int32, + ValueKind.Int64, + ValueKind.Float32, + ValueKind.Float64, + }, + new ValueKind[] { + ValueKind.Int32, + ValueKind.Int64, + ValueKind.Float32, + ValueKind.Float64, + } + }; + + yield return new object[] { + "other", + "function_from_module", + Array.Empty(), + Array.Empty(), + }; + } + } +} diff --git a/crates/misc/dotnet/tests/FunctionThunkingTests.cs b/crates/misc/dotnet/tests/FunctionThunkingTests.cs new file mode 100644 index 0000000000..97eba2a299 --- /dev/null +++ b/crates/misc/dotnet/tests/FunctionThunkingTests.cs @@ -0,0 +1,69 @@ +using FluentAssertions; +using System; +using System.Linq; +using Xunit; + +namespace Wasmtime.Tests +{ + public class FunctionThunkingFixture : ModuleFixture + { + protected override string ModuleFileName => "FunctionThunking.wasm"; + } + + public class FunctionThunkingTests : IClassFixture + { + const string THROW_MESSAGE = "Test error message for wasmtime dotnet unit tests."; + + class MyHost : IHost + { + public Instance Instance { get; set; } + + [Import("add", Module = "env")] + public int Add(int x, int y) => x + y; + + [Import("do_throw", Module = "env")] + public void Throw() => throw new Exception(THROW_MESSAGE); + } + + public FunctionThunkingTests(FunctionThunkingFixture fixture) + { + Fixture = fixture; + } + + private FunctionThunkingFixture Fixture { get; } + + [Fact] + public void ItBindsImportMethodsAndCallsThemCorrectly() + { + var host = new MyHost(); + using var instance = Fixture.Module.Instantiate(host); + + var add_func = instance.Externs.Functions.Where(f => f.Name == "add_wrapper").Single(); + int invoke_add(int x, int y) => (int)add_func.Invoke(new object[] { x, y }); + + invoke_add(40, 2).Should().Be(42); + invoke_add(22, 5).Should().Be(27); + + //Collect garbage to make sure delegate function pointers pasted to wasmtime are rooted. + GC.Collect(); + GC.WaitForPendingFinalizers(); + + invoke_add(1970, 50).Should().Be(2020); + } + + [Fact] + public void ItPropagatesExceptionsToCallersViaTraps() + { + var host = new MyHost(); + using var instance = Fixture.Module.Instantiate(host); + + var throw_func = instance.Externs.Functions.Where(f => f.Name == "do_throw_wrapper").Single(); + Action action = () => throw_func.Invoke(); + + action + .Should() + .Throw() + .WithMessage(THROW_MESSAGE); + } + } +} diff --git a/crates/misc/dotnet/tests/GlobalExportsTests.cs b/crates/misc/dotnet/tests/GlobalExportsTests.cs new file mode 100644 index 0000000000..ca143f7286 --- /dev/null +++ b/crates/misc/dotnet/tests/GlobalExportsTests.cs @@ -0,0 +1,219 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using FluentAssertions; +using Wasmtime; +using Xunit; + +namespace Wasmtime.Tests +{ + public class GlobalExportsFixture : ModuleFixture + { + protected override string ModuleFileName => "GlobalExports.wasm"; + } + + public class GlobalExportsTests : IClassFixture + { + public class Host : IHost + { + public Instance Instance { get; set; } + } + + public GlobalExportsTests(GlobalExportsFixture fixture) + { + Fixture = fixture; + } + + private GlobalExportsFixture Fixture { get; set; } + + [Theory] + [MemberData(nameof(GetGlobalExports))] + public void ItHasTheExpectedGlobalExports(string exportName, ValueKind expectedKind, bool expectedMutable) + { + var export = Fixture.Module.Exports.Globals.Where(f => f.Name == exportName).FirstOrDefault(); + export.Should().NotBeNull(); + export.Kind.Should().Be(expectedKind); + export.IsMutable.Should().Be(expectedMutable); + } + + [Fact] + public void ItHasTheExpectedNumberOfExportedGlobals() + { + GetGlobalExports().Count().Should().Be(Fixture.Module.Exports.Globals.Count); + } + + [Fact] + public void ItCreatesExternsForTheGlobals() + { + using var instance = Fixture.Module.Instantiate(new Host()); + + dynamic dyn = instance; + var globals = instance.Externs.Globals; + globals.Count.Should().Be(8); + + var i32 = globals[0]; + i32.Name.Should().Be("global_i32"); + i32.Kind.Should().Be(ValueKind.Int32); + i32.IsMutable.Should().Be(false); + i32.Value.Should().Be(0); + + var i32Mut = globals[1]; + i32Mut.Name.Should().Be("global_i32_mut"); + i32Mut.Kind.Should().Be(ValueKind.Int32); + i32Mut.IsMutable.Should().Be(true); + i32Mut.Value.Should().Be(1); + i32Mut.Value = 11; + i32Mut.Value.Should().Be(11); + dyn.global_i32_mut = 12; + ((int)dyn.global_i32_mut).Should().Be(12); + i32Mut.Value.Should().Be(12); + + var i64 = globals[2]; + i64.Name.Should().Be("global_i64"); + i64.Kind.Should().Be(ValueKind.Int64); + i64.IsMutable.Should().Be(false); + i64.Value.Should().Be(2); + + var i64Mut = globals[3]; + i64Mut.Name.Should().Be("global_i64_mut"); + i64Mut.Kind.Should().Be(ValueKind.Int64); + i64Mut.IsMutable.Should().Be(true); + i64Mut.Value.Should().Be(3); + i64Mut.Value = 13; + i64Mut.Value.Should().Be(13); + dyn.global_i64_mut = 14; + ((long)dyn.global_i64_mut).Should().Be(14); + i64Mut.Value.Should().Be(14); + + var f32 = globals[4]; + f32.Name.Should().Be("global_f32"); + f32.Kind.Should().Be(ValueKind.Float32); + f32.IsMutable.Should().Be(false); + f32.Value.Should().Be(4); + + var f32Mut = globals[5]; + f32Mut.Name.Should().Be("global_f32_mut"); + f32Mut.Kind.Should().Be(ValueKind.Float32); + f32Mut.IsMutable.Should().Be(true); + f32Mut.Value.Should().Be(5); + f32Mut.Value = 15; + f32Mut.Value.Should().Be(15); + dyn.global_f32_mut = 16; + ((float)dyn.global_f32_mut).Should().Be(16); + f32Mut.Value.Should().Be(16); + + var f64 = globals[6]; + f64.Name.Should().Be("global_f64"); + f64.Kind.Should().Be(ValueKind.Float64); + f64.IsMutable.Should().Be(false); + f64.Value.Should().Be(6); + + var f64Mut = globals[7]; + f64Mut.Name.Should().Be("global_f64_mut"); + f64Mut.Kind.Should().Be(ValueKind.Float64); + f64Mut.IsMutable.Should().Be(true); + f64Mut.Value.Should().Be(7); + f64Mut.Value = 17; + f64Mut.Value.Should().Be(17); + dyn.global_f64_mut = 17; + ((double)dyn.global_f64_mut).Should().Be(17); + f64Mut.Value.Should().Be(17); + + Action action = () => i32.Value = 0; + action + .Should() + .Throw() + .WithMessage("The value of global 'global_i32' cannot be modified."); + action = () => dyn.global_i32 = 0; + action + .Should() + .Throw() + .WithMessage("The value of global 'global_i32' cannot be modified."); + + action = () => i64.Value = 0; + action + .Should() + .Throw() + .WithMessage("The value of global 'global_i64' cannot be modified."); + action = () => dyn.global_i64 = 0; + action + .Should() + .Throw() + .WithMessage("The value of global 'global_i64' cannot be modified."); + + action = () => f32.Value = 0; + action + .Should() + .Throw() + .WithMessage("The value of global 'global_f32' cannot be modified."); + action = () => dyn.global_f32 = 0; + action + .Should() + .Throw() + .WithMessage("The value of global 'global_f32' cannot be modified."); + + action = () => f64.Value = 0; + action + .Should() + .Throw() + .WithMessage("The value of global 'global_f64' cannot be modified."); + action = () => dyn.global_f64 = 0; + action + .Should() + .Throw() + .WithMessage("The value of global 'global_f64' cannot be modified."); + } + + public static IEnumerable GetGlobalExports() + { + yield return new object[] { + "global_i32", + ValueKind.Int32, + false + }; + + yield return new object[] { + "global_i32_mut", + ValueKind.Int32, + true + }; + + yield return new object[] { + "global_i64", + ValueKind.Int64, + false + }; + + yield return new object[] { + "global_i64_mut", + ValueKind.Int64, + true + }; + + yield return new object[] { + "global_f32", + ValueKind.Float32, + false + }; + + yield return new object[] { + "global_f32_mut", + ValueKind.Float32, + true + }; + + yield return new object[] { + "global_f64", + ValueKind.Float64, + false + }; + + yield return new object[] { + "global_f64_mut", + ValueKind.Float64, + true + }; + } + } +} diff --git a/crates/misc/dotnet/tests/GlobalImportBindingTests.cs b/crates/misc/dotnet/tests/GlobalImportBindingTests.cs new file mode 100644 index 0000000000..7969a90641 --- /dev/null +++ b/crates/misc/dotnet/tests/GlobalImportBindingTests.cs @@ -0,0 +1,258 @@ +using System; +using FluentAssertions; +using Xunit; + +namespace Wasmtime.Tests +{ + public class GlobalImportBindingFixture : ModuleFixture + { + protected override string ModuleFileName => "GlobalImportBindings.wasm"; + } + + public class GlobalImportBindingTests : IClassFixture + { + class NoImportsHost : IHost + { + public Instance Instance { get; set; } + } + + class GlobalIsStaticHost : IHost + { + public Instance Instance { get; set; } + + [Import("global_i32_mut")] + public static int x = 0; + } + + class GlobalIsNotReadOnlyHost : IHost + { + public Instance Instance { get; set; } + + [Import("global_i32_mut")] + public int x = 0; + } + + class NotAGlobalHost : IHost + { + public Instance Instance { get; set; } + + [Import("global_i32_mut")] + public readonly int x = 0; + } + + class NotAValidGlobalTypeHost : IHost + { + public struct NotAValue + { + } + + public Instance Instance { get; set; } + + [Import("global_i32_mut")] + public readonly MutableGlobal x = new MutableGlobal(new NotAValue()); + } + + class TypeMismatchHost : IHost + { + public Instance Instance { get; set; } + + [Import("global_i32_mut")] + public readonly MutableGlobal x = new MutableGlobal(0); + } + + class NotMutHost : IHost + { + public Instance Instance { get; set; } + + [Import("global_i32_mut")] + public readonly Global Int32Mut = new Global(0); + } + + class MutHost : IHost + { + public Instance Instance { get; set; } + + [Import("global_i32_mut")] + public readonly MutableGlobal Int32Mut = new MutableGlobal(0); + + [Import("global_i32")] + public readonly MutableGlobal Int32 = new MutableGlobal(0); + } + + class ValidHost : IHost + { + public Instance Instance { get; set; } + + [Import("global_i32_mut")] + public readonly MutableGlobal Int32Mut = new MutableGlobal(0); + + [Import("global_i32")] + public readonly Global Int32 = new Global(1); + + [Import("global_i64_mut")] + public readonly MutableGlobal Int64Mut = new MutableGlobal(2); + + [Import("global_i64")] + public readonly Global Int64 = new Global(3); + + [Import("global_f32_mut")] + public readonly MutableGlobal Float32Mut = new MutableGlobal(4); + + [Import("global_f32")] + public readonly Global Float32 = new Global(5); + + [Import("global_f64_mut")] + public readonly MutableGlobal Float64Mut = new MutableGlobal(6); + + [Import("global_f64")] + public readonly Global Float64 = new Global(7); + } + + public GlobalImportBindingTests(GlobalImportBindingFixture fixture) + { + Fixture = fixture; + } + + private GlobalImportBindingFixture Fixture { get; set; } + + [Fact] + public void ItFailsToInstantiateWithMissingImport() + { + Action action = () => { using var instance = Fixture.Module.Instantiate(new NoImportsHost()); }; + + action + .Should() + .Throw() + .WithMessage("Failed to bind global import 'global_i32_mut': the host does not contain a global field with a matching 'Import' attribute."); + } + + [Fact] + public void ItFailsToInstantiateWithStaticField() + { + Action action = () => { using var instance = Fixture.Module.Instantiate(new GlobalIsStaticHost()); }; + + action + .Should() + .Throw() + .WithMessage("Unable to bind 'GlobalIsStaticHost.x' to WebAssembly import 'global_i32_mut': field cannot be static."); + } + + [Fact] + public void ItFailsToInstantiateWithNonReadOnlyField() + { + Action action = () => { using var instance = Fixture.Module.Instantiate(new GlobalIsNotReadOnlyHost()); }; + + action + .Should() + .Throw() + .WithMessage("Unable to bind 'GlobalIsNotReadOnlyHost.x' to WebAssembly import 'global_i32_mut': field must be readonly."); + } + + [Fact] + public void ItFailsToInstantiateWithInvalidType() + { + Action action = () => { using var instance = Fixture.Module.Instantiate(new NotAGlobalHost()); }; + + action + .Should() + .Throw() + .WithMessage("Unable to bind 'NotAGlobalHost.x' to WebAssembly import 'global_i32_mut': field is expected to be of type 'Global'."); + } + + [Fact] + public void ItFailsToInstantiateWithInvalidGlobalType() + { + Action action = () => { using var instance = Fixture.Module.Instantiate(new NotAValidGlobalTypeHost()); }; + + action + .Should() + .Throw() + .WithMessage("Type 'Wasmtime.Tests.GlobalImportBindingTests+NotAValidGlobalTypeHost+NotAValue' is not a supported WebAssembly value type."); + } + + [Fact] + public void ItFailsToInstantiateWithGlobalTypeMismatch() + { + Action action = () => { using var instance = Fixture.Module.Instantiate(new TypeMismatchHost()); }; + + action + .Should() + .Throw() + .WithMessage("Unable to bind 'TypeMismatchHost.x' to WebAssembly import 'global_i32_mut': global type argument is expected to be of type 'int'."); + } + + [Fact] + public void ItFailsToInstantiateWhenGlobalIsNotMut() + { + Action action = () => { using var instance = Fixture.Module.Instantiate(new NotMutHost()); }; + + action + .Should() + .Throw() + .WithMessage("Unable to bind 'NotMutHost.Int32Mut' to WebAssembly import 'global_i32_mut': the import is mutable (use the 'MutableGlobal' type)."); + } + + [Fact] + public void ItFailsToInstantiateWhenGlobalIsMut() + { + Action action = () => { using var instance = Fixture.Module.Instantiate(new MutHost()); }; + + action + .Should() + .Throw() + .WithMessage("Unable to bind 'MutHost.Int32' to WebAssembly import 'global_i32': the import is constant (use the 'Global' type)."); + } + + [Fact] + public void ItBindsTheGlobalsCorrectly() + { + var host = new ValidHost(); + using dynamic instance = Fixture.Module.Instantiate(host); + + host.Int32Mut.Value.Should().Be(0); + ((int)instance.get_global_i32_mut()).Should().Be(0); + host.Int32.Value.Should().Be(1); + ((int)instance.get_global_i32()).Should().Be(1); + host.Int64Mut.Value.Should().Be(2); + ((long)instance.get_global_i64_mut()).Should().Be(2); + host.Int64.Value.Should().Be(3); + ((long)instance.get_global_i64()).Should().Be(3); + host.Float32Mut.Value.Should().Be(4); + ((float)instance.get_global_f32_mut()).Should().Be(4); + host.Float32.Value.Should().Be(5); + ((float)instance.get_global_f32()).Should().Be(5); + host.Float64Mut.Value.Should().Be(6); + ((double)instance.get_global_f64_mut()).Should().Be(6); + host.Float64.Value.Should().Be(7); + ((double)instance.get_global_f64()).Should().Be(7); + + host.Int32Mut.Value = 10; + host.Int32Mut.Value.Should().Be(10); + ((int)instance.get_global_i32_mut()).Should().Be(10); + instance.set_global_i32_mut(11); + host.Int32Mut.Value.Should().Be(11); + ((int)instance.get_global_i32_mut()).Should().Be(11); + + host.Int64Mut.Value = 12; + host.Int64Mut.Value.Should().Be(12); + ((long)instance.get_global_i64_mut()).Should().Be(12); + instance.set_global_i64_mut(13); + host.Int64Mut.Value.Should().Be(13); + ((long)instance.get_global_i64_mut()).Should().Be(13); + + host.Float32Mut.Value = 14; + host.Float32Mut.Value.Should().Be(14); + ((float)instance.get_global_f32_mut()).Should().Be(14); + instance.set_global_f32_mut(15); + host.Float32Mut.Value.Should().Be(15); + ((float)instance.get_global_f32_mut()).Should().Be(15); + + host.Float64Mut.Value = 16; + host.Float64Mut.Value.Should().Be(16); + ((double)instance.get_global_f64_mut()).Should().Be(16); + instance.set_global_f64_mut(17); + host.Float64Mut.Value.Should().Be(17); + ((double)instance.get_global_f64_mut()).Should().Be(17); + } + } +} diff --git a/crates/misc/dotnet/tests/GlobalImportsTests.cs b/crates/misc/dotnet/tests/GlobalImportsTests.cs new file mode 100644 index 0000000000..2479baf132 --- /dev/null +++ b/crates/misc/dotnet/tests/GlobalImportsTests.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Xunit; + +namespace Wasmtime.Tests +{ + public class GlobalImportsFixture : ModuleFixture + { + protected override string ModuleFileName => "GlobalImports.wasm"; + } + + public class GlobalImportsTests : IClassFixture + { + public GlobalImportsTests(GlobalImportsFixture fixture) + { + Fixture = fixture; + } + + private GlobalImportsFixture Fixture { get; set; } + + [Theory] + [MemberData(nameof(GetGlobalImports))] + public void ItHasTheExpectedGlobalImports(string importModule, string importName, ValueKind expectedKind, bool expectedMutable) + { + var import = Fixture.Module.Imports.Globals.Where(f => f.ModuleName == importModule && f.Name == importName).FirstOrDefault(); + import.Should().NotBeNull(); + import.Kind.Should().Be(expectedKind); + import.IsMutable.Should().Be(expectedMutable); + } + + [Fact] + public void ItHasTheExpectedNumberOfExportedGlobals() + { + GetGlobalImports().Count().Should().Be(Fixture.Module.Imports.Globals.Count); + } + + public static IEnumerable GetGlobalImports() + { + yield return new object[] { + "", + "global_i32", + ValueKind.Int32, + false + }; + + yield return new object[] { + "", + "global_i32_mut", + ValueKind.Int32, + true + }; + + yield return new object[] { + "", + "global_i64", + ValueKind.Int64, + false + }; + + yield return new object[] { + "", + "global_i64_mut", + ValueKind.Int64, + true + }; + + yield return new object[] { + "", + "global_f32", + ValueKind.Float32, + false + }; + + yield return new object[] { + "", + "global_f32_mut", + ValueKind.Float32, + true + }; + + yield return new object[] { + "", + "global_f64", + ValueKind.Float64, + false + }; + + yield return new object[] { + "", + "global_f64_mut", + ValueKind.Float64, + true + }; + + yield return new object[] { + "other", + "global_from_module", + ValueKind.Int32, + false + }; + } + } +} diff --git a/crates/misc/dotnet/tests/MemoryExportsTests.cs b/crates/misc/dotnet/tests/MemoryExportsTests.cs new file mode 100644 index 0000000000..08242e75c7 --- /dev/null +++ b/crates/misc/dotnet/tests/MemoryExportsTests.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Xunit; + +namespace Wasmtime.Tests +{ + public class MemoryExportsFixture : ModuleFixture + { + protected override string ModuleFileName => "MemoryExports.wasm"; + } + + public class MemoryExportsTests : IClassFixture + { + public class Host : IHost + { + public Instance Instance { get; set; } + } + + public MemoryExportsTests(MemoryExportsFixture fixture) + { + Fixture = fixture; + } + + private MemoryExportsFixture Fixture { get; set; } + + [Theory] + [MemberData(nameof(GetMemoryExports))] + public void ItHasTheExpectedMemoryExports(string exportName, uint expectedMinimum, uint expectedMaximum) + { + var export = Fixture.Module.Exports.Memories.Where(m => m.Name == exportName).FirstOrDefault(); + export.Should().NotBeNull(); + export.Minimum.Should().Be(expectedMinimum); + export.Maximum.Should().Be(expectedMaximum); + } + + [Fact] + public void ItHasTheExpectedNumberOfExportedTables() + { + GetMemoryExports().Count().Should().Be(Fixture.Module.Exports.Memories.Count); + } + + [Fact] + public void ItCreatesExternsForTheMemories() + { + var host = new Host(); + using var instance = Fixture.Module.Instantiate(host); + + instance.Externs.Memories.Count.Should().Be(1); + + var memory = instance.Externs.Memories[0]; + memory.ReadString(0, 11).Should().Be("Hello World"); + int written = memory.WriteString(0, "WebAssembly Rocks!"); + memory.ReadString(0, written).Should().Be("WebAssembly Rocks!"); + + memory.ReadByte(20).Should().Be(1); + memory.WriteByte(20, 11); + memory.ReadByte(20).Should().Be(11); + + memory.ReadInt16(21).Should().Be(2); + memory.WriteInt16(21, 12); + memory.ReadInt16(21).Should().Be(12); + + memory.ReadInt32(23).Should().Be(3); + memory.WriteInt32(23, 13); + memory.ReadInt32(23).Should().Be(13); + + memory.ReadInt64(27).Should().Be(4); + memory.WriteInt64(27, 14); + memory.ReadInt64(27).Should().Be(14); + + memory.ReadSingle(35).Should().Be(5); + memory.WriteSingle(35, 15); + memory.ReadSingle(35).Should().Be(15); + + memory.ReadDouble(39).Should().Be(6); + memory.WriteDouble(39, 16); + memory.ReadDouble(39).Should().Be(16); + + memory.ReadIntPtr(48).Should().Be((IntPtr)7); + memory.WriteIntPtr(48, (IntPtr)17); + memory.ReadIntPtr(48).Should().Be((IntPtr)17); + } + + public static IEnumerable GetMemoryExports() + { + yield return new object[] { + "mem", + 1, + 2 + }; + } + } +} diff --git a/crates/misc/dotnet/tests/MemoryImportBindingTests.cs b/crates/misc/dotnet/tests/MemoryImportBindingTests.cs new file mode 100644 index 0000000000..cab85e2434 --- /dev/null +++ b/crates/misc/dotnet/tests/MemoryImportBindingTests.cs @@ -0,0 +1,186 @@ +using System; +using FluentAssertions; +using Xunit; + +namespace Wasmtime.Tests +{ + public class MemoryImportBindingFixture : ModuleFixture + { + protected override string ModuleFileName => "MemoryImportBinding.wasm"; + } + + public class MemoryImportBindingTests : IClassFixture + { + class MissingImportsHost : IHost + { + public Instance Instance { get; set; } + } + + class MemoryIsStaticHost : IHost + { + public Instance Instance { get; set; } + + [Import("mem")] + public static Memory x = new Memory(minimum: 1); + } + + class MemoryIsNotReadOnlyHost : IHost + { + public Instance Instance { get; set; } + + [Import("mem")] + public Memory x = new Memory(minimum: 1); + } + + class NotAMemoryHost : IHost + { + public Instance Instance { get; set; } + + [Import("mem")] + public readonly int x = 0; + } + + class InvalidMinimumHost : IHost + { + public Instance Instance { get; set; } + + [Import("mem")] + public readonly Memory Mem = new Memory(minimum: 2); + } + + class InvalidMaximumHost : IHost + { + public Instance Instance { get; set; } + + [Import("mem")] + public readonly Memory Mem = new Memory(maximum: 2); + } + + class ValidHost : IHost + { + public Instance Instance { get; set; } + + [Import("mem")] + public readonly Memory Mem = new Memory(minimum: 1); + } + + public MemoryImportBindingTests(MemoryImportBindingFixture fixture) + { + Fixture = fixture; + } + + private MemoryImportBindingFixture Fixture { get; set; } + + [Fact] + public void ItFailsToInstantiateWithMissingImport() + { + Action action = () => { using var instance = Fixture.Module.Instantiate(new MissingImportsHost()); }; + + action + .Should() + .Throw() + .WithMessage("Failed to bind memory import 'mem': the host does not contain a memory field with a matching 'Import' attribute."); + } + + [Fact] + public void ItFailsToInstantiateWithStaticField() + { + Action action = () => { using var instance = Fixture.Module.Instantiate(new MemoryIsStaticHost()); }; + + action + .Should() + .Throw() + .WithMessage("Unable to bind 'MemoryIsStaticHost.x' to WebAssembly import 'mem': field cannot be static."); + } + + [Fact] + public void ItFailsToInstantiateWithNonReadOnlyField() + { + Action action = () => { using var instance = Fixture.Module.Instantiate(new MemoryIsNotReadOnlyHost()); }; + + action + .Should() + .Throw() + .WithMessage("Unable to bind 'MemoryIsNotReadOnlyHost.x' to WebAssembly import 'mem': field must be readonly."); + } + + [Fact] + public void ItFailsToInstantiateWithInvalidType() + { + Action action = () => { using var instance = Fixture.Module.Instantiate(new NotAMemoryHost()); }; + + action + .Should() + .Throw() + .WithMessage("Unable to bind 'NotAMemoryHost.x' to WebAssembly import 'mem': field is expected to be of type 'Memory'."); + } + + [Fact] + public void ItFailsToInstantiateWhenMemoryHasInvalidMinimum() + { + Action action = () => { using var instance = Fixture.Module.Instantiate(new InvalidMinimumHost()); }; + + action + .Should() + .Throw() + .WithMessage("Unable to bind 'InvalidMinimumHost.Mem' to WebAssembly import 'mem': Memory does not have the expected minimum of 1 page(s)."); + } + + [Fact] + public void ItFailsToInstantiateWhenMemoryHasInvalidMaximum() + { + Action action = () => { using var instance = Fixture.Module.Instantiate(new InvalidMaximumHost()); }; + + action + .Should() + .Throw() + .WithMessage("Unable to bind 'InvalidMaximumHost.Mem' to WebAssembly import 'mem': Memory does not have the expected maximum of 4294967295 page(s)."); + } + + [Fact] + public void ItBindsTheGlobalsCorrectly() + { + var host = new ValidHost(); + using dynamic instance = Fixture.Module.Instantiate(host); + + host.Mem.ReadString(0, 11).Should().Be("Hello World"); + int written = host.Mem.WriteString(0, "WebAssembly Rocks!"); + host.Mem.ReadString(0, written).Should().Be("WebAssembly Rocks!"); + + host.Mem.ReadByte(20).Should().Be(1); + host.Mem.WriteByte(20, 11); + host.Mem.ReadByte(20).Should().Be(11); + ((byte)instance.ReadByte()).Should().Be(11); + + host.Mem.ReadInt16(21).Should().Be(2); + host.Mem.WriteInt16(21, 12); + host.Mem.ReadInt16(21).Should().Be(12); + ((short)instance.ReadInt16()).Should().Be(12); + + host.Mem.ReadInt32(23).Should().Be(3); + host.Mem.WriteInt32(23, 13); + host.Mem.ReadInt32(23).Should().Be(13); + ((int)instance.ReadInt32()).Should().Be(13); + + host.Mem.ReadInt64(27).Should().Be(4); + host.Mem.WriteInt64(27, 14); + host.Mem.ReadInt64(27).Should().Be(14); + ((long)instance.ReadInt64()).Should().Be(14); + + host.Mem.ReadSingle(35).Should().Be(5); + host.Mem.WriteSingle(35, 15); + host.Mem.ReadSingle(35).Should().Be(15); + ((float)instance.ReadFloat32()).Should().Be(15); + + host.Mem.ReadDouble(39).Should().Be(6); + host.Mem.WriteDouble(39, 16); + host.Mem.ReadDouble(39).Should().Be(16); + ((double)instance.ReadFloat64()).Should().Be(16); + + host.Mem.ReadIntPtr(48).Should().Be((IntPtr)7); + host.Mem.WriteIntPtr(48, (IntPtr)17); + host.Mem.ReadIntPtr(48).Should().Be((IntPtr)17); + ((IntPtr)instance.ReadIntPtr()).Should().Be((IntPtr)17); + } + } +} diff --git a/crates/misc/dotnet/tests/MemoryImportFromModuleTests.cs b/crates/misc/dotnet/tests/MemoryImportFromModuleTests.cs new file mode 100644 index 0000000000..b47da19374 --- /dev/null +++ b/crates/misc/dotnet/tests/MemoryImportFromModuleTests.cs @@ -0,0 +1,34 @@ +using System; +using FluentAssertions; +using Xunit; + +namespace Wasmtime.Tests +{ + public class MemoryImportFromModuleFixture : ModuleFixture + { + protected override string ModuleFileName => "MemoryImportFromModule.wasm"; + } + + public class MemoryImportFromModuleTests : IClassFixture + { + public MemoryImportFromModuleTests(MemoryImportFromModuleFixture fixture) + { + Fixture = fixture; + } + + private MemoryImportFromModuleFixture Fixture { get; set; } + + [Fact] + public void ItHasTheExpectedImport() + { + Fixture.Module.Imports.Memories.Count.Should().Be(1); + + var memory = Fixture.Module.Imports.Memories[0]; + + memory.ModuleName.Should().Be("js"); + memory.Name.Should().Be("mem"); + memory.Minimum.Should().Be(1); + memory.Maximum.Should().Be(2); + } + } +} diff --git a/crates/misc/dotnet/tests/MemoryImportNoUpperBoundTests.cs b/crates/misc/dotnet/tests/MemoryImportNoUpperBoundTests.cs new file mode 100644 index 0000000000..8a9586bd28 --- /dev/null +++ b/crates/misc/dotnet/tests/MemoryImportNoUpperBoundTests.cs @@ -0,0 +1,34 @@ +using System; +using FluentAssertions; +using Xunit; + +namespace Wasmtime.Tests +{ + public class MemoryImportNoUpperBoundFixture : ModuleFixture + { + protected override string ModuleFileName => "MemoryImportNoUpperBound.wasm"; + } + + public class MemoryImportNoUpperBoundTests : IClassFixture + { + public MemoryImportNoUpperBoundTests(MemoryImportNoUpperBoundFixture fixture) + { + Fixture = fixture; + } + + private MemoryImportNoUpperBoundFixture Fixture { get; set; } + + [Fact] + public void ItHasTheExpectedImport() + { + Fixture.Module.Imports.Memories.Count.Should().Be(1); + + var memory = Fixture.Module.Imports.Memories[0]; + + memory.ModuleName.Should().Be(""); + memory.Name.Should().Be("mem"); + memory.Minimum.Should().Be(1); + memory.Maximum.Should().Be(uint.MaxValue); + } + } +} diff --git a/crates/misc/dotnet/tests/MemoryImportWithUpperBoundTests.cs b/crates/misc/dotnet/tests/MemoryImportWithUpperBoundTests.cs new file mode 100644 index 0000000000..ec73d9da21 --- /dev/null +++ b/crates/misc/dotnet/tests/MemoryImportWithUpperBoundTests.cs @@ -0,0 +1,34 @@ +using System; +using FluentAssertions; +using Xunit; + +namespace Wasmtime.Tests +{ + public class MemoryImportWithUpperBoundFixture : ModuleFixture + { + protected override string ModuleFileName => "MemoryImportWithUpperBound.wasm"; + } + + public class MemoryImportWithUpperBoundTests : IClassFixture + { + public MemoryImportWithUpperBoundTests(MemoryImportWithUpperBoundFixture fixture) + { + Fixture = fixture; + } + + private MemoryImportWithUpperBoundFixture Fixture { get; set; } + + [Fact] + public void ItHasTheExpectedImport() + { + Fixture.Module.Imports.Memories.Count.Should().Be(1); + + var memory = Fixture.Module.Imports.Memories[0]; + + memory.ModuleName.Should().Be(""); + memory.Name.Should().Be("mem"); + memory.Minimum.Should().Be(10); + memory.Maximum.Should().Be(100); + } + } +} diff --git a/crates/misc/dotnet/tests/Modules/FunctionExports.wasm b/crates/misc/dotnet/tests/Modules/FunctionExports.wasm new file mode 100644 index 0000000000..73e90b1387 Binary files /dev/null and b/crates/misc/dotnet/tests/Modules/FunctionExports.wasm differ diff --git a/crates/misc/dotnet/tests/Modules/FunctionExports.wat b/crates/misc/dotnet/tests/Modules/FunctionExports.wat new file mode 100644 index 0000000000..c939a1d23d --- /dev/null +++ b/crates/misc/dotnet/tests/Modules/FunctionExports.wat @@ -0,0 +1,26 @@ +(module + (func $no_params_no_results) + (func $one_i32_param_no_results (param i32)) + (func $one_i64_param_no_results (param i64)) + (func $one_f32_param_no_results (param f32)) + (func $one_f64_param_no_results (param f64)) + (func $one_param_of_each_type (param i32 i64 f32 f64)) + (func $no_params_one_i32_result (result i32) i32.const 0) + (func $no_params_one_i64_result (result i64) i64.const 0) + (func $no_params_one_f32_result (result f32) f32.const 0) + (func $no_params_one_f64_result (result f64) f64.const 0) + (func $one_result_of_each_type (result i32 i64 f32 f64) i32.const 0 i64.const 0 f32.const 0 f64.const 0) + (func $one_param_and_result_of_each_type (param i32 i64 f32 f64) (result i32 i64 f32 f64) i32.const 0 i64.const 0 f32.const 0 f64.const 0) + (export "no_params_no_results" (func $no_params_no_results)) + (export "one_i32_param_no_results" (func $one_i32_param_no_results)) + (export "one_i64_param_no_results" (func $one_i64_param_no_results)) + (export "one_f32_param_no_results" (func $one_f32_param_no_results)) + (export "one_f64_param_no_results" (func $one_f64_param_no_results)) + (export "one_param_of_each_type" (func $one_param_of_each_type)) + (export "no_params_one_i32_result" (func $no_params_one_i32_result)) + (export "no_params_one_i64_result" (func $no_params_one_i64_result)) + (export "no_params_one_f32_result" (func $no_params_one_f32_result)) + (export "no_params_one_f64_result" (func $no_params_one_f64_result)) + (export "one_result_of_each_type" (func $one_result_of_each_type)) + (export "one_param_and_result_of_each_type" (func $one_param_and_result_of_each_type)) +) diff --git a/crates/misc/dotnet/tests/Modules/FunctionImports.wasm b/crates/misc/dotnet/tests/Modules/FunctionImports.wasm new file mode 100644 index 0000000000..f52c32c22f Binary files /dev/null and b/crates/misc/dotnet/tests/Modules/FunctionImports.wasm differ diff --git a/crates/misc/dotnet/tests/Modules/FunctionImports.wat b/crates/misc/dotnet/tests/Modules/FunctionImports.wat new file mode 100644 index 0000000000..2f07daf63c --- /dev/null +++ b/crates/misc/dotnet/tests/Modules/FunctionImports.wat @@ -0,0 +1,27 @@ +(module + (type $t0 (func)) + (type $t1 (func (param i32))) + (type $t2 (func (param i64))) + (type $t3 (func (param f32))) + (type $t4 (func (param f64))) + (type $t5 (func (param i32 i64 f32 f64))) + (type $t6 (func (result i32))) + (type $t7 (func (result i64))) + (type $t8 (func (result f32))) + (type $t9 (func (result f64))) + (type $t10 (func (result i32 i64 f32 f64))) + (type $t11 (func (param i32 i64 f32 f64) (result i32 i64 f32 f64))) + (import "" "no_params_no_results" (func $.f0 (type $t0))) + (import "" "one_i32_param_no_results" (func $.f1 (type $t1))) + (import "" "one_i64_param_no_results" (func $.f2 (type $t2))) + (import "" "one_f32_param_no_results" (func $.f3 (type $t3))) + (import "" "one_f64_param_no_results" (func $.f4 (type $t4))) + (import "" "one_param_of_each_type" (func $.f5 (type $t5))) + (import "" "no_params_one_i32_result" (func $.f6 (type $t6))) + (import "" "no_params_one_i64_result" (func $.f7 (type $t7))) + (import "" "no_params_one_f32_result" (func $.f8 (type $t8))) + (import "" "no_params_one_f64_result" (func $.f9 (type $t9))) + (import "" "one_result_of_each_type" (func $.f10 (type $t10))) + (import "" "one_param_and_result_of_each_type" (func $.f11 (type $t11))) + (import "other" "function_from_module" (func $.f12 (type $t0))) +) diff --git a/crates/misc/dotnet/tests/Modules/FunctionThunking.wasm b/crates/misc/dotnet/tests/Modules/FunctionThunking.wasm new file mode 100644 index 0000000000..a8a67acb1c Binary files /dev/null and b/crates/misc/dotnet/tests/Modules/FunctionThunking.wasm differ diff --git a/crates/misc/dotnet/tests/Modules/FunctionThunking.wat b/crates/misc/dotnet/tests/Modules/FunctionThunking.wat new file mode 100644 index 0000000000..fb487afaa8 --- /dev/null +++ b/crates/misc/dotnet/tests/Modules/FunctionThunking.wat @@ -0,0 +1,20 @@ +(module + (type $FUNCSIG$iii (func (param i32 i32) (result i32))) + (type $FUNCSIG$v (func)) + (import "env" "add" (func $add (param i32 i32) (result i32))) + (import "env" "do_throw" (func $do_throw)) + (table 0 anyfunc) + (memory $0 1) + (export "memory" (memory $0)) + (export "add_wrapper" (func $add_wrapper)) + (export "do_throw_wrapper" (func $do_throw_wrapper)) + (func $add_wrapper (; 2 ;) (param $0 i32) (param $1 i32) (result i32) + (call $add + (get_local $0) + (get_local $1) + ) + ) + (func $do_throw_wrapper (; 3 ;) + (call $do_throw) + ) +) diff --git a/crates/misc/dotnet/tests/Modules/GlobalExports.wasm b/crates/misc/dotnet/tests/Modules/GlobalExports.wasm new file mode 100644 index 0000000000..9e9f358fc5 Binary files /dev/null and b/crates/misc/dotnet/tests/Modules/GlobalExports.wasm differ diff --git a/crates/misc/dotnet/tests/Modules/GlobalExports.wat b/crates/misc/dotnet/tests/Modules/GlobalExports.wat new file mode 100644 index 0000000000..f8ab099c5d --- /dev/null +++ b/crates/misc/dotnet/tests/Modules/GlobalExports.wat @@ -0,0 +1,18 @@ +(module + (global $g1 i32 (i32.const 0)) + (global $g2 (mut i32) (i32.const 1)) + (global $g3 i64 (i64.const 2)) + (global $g4 (mut i64) (i64.const 3)) + (global $g5 f32 (f32.const 4)) + (global $g6 (mut f32) (f32.const 5)) + (global $g7 f64 (f64.const 6)) + (global $g8 (mut f64) (f64.const 7)) + (export "global_i32" (global $g1)) + (export "global_i32_mut" (global $g2)) + (export "global_i64" (global $g3)) + (export "global_i64_mut" (global $g4)) + (export "global_f32" (global $g5)) + (export "global_f32_mut" (global $g6)) + (export "global_f64" (global $g7)) + (export "global_f64_mut" (global $g8)) +) diff --git a/crates/misc/dotnet/tests/Modules/GlobalImportBindings.wasm b/crates/misc/dotnet/tests/Modules/GlobalImportBindings.wasm new file mode 100644 index 0000000000..8933739d65 Binary files /dev/null and b/crates/misc/dotnet/tests/Modules/GlobalImportBindings.wasm differ diff --git a/crates/misc/dotnet/tests/Modules/GlobalImportBindings.wat b/crates/misc/dotnet/tests/Modules/GlobalImportBindings.wat new file mode 100644 index 0000000000..3d5aba2a65 --- /dev/null +++ b/crates/misc/dotnet/tests/Modules/GlobalImportBindings.wat @@ -0,0 +1,22 @@ +(module + (import "" "global_i32_mut" (global $global_i32_mut (mut i32))) + (import "" "global_i32" (global $global_i32 i32)) + (import "" "global_i64_mut" (global $global_i64_mut (mut i64))) + (import "" "global_i64" (global $global_i64 i64)) + (import "" "global_f32_mut" (global $global_f32_mut (mut f32))) + (import "" "global_f32" (global $global_f32 f32)) + (import "" "global_f64_mut" (global $global_f64_mut (mut f64))) + (import "" "global_f64" (global $global_f64 f64)) + (func (export "get_global_i32_mut") (result i32) (global.get $global_i32_mut)) + (func (export "get_global_i32") (result i32) (global.get $global_i32)) + (func (export "set_global_i32_mut") (param i32) (global.set $global_i32_mut (local.get 0))) + (func (export "get_global_i64_mut") (result i64) (global.get $global_i64_mut)) + (func (export "get_global_i64") (result i64) (global.get $global_i64)) + (func (export "set_global_i64_mut") (param i64) (global.set $global_i64_mut (local.get 0))) + (func (export "get_global_f32_mut") (result f32) (global.get $global_f32_mut)) + (func (export "get_global_f32") (result f32) (global.get $global_f32)) + (func (export "set_global_f32_mut") (param f32) (global.set $global_f32_mut (local.get 0))) + (func (export "get_global_f64_mut") (result f64) (global.get $global_f64_mut)) + (func (export "get_global_f64") (result f64) (global.get $global_f64)) + (func (export "set_global_f64_mut") (param f64) (global.set $global_f64_mut (local.get 0))) +) diff --git a/crates/misc/dotnet/tests/Modules/GlobalImports.wasm b/crates/misc/dotnet/tests/Modules/GlobalImports.wasm new file mode 100644 index 0000000000..5a80fbf3e9 Binary files /dev/null and b/crates/misc/dotnet/tests/Modules/GlobalImports.wasm differ diff --git a/crates/misc/dotnet/tests/Modules/GlobalImports.wat b/crates/misc/dotnet/tests/Modules/GlobalImports.wat new file mode 100644 index 0000000000..1a0bbb704f --- /dev/null +++ b/crates/misc/dotnet/tests/Modules/GlobalImports.wat @@ -0,0 +1,11 @@ +(module + (global $g1 (import "" "global_i32") i32) + (global $g2 (import "" "global_i32_mut") (mut i32)) + (global $g3 (import "" "global_i64") i64) + (global $g4 (import "" "global_i64_mut") (mut i64)) + (global $g5 (import "" "global_f32") f32) + (global $g6 (import "" "global_f32_mut") (mut f32)) + (global $g7 (import "" "global_f64") f64) + (global $g8 (import "" "global_f64_mut") (mut f64)) + (global $g9 (import "other" "global_from_module") i32) +) diff --git a/crates/misc/dotnet/tests/Modules/MemoryExports.wasm b/crates/misc/dotnet/tests/Modules/MemoryExports.wasm new file mode 100644 index 0000000000..bc0fe01a33 Binary files /dev/null and b/crates/misc/dotnet/tests/Modules/MemoryExports.wasm differ diff --git a/crates/misc/dotnet/tests/Modules/MemoryExports.wat b/crates/misc/dotnet/tests/Modules/MemoryExports.wat new file mode 100644 index 0000000000..d317e667eb --- /dev/null +++ b/crates/misc/dotnet/tests/Modules/MemoryExports.wat @@ -0,0 +1,11 @@ +(module + (memory (export "mem") 1 2) + (data (i32.const 0) "Hello World") + (data (i32.const 20) "\01") + (data (i32.const 21) "\02\00") + (data (i32.const 23) "\03\00\00\00") + (data (i32.const 27) "\04\00\00\00\00\00\00\00") + (data (i32.const 35) "\00\00\a0\40") + (data (i32.const 39) "\00\00\00\00\00\00\18\40") + (data (i32.const 48) "\07\00\00\00\00\00\00\00") +) diff --git a/crates/misc/dotnet/tests/Modules/MemoryImportBinding.wasm b/crates/misc/dotnet/tests/Modules/MemoryImportBinding.wasm new file mode 100644 index 0000000000..83e5604526 Binary files /dev/null and b/crates/misc/dotnet/tests/Modules/MemoryImportBinding.wasm differ diff --git a/crates/misc/dotnet/tests/Modules/MemoryImportBinding.wat b/crates/misc/dotnet/tests/Modules/MemoryImportBinding.wat new file mode 100644 index 0000000000..a0b519d3cc --- /dev/null +++ b/crates/misc/dotnet/tests/Modules/MemoryImportBinding.wat @@ -0,0 +1,39 @@ +(module + (import "" "mem" (memory 1)) + (data (i32.const 0) "Hello World") + (data (i32.const 20) "\01") + (data (i32.const 21) "\02\00") + (data (i32.const 23) "\03\00\00\00") + (data (i32.const 27) "\04\00\00\00\00\00\00\00") + (data (i32.const 35) "\00\00\a0\40") + (data (i32.const 39) "\00\00\00\00\00\00\18\40") + (data (i32.const 48) "\07\00\00\00\00\00\00\00") + (func (export "ReadByte") (result i32) + i32.const 20 + i32.load8_s + ) + (func (export "ReadInt16") (result i32) + i32.const 21 + i32.load16_s + ) + (func (export "ReadInt32") (result i32) + i32.const 23 + i32.load + ) + (func (export "ReadInt64") (result i64) + i32.const 27 + i64.load + ) + (func (export "ReadFloat32") (result f32) + i32.const 35 + f32.load + ) + (func (export "ReadFloat64") (result f64) + i32.const 39 + f64.load + ) + (func (export "ReadIntPtr") (result i64) + i32.const 48 + i64.load + ) +) diff --git a/crates/misc/dotnet/tests/Modules/MemoryImportFromModule.wasm b/crates/misc/dotnet/tests/Modules/MemoryImportFromModule.wasm new file mode 100644 index 0000000000..73e12cb7cf Binary files /dev/null and b/crates/misc/dotnet/tests/Modules/MemoryImportFromModule.wasm differ diff --git a/crates/misc/dotnet/tests/Modules/MemoryImportFromModule.wat b/crates/misc/dotnet/tests/Modules/MemoryImportFromModule.wat new file mode 100644 index 0000000000..23e4d4a5ea --- /dev/null +++ b/crates/misc/dotnet/tests/Modules/MemoryImportFromModule.wat @@ -0,0 +1,3 @@ +(module + (import "js" "mem" (memory 1 2)) +) diff --git a/crates/misc/dotnet/tests/Modules/MemoryImportNoUpperBound.wasm b/crates/misc/dotnet/tests/Modules/MemoryImportNoUpperBound.wasm new file mode 100644 index 0000000000..813fefe664 Binary files /dev/null and b/crates/misc/dotnet/tests/Modules/MemoryImportNoUpperBound.wasm differ diff --git a/crates/misc/dotnet/tests/Modules/MemoryImportNoUpperBound.wat b/crates/misc/dotnet/tests/Modules/MemoryImportNoUpperBound.wat new file mode 100644 index 0000000000..86dda0097b --- /dev/null +++ b/crates/misc/dotnet/tests/Modules/MemoryImportNoUpperBound.wat @@ -0,0 +1,3 @@ +(module + (import "" "mem" (memory 1)) +) diff --git a/crates/misc/dotnet/tests/Modules/MemoryImportWithUpperBound.wasm b/crates/misc/dotnet/tests/Modules/MemoryImportWithUpperBound.wasm new file mode 100644 index 0000000000..0776ce28e2 Binary files /dev/null and b/crates/misc/dotnet/tests/Modules/MemoryImportWithUpperBound.wasm differ diff --git a/crates/misc/dotnet/tests/Modules/MemoryImportWithUpperBound.wat b/crates/misc/dotnet/tests/Modules/MemoryImportWithUpperBound.wat new file mode 100644 index 0000000000..336177e453 --- /dev/null +++ b/crates/misc/dotnet/tests/Modules/MemoryImportWithUpperBound.wat @@ -0,0 +1,3 @@ +(module + (import "" "mem" (memory 10 100)) +) diff --git a/crates/misc/dotnet/tests/Modules/TableExports.wasm b/crates/misc/dotnet/tests/Modules/TableExports.wasm new file mode 100644 index 0000000000..8581e98b95 Binary files /dev/null and b/crates/misc/dotnet/tests/Modules/TableExports.wasm differ diff --git a/crates/misc/dotnet/tests/Modules/TableExports.wat b/crates/misc/dotnet/tests/Modules/TableExports.wat new file mode 100644 index 0000000000..5284df1b07 --- /dev/null +++ b/crates/misc/dotnet/tests/Modules/TableExports.wat @@ -0,0 +1,8 @@ +(module + (table $t0 1 10 funcref) + (table $t1 10 anyref) + (table $t2 100 1000 funcref) + (export "table1" (table $t0)) + (export "table2" (table $t1)) + (export "table3" (table $t2)) +) diff --git a/crates/misc/dotnet/tests/Modules/TableImports.wasm b/crates/misc/dotnet/tests/Modules/TableImports.wasm new file mode 100644 index 0000000000..0bb86637f4 Binary files /dev/null and b/crates/misc/dotnet/tests/Modules/TableImports.wasm differ diff --git a/crates/misc/dotnet/tests/Modules/TableImports.wat b/crates/misc/dotnet/tests/Modules/TableImports.wat new file mode 100644 index 0000000000..a4c3b094c6 --- /dev/null +++ b/crates/misc/dotnet/tests/Modules/TableImports.wat @@ -0,0 +1,5 @@ +(module + (import "" "table1" (table $t1 10 funcref)) + (import "" "table2" (table $t2 15 anyref)) + (import "other" "table3" (table $t3 1 funcref)) +) diff --git a/crates/misc/dotnet/tests/Modules/Wasi.wasm b/crates/misc/dotnet/tests/Modules/Wasi.wasm new file mode 100644 index 0000000000..62644ebfed Binary files /dev/null and b/crates/misc/dotnet/tests/Modules/Wasi.wasm differ diff --git a/crates/misc/dotnet/tests/Modules/Wasi.wat b/crates/misc/dotnet/tests/Modules/Wasi.wat new file mode 100644 index 0000000000..da028cb4dd --- /dev/null +++ b/crates/misc/dotnet/tests/Modules/Wasi.wat @@ -0,0 +1,66 @@ +(module + (type $t0 (func (param i32 i32) (result i32))) + (type $t1 (func (param i32 i32 i32 i32) (result i32))) + (type $t2 (func (param i32) (result i32))) + (type $t3 (func (param i32 i32 i32 i32 i32 i64 i64 i32 i32) (result i32))) + (import "wasi_snapshot_preview1" "environ_sizes_get" (func $wasi_snapshot_preview1.environ_sizes_get (type $t0))) + (import "wasi_snapshot_preview1" "environ_get" (func $wasi_snapshot_preview1.environ_get (type $t0))) + (import "wasi_snapshot_preview1" "args_sizes_get" (func $wasi_snapshot_preview1.args_sizes_get (type $t0))) + (import "wasi_snapshot_preview1" "args_get" (func $wasi_snapshot_preview1.args_get (type $t0))) + (import "wasi_snapshot_preview1" "fd_write" (func $wasi_snapshot_preview1.fd_write (type $t1))) + (import "wasi_snapshot_preview1" "fd_read" (func $wasi_snapshot_preview1.fd_read (type $t1))) + (import "wasi_snapshot_preview1" "fd_close" (func $wasi_snapshot_preview1.fd_close (type $t2))) + (import "wasi_snapshot_preview1" "path_open" (func $wasi_snapshot_preview1.path_open (type $t3))) + (memory $memory 1) + (export "memory" (memory 0)) + (func $call_environ_sizes_get (type $t0) (param $p0 i32) (param $p1 i32) (result i32) + local.get $p0 + local.get $p1 + call $wasi_snapshot_preview1.environ_sizes_get) + (func $call_environ_get (type $t0) (param $p0 i32) (param $p1 i32) (result i32) + local.get $p0 + local.get $p1 + call $wasi_snapshot_preview1.environ_get) + (func $call_args_sizes_get (type $t0) (param $p0 i32) (param $p1 i32) (result i32) + local.get $p0 + local.get $p1 + call $wasi_snapshot_preview1.args_sizes_get) + (func $call_args_get (type $t0) (param $p0 i32) (param $p1 i32) (result i32) + local.get $p0 + local.get $p1 + call $wasi_snapshot_preview1.args_get) + (func $call_fd_write (type $t1) (param $p0 i32) (param $p1 i32) (param $p2 i32) (param $p3 i32) (result i32) + local.get $p0 + local.get $p1 + local.get $p2 + local.get $p3 + call $wasi_snapshot_preview1.fd_write) + (func $call_fd_read (type $t1) (param $p0 i32) (param $p1 i32) (param $p2 i32) (param $p3 i32) (result i32) + local.get $p0 + local.get $p1 + local.get $p2 + local.get $p3 + call $wasi_snapshot_preview1.fd_read) + (func $call_fd_close (type $t2) (param $p0 i32) (result i32) + local.get $p0 + call $wasi_snapshot_preview1.fd_close) + (func $call_path_open (type $t3) (param $p0 i32) (param $p1 i32) (param $p2 i32) (param $p3 i32) (param $p4 i32) (param $p5 i64) (param $p6 i64) (param $p7 i32) (param $p8 i32) (result i32) + local.get $p0 + local.get $p1 + local.get $p2 + local.get $p3 + local.get $p4 + local.get $p5 + local.get $p6 + local.get $p7 + local.get $p8 + call $wasi_snapshot_preview1.path_open) + (export "call_environ_sizes_get" (func $call_environ_sizes_get)) + (export "call_environ_get" (func $call_environ_get)) + (export "call_args_sizes_get" (func $call_args_sizes_get)) + (export "call_args_get" (func $call_args_get)) + (export "call_fd_write" (func $call_fd_write)) + (export "call_fd_read" (func $call_fd_read)) + (export "call_fd_close" (func $call_fd_close)) + (export "call_path_open" (func $call_path_open)) +) diff --git a/crates/misc/dotnet/tests/TableExportsTests.cs b/crates/misc/dotnet/tests/TableExportsTests.cs new file mode 100644 index 0000000000..ebd86658c6 --- /dev/null +++ b/crates/misc/dotnet/tests/TableExportsTests.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Xunit; + +namespace Wasmtime.Tests +{ + public class TableExportsFixture : ModuleFixture + { + protected override string ModuleFileName => "TableExports.wasm"; + } + + public class TableExportsTests : IClassFixture + { + public TableExportsTests(TableExportsFixture fixture) + { + Fixture = fixture; + } + + private TableExportsFixture Fixture { get; set; } + + [Theory] + [MemberData(nameof(GetTableExports))] + public void ItHasTheExpectedTableExports(string exportName, ValueKind expectedKind, uint expectedMinimum, uint expectedMaximum) + { + var export = Fixture.Module.Exports.Tables.Where(f => f.Name == exportName).FirstOrDefault(); + export.Should().NotBeNull(); + export.Kind.Should().Be(expectedKind); + export.Minimum.Should().Be(expectedMinimum); + export.Maximum.Should().Be(expectedMaximum); + } + + [Fact] + public void ItHasTheExpectedNumberOfExportedTables() + { + GetTableExports().Count().Should().Be(Fixture.Module.Exports.Tables.Count); + } + + public static IEnumerable GetTableExports() + { + yield return new object[] { + "table1", + ValueKind.FuncRef, + 1, + 10 + }; + + yield return new object[] { + "table2", + ValueKind.AnyRef, + 10, + uint.MaxValue + }; + + yield return new object[] { + "table3", + ValueKind.FuncRef, + 100, + 1000 + }; + } + } +} diff --git a/crates/misc/dotnet/tests/TableImportsTests.cs b/crates/misc/dotnet/tests/TableImportsTests.cs new file mode 100644 index 0000000000..265205e0c1 --- /dev/null +++ b/crates/misc/dotnet/tests/TableImportsTests.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Xunit; + +namespace Wasmtime.Tests +{ + public class TableImportsFixture : ModuleFixture + { + protected override string ModuleFileName => "TableImports.wasm"; + } + + public class TableImportsTests : IClassFixture + { + public TableImportsTests(TableImportsFixture fixture) + { + Fixture = fixture; + } + + private TableImportsFixture Fixture { get; set; } + + [Theory] + [MemberData(nameof(GetTableImports))] + public void ItHasTheExpectedTableImports(string importModule, string importName, ValueKind expectedKind, uint expectedMinimum, uint expectedMaximum) + { + var import = Fixture.Module.Imports.Tables.Where(f => f.ModuleName == importModule && f.Name == importName).FirstOrDefault(); + import.Should().NotBeNull(); + import.Kind.Should().Be(expectedKind); + import.Minimum.Should().Be(expectedMinimum); + import.Maximum.Should().Be(expectedMaximum); + } + + [Fact] + public void ItHasTheExpectedNumberOfExportedTables() + { + GetTableImports().Count().Should().Be(Fixture.Module.Imports.Tables.Count); + } + + public static IEnumerable GetTableImports() + { + yield return new object[] { + "", + "table1", + ValueKind.FuncRef, + 10, + uint.MaxValue + }; + + yield return new object[] { + "", + "table2", + ValueKind.AnyRef, + 15, + uint.MaxValue + }; + + yield return new object[] { + "other", + "table3", + ValueKind.FuncRef, + 1, + uint.MaxValue + }; + } + } +} diff --git a/crates/misc/dotnet/tests/TempFile.cs b/crates/misc/dotnet/tests/TempFile.cs new file mode 100644 index 0000000000..6456b37b72 --- /dev/null +++ b/crates/misc/dotnet/tests/TempFile.cs @@ -0,0 +1,24 @@ +using System; +using System.IO; + +namespace Wasmtime.Tests +{ + internal class TempFile : IDisposable + { + public TempFile() + { + Path = System.IO.Path.GetTempFileName(); + } + + public void Dispose() + { + if (Path != null) + { + File.Delete(Path); + Path = null; + } + } + + public string Path { get; private set; } + } +} \ No newline at end of file diff --git a/crates/misc/dotnet/tests/WasiTests.cs b/crates/misc/dotnet/tests/WasiTests.cs new file mode 100644 index 0000000000..06d332634d --- /dev/null +++ b/crates/misc/dotnet/tests/WasiTests.cs @@ -0,0 +1,246 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.IO; +using FluentAssertions; +using Xunit; + +namespace Wasmtime.Tests +{ + public class WasiFixture : ModuleFixture + { + protected override string ModuleFileName => "Wasi.wasm"; + } + + public class WasiTests : IClassFixture + { + public WasiTests(WasiFixture fixture) + { + Fixture = fixture; + } + + private WasiFixture Fixture { get; set; } + + [Fact] + public void ItHasNoEnvironmentByDefault() + { + using var instance = Fixture.Module.Instantiate(new Wasi(Fixture.Module.Store)); + dynamic inst = instance; + + var memory = instance.Externs.Memories[0]; + + Assert.Equal(0, inst.call_environ_sizes_get(0, 4)); + Assert.Equal(0, memory.ReadInt32(0)); + Assert.Equal(0, memory.ReadInt32(4)); + } + + [Fact] + public void ItHasSpecifiedEnvironment() + { + var env = new Dictionary() { + {"FOO", "BAR"}, + {"WASM", "IS"}, + {"VERY", "COOL"}, + }; + + var wasi = new WasiBuilder() + .WithEnvironmentVariables(env.Select(kvp => (kvp.Key, kvp.Value))) + .Build(Fixture.Module.Store); + + using var instance = Fixture.Module.Instantiate(wasi); + dynamic inst = instance; + + var memory = instance.Externs.Memories[0]; + + Assert.Equal(0, inst.call_environ_sizes_get(0, 4)); + Assert.Equal(env.Count, memory.ReadInt32(0)); + Assert.Equal(env.Sum(kvp => kvp.Key.Length + kvp.Value.Length + 2), memory.ReadInt32(4)); + Assert.Equal(0, inst.call_environ_get(0, 4 * env.Count)); + + for (int i = 0; i < env.Count; ++i) + { + var kvp = memory.ReadNullTerminatedString(memory.ReadInt32(i * 4)).Split("="); + Assert.Equal(env[kvp[0]], kvp[1]); + } + } + + [Fact] + public void ItInheritsEnvironment() + { + var wasi = new WasiBuilder() + .WithInheritedEnvironment() + .Build(Fixture.Module.Store); + + using var instance = Fixture.Module.Instantiate(wasi); + dynamic inst = instance; + + var memory = instance.Externs.Memories[0]; + + Assert.Equal(0, inst.call_environ_sizes_get(0, 4)); + Assert.Equal(Environment.GetEnvironmentVariables().Keys.Count, memory.ReadInt32(0)); + } + + [Fact] + public void ItHasNoArgumentsByDefault() + { + using var instance = Fixture.Module.Instantiate(new Wasi(Fixture.Module.Store)); + dynamic inst = instance; + + var memory = instance.Externs.Memories[0]; + + Assert.Equal(0, inst.call_args_sizes_get(0, 4)); + Assert.Equal(0, memory.ReadInt32(0)); + Assert.Equal(0, memory.ReadInt32(4)); + } + + [Fact] + public void ItHasSpecifiedArguments() + { + var args = new List() { + "WASM", + "IS", + "VERY", + "COOL" + }; + + var wasi = new WasiBuilder() + .WithArgs(args) + .Build(Fixture.Module.Store); + + using var instance = Fixture.Module.Instantiate(wasi); + dynamic inst = instance; + + var memory = instance.Externs.Memories[0]; + + Assert.Equal(0, inst.call_args_sizes_get(0, 4)); + Assert.Equal(args.Count, memory.ReadInt32(0)); + Assert.Equal(args.Sum(a => a.Length + 1), memory.ReadInt32(4)); + Assert.Equal(0, inst.call_args_get(0, 4 * args.Count)); + + for (int i = 0; i < args.Count; ++i) + { + var arg = memory.ReadNullTerminatedString(memory.ReadInt32(i * 4)); + Assert.Equal(args[i], arg); + } + } + + [Fact] + public void ItInheritsArguments() + { + var wasi = new WasiBuilder() + .WithInheritedArgs() + .Build(Fixture.Module.Store); + + using var instance = Fixture.Module.Instantiate(wasi); + dynamic inst = instance; + + var memory = instance.Externs.Memories[0]; + + Assert.Equal(0, inst.call_args_sizes_get(0, 4)); + Assert.Equal(Environment.GetCommandLineArgs().Length, memory.ReadInt32(0)); + } + + [Fact] + public void ItSetsStdIn() + { + const string MESSAGE = "WASM IS VERY COOL"; + + using var file = new TempFile(); + File.WriteAllText(file.Path, MESSAGE); + + var wasi = new WasiBuilder() + .WithStandardInput(file.Path) + .Build(Fixture.Module.Store); + + using var instance = Fixture.Module.Instantiate(wasi); + dynamic inst = instance; + + var memory = instance.Externs.Memories[0]; + memory.WriteInt32(0, 8); + memory.WriteInt32(4, MESSAGE.Length); + + Assert.Equal(0, inst.call_fd_read(0, 0, 1, 32)); + Assert.Equal(MESSAGE.Length, memory.ReadInt32(32)); + Assert.Equal(MESSAGE, memory.ReadString(8, MESSAGE.Length)); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + public void ItSetsStdOutAndStdErr(int fd) + { + const string MESSAGE = "WASM IS VERY COOL"; + + using var file = new TempFile(); + + var builder = new WasiBuilder(); + if (fd == 1) + { + builder.WithStandardOutput(file.Path); + } + else if (fd == 2) + { + builder.WithStandardError(file.Path); + } + + var wasi = builder.Build(Fixture.Module.Store); + + using var instance = Fixture.Module.Instantiate(wasi); + dynamic inst = instance; + + var memory = instance.Externs.Memories[0]; + memory.WriteInt32(0, 8); + memory.WriteInt32(4, MESSAGE.Length); + memory.WriteString(8, MESSAGE); + + Assert.Equal(0, inst.call_fd_write(fd, 0, 1, 32)); + Assert.Equal(MESSAGE.Length, memory.ReadInt32(32)); + Assert.Equal(0, inst.call_fd_close(fd)); + Assert.Equal(MESSAGE, File.ReadAllText(file.Path)); + } + + [Fact] + public void ItSetsPreopenDirectories() + { + const string MESSAGE = "WASM IS VERY COOL"; + + using var file = new TempFile(); + + var wasi = new WasiBuilder() + .WithPreopenedDirectory(Path.GetDirectoryName(file.Path), "/foo") + .Build(Fixture.Module.Store); + + using var instance = Fixture.Module.Instantiate(wasi); + dynamic inst = instance; + + var memory = instance.Externs.Memories[0]; + var fileName = Path.GetFileName(file.Path); + memory.WriteString(0, fileName); + + Assert.Equal(0, inst.call_path_open( + 3, + 0, + 0, + fileName.Length, + 0, + 0x40 /* RIGHTS_FD_WRITE */, + 0, + 0, + 64 + ) + ); + + var fileFd = (int) memory.ReadInt32(64); + Assert.True(fileFd > 3); + + memory.WriteInt32(0, 8); + memory.WriteInt32(4, MESSAGE.Length); + memory.WriteString(8, MESSAGE); + + Assert.Equal(0, inst.call_fd_write(fileFd, 0, 1, 64)); + Assert.Equal(MESSAGE.Length, memory.ReadInt32(64)); + Assert.Equal(0, inst.call_fd_close(fileFd)); + Assert.Equal(MESSAGE, File.ReadAllText(file.Path)); + } + } +} diff --git a/crates/misc/dotnet/tests/Wasmtime.Tests.csproj b/crates/misc/dotnet/tests/Wasmtime.Tests.csproj new file mode 100644 index 0000000000..20f4181420 --- /dev/null +++ b/crates/misc/dotnet/tests/Wasmtime.Tests.csproj @@ -0,0 +1,33 @@ + + + + netcoreapp3.0 + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crates/misc/py/.gitignore b/crates/misc/py/.gitignore new file mode 100644 index 0000000000..cdee78184b --- /dev/null +++ b/crates/misc/py/.gitignore @@ -0,0 +1,15 @@ +*.bk +*.swp +*.swo +*.swx +tags +target +Cargo.lock +.*.rustfmt +cranelift.dbg* +rusty-tags.* +*~ +\#*\# +build +dist +*.egg-info diff --git a/crates/misc/py/Cargo.toml b/crates/misc/py/Cargo.toml new file mode 100644 index 0000000000..001ae2930d --- /dev/null +++ b/crates/misc/py/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "wasmtime-py" +version = "0.12.0" +authors = ["The Wasmtime Project Developers"] +description = "Python extension for Wasmtime" +license = "Apache-2.0 WITH LLVM-exception" +categories = ["wasm", "python"] +keywords = ["webassembly", "wasm"] +repository = "https://github.com/bytecodealliance/wasmtime" +readme = "README.md" +edition = "2018" + +[lib] +name = "_wasmtime" +crate-type = ["cdylib"] +test = false +doc = false + +[dependencies] +wasmtime = { path = "../../api", version = "0.12.0" } +wasmtime-interface-types = { path = "../../interface-types", version = "0.12.0" } +wasmtime-wasi = { path = "../../wasi", version = "0.12.0" } +target-lexicon = { version = "0.10.0", default-features = false } +anyhow = "1.0.19" +region = "2.0.0" +wasmparser = "0.51.2" +pyo3 = { version = "0.8.0", features = ["extension-module"] } + +[badges] +maintenance = { status = "actively-developed" } diff --git a/crates/misc/py/LICENSE b/crates/misc/py/LICENSE new file mode 100644 index 0000000000..f9d81955f4 --- /dev/null +++ b/crates/misc/py/LICENSE @@ -0,0 +1,220 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + diff --git a/crates/misc/py/README.md b/crates/misc/py/README.md new file mode 100644 index 0000000000..4e38bd46fa --- /dev/null +++ b/crates/misc/py/README.md @@ -0,0 +1,20 @@ +Python 3 extension for interface with Wasmtime/Cranelift. + +# Build + +First, you'll need to install some Python dependencies: + +``` +$ pip3 install setuptools wheel==0.31.1 setuptools-rust +``` + +Next you can build the extension with: + +``` +rustup run nightly python3 setup.py build +``` + +Note that a nightly version of Rust is required due to our usage of PyO3. + +This will create a directory called `build/lib` which you can add to +`PYTHONPATH` in order to get `import wasmtime` working. diff --git a/crates/misc/py/examples/gcd/.gitignore b/crates/misc/py/examples/gcd/.gitignore new file mode 100644 index 0000000000..46cc3441e8 --- /dev/null +++ b/crates/misc/py/examples/gcd/.gitignore @@ -0,0 +1 @@ +gcd.wasm diff --git a/crates/misc/py/examples/gcd/README.md b/crates/misc/py/examples/gcd/README.md new file mode 100644 index 0000000000..f8be453d28 --- /dev/null +++ b/crates/misc/py/examples/gcd/README.md @@ -0,0 +1,15 @@ +# Build example's file + +To build `gcd.wasm` use rustc (nightly) for wasm32 target with debug information: + +``` +rustc +nightly --target=wasm32-unknown-unknown -g gcd.rs --crate-type=cdylib +``` + +# Run example + +Point path to the built wasmtime_py library location when running python, e.g. + +``` +PYTHONPATH=../../target/debug python3 run.py +``` diff --git a/crates/misc/py/examples/gcd/gcd.rs b/crates/misc/py/examples/gcd/gcd.rs new file mode 100644 index 0000000000..2f6ef85b62 --- /dev/null +++ b/crates/misc/py/examples/gcd/gcd.rs @@ -0,0 +1,17 @@ +#[inline(never)] +#[no_mangle] +pub extern "C" fn gcd(m_: u32, n_: u32) -> u32 { + let mut m = m_; + let mut n = n_; + while m > 0 { + let tmp = m; + m = n % m; + n = tmp; + } + return n; +} + +#[no_mangle] +pub extern "C" fn test() -> u32 { + gcd(24, 9) +} diff --git a/crates/misc/py/examples/gcd/run.py b/crates/misc/py/examples/gcd/run.py new file mode 100644 index 0000000000..648d1ecc6d --- /dev/null +++ b/crates/misc/py/examples/gcd/run.py @@ -0,0 +1,5 @@ +import wasmtime +import gcd + +print("gcd(27, 6) =", gcd.gcd(27, 6)) + diff --git a/crates/misc/py/examples/import/.gitignore b/crates/misc/py/examples/import/.gitignore new file mode 100644 index 0000000000..2a6475ec63 --- /dev/null +++ b/crates/misc/py/examples/import/.gitignore @@ -0,0 +1,3 @@ +import.wasm +main.wasm +__pycache__ \ No newline at end of file diff --git a/crates/misc/py/examples/import/README.md b/crates/misc/py/examples/import/README.md new file mode 100644 index 0000000000..db56dcbe9b --- /dev/null +++ b/crates/misc/py/examples/import/README.md @@ -0,0 +1,15 @@ +# Build example's file + +To build `demo.wasm` use rustc (nightly) for wasm32 target with debug information: + +``` +rustc +nightly --target=wasm32-unknown-unknown demo.rs --crate-type=cdylib +``` + +# Run example + +Point path to the built `wasmtime_py` library location when running python, e.g. + +``` +PYTHONPATH=../../target/debug python3 run.py +``` diff --git a/crates/misc/py/examples/import/demo.rs b/crates/misc/py/examples/import/demo.rs new file mode 100644 index 0000000000..cbe9cae958 --- /dev/null +++ b/crates/misc/py/examples/import/demo.rs @@ -0,0 +1,11 @@ +extern "C" { + fn callback(s: *const u8, s_len: u32) -> u32; +} + +#[no_mangle] +pub extern "C" fn test() { + let msg = "Hello, world!"; + unsafe { + callback(msg.as_ptr(), msg.len() as u32); + } +} diff --git a/crates/misc/py/examples/import/env.py b/crates/misc/py/examples/import/env.py new file mode 100644 index 0000000000..83da582a16 --- /dev/null +++ b/crates/misc/py/examples/import/env.py @@ -0,0 +1,10 @@ +def callback(msg_p: 'i32', msg_len: 'i32') -> 'i32': + print('callback:', msg_p, msg_len) + +# global memory +# mv = memoryview(memory) + +# msg = bytes(mv[msg_p:(msg_p + msg_len)]).decode('utf-8') +# print(msg) + + return 42 diff --git a/crates/misc/py/examples/import/run.py b/crates/misc/py/examples/import/run.py new file mode 100644 index 0000000000..f39baccd49 --- /dev/null +++ b/crates/misc/py/examples/import/run.py @@ -0,0 +1,4 @@ +import wasmtime +import demo + +demo.test() diff --git a/crates/misc/py/examples/two_modules/.gitignore b/crates/misc/py/examples/two_modules/.gitignore new file mode 100644 index 0000000000..64ee0b754f --- /dev/null +++ b/crates/misc/py/examples/two_modules/.gitignore @@ -0,0 +1,3 @@ +one.wasm +two.wasm +__pycache__ \ No newline at end of file diff --git a/crates/misc/py/examples/two_modules/README.md b/crates/misc/py/examples/two_modules/README.md new file mode 100644 index 0000000000..e23101408b --- /dev/null +++ b/crates/misc/py/examples/two_modules/README.md @@ -0,0 +1,20 @@ +# Build example's file + +To build `one.wasm` use rustc (nightly) for wasm32 target with debug information: + +``` +rustc +nightly --target=wasm32-unknown-unknown one.rs --crate-type=cdylib +``` + +To build `two.wasm` use wabt. +``` +wat2wasm two.wat -o two.wasm +``` + +# Run example + +Point path to the built wasmtime_py library location when running python, e.g. + +``` +PYTHONPATH=../../target/debug python3 run.py +``` diff --git a/crates/misc/py/examples/two_modules/env.py b/crates/misc/py/examples/two_modules/env.py new file mode 100644 index 0000000000..5af7f13e3f --- /dev/null +++ b/crates/misc/py/examples/two_modules/env.py @@ -0,0 +1,2 @@ +def answer() -> 'i32': + return 42 diff --git a/crates/misc/py/examples/two_modules/one.rs b/crates/misc/py/examples/two_modules/one.rs new file mode 100644 index 0000000000..032b83f822 --- /dev/null +++ b/crates/misc/py/examples/two_modules/one.rs @@ -0,0 +1,16 @@ +extern "C" { + fn answer() -> u32; +} + +// For the purpose of this wasm example, we don't worry about multi-threading, +// and will be using the PLACE in unsafe manner below. +static mut PLACE: u32 = 23; + +#[no_mangle] +pub extern "C" fn bar() -> *const u32 { + unsafe { + PLACE = answer(); + // Return a pointer to the exported memory. + (&PLACE) as *const u32 + } +} diff --git a/crates/misc/py/examples/two_modules/run.py b/crates/misc/py/examples/two_modules/run.py new file mode 100644 index 0000000000..f5b0aa6c25 --- /dev/null +++ b/crates/misc/py/examples/two_modules/run.py @@ -0,0 +1,4 @@ +import wasmtime +import two + +print("answer() returned", two.ask()) diff --git a/crates/misc/py/examples/two_modules/two.wat b/crates/misc/py/examples/two_modules/two.wat new file mode 100644 index 0000000000..265d633284 --- /dev/null +++ b/crates/misc/py/examples/two_modules/two.wat @@ -0,0 +1,11 @@ +(module + (import "one" "memory" (memory $memory 0)) + (import "one" "bar" (func $bar (result i32))) + (export "ask" (func $foo)) + + (func $foo (result i32) + call $bar + ;; Deference returned pointer to the value from imported memory + i32.load + ) +) diff --git a/crates/misc/py/python/wasmtime/__init__.py b/crates/misc/py/python/wasmtime/__init__.py new file mode 100644 index 0000000000..8c611175fd --- /dev/null +++ b/crates/misc/py/python/wasmtime/__init__.py @@ -0,0 +1,49 @@ +from .lib_wasmtime import imported_modules, instantiate +import sys +import os.path + +from importlib import import_module +from importlib.abc import Loader, MetaPathFinder +from importlib.util import spec_from_file_location + +# Mostly copied from +# https://stackoverflow.com/questions/43571737/how-to-implement-an-import-hook-that-can-modify-the-source-code-on-the-fly-using +class MyMetaFinder(MetaPathFinder): + def find_spec(self, fullname, path, target=None): + if path is None or path == "": + path = [os.getcwd()] # top level import -- + path.extend(sys.path) + if "." in fullname: + *parents, name = fullname.split(".") + else: + name = fullname + for entry in path: + filename = os.path.join(entry, name + ".wasm") + if not os.path.exists(filename): + continue + + return spec_from_file_location(fullname, filename, loader=MyLoader(filename)) + return None + +class MyLoader(Loader): + def __init__(self, filename): + self.filename = filename + + def create_module(self, spec): + return None # use default module creation semantics + + def exec_module(self, module): + with open(self.filename, "rb") as f: + data = f.read() + + imports = {} + for module_name, fields in imported_modules(data).items(): + imports[module_name] = {} + imported_module = import_module(module_name) + for field_name in fields: + imports[module_name][field_name] = imported_module.__dict__[field_name] + + res = instantiate(data, imports) + module.__dict__.update(res.instance.exports) + +sys.meta_path.insert(0, MyMetaFinder()) diff --git a/crates/misc/py/setup.py b/crates/misc/py/setup.py new file mode 100644 index 0000000000..9ad1e3f075 --- /dev/null +++ b/crates/misc/py/setup.py @@ -0,0 +1,31 @@ +from setuptools import setup +from setuptools_rust import Binding, RustExtension + + +def no_tag_default_to_dev(version): + if version.exact: + return version.format_with("{tag}") + return "0.0.1" + + +setup(name='wasmtime', + classifiers=[ + "Development Status :: 1 - Planning", + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Rust", + "Operating System :: POSIX", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + ], + packages=['wasmtime'], + package_dir={'wasmtime': 'python/wasmtime'}, + use_scm_version = { + "root": "../../..", + "relative_to": __file__, + "version_scheme": no_tag_default_to_dev, + "local_scheme": lambda _: "", + }, + setup_requires=['setuptools_scm'], + rust_extensions=[RustExtension('wasmtime.lib_wasmtime', 'Cargo.toml', binding=Binding.PyO3)], + zip_safe=False) diff --git a/crates/misc/py/src/function.rs b/crates/misc/py/src/function.rs new file mode 100644 index 0000000000..4b73bf9430 --- /dev/null +++ b/crates/misc/py/src/function.rs @@ -0,0 +1,154 @@ +//! Support for a calling of a bounds (exported) function. + +use crate::value::{pyobj_to_value, value_to_pyobj}; +use pyo3::exceptions::Exception; +use pyo3::prelude::*; +use pyo3::types::{PyAny, PyDict, PyTuple}; +use std::rc::Rc; +use wasmtime_interface_types::ModuleData; + +// TODO support non-export functions +#[pyclass] +pub struct Function { + pub instance: wasmtime::Instance, + pub export_name: String, + pub args_types: Vec, + pub data: Rc, +} + +impl Function { + pub fn func(&self) -> wasmtime::Func { + let e = self + .instance + .get_export(&self.export_name) + .expect("named export") + .clone(); + e.func().expect("function export").clone() + } +} + +#[pymethods] +impl Function { + #[__call__] + #[args(args = "*")] + fn call(&self, py: Python, args: &PyTuple) -> PyResult { + let mut runtime_args = Vec::new(); + for item in args.iter() { + runtime_args.push(pyobj_to_value(py, item)?); + } + let results = self + .data + .invoke_export(&self.instance, self.export_name.as_str(), &runtime_args) + .map_err(crate::err2py)?; + let mut py_results = Vec::new(); + for result in results { + py_results.push(value_to_pyobj(py, result)?); + } + if py_results.len() == 1 { + Ok(py_results[0].clone_ref(py)) + } else { + Ok(PyTuple::new(py, py_results).to_object(py)) + } + } +} + +fn parse_annotation_type(s: &str) -> wasmtime::ValType { + match s { + "I32" | "i32" => wasmtime::ValType::I32, + "I64" | "i64" => wasmtime::ValType::I64, + "F32" | "f32" => wasmtime::ValType::F32, + "F64" | "f64" => wasmtime::ValType::F64, + _ => panic!("unknown type in annotations"), + } +} + +struct WrappedFn { + func: PyObject, + returns_types: Vec, +} + +impl WrappedFn { + pub fn new(func: PyObject, returns_types: Vec) -> Self { + WrappedFn { + func, + returns_types, + } + } +} + +impl wasmtime::Callable for WrappedFn { + fn call( + &self, + params: &[wasmtime::Val], + returns: &mut [wasmtime::Val], + ) -> Result<(), wasmtime::Trap> { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let params = params + .iter() + .map(|p| match p { + wasmtime::Val::I32(i) => i.clone().into_py(py), + wasmtime::Val::I64(i) => i.clone().into_py(py), + _ => { + panic!(); + } + }) + .collect::>(); + + let result = self + .func + .call(py, PyTuple::new(py, params), None) + .expect("TODO: convert result to trap"); + + let result = if let Ok(t) = result.cast_as::(py) { + t + } else { + if result.is_none() { + PyTuple::empty(py) + } else { + PyTuple::new(py, &[result]) + } + }; + for (i, ty) in self.returns_types.iter().enumerate() { + let result_item = result.get_item(i); + returns[i] = match ty { + wasmtime::ValType::I32 => wasmtime::Val::I32(result_item.extract::().unwrap()), + wasmtime::ValType::I64 => wasmtime::Val::I64(result_item.extract::().unwrap()), + _ => { + panic!(); + } + }; + } + Ok(()) + } +} + +pub fn wrap_into_pyfunction(store: &wasmtime::Store, callable: &PyAny) -> PyResult { + if !callable.hasattr("__annotations__")? { + // TODO support calls without annotations? + return Err(PyErr::new::( + "import is not a function".to_string(), + )); + } + + let annot = callable.getattr("__annotations__")?.cast_as::()?; + let mut params = Vec::new(); + let mut returns = Vec::new(); + for (name, value) in annot.iter() { + let ty = parse_annotation_type(&value.to_string()); + match name.to_string().as_str() { + "return" => returns.push(ty), + _ => params.push(ty), + } + } + + let ft = wasmtime::FuncType::new( + params.into_boxed_slice(), + returns.clone().into_boxed_slice(), + ); + + let gil = Python::acquire_gil(); + let wrapped = WrappedFn::new(callable.to_object(gil.python()), returns); + Ok(wasmtime::Func::new(store, ft, Rc::new(wrapped))) +} diff --git a/crates/misc/py/src/instance.rs b/crates/misc/py/src/instance.rs new file mode 100644 index 0000000000..717d24aa2a --- /dev/null +++ b/crates/misc/py/src/instance.rs @@ -0,0 +1,60 @@ +//! WebAssembly Instance API object. + +use crate::function::Function; +use crate::memory::Memory; +use pyo3::prelude::*; +use pyo3::types::PyDict; +use std::rc::Rc; +use wasmtime_interface_types::ModuleData; + +#[pyclass] +pub struct Instance { + pub instance: wasmtime::Instance, + pub data: Rc, +} + +#[pymethods] +impl Instance { + #[getter(exports)] + fn get_exports(&mut self) -> PyResult { + let gil = Python::acquire_gil(); + let py = gil.python(); + let exports = PyDict::new(py); + let module = self.instance.module().clone(); + for (i, e) in module.exports().iter().enumerate() { + match e.ty() { + wasmtime::ExternType::Func(ft) => { + let mut args_types = Vec::new(); + for ty in ft.params().iter() { + args_types.push(ty.clone()); + } + let f = Py::new( + py, + Function { + instance: self.instance.clone(), + data: self.data.clone(), + export_name: e.name().to_string(), + args_types, + }, + )?; + exports.set_item(e.name().to_string(), f)?; + } + wasmtime::ExternType::Memory(_) => { + let f = Py::new( + py, + Memory { + memory: self.instance.exports()[i].memory().unwrap().clone(), + }, + )?; + exports.set_item(e.name().to_string(), f)?; + } + _ => { + // Skip unknown export type. + continue; + } + } + } + + Ok(exports.to_object(py)) + } +} diff --git a/crates/misc/py/src/lib.rs b/crates/misc/py/src/lib.rs new file mode 100644 index 0000000000..b9c8d70874 --- /dev/null +++ b/crates/misc/py/src/lib.rs @@ -0,0 +1,178 @@ +use crate::function::{wrap_into_pyfunction, Function}; +use crate::instance::Instance; +use crate::memory::Memory; +use crate::module::Module; +use pyo3::exceptions::Exception; +use pyo3::prelude::*; +use pyo3::types::{PyAny, PyBytes, PyDict, PySet}; +use pyo3::wrap_pyfunction; +use std::rc::Rc; +use wasmtime_interface_types::ModuleData; + +mod function; +mod instance; +mod memory; +mod module; +mod value; + +fn err2py(err: anyhow::Error) -> PyErr { + PyErr::new::(format!("{:?}", err)) +} + +#[pyclass] +pub struct InstantiateResultObject { + instance: Py, + module: Py, +} + +#[pymethods] +impl InstantiateResultObject { + #[getter(instance)] + fn get_instance(&self) -> PyResult> { + let gil = Python::acquire_gil(); + let py = gil.python(); + Ok(self.instance.clone_ref(py)) + } + + #[getter(module)] + fn get_module(&self) -> PyResult> { + let gil = Python::acquire_gil(); + let py = gil.python(); + Ok(self.module.clone_ref(py)) + } +} + +fn find_export_in(obj: &PyAny, store: &wasmtime::Store, name: &str) -> PyResult { + let obj = obj.cast_as::()?; + + Ok(if let Some(item) = obj.get_item(name) { + if item.is_callable() { + if item.get_type().is_subclass::()? { + let wasm_fn = item.cast_as::()?; + wasm_fn.func().into() + } else { + wrap_into_pyfunction(store, item)?.into() + } + } else if item.get_type().is_subclass::()? { + let wasm_mem = item.cast_as::()?; + wasm_mem.memory.clone().into() + } else { + return Err(PyErr::new::(format!( + "unsupported import type {}", + name + ))); + } + } else { + return Err(PyErr::new::(format!( + "import {} is not found", + name + ))); + }) +} + +/// WebAssembly instantiate API method. +#[pyfunction] +pub fn instantiate( + py: Python, + buffer_source: &PyBytes, + import_obj: &PyDict, +) -> PyResult> { + let wasm_data = buffer_source.as_bytes(); + + let engine = wasmtime::Engine::new(&wasmtime::Config::new().wasm_multi_value(true)); + let store = wasmtime::Store::new(&engine); + + let module = wasmtime::Module::new(&store, wasm_data).map_err(err2py)?; + + let data = Rc::new(ModuleData::new(wasm_data).map_err(err2py)?); + + // If this module expects to be able to use wasi then go ahead and hook + // that up into the imported crates. + let wasi = if let Some(module_name) = data.find_wasi_module_name() { + let cx = wasmtime_wasi::WasiCtxBuilder::new() + .build() + .map_err(|e| err2py(e.into()))?; + let wasi = wasmtime_wasi::Wasi::new(&store, cx); + Some((module_name, wasi)) + } else { + None + }; + + let mut imports: Vec = Vec::new(); + for i in module.imports() { + let module_name = i.module(); + if let Some(m) = import_obj.get_item(module_name) { + let e = find_export_in(m, &store, i.name())?; + imports.push(e); + } else if wasi.is_some() && module_name == wasi.as_ref().unwrap().0 { + let e = wasi + .as_ref() + .unwrap() + .1 + .get_export(i.name()) + .ok_or_else(|| { + PyErr::new::(format!("wasi export {} is not found", i.name(),)) + })?; + imports.push(e.clone().into()); + } else { + return Err(PyErr::new::(format!( + "imported module {} is not found", + module_name + ))); + } + } + + let instance = wasmtime::Instance::new(&module, &imports) + .map_err(|t| PyErr::new::(format!("instantiated with trap {:?}", t)))?; + + let module = Py::new(py, Module { module })?; + + let instance = Py::new(py, Instance { instance, data })?; + + Py::new(py, InstantiateResultObject { instance, module }) +} + +#[pyfunction] +pub fn imported_modules<'p>(py: Python<'p>, buffer_source: &PyBytes) -> PyResult<&'p PyDict> { + let wasm_data = buffer_source.as_bytes(); + let dict = PyDict::new(py); + // TODO: error handling + let mut parser = wasmparser::ModuleReader::new(wasm_data).unwrap(); + while !parser.eof() { + let section = parser.read().unwrap(); + match section.code { + wasmparser::SectionCode::Import => {} + _ => continue, + }; + let reader = section.get_import_section_reader().unwrap(); + for import in reader { + let import = import.unwrap(); + // Skip over wasi-looking imports since those aren't imported from + // Python but rather they're implemented natively. + if wasmtime_wasi::is_wasi_module(import.module) { + continue; + } + let set = match dict.get_item(import.module) { + Some(set) => set.downcast_ref::().unwrap(), + None => { + let set = PySet::new::(py, &[])?; + dict.set_item(import.module, set)?; + set + } + }; + set.add(import.field)?; + } + } + Ok(dict) +} + +#[pymodule] +fn lib_wasmtime(_: Python, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_wrapped(wrap_pyfunction!(instantiate))?; + m.add_wrapped(wrap_pyfunction!(imported_modules))?; + Ok(()) +} diff --git a/crates/misc/py/src/memory.rs b/crates/misc/py/src/memory.rs new file mode 100644 index 0000000000..1a43c82d84 --- /dev/null +++ b/crates/misc/py/src/memory.rs @@ -0,0 +1,82 @@ +//! WebAssembly Memory API object. + +use pyo3::class::PyBufferProtocol; +use pyo3::exceptions::BufferError; +use pyo3::ffi; +use pyo3::prelude::*; +use std::ffi::CStr; +use std::os::raw::{c_int, c_void}; +use std::ptr; + +#[pyclass] +pub struct Memory { + pub memory: wasmtime::Memory, +} + +#[pymethods] +impl Memory { + #[getter(current)] + pub fn current(&self) -> u32 { + self.memory.size() + } + + pub fn grow(&self, _number: u32) -> u32 { + (-1i32) as u32 + } +} + +#[pyproto] +impl PyBufferProtocol for Memory { + fn bf_getbuffer(&self, view: *mut ffi::Py_buffer, flags: c_int) -> PyResult<()> { + if view.is_null() { + return Err(BufferError::py_err("View is null")); + } + + unsafe { + /* + As a special case, for temporary buffers that are wrapped by + PyMemoryView_FromBuffer() or PyBuffer_FillInfo() this field is NULL. + In general, exporting objects MUST NOT use this scheme. + */ + (*view).obj = ptr::null_mut(); + } + + let readonly = if (flags & ffi::PyBUF_WRITABLE) == ffi::PyBUF_WRITABLE { + 0 + } else { + 1 + }; + + unsafe { + let base = self.memory.data_ptr(); + let current_length = self.memory.data_size(); + + (*view).buf = base as *mut c_void; + (*view).len = current_length as isize; + (*view).readonly = readonly; + (*view).itemsize = 1; + + (*view).format = ptr::null_mut(); + if (flags & ffi::PyBUF_FORMAT) == ffi::PyBUF_FORMAT { + let msg = CStr::from_bytes_with_nul(b"B\0").unwrap(); + (*view).format = msg.as_ptr() as *mut _; + } + + (*view).ndim = 1; + (*view).shape = ptr::null_mut(); + if (flags & ffi::PyBUF_ND) == ffi::PyBUF_ND { + (*view).shape = (&((*view).len)) as *const _ as *mut _; + } + + (*view).strides = ptr::null_mut(); + if (flags & ffi::PyBUF_STRIDES) == ffi::PyBUF_STRIDES { + (*view).strides = &((*view).itemsize) as *const _ as *mut _; + } + + (*view).suboffsets = ptr::null_mut(); + (*view).internal = ptr::null_mut(); + } + + Ok(()) + } +} diff --git a/crates/misc/py/src/module.rs b/crates/misc/py/src/module.rs new file mode 100644 index 0000000000..4c7e4a30c2 --- /dev/null +++ b/crates/misc/py/src/module.rs @@ -0,0 +1,8 @@ +//! WebAssembly Module API object. + +use pyo3::prelude::*; + +#[pyclass] +pub struct Module { + pub module: wasmtime::Module, +} diff --git a/crates/misc/py/src/value.rs b/crates/misc/py/src/value.rs new file mode 100644 index 0000000000..74ab7c61ac --- /dev/null +++ b/crates/misc/py/src/value.rs @@ -0,0 +1,38 @@ +//! Utility functions to handle values conversion between abstractions/targets. + +use pyo3::exceptions::Exception; +use pyo3::prelude::*; +use pyo3::types::PyAny; +use wasmtime_interface_types::Value; + +pub fn pyobj_to_value(_: Python, p: &PyAny) -> PyResult { + if let Ok(n) = p.extract() { + Ok(Value::I32(n)) + } else if let Ok(n) = p.extract() { + Ok(Value::U32(n)) + } else if let Ok(n) = p.extract() { + Ok(Value::I64(n)) + } else if let Ok(n) = p.extract() { + Ok(Value::U64(n)) + } else if let Ok(n) = p.extract() { + Ok(Value::F64(n)) + } else if let Ok(n) = p.extract() { + Ok(Value::F32(n)) + } else if let Ok(s) = p.extract() { + Ok(Value::String(s)) + } else { + Err(PyErr::new::("unsupported value type")) + } +} + +pub fn value_to_pyobj(py: Python, value: Value) -> PyResult { + Ok(match value { + Value::I32(i) => i.into_py(py), + Value::U32(i) => i.into_py(py), + Value::I64(i) => i.into_py(py), + Value::U64(i) => i.into_py(py), + Value::F32(i) => i.into_py(py), + Value::F64(i) => i.into_py(py), + Value::String(i) => i.into_py(py), + }) +} diff --git a/crates/misc/rust/Cargo.toml b/crates/misc/rust/Cargo.toml new file mode 100644 index 0000000000..d5d2d53309 --- /dev/null +++ b/crates/misc/rust/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "wasmtime-rust" +version = "0.12.0" +authors = ["Alex Crichton "] +description = "Rust extension for Wasmtime" +license = "Apache-2.0 WITH LLVM-exception" +categories = ["wasm"] +keywords = ["webassembly", "wasm"] +repository = "https://github.com/bytecodealliance/wasmtime" +readme = "README.md" +edition = "2018" + +[lib] +test = false +doctest = false + +[dependencies] +wasmtime-interface-types = { path = "../../interface-types", version = "0.12.0" } +wasmtime-rust-macro = { path = "./macro", version = "0.12.0" } +wasmtime-wasi = { path = "../../wasi", version = "0.12.0" } +wasmtime = { path = "../../api", version = "0.12.0" } +anyhow = "1.0.19" + +[badges] +maintenance = { status = "actively-developed" } diff --git a/crates/misc/rust/README.md b/crates/misc/rust/README.md new file mode 100644 index 0000000000..498e29160b --- /dev/null +++ b/crates/misc/rust/README.md @@ -0,0 +1,37 @@ +# `wasmtime-rust` - Using WebAssembly from Rust + +This crate is intended to be an example of how to load WebAssembly files from a +native Rust application. You can always use `wasmtime` and its family of crates +directly, but the purpose of this crate is to provide an ergonomic macro: + +```rust +#[wasmtime_rust::wasmtime] +trait WasmMarkdown { + fn render(&mut self, input: &str) -> String; +} + +fn main() -> anyhow::Result<()> { + let mut markdown = WasmMarkdown::load_file("markdown.wasm")?; + println!("{}", markdown.render("# Hello, Rust!")); + + Ok(()) +} +``` + +The `wasmtime` macro defined in the `wasmtime-rust` crate is placed on a `trait` +which includes the set of functionality which a wasm module should export. In +this case we're expecting one `render` function which takes and returns a +string. + +The macro expands to a `struct` with all of the methods on the trait (they must +all be `&mut self`) and one function called `load_file` to actually instantiate +the module. + +Note that this macro is still in early stages of development, so error messages +aren't great yet and all functionality isn't supported yet. + +## Missing features + +Currently if the wasm module imports any symbols outside of the WASI namespace +the module will not load. It's intended that support for this will be added soon +though! diff --git a/crates/misc/rust/examples/markdown.rs b/crates/misc/rust/examples/markdown.rs new file mode 100644 index 0000000000..dde903c574 --- /dev/null +++ b/crates/misc/rust/examples/markdown.rs @@ -0,0 +1,13 @@ +use wasmtime_rust::wasmtime; + +#[wasmtime] +trait WasmMarkdown { + fn render(&mut self, input: &str) -> String; +} + +fn main() -> anyhow::Result<()> { + let mut markdown = WasmMarkdown::load_file("markdown.wasm")?; + println!("{}", markdown.render("# Hello, Rust!")); + + Ok(()) +} diff --git a/crates/misc/rust/macro/Cargo.toml b/crates/misc/rust/macro/Cargo.toml new file mode 100644 index 0000000000..f84ffef2ec --- /dev/null +++ b/crates/misc/rust/macro/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "wasmtime-rust-macro" +version = "0.12.0" +authors = ["Alex Crichton "] +description = "Macro support crate for wasmtime-rust" +license = "Apache-2.0 WITH LLVM-exception" +repository = "https://github.com/bytecodealliance/wasmtime" +readme = "README.md" +edition = "2018" + +[lib] +proc-macro = true +test = false +doctest = false + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "1.0", features = ['full'] } + +[badges] +maintenance = { status = "actively-developed" } diff --git a/crates/misc/rust/macro/README.md b/crates/misc/rust/macro/README.md new file mode 100644 index 0000000000..8199860771 --- /dev/null +++ b/crates/misc/rust/macro/README.md @@ -0,0 +1,5 @@ +# `wasmtime-rust-macro` + +This is the actual definition of the `#[wasmtime]` macro, but it's intended that +this crate isn't used directly but rather the `wasmtime-rust` crate is used +instead. diff --git a/crates/misc/rust/macro/src/lib.rs b/crates/misc/rust/macro/src/lib.rs new file mode 100644 index 0000000000..92c14ae92d --- /dev/null +++ b/crates/misc/rust/macro/src/lib.rs @@ -0,0 +1,185 @@ +extern crate proc_macro; + +use proc_macro2::TokenStream; +use quote::quote; +use syn::spanned::Spanned; + +#[proc_macro_attribute] +pub fn wasmtime( + _attr: proc_macro::TokenStream, + item: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + let item = syn::parse_macro_input!(item as syn::ItemTrait); + expand(item).unwrap_or_else(|e| e.to_compile_error()).into() +} + +fn expand(item: syn::ItemTrait) -> syn::Result { + let definition = generate_struct(&item)?; + let load = generate_load(&item)?; + let methods = generate_methods(&item)?; + let name = &item.ident; + + Ok(quote! { + #definition + impl #name { + #load + #methods + } + }) +} + +fn generate_struct(item: &syn::ItemTrait) -> syn::Result { + let vis = &item.vis; + let name = &item.ident; + let root = root(); + Ok(quote! { + #vis struct #name { + instance: #root::wasmtime::Instance, + data: #root::wasmtime_interface_types::ModuleData, + } + }) +} + +fn generate_load(item: &syn::ItemTrait) -> syn::Result { + let vis = &item.vis; + let name = &item.ident; + let root = root(); + Ok(quote! { + #vis fn load_file(path: impl AsRef) -> #root::anyhow::Result<#name> { + Self::load_bytes(std::fs::read(path)?) + } + + #vis fn load_bytes(bytes: impl AsRef<[u8]>) -> #root::anyhow::Result<#name> { + use #root::wasmtime::{Config, Extern, Engine, Store, Instance, Module}; + use #root::anyhow::{bail, format_err}; + + let engine = Engine::new(Config::new().wasm_multi_value(true)); + let store = Store::new(&engine); + + let data = #root::wasmtime_interface_types::ModuleData::new(bytes.as_ref())?; + + let module = Module::new(&store, bytes.as_ref())?; + + let mut imports: Vec = Vec::new(); + if let Some(module_name) = data.find_wasi_module_name() { + let wasi_cx = #root::wasmtime_wasi::WasiCtxBuilder::new().build()?; + let wasi = #root::wasmtime_wasi::Wasi::new(&store, wasi_cx); + for i in module.imports().iter() { + if i.module() != module_name { + bail!("unknown import module {}", i.module()); + } + if let Some(export) = wasi.get_export(i.name()) { + imports.push(export.clone().into()); + } else { + bail!("unknown import {}:{}", i.module(), i.name()) + } + } + } + let instance = + Instance::new(&module, &imports).map_err(|t| format_err!("instantiation trap: {:?}", t))?; + + Ok(#name { instance, data }) + } + }) +} + +fn generate_methods(item: &syn::ItemTrait) -> syn::Result { + macro_rules! bail { + ($e:expr, $($fmt:tt)*) => ( + return Err(syn::Error::new($e.span(), format!($($fmt)*))); + ) + } + let mut result = TokenStream::new(); + let root = root(); + let vis = &item.vis; + + for item in item.items.iter() { + let method = match item { + syn::TraitItem::Method(f) => f, + other => bail!(other, "only methods are allowed"), + }; + if let Some(e) = &method.default { + bail!(e, "cannot specify an implementation of methods"); + } + if let Some(t) = &method.sig.constness { + bail!(t, "cannot be `const`"); + } + if let Some(t) = &method.sig.asyncness { + bail!(t, "cannot be `async`"); + } + match &method.sig.inputs.first() { + Some(syn::FnArg::Receiver(_)) => {} + Some(t) => bail!(t, "first arugment needs to be \"self\""), + None => bail!( + method.sig, + "trait method requires at least one argument which needs to be \"self\"" + ), + } + + let mut args = Vec::new(); + for arg in method.sig.inputs.iter() { + let arg = match arg { + syn::FnArg::Receiver(_) => continue, + syn::FnArg::Typed(arg) => arg, + }; + let ident = match &*arg.pat { + syn::Pat::Ident(i) => i, + other => bail!(other, "must use bare idents for arguments"), + }; + if let Some(t) = &ident.by_ref { + bail!(t, "arguments cannot bind by reference"); + } + if let Some(t) = &ident.mutability { + bail!(t, "arguments cannot be mutable"); + } + if let Some((_, t)) = &ident.subpat { + bail!(t, "arguments cannot have sub-bindings"); + } + let ident = &ident.ident; + args.push(quote! { + #root::wasmtime_interface_types::Value::from(#ident) + }); + } + + let convert_ret = match &method.sig.output { + syn::ReturnType::Default => { + quote! { + <() as #root::FromVecValue>::from(results) + } + } + syn::ReturnType::Type(_, ty) => match &**ty { + syn::Type::Tuple(..) => { + quote! { <#ty as #root::FromVecValue>::from(results) } + } + _ => { + quote! { <(#ty,) as #root::FromVecValue>::from(results).map(|t| t.0) } + } + }, + }; + + let sig = &method.sig; + let attrs = &method.attrs; + let name = &method.sig.ident; + + result.extend(quote! { + #(#attrs)* + #vis #sig { + let args = [ + #(#args),* + ]; + let results = self.data.invoke_export( + &self.instance, + stringify!(#name), + &args, + ).expect("wasm execution failed"); + #convert_ret.expect("failed to convert return type") + } + }); + } + + Ok(result) +} + +fn root() -> TokenStream { + quote! { wasmtime_rust::__rt } +} diff --git a/crates/misc/rust/src/lib.rs b/crates/misc/rust/src/lib.rs new file mode 100644 index 0000000000..d976bf73a0 --- /dev/null +++ b/crates/misc/rust/src/lib.rs @@ -0,0 +1,46 @@ +pub use wasmtime_rust_macro::wasmtime; + +// modules used by the macro +#[doc(hidden)] +pub mod __rt { + pub use anyhow; + pub use wasmtime; + pub use wasmtime_interface_types; + pub use wasmtime_wasi; + + use std::convert::{TryFrom, TryInto}; + use wasmtime_interface_types::Value; + + pub trait FromVecValue: Sized { + fn from(list: Vec) -> anyhow::Result; + } + + macro_rules! tuple { + ($(($($a:ident),*),)*) => ($( + impl<$($a: TryFrom),*> FromVecValue for ($($a,)*) + where $(anyhow::Error: From<$a::Error>,)* + { + #[allow(non_snake_case)] + fn from(list: Vec) -> anyhow::Result { + let mut iter = list.into_iter(); + $( + let $a = iter.next() + .ok_or_else(|| anyhow::format_err!("not enough values"))? + .try_into()?; + )* + if iter.next().is_some() { + anyhow::bail!("too many return values"); + } + Ok(($($a,)*)) + } + } + )*) + } + + tuple! { + (), + (A), + (A, B), + (A, B, C), + } +} diff --git a/crates/obj/.gitignore b/crates/obj/.gitignore new file mode 100644 index 0000000000..4308d82204 --- /dev/null +++ b/crates/obj/.gitignore @@ -0,0 +1,3 @@ +target/ +**/*.rs.bk +Cargo.lock diff --git a/crates/obj/Cargo.toml b/crates/obj/Cargo.toml new file mode 100644 index 0000000000..aa141ecbbb --- /dev/null +++ b/crates/obj/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "wasmtime-obj" +version = "0.12.0" +authors = ["The Wasmtime Project Developers"] +description = "Native object file output for WebAsssembly code in Wasmtime" +license = "Apache-2.0 WITH LLVM-exception" +repository = "https://github.com/bytecodealliance/wasmtime" +categories = ["wasm"] +keywords = ["webassembly", "wasm"] +readme = "README.md" +edition = "2018" + +[dependencies] +anyhow = "1.0" +wasmtime-environ = { path = "../environ", version = "0.12.0" } +faerie = "0.14.0" +more-asserts = "0.2.1" + +[badges] +maintenance = { status = "experimental" } diff --git a/crates/obj/LICENSE b/crates/obj/LICENSE new file mode 100644 index 0000000000..f9d81955f4 --- /dev/null +++ b/crates/obj/LICENSE @@ -0,0 +1,220 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + diff --git a/crates/obj/README.md b/crates/obj/README.md new file mode 100644 index 0000000000..0ea2b97ce5 --- /dev/null +++ b/crates/obj/README.md @@ -0,0 +1,5 @@ +This is the `wasmtime-obj` crate, which contains an experimental prototype +for writing out native object files, using the wasm ABI defined by +[`wasmtime-environ`]. + +[`wasmtime-environ`]: https://crates.io/crates/wasmtime-environ diff --git a/crates/obj/src/context.rs b/crates/obj/src/context.rs new file mode 100644 index 0000000000..9faa5768a2 --- /dev/null +++ b/crates/obj/src/context.rs @@ -0,0 +1,92 @@ +#![allow(clippy::cast_ptr_alignment)] + +use more_asserts::assert_le; +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::ptr; +use wasmtime_environ::entity::EntityRef; +use wasmtime_environ::isa::TargetFrontendConfig; +use wasmtime_environ::wasm::GlobalInit; +use wasmtime_environ::{Module, TargetSharedSignatureIndex, VMOffsets}; + +pub struct TableRelocation { + pub index: usize, + pub offset: usize, +} + +pub fn layout_vmcontext( + module: &Module, + target_config: &TargetFrontendConfig, +) -> (Box<[u8]>, Box<[TableRelocation]>) { + let ofs = VMOffsets::new(target_config.pointer_bytes(), &module.local); + let out_len = ofs.size_of_vmctx() as usize; + let mut out = vec![0; out_len]; + + // Assign unique indicies to unique signatures. + let mut signature_registry = HashMap::new(); + let mut signature_registry_len = signature_registry.len(); + for (index, sig) in module.local.signatures.iter() { + let offset = ofs.vmctx_vmshared_signature_id(index) as usize; + let target_index = match signature_registry.entry(sig) { + Entry::Occupied(o) => *o.get(), + Entry::Vacant(v) => { + assert_le!(signature_registry_len, std::u32::MAX as usize); + let id = TargetSharedSignatureIndex::new(signature_registry_len as u32); + signature_registry_len += 1; + *v.insert(id) + } + }; + unsafe { + let to = out.as_mut_ptr().add(offset) as *mut TargetSharedSignatureIndex; + ptr::write(to, target_index); + } + } + + let num_tables_imports = module.imported_tables.len(); + let mut table_relocs = Vec::with_capacity(module.local.table_plans.len() - num_tables_imports); + for (index, table) in module.local.table_plans.iter().skip(num_tables_imports) { + let def_index = module.local.defined_table_index(index).unwrap(); + let offset = ofs.vmctx_vmtable_definition(def_index) as usize; + let current_elements = table.table.minimum; + unsafe { + assert_eq!( + ::std::mem::size_of::() as u8, + ofs.size_of_vmtable_definition_current_elements(), + "vmtable_definition_current_elements expected to be u32" + ); + let to = out + .as_mut_ptr() + .add(offset) + .add(ofs.vmtable_definition_current_elements() as usize); + ptr::write(to as *mut u32, current_elements); + } + table_relocs.push(TableRelocation { + index: def_index.index(), + offset, + }); + } + + let num_globals_imports = module.imported_globals.len(); + for (index, global) in module.local.globals.iter().skip(num_globals_imports) { + let def_index = module.local.defined_global_index(index).unwrap(); + let offset = ofs.vmctx_vmglobal_definition(def_index) as usize; + let to = unsafe { out.as_mut_ptr().add(offset) }; + match global.initializer { + GlobalInit::I32Const(x) => unsafe { + ptr::write(to as *mut i32, x); + }, + GlobalInit::I64Const(x) => unsafe { + ptr::write(to as *mut i64, x); + }, + GlobalInit::F32Const(x) => unsafe { + ptr::write(to as *mut u32, x); + }, + GlobalInit::F64Const(x) => unsafe { + ptr::write(to as *mut u64, x); + }, + _ => panic!("unsupported global type"), + } + } + + (out.into_boxed_slice(), table_relocs.into_boxed_slice()) +} diff --git a/crates/obj/src/data_segment.rs b/crates/obj/src/data_segment.rs new file mode 100644 index 0000000000..4662544753 --- /dev/null +++ b/crates/obj/src/data_segment.rs @@ -0,0 +1,25 @@ +use anyhow::Result; +use faerie::{Artifact, Decl}; +use wasmtime_environ::DataInitializer; + +/// Declares data segment symbol +pub fn declare_data_segment( + obj: &mut Artifact, + _data_initaliazer: &DataInitializer, + index: usize, +) -> Result<()> { + let name = format!("_memory_{}", index); + obj.declare(name, Decl::data())?; + Ok(()) +} + +/// Emit segment data and initialization location +pub fn emit_data_segment( + obj: &mut Artifact, + data_initaliazer: &DataInitializer, + index: usize, +) -> Result<()> { + let name = format!("_memory_{}", index); + obj.define(name, Vec::from(data_initaliazer.data))?; + Ok(()) +} diff --git a/crates/obj/src/function.rs b/crates/obj/src/function.rs new file mode 100644 index 0000000000..e5598b2937 --- /dev/null +++ b/crates/obj/src/function.rs @@ -0,0 +1,75 @@ +use anyhow::Result; +use faerie::{Artifact, Decl, Link}; +use wasmtime_environ::entity::EntityRef; +use wasmtime_environ::settings; +use wasmtime_environ::settings::Configurable; +use wasmtime_environ::{Compilation, Module, RelocationTarget, Relocations}; + +/// Defines module functions +pub fn declare_functions( + obj: &mut Artifact, + module: &Module, + relocations: &Relocations, +) -> Result<()> { + for i in 0..module.imported_funcs.len() { + let string_name = format!("_wasm_function_{}", i); + obj.declare(string_name, Decl::function_import())?; + } + for (i, _function_relocs) in relocations.iter().rev() { + let func_index = module.local.func_index(i); + let string_name = format!("_wasm_function_{}", func_index.index()); + obj.declare(string_name, Decl::function().global())?; + } + Ok(()) +} + +/// Emits module functions +pub fn emit_functions( + obj: &mut Artifact, + module: &Module, + compilation: &Compilation, + relocations: &Relocations, +) -> Result<()> { + debug_assert!( + module.start_func.is_none() + || module.start_func.unwrap().index() >= module.imported_funcs.len(), + "imported start functions not supported yet" + ); + + let mut shared_builder = settings::builder(); + shared_builder + .enable("enable_verifier") + .expect("Missing enable_verifier setting"); + + for (i, _function_relocs) in relocations.iter() { + let body = &compilation.get(i).body; + let func_index = module.local.func_index(i); + let string_name = format!("_wasm_function_{}", func_index.index()); + + obj.define(string_name, body.clone())?; + } + + for (i, function_relocs) in relocations.iter() { + let func_index = module.local.func_index(i); + let string_name = format!("_wasm_function_{}", func_index.index()); + for r in function_relocs { + debug_assert_eq!(r.addend, 0); + match r.reloc_target { + RelocationTarget::UserFunc(target_index) => { + let target_name = format!("_wasm_function_{}", target_index.index()); + obj.link(Link { + from: &string_name, + to: &target_name, + at: r.offset as u64, + })?; + } + RelocationTarget::JumpTable(_, _) => { + // ignore relocations for jump tables + } + _ => panic!("relocations target not supported yet: {:?}", r.reloc_target), + }; + } + } + + Ok(()) +} diff --git a/crates/obj/src/lib.rs b/crates/obj/src/lib.rs new file mode 100644 index 0000000000..197034e134 --- /dev/null +++ b/crates/obj/src/lib.rs @@ -0,0 +1,35 @@ +//! Object-file writing library using the wasmtime environment. + +#![deny( + missing_docs, + trivial_numeric_casts, + unused_extern_crates, + unstable_features +)] +#![warn(unused_import_braces)] +#![cfg_attr(feature = "clippy", plugin(clippy(conf_file = "../../clippy.toml")))] +#![cfg_attr(feature = "cargo-clippy", allow(clippy::new_without_default))] +#![cfg_attr( + feature = "cargo-clippy", + warn( + clippy::float_arithmetic, + clippy::mut_mut, + clippy::nonminimal_bool, + clippy::option_map_unwrap_or, + clippy::option_map_unwrap_or_else, + clippy::print_stdout, + clippy::unicode_not_nfc, + clippy::use_self + ) +)] + +mod context; +mod data_segment; +mod function; +mod module; +mod table; + +pub use crate::module::emit_module; + +/// Version number of this crate. +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/crates/obj/src/module.rs b/crates/obj/src/module.rs new file mode 100644 index 0000000000..d5a1b1ed7e --- /dev/null +++ b/crates/obj/src/module.rs @@ -0,0 +1,61 @@ +use crate::context::layout_vmcontext; +use crate::data_segment::{declare_data_segment, emit_data_segment}; +use crate::function::{declare_functions, emit_functions}; +use crate::table::{declare_table, emit_table}; +use anyhow::Result; +use faerie::{Artifact, Decl, Link}; +use wasmtime_environ::isa::TargetFrontendConfig; +use wasmtime_environ::{Compilation, DataInitializer, Module, Relocations}; + +fn emit_vmcontext_init( + obj: &mut Artifact, + module: &Module, + target_config: &TargetFrontendConfig, +) -> Result<()> { + let (data, table_relocs) = layout_vmcontext(module, target_config); + obj.declare_with("_vmcontext_init", Decl::data().global(), data.to_vec())?; + for reloc in table_relocs.iter() { + let target_name = format!("_table_{}", reloc.index); + obj.link(Link { + from: "_vmcontext_init", + to: &target_name, + at: reloc.offset as u64, + })?; + } + Ok(()) +} + +/// Emits a module that has been emitted with the `wasmtime-environ` environment +/// implementation to a native object file. +pub fn emit_module( + obj: &mut Artifact, + module: &Module, + compilation: &Compilation, + relocations: &Relocations, + data_initializers: &[DataInitializer], + target_config: &TargetFrontendConfig, +) -> Result<()> { + declare_functions(obj, module, relocations)?; + + for (i, initializer) in data_initializers.iter().enumerate() { + declare_data_segment(obj, initializer, i)?; + } + + for i in 0..module.local.table_plans.len() { + declare_table(obj, i)?; + } + + emit_functions(obj, module, compilation, relocations)?; + + for (i, initializer) in data_initializers.iter().enumerate() { + emit_data_segment(obj, initializer, i)?; + } + + for i in 0..module.local.table_plans.len() { + emit_table(obj, i)?; + } + + emit_vmcontext_init(obj, module, target_config)?; + + Ok(()) +} diff --git a/crates/obj/src/table.rs b/crates/obj/src/table.rs new file mode 100644 index 0000000000..fe4b52c6a0 --- /dev/null +++ b/crates/obj/src/table.rs @@ -0,0 +1,17 @@ +use anyhow::Result; +use faerie::{Artifact, Decl}; + +/// Declares data segment symbol +pub fn declare_table(obj: &mut Artifact, index: usize) -> Result<()> { + let name = format!("_table_{}", index); + obj.declare(name, Decl::data())?; + Ok(()) +} + +/// Emit segment data and initialization location +pub fn emit_table(obj: &mut Artifact, index: usize) -> Result<()> { + let name = format!("_table_{}", index); + // FIXME: We need to initialize table using function symbols + obj.define(name, Vec::new())?; + Ok(()) +} diff --git a/crates/profiling/Cargo.toml b/crates/profiling/Cargo.toml new file mode 100644 index 0000000000..8ca66bd1d9 --- /dev/null +++ b/crates/profiling/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "wasmtime-profiling" +version = "0.12.0" +authors = ["The Wasmtime Project Developers"] +description = "Runtime library support for Wasmtime" +license = "Apache-2.0 WITH LLVM-exception" +categories = ["wasm"] +keywords = ["webassembly", "wasm"] +repository = "https://github.com/bytecodealliance/wasmtime" +readme = "README.md" +edition = "2018" + +[dependencies] +libc = { version = "0.2.60", default-features = false } +goblin = "0.1.3" +serde = { version = "1.0.99", features = ["derive"] } +scroll = "0.10.1" +gimli = "0.20.0" +object = "0.17.0" +target-lexicon = "0.10.0" +lazy_static = "1.4" + +[badges] +maintenance = { status = "actively-developed" } + +[features] +jitdump = [] diff --git a/crates/profiling/README.md b/crates/profiling/README.md new file mode 100644 index 0000000000..e1a4c5020c --- /dev/null +++ b/crates/profiling/README.md @@ -0,0 +1,2 @@ +This is the `wasmtime-profiling` crate, which contains runtime performance +profiling support for Wasmtime. diff --git a/crates/profiling/src/jitdump.rs b/crates/profiling/src/jitdump.rs new file mode 100644 index 0000000000..cff4155c83 --- /dev/null +++ b/crates/profiling/src/jitdump.rs @@ -0,0 +1,698 @@ +//! Support for jitdump files which can be used by perf for profiling jitted code. +//! Spec definitions for the output format is as described here: +//! https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/tools/perf/Documentation/jitdump-specification.txt +//! +//! Usage Example: +//! Record +//! sudo perf record -k 1 -e instructions:u target/debug/wasmtime -g --jitdump test.wasm +//! Combine +//! sudo perf inject -v -j -i perf.data -o perf.jit.data +//! Report +//! sudo perf report -i perf.jit.data -F+period,srcline +//! Note: For descriptive results, the WASM file being executed should contain dwarf debug data +use libc::c_int; +#[cfg(not(target_os = "windows"))] +use libc::{c_void, clock_gettime, mmap, open, sysconf}; +use object::Object; +use scroll::{IOwrite, SizeWith, NATIVE}; +use serde::{Deserialize, Serialize}; +use std::error::Error; +#[cfg(not(target_os = "windows"))] +use std::ffi::CString; +use std::fmt::Debug; +use std::fs::File; +use std::io::Write; +#[cfg(not(target_os = "windows"))] +use std::os::unix::io::FromRawFd; +use std::{borrow, mem, process}; +use target_lexicon::Architecture; + +#[cfg(target_pointer_width = "64")] +use goblin::elf64 as elf; + +#[cfg(target_pointer_width = "32")] +use goblin::elf32 as elf; + +/// Defines jitdump record types +#[repr(u32)] +pub enum RecordId { + /// Value 0: JIT_CODE_LOAD: record describing a jitted function + JitCodeLoad = 0, + /// Value 1: JIT_CODE_MOVE: record describing an already jitted function which is moved + _JitCodeMove = 1, + /// Value 2: JIT_CODE_DEBUG_INFO: record describing the debug information for a jitted function + JitCodeDebugInfo = 2, + /// Value 3: JIT_CODE_CLOSE: record marking the end of the jit runtime (optional) + _JitCodeClose = 3, + /// Value 4: JIT_CODE_UNWINDING_INFO: record describing a function unwinding information + _JitCodeUnwindingInfo = 4, +} + +/// Each record starts with this fixed size record header which describes the record that follows +#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy, IOwrite, SizeWith)] +#[repr(C)] +pub struct RecordHeader { + /// uint32_t id: a value identifying the record type (see below) + id: u32, + /// uint32_t total_size: the size in bytes of the record including the header. + record_size: u32, + /// uint64_t timestamp: a timestamp of when the record was created. + timestamp: u64, +} + +/// The CodeLoadRecord is used for describing jitted functions +#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy, IOwrite, SizeWith)] +#[repr(C)] +pub struct CodeLoadRecord { + /// Fixed sized header that describes this record + header: RecordHeader, + /// uint32_t pid: OS process id of the runtime generating the jitted code + pid: u32, + /// uint32_t tid: OS thread identification of the runtime thread generating the jitted code + tid: u32, + /// uint64_t vma: virtual address of jitted code start + virtual_address: u64, + /// uint64_t code_addr: code start address for the jitted code. By default vma = code_addr + address: u64, + /// uint64_t code_size: size in bytes of the generated jitted code + size: u64, + /// uint64_t code_index: unique identifier for the jitted code (see below) + index: u64, +} + +/// Describes source line information for a jitted function +#[derive(Serialize, Deserialize, Debug, Default)] +#[repr(C)] +pub struct DebugEntry { + /// uint64_t code_addr: address of function for which the debug information is generated + address: u64, + /// uint32_t line: source file line number (starting at 1) + line: u32, + /// uint32_t discrim: column discriminator, 0 is default + discriminator: u32, + /// char name[n]: source file name in ASCII, including null termination + filename: String, +} + +/// Describes debug information for a jitted function. An array of debug entries are +/// appended to this record during writting. Note, this record must preceed the code +/// load record that describes the same jitted function. +#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy, IOwrite, SizeWith)] +#[repr(C)] +pub struct DebugInfoRecord { + /// Fixed sized header that describes this record + header: RecordHeader, + /// uint64_t code_addr: address of function for which the debug information is generated + address: u64, + /// uint64_t nr_entry: number of debug entries for the function appended to this record + count: u64, +} + +/// Fixed-sized header for each jitdump file +#[derive(Serialize, Deserialize, Debug, Default, IOwrite, SizeWith)] +#[repr(C)] +pub struct FileHeader { + /// uint32_t magic: a magic number tagging the file type. The value is 4-byte long and represents the + /// string "JiTD" in ASCII form. It is 0x4A695444 or 0x4454694a depending on the endianness. The field can + /// be used to detect the endianness of the file + magic: u32, + /// uint32_t version: a 4-byte value representing the format version. It is currently set to 2 + version: u32, + /// uint32_t total_size: size in bytes of file header + size: u32, + /// uint32_t elf_mach: ELF architecture encoding (ELF e_machine value as specified in /usr/include/elf.h) + e_machine: u32, + /// uint32_t pad1: padding. Reserved for future use + pad1: u32, + /// uint32_t pid: JIT runtime process identification (OS specific) + pid: u32, + /// uint64_t timestamp: timestamp of when the file was created + timestamp: u64, + /// uint64_t flags: a bitmask of flags + flags: u64, +} + +/// Interface for driving the creation of jitdump files +#[derive(Debug, Default)] +pub struct JitDumpAgent { + /// File instance for the jit dump file + pub jitdump_file: Option, + /// Unique identifier for jitted code + pub code_index: u64, + /// Flag for experimenting with dumping code load record + /// after each function (true) or after each module. This + /// flag is currently set to true. + pub dump_funcs: bool, +} + +impl JitDumpAgent { + /// Intialize a JitDumpAgent and write out the header + pub fn init(&mut self) -> Result<(), Box> { + #[cfg(target_os = "windows")] + return Err("Target OS not supported."); + #[cfg(not(target_os = "windows"))] + { + let filename = format!("./jit-{}.dump", process::id()); + let mut jitdump_file; + unsafe { + let filename_c = CString::new(filename)?; + let fd = open( + filename_c.as_ptr(), + libc::O_CREAT | libc::O_TRUNC | libc::O_RDWR, + 0666, + ); + let pgsz = sysconf(libc::_SC_PAGESIZE) as usize; + mmap( + 0 as *mut c_void, + pgsz, + libc::PROT_EXEC | libc::PROT_READ, + libc::MAP_PRIVATE, + fd, + 0, + ); + jitdump_file = File::from_raw_fd(fd); + } + JitDumpAgent::write_file_header(&mut jitdump_file)?; + self.jitdump_file = Some(jitdump_file); + self.code_index = 0; + self.dump_funcs = true; + Ok(()) + } + } + + /// Returns timestamp from a single source + #[allow(unused_variables)] + fn get_time_stamp(timestamp: &mut u64) -> c_int { + #[cfg(not(target_os = "windows"))] + { + unsafe { + let mut ts = mem::MaybeUninit::zeroed().assume_init(); + clock_gettime(libc::CLOCK_MONOTONIC, &mut ts); + // TODO: What does it mean for either sec or nsec to be negative? + *timestamp = (ts.tv_sec * 1000000000 + ts.tv_nsec) as u64; + } + } + return 0; + } + + /// Returns the ELF machine architecture. + #[allow(dead_code)] + fn get_e_machine() -> u32 { + match target_lexicon::HOST.architecture { + Architecture::X86_64 => elf::header::EM_X86_64 as u32, + Architecture::I686 => elf::header::EM_386 as u32, + Architecture::Arm(_) => elf::header::EM_ARM as u32, + Architecture::Aarch64(_) => elf::header::EM_AARCH64 as u32, + _ => unimplemented!("unrecognized architecture"), + } + } + + #[allow(dead_code)] + fn write_file_header(file: &mut File) -> Result<(), JitDumpError> { + let mut header: FileHeader = Default::default(); + let mut timestamp: u64 = 0; + JitDumpAgent::get_time_stamp(&mut timestamp); + header.timestamp = timestamp; + + let e_machine = JitDumpAgent::get_e_machine(); + if e_machine != elf::header::EM_NONE as u32 { + header.e_machine = e_machine; + } + + if cfg!(target_endian = "little") { + header.magic = 0x4A695444 + } else { + header.magic = 0x4454694a + } + header.version = 1; + header.size = mem::size_of::() as u32; + header.pad1 = 0; + header.pid = process::id(); + header.flags = 0; + + file.iowrite_with(header, NATIVE)?; + Ok(()) + } + + fn write_code_load_record( + &mut self, + record_name: &str, + cl_record: CodeLoadRecord, + code_buffer: &[u8], + ) -> Result<(), JitDumpError> { + let mut jitdump_file = self.jitdump_file.as_ref().unwrap(); + jitdump_file.iowrite_with(cl_record, NATIVE)?; + jitdump_file.write_all(record_name.as_bytes())?; + jitdump_file.write_all(b"\0")?; + jitdump_file.write_all(code_buffer)?; + Ok(()) + } + + /// Write DebugInfoRecord to open jit dump file. + /// Must be written before the corresponding CodeLoadRecord. + fn write_debug_info_record(&mut self, dir_record: DebugInfoRecord) -> Result<(), JitDumpError> { + self.jitdump_file + .as_ref() + .unwrap() + .iowrite_with(dir_record, NATIVE)?; + Ok(()) + } + + /// Write DebugInfoRecord to open jit dump file. + /// Must be written before the corresponding CodeLoadRecord. + fn write_debug_info_entries( + &mut self, + die_entries: Vec, + ) -> Result<(), JitDumpError> { + for entry in die_entries.iter() { + let mut jitdump_file = self.jitdump_file.as_ref().unwrap(); + jitdump_file.iowrite_with(entry.address, NATIVE)?; + jitdump_file.iowrite_with(entry.line, NATIVE)?; + jitdump_file.iowrite_with(entry.discriminator, NATIVE)?; + jitdump_file.write_all(entry.filename.as_bytes())?; + jitdump_file.write_all(b"\0")?; + } + Ok(()) + } + + /// Sent when a method is compiled and loaded into memory by the VM. + pub fn module_load( + &mut self, + module_name: &str, + addr: *const u8, + len: usize, + dbg_image: Option<&[u8]>, + ) -> () { + let pid = process::id(); + let tid = pid; // ThreadId does appear to track underlying thread. Using PID. + + if let Some(img) = &dbg_image { + if let Err(err) = self.dump_from_debug_image(img, module_name, addr, len, pid, tid) { + println!( + "Jitdump: module_load failed dumping from debug image: {:?}\n", + err + ); + } + } else { + let mut timestamp: u64 = 0; + JitDumpAgent::get_time_stamp(&mut timestamp); + self.dump_code_load_record(module_name, addr, len, timestamp, pid, tid); + } + } + + fn dump_code_load_record( + &mut self, + method_name: &str, + addr: *const u8, + len: usize, + timestamp: u64, + pid: u32, + tid: u32, + ) -> () { + let name_len = method_name.len() + 1; + let size_limit = mem::size_of::(); + + let rh = RecordHeader { + id: RecordId::JitCodeLoad as u32, + record_size: size_limit as u32 + name_len as u32 + len as u32, + timestamp: timestamp, + }; + + let clr = CodeLoadRecord { + header: rh, + pid: pid, + tid: tid, + virtual_address: addr as u64, + address: addr as u64, + size: len as u64, + index: self.code_index, + }; + self.code_index += 1; + + unsafe { + let code_buffer: &[u8] = std::slice::from_raw_parts(addr, len); + if let Err(err) = self.write_code_load_record(method_name, clr, code_buffer) { + println!("Jitdump: write_code_load_failed_record failed: {:?}\n", err); + } + } + } + + /// Attempts to dump debuginfo data structures, adding method and line level + /// for the jitted function. + pub fn dump_from_debug_image( + &mut self, + dbg_image: &[u8], + module_name: &str, + addr: *const u8, + len: usize, + pid: u32, + tid: u32, + ) -> Result<(), JitDumpError> { + let file = object::File::parse(&dbg_image).unwrap(); + let endian = if file.is_little_endian() { + gimli::RunTimeEndian::Little + } else { + gimli::RunTimeEndian::Big + }; + + let load_section = |id: gimli::SectionId| -> Result, JitDumpError> { + Ok(file + .section_data_by_name(id.name()) + .unwrap_or(borrow::Cow::Borrowed(&[][..]))) + }; + + let load_section_sup = |_| Ok(borrow::Cow::Borrowed(&[][..])); + let dwarf_cow = gimli::Dwarf::load(&load_section, &load_section_sup)?; + let borrow_section: &dyn for<'a> Fn( + &'a borrow::Cow<[u8]>, + ) + -> gimli::EndianSlice<'a, gimli::RunTimeEndian> = + &|section| gimli::EndianSlice::new(&*section, endian); + + let dwarf = dwarf_cow.borrow(&borrow_section); + + let mut iter = dwarf.units(); + while let Some(header) = iter.next()? { + let unit = match dwarf.unit(header) { + Ok(unit) => unit, + Err(_err) => { + return Ok(()); + } + }; + self.dump_entries(unit, &dwarf, module_name, addr, len, pid, tid)?; + // TODO: Temp exit to avoid duplicate addresses being covered by only + // processing the top unit + break; + } + if !self.dump_funcs { + let mut timestamp: u64 = 0; + JitDumpAgent::get_time_stamp(&mut timestamp); + self.dump_code_load_record(module_name, addr, len, timestamp, pid, tid); + } + Ok(()) + } + + fn dump_entries( + &mut self, + unit: gimli::Unit, + dwarf: &gimli::Dwarf, + module_name: &str, + addr: *const u8, + len: usize, + pid: u32, + tid: u32, + ) -> Result<(), JitDumpError> { + let mut depth = 0; + let mut entries = unit.entries(); + while let Some((delta_depth, entry)) = entries.next_dfs()? { + if self.dump_funcs { + let record_header = RecordHeader { + id: RecordId::JitCodeLoad as u32, + record_size: 0, + timestamp: 0, + }; + + let mut clr = CodeLoadRecord { + header: record_header, + pid: pid, + tid: tid, + virtual_address: 0, + address: 0, + size: 0, + index: 0, + }; + let mut clr_name: String = String::from(module_name); + let mut get_debug_entry = false; + depth += delta_depth; + assert!(depth >= 0); + + if entry.tag() == gimli::constants::DW_TAG_subprogram { + get_debug_entry = true; + + let mut attrs = entry.attrs(); + while let Some(attr) = attrs.next()? { + if let Some(n) = attr.name().static_string() { + if n == "DW_AT_low_pc" { + clr.address = match attr.value() { + gimli::AttributeValue::Addr(address) => address, + _ => 0, + }; + clr.virtual_address = clr.address; + } else if n == "DW_AT_high_pc" { + clr.size = match attr.value() { + gimli::AttributeValue::Udata(data) => data, + _ => 0, + }; + } else if n == "DW_AT_name" { + clr_name = match attr.value() { + gimli::AttributeValue::DebugStrRef(offset) => { + if let Ok(s) = dwarf.debug_str.get_str(offset) { + clr_name.push_str("::"); + clr_name.push_str(&s.to_string_lossy()?); + clr_name + } else { + clr_name.push_str("::"); + clr_name.push_str("?"); + clr_name + } + } + _ => { + clr_name.push_str("??"); + clr_name + } + }; + } + } + } + } + if get_debug_entry { + // TODO: Temp check to make sure well only formed data is processed. + if clr.address == 0 { + continue; + } + // TODO: Temp check to make sure well only formed data is processed. + if clr_name == "?" { + continue; + } + if clr.address == 0 || clr.size == 0 { + clr.address = addr as u64; + clr.virtual_address = addr as u64; + clr.size = len as u64; + } + clr.header.record_size = mem::size_of::() as u32 + + (clr_name.len() + 1) as u32 + + clr.size as u32; + clr.index = self.code_index; + self.code_index += 1; + self.dump_debug_info(&unit, &dwarf, clr.address, clr.size, None)?; + + let mut timestamp: u64 = 0; + JitDumpAgent::get_time_stamp(&mut timestamp); + clr.header.timestamp = timestamp; + + unsafe { + let code_buffer: &[u8] = + std::slice::from_raw_parts(clr.address as *const u8, clr.size as usize); + let _ = self.write_code_load_record(&clr_name, clr, code_buffer); + } + } + } else { + let mut func_name: String = String::from("?"); + let mut func_addr = 0; + let mut func_size = 0; + + let mut get_debug_entry = false; + depth += delta_depth; + assert!(depth >= 0); + if entry.tag() == gimli::constants::DW_TAG_subprogram { + get_debug_entry = true; + + let mut attrs = entry.attrs(); + while let Some(attr) = attrs.next()? { + if let Some(n) = attr.name().static_string() { + if n == "DW_AT_low_pc" { + func_addr = match attr.value() { + gimli::AttributeValue::Addr(address) => address, + _ => 0, + }; + } else if n == "DW_AT_high_pc" { + func_size = match attr.value() { + gimli::AttributeValue::Udata(data) => data, + _ => 0, + }; + } else if n == "DW_AT_name" { + func_name = match attr.value() { + gimli::AttributeValue::DebugStrRef(offset) => { + if let Ok(s) = dwarf.debug_str.get_str(offset) { + func_name.clear(); + func_name.push_str(&s.to_string_lossy()?); + func_name + } else { + func_name.push_str("?"); + func_name + } + } + _ => { + func_name.push_str("??"); + func_name + } + }; + } + } + } + } + if get_debug_entry { + // TODO: Temp check to make sure well only formed data is processed. + if func_addr == 0 { + continue; + } + // TODO: Temp check to make sure well only formed data is processed. + if func_name == "?" { + continue; + } + self.dump_debug_info( + &unit, + &dwarf, + func_addr, + func_size, + Some(func_name.as_str()), + )?; + } + } + } + Ok(()) + } + + fn dump_debug_info( + &mut self, + unit: &gimli::Unit, + dwarf: &gimli::Dwarf, + address: u64, + size: u64, + file_suffix: Option<&str>, + ) -> Result<(), JitDumpError> { + let mut timestamp: u64 = 0; + JitDumpAgent::get_time_stamp(&mut timestamp); + if let Some(program) = unit.line_program.clone() { + let mut debug_info_record = DebugInfoRecord { + header: RecordHeader { + id: RecordId::JitCodeDebugInfo as u32, + record_size: 0, + timestamp: timestamp, + }, + address: address, + count: 0, + }; + + let mut debug_entries = Vec::new(); + let mut debug_entries_total_filenames_len = 0; + let mut rows = program.rows(); + while let Some((header, row)) = rows.next_row()? { + let row_file_index = row.file_index() - 1; + let myfile = dwarf + .attr_string( + &unit, + header.file_names()[row_file_index as usize].path_name(), + ) + .unwrap(); + let filename = myfile.to_string_lossy()?; + let line = row.line().unwrap_or(0); + let column = match row.column() { + gimli::ColumnType::Column(column) => column, + gimli::ColumnType::LeftEdge => 0, + }; + + if (row.address() < address) || (row.address() > (address + size)) { + continue; + } + let mut debug_entry = DebugEntry { + address: row.address(), + line: line as u32, + discriminator: column as u32, + filename: filename.to_string(), + }; + + if let Some(suffix) = file_suffix { + debug_entry.filename.push_str("::"); + debug_entry.filename.push_str(suffix); + } + + debug_entries_total_filenames_len += debug_entry.filename.len() + 1; + debug_entries.push(debug_entry); + } + + debug_info_record.count = debug_entries.len() as u64; + + let debug_entries_size = (debug_info_record.count + * (mem::size_of::() as u64 - mem::size_of::() as u64)) + + debug_entries_total_filenames_len as u64; + debug_info_record.header.record_size = + mem::size_of::() as u32 + debug_entries_size as u32; + + let _ = self.write_debug_info_record(debug_info_record); + let _ = self.write_debug_info_entries(debug_entries); + } + Ok(()) + } +} + +use crate::ProfilingAgent; +impl ProfilingAgent for JitDumpAgent { + fn module_load( + &mut self, + module_name: &str, + addr: *const u8, + len: usize, + dbg_image: Option<&[u8]>, + ) -> () { + if self.jitdump_file.is_none() { + if JitDumpAgent::init(self).ok().is_some() { + JitDumpAgent::module_load(self, module_name, addr, len, dbg_image); + } else { + println!("Jitdump: Failed to initialize JitDumpAgent\n"); + } + } else { + JitDumpAgent::module_load(self, module_name, addr, len, dbg_image); + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum JitDumpError { + GimliError(gimli::Error), + IOError, +} + +impl Error for JitDumpError { + fn description(&self) -> &str { + match *self { + JitDumpError::GimliError(ref err) => err.description(), + JitDumpError::IOError => "An I/O error occurred.", + } + } +} + +impl std::fmt::Display for JitDumpError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> { + Debug::fmt(self, f) + } +} + +impl From for JitDumpError { + fn from(err: gimli::Error) -> Self { + JitDumpError::GimliError(err) + } +} + +impl From for JitDumpError { + fn from(_err: std::io::Error) -> Self { + JitDumpError::IOError + } +} + +trait Reader: gimli::Reader + Send + Sync {} + +impl<'input, Endian> Reader for gimli::EndianSlice<'input, Endian> where + Endian: gimli::Endianity + Send + Sync +{ +} diff --git a/crates/profiling/src/lib.rs b/crates/profiling/src/lib.rs new file mode 100644 index 0000000000..7e6c851a10 --- /dev/null +++ b/crates/profiling/src/lib.rs @@ -0,0 +1,65 @@ +use std::error::Error; +use std::fmt; + +#[cfg(feature = "jitdump")] +mod jitdump; + +#[cfg(feature = "jitdump")] +pub use crate::jitdump::JitDumpAgent; + +#[cfg(not(feature = "jitdump"))] +pub type JitDumpAgent = NullProfilerAgent; + +/// Select which profiling technique to use +#[derive(Debug, Clone, Copy)] +pub enum ProfilingStrategy { + /// No profiler support + NullProfiler, + + /// Collect profile for jitdump file format + JitDumpProfiler, +} + +/// Common interface for profiling tools. +pub trait ProfilingAgent { + /// Notify the profiler of a new module loaded into memory + fn module_load( + &mut self, + module_name: &str, + addr: *const u8, + len: usize, + dbg_image: Option<&[u8]>, + ) -> (); +} + +/// Default agent for unsupported profiling build. +#[derive(Debug, Default, Clone, Copy)] +pub struct NullProfilerAgent {} + +#[derive(Debug)] +struct NullProfilerAgentError; + +impl fmt::Display for NullProfilerAgentError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "A profiler agent is not supported by this build") + } +} + +// This is important for other errors to wrap this one. +impl Error for NullProfilerAgentError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + // Generic error, underlying cause isn't tracked. + None + } +} + +impl ProfilingAgent for NullProfilerAgent { + fn module_load( + &mut self, + _module_name: &str, + _addr: *const u8, + _len: usize, + _dbg_image: Option<&[u8]>, + ) -> () { + } +} diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml new file mode 100644 index 0000000000..6a40360d28 --- /dev/null +++ b/crates/runtime/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "wasmtime-runtime" +version = "0.12.0" +authors = ["The Wasmtime Project Developers"] +description = "Runtime library support for Wasmtime" +license = "Apache-2.0 WITH LLVM-exception" +categories = ["wasm"] +keywords = ["webassembly", "wasm"] +repository = "https://github.com/bytecodealliance/wasmtime" +readme = "README.md" +edition = "2018" + +[dependencies] +wasmtime-profiling = { path = "../profiling", version = "0.12.0" } +wasmtime-environ = { path = "../environ", version = "0.12.0" } +region = "2.0.0" +libc = { version = "0.2.60", default-features = false } +memoffset = "0.5.3" +indexmap = "1.0.2" +thiserror = "1.0.4" +more-asserts = "0.2.1" +cfg-if = "0.1.9" +backtrace = "0.3.42" + +[target.'cfg(target_os = "windows")'.dependencies] +winapi = { version = "0.3.7", features = ["winbase", "memoryapi"] } + +[build-dependencies] +cc = "1.0" + +[badges] +maintenance = { status = "actively-developed" } diff --git a/crates/runtime/LICENSE b/crates/runtime/LICENSE new file mode 100644 index 0000000000..f9d81955f4 --- /dev/null +++ b/crates/runtime/LICENSE @@ -0,0 +1,220 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + diff --git a/crates/runtime/README.md b/crates/runtime/README.md new file mode 100644 index 0000000000..3e685ad92e --- /dev/null +++ b/crates/runtime/README.md @@ -0,0 +1,10 @@ +This is the `wasmtime-runtime` crate, which contains wasm runtime library +support, supporting the wasm ABI used by [`wasmtime-environ`], +[`wasmtime-jit`], and [`wasmtime-obj`]. + +This crate does not make a host vs. target distinction; it is meant to be +compiled for the target. + +[`wasmtime-environ`]: https://crates.io/crates/wasmtime-environ +[`wasmtime-jit`]: https://crates.io/crates/wasmtime-jit +[`wasmtime-obj`]: https://crates.io/crates/wasmtime-obj diff --git a/crates/runtime/build.rs b/crates/runtime/build.rs new file mode 100644 index 0000000000..a2db1468e8 --- /dev/null +++ b/crates/runtime/build.rs @@ -0,0 +1,20 @@ +fn main() { + println!("cargo:rerun-if-changed=signalhandlers/SignalHandlers.cpp"); + println!("cargo:rerun-if-changed=signalhandlers/SignalHandlers.hpp"); + println!("cargo:rerun-if-changed=signalhandlers/Trampolines.cpp"); + let target = std::env::var("TARGET").unwrap(); + let mut build = cc::Build::new(); + build + .cpp(true) + .warnings(false) + .file("signalhandlers/SignalHandlers.cpp") + .file("signalhandlers/Trampolines.cpp"); + if !target.contains("windows") { + build + .flag("-std=c++11") + .flag("-fno-exceptions") + .flag("-fno-rtti"); + } + + build.compile("signalhandlers"); +} diff --git a/crates/runtime/signalhandlers/SignalHandlers.cpp b/crates/runtime/signalhandlers/SignalHandlers.cpp new file mode 100644 index 0000000000..c715091703 --- /dev/null +++ b/crates/runtime/signalhandlers/SignalHandlers.cpp @@ -0,0 +1,755 @@ +//! This file is largely derived from the code in WasmSignalHandlers.cpp in SpiderMonkey: +//! +//! https://dxr.mozilla.org/mozilla-central/source/js/src/wasm/WasmSignalHandlers.cpp +//! +//! Use of Mach ports on Darwin platforms (the USE_APPLE_MACH_PORTS code below) is +//! currently disabled. + +#include "SignalHandlers.hpp" + +#include +#include +#include +#include + +#if defined(_WIN32) + +# include +# include + +#elif defined(USE_APPLE_MACH_PORTS) +# include +# include +# include +#else +# include +#endif + +// ============================================================================= +// This following pile of macros and includes defines the ToRegisterState() and +// the ContextToPC() functions from the (highly) platform-specific CONTEXT +// struct which is provided to the signal handler. +// ============================================================================= + +#if defined(__FreeBSD__) || defined(__FreeBSD_kernel__) +# include // for ucontext_t, mcontext_t +#endif + +#if defined(__x86_64__) +# if defined(__DragonFly__) +# include // for union savefpu +# elif defined(__FreeBSD__) || defined(__FreeBSD_kernel__) || \ + defined(__NetBSD__) || defined(__OpenBSD__) +# include // for struct savefpu/fxsave64 +# endif +#endif + +#if defined(_WIN32) +# define EIP_sig(p) ((p)->Eip) +# define EBP_sig(p) ((p)->Ebp) +# define ESP_sig(p) ((p)->Esp) +# define RIP_sig(p) ((p)->Rip) +# define RSP_sig(p) ((p)->Rsp) +# define RBP_sig(p) ((p)->Rbp) +# define R11_sig(p) ((p)->R11) +# define R13_sig(p) ((p)->R13) +# define R14_sig(p) ((p)->R14) +# define R15_sig(p) ((p)->R15) +# define EPC_sig(p) ((p)->Pc) +# define RFP_sig(p) ((p)->Fp) +# define R31_sig(p) ((p)->Sp) +# define RLR_sig(p) ((p)->Lr) +#elif defined(__OpenBSD__) +# define EIP_sig(p) ((p)->sc_eip) +# define EBP_sig(p) ((p)->sc_ebp) +# define ESP_sig(p) ((p)->sc_esp) +# define RIP_sig(p) ((p)->sc_rip) +# define RSP_sig(p) ((p)->sc_rsp) +# define RBP_sig(p) ((p)->sc_rbp) +# define R11_sig(p) ((p)->sc_r11) +# if defined(__arm__) +# define R13_sig(p) ((p)->sc_usr_sp) +# define R14_sig(p) ((p)->sc_usr_lr) +# define R15_sig(p) ((p)->sc_pc) +# else +# define R13_sig(p) ((p)->sc_r13) +# define R14_sig(p) ((p)->sc_r14) +# define R15_sig(p) ((p)->sc_r15) +# endif +# if defined(__aarch64__) +# define EPC_sig(p) ((p)->sc_elr) +# define RFP_sig(p) ((p)->sc_x[29]) +# define RLR_sig(p) ((p)->sc_lr) +# define R31_sig(p) ((p)->sc_sp) +# endif +# if defined(__mips__) +# define EPC_sig(p) ((p)->sc_pc) +# define RFP_sig(p) ((p)->sc_regs[30]) +# endif +#elif defined(__linux__) || defined(__sun) +# if defined(__linux__) +# define EIP_sig(p) ((p)->uc_mcontext.gregs[REG_EIP]) +# define EBP_sig(p) ((p)->uc_mcontext.gregs[REG_EBP]) +# define ESP_sig(p) ((p)->uc_mcontext.gregs[REG_ESP]) +# else +# define EIP_sig(p) ((p)->uc_mcontext.gregs[REG_PC]) +# define EBP_sig(p) ((p)->uc_mcontext.gregs[REG_EBP]) +# define ESP_sig(p) ((p)->uc_mcontext.gregs[REG_ESP]) +# endif +# define RIP_sig(p) ((p)->uc_mcontext.gregs[REG_RIP]) +# define RSP_sig(p) ((p)->uc_mcontext.gregs[REG_RSP]) +# define RBP_sig(p) ((p)->uc_mcontext.gregs[REG_RBP]) +# if defined(__linux__) && defined(__arm__) +# define R11_sig(p) ((p)->uc_mcontext.arm_fp) +# define R13_sig(p) ((p)->uc_mcontext.arm_sp) +# define R14_sig(p) ((p)->uc_mcontext.arm_lr) +# define R15_sig(p) ((p)->uc_mcontext.arm_pc) +# else +# define R11_sig(p) ((p)->uc_mcontext.gregs[REG_R11]) +# define R13_sig(p) ((p)->uc_mcontext.gregs[REG_R13]) +# define R14_sig(p) ((p)->uc_mcontext.gregs[REG_R14]) +# define R15_sig(p) ((p)->uc_mcontext.gregs[REG_R15]) +# endif +# if defined(__linux__) && defined(__aarch64__) +# define EPC_sig(p) ((p)->uc_mcontext.pc) +# define RFP_sig(p) ((p)->uc_mcontext.regs[29]) +# define RLR_sig(p) ((p)->uc_mcontext.regs[30]) +# define R31_sig(p) ((p)->uc_mcontext.regs[31]) +# endif +# if defined(__linux__) && defined(__mips__) +# define EPC_sig(p) ((p)->uc_mcontext.pc) +# define RFP_sig(p) ((p)->uc_mcontext.gregs[30]) +# define RSP_sig(p) ((p)->uc_mcontext.gregs[29]) +# define R31_sig(p) ((p)->uc_mcontext.gregs[31]) +# endif +# if defined(__linux__) && (defined(__sparc__) && defined(__arch64__)) +# define PC_sig(p) ((p)->uc_mcontext.mc_gregs[MC_PC]) +# define FP_sig(p) ((p)->uc_mcontext.mc_fp) +# define SP_sig(p) ((p)->uc_mcontext.mc_i7) +# endif +# if defined(__linux__) && \ + (defined(__ppc64__) || defined (__PPC64__) || defined(__ppc64le__) || defined (__PPC64LE__)) +# define R01_sig(p) ((p)->uc_mcontext.gp_regs[1]) +# define R32_sig(p) ((p)->uc_mcontext.gp_regs[32]) +# endif +#elif defined(__NetBSD__) +# define EIP_sig(p) ((p)->uc_mcontext.__gregs[_REG_EIP]) +# define EBP_sig(p) ((p)->uc_mcontext.__gregs[_REG_EBP]) +# define ESP_sig(p) ((p)->uc_mcontext.__gregs[_REG_ESP]) +# define RIP_sig(p) ((p)->uc_mcontext.__gregs[_REG_RIP]) +# define RSP_sig(p) ((p)->uc_mcontext.__gregs[_REG_RSP]) +# define RBP_sig(p) ((p)->uc_mcontext.__gregs[_REG_RBP]) +# define R11_sig(p) ((p)->uc_mcontext.__gregs[_REG_R11]) +# define R13_sig(p) ((p)->uc_mcontext.__gregs[_REG_R13]) +# define R14_sig(p) ((p)->uc_mcontext.__gregs[_REG_R14]) +# define R15_sig(p) ((p)->uc_mcontext.__gregs[_REG_R15]) +# if defined(__aarch64__) +# define EPC_sig(p) ((p)->uc_mcontext.__gregs[_REG_PC]) +# define RFP_sig(p) ((p)->uc_mcontext.__gregs[_REG_X29]) +# define RLR_sig(p) ((p)->uc_mcontext.__gregs[_REG_X30]) +# define R31_sig(p) ((p)->uc_mcontext.__gregs[_REG_SP]) +# endif +# if defined(__mips__) +# define EPC_sig(p) ((p)->uc_mcontext.__gregs[_REG_EPC]) +# define RFP_sig(p) ((p)->uc_mcontext.__gregs[_REG_S8]) +# endif +#elif defined(__DragonFly__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) +# define EIP_sig(p) ((p)->uc_mcontext.mc_eip) +# define EBP_sig(p) ((p)->uc_mcontext.mc_ebp) +# define ESP_sig(p) ((p)->uc_mcontext.mc_esp) +# define RIP_sig(p) ((p)->uc_mcontext.mc_rip) +# define RSP_sig(p) ((p)->uc_mcontext.mc_rsp) +# define RBP_sig(p) ((p)->uc_mcontext.mc_rbp) +# if defined(__FreeBSD__) && defined(__arm__) +# define R11_sig(p) ((p)->uc_mcontext.__gregs[_REG_R11]) +# define R13_sig(p) ((p)->uc_mcontext.__gregs[_REG_R13]) +# define R14_sig(p) ((p)->uc_mcontext.__gregs[_REG_R14]) +# define R15_sig(p) ((p)->uc_mcontext.__gregs[_REG_R15]) +# else +# define R11_sig(p) ((p)->uc_mcontext.mc_r11) +# define R13_sig(p) ((p)->uc_mcontext.mc_r13) +# define R14_sig(p) ((p)->uc_mcontext.mc_r14) +# define R15_sig(p) ((p)->uc_mcontext.mc_r15) +# endif +# if defined(__FreeBSD__) && defined(__aarch64__) +# define EPC_sig(p) ((p)->uc_mcontext.mc_gpregs.gp_elr) +# define RFP_sig(p) ((p)->uc_mcontext.mc_gpregs.gp_x[29]) +# define RLR_sig(p) ((p)->uc_mcontext.mc_gpregs.gp_lr) +# define R31_sig(p) ((p)->uc_mcontext.mc_gpregs.gp_sp) +# endif +# if defined(__FreeBSD__) && defined(__mips__) +# define EPC_sig(p) ((p)->uc_mcontext.mc_pc) +# define RFP_sig(p) ((p)->uc_mcontext.mc_regs[30]) +# endif +#elif defined(USE_APPLE_MACH_PORTS) +# define EIP_sig(p) ((p)->thread.uts.ts32.__eip) +# define EBP_sig(p) ((p)->thread.uts.ts32.__ebp) +# define ESP_sig(p) ((p)->thread.uts.ts32.__esp) +# define RIP_sig(p) ((p)->thread.__rip) +# define RBP_sig(p) ((p)->thread.__rbp) +# define RSP_sig(p) ((p)->thread.__rsp) +# define R11_sig(p) ((p)->thread.__r[11]) +# define R13_sig(p) ((p)->thread.__sp) +# define R14_sig(p) ((p)->thread.__lr) +# define R15_sig(p) ((p)->thread.__pc) +#elif defined(__APPLE__) +# define EIP_sig(p) ((p)->uc_mcontext->__ss.__eip) +# define EBP_sig(p) ((p)->uc_mcontext->__ss.__ebp) +# define ESP_sig(p) ((p)->uc_mcontext->__ss.__esp) +# define RIP_sig(p) ((p)->uc_mcontext->__ss.__rip) +# define RBP_sig(p) ((p)->uc_mcontext->__ss.__rbp) +# define RSP_sig(p) ((p)->uc_mcontext->__ss.__rsp) +# define R11_sig(p) ((p)->uc_mcontext->__ss.__r11) +# define R13_sig(p) ((p)->uc_mcontext->__ss.__sp) +# define R14_sig(p) ((p)->uc_mcontext->__ss.__lr) +# define R15_sig(p) ((p)->uc_mcontext->__ss.__pc) +#else +# error "Don't know how to read/write to the thread state via the mcontext_t." +#endif + +#if defined(ANDROID) +// Not all versions of the Android NDK define ucontext_t or mcontext_t. +// Detect this and provide custom but compatible definitions. Note that these +// follow the GLibc naming convention to access register values from +// mcontext_t. +// +// See: https://chromiumcodereview.appspot.com/10829122/ +// See: http://code.google.com/p/android/issues/detail?id=34784 +# if !defined(__BIONIC_HAVE_UCONTEXT_T) +# if defined(__arm__) + +// GLibc on ARM defines mcontext_t has a typedef for 'struct sigcontext'. +// Old versions of the C library didn't define the type. +# if !defined(__BIONIC_HAVE_STRUCT_SIGCONTEXT) +# include +# endif + +typedef struct sigcontext mcontext_t; + +typedef struct ucontext { + uint32_t uc_flags; + struct ucontext* uc_link; + stack_t uc_stack; + mcontext_t uc_mcontext; + // Other fields are not used so don't define them here. +} ucontext_t; + +# elif defined(__mips__) + +typedef struct { + uint32_t regmask; + uint32_t status; + uint64_t pc; + uint64_t gregs[32]; + uint64_t fpregs[32]; + uint32_t acx; + uint32_t fpc_csr; + uint32_t fpc_eir; + uint32_t used_math; + uint32_t dsp; + uint64_t mdhi; + uint64_t mdlo; + uint32_t hi1; + uint32_t lo1; + uint32_t hi2; + uint32_t lo2; + uint32_t hi3; + uint32_t lo3; +} mcontext_t; + +typedef struct ucontext { + uint32_t uc_flags; + struct ucontext* uc_link; + stack_t uc_stack; + mcontext_t uc_mcontext; + // Other fields are not used so don't define them here. +} ucontext_t; + +# elif defined(__i386__) +// x86 version for Android. +typedef struct { + uint32_t gregs[19]; + void* fpregs; + uint32_t oldmask; + uint32_t cr2; +} mcontext_t; + +typedef uint32_t kernel_sigset_t[2]; // x86 kernel uses 64-bit signal masks +typedef struct ucontext { + uint32_t uc_flags; + struct ucontext* uc_link; + stack_t uc_stack; + mcontext_t uc_mcontext; + // Other fields are not used by V8, don't define them here. +} ucontext_t; +enum { REG_EIP = 14 }; +# endif // defined(__i386__) +# endif // !defined(__BIONIC_HAVE_UCONTEXT_T) +#endif // defined(ANDROID) + +#if defined(USE_APPLE_MACH_PORTS) +# if defined(__x86_64__) +struct macos_x64_context { + x86_thread_state64_t thread; + x86_float_state64_t float_; +}; +# define CONTEXT macos_x64_context +# elif defined(__i386__) +struct macos_x86_context { + x86_thread_state_t thread; + x86_float_state_t float_; +}; +# define CONTEXT macos_x86_context +# elif defined(__arm__) +struct macos_arm_context { + arm_thread_state_t thread; + arm_neon_state_t float_; +}; +# define CONTEXT macos_arm_context +# else +# error Unsupported architecture +# endif +#elif !defined(_WIN32) +# define CONTEXT ucontext_t +#endif + +#if defined(_M_X64) || defined(__x86_64__) +# define PC_sig(p) RIP_sig(p) +# define FP_sig(p) RBP_sig(p) +# define SP_sig(p) RSP_sig(p) +#elif defined(_M_IX86) || defined(__i386__) +# define PC_sig(p) EIP_sig(p) +# define FP_sig(p) EBP_sig(p) +# define SP_sig(p) ESP_sig(p) +#elif defined(__arm__) +# define FP_sig(p) R11_sig(p) +# define SP_sig(p) R13_sig(p) +# define LR_sig(p) R14_sig(p) +# define PC_sig(p) R15_sig(p) +#elif defined(_M_ARM64) || defined(__aarch64__) +# define PC_sig(p) EPC_sig(p) +# define FP_sig(p) RFP_sig(p) +# define SP_sig(p) R31_sig(p) +# define LR_sig(p) RLR_sig(p) +#elif defined(__mips__) +# define PC_sig(p) EPC_sig(p) +# define FP_sig(p) RFP_sig(p) +# define SP_sig(p) RSP_sig(p) +# define LR_sig(p) R31_sig(p) +#elif defined(__ppc64__) || defined (__PPC64__) || defined(__ppc64le__) || defined (__PPC64LE__) +# define PC_sig(p) R32_sig(p) +# define SP_sig(p) R01_sig(p) +# define FP_sig(p) R01_sig(p) +#endif + +static void +SetContextPC(CONTEXT* context, const uint8_t* pc) +{ +#ifdef PC_sig + PC_sig(context) = reinterpret_cast(pc); +#else + abort(); +#endif +} + +static const uint8_t* +ContextToPC(CONTEXT* context) +{ +#ifdef PC_sig + return reinterpret_cast(static_cast(PC_sig(context))); +#else + abort(); +#endif +} + +// ============================================================================= +// All signals/exceptions funnel down to this one trap-handling function which +// tests whether the pc is in a wasm module and, if so, whether there is +// actually a trap expected at this pc. These tests both avoid real bugs being +// silently converted to wasm traps and provides the trapping wasm bytecode +// offset we need to report in the error. +// +// Crashing inside wasm trap handling (due to a bug in trap handling or exposed +// during trap handling) must be reported like a normal crash, not cause the +// crash report to be lost. On Windows and non-Mach Unix, a crash during the +// handler reenters the handler, possibly repeatedly until exhausting the stack, +// and so we prevent recursion with the thread-local sAlreadyHandlingTrap. On +// Mach, the wasm exception handler has its own thread and is installed only on +// the thread-level debugging ports of our threads, so a crash on +// exception handler thread will not recurse; it will bubble up to the +// process-level debugging ports (where Breakpad is installed). +// ============================================================================= + +static thread_local bool sAlreadyHandlingTrap; + +namespace { + +struct AutoHandlingTrap +{ + AutoHandlingTrap() { + assert(!sAlreadyHandlingTrap); + sAlreadyHandlingTrap = true; + } + + ~AutoHandlingTrap() { + assert(sAlreadyHandlingTrap); + sAlreadyHandlingTrap = false; + } +}; + +} + +// ============================================================================= +// The following platform-specific handlers funnel all signals/exceptions into +// the HandleTrap() function defined in Rust. Note that the Rust function has a +// different ABI depending on the platform. +// ============================================================================= + +#if defined(_WIN32) +// Obtained empirically from thread_local codegen on x86/x64/arm64. +// Compiled in all user binaries, so should be stable over time. +static const unsigned sThreadLocalArrayPointerIndex = 11; + +static LONG WINAPI +WasmTrapHandler(LPEXCEPTION_POINTERS exception) +{ + // Make sure TLS is initialized before reading sAlreadyHandlingTrap. + if (!NtCurrentTeb()->Reserved1[sThreadLocalArrayPointerIndex]) { + return EXCEPTION_CONTINUE_SEARCH; + } + + if (sAlreadyHandlingTrap) { + return EXCEPTION_CONTINUE_SEARCH; + } + AutoHandlingTrap aht; + + EXCEPTION_RECORD* record = exception->ExceptionRecord; + if (record->ExceptionCode != EXCEPTION_ACCESS_VIOLATION && + record->ExceptionCode != EXCEPTION_ILLEGAL_INSTRUCTION && + record->ExceptionCode != EXCEPTION_STACK_OVERFLOW && + record->ExceptionCode != EXCEPTION_INT_DIVIDE_BY_ZERO && + record->ExceptionCode != EXCEPTION_INT_OVERFLOW) + { + return EXCEPTION_CONTINUE_SEARCH; + } + + void *JmpBuf = HandleTrap(ContextToPC(exception->ContextRecord), exception); + // Test if a custom instance signal handler handled the exception + if (((size_t) JmpBuf) == 1) + return EXCEPTION_CONTINUE_EXECUTION; + + // Otherwise test if we need to longjmp to this buffer + if (JmpBuf != nullptr) { + sAlreadyHandlingTrap = false; + Unwind(JmpBuf); + } + + // ... and otherwise keep looking for a handler + return EXCEPTION_CONTINUE_SEARCH; +} + +#elif defined(USE_APPLE_MACH_PORTS) +// On OSX we are forced to use the lower-level Mach exception mechanism instead +// of Unix signals because breakpad uses Mach exceptions and would otherwise +// report a crash before wasm gets a chance to handle the exception. + +// This definition was generated by mig (the Mach Interface Generator) for the +// routine 'exception_raise' (exc.defs). +#pragma pack(4) +typedef struct { + mach_msg_header_t Head; + /* start of the kernel processed data */ + mach_msg_body_t msgh_body; + mach_msg_port_descriptor_t thread; + mach_msg_port_descriptor_t task; + /* end of the kernel processed data */ + NDR_record_t NDR; + exception_type_t exception; + mach_msg_type_number_t codeCnt; + int64_t code[2]; +} Request__mach_exception_raise_t; +#pragma pack() + +// The full Mach message also includes a trailer. +struct ExceptionRequest +{ + Request__mach_exception_raise_t body; + mach_msg_trailer_t trailer; +}; + +static bool +HandleMachException(const ExceptionRequest& request) +{ + // Get the port of the thread from the message. + mach_port_t cxThread = request.body.thread.name; + + // Read out the thread's register state. + CONTEXT context; +# if defined(__x86_64__) + unsigned int thread_state_count = x86_THREAD_STATE64_COUNT; + unsigned int float_state_count = x86_FLOAT_STATE64_COUNT; + int thread_state = x86_THREAD_STATE64; + int float_state = x86_FLOAT_STATE64; +# elif defined(__i386__) + unsigned int thread_state_count = x86_THREAD_STATE_COUNT; + unsigned int float_state_count = x86_FLOAT_STATE_COUNT; + int thread_state = x86_THREAD_STATE; + int float_state = x86_FLOAT_STATE; +# elif defined(__arm__) + unsigned int thread_state_count = ARM_THREAD_STATE_COUNT; + unsigned int float_state_count = ARM_NEON_STATE_COUNT; + int thread_state = ARM_THREAD_STATE; + int float_state = ARM_NEON_STATE; +# else +# error Unsupported architecture +# endif + kern_return_t kret; + kret = thread_get_state(cxThread, thread_state, + (thread_state_t)&context.thread, &thread_state_count); + if (kret != KERN_SUCCESS) { + return false; + } + kret = thread_get_state(cxThread, float_state, + (thread_state_t)&context.float_, &float_state_count); + if (kret != KERN_SUCCESS) { + return false; + } + + if (request.body.exception != EXC_BAD_ACCESS && + request.body.exception != EXC_BAD_INSTRUCTION) + { + return false; + } + + { + AutoHandlingTrap aht; + if (!HandleTrap(&context, false)) { + return false; + } + } + + // Update the thread state with the new pc and register values. + kret = thread_set_state(cxThread, float_state, (thread_state_t)&context.float_, float_state_count); + if (kret != KERN_SUCCESS) { + return false; + } + kret = thread_set_state(cxThread, thread_state, (thread_state_t)&context.thread, thread_state_count); + if (kret != KERN_SUCCESS) { + return false; + } + + return true; +} + +static mach_port_t sMachDebugPort = MACH_PORT_NULL; + +static void* +MachExceptionHandlerThread(void* arg) +{ + // Taken from mach_exc in /usr/include/mach/mach_exc.defs. + static const unsigned EXCEPTION_MSG_ID = 2405; + + while (true) { + ExceptionRequest request; + kern_return_t kret = mach_msg(&request.body.Head, MACH_RCV_MSG, 0, sizeof(request), + sMachDebugPort, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL); + + // If we fail even receiving the message, we can't even send a reply! + // Rather than hanging the faulting thread (hanging the browser), crash. + if (kret != KERN_SUCCESS) { + fprintf(stderr, "MachExceptionHandlerThread: mach_msg failed with %d\n", (int)kret); + abort(); + } + + if (request.body.Head.msgh_id != EXCEPTION_MSG_ID) { + fprintf(stderr, "Unexpected msg header id %d\n", (int)request.body.Head.msgh_bits); + abort(); + } + + // Some thread just commited an EXC_BAD_ACCESS and has been suspended by + // the kernel. The kernel is waiting for us to reply with instructions. + // Our default is the "not handled" reply (by setting the RetCode field + // of the reply to KERN_FAILURE) which tells the kernel to continue + // searching at the process and system level. If this is an + // expected exception, we handle it and return KERN_SUCCESS. + bool handled = HandleMachException(request); + kern_return_t replyCode = handled ? KERN_SUCCESS : KERN_FAILURE; + + // This magic incantation to send a reply back to the kernel was + // derived from the exc_server generated by + // 'mig -v /usr/include/mach/mach_exc.defs'. + __Reply__exception_raise_t reply; + reply.Head.msgh_bits = MACH_MSGH_BITS(MACH_MSGH_BITS_REMOTE(request.body.Head.msgh_bits), 0); + reply.Head.msgh_size = sizeof(reply); + reply.Head.msgh_remote_port = request.body.Head.msgh_remote_port; + reply.Head.msgh_local_port = MACH_PORT_NULL; + reply.Head.msgh_id = request.body.Head.msgh_id + 100; + reply.NDR = NDR_record; + reply.RetCode = replyCode; + mach_msg(&reply.Head, MACH_SEND_MSG, sizeof(reply), 0, MACH_PORT_NULL, + MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL); + } + + return nullptr; +} + +#else // If not Windows or Mac, assume Unix + +static struct sigaction sPrevSIGSEGVHandler; +static struct sigaction sPrevSIGBUSHandler; +static struct sigaction sPrevSIGILLHandler; +static struct sigaction sPrevSIGFPEHandler; + +static void +WasmTrapHandler(int signum, siginfo_t* info, void* context) +{ + if (!sAlreadyHandlingTrap) { + AutoHandlingTrap aht; + assert(signum == SIGSEGV || signum == SIGBUS || signum == SIGFPE || signum == SIGILL); + + void *JmpBuf = HandleTrap(ContextToPC(static_cast(context)), signum, info, context); + + // Test if a custom instance signal handler handled the exception + if (((size_t) JmpBuf) == 1) + return; + + // Otherwise test if we need to longjmp to this buffer + if (JmpBuf != nullptr) { + sAlreadyHandlingTrap = false; + Unwind(JmpBuf); + } + + // ... and otherwise call the previous signal handler, if one is there + } + + struct sigaction* previousSignal = nullptr; + switch (signum) { + case SIGSEGV: previousSignal = &sPrevSIGSEGVHandler; break; + case SIGBUS: previousSignal = &sPrevSIGBUSHandler; break; + case SIGFPE: previousSignal = &sPrevSIGFPEHandler; break; + case SIGILL: previousSignal = &sPrevSIGILLHandler; break; + } + assert(previousSignal); + + // This signal is not for any compiled wasm code we expect, so we need to + // forward the signal to the next handler. If there is no next handler (SIG_IGN + // or SIG_DFL), then it's time to crash. To do this, we set the signal back to + // its original disposition and return. This will cause the faulting op to + // be re-executed which will crash in the normal way. The advantage of + // doing this to calling _exit() is that we remove ourselves from the crash + // stack which improves crash reports. If there is a next handler, call it. + // It will either crash synchronously, fix up the instruction so that + // execution can continue and return, or trigger a crash by returning the + // signal to it's original disposition and returning. + // + // Note: the order of these tests matter. + if (previousSignal->sa_flags & SA_SIGINFO) { + previousSignal->sa_sigaction(signum, info, context); + } else if (previousSignal->sa_handler == SIG_DFL || previousSignal->sa_handler == SIG_IGN) { + sigaction(signum, previousSignal, nullptr); + } else { + previousSignal->sa_handler(signum); + } +} +# endif // _WIN32 || __APPLE__ || assume unix + +#if defined(ANDROID) && defined(MOZ_LINKER) +extern "C" MFBT_API bool IsSignalHandlingBroken(); +#endif + +int +EnsureEagerSignalHandlers() +{ +#if defined(ANDROID) && defined(MOZ_LINKER) + // Signal handling is broken on some android systems. + if (IsSignalHandlingBroken()) { + return false; + } +#endif + + sAlreadyHandlingTrap = false; + + // Install whatever exception/signal handler is appropriate for the OS. +#if defined(_WIN32) + +# if defined(MOZ_ASAN) + // Under ASan we need to let the ASan runtime's ShadowExceptionHandler stay + // in the first handler position. This requires some coordination with + // MemoryProtectionExceptionHandler::isDisabled(). + const bool firstHandler = false; +# else + // Otherwise, WasmTrapHandler needs to go first, so that we can recover + // from wasm faults and continue execution without triggering handlers + // such as MemoryProtectionExceptionHandler that assume we are crashing. + const bool firstHandler = true; +# endif + if (!AddVectoredExceptionHandler(firstHandler, WasmTrapHandler)) { + // Windows has all sorts of random security knobs for disabling things + // so make this a dynamic failure that disables wasm, not an abort(). + return false; + } + +#elif defined(USE_APPLE_MACH_PORTS) + // All the Mach setup in EnsureDarwinMachPorts. +#else + // SA_ONSTACK allows us to handle signals on an alternate stack, so that + // the handler can run in response to running out of stack space on the + // main stack. Rust installs an alternate stack with sigaltstack, so we + // rely on that. + + // SA_NODEFER allows us to reenter the signal handler if we crash while + // handling the signal, and fall through to the Breakpad handler by testing + // handlingSegFault. + + // Allow handling OOB with signals on all architectures + struct sigaction faultHandler; + faultHandler.sa_flags = SA_SIGINFO | SA_NODEFER | SA_ONSTACK; + faultHandler.sa_sigaction = WasmTrapHandler; + sigemptyset(&faultHandler.sa_mask); + if (sigaction(SIGSEGV, &faultHandler, &sPrevSIGSEGVHandler)) { + perror("unable to install SIGSEGV handler"); + abort(); + } + +# if defined(__arm__) || defined(__APPLE__) + // On ARM, handle Unaligned Accesses. + // On Darwin, guard page accesses are raised as SIGBUS. + struct sigaction busHandler; + busHandler.sa_flags = SA_SIGINFO | SA_NODEFER | SA_ONSTACK; + busHandler.sa_sigaction = WasmTrapHandler; + sigemptyset(&busHandler.sa_mask); + if (sigaction(SIGBUS, &busHandler, &sPrevSIGBUSHandler)) { + perror("unable to install SIGBUS handler"); + abort(); + } +# endif + +# if !defined(__mips__) + // Wasm traps for MIPS currently only raise integer overflow fp exception. + struct sigaction illHandler; + illHandler.sa_flags = SA_SIGINFO | SA_NODEFER | SA_ONSTACK; + illHandler.sa_sigaction = WasmTrapHandler; + sigemptyset(&illHandler.sa_mask); + if (sigaction(SIGILL, &illHandler, &sPrevSIGILLHandler)) { + perror("unable to install wasm SIGILL handler"); + abort(); + } +# endif + +# if defined(__i386__) || defined(__x86_64__) || defined(__mips__) + // x86 uses SIGFPE to report division by zero, and wasm traps for MIPS + // currently raise integer overflow fp exception. + struct sigaction fpeHandler; + fpeHandler.sa_flags = SA_SIGINFO | SA_NODEFER | SA_ONSTACK; + fpeHandler.sa_sigaction = WasmTrapHandler; + sigemptyset(&fpeHandler.sa_mask); + if (sigaction(SIGFPE, &fpeHandler, &sPrevSIGFPEHandler)) { + perror("unable to install wasm SIGFPE handler"); + abort(); + } +# endif + +#endif + + return true; +} diff --git a/crates/runtime/signalhandlers/SignalHandlers.hpp b/crates/runtime/signalhandlers/SignalHandlers.hpp new file mode 100644 index 0000000000..5861da7a87 --- /dev/null +++ b/crates/runtime/signalhandlers/SignalHandlers.hpp @@ -0,0 +1,41 @@ +#ifndef signal_handlers_h +#define signal_handlers_h + +#include +#include +#ifndef __cplusplus +#include +#endif + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(_WIN32) +#include +#include +void* HandleTrap(const uint8_t*, LPEXCEPTION_POINTERS); +#else +void* HandleTrap(const uint8_t*, int, siginfo_t *, void *); +#endif + +void Unwind(void*); + +// This function performs the low-overhead signal handler initialization that we +// want to do eagerly to ensure a more-deterministic global process state. This +// is especially relevant for signal handlers since handler ordering depends on +// installation order: the wasm signal handler must run *before* the other crash +// handlers and since POSIX signal handlers work LIFO, this function needs to be +// called at the end of the startup process, after other handlers have been +// installed. This function can thus be called multiple times, having no effect +// after the first call. +int +EnsureEagerSignalHandlers(void); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // signal_handlers_h diff --git a/crates/runtime/signalhandlers/Trampolines.cpp b/crates/runtime/signalhandlers/Trampolines.cpp new file mode 100644 index 0000000000..e0702c349d --- /dev/null +++ b/crates/runtime/signalhandlers/Trampolines.cpp @@ -0,0 +1,23 @@ +#include + +#include "SignalHandlers.hpp" + +extern "C" +int RegisterSetjmp( + void **buf_storage, + void (*body)(void*), + void *payload) { + jmp_buf buf; + if (setjmp(buf) != 0) { + return 0; + } + *buf_storage = &buf; + body(payload); + return 1; +} + +extern "C" +void Unwind(void *JmpBuf) { + jmp_buf *buf = (jmp_buf*) JmpBuf; + longjmp(*buf, 1); +} diff --git a/crates/runtime/src/export.rs b/crates/runtime/src/export.rs new file mode 100644 index 0000000000..547d9593cd --- /dev/null +++ b/crates/runtime/src/export.rs @@ -0,0 +1,104 @@ +use crate::vmcontext::{ + VMContext, VMFunctionBody, VMGlobalDefinition, VMMemoryDefinition, VMTableDefinition, +}; +use wasmtime_environ::ir; +use wasmtime_environ::wasm::Global; +use wasmtime_environ::{MemoryPlan, TablePlan}; + +/// The value of an export passed from one instance to another. +#[derive(Debug, Clone)] +pub enum Export { + /// A function export value. + Function { + /// The address of the native-code function. + address: *const VMFunctionBody, + /// Pointer to the containing `VMContext`. + vmctx: *mut VMContext, + /// The function signature declaration, used for compatibilty checking. + signature: ir::Signature, + }, + + /// A table export value. + Table { + /// The address of the table descriptor. + definition: *mut VMTableDefinition, + /// Pointer to the containing `VMContext`. + vmctx: *mut VMContext, + /// The table declaration, used for compatibilty checking. + table: TablePlan, + }, + + /// A memory export value. + Memory { + /// The address of the memory descriptor. + definition: *mut VMMemoryDefinition, + /// Pointer to the containing `VMContext`. + vmctx: *mut VMContext, + /// The memory declaration, used for compatibilty checking. + memory: MemoryPlan, + }, + + /// A global export value. + Global { + /// The address of the global storage. + definition: *mut VMGlobalDefinition, + /// Pointer to the containing `VMContext`. + vmctx: *mut VMContext, + /// The global declaration, used for compatibilty checking. + global: Global, + }, +} + +impl Export { + /// Construct a function export value. + pub fn function( + address: *const VMFunctionBody, + vmctx: *mut VMContext, + signature: ir::Signature, + ) -> Self { + Self::Function { + address, + vmctx, + signature, + } + } + + /// Construct a table export value. + pub fn table( + definition: *mut VMTableDefinition, + vmctx: *mut VMContext, + table: TablePlan, + ) -> Self { + Self::Table { + definition, + vmctx, + table, + } + } + + /// Construct a memory export value. + pub fn memory( + definition: *mut VMMemoryDefinition, + vmctx: *mut VMContext, + memory: MemoryPlan, + ) -> Self { + Self::Memory { + definition, + vmctx, + memory, + } + } + + /// Construct a global export value. + pub fn global( + definition: *mut VMGlobalDefinition, + vmctx: *mut VMContext, + global: Global, + ) -> Self { + Self::Global { + definition, + vmctx, + global, + } + } +} diff --git a/crates/runtime/src/imports.rs b/crates/runtime/src/imports.rs new file mode 100644 index 0000000000..0214ce67b0 --- /dev/null +++ b/crates/runtime/src/imports.rs @@ -0,0 +1,54 @@ +use crate::instance::InstanceHandle; +use crate::vmcontext::{VMFunctionImport, VMGlobalImport, VMMemoryImport, VMTableImport}; +use std::collections::HashSet; +use wasmtime_environ::entity::{BoxedSlice, PrimaryMap}; +use wasmtime_environ::wasm::{FuncIndex, GlobalIndex, MemoryIndex, TableIndex}; + +/// Resolved import pointers. +#[derive(Clone)] +pub struct Imports { + /// The set of instances that the imports depend on. + pub dependencies: HashSet, + + /// Resolved addresses for imported functions. + pub functions: BoxedSlice, + + /// Resolved addresses for imported tables. + pub tables: BoxedSlice, + + /// Resolved addresses for imported memories. + pub memories: BoxedSlice, + + /// Resolved addresses for imported globals. + pub globals: BoxedSlice, +} + +impl Imports { + /// Construct a new `Imports` instance. + pub fn new( + dependencies: HashSet, + function_imports: PrimaryMap, + table_imports: PrimaryMap, + memory_imports: PrimaryMap, + global_imports: PrimaryMap, + ) -> Self { + Self { + dependencies, + functions: function_imports.into_boxed_slice(), + tables: table_imports.into_boxed_slice(), + memories: memory_imports.into_boxed_slice(), + globals: global_imports.into_boxed_slice(), + } + } + + /// Construct a new `Imports` instance with no imports. + pub fn none() -> Self { + Self { + dependencies: HashSet::new(), + functions: PrimaryMap::new().into_boxed_slice(), + tables: PrimaryMap::new().into_boxed_slice(), + memories: PrimaryMap::new().into_boxed_slice(), + globals: PrimaryMap::new().into_boxed_slice(), + } + } +} diff --git a/crates/runtime/src/instance.rs b/crates/runtime/src/instance.rs new file mode 100644 index 0000000000..94994929af --- /dev/null +++ b/crates/runtime/src/instance.rs @@ -0,0 +1,1336 @@ +//! An `Instance` contains all the runtime state used by execution of a +//! wasm module (except its callstack and register state). An +//! `InstanceHandle` is a reference-counting handle for an `Instance`. + +use crate::export::Export; +use crate::imports::Imports; +use crate::jit_int::GdbJitImageRegistration; +use crate::memory::LinearMemory; +use crate::signalhandlers; +use crate::table::Table; +use crate::traphandlers::{catch_traps, Trap}; +use crate::vmcontext::{ + VMBuiltinFunctionsArray, VMCallerCheckedAnyfunc, VMContext, VMFunctionBody, VMFunctionImport, + VMGlobalDefinition, VMGlobalImport, VMMemoryDefinition, VMMemoryImport, VMSharedSignatureIndex, + VMTableDefinition, VMTableImport, +}; +use crate::TrapRegistration; +use memoffset::offset_of; +use more_asserts::assert_lt; +use std::alloc::{self, Layout}; +use std::any::Any; +use std::cell::{Cell, RefCell}; +use std::collections::HashMap; +use std::collections::HashSet; +use std::convert::TryFrom; +use std::rc::Rc; +use std::sync::Arc; +use std::{mem, ptr, slice}; +use thiserror::Error; +use wasmtime_environ::entity::{packed_option::ReservedValue, BoxedSlice, EntityRef, PrimaryMap}; +use wasmtime_environ::wasm::{ + DefinedFuncIndex, DefinedGlobalIndex, DefinedMemoryIndex, DefinedTableIndex, FuncIndex, + GlobalIndex, GlobalInit, MemoryIndex, PassiveElemIndex, SignatureIndex, TableIndex, +}; +use wasmtime_environ::{ir, DataInitializer, Module, TableElements, VMOffsets}; + +cfg_if::cfg_if! { + if #[cfg(unix)] { + pub type SignalHandler = dyn Fn(libc::c_int, *const libc::siginfo_t, *const libc::c_void) -> bool; + + impl InstanceHandle { + /// Set a custom signal handler + pub fn set_signal_handler(&mut self, handler: H) + where + H: 'static + Fn(libc::c_int, *const libc::siginfo_t, *const libc::c_void) -> bool, + { + self.instance().signal_handler.set(Some(Box::new(handler))); + } + } + } else if #[cfg(target_os = "windows")] { + pub type SignalHandler = dyn Fn(winapi::um::winnt::PEXCEPTION_POINTERS) -> bool; + + impl InstanceHandle { + /// Set a custom signal handler + pub fn set_signal_handler(&mut self, handler: H) + where + H: 'static + Fn(winapi::um::winnt::PEXCEPTION_POINTERS) -> bool, + { + self.instance().signal_handler.set(Some(Box::new(handler))); + } + } + } +} + +/// A WebAssembly instance. +/// +/// This is repr(C) to ensure that the vmctx field is last. +#[repr(C)] +pub(crate) struct Instance { + /// The number of references to this `Instance`. + refcount: Cell, + + /// `Instance`s from which this `Instance` imports. These won't + /// create reference cycles because wasm instances can't cyclically + /// import from each other. + dependencies: HashSet, + + /// The `Module` this `Instance` was instantiated from. + module: Arc, + + /// Offsets in the `vmctx` region. + offsets: VMOffsets, + + /// WebAssembly linear memory data. + memories: BoxedSlice, + + /// WebAssembly table data. + tables: BoxedSlice, + + /// Passive elements in this instantiation. As `elem.drop`s happen, these + /// entries get removed. A missing entry is considered equivalent to an + /// empty slice. + passive_elements: RefCell>>, + + /// Pointers to functions in executable memory. + finished_functions: BoxedSlice, + + /// Hosts can store arbitrary per-instance information here. + host_state: Box, + + /// Optional image of JIT'ed code for debugger registration. + dbg_jit_registration: Option>, + + /// Handler run when `SIGBUS`, `SIGFPE`, `SIGILL`, or `SIGSEGV` are caught by the instance thread. + pub(crate) signal_handler: Cell>>, + + /// Handle to our registration of traps so signals know what trap to return + /// when a segfault/sigill happens. + pub(crate) trap_registration: TrapRegistration, + + /// Additional context used by compiled wasm code. This field is last, and + /// represents a dynamically-sized array that extends beyond the nominal + /// end of the struct (similar to a flexible array member). + vmctx: VMContext, +} + +#[allow(clippy::cast_ptr_alignment)] +impl Instance { + /// Helper function to access various locations offset from our `*mut + /// VMContext` object. + unsafe fn vmctx_plus_offset(&self, offset: u32) -> *mut T { + (self.vmctx_ptr() as *mut u8) + .add(usize::try_from(offset).unwrap()) + .cast() + } + + /// Return the indexed `VMSharedSignatureIndex`. + fn signature_id(&self, index: SignatureIndex) -> VMSharedSignatureIndex { + let index = usize::try_from(index.as_u32()).unwrap(); + unsafe { *self.signature_ids_ptr().add(index) } + } + + pub(crate) fn module(&self) -> &Arc { + &self.module + } + + pub(crate) fn module_ref(&self) -> &Module { + &*self.module + } + + /// Return a pointer to the `VMSharedSignatureIndex`s. + fn signature_ids_ptr(&self) -> *mut VMSharedSignatureIndex { + unsafe { self.vmctx_plus_offset(self.offsets.vmctx_signature_ids_begin()) } + } + + /// Return the indexed `VMFunctionImport`. + fn imported_function(&self, index: FuncIndex) -> &VMFunctionImport { + let index = usize::try_from(index.as_u32()).unwrap(); + unsafe { &*self.imported_functions_ptr().add(index) } + } + + /// Return a pointer to the `VMFunctionImport`s. + fn imported_functions_ptr(&self) -> *mut VMFunctionImport { + unsafe { self.vmctx_plus_offset(self.offsets.vmctx_imported_functions_begin()) } + } + + /// Return the index `VMTableImport`. + fn imported_table(&self, index: TableIndex) -> &VMTableImport { + let index = usize::try_from(index.as_u32()).unwrap(); + unsafe { &*self.imported_tables_ptr().add(index) } + } + + /// Return a pointer to the `VMTableImports`s. + fn imported_tables_ptr(&self) -> *mut VMTableImport { + unsafe { self.vmctx_plus_offset(self.offsets.vmctx_imported_tables_begin()) } + } + + /// Return the indexed `VMMemoryImport`. + fn imported_memory(&self, index: MemoryIndex) -> &VMMemoryImport { + let index = usize::try_from(index.as_u32()).unwrap(); + unsafe { &*self.imported_memories_ptr().add(index) } + } + + /// Return a pointer to the `VMMemoryImport`s. + fn imported_memories_ptr(&self) -> *mut VMMemoryImport { + unsafe { self.vmctx_plus_offset(self.offsets.vmctx_imported_memories_begin()) } + } + + /// Return the indexed `VMGlobalImport`. + fn imported_global(&self, index: GlobalIndex) -> &VMGlobalImport { + let index = usize::try_from(index.as_u32()).unwrap(); + unsafe { &*self.imported_globals_ptr().add(index) } + } + + /// Return a pointer to the `VMGlobalImport`s. + fn imported_globals_ptr(&self) -> *mut VMGlobalImport { + unsafe { self.vmctx_plus_offset(self.offsets.vmctx_imported_globals_begin()) } + } + + /// Return the indexed `VMTableDefinition`. + #[allow(dead_code)] + fn table(&self, index: DefinedTableIndex) -> VMTableDefinition { + unsafe { *self.table_ptr(index) } + } + + /// Updates the value for a defined table to `VMTableDefinition`. + fn set_table(&self, index: DefinedTableIndex, table: VMTableDefinition) { + unsafe { + *self.table_ptr(index) = table; + } + } + + /// Return the indexed `VMTableDefinition`. + fn table_ptr(&self, index: DefinedTableIndex) -> *mut VMTableDefinition { + let index = usize::try_from(index.as_u32()).unwrap(); + unsafe { self.tables_ptr().add(index) } + } + + /// Return a pointer to the `VMTableDefinition`s. + fn tables_ptr(&self) -> *mut VMTableDefinition { + unsafe { self.vmctx_plus_offset(self.offsets.vmctx_tables_begin()) } + } + + /// Get a locally defined or imported memory. + pub(crate) fn get_memory(&self, index: MemoryIndex) -> VMMemoryDefinition { + if let Some(defined_index) = self.module.local.defined_memory_index(index) { + self.memory(defined_index) + } else { + let import = self.imported_memory(index); + *unsafe { import.from.as_ref().unwrap() } + } + } + + /// Return the indexed `VMMemoryDefinition`. + fn memory(&self, index: DefinedMemoryIndex) -> VMMemoryDefinition { + unsafe { *self.memory_ptr(index) } + } + + /// Set the indexed memory to `VMMemoryDefinition`. + fn set_memory(&self, index: DefinedMemoryIndex, mem: VMMemoryDefinition) { + unsafe { + *self.memory_ptr(index) = mem; + } + } + + /// Return the indexed `VMMemoryDefinition`. + fn memory_ptr(&self, index: DefinedMemoryIndex) -> *mut VMMemoryDefinition { + let index = usize::try_from(index.as_u32()).unwrap(); + unsafe { self.memories_ptr().add(index) } + } + + /// Return a pointer to the `VMMemoryDefinition`s. + fn memories_ptr(&self) -> *mut VMMemoryDefinition { + unsafe { self.vmctx_plus_offset(self.offsets.vmctx_memories_begin()) } + } + + /// Return the indexed `VMGlobalDefinition`. + fn global(&self, index: DefinedGlobalIndex) -> VMGlobalDefinition { + unsafe { *self.global_ptr(index) } + } + + /// Set the indexed global to `VMGlobalDefinition`. + #[allow(dead_code)] + fn set_global(&self, index: DefinedGlobalIndex, global: VMGlobalDefinition) { + unsafe { + *self.global_ptr(index) = global; + } + } + + /// Return the indexed `VMGlobalDefinition`. + fn global_ptr(&self, index: DefinedGlobalIndex) -> *mut VMGlobalDefinition { + let index = usize::try_from(index.as_u32()).unwrap(); + unsafe { self.globals_ptr().add(index) } + } + + /// Return a pointer to the `VMGlobalDefinition`s. + fn globals_ptr(&self) -> *mut VMGlobalDefinition { + unsafe { self.vmctx_plus_offset(self.offsets.vmctx_globals_begin()) } + } + + /// Return a pointer to the `VMBuiltinFunctionsArray`. + fn builtin_functions_ptr(&self) -> *mut VMBuiltinFunctionsArray { + unsafe { self.vmctx_plus_offset(self.offsets.vmctx_builtin_functions_begin()) } + } + + /// Return a reference to the vmctx used by compiled wasm code. + pub fn vmctx(&self) -> &VMContext { + &self.vmctx + } + + /// Return a raw pointer to the vmctx used by compiled wasm code. + pub fn vmctx_ptr(&self) -> *mut VMContext { + self.vmctx() as *const VMContext as *mut VMContext + } + + /// Lookup an export with the given name. + pub fn lookup(&self, field: &str) -> Option { + let export = if let Some(export) = self.module.exports.get(field) { + export.clone() + } else { + return None; + }; + Some(self.lookup_by_declaration(&export)) + } + + /// Lookup an export with the given export declaration. + pub fn lookup_by_declaration(&self, export: &wasmtime_environ::Export) -> Export { + match export { + wasmtime_environ::Export::Function(index) => { + let signature = + self.module.local.signatures[self.module.local.functions[*index]].clone(); + let (address, vmctx) = + if let Some(def_index) = self.module.local.defined_func_index(*index) { + ( + self.finished_functions[def_index] as *const _, + self.vmctx_ptr(), + ) + } else { + let import = self.imported_function(*index); + (import.body, import.vmctx) + }; + Export::Function { + address, + signature, + vmctx, + } + } + wasmtime_environ::Export::Table(index) => { + let (definition, vmctx) = + if let Some(def_index) = self.module.local.defined_table_index(*index) { + (self.table_ptr(def_index), self.vmctx_ptr()) + } else { + let import = self.imported_table(*index); + (import.from, import.vmctx) + }; + Export::Table { + definition, + vmctx, + table: self.module.local.table_plans[*index].clone(), + } + } + wasmtime_environ::Export::Memory(index) => { + let (definition, vmctx) = + if let Some(def_index) = self.module.local.defined_memory_index(*index) { + (self.memory_ptr(def_index), self.vmctx_ptr()) + } else { + let import = self.imported_memory(*index); + (import.from, import.vmctx) + }; + Export::Memory { + definition, + vmctx, + memory: self.module.local.memory_plans[*index].clone(), + } + } + wasmtime_environ::Export::Global(index) => Export::Global { + definition: if let Some(def_index) = self.module.local.defined_global_index(*index) + { + self.global_ptr(def_index) + } else { + self.imported_global(*index).from + }, + vmctx: self.vmctx_ptr(), + global: self.module.local.globals[*index], + }, + } + } + + /// Return an iterator over the exports of this instance. + /// + /// Specifically, it provides access to the key-value pairs, where they keys + /// are export names, and the values are export declarations which can be + /// resolved `lookup_by_declaration`. + pub fn exports(&self) -> indexmap::map::Iter { + self.module.exports.iter() + } + + /// Return a reference to the custom state attached to this instance. + pub fn host_state(&self) -> &dyn Any { + &*self.host_state + } + + /// Invoke the WebAssembly start function of the instance, if one is present. + fn invoke_start_function(&self) -> Result<(), InstantiationError> { + let start_index = match self.module.start_func { + Some(idx) => idx, + None => return Ok(()), + }; + + let (callee_address, callee_vmctx) = match self.module.local.defined_func_index(start_index) + { + Some(defined_index) => { + let body = *self + .finished_functions + .get(defined_index) + .expect("function index is out of bounds"); + (body as *const _, self.vmctx_ptr()) + } + None => { + assert_lt!(start_index.index(), self.module.imported_funcs.len()); + let import = self.imported_function(start_index); + (import.body, import.vmctx) + } + }; + + // Make the call. + unsafe { + catch_traps(callee_vmctx, || { + mem::transmute::< + *const VMFunctionBody, + unsafe extern "C" fn(*mut VMContext, *mut VMContext), + >(callee_address)(callee_vmctx, self.vmctx_ptr()) + }) + .map_err(InstantiationError::StartTrap) + } + } + + /// Return the offset from the vmctx pointer to its containing Instance. + pub(crate) fn vmctx_offset() -> isize { + offset_of!(Self, vmctx) as isize + } + + /// Return the table index for the given `VMTableDefinition`. + pub(crate) fn table_index(&self, table: &VMTableDefinition) -> DefinedTableIndex { + let offsets = &self.offsets; + let begin = unsafe { + (&self.vmctx as *const VMContext as *const u8) + .add(usize::try_from(offsets.vmctx_tables_begin()).unwrap()) + } as *const VMTableDefinition; + let end: *const VMTableDefinition = table; + // TODO: Use `offset_from` once it stablizes. + let index = DefinedTableIndex::new( + (end as usize - begin as usize) / mem::size_of::(), + ); + assert_lt!(index.index(), self.tables.len()); + index + } + + /// Return the memory index for the given `VMMemoryDefinition`. + pub(crate) fn memory_index(&self, memory: &VMMemoryDefinition) -> DefinedMemoryIndex { + let offsets = &self.offsets; + let begin = unsafe { + (&self.vmctx as *const VMContext as *const u8) + .add(usize::try_from(offsets.vmctx_memories_begin()).unwrap()) + } as *const VMMemoryDefinition; + let end: *const VMMemoryDefinition = memory; + // TODO: Use `offset_from` once it stablizes. + let index = DefinedMemoryIndex::new( + (end as usize - begin as usize) / mem::size_of::(), + ); + assert_lt!(index.index(), self.memories.len()); + index + } + + /// Grow memory by the specified amount of pages. + /// + /// Returns `None` if memory can't be grown by the specified amount + /// of pages. + pub(crate) fn memory_grow(&self, memory_index: DefinedMemoryIndex, delta: u32) -> Option { + let result = self + .memories + .get(memory_index) + .unwrap_or_else(|| panic!("no memory for index {}", memory_index.index())) + .grow(delta); + + // Keep current the VMContext pointers used by compiled wasm code. + self.set_memory(memory_index, self.memories[memory_index].vmmemory()); + + result + } + + /// Grow imported memory by the specified amount of pages. + /// + /// Returns `None` if memory can't be grown by the specified amount + /// of pages. + /// + /// # Safety + /// This and `imported_memory_size` are currently unsafe because they + /// dereference the memory import's pointers. + pub(crate) unsafe fn imported_memory_grow( + &self, + memory_index: MemoryIndex, + delta: u32, + ) -> Option { + let import = self.imported_memory(memory_index); + let foreign_instance = (&*import.vmctx).instance(); + let foreign_memory = &*import.from; + let foreign_index = foreign_instance.memory_index(foreign_memory); + + foreign_instance.memory_grow(foreign_index, delta) + } + + /// Returns the number of allocated wasm pages. + pub(crate) fn memory_size(&self, memory_index: DefinedMemoryIndex) -> u32 { + self.memories + .get(memory_index) + .unwrap_or_else(|| panic!("no memory for index {}", memory_index.index())) + .size() + } + + /// Returns the number of allocated wasm pages in an imported memory. + /// + /// # Safety + /// This and `imported_memory_grow` are currently unsafe because they + /// dereference the memory import's pointers. + pub(crate) unsafe fn imported_memory_size(&self, memory_index: MemoryIndex) -> u32 { + let import = self.imported_memory(memory_index); + let foreign_instance = (&mut *import.vmctx).instance(); + let foreign_memory = &mut *import.from; + let foreign_index = foreign_instance.memory_index(foreign_memory); + + foreign_instance.memory_size(foreign_index) + } + + /// Grow table by the specified amount of elements. + /// + /// Returns `None` if table can't be grown by the specified amount + /// of elements. + pub(crate) fn table_grow(&self, table_index: DefinedTableIndex, delta: u32) -> Option { + let result = self + .tables + .get(table_index) + .unwrap_or_else(|| panic!("no table for index {}", table_index.index())) + .grow(delta); + + // Keep current the VMContext pointers used by compiled wasm code. + self.set_table(table_index, self.tables[table_index].vmtable()); + + result + } + + // Get table element by index. + fn table_get( + &self, + table_index: DefinedTableIndex, + index: u32, + ) -> Option { + self.tables + .get(table_index) + .unwrap_or_else(|| panic!("no table for index {}", table_index.index())) + .get(index) + } + + fn table_set( + &self, + table_index: DefinedTableIndex, + index: u32, + val: VMCallerCheckedAnyfunc, + ) -> Result<(), ()> { + self.tables + .get(table_index) + .unwrap_or_else(|| panic!("no table for index {}", table_index.index())) + .set(index, val) + } + + fn alloc_layout(&self) -> Layout { + let size = mem::size_of_val(self) + .checked_add(usize::try_from(self.offsets.size_of_vmctx()).unwrap()) + .unwrap(); + let align = mem::align_of_val(self); + Layout::from_size_align(size, align).unwrap() + } + + /// Get a `VMCallerCheckedAnyfunc` for the given `FuncIndex`. + fn get_caller_checked_anyfunc(&self, index: FuncIndex) -> VMCallerCheckedAnyfunc { + if index == FuncIndex::reserved_value() { + return VMCallerCheckedAnyfunc::default(); + } + + let sig = self.module.local.functions[index]; + let type_index = self.signature_id(sig); + + let (func_ptr, vmctx) = if let Some(def_index) = self.module.local.defined_func_index(index) + { + ( + self.finished_functions[def_index] as *const _, + self.vmctx_ptr(), + ) + } else { + let import = self.imported_function(index); + (import.body, import.vmctx) + }; + VMCallerCheckedAnyfunc { + func_ptr, + type_index, + vmctx, + } + } + + /// The `table.init` operation: initializes a portion of a table with a + /// passive element. + /// + /// # Errors + /// + /// Returns a `Trap` error when the range within the table is out of bounds + /// or the range within the passive element is out of bounds. + pub(crate) fn table_init( + &self, + table_index: TableIndex, + elem_index: PassiveElemIndex, + dst: u32, + src: u32, + len: u32, + source_loc: ir::SourceLoc, + ) -> Result<(), Trap> { + // https://webassembly.github.io/bulk-memory-operations/core/exec/instructions.html#exec-table-init + + let table = self.get_table(table_index); + let passive_elements = self.passive_elements.borrow(); + let elem = passive_elements + .get(&elem_index) + .map(|e| &**e) + .unwrap_or_else(|| &[]); + + if src + .checked_add(len) + .map_or(true, |n| n as usize > elem.len()) + || dst.checked_add(len).map_or(true, |m| m > table.size()) + { + return Err(Trap::wasm(source_loc, ir::TrapCode::TableOutOfBounds)); + } + + // TODO(#983): investigate replacing this get/set loop with a `memcpy`. + for (dst, src) in (dst..dst + len).zip(src..src + len) { + table + .set(dst, elem[src as usize].clone()) + .expect("should never panic because we already did the bounds check above"); + } + + Ok(()) + } + + /// Drop an element. + pub(crate) fn elem_drop(&self, elem_index: PassiveElemIndex) { + // https://webassembly.github.io/reference-types/core/exec/instructions.html#exec-elem-drop + + let mut passive_elements = self.passive_elements.borrow_mut(); + passive_elements.remove(&elem_index); + // Note that we don't check that we actually removed an element because + // dropping a non-passive element is a no-op (not a trap). + } + + /// Do a `memory.copy` for a locally defined memory. + /// + /// # Errors + /// + /// Returns a `Trap` error when the source or destination ranges are out of + /// bounds. + pub(crate) fn defined_memory_copy( + &self, + memory_index: DefinedMemoryIndex, + dst: u32, + src: u32, + len: u32, + source_loc: ir::SourceLoc, + ) -> Result<(), Trap> { + // https://webassembly.github.io/reference-types/core/exec/instructions.html#exec-memory-copy + + let memory = self.memory(memory_index); + + if src + .checked_add(len) + .map_or(true, |n| n as usize > memory.current_length) + || dst + .checked_add(len) + .map_or(true, |m| m as usize > memory.current_length) + { + return Err(Trap::wasm(source_loc, ir::TrapCode::HeapOutOfBounds)); + } + + let dst = usize::try_from(dst).unwrap(); + let src = usize::try_from(src).unwrap(); + + // Bounds and casts are checked above, by this point we know that + // everything is safe. + unsafe { + let dst = memory.base.add(dst); + let src = memory.base.add(src); + ptr::copy(src, dst, len as usize); + } + + Ok(()) + } + + /// Perform a `memory.copy` on an imported memory. + pub(crate) fn imported_memory_copy( + &self, + memory_index: MemoryIndex, + dst: u32, + src: u32, + len: u32, + source_loc: ir::SourceLoc, + ) -> Result<(), Trap> { + let import = self.imported_memory(memory_index); + unsafe { + let foreign_instance = (&*import.vmctx).instance(); + let foreign_memory = &*import.from; + let foreign_index = foreign_instance.memory_index(foreign_memory); + foreign_instance.defined_memory_copy(foreign_index, dst, src, len, source_loc) + } + } + + /// Perform the `memory.fill` operation on a locally defined memory. + /// + /// # Errors + /// + /// Returns a `Trap` error if the memory range is out of bounds. + pub(crate) fn defined_memory_fill( + &self, + memory_index: DefinedMemoryIndex, + dst: u32, + val: u32, + len: u32, + source_loc: ir::SourceLoc, + ) -> Result<(), Trap> { + let memory = self.memory(memory_index); + + if dst + .checked_add(len) + .map_or(true, |m| m as usize > memory.current_length) + { + return Err(Trap::wasm(source_loc, ir::TrapCode::HeapOutOfBounds)); + } + + let dst = isize::try_from(dst).unwrap(); + let val = val as u8; + + // Bounds and casts are checked above, by this point we know that + // everything is safe. + unsafe { + let dst = memory.base.offset(dst); + ptr::write_bytes(dst, val, len as usize); + } + + Ok(()) + } + + /// Perform the `memory.fill` operation on an imported memory. + /// + /// # Errors + /// + /// Returns a `Trap` error if the memory range is out of bounds. + pub(crate) fn imported_memory_fill( + &self, + memory_index: MemoryIndex, + dst: u32, + val: u32, + len: u32, + source_loc: ir::SourceLoc, + ) -> Result<(), Trap> { + let import = self.imported_memory(memory_index); + unsafe { + let foreign_instance = (&*import.vmctx).instance(); + let foreign_memory = &*import.from; + let foreign_index = foreign_instance.memory_index(foreign_memory); + foreign_instance.defined_memory_fill(foreign_index, dst, val, len, source_loc) + } + } + + /// Get a table by index regardless of whether it is locally-defined or an + /// imported, foreign table. + pub(crate) fn get_table(&self, table_index: TableIndex) -> &Table { + if let Some(defined_table_index) = self.module.local.defined_table_index(table_index) { + self.get_defined_table(defined_table_index) + } else { + self.get_foreign_table(table_index) + } + } + + /// Get a locally-defined table. + pub(crate) fn get_defined_table(&self, index: DefinedTableIndex) -> &Table { + &self.tables[index] + } + + /// Get an imported, foreign table. + pub(crate) fn get_foreign_table(&self, index: TableIndex) -> &Table { + let import = self.imported_table(index); + let foreign_instance = unsafe { (&mut *(import).vmctx).instance() }; + let foreign_table = unsafe { &mut *(import).from }; + let foreign_index = foreign_instance.table_index(foreign_table); + &foreign_instance.tables[foreign_index] + } +} + +/// A handle holding an `Instance` of a WebAssembly module. +#[derive(Hash, PartialEq, Eq)] +pub struct InstanceHandle { + instance: *mut Instance, +} + +impl InstanceHandle { + /// Create a new `InstanceHandle` pointing at a new `Instance`. + /// + /// # Unsafety + /// + /// This method is not necessarily inherently unsafe to call, but in general + /// the APIs of an `Instance` are quite unsafe and have not been really + /// audited for safety that much. As a result the unsafety here on this + /// method is a low-overhead way of saying "this is an extremely unsafe type + /// to work with". + /// + /// Extreme care must be taken when working with `InstanceHandle` and it's + /// recommended to have relatively intimate knowledge of how it works + /// internally if you'd like to do so. If possible it's recommended to use + /// the `wasmtime` crate API rather than this type since that is vetted for + /// safety. + pub unsafe fn new( + module: Arc, + trap_registration: TrapRegistration, + finished_functions: BoxedSlice, + imports: Imports, + data_initializers: &[DataInitializer<'_>], + vmshared_signatures: BoxedSlice, + dbg_jit_registration: Option>, + is_bulk_memory: bool, + host_state: Box, + ) -> Result { + let tables = create_tables(&module); + let memories = create_memories(&module)?; + + let vmctx_tables = tables + .values() + .map(Table::vmtable) + .collect::>() + .into_boxed_slice(); + + let vmctx_memories = memories + .values() + .map(LinearMemory::vmmemory) + .collect::>() + .into_boxed_slice(); + + let vmctx_globals = create_globals(&module); + + let offsets = VMOffsets::new(mem::size_of::<*const u8>() as u8, &module.local); + + let handle = { + let instance = Instance { + refcount: Cell::new(1), + dependencies: imports.dependencies, + module, + offsets, + memories, + tables, + passive_elements: Default::default(), + finished_functions, + dbg_jit_registration, + host_state, + signal_handler: Cell::new(None), + trap_registration, + vmctx: VMContext {}, + }; + let layout = instance.alloc_layout(); + let instance_ptr = alloc::alloc(layout) as *mut Instance; + if instance_ptr.is_null() { + alloc::handle_alloc_error(layout); + } + ptr::write(instance_ptr, instance); + InstanceHandle { + instance: instance_ptr, + } + }; + let instance = handle.instance(); + + ptr::copy( + vmshared_signatures.values().as_slice().as_ptr(), + instance.signature_ids_ptr() as *mut VMSharedSignatureIndex, + vmshared_signatures.len(), + ); + ptr::copy( + imports.functions.values().as_slice().as_ptr(), + instance.imported_functions_ptr() as *mut VMFunctionImport, + imports.functions.len(), + ); + ptr::copy( + imports.tables.values().as_slice().as_ptr(), + instance.imported_tables_ptr() as *mut VMTableImport, + imports.tables.len(), + ); + ptr::copy( + imports.memories.values().as_slice().as_ptr(), + instance.imported_memories_ptr() as *mut VMMemoryImport, + imports.memories.len(), + ); + ptr::copy( + imports.globals.values().as_slice().as_ptr(), + instance.imported_globals_ptr() as *mut VMGlobalImport, + imports.globals.len(), + ); + ptr::copy( + vmctx_tables.values().as_slice().as_ptr(), + instance.tables_ptr() as *mut VMTableDefinition, + vmctx_tables.len(), + ); + ptr::copy( + vmctx_memories.values().as_slice().as_ptr(), + instance.memories_ptr() as *mut VMMemoryDefinition, + vmctx_memories.len(), + ); + ptr::copy( + vmctx_globals.values().as_slice().as_ptr(), + instance.globals_ptr() as *mut VMGlobalDefinition, + vmctx_globals.len(), + ); + ptr::write( + instance.builtin_functions_ptr() as *mut VMBuiltinFunctionsArray, + VMBuiltinFunctionsArray::initialized(), + ); + + // Check initializer bounds before initializing anything. Only do this + // when bulk memory is disabled, since the bulk memory proposal changes + // instantiation such that the intermediate results of failed + // initializations are visible. + if !is_bulk_memory { + check_table_init_bounds(instance)?; + check_memory_init_bounds(instance, data_initializers)?; + } + + // Apply the initializers. + initialize_tables(instance)?; + initialize_passive_elements(instance); + initialize_memories(instance, data_initializers)?; + initialize_globals(instance); + + // Ensure that our signal handlers are ready for action. + // TODO: Move these calls out of `InstanceHandle`. + signalhandlers::init(); + + // The WebAssembly spec specifies that the start function is + // invoked automatically at instantiation time. + instance.invoke_start_function()?; + + Ok(handle) + } + + /// Create a new `InstanceHandle` pointing at the instance + /// pointed to by the given `VMContext` pointer. + /// + /// # Safety + /// This is unsafe because it doesn't work on just any `VMContext`, it must + /// be a `VMContext` allocated as part of an `Instance`. + pub unsafe fn from_vmctx(vmctx: *mut VMContext) -> Self { + let instance = (&mut *vmctx).instance(); + instance.refcount.set(instance.refcount.get() + 1); + Self { + instance: instance as *const Instance as *mut Instance, + } + } + + /// Return a reference to the vmctx used by compiled wasm code. + pub fn vmctx(&self) -> &VMContext { + self.instance().vmctx() + } + + /// Return a raw pointer to the vmctx used by compiled wasm code. + pub fn vmctx_ptr(&self) -> *mut VMContext { + self.instance().vmctx_ptr() + } + + /// Return a reference-counting pointer to a module. + pub fn module(&self) -> &Arc { + self.instance().module() + } + + /// Return a reference to a module. + pub fn module_ref(&self) -> &Module { + self.instance().module_ref() + } + + /// Lookup an export with the given name. + pub fn lookup(&self, field: &str) -> Option { + self.instance().lookup(field) + } + + /// Lookup an export with the given export declaration. + pub fn lookup_by_declaration(&self, export: &wasmtime_environ::Export) -> Export { + self.instance().lookup_by_declaration(export) + } + + /// Return an iterator over the exports of this instance. + /// + /// Specifically, it provides access to the key-value pairs, where the keys + /// are export names, and the values are export declarations which can be + /// resolved `lookup_by_declaration`. + pub fn exports(&self) -> indexmap::map::Iter { + self.instance().exports() + } + + /// Return a reference to the custom state attached to this instance. + pub fn host_state(&self) -> &dyn Any { + self.instance().host_state() + } + + /// Return the memory index for the given `VMMemoryDefinition` in this instance. + pub fn memory_index(&self, memory: &VMMemoryDefinition) -> DefinedMemoryIndex { + self.instance().memory_index(memory) + } + + /// Grow memory in this instance by the specified amount of pages. + /// + /// Returns `None` if memory can't be grown by the specified amount + /// of pages. + pub fn memory_grow(&self, memory_index: DefinedMemoryIndex, delta: u32) -> Option { + self.instance().memory_grow(memory_index, delta) + } + + /// Return the table index for the given `VMTableDefinition` in this instance. + pub fn table_index(&self, table: &VMTableDefinition) -> DefinedTableIndex { + self.instance().table_index(table) + } + + /// Grow table in this instance by the specified amount of pages. + /// + /// Returns `None` if memory can't be grown by the specified amount + /// of pages. + pub fn table_grow(&self, table_index: DefinedTableIndex, delta: u32) -> Option { + self.instance().table_grow(table_index, delta) + } + + /// Get table element reference. + /// + /// Returns `None` if index is out of bounds. + pub fn table_get( + &self, + table_index: DefinedTableIndex, + index: u32, + ) -> Option { + self.instance().table_get(table_index, index) + } + + /// Set table element reference. + /// + /// Returns an error if the index is out of bounds + pub fn table_set( + &self, + table_index: DefinedTableIndex, + index: u32, + val: VMCallerCheckedAnyfunc, + ) -> Result<(), ()> { + self.instance().table_set(table_index, index, val) + } + + /// Get a table defined locally within this module. + pub fn get_defined_table(&self, index: DefinedTableIndex) -> &Table { + self.instance().get_defined_table(index) + } + + /// Return a reference to the contained `Instance`. + pub(crate) fn instance(&self) -> &Instance { + unsafe { &*(self.instance as *const Instance) } + } +} + +impl Clone for InstanceHandle { + fn clone(&self) -> Self { + let instance = self.instance(); + instance.refcount.set(instance.refcount.get() + 1); + Self { + instance: self.instance, + } + } +} + +impl Drop for InstanceHandle { + fn drop(&mut self) { + let instance = self.instance(); + let count = instance.refcount.get(); + instance.refcount.set(count - 1); + if count == 1 { + let layout = instance.alloc_layout(); + unsafe { + ptr::drop_in_place(self.instance); + alloc::dealloc(self.instance.cast(), layout); + } + } + } +} + +fn check_table_init_bounds(instance: &Instance) -> Result<(), InstantiationError> { + let module = Arc::clone(&instance.module); + for init in &module.table_elements { + let start = get_table_init_start(init, instance); + let table = instance.get_table(init.table_index); + + let size = usize::try_from(table.size()).unwrap(); + if size < start + init.elements.len() { + return Err(InstantiationError::Link(LinkError( + "table out of bounds: elements segment does not fit".to_owned(), + ))); + } + } + + Ok(()) +} + +/// Compute the offset for a memory data initializer. +fn get_memory_init_start(init: &DataInitializer<'_>, instance: &Instance) -> usize { + let mut start = init.location.offset; + + if let Some(base) = init.location.base { + let val = unsafe { + if let Some(def_index) = instance.module.local.defined_global_index(base) { + *instance.global(def_index).as_u32() + } else { + *(*instance.imported_global(base).from).as_u32() + } + }; + start += usize::try_from(val).unwrap(); + } + + start +} + +/// Return a byte-slice view of a memory's data. +unsafe fn get_memory_slice<'instance>( + init: &DataInitializer<'_>, + instance: &'instance Instance, +) -> &'instance mut [u8] { + let memory = if let Some(defined_memory_index) = instance + .module + .local + .defined_memory_index(init.location.memory_index) + { + instance.memory(defined_memory_index) + } else { + let import = instance.imported_memory(init.location.memory_index); + let foreign_instance = (&mut *(import).vmctx).instance(); + let foreign_memory = &mut *(import).from; + let foreign_index = foreign_instance.memory_index(foreign_memory); + foreign_instance.memory(foreign_index) + }; + slice::from_raw_parts_mut(memory.base, memory.current_length) +} + +fn check_memory_init_bounds( + instance: &Instance, + data_initializers: &[DataInitializer<'_>], +) -> Result<(), InstantiationError> { + for init in data_initializers { + let start = get_memory_init_start(init, instance); + unsafe { + let mem_slice = get_memory_slice(init, instance); + if mem_slice.get_mut(start..start + init.data.len()).is_none() { + return Err(InstantiationError::Link(LinkError( + "memory out of bounds: data segment does not fit".into(), + ))); + } + } + } + + Ok(()) +} + +/// Allocate memory for just the tables of the current module. +fn create_tables(module: &Module) -> BoxedSlice { + let num_imports = module.imported_tables.len(); + let mut tables: PrimaryMap = + PrimaryMap::with_capacity(module.local.table_plans.len() - num_imports); + for table in &module.local.table_plans.values().as_slice()[num_imports..] { + tables.push(Table::new(table)); + } + tables.into_boxed_slice() +} + +/// Compute the offset for a table element initializer. +fn get_table_init_start(init: &TableElements, instance: &Instance) -> usize { + let mut start = init.offset; + + if let Some(base) = init.base { + let val = unsafe { + if let Some(def_index) = instance.module.local.defined_global_index(base) { + *instance.global(def_index).as_u32() + } else { + *(*instance.imported_global(base).from).as_u32() + } + }; + start += usize::try_from(val).unwrap(); + } + + start +} + +/// Initialize the table memory from the provided initializers. +fn initialize_tables(instance: &Instance) -> Result<(), InstantiationError> { + let module = Arc::clone(&instance.module); + for init in &module.table_elements { + let start = get_table_init_start(init, instance); + let table = instance.get_table(init.table_index); + + if start + .checked_add(init.elements.len()) + .map_or(true, |end| end > table.size() as usize) + { + return Err(InstantiationError::Trap(Trap::wasm( + ir::SourceLoc::default(), + ir::TrapCode::HeapOutOfBounds, + ))); + } + + for (i, func_idx) in init.elements.iter().enumerate() { + let anyfunc = instance.get_caller_checked_anyfunc(*func_idx); + table + .set(u32::try_from(start + i).unwrap(), anyfunc) + .unwrap(); + } + } + + Ok(()) +} + +/// Initialize the `Instance::passive_elements` map by resolving the +/// `Module::passive_elements`'s `FuncIndex`s into `VMCallerCheckedAnyfunc`s for +/// this instance. +fn initialize_passive_elements(instance: &Instance) { + let mut passive_elements = instance.passive_elements.borrow_mut(); + debug_assert!( + passive_elements.is_empty(), + "should only be called once, at initialization time" + ); + + passive_elements.extend( + instance + .module + .passive_elements + .iter() + .filter(|(_, segments)| !segments.is_empty()) + .map(|(idx, segments)| { + ( + *idx, + segments + .iter() + .map(|s| instance.get_caller_checked_anyfunc(*s)) + .collect(), + ) + }), + ); +} + +/// Allocate memory for just the memories of the current module. +fn create_memories( + module: &Module, +) -> Result, InstantiationError> { + let num_imports = module.imported_memories.len(); + let mut memories: PrimaryMap = + PrimaryMap::with_capacity(module.local.memory_plans.len() - num_imports); + for plan in &module.local.memory_plans.values().as_slice()[num_imports..] { + memories.push(LinearMemory::new(plan).map_err(InstantiationError::Resource)?); + } + Ok(memories.into_boxed_slice()) +} + +/// Initialize the table memory from the provided initializers. +fn initialize_memories( + instance: &Instance, + data_initializers: &[DataInitializer<'_>], +) -> Result<(), InstantiationError> { + for init in data_initializers { + let memory = instance.get_memory(init.location.memory_index); + + let start = get_memory_init_start(init, instance); + if start + .checked_add(init.data.len()) + .map_or(true, |end| end > memory.current_length) + { + return Err(InstantiationError::Trap(Trap::wasm( + ir::SourceLoc::default(), + ir::TrapCode::HeapOutOfBounds, + ))); + } + + unsafe { + let mem_slice = get_memory_slice(init, instance); + let end = start + init.data.len(); + let to_init = &mut mem_slice[start..end]; + to_init.copy_from_slice(init.data); + } + } + + Ok(()) +} + +/// Allocate memory for just the globals of the current module, +/// with initializers applied. +fn create_globals(module: &Module) -> BoxedSlice { + let num_imports = module.imported_globals.len(); + let mut vmctx_globals = PrimaryMap::with_capacity(module.local.globals.len() - num_imports); + + for _ in &module.local.globals.values().as_slice()[num_imports..] { + vmctx_globals.push(VMGlobalDefinition::new()); + } + + vmctx_globals.into_boxed_slice() +} + +fn initialize_globals(instance: &Instance) { + let module = Arc::clone(&instance.module); + let num_imports = module.imported_globals.len(); + for (index, global) in module.local.globals.iter().skip(num_imports) { + let def_index = module.local.defined_global_index(index).unwrap(); + unsafe { + let to = instance.global_ptr(def_index); + match global.initializer { + GlobalInit::I32Const(x) => *(*to).as_i32_mut() = x, + GlobalInit::I64Const(x) => *(*to).as_i64_mut() = x, + GlobalInit::F32Const(x) => *(*to).as_f32_bits_mut() = x, + GlobalInit::F64Const(x) => *(*to).as_f64_bits_mut() = x, + GlobalInit::V128Const(x) => *(*to).as_u128_bits_mut() = x.0, + GlobalInit::GetGlobal(x) => { + let from = if let Some(def_x) = module.local.defined_global_index(x) { + instance.global(def_x) + } else { + *instance.imported_global(x).from + }; + *to = from; + } + GlobalInit::Import => panic!("locally-defined global initialized as import"), + GlobalInit::RefNullConst | GlobalInit::RefFunc(_) => unimplemented!(), + } + } + } +} + +/// An link error while instantiating a module. +#[derive(Error, Debug)] +#[error("Link error: {0}")] +pub struct LinkError(pub String); + +/// An error while instantiating a module. +#[derive(Error, Debug)] +pub enum InstantiationError { + /// Insufficient resources available for execution. + #[error("Insufficient resources: {0}")] + Resource(String), + + /// A wasm link error occured. + #[error("Failed to link module")] + Link(#[from] LinkError), + + /// A trap ocurred during instantiation, after linking. + #[error("Trap occurred during instantiation")] + Trap(#[source] Trap), + + /// A compilation error occured. + #[error("Trap occurred while invoking start function")] + StartTrap(#[source] Trap), +} diff --git a/crates/runtime/src/jit_int.rs b/crates/runtime/src/jit_int.rs new file mode 100644 index 0000000000..a8a0a44d6f --- /dev/null +++ b/crates/runtime/src/jit_int.rs @@ -0,0 +1,118 @@ +//! The GDB's JIT compilation interface. The low level module that exposes +//! the __jit_debug_register_code() and __jit_debug_descriptor to register +//! or unregister generated object images with debuggers. + +use std::ptr; + +#[repr(C)] +struct JITCodeEntry { + next_entry: *mut JITCodeEntry, + prev_entry: *mut JITCodeEntry, + symfile_addr: *const u8, + symfile_size: u64, +} + +const JIT_NOACTION: u32 = 0; +const JIT_REGISTER_FN: u32 = 1; +const JIT_UNREGISTER_FN: u32 = 2; + +#[repr(C)] +struct JITDescriptor { + version: u32, + action_flag: u32, + relevant_entry: *mut JITCodeEntry, + first_entry: *mut JITCodeEntry, +} + +#[no_mangle] +#[used] +static mut __jit_debug_descriptor: JITDescriptor = JITDescriptor { + version: 1, + action_flag: JIT_NOACTION, + relevant_entry: ptr::null_mut(), + first_entry: ptr::null_mut(), +}; + +#[no_mangle] +#[inline(never)] +extern "C" fn __jit_debug_register_code() { + // Hack to not allow inlining even when Rust wants to do it in release mode. + let x = 3; + unsafe { + std::ptr::read_volatile(&x); + } +} + +/// Registeration for JIT image +pub struct GdbJitImageRegistration { + entry: *mut JITCodeEntry, + file: Vec, +} + +impl GdbJitImageRegistration { + /// Registers JIT image using __jit_debug_register_code + pub fn register(file: Vec) -> Self { + Self { + entry: unsafe { register_gdb_jit_image(&file) }, + file, + } + } + + /// JIT image used in registration + pub fn file(&self) -> &[u8] { + &self.file + } +} + +impl Drop for GdbJitImageRegistration { + fn drop(&mut self) { + unsafe { + unregister_gdb_jit_image(self.entry); + } + } +} + +unsafe fn register_gdb_jit_image(file: &[u8]) -> *mut JITCodeEntry { + // Create a code entry for the file, which gives the start and size of the symbol file. + let entry = Box::into_raw(Box::new(JITCodeEntry { + next_entry: __jit_debug_descriptor.first_entry, + prev_entry: ptr::null_mut(), + symfile_addr: file.as_ptr(), + symfile_size: file.len() as u64, + })); + // Add it to the linked list in the JIT descriptor. + if !__jit_debug_descriptor.first_entry.is_null() { + (*__jit_debug_descriptor.first_entry).prev_entry = entry; + } + __jit_debug_descriptor.first_entry = entry; + // Point the relevant_entry field of the descriptor at the entry. + __jit_debug_descriptor.relevant_entry = entry; + // Set action_flag to JIT_REGISTER and call __jit_debug_register_code. + __jit_debug_descriptor.action_flag = JIT_REGISTER_FN; + __jit_debug_register_code(); + + __jit_debug_descriptor.action_flag = JIT_NOACTION; + __jit_debug_descriptor.relevant_entry = ptr::null_mut(); + entry +} + +unsafe fn unregister_gdb_jit_image(entry: *mut JITCodeEntry) { + // Remove the code entry corresponding to the code from the linked list. + if !(*entry).prev_entry.is_null() { + (*(*entry).prev_entry).next_entry = (*entry).next_entry; + } else { + __jit_debug_descriptor.first_entry = (*entry).next_entry; + } + if !(*entry).next_entry.is_null() { + (*(*entry).next_entry).prev_entry = (*entry).prev_entry; + } + // Point the relevant_entry field of the descriptor at the code entry. + __jit_debug_descriptor.relevant_entry = entry; + // Set action_flag to JIT_UNREGISTER and call __jit_debug_register_code. + __jit_debug_descriptor.action_flag = JIT_UNREGISTER_FN; + __jit_debug_register_code(); + + __jit_debug_descriptor.action_flag = JIT_NOACTION; + __jit_debug_descriptor.relevant_entry = ptr::null_mut(); + let _box = Box::from_raw(entry); +} diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs new file mode 100644 index 0000000000..736c40e2df --- /dev/null +++ b/crates/runtime/src/lib.rs @@ -0,0 +1,58 @@ +//! Runtime library support for Wasmtime. + +#![deny(missing_docs, trivial_numeric_casts, unused_extern_crates)] +#![warn(unused_import_braces)] +#![cfg_attr(feature = "clippy", plugin(clippy(conf_file = "../../clippy.toml")))] +#![cfg_attr( + feature = "cargo-clippy", + allow(clippy::new_without_default, clippy::new_without_default) +)] +#![cfg_attr( + feature = "cargo-clippy", + warn( + clippy::float_arithmetic, + clippy::mut_mut, + clippy::nonminimal_bool, + clippy::option_map_unwrap_or, + clippy::option_map_unwrap_or_else, + clippy::print_stdout, + clippy::unicode_not_nfc, + clippy::use_self + ) +)] + +mod export; +mod imports; +mod instance; +mod jit_int; +mod memory; +mod mmap; +mod sig_registry; +mod signalhandlers; +mod table; +mod trap_registry; +mod traphandlers; +mod vmcontext; + +pub mod libcalls; + +pub use crate::export::Export; +pub use crate::imports::Imports; +pub use crate::instance::{InstanceHandle, InstantiationError, LinkError}; +pub use crate::jit_int::GdbJitImageRegistration; +pub use crate::mmap::Mmap; +pub use crate::sig_registry::SignatureRegistry; +pub use crate::table::Table; +pub use crate::trap_registry::{TrapDescription, TrapRegistration, TrapRegistry}; +pub use crate::traphandlers::resume_panic; +pub use crate::traphandlers::{ + catch_traps, raise_lib_trap, raise_user_trap, wasmtime_call_trampoline, Trap, +}; +pub use crate::vmcontext::{ + VMCallerCheckedAnyfunc, VMContext, VMFunctionBody, VMFunctionImport, VMGlobalDefinition, + VMGlobalImport, VMInvokeArgument, VMMemoryDefinition, VMMemoryImport, VMSharedSignatureIndex, + VMTableDefinition, VMTableImport, +}; + +/// Version number of this crate. +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/crates/runtime/src/libcalls.rs b/crates/runtime/src/libcalls.rs new file mode 100644 index 0000000000..62840072e7 --- /dev/null +++ b/crates/runtime/src/libcalls.rs @@ -0,0 +1,301 @@ +//! Runtime library calls. +//! +//! Note that Wasm compilers may sometimes perform these inline rather than +//! calling them, particularly when CPUs have special instructions which compute +//! them directly. +//! +//! These functions are called by compiled Wasm code, and therefore must take +//! certain care about some things: +//! +//! * They must always be `pub extern "C"` and should only contain basic, raw +//! i32/i64/f32/f64/pointer parameters that are safe to pass across the system +//! ABI! +//! +//! * If any nested function propagates an `Err(trap)` out to the library +//! function frame, we need to raise it. This involves some nasty and quite +//! unsafe code under the covers! Notable, after raising the trap, drops +//! **will not** be run for local variables! This can lead to things like +//! leaking `InstanceHandle`s which leads to never deallocating JIT code, +//! instances, and modules! Therefore, always use nested blocks to ensure +//! drops run before raising a trap: +//! +//! ```ignore +//! pub extern "C" fn my_lib_function(...) { +//! let result = { +//! // Do everything in here so drops run at the end of the block. +//! ... +//! }; +//! if let Err(trap) = result { +//! // Now we can safely raise the trap without leaking! +//! raise_lib_trap(trap); +//! } +//! } +//! ``` + +use crate::table::Table; +use crate::traphandlers::raise_lib_trap; +use crate::vmcontext::VMContext; +use wasmtime_environ::ir; +use wasmtime_environ::wasm::{DefinedMemoryIndex, MemoryIndex, PassiveElemIndex, TableIndex}; + +/// Implementation of f32.ceil +pub extern "C" fn wasmtime_f32_ceil(x: f32) -> f32 { + x.ceil() +} + +/// Implementation of f32.floor +pub extern "C" fn wasmtime_f32_floor(x: f32) -> f32 { + x.floor() +} + +/// Implementation of f32.trunc +pub extern "C" fn wasmtime_f32_trunc(x: f32) -> f32 { + x.trunc() +} + +/// Implementation of f32.nearest +#[allow(clippy::float_arithmetic, clippy::float_cmp)] +pub extern "C" fn wasmtime_f32_nearest(x: f32) -> f32 { + // Rust doesn't have a nearest function, so do it manually. + if x == 0.0 { + // Preserve the sign of zero. + x + } else { + // Nearest is either ceil or floor depending on which is nearest or even. + let u = x.ceil(); + let d = x.floor(); + let um = (x - u).abs(); + let dm = (x - d).abs(); + if um < dm + || (um == dm && { + let h = u / 2.; + h.floor() == h + }) + { + u + } else { + d + } + } +} + +/// Implementation of f64.ceil +pub extern "C" fn wasmtime_f64_ceil(x: f64) -> f64 { + x.ceil() +} + +/// Implementation of f64.floor +pub extern "C" fn wasmtime_f64_floor(x: f64) -> f64 { + x.floor() +} + +/// Implementation of f64.trunc +pub extern "C" fn wasmtime_f64_trunc(x: f64) -> f64 { + x.trunc() +} + +/// Implementation of f64.nearest +#[allow(clippy::float_arithmetic, clippy::float_cmp)] +pub extern "C" fn wasmtime_f64_nearest(x: f64) -> f64 { + // Rust doesn't have a nearest function, so do it manually. + if x == 0.0 { + // Preserve the sign of zero. + x + } else { + // Nearest is either ceil or floor depending on which is nearest or even. + let u = x.ceil(); + let d = x.floor(); + let um = (x - u).abs(); + let dm = (x - d).abs(); + if um < dm + || (um == dm && { + let h = u / 2.; + h.floor() == h + }) + { + u + } else { + d + } + } +} + +/// Implementation of memory.grow for locally-defined 32-bit memories. +pub unsafe extern "C" fn wasmtime_memory32_grow( + vmctx: *mut VMContext, + delta: u32, + memory_index: u32, +) -> u32 { + let instance = (&mut *vmctx).instance(); + let memory_index = DefinedMemoryIndex::from_u32(memory_index); + + instance + .memory_grow(memory_index, delta) + .unwrap_or(u32::max_value()) +} + +/// Implementation of memory.grow for imported 32-bit memories. +pub unsafe extern "C" fn wasmtime_imported_memory32_grow( + vmctx: *mut VMContext, + delta: u32, + memory_index: u32, +) -> u32 { + let instance = (&mut *vmctx).instance(); + let memory_index = MemoryIndex::from_u32(memory_index); + + instance + .imported_memory_grow(memory_index, delta) + .unwrap_or(u32::max_value()) +} + +/// Implementation of memory.size for locally-defined 32-bit memories. +pub unsafe extern "C" fn wasmtime_memory32_size(vmctx: *mut VMContext, memory_index: u32) -> u32 { + let instance = (&mut *vmctx).instance(); + let memory_index = DefinedMemoryIndex::from_u32(memory_index); + + instance.memory_size(memory_index) +} + +/// Implementation of memory.size for imported 32-bit memories. +pub unsafe extern "C" fn wasmtime_imported_memory32_size( + vmctx: *mut VMContext, + memory_index: u32, +) -> u32 { + let instance = (&mut *vmctx).instance(); + let memory_index = MemoryIndex::from_u32(memory_index); + + instance.imported_memory_size(memory_index) +} + +/// Implementation of `table.copy`. +pub unsafe extern "C" fn wasmtime_table_copy( + vmctx: *mut VMContext, + dst_table_index: u32, + src_table_index: u32, + dst: u32, + src: u32, + len: u32, + source_loc: u32, +) { + let result = { + let dst_table_index = TableIndex::from_u32(dst_table_index); + let src_table_index = TableIndex::from_u32(src_table_index); + let source_loc = ir::SourceLoc::new(source_loc); + let instance = (&mut *vmctx).instance(); + let dst_table = instance.get_table(dst_table_index); + let src_table = instance.get_table(src_table_index); + Table::copy(dst_table, src_table, dst, src, len, source_loc) + }; + if let Err(trap) = result { + raise_lib_trap(trap); + } +} + +/// Implementation of `table.init`. +pub unsafe extern "C" fn wasmtime_table_init( + vmctx: *mut VMContext, + table_index: u32, + elem_index: u32, + dst: u32, + src: u32, + len: u32, + source_loc: u32, +) { + let result = { + let table_index = TableIndex::from_u32(table_index); + let source_loc = ir::SourceLoc::new(source_loc); + let elem_index = PassiveElemIndex::from_u32(elem_index); + let instance = (&mut *vmctx).instance(); + instance.table_init(table_index, elem_index, dst, src, len, source_loc) + }; + if let Err(trap) = result { + raise_lib_trap(trap); + } +} + +/// Implementation of `elem.drop`. +pub unsafe extern "C" fn wasmtime_elem_drop(vmctx: *mut VMContext, elem_index: u32) { + let elem_index = PassiveElemIndex::from_u32(elem_index); + let instance = (&mut *vmctx).instance(); + instance.elem_drop(elem_index); +} + +/// Implementation of `memory.copy` for locally defined memories. +pub unsafe extern "C" fn wasmtime_defined_memory_copy( + vmctx: *mut VMContext, + memory_index: u32, + dst: u32, + src: u32, + len: u32, + source_loc: u32, +) { + let result = { + let memory_index = DefinedMemoryIndex::from_u32(memory_index); + let source_loc = ir::SourceLoc::new(source_loc); + let instance = (&mut *vmctx).instance(); + instance.defined_memory_copy(memory_index, dst, src, len, source_loc) + }; + if let Err(trap) = result { + raise_lib_trap(trap); + } +} + +/// Implementation of `memory.copy` for imported memories. +pub unsafe extern "C" fn wasmtime_imported_memory_copy( + vmctx: *mut VMContext, + memory_index: u32, + dst: u32, + src: u32, + len: u32, + source_loc: u32, +) { + let result = { + let memory_index = MemoryIndex::from_u32(memory_index); + let source_loc = ir::SourceLoc::new(source_loc); + let instance = (&mut *vmctx).instance(); + instance.imported_memory_copy(memory_index, dst, src, len, source_loc) + }; + if let Err(trap) = result { + raise_lib_trap(trap); + } +} + +/// Implementation of `memory.fill` for locally defined memories. +pub unsafe extern "C" fn wasmtime_memory_fill( + vmctx: *mut VMContext, + memory_index: u32, + dst: u32, + val: u32, + len: u32, + source_loc: u32, +) { + let result = { + let memory_index = DefinedMemoryIndex::from_u32(memory_index); + let source_loc = ir::SourceLoc::new(source_loc); + let instance = (&mut *vmctx).instance(); + instance.defined_memory_fill(memory_index, dst, val, len, source_loc) + }; + if let Err(trap) = result { + raise_lib_trap(trap); + } +} + +/// Implementation of `memory.fill` for imported memories. +pub unsafe extern "C" fn wasmtime_imported_memory_fill( + vmctx: *mut VMContext, + memory_index: u32, + dst: u32, + val: u32, + len: u32, + source_loc: u32, +) { + let result = { + let memory_index = MemoryIndex::from_u32(memory_index); + let source_loc = ir::SourceLoc::new(source_loc); + let instance = (&mut *vmctx).instance(); + instance.imported_memory_fill(memory_index, dst, val, len, source_loc) + }; + if let Err(trap) = result { + raise_lib_trap(trap); + } +} diff --git a/crates/runtime/src/memory.rs b/crates/runtime/src/memory.rs new file mode 100644 index 0000000000..747c9c969f --- /dev/null +++ b/crates/runtime/src/memory.rs @@ -0,0 +1,153 @@ +//! Memory management for linear memories. +//! +//! `LinearMemory` is to WebAssembly linear memories what `Table` is to WebAssembly tables. + +use crate::mmap::Mmap; +use crate::vmcontext::VMMemoryDefinition; +use more_asserts::{assert_ge, assert_le}; +use std::cell::RefCell; +use std::convert::TryFrom; +use wasmtime_environ::{MemoryPlan, MemoryStyle, WASM_MAX_PAGES, WASM_PAGE_SIZE}; + +/// A linear memory instance. +#[derive(Debug)] +pub struct LinearMemory { + // The underlying allocation. + mmap: RefCell, + + // The optional maximum size in wasm pages of this linear memory. + maximum: Option, + + // Size in bytes of extra guard pages after the end to optimize loads and stores with + // constant offsets. + offset_guard_size: usize, + + // Records whether we're using a bounds-checking strategy which requires + // handlers to catch trapping accesses. + pub(crate) needs_signal_handlers: bool, +} + +#[derive(Debug)] +struct WasmMmap { + // Our OS allocation of mmap'd memory. + alloc: Mmap, + // The current logical size in wasm pages of this linear memory. + size: u32, +} + +impl LinearMemory { + /// Create a new linear memory instance with specified minimum and maximum number of wasm pages. + pub fn new(plan: &MemoryPlan) -> Result { + // `maximum` cannot be set to more than `65536` pages. + assert_le!(plan.memory.minimum, WASM_MAX_PAGES); + assert!(plan.memory.maximum.is_none() || plan.memory.maximum.unwrap() <= WASM_MAX_PAGES); + + let offset_guard_bytes = plan.offset_guard_size as usize; + + // If we have an offset guard, or if we're doing the static memory + // allocation strategy, we need signal handlers to catch out of bounds + // acceses. + let needs_signal_handlers = offset_guard_bytes > 0 + || match plan.style { + MemoryStyle::Dynamic => false, + MemoryStyle::Static { .. } => true, + }; + + let minimum_pages = match plan.style { + MemoryStyle::Dynamic => plan.memory.minimum, + MemoryStyle::Static { bound } => { + assert_ge!(bound, plan.memory.minimum); + bound + } + } as usize; + let minimum_bytes = minimum_pages.checked_mul(WASM_PAGE_SIZE as usize).unwrap(); + let request_bytes = minimum_bytes.checked_add(offset_guard_bytes).unwrap(); + let mapped_pages = plan.memory.minimum as usize; + let mapped_bytes = mapped_pages * WASM_PAGE_SIZE as usize; + + let mmap = WasmMmap { + alloc: Mmap::accessible_reserved(mapped_bytes, request_bytes)?, + size: plan.memory.minimum, + }; + + Ok(Self { + mmap: mmap.into(), + maximum: plan.memory.maximum, + offset_guard_size: offset_guard_bytes, + needs_signal_handlers, + }) + } + + /// Returns the number of allocated wasm pages. + pub fn size(&self) -> u32 { + self.mmap.borrow().size + } + + /// Grow memory by the specified amount of wasm pages. + /// + /// Returns `None` if memory can't be grown by the specified amount + /// of wasm pages. + pub fn grow(&self, delta: u32) -> Option { + // Optimization of memory.grow 0 calls. + let mut mmap = self.mmap.borrow_mut(); + if delta == 0 { + return Some(mmap.size); + } + + let new_pages = match mmap.size.checked_add(delta) { + Some(new_pages) => new_pages, + // Linear memory size overflow. + None => return None, + }; + let prev_pages = mmap.size; + + if let Some(maximum) = self.maximum { + if new_pages > maximum { + // Linear memory size would exceed the declared maximum. + return None; + } + } + + // Wasm linear memories are never allowed to grow beyond what is + // indexable. If the memory has no maximum, enforce the greatest + // limit here. + if new_pages >= WASM_MAX_PAGES { + // Linear memory size would exceed the index range. + return None; + } + + let delta_bytes = usize::try_from(delta).unwrap() * WASM_PAGE_SIZE as usize; + let prev_bytes = usize::try_from(prev_pages).unwrap() * WASM_PAGE_SIZE as usize; + let new_bytes = usize::try_from(new_pages).unwrap() * WASM_PAGE_SIZE as usize; + + if new_bytes > mmap.alloc.len() - self.offset_guard_size { + // If the new size is within the declared maximum, but needs more memory than we + // have on hand, it's a dynamic heap and it can move. + let guard_bytes = self.offset_guard_size; + let request_bytes = new_bytes.checked_add(guard_bytes)?; + + let mut new_mmap = Mmap::accessible_reserved(new_bytes, request_bytes).ok()?; + + let copy_len = mmap.alloc.len() - self.offset_guard_size; + new_mmap.as_mut_slice()[..copy_len].copy_from_slice(&mmap.alloc.as_slice()[..copy_len]); + + mmap.alloc = new_mmap; + } else if delta_bytes > 0 { + // Make the newly allocated pages accessible. + mmap.alloc.make_accessible(prev_bytes, delta_bytes).ok()?; + } + + mmap.size = new_pages; + + Some(prev_pages) + } + + /// Return a `VMMemoryDefinition` for exposing the memory to compiled wasm code. + pub fn vmmemory(&self) -> VMMemoryDefinition { + let mut mmap = self.mmap.borrow_mut(); + VMMemoryDefinition { + base: mmap.alloc.as_mut_ptr(), + current_length: mmap.size as usize * WASM_PAGE_SIZE as usize, + } + } +} diff --git a/crates/runtime/src/mmap.rs b/crates/runtime/src/mmap.rs new file mode 100644 index 0000000000..8cba107404 --- /dev/null +++ b/crates/runtime/src/mmap.rs @@ -0,0 +1,289 @@ +//! Low-level abstraction for allocating and managing zero-filled pages +//! of memory. + +use more_asserts::assert_le; +use more_asserts::assert_lt; +use std::io; +use std::ptr; +use std::slice; + +/// Round `size` up to the nearest multiple of `page_size`. +fn round_up_to_page_size(size: usize, page_size: usize) -> usize { + (size + (page_size - 1)) & !(page_size - 1) +} + +/// A simple struct consisting of a page-aligned pointer to page-aligned +/// and initially-zeroed memory and a length. +#[derive(Debug)] +pub struct Mmap { + // Note that this is stored as a `usize` instead of a `*const` or `*mut` + // pointer to allow this structure to be natively `Send` and `Sync` without + // `unsafe impl`. This type is sendable across threads and shareable since + // the coordination all happens at the OS layer. + ptr: usize, + len: usize, +} + +impl Mmap { + /// Construct a new empty instance of `Mmap`. + pub fn new() -> Self { + // Rust's slices require non-null pointers, even when empty. `Vec` + // contains code to create a non-null dangling pointer value when + // constructed empty, so we reuse that here. + let empty = Vec::::new(); + Self { + ptr: empty.as_ptr() as usize, + len: 0, + } + } + + /// Create a new `Mmap` pointing to at least `size` bytes of page-aligned accessible memory. + pub fn with_at_least(size: usize) -> Result { + let page_size = region::page::size(); + let rounded_size = round_up_to_page_size(size, page_size); + Self::accessible_reserved(rounded_size, rounded_size) + } + + /// Create a new `Mmap` pointing to `accessible_size` bytes of page-aligned accessible memory, + /// within a reserved mapping of `mapping_size` bytes. `accessible_size` and `mapping_size` + /// must be native page-size multiples. + #[cfg(not(target_os = "windows"))] + pub fn accessible_reserved( + accessible_size: usize, + mapping_size: usize, + ) -> Result { + let page_size = region::page::size(); + assert_le!(accessible_size, mapping_size); + assert_eq!(mapping_size & (page_size - 1), 0); + assert_eq!(accessible_size & (page_size - 1), 0); + + // Mmap may return EINVAL if the size is zero, so just + // special-case that. + if mapping_size == 0 { + return Ok(Self::new()); + } + + Ok(if accessible_size == mapping_size { + // Allocate a single read-write region at once. + let ptr = unsafe { + libc::mmap( + ptr::null_mut(), + mapping_size, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_PRIVATE | libc::MAP_ANON, + -1, + 0, + ) + }; + if ptr as isize == -1_isize { + return Err(io::Error::last_os_error().to_string()); + } + + Self { + ptr: ptr as usize, + len: mapping_size, + } + } else { + // Reserve the mapping size. + let ptr = unsafe { + libc::mmap( + ptr::null_mut(), + mapping_size, + libc::PROT_NONE, + libc::MAP_PRIVATE | libc::MAP_ANON, + -1, + 0, + ) + }; + if ptr as isize == -1_isize { + return Err(io::Error::last_os_error().to_string()); + } + + let mut result = Self { + ptr: ptr as usize, + len: mapping_size, + }; + + if accessible_size != 0 { + // Commit the accessible size. + result.make_accessible(0, accessible_size)?; + } + + result + }) + } + + /// Create a new `Mmap` pointing to `accessible_size` bytes of page-aligned accessible memory, + /// within a reserved mapping of `mapping_size` bytes. `accessible_size` and `mapping_size` + /// must be native page-size multiples. + #[cfg(target_os = "windows")] + pub fn accessible_reserved( + accessible_size: usize, + mapping_size: usize, + ) -> Result { + use winapi::um::memoryapi::VirtualAlloc; + use winapi::um::winnt::{MEM_COMMIT, MEM_RESERVE, PAGE_NOACCESS, PAGE_READWRITE}; + + let page_size = region::page::size(); + assert_le!(accessible_size, mapping_size); + assert_eq!(mapping_size & (page_size - 1), 0); + assert_eq!(accessible_size & (page_size - 1), 0); + + Ok(if accessible_size == mapping_size { + // Allocate a single read-write region at once. + let ptr = unsafe { + VirtualAlloc( + ptr::null_mut(), + mapping_size, + MEM_RESERVE | MEM_COMMIT, + PAGE_READWRITE, + ) + }; + if ptr.is_null() { + return Err(io::Error::last_os_error().to_string()); + } + + Self { + ptr: ptr as usize, + len: mapping_size, + } + } else { + // Reserve the mapping size. + let ptr = + unsafe { VirtualAlloc(ptr::null_mut(), mapping_size, MEM_RESERVE, PAGE_NOACCESS) }; + if ptr.is_null() { + return Err(io::Error::last_os_error().to_string()); + } + + let mut result = Self { + ptr: ptr as usize, + len: mapping_size, + }; + + if accessible_size != 0 { + // Commit the accessible size. + result.make_accessible(0, accessible_size)?; + } + + result + }) + } + + /// Make the memory starting at `start` and extending for `len` bytes accessible. + /// `start` and `len` must be native page-size multiples and describe a range within + /// `self`'s reserved memory. + #[cfg(not(target_os = "windows"))] + pub fn make_accessible(&mut self, start: usize, len: usize) -> Result<(), String> { + let page_size = region::page::size(); + assert_eq!(start & (page_size - 1), 0); + assert_eq!(len & (page_size - 1), 0); + assert_lt!(len, self.len); + assert_lt!(start, self.len - len); + + // Commit the accessible size. + let ptr = self.ptr as *const u8; + unsafe { region::protect(ptr.add(start), len, region::Protection::ReadWrite) } + .map_err(|e| e.to_string()) + } + + /// Make the memory starting at `start` and extending for `len` bytes accessible. + /// `start` and `len` must be native page-size multiples and describe a range within + /// `self`'s reserved memory. + #[cfg(target_os = "windows")] + pub fn make_accessible(&mut self, start: usize, len: usize) -> Result<(), String> { + use winapi::ctypes::c_void; + use winapi::um::memoryapi::VirtualAlloc; + use winapi::um::winnt::{MEM_COMMIT, PAGE_READWRITE}; + let page_size = region::page::size(); + assert_eq!(start & (page_size - 1), 0); + assert_eq!(len & (page_size - 1), 0); + assert_lt!(len, self.len); + assert_lt!(start, self.len - len); + + // Commit the accessible size. + let ptr = self.ptr as *const u8; + if unsafe { + VirtualAlloc( + ptr.add(start) as *mut c_void, + len, + MEM_COMMIT, + PAGE_READWRITE, + ) + } + .is_null() + { + return Err(io::Error::last_os_error().to_string()); + } + + Ok(()) + } + + /// Return the allocated memory as a slice of u8. + pub fn as_slice(&self) -> &[u8] { + unsafe { slice::from_raw_parts(self.ptr as *const u8, self.len) } + } + + /// Return the allocated memory as a mutable slice of u8. + pub fn as_mut_slice(&mut self) -> &mut [u8] { + unsafe { slice::from_raw_parts_mut(self.ptr as *mut u8, self.len) } + } + + /// Return the allocated memory as a pointer to u8. + pub fn as_ptr(&self) -> *const u8 { + self.ptr as *const u8 + } + + /// Return the allocated memory as a mutable pointer to u8. + pub fn as_mut_ptr(&mut self) -> *mut u8 { + self.ptr as *mut u8 + } + + /// Return the length of the allocated memory. + pub fn len(&self) -> usize { + self.len + } + + /// Return whether any memory has been allocated. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +impl Drop for Mmap { + #[cfg(not(target_os = "windows"))] + fn drop(&mut self) { + if self.len != 0 { + let r = unsafe { libc::munmap(self.ptr as *mut libc::c_void, self.len) }; + assert_eq!(r, 0, "munmap failed: {}", io::Error::last_os_error()); + } + } + + #[cfg(target_os = "windows")] + fn drop(&mut self) { + if self.len != 0 { + use winapi::ctypes::c_void; + use winapi::um::memoryapi::VirtualFree; + use winapi::um::winnt::MEM_RELEASE; + let r = unsafe { VirtualFree(self.ptr as *mut c_void, 0, MEM_RELEASE) }; + assert_ne!(r, 0); + } + } +} + +fn _assert() { + fn _assert_send_sync() {} + _assert_send_sync::(); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_round_up_to_page_size() { + assert_eq!(round_up_to_page_size(0, 4096), 0); + assert_eq!(round_up_to_page_size(1, 4096), 4096); + assert_eq!(round_up_to_page_size(4096, 4096), 4096); + assert_eq!(round_up_to_page_size(4097, 4096), 8192); + } +} diff --git a/crates/runtime/src/sig_registry.rs b/crates/runtime/src/sig_registry.rs new file mode 100644 index 0000000000..7ad66149ac --- /dev/null +++ b/crates/runtime/src/sig_registry.rs @@ -0,0 +1,73 @@ +//! Implement a registry of function signatures, for fast indirect call +//! signature checking. + +use crate::vmcontext::VMSharedSignatureIndex; +use more_asserts::{assert_lt, debug_assert_lt}; +use std::collections::{hash_map, HashMap}; +use std::convert::TryFrom; +use std::sync::RwLock; +use wasmtime_environ::ir; + +/// WebAssembly requires that the caller and callee signatures in an indirect +/// call must match. To implement this efficiently, keep a registry of all +/// signatures, shared by all instances, so that call sites can just do an +/// index comparison. +#[derive(Debug)] +pub struct SignatureRegistry { + // This structure is stored in a `Compiler` and is intended to be shared + // across many instances. Ideally instances can themselves be sent across + // threads, and ideally we can compile across many threads. As a result we + // use interior mutability here with a lock to avoid having callers to + // externally synchronize calls to compilation. + inner: RwLock, +} + +#[derive(Debug, Default)] +struct Inner { + signature2index: HashMap, + index2signature: HashMap, +} + +impl SignatureRegistry { + /// Create a new `SignatureRegistry`. + pub fn new() -> Self { + Self { + inner: Default::default(), + } + } + + /// Register a signature and return its unique index. + pub fn register(&self, sig: &ir::Signature) -> VMSharedSignatureIndex { + let mut inner = self.inner.write().unwrap(); + let len = inner.signature2index.len(); + match inner.signature2index.entry(sig.clone()) { + hash_map::Entry::Occupied(entry) => *entry.get(), + hash_map::Entry::Vacant(entry) => { + // Keep `signature_hash` len under 2**32 -- VMSharedSignatureIndex::new(std::u32::MAX) + // is reserved for VMSharedSignatureIndex::default(). + debug_assert_lt!( + len, + std::u32::MAX as usize, + "Invariant check: signature_hash.len() < std::u32::MAX" + ); + let sig_id = VMSharedSignatureIndex::new(u32::try_from(len).unwrap()); + entry.insert(sig_id); + inner.index2signature.insert(sig_id, sig.clone()); + sig_id + } + } + } + + /// Looks up a shared signature index within this registry. + /// + /// Note that for this operation to be semantically correct the `idx` must + /// have previously come from a call to `register` of this same object. + pub fn lookup(&self, idx: VMSharedSignatureIndex) -> Option { + self.inner + .read() + .unwrap() + .index2signature + .get(&idx) + .cloned() + } +} diff --git a/crates/runtime/src/signalhandlers.rs b/crates/runtime/src/signalhandlers.rs new file mode 100644 index 0000000000..9813d3ca6c --- /dev/null +++ b/crates/runtime/src/signalhandlers.rs @@ -0,0 +1,38 @@ +//! Interface to low-level signal-handling mechanisms. + +use std::sync::Once; + +extern "C" { + fn EnsureEagerSignalHandlers() -> libc::c_int; +} + +/// This function performs the low-overhead signal handler initialization that +/// we want to do eagerly to ensure a more-deterministic global process state. +/// +/// This is especially relevant for signal handlers since handler ordering +/// depends on installation order: the wasm signal handler must run *before* +/// the other crash handlers and since POSIX signal handlers work LIFO, this +/// function needs to be called at the end of the startup process, after other +/// handlers have been installed. This function can thus be called multiple +/// times, having no effect after the first call. +pub fn init() { + static INIT: Once = Once::new(); + INIT.call_once(real_init); +} + +fn real_init() { + // This is a really weird and unfortunate function call. For all the gory + // details see #829, but the tl;dr; is that in a trap handler we have 2 + // pages of stack space on Linux, and calling into libunwind which triggers + // the dynamic loader blows the stack. + // + // This is a dumb hack to work around this system-specific issue by + // capturing a backtrace once in the lifetime of a process to ensure that + // when we capture a backtrace in the trap handler all caches are primed, + // aka the dynamic loader has resolved all the relevant symbols. + drop(backtrace::Backtrace::new_unresolved()); + + if unsafe { EnsureEagerSignalHandlers() == 0 } { + panic!("failed to install signal handlers"); + } +} diff --git a/crates/runtime/src/table.rs b/crates/runtime/src/table.rs new file mode 100644 index 0000000000..e938b5dd29 --- /dev/null +++ b/crates/runtime/src/table.rs @@ -0,0 +1,145 @@ +//! Memory management for tables. +//! +//! `Table` is to WebAssembly tables what `LinearMemory` is to WebAssembly linear memories. + +use crate::vmcontext::{VMCallerCheckedAnyfunc, VMTableDefinition}; +use crate::Trap; +use std::cell::RefCell; +use std::convert::{TryFrom, TryInto}; +use wasmtime_environ::wasm::TableElementType; +use wasmtime_environ::{ir, TablePlan, TableStyle}; + +/// A table instance. +#[derive(Debug)] +pub struct Table { + vec: RefCell>, + maximum: Option, +} + +impl Table { + /// Create a new table instance with specified minimum and maximum number of elements. + pub fn new(plan: &TablePlan) -> Self { + match plan.table.ty { + TableElementType::Func => (), + TableElementType::Val(ty) => { + unimplemented!("tables of types other than anyfunc ({})", ty) + } + }; + match plan.style { + TableStyle::CallerChecksSignature => Self { + vec: RefCell::new(vec![ + VMCallerCheckedAnyfunc::default(); + usize::try_from(plan.table.minimum).unwrap() + ]), + maximum: plan.table.maximum, + }, + } + } + + /// Returns the number of allocated elements. + pub fn size(&self) -> u32 { + self.vec.borrow().len().try_into().unwrap() + } + + /// Grow table by the specified amount of elements. + /// + /// Returns `None` if table can't be grown by the specified amount + /// of elements. + pub fn grow(&self, delta: u32) -> Option { + let new_len = match self.size().checked_add(delta) { + Some(len) => { + if let Some(max) = self.maximum { + if len > max { + return None; + } + } + len + } + None => { + return None; + } + }; + self.vec.borrow_mut().resize( + usize::try_from(new_len).unwrap(), + VMCallerCheckedAnyfunc::default(), + ); + Some(new_len) + } + + /// Get reference to the specified element. + /// + /// Returns `None` if the index is out of bounds. + pub fn get(&self, index: u32) -> Option { + self.vec.borrow().get(index as usize).cloned() + } + + /// Set reference to the specified element. + /// + /// # Panics + /// + /// Panics if `index` is out of bounds. + pub fn set(&self, index: u32, func: VMCallerCheckedAnyfunc) -> Result<(), ()> { + match self.vec.borrow_mut().get_mut(index as usize) { + Some(slot) => { + *slot = func; + Ok(()) + } + None => Err(()), + } + } + + /// Copy `len` elements from `src_table[src_index..]` into `dst_table[dst_index..]`. + /// + /// # Errors + /// + /// Returns an error if the range is out of bounds of either the source or + /// destination tables. + pub fn copy( + dst_table: &Self, + src_table: &Self, + dst_index: u32, + src_index: u32, + len: u32, + source_loc: ir::SourceLoc, + ) -> Result<(), Trap> { + // https://webassembly.github.io/bulk-memory-operations/core/exec/instructions.html#exec-table-copy + + if src_index + .checked_add(len) + .map_or(true, |n| n > src_table.size()) + || dst_index + .checked_add(len) + .map_or(true, |m| m > dst_table.size()) + { + return Err(Trap::wasm(source_loc, ir::TrapCode::TableOutOfBounds)); + } + + let srcs = src_index..src_index + len; + let dsts = dst_index..dst_index + len; + + // Note on the unwraps: the bounds check above means that these will + // never panic. + // + // TODO(#983): investigate replacing this get/set loop with a `memcpy`. + if dst_index <= src_index { + for (s, d) in (srcs).zip(dsts) { + dst_table.set(d, src_table.get(s).unwrap()).unwrap(); + } + } else { + for (s, d) in srcs.rev().zip(dsts.rev()) { + dst_table.set(d, src_table.get(s).unwrap()).unwrap(); + } + } + + Ok(()) + } + + /// Return a `VMTableDefinition` for exposing the table to compiled wasm code. + pub fn vmtable(&self) -> VMTableDefinition { + let mut vec = self.vec.borrow_mut(); + VMTableDefinition { + base: vec.as_mut_ptr() as *mut u8, + current_elements: vec.len().try_into().unwrap(), + } + } +} diff --git a/crates/runtime/src/trap_registry.rs b/crates/runtime/src/trap_registry.rs new file mode 100644 index 0000000000..f8f4c8b56d --- /dev/null +++ b/crates/runtime/src/trap_registry.rs @@ -0,0 +1,173 @@ +use std::collections::{BTreeMap, HashMap}; +use std::fmt; +use std::sync::{Arc, RwLock}; +use wasmtime_environ::ir; + +/// The registry maintains descriptions of traps in currently allocated functions. +#[derive(Default)] +pub struct TrapRegistry { + // This data structure is intended to be safe to use across many threads + // since this is stored inside of a `Compiler` which, eventually, will be + // used across many threads. To that end this is internally use an `Arc` + // plus an `RwLock`. + // + // The problem that this data structure is solving is that when a + // segfault/illegal instruction happens we need to answer "given this + // hardware program counter what is the wasm reason this trap is being + // raised"? + // + // The way this is answered here is done to minimize the amount of + // synchronization (in theory) and have something like so: + // + // * Each module bulk-registers a list of in-memory pc addresses that have + // traps. We assume that the range of traps for each module are always + // disjoint. + // * Each key in this `BTreeMap` is the highest trapping address and the + // value contains the lowest address as well as all the individual + // addresses in their own `HashMap`. + // * Registration then looks by calculating the start/end and inserting + // into this map (with some assertions about disjointed-ness) + // * Lookup is done in two layers. First we find the corresponding entry + // in the map and verify that a program counter falls in the start/end + // range. Next we look up the address in the `traps` hash map below. + // + // The `register_traps` function works by returning an RAII guard that owns + // a handle to this `Arc` as well, and when that type is dropped it will + // automatically remove all trap information from this `ranges` list. + ranges: Arc>>, +} + +#[derive(Debug)] +struct TrapGroup { + /// The lowest key in the `trap` field. + /// + /// This represents the start of the range of this group of traps, and the + /// end of the range for this group of traps is stored as the key in the + /// `ranges` struct above in `TrapRegistry`. + start: usize, + + /// All known traps in this group, mapped from program counter to the + /// description of the trap itself. + traps: HashMap, +} + +/// RAII structure returned from `TrapRegistry::register_trap` to unregister +/// trap information on drop. +#[derive(Clone)] +pub struct TrapRegistration { + ranges: Arc>>, + end: Option, +} + +/// Description of a trap. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct TrapDescription { + /// Location of the trap in source binary module. + pub source_loc: ir::SourceLoc, + /// Code of the trap. + pub trap_code: ir::TrapCode, +} + +impl fmt::Display for TrapDescription { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "wasm trap: {}, source location: {}", + trap_code_to_expected_string(self.trap_code), + self.source_loc + ) + } +} + +fn trap_code_to_expected_string(trap_code: ir::TrapCode) -> String { + use ir::TrapCode::*; + match trap_code { + StackOverflow => "call stack exhausted".to_string(), + HeapOutOfBounds => "out of bounds memory access".to_string(), + TableOutOfBounds => "undefined element: out of bounds".to_string(), + OutOfBounds => "out of bounds".to_string(), // Note: not covered by the test suite + IndirectCallToNull => "uninitialized element".to_string(), + BadSignature => "indirect call type mismatch".to_string(), + IntegerOverflow => "integer overflow".to_string(), + IntegerDivisionByZero => "integer divide by zero".to_string(), + BadConversionToInteger => "invalid conversion to integer".to_string(), + UnreachableCodeReached => "unreachable".to_string(), + Interrupt => "interrupt".to_string(), // Note: not covered by the test suite + User(x) => format!("user trap {}", x), // Note: not covered by the test suite + } +} + +impl TrapRegistry { + /// Registers a list of traps. + /// + /// Returns a RAII guard that deregisters all traps when dropped. + pub fn register_traps( + &self, + list: impl IntoIterator, + ) -> TrapRegistration { + let mut start = usize::max_value(); + let mut end = 0; + let mut traps = HashMap::new(); + for (addr, source_loc, trap_code) in list.into_iter() { + traps.insert( + addr, + TrapDescription { + source_loc, + trap_code, + }, + ); + if addr < start { + start = addr; + } + if addr > end { + end = addr; + } + } + if traps.len() == 0 { + return TrapRegistration { + ranges: self.ranges.clone(), + end: None, + }; + } + let mut ranges = self.ranges.write().unwrap(); + + // Sanity check that no other group of traps overlaps with our + // registration... + if let Some((_, prev)) = ranges.range(end..).next() { + assert!(prev.start > end); + } + if let Some((prev_end, _)) = ranges.range(..=start).next_back() { + assert!(*prev_end < start); + } + + // ... and then register ourselves + assert!(ranges.insert(end, TrapGroup { start, traps }).is_none()); + TrapRegistration { + ranges: self.ranges.clone(), + end: Some(end), + } + } +} + +impl TrapRegistration { + /// Gets a trap description at given address. + pub fn get_trap(&self, address: usize) -> Option { + let ranges = self.ranges.read().ok()?; + let (end, group) = ranges.range(address..).next()?; + if group.start <= address && address <= *end { + group.traps.get(&address).copied() + } else { + None + } + } +} + +impl Drop for TrapRegistration { + fn drop(&mut self) { + if let Some(end) = self.end { + if let Ok(mut ranges) = self.ranges.write() { + ranges.remove(&end); + } + } + } +} diff --git a/crates/runtime/src/traphandlers.rs b/crates/runtime/src/traphandlers.rs new file mode 100644 index 0000000000..7182a6506d --- /dev/null +++ b/crates/runtime/src/traphandlers.rs @@ -0,0 +1,399 @@ +//! WebAssembly trap handling, which is built on top of the lower-level +//! signalhandling mechanisms. + +use crate::instance::{InstanceHandle, SignalHandler}; +use crate::trap_registry::TrapDescription; +use crate::vmcontext::{VMContext, VMFunctionBody}; +use backtrace::Backtrace; +use std::any::Any; +use std::cell::Cell; +use std::error::Error; +use std::fmt; +use std::mem; +use std::ptr; +use wasmtime_environ::ir; + +extern "C" { + fn RegisterSetjmp( + jmp_buf: *mut *const u8, + callback: extern "C" fn(*mut u8), + payload: *mut u8, + ) -> i32; + fn Unwind(jmp_buf: *const u8) -> !; +} + +cfg_if::cfg_if! { + if #[cfg(unix)] { + #[no_mangle] + pub unsafe extern "C" fn HandleTrap( + pc: *mut u8, + signum: libc::c_int, + siginfo: *mut libc::siginfo_t, + context: *mut libc::c_void, + ) -> *const u8 { + tls::with(|info| { + match info { + Some(info) => info.handle_trap(pc, false, |handler| handler(signum, siginfo, context)), + None => ptr::null(), + } + }) + } + } else if #[cfg(target_os = "windows")] { + use winapi::um::winnt::PEXCEPTION_POINTERS; + use winapi::um::minwinbase::EXCEPTION_STACK_OVERFLOW; + + #[no_mangle] + pub unsafe extern "C" fn HandleTrap( + pc: *mut u8, + exception_info: PEXCEPTION_POINTERS + ) -> *const u8 { + tls::with(|info| { + let reset_guard_page = (*(*exception_info).ExceptionRecord).ExceptionCode == EXCEPTION_STACK_OVERFLOW; + match info { + Some(info) => info.handle_trap(pc, reset_guard_page, |handler| handler(exception_info)), + None => ptr::null(), + } + }) + } + } +} + +/// Raises a user-defined trap immediately. +/// +/// This function performs as-if a wasm trap was just executed, only the trap +/// has a dynamic payload associated with it which is user-provided. This trap +/// payload is then returned from `wasmtime_call` an `wasmtime_call_trampoline` +/// below. +/// +/// # Safety +/// +/// Only safe to call when wasm code is on the stack, aka `wasmtime_call` or +/// `wasmtime_call_trampoline` must have been previously called. +pub unsafe fn raise_user_trap(data: Box) -> ! { + tls::with(|info| info.unwrap().unwind_with(UnwindReason::UserTrap(data))) +} + +/// Raises a trap from inside library code immediately. +/// +/// This function performs as-if a wasm trap was just executed. This trap +/// payload is then returned from `wasmtime_call` and `wasmtime_call_trampoline` +/// below. +/// +/// # Safety +/// +/// Only safe to call when wasm code is on the stack, aka `wasmtime_call` or +/// `wasmtime_call_trampoline` must have been previously called. +pub unsafe fn raise_lib_trap(trap: Trap) -> ! { + tls::with(|info| info.unwrap().unwind_with(UnwindReason::LibTrap(trap))) +} + +/// Carries a Rust panic across wasm code and resumes the panic on the other +/// side. +/// +/// # Safety +/// +/// Only safe to call when wasm code is on the stack, aka `wasmtime_call` or +/// `wasmtime_call_trampoline` must have been previously called. +pub unsafe fn resume_panic(payload: Box) -> ! { + tls::with(|info| info.unwrap().unwind_with(UnwindReason::Panic(payload))) +} + +#[cfg(target_os = "windows")] +fn reset_guard_page() { + extern "C" { + fn _resetstkoflw() -> winapi::ctypes::c_int; + } + + // We need to restore guard page under stack to handle future stack overflows properly. + // https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/resetstkoflw?view=vs-2019 + if unsafe { _resetstkoflw() } == 0 { + panic!("failed to restore stack guard page"); + } +} + +#[cfg(not(target_os = "windows"))] +fn reset_guard_page() {} + +/// Stores trace message with backtrace. +#[derive(Debug)] +pub enum Trap { + /// A user-raised trap through `raise_user_trap`. + User(Box), + /// A wasm-originating trap from wasm code itself. + Wasm { + /// What sort of trap happened, as well as where in the original wasm module + /// it happened. + desc: TrapDescription, + /// Native stack backtrace at the time the trap occurred + backtrace: Backtrace, + }, +} + +impl fmt::Display for Trap { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Trap::User(user) => user.fmt(f), + Trap::Wasm { desc, .. } => desc.fmt(f), + } + } +} + +impl std::error::Error for Trap {} + +impl Trap { + /// Construct a new Wasm trap with the given source location and trap code. + /// + /// Internally saves a backtrace when constructed. + pub fn wasm(source_loc: ir::SourceLoc, trap_code: ir::TrapCode) -> Self { + let desc = TrapDescription { + source_loc, + trap_code, + }; + let backtrace = Backtrace::new(); + Trap::Wasm { desc, backtrace } + } +} + +/// Call the wasm function pointed to by `callee`. +/// +/// * `vmctx` - the callee vmctx argument +/// * `caller_vmctx` - the caller vmctx argument +/// * `trampoline` - the jit-generated trampoline whose ABI takes 4 values, the +/// callee vmctx, the caller vmctx, the `callee` argument below, and then the +/// `values_vec` argument. +/// * `callee` - the third argument to the `trampoline` function +/// * `values_vec` - points to a buffer which holds the incoming arguments, and to +/// which the outgoing return values will be written. +/// +/// Wildly unsafe because it calls raw function pointers and reads/writes raw +/// function pointers. +pub unsafe fn wasmtime_call_trampoline( + vmctx: *mut VMContext, + caller_vmctx: *mut VMContext, + trampoline: *const VMFunctionBody, + callee: *const VMFunctionBody, + values_vec: *mut u8, +) -> Result<(), Trap> { + catch_traps(vmctx, || { + mem::transmute::< + _, + extern "C" fn(*mut VMContext, *mut VMContext, *const VMFunctionBody, *mut u8), + >(trampoline)(vmctx, caller_vmctx, callee, values_vec) + }) +} + +/// Catches any wasm traps that happen within the execution of `closure`, +/// returning them as a `Result`. +/// +/// Highly unsafe since `closure` won't have any dtors run. +pub unsafe fn catch_traps(vmctx: *mut VMContext, mut closure: F) -> Result<(), Trap> +where + F: FnMut(), +{ + return CallThreadState::new(vmctx).with(|cx| { + RegisterSetjmp( + cx.jmp_buf.as_ptr(), + call_closure::, + &mut closure as *mut F as *mut u8, + ) + }); + + extern "C" fn call_closure(payload: *mut u8) + where + F: FnMut(), + { + unsafe { (*(payload as *mut F))() } + } +} + +/// Temporary state stored on the stack which is registered in the `tls` module +/// below for calls into wasm. +pub struct CallThreadState { + unwind: Cell, + jmp_buf: Cell<*const u8>, + reset_guard_page: Cell, + prev: Option<*const CallThreadState>, + vmctx: *mut VMContext, +} + +enum UnwindReason { + None, + Panic(Box), + UserTrap(Box), + LibTrap(Trap), + Trap { backtrace: Backtrace, pc: usize }, +} + +impl CallThreadState { + fn new(vmctx: *mut VMContext) -> CallThreadState { + CallThreadState { + unwind: Cell::new(UnwindReason::None), + vmctx, + jmp_buf: Cell::new(ptr::null()), + reset_guard_page: Cell::new(false), + prev: None, + } + } + + fn with(mut self, closure: impl FnOnce(&CallThreadState) -> i32) -> Result<(), Trap> { + tls::with(|prev| { + self.prev = prev.map(|p| p as *const _); + let ret = tls::set(&self, || closure(&self)); + match self.unwind.replace(UnwindReason::None) { + UnwindReason::None => { + debug_assert_eq!(ret, 1); + Ok(()) + } + UnwindReason::UserTrap(data) => { + debug_assert_eq!(ret, 0); + Err(Trap::User(data)) + } + UnwindReason::LibTrap(trap) => Err(trap), + UnwindReason::Trap { backtrace, pc } => { + debug_assert_eq!(ret, 0); + let instance = unsafe { InstanceHandle::from_vmctx(self.vmctx) }; + + Err(Trap::Wasm { + desc: instance + .instance() + .trap_registration + .get_trap(pc) + .unwrap_or_else(|| TrapDescription { + source_loc: ir::SourceLoc::default(), + trap_code: ir::TrapCode::StackOverflow, + }), + backtrace, + }) + } + UnwindReason::Panic(panic) => { + debug_assert_eq!(ret, 0); + std::panic::resume_unwind(panic) + } + } + }) + } + + fn any_instance(&self, func: impl Fn(&InstanceHandle) -> bool) -> bool { + unsafe { + if func(&InstanceHandle::from_vmctx(self.vmctx)) { + return true; + } + match self.prev { + Some(prev) => (*prev).any_instance(func), + None => false, + } + } + } + + fn unwind_with(&self, reason: UnwindReason) -> ! { + self.unwind.replace(reason); + unsafe { + Unwind(self.jmp_buf.get()); + } + } + + /// Trap handler using our thread-local state. + /// + /// * `pc` - the program counter the trap happened at + /// * `reset_guard_page` - whether or not to reset the guard page, + /// currently Windows specific + /// * `call_handler` - a closure used to invoke the platform-specific + /// signal handler for each instance, if available. + /// + /// Attempts to handle the trap if it's a wasm trap. Returns a few + /// different things: + /// + /// * null - the trap didn't look like a wasm trap and should continue as a + /// trap + /// * 1 as a pointer - the trap was handled by a custom trap handler on an + /// instance, and the trap handler should quickly return. + /// * a different pointer - a jmp_buf buffer to longjmp to, meaning that + /// the wasm trap was succesfully handled. + fn handle_trap( + &self, + pc: *const u8, + reset_guard_page: bool, + call_handler: impl Fn(&SignalHandler) -> bool, + ) -> *const u8 { + // First up see if any instance registered has a custom trap handler, + // in which case run them all. If anything handles the trap then we + // return that the trap was handled. + if self.any_instance(|i| { + let handler = match i.instance().signal_handler.replace(None) { + Some(handler) => handler, + None => return false, + }; + let result = call_handler(&handler); + i.instance().signal_handler.set(Some(handler)); + return result; + }) { + return 1 as *const _; + } + + // TODO: stack overflow can happen at any random time (i.e. in malloc() + // in memory.grow) and it's really hard to determine if the cause was + // stack overflow and if it happened in WebAssembly module. + // + // So, let's assume that any untrusted code called from WebAssembly + // doesn't trap. Then, if we have called some WebAssembly code, it + // means the trap is stack overflow. + if self.jmp_buf.get().is_null() { + return ptr::null(); + } + let backtrace = Backtrace::new_unresolved(); + self.reset_guard_page.set(reset_guard_page); + self.unwind.replace(UnwindReason::Trap { + backtrace, + pc: pc as usize, + }); + self.jmp_buf.get() + } +} + +impl Drop for CallThreadState { + fn drop(&mut self) { + if self.reset_guard_page.get() { + reset_guard_page(); + } + } +} + +// A private inner module for managing the TLS state that we require across +// calls in wasm. The WebAssembly code is called from C++ and then a trap may +// happen which requires us to read some contextual state to figure out what to +// do with the trap. This `tls` module is used to persist that information from +// the caller to the trap site. +mod tls { + use super::CallThreadState; + use std::cell::Cell; + use std::ptr; + + thread_local!(static PTR: Cell<*const CallThreadState> = Cell::new(ptr::null())); + + /// Configures thread local state such that for the duration of the + /// execution of `closure` any call to `with` will yield `ptr`, unless this + /// is recursively called again. + pub fn set(ptr: &CallThreadState, closure: impl FnOnce() -> R) -> R { + struct Reset<'a, T: Copy>(&'a Cell, T); + + impl Drop for Reset<'_, T> { + fn drop(&mut self) { + self.0.set(self.1); + } + } + + PTR.with(|p| { + let _r = Reset(p, p.replace(ptr)); + closure() + }) + } + + /// Returns the last pointer configured with `set` above. Panics if `set` + /// has not been previously called. + pub fn with(closure: impl FnOnce(Option<&CallThreadState>) -> R) -> R { + PTR.with(|ptr| { + let p = ptr.get(); + unsafe { closure(if p.is_null() { None } else { Some(&*p) }) } + }) + } +} diff --git a/crates/runtime/src/vmcontext.rs b/crates/runtime/src/vmcontext.rs new file mode 100644 index 0000000000..dfa05b3566 --- /dev/null +++ b/crates/runtime/src/vmcontext.rs @@ -0,0 +1,643 @@ +//! This file declares `VMContext` and several related structs which contain +//! fields that compiled wasm code accesses directly. + +use crate::instance::Instance; +use std::any::Any; +use std::{ptr, u32}; +use wasmtime_environ::BuiltinFunctionIndex; + +/// An imported function. +#[derive(Debug, Copy, Clone)] +#[repr(C)] +pub struct VMFunctionImport { + /// A pointer to the imported function body. + pub body: *const VMFunctionBody, + + /// A pointer to the `VMContext` that owns the function. + pub vmctx: *mut VMContext, +} + +#[cfg(test)] +mod test_vmfunction_import { + use super::VMFunctionImport; + use memoffset::offset_of; + use std::mem::size_of; + use wasmtime_environ::{Module, VMOffsets}; + + #[test] + fn check_vmfunction_import_offsets() { + let module = Module::new(); + let offsets = VMOffsets::new(size_of::<*mut u8>() as u8, &module.local); + assert_eq!( + size_of::(), + usize::from(offsets.size_of_vmfunction_import()) + ); + assert_eq!( + offset_of!(VMFunctionImport, body), + usize::from(offsets.vmfunction_import_body()) + ); + assert_eq!( + offset_of!(VMFunctionImport, vmctx), + usize::from(offsets.vmfunction_import_vmctx()) + ); + } +} + +/// A placeholder byte-sized type which is just used to provide some amount of type +/// safety when dealing with pointers to JIT-compiled function bodies. Note that it's +/// deliberately not Copy, as we shouldn't be carelessly copying function body bytes +/// around. +#[repr(C)] +pub struct VMFunctionBody(u8); + +#[cfg(test)] +mod test_vmfunction_body { + use super::VMFunctionBody; + use std::mem::size_of; + + #[test] + fn check_vmfunction_body_offsets() { + assert_eq!(size_of::(), 1); + } +} + +/// The fields compiled code needs to access to utilize a WebAssembly table +/// imported from another instance. +#[derive(Debug, Copy, Clone)] +#[repr(C)] +pub struct VMTableImport { + /// A pointer to the imported table description. + pub from: *mut VMTableDefinition, + + /// A pointer to the `VMContext` that owns the table description. + pub vmctx: *mut VMContext, +} + +#[cfg(test)] +mod test_vmtable_import { + use super::VMTableImport; + use memoffset::offset_of; + use std::mem::size_of; + use wasmtime_environ::{Module, VMOffsets}; + + #[test] + fn check_vmtable_import_offsets() { + let module = Module::new(); + let offsets = VMOffsets::new(size_of::<*mut u8>() as u8, &module.local); + assert_eq!( + size_of::(), + usize::from(offsets.size_of_vmtable_import()) + ); + assert_eq!( + offset_of!(VMTableImport, from), + usize::from(offsets.vmtable_import_from()) + ); + assert_eq!( + offset_of!(VMTableImport, vmctx), + usize::from(offsets.vmtable_import_vmctx()) + ); + } +} + +/// The fields compiled code needs to access to utilize a WebAssembly linear +/// memory imported from another instance. +#[derive(Debug, Copy, Clone)] +#[repr(C)] +pub struct VMMemoryImport { + /// A pointer to the imported memory description. + pub from: *mut VMMemoryDefinition, + + /// A pointer to the `VMContext` that owns the memory description. + pub vmctx: *mut VMContext, +} + +#[cfg(test)] +mod test_vmmemory_import { + use super::VMMemoryImport; + use memoffset::offset_of; + use std::mem::size_of; + use wasmtime_environ::{Module, VMOffsets}; + + #[test] + fn check_vmmemory_import_offsets() { + let module = Module::new(); + let offsets = VMOffsets::new(size_of::<*mut u8>() as u8, &module.local); + assert_eq!( + size_of::(), + usize::from(offsets.size_of_vmmemory_import()) + ); + assert_eq!( + offset_of!(VMMemoryImport, from), + usize::from(offsets.vmmemory_import_from()) + ); + assert_eq!( + offset_of!(VMMemoryImport, vmctx), + usize::from(offsets.vmmemory_import_vmctx()) + ); + } +} + +/// The fields compiled code needs to access to utilize a WebAssembly global +/// variable imported from another instance. +#[derive(Debug, Copy, Clone)] +#[repr(C)] +pub struct VMGlobalImport { + /// A pointer to the imported global variable description. + pub from: *mut VMGlobalDefinition, +} + +#[cfg(test)] +mod test_vmglobal_import { + use super::VMGlobalImport; + use memoffset::offset_of; + use std::mem::size_of; + use wasmtime_environ::{Module, VMOffsets}; + + #[test] + fn check_vmglobal_import_offsets() { + let module = Module::new(); + let offsets = VMOffsets::new(size_of::<*mut u8>() as u8, &module.local); + assert_eq!( + size_of::(), + usize::from(offsets.size_of_vmglobal_import()) + ); + assert_eq!( + offset_of!(VMGlobalImport, from), + usize::from(offsets.vmglobal_import_from()) + ); + } +} + +/// The fields compiled code needs to access to utilize a WebAssembly linear +/// memory defined within the instance, namely the start address and the +/// size in bytes. +#[derive(Debug, Copy, Clone)] +#[repr(C)] +pub struct VMMemoryDefinition { + /// The start address. + pub base: *mut u8, + + /// The current logical size of this linear memory in bytes. + pub current_length: usize, +} + +#[cfg(test)] +mod test_vmmemory_definition { + use super::VMMemoryDefinition; + use memoffset::offset_of; + use std::mem::size_of; + use wasmtime_environ::{Module, VMOffsets}; + + #[test] + fn check_vmmemory_definition_offsets() { + let module = Module::new(); + let offsets = VMOffsets::new(size_of::<*mut u8>() as u8, &module.local); + assert_eq!( + size_of::(), + usize::from(offsets.size_of_vmmemory_definition()) + ); + assert_eq!( + offset_of!(VMMemoryDefinition, base), + usize::from(offsets.vmmemory_definition_base()) + ); + assert_eq!( + offset_of!(VMMemoryDefinition, current_length), + usize::from(offsets.vmmemory_definition_current_length()) + ); + /* TODO: Assert that the size of `current_length` matches. + assert_eq!( + size_of::(), + usize::from(offsets.size_of_vmmemory_definition_current_length()) + ); + */ + } +} + +/// The fields compiled code needs to access to utilize a WebAssembly table +/// defined within the instance. +#[derive(Debug, Copy, Clone)] +#[repr(C)] +pub struct VMTableDefinition { + /// Pointer to the table data. + pub base: *mut u8, + + /// The current number of elements in the table. + pub current_elements: u32, +} + +#[cfg(test)] +mod test_vmtable_definition { + use super::VMTableDefinition; + use memoffset::offset_of; + use std::mem::size_of; + use wasmtime_environ::{Module, VMOffsets}; + + #[test] + fn check_vmtable_definition_offsets() { + let module = Module::new(); + let offsets = VMOffsets::new(size_of::<*mut u8>() as u8, &module.local); + assert_eq!( + size_of::(), + usize::from(offsets.size_of_vmtable_definition()) + ); + assert_eq!( + offset_of!(VMTableDefinition, base), + usize::from(offsets.vmtable_definition_base()) + ); + assert_eq!( + offset_of!(VMTableDefinition, current_elements), + usize::from(offsets.vmtable_definition_current_elements()) + ); + } +} + +/// The storage for a WebAssembly global defined within the instance. +/// +/// TODO: Pack the globals more densely, rather than using the same size +/// for every type. +#[derive(Debug, Copy, Clone)] +#[repr(C, align(16))] +pub struct VMGlobalDefinition { + storage: [u8; 16], + // If more elements are added here, remember to add offset_of tests below! +} + +#[cfg(test)] +mod test_vmglobal_definition { + use super::VMGlobalDefinition; + use more_asserts::assert_ge; + use std::mem::{align_of, size_of}; + use wasmtime_environ::{Module, VMOffsets}; + + #[test] + fn check_vmglobal_definition_alignment() { + assert_ge!(align_of::(), align_of::()); + assert_ge!(align_of::(), align_of::()); + assert_ge!(align_of::(), align_of::()); + assert_ge!(align_of::(), align_of::()); + assert_ge!(align_of::(), align_of::<[u8; 16]>()); + } + + #[test] + fn check_vmglobal_definition_offsets() { + let module = Module::new(); + let offsets = VMOffsets::new(size_of::<*mut u8>() as u8, &module.local); + assert_eq!( + size_of::(), + usize::from(offsets.size_of_vmglobal_definition()) + ); + } + + #[test] + fn check_vmglobal_begins_aligned() { + let module = Module::new(); + let offsets = VMOffsets::new(size_of::<*mut u8>() as u8, &module.local); + assert_eq!(offsets.vmctx_globals_begin() % 16, 0); + } +} + +impl VMGlobalDefinition { + /// Construct a `VMGlobalDefinition`. + pub fn new() -> Self { + Self { storage: [0; 16] } + } + + /// Return a reference to the value as an i32. + #[allow(clippy::cast_ptr_alignment)] + pub unsafe fn as_i32(&self) -> &i32 { + &*(self.storage.as_ref().as_ptr() as *const i32) + } + + /// Return a mutable reference to the value as an i32. + #[allow(clippy::cast_ptr_alignment)] + pub unsafe fn as_i32_mut(&mut self) -> &mut i32 { + &mut *(self.storage.as_mut().as_mut_ptr() as *mut i32) + } + + /// Return a reference to the value as a u32. + #[allow(clippy::cast_ptr_alignment)] + pub unsafe fn as_u32(&self) -> &u32 { + &*(self.storage.as_ref().as_ptr() as *const u32) + } + + /// Return a mutable reference to the value as an u32. + #[allow(clippy::cast_ptr_alignment)] + pub unsafe fn as_u32_mut(&mut self) -> &mut u32 { + &mut *(self.storage.as_mut().as_mut_ptr() as *mut u32) + } + + /// Return a reference to the value as an i64. + #[allow(clippy::cast_ptr_alignment)] + pub unsafe fn as_i64(&self) -> &i64 { + &*(self.storage.as_ref().as_ptr() as *const i64) + } + + /// Return a mutable reference to the value as an i64. + #[allow(clippy::cast_ptr_alignment)] + pub unsafe fn as_i64_mut(&mut self) -> &mut i64 { + &mut *(self.storage.as_mut().as_mut_ptr() as *mut i64) + } + + /// Return a reference to the value as an u64. + #[allow(clippy::cast_ptr_alignment)] + pub unsafe fn as_u64(&self) -> &u64 { + &*(self.storage.as_ref().as_ptr() as *const u64) + } + + /// Return a mutable reference to the value as an u64. + #[allow(clippy::cast_ptr_alignment)] + pub unsafe fn as_u64_mut(&mut self) -> &mut u64 { + &mut *(self.storage.as_mut().as_mut_ptr() as *mut u64) + } + + /// Return a reference to the value as an f32. + #[allow(clippy::cast_ptr_alignment)] + pub unsafe fn as_f32(&self) -> &f32 { + &*(self.storage.as_ref().as_ptr() as *const f32) + } + + /// Return a mutable reference to the value as an f32. + #[allow(clippy::cast_ptr_alignment)] + pub unsafe fn as_f32_mut(&mut self) -> &mut f32 { + &mut *(self.storage.as_mut().as_mut_ptr() as *mut f32) + } + + /// Return a reference to the value as f32 bits. + #[allow(clippy::cast_ptr_alignment)] + pub unsafe fn as_f32_bits(&self) -> &u32 { + &*(self.storage.as_ref().as_ptr() as *const u32) + } + + /// Return a mutable reference to the value as f32 bits. + #[allow(clippy::cast_ptr_alignment)] + pub unsafe fn as_f32_bits_mut(&mut self) -> &mut u32 { + &mut *(self.storage.as_mut().as_mut_ptr() as *mut u32) + } + + /// Return a reference to the value as an f64. + #[allow(clippy::cast_ptr_alignment)] + pub unsafe fn as_f64(&self) -> &f64 { + &*(self.storage.as_ref().as_ptr() as *const f64) + } + + /// Return a mutable reference to the value as an f64. + #[allow(clippy::cast_ptr_alignment)] + pub unsafe fn as_f64_mut(&mut self) -> &mut f64 { + &mut *(self.storage.as_mut().as_mut_ptr() as *mut f64) + } + + /// Return a reference to the value as f64 bits. + #[allow(clippy::cast_ptr_alignment)] + pub unsafe fn as_f64_bits(&self) -> &u64 { + &*(self.storage.as_ref().as_ptr() as *const u64) + } + + /// Return a mutable reference to the value as f64 bits. + #[allow(clippy::cast_ptr_alignment)] + pub unsafe fn as_f64_bits_mut(&mut self) -> &mut u64 { + &mut *(self.storage.as_mut().as_mut_ptr() as *mut u64) + } + + /// Return a reference to the value as an u128. + #[allow(clippy::cast_ptr_alignment)] + pub unsafe fn as_u128(&self) -> &u128 { + &*(self.storage.as_ref().as_ptr() as *const u128) + } + + /// Return a mutable reference to the value as an u128. + #[allow(clippy::cast_ptr_alignment)] + pub unsafe fn as_u128_mut(&mut self) -> &mut u128 { + &mut *(self.storage.as_mut().as_mut_ptr() as *mut u128) + } + + /// Return a reference to the value as u128 bits. + #[allow(clippy::cast_ptr_alignment)] + pub unsafe fn as_u128_bits(&self) -> &[u8; 16] { + &*(self.storage.as_ref().as_ptr() as *const [u8; 16]) + } + + /// Return a mutable reference to the value as u128 bits. + #[allow(clippy::cast_ptr_alignment)] + pub unsafe fn as_u128_bits_mut(&mut self) -> &mut [u8; 16] { + &mut *(self.storage.as_mut().as_mut_ptr() as *mut [u8; 16]) + } +} + +/// An index into the shared signature registry, usable for checking signatures +/// at indirect calls. +#[repr(C)] +#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)] +pub struct VMSharedSignatureIndex(u32); + +#[cfg(test)] +mod test_vmshared_signature_index { + use super::VMSharedSignatureIndex; + use std::mem::size_of; + use wasmtime_environ::{Module, TargetSharedSignatureIndex, VMOffsets}; + + #[test] + fn check_vmshared_signature_index() { + let module = Module::new(); + let offsets = VMOffsets::new(size_of::<*mut u8>() as u8, &module.local); + assert_eq!( + size_of::(), + usize::from(offsets.size_of_vmshared_signature_index()) + ); + } + + #[test] + fn check_target_shared_signature_index() { + assert_eq!( + size_of::(), + size_of::() + ); + } +} + +impl VMSharedSignatureIndex { + /// Create a new `VMSharedSignatureIndex`. + pub fn new(value: u32) -> Self { + Self(value) + } +} + +impl Default for VMSharedSignatureIndex { + fn default() -> Self { + Self::new(u32::MAX) + } +} + +/// The VM caller-checked "anyfunc" record, for caller-side signature checking. +/// It consists of the actual function pointer and a signature id to be checked +/// by the caller. +#[derive(Debug, Clone)] +#[repr(C)] +pub struct VMCallerCheckedAnyfunc { + /// Function body. + pub func_ptr: *const VMFunctionBody, + /// Function signature id. + pub type_index: VMSharedSignatureIndex, + /// Function `VMContext`. + pub vmctx: *mut VMContext, + // If more elements are added here, remember to add offset_of tests below! +} + +#[cfg(test)] +mod test_vmcaller_checked_anyfunc { + use super::VMCallerCheckedAnyfunc; + use memoffset::offset_of; + use std::mem::size_of; + use wasmtime_environ::{Module, VMOffsets}; + + #[test] + fn check_vmcaller_checked_anyfunc_offsets() { + let module = Module::new(); + let offsets = VMOffsets::new(size_of::<*mut u8>() as u8, &module.local); + assert_eq!( + size_of::(), + usize::from(offsets.size_of_vmcaller_checked_anyfunc()) + ); + assert_eq!( + offset_of!(VMCallerCheckedAnyfunc, func_ptr), + usize::from(offsets.vmcaller_checked_anyfunc_func_ptr()) + ); + assert_eq!( + offset_of!(VMCallerCheckedAnyfunc, type_index), + usize::from(offsets.vmcaller_checked_anyfunc_type_index()) + ); + assert_eq!( + offset_of!(VMCallerCheckedAnyfunc, vmctx), + usize::from(offsets.vmcaller_checked_anyfunc_vmctx()) + ); + } +} + +impl Default for VMCallerCheckedAnyfunc { + fn default() -> Self { + Self { + func_ptr: ptr::null_mut(), + type_index: Default::default(), + vmctx: ptr::null_mut(), + } + } +} + +/// An array that stores addresses of builtin functions. We translate code +/// to use indirect calls. This way, we don't have to patch the code. +#[repr(C)] +pub struct VMBuiltinFunctionsArray { + ptrs: [usize; Self::len()], +} + +impl VMBuiltinFunctionsArray { + pub const fn len() -> usize { + BuiltinFunctionIndex::builtin_functions_total_number() as usize + } + + pub fn initialized() -> Self { + use crate::libcalls::*; + + let mut ptrs = [0; Self::len()]; + + ptrs[BuiltinFunctionIndex::get_memory32_grow_index().index() as usize] = + wasmtime_memory32_grow as usize; + ptrs[BuiltinFunctionIndex::get_imported_memory32_grow_index().index() as usize] = + wasmtime_imported_memory32_grow as usize; + + ptrs[BuiltinFunctionIndex::get_memory32_size_index().index() as usize] = + wasmtime_memory32_size as usize; + ptrs[BuiltinFunctionIndex::get_imported_memory32_size_index().index() as usize] = + wasmtime_imported_memory32_size as usize; + + ptrs[BuiltinFunctionIndex::get_table_copy_index().index() as usize] = + wasmtime_table_copy as usize; + + ptrs[BuiltinFunctionIndex::get_table_init_index().index() as usize] = + wasmtime_table_init as usize; + ptrs[BuiltinFunctionIndex::get_elem_drop_index().index() as usize] = + wasmtime_elem_drop as usize; + + ptrs[BuiltinFunctionIndex::get_defined_memory_copy_index().index() as usize] = + wasmtime_defined_memory_copy as usize; + ptrs[BuiltinFunctionIndex::get_imported_memory_copy_index().index() as usize] = + wasmtime_imported_memory_copy as usize; + ptrs[BuiltinFunctionIndex::get_memory_fill_index().index() as usize] = + wasmtime_memory_fill as usize; + ptrs[BuiltinFunctionIndex::get_imported_memory_fill_index().index() as usize] = + wasmtime_imported_memory_fill as usize; + + debug_assert!(ptrs.iter().cloned().all(|p| p != 0)); + + Self { ptrs } + } +} + +/// The storage for a WebAssembly invocation argument +/// +/// TODO: These could be packed more densely, rather than using the same size for every type. +#[derive(Debug, Copy, Clone)] +#[repr(C, align(16))] +pub struct VMInvokeArgument([u8; 16]); + +#[cfg(test)] +mod test_vm_invoke_argument { + use super::VMInvokeArgument; + use std::mem::{align_of, size_of}; + use wasmtime_environ::{Module, VMOffsets}; + + #[test] + fn check_vm_invoke_argument_alignment() { + assert_eq!(align_of::(), 16); + } + + #[test] + fn check_vmglobal_definition_offsets() { + let module = Module::new(); + let offsets = VMOffsets::new(size_of::<*mut u8>() as u8, &module.local); + assert_eq!( + size_of::(), + usize::from(offsets.size_of_vmglobal_definition()) + ); + } +} + +impl VMInvokeArgument { + /// Create a new invocation argument filled with zeroes + pub fn new() -> Self { + Self([0; 16]) + } +} + +/// The VM "context", which is pointed to by the `vmctx` arg in Cranelift. +/// This has information about globals, memories, tables, and other runtime +/// state associated with the current instance. +/// +/// The struct here is empty, as the sizes of these fields are dynamic, and +/// we can't describe them in Rust's type system. Sufficient memory is +/// allocated at runtime. +/// +/// TODO: We could move the globals into the `vmctx` allocation too. +#[derive(Debug)] +#[repr(C, align(16))] // align 16 since globals are aligned to that and contained inside +pub struct VMContext {} + +impl VMContext { + /// Return a mutable reference to the associated `Instance`. + /// + /// # Safety + /// This is unsafe because it doesn't work on just any `VMContext`, it must + /// be a `VMContext` allocated as part of an `Instance`. + #[allow(clippy::cast_ptr_alignment)] + pub(crate) unsafe fn instance(&self) -> &Instance { + &*((self as *const Self as *mut u8).offset(-Instance::vmctx_offset()) as *const Instance) + } + + /// Return a reference to the host state associated with this `Instance`. + /// + /// # Safety + /// This is unsafe because it doesn't work on just any `VMContext`, it must + /// be a `VMContext` allocated as part of an `Instance`. + pub unsafe fn host_state(&self) -> &dyn Any { + self.instance().host_state() + } +} diff --git a/crates/test-programs/Cargo.toml b/crates/test-programs/Cargo.toml new file mode 100644 index 0000000000..60b8b85081 --- /dev/null +++ b/crates/test-programs/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "test-programs" +version = "0.12.0" +authors = ["The Wasmtime Project Developers"] +readme = "README.md" +edition = "2018" +publish = false + +[build-dependencies] +cfg-if = "0.1.9" + +[dev-dependencies] +wasi-common = { path = "../wasi-common", version = "0.12.0" } +wasmtime-wasi = { path = "../wasi", version = "0.12.0" } +wasmtime = { path = "../api", version = "0.12.0" } +target-lexicon = "0.10.0" +pretty_env_logger = "0.3.0" +tempfile = "3.1.0" +os_pipe = "0.9" +anyhow = "1.0.19" +wat = "1.0.10" + +[features] +test_programs = [] diff --git a/crates/test-programs/README.md b/crates/test-programs/README.md new file mode 100644 index 0000000000..662216f426 --- /dev/null +++ b/crates/test-programs/README.md @@ -0,0 +1,2 @@ +This is the `test-programs` crate, which builds and runs whole programs +compiled to wasm32-wasi. diff --git a/crates/test-programs/build.rs b/crates/test-programs/build.rs new file mode 100644 index 0000000000..c7c9460623 --- /dev/null +++ b/crates/test-programs/build.rs @@ -0,0 +1,203 @@ +//! Build program to generate a program which runs all the testsuites. +//! +//! By generating a separate `#[test]` test for each file, we allow cargo test +//! to automatically run the files in parallel. + +fn main() { + #[cfg(feature = "test_programs")] + wasi_tests::build_and_generate_tests() +} + +#[cfg(feature = "test_programs")] +mod wasi_tests { + use std::env; + use std::fs::{read_dir, DirEntry, File}; + use std::io::{self, Write}; + use std::path::{Path, PathBuf}; + use std::process::{Command, Stdio}; + + pub(super) fn build_and_generate_tests() { + // Validate if any of test sources are present and if they changed + // This should always work since there is no submodule to init anymore + let bin_tests = std::fs::read_dir("wasi-tests/src/bin").unwrap(); + for test in bin_tests { + if let Ok(test_file) = test { + let test_file_path = test_file + .path() + .into_os_string() + .into_string() + .expect("test file path"); + println!("cargo:rerun-if-changed={}", test_file_path); + } + } + println!("cargo:rerun-if-changed=wasi-tests/Cargo.toml"); + println!("cargo:rerun-if-changed=wasi-tests/src/lib.rs"); + // Build tests to OUT_DIR (target/*/build/wasi-common-*/out/wasm32-wasi/release/*.wasm) + let out_dir = PathBuf::from( + env::var("OUT_DIR").expect("The OUT_DIR environment variable must be set"), + ); + let mut out = + File::create(out_dir.join("wasi_tests.rs")).expect("error generating test source file"); + build_tests("wasi-tests", &out_dir).expect("building tests"); + test_directory(&mut out, "wasi-tests", &out_dir).expect("generating tests"); + } + + fn build_tests(testsuite: &str, out_dir: &Path) -> io::Result<()> { + let mut cmd = Command::new("cargo"); + cmd.args(&[ + "build", + "--release", + "--target=wasm32-wasi", + "--target-dir", + out_dir.to_str().unwrap(), + ]) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .current_dir(testsuite); + let output = cmd.output()?; + + let status = output.status; + if !status.success() { + panic!( + "Building tests failed: exit code: {}", + status.code().unwrap() + ); + } + + Ok(()) + } + + fn test_directory(out: &mut File, testsuite: &str, out_dir: &Path) -> io::Result<()> { + let mut dir_entries: Vec<_> = read_dir(out_dir.join("wasm32-wasi/release")) + .expect("reading testsuite directory") + .map(|r| r.expect("reading testsuite directory entry")) + .filter(|dir_entry| { + let p = dir_entry.path(); + if let Some(ext) = p.extension() { + // Only look at wast files. + if ext == "wasm" { + // Ignore files starting with `.`, which could be editor temporary files + if let Some(stem) = p.file_stem() { + if let Some(stemstr) = stem.to_str() { + if !stemstr.starts_with('.') { + return true; + } + } + } + } + } + false + }) + .collect(); + + dir_entries.sort_by_key(|dir| dir.path()); + + writeln!( + out, + "mod {} {{", + Path::new(testsuite) + .file_stem() + .expect("testsuite filename should have a stem") + .to_str() + .expect("testsuite filename should be representable as a string") + .replace("-", "_") + )?; + writeln!(out, " use super::{{runtime, utils, setup_log}};")?; + for dir_entry in dir_entries { + write_testsuite_tests(out, dir_entry, testsuite)?; + } + writeln!(out, "}}")?; + Ok(()) + } + + fn write_testsuite_tests( + out: &mut File, + dir_entry: DirEntry, + testsuite: &str, + ) -> io::Result<()> { + let path = dir_entry.path(); + let stemstr = path + .file_stem() + .expect("file_stem") + .to_str() + .expect("to_str"); + + writeln!(out, " #[test]")?; + if ignore(testsuite, stemstr) { + writeln!(out, " #[ignore]")?; + } + writeln!( + out, + " fn r#{}() -> anyhow::Result<()> {{", + &stemstr.replace("-", "_") + )?; + writeln!(out, " setup_log();")?; + writeln!( + out, + " let path = std::path::Path::new(r#\"{}\"#);", + path.display() + )?; + writeln!(out, " let data = wat::parse_file(path)?;")?; + writeln!( + out, + " let bin_name = utils::extract_exec_name_from_path(path)?;" + )?; + let workspace = if no_preopens(testsuite, stemstr) { + "None" + } else { + writeln!( + out, + " let workspace = utils::prepare_workspace(&bin_name)?;" + )?; + "Some(workspace.path())" + }; + writeln!( + out, + " runtime::instantiate(&data, &bin_name, {})", + workspace + )?; + writeln!(out, " }}")?; + writeln!(out)?; + Ok(()) + } + + cfg_if::cfg_if! { + if #[cfg(not(windows))] { + /// Ignore tests that aren't supported yet. + fn ignore(_testsuite: &str, _name: &str) -> bool { + false + } + } else { + /// Ignore tests that aren't supported yet. + fn ignore(testsuite: &str, name: &str) -> bool { + if testsuite == "wasi-tests" { + match name { + "readlink_no_buffer" => true, + "dangling_symlink" => true, + "symlink_loop" => true, + "truncation_rights" => true, + "path_link" => true, + "dangling_fd" => true, + _ => false, + } + } else { + unreachable!() + } + } + } + } + + /// Mark tests which do not require preopens + fn no_preopens(testsuite: &str, name: &str) -> bool { + if testsuite == "wasi-tests" { + match name { + "big_random_buf" => true, + "clock_time_get" => true, + "sched_yield" => true, + _ => false, + } + } else { + unreachable!() + } + } +} diff --git a/crates/test-programs/src/lib.rs b/crates/test-programs/src/lib.rs new file mode 100644 index 0000000000..efeb701d08 --- /dev/null +++ b/crates/test-programs/src/lib.rs @@ -0,0 +1,2 @@ +// This crate doesn't contain any code; it just exists to run tests for +// other crates in the workspace. diff --git a/crates/test-programs/tests/wasm_tests/main.rs b/crates/test-programs/tests/wasm_tests/main.rs new file mode 100644 index 0000000000..41e542bc8f --- /dev/null +++ b/crates/test-programs/tests/wasm_tests/main.rs @@ -0,0 +1,15 @@ +#![cfg(feature = "test_programs")] +mod runtime; +mod utils; + +use std::sync::Once; + +static LOG_INIT: Once = Once::new(); + +fn setup_log() { + LOG_INIT.call_once(|| { + pretty_env_logger::init(); + }) +} + +include!(concat!(env!("OUT_DIR"), "/wasi_tests.rs")); diff --git a/crates/test-programs/tests/wasm_tests/runtime.rs b/crates/test-programs/tests/wasm_tests/runtime.rs new file mode 100644 index 0000000000..42d4c7221b --- /dev/null +++ b/crates/test-programs/tests/wasm_tests/runtime.rs @@ -0,0 +1,83 @@ +use anyhow::{bail, Context}; +use std::fs::File; +use std::path::Path; +use wasmtime::{Instance, Module, Store}; + +pub fn instantiate(data: &[u8], bin_name: &str, workspace: Option<&Path>) -> anyhow::Result<()> { + let store = Store::default(); + + let get_preopens = |workspace: Option<&Path>| -> anyhow::Result> { + if let Some(workspace) = workspace { + let preopen_dir = wasi_common::preopen_dir(workspace) + .context(format!("error while preopening {:?}", workspace))?; + + Ok(vec![(".".to_owned(), preopen_dir)]) + } else { + Ok(vec![]) + } + }; + + // Create our wasi context with pretty standard arguments/inheritance/etc. + // Additionally register andy preopened directories if we have them. + let mut builder = wasi_common::WasiCtxBuilder::new(); + + builder.arg(bin_name).arg(".").inherit_stdio(); + + for (dir, file) in get_preopens(workspace)? { + builder.preopened_dir(file, dir); + } + + // The nonstandard thing we do with `WasiCtxBuilder` is to ensure that + // `stdin` is always an unreadable pipe. This is expected in the test suite + // where `stdin` is never ready to be read. In some CI systems, however, + // stdin is closed which causes tests to fail. + let (reader, _writer) = os_pipe::pipe()?; + builder.stdin(reader_to_file(reader)); + let snapshot1 = wasmtime_wasi::Wasi::new(&store, builder.build()?); + let module = Module::new(&store, &data).context("failed to create wasm module")?; + let imports = module + .imports() + .iter() + .map(|i| { + let field_name = i.name(); + if let Some(export) = snapshot1.get_export(field_name) { + Ok(export.clone().into()) + } else { + bail!( + "import {} was not found in module {}", + field_name, + i.module(), + ) + } + }) + .collect::, _>>()?; + + let instance = Instance::new(&module, &imports).context(format!( + "error while instantiating Wasm module '{}'", + bin_name, + ))?; + + let export = instance + .get_export("_start") + .context("expected a _start export")? + .clone(); + + export + .func() + .context("expected export to be a func")? + .call(&[])?; + + Ok(()) +} + +#[cfg(unix)] +fn reader_to_file(reader: os_pipe::PipeReader) -> File { + use std::os::unix::prelude::*; + unsafe { File::from_raw_fd(reader.into_raw_fd()) } +} + +#[cfg(windows)] +fn reader_to_file(reader: os_pipe::PipeReader) -> File { + use std::os::windows::prelude::*; + unsafe { File::from_raw_handle(reader.into_raw_handle()) } +} diff --git a/crates/test-programs/tests/wasm_tests/utils.rs b/crates/test-programs/tests/wasm_tests/utils.rs new file mode 100644 index 0000000000..c8b35f92d9 --- /dev/null +++ b/crates/test-programs/tests/wasm_tests/utils.rs @@ -0,0 +1,20 @@ +use std::path::Path; +use tempfile::{Builder, TempDir}; + +pub fn prepare_workspace(exe_name: &str) -> anyhow::Result { + let prefix = format!("wasi_common_{}", exe_name); + let tempdir = Builder::new().prefix(&prefix).tempdir()?; + Ok(tempdir) +} + +pub fn extract_exec_name_from_path(path: &Path) -> anyhow::Result { + path.file_stem() + .and_then(|s| s.to_str()) + .map(String::from) + .ok_or_else(|| { + anyhow::anyhow!( + "couldn't extract the file stem from path {}", + path.display() + ) + }) +} diff --git a/crates/test-programs/wasi-tests/Cargo.lock b/crates/test-programs/wasi-tests/Cargo.lock new file mode 100644 index 0000000000..365383eec4 --- /dev/null +++ b/crates/test-programs/wasi-tests/Cargo.lock @@ -0,0 +1,30 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "libc" +version = "0.2.66" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "more-asserts" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "wasi-tests" +version = "0.12.0" +dependencies = [ + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "more-asserts 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[metadata] +"checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" +"checksum more-asserts 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0debeb9fcf88823ea64d64e4a815ab1643f33127d995978e099942ce38f25238" +"checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" diff --git a/crates/test-programs/wasi-tests/Cargo.toml b/crates/test-programs/wasi-tests/Cargo.toml new file mode 100644 index 0000000000..79affc2fff --- /dev/null +++ b/crates/test-programs/wasi-tests/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "wasi-tests" +version = "0.12.0" +authors = ["The Wasmtime Project Developers"] +readme = "README.md" +edition = "2018" +publish = false + +[dependencies] +libc = "0.2.65" +wasi = "0.9.0" +more-asserts = "0.2.1" + +# This crate is built with the wasm32-wasi target, so it's separate +# from the main Wasmtime build, so use this directive to exclude it +# from the parent directory's workspace. +[workspace] diff --git a/crates/test-programs/wasi-tests/README.md b/crates/test-programs/wasi-tests/README.md new file mode 100644 index 0000000000..f281fa1a8e --- /dev/null +++ b/crates/test-programs/wasi-tests/README.md @@ -0,0 +1,3 @@ +This is the `wasi-tests` crate, which contains source code for the system-level WASI tests. + +Building these tests requires that the `wasm32-wasi` target be installed. diff --git a/crates/test-programs/wasi-tests/src/bin/big_random_buf.rs b/crates/test-programs/wasi-tests/src/bin/big_random_buf.rs new file mode 100644 index 0000000000..ad40497f5c --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/big_random_buf.rs @@ -0,0 +1,15 @@ +fn test_big_random_buf() { + let mut buf = Vec::new(); + buf.resize(1024, 0); + unsafe { + wasi::random_get(buf.as_mut_ptr(), 1024).expect("failed to call random_get"); + } + // Chances are pretty good that at least *one* byte will be non-zero in + // any meaningful random function producing 1024 u8 values. + assert!(buf.iter().any(|x| *x != 0), "random_get returned all zeros"); +} + +fn main() { + // Run the tests. + test_big_random_buf() +} diff --git a/crates/test-programs/wasi-tests/src/bin/clock_time_get.rs b/crates/test-programs/wasi-tests/src/bin/clock_time_get.rs new file mode 100644 index 0000000000..2ef8a6c8f3 --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/clock_time_get.rs @@ -0,0 +1,19 @@ +use more_asserts::assert_le; + +unsafe fn test_clock_time_get() { + // Test that clock_time_get succeeds. Even in environments where it's not + // desirable to expose high-precision timers, it should still succeed. + // clock_res_get is where information about precision can be provided. + wasi::clock_time_get(wasi::CLOCKID_MONOTONIC, 1).expect("precision 1 should work"); + + let first_time = + wasi::clock_time_get(wasi::CLOCKID_MONOTONIC, 0).expect("precision 0 should work"); + + let time = wasi::clock_time_get(wasi::CLOCKID_MONOTONIC, 0).expect("re-fetch time should work"); + assert_le!(first_time, time, "CLOCK_MONOTONIC should be monotonic"); +} + +fn main() { + // Run the tests. + unsafe { test_clock_time_get() } +} diff --git a/crates/test-programs/wasi-tests/src/bin/close_preopen.rs b/crates/test-programs/wasi-tests/src/bin/close_preopen.rs new file mode 100644 index 0000000000..26ca315465 --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/close_preopen.rs @@ -0,0 +1,75 @@ +use more_asserts::assert_gt; +use std::{env, process}; +use wasi_tests::open_scratch_directory; + +unsafe fn test_close_preopen(dir_fd: wasi::Fd) { + let pre_fd: wasi::Fd = (libc::STDERR_FILENO + 1) as wasi::Fd; + + assert_gt!(dir_fd, pre_fd, "dir_fd number"); + + // Try to close a preopened directory handle. + assert_eq!( + wasi::fd_close(pre_fd) + .expect_err("closing a preopened file descriptor") + .raw_error(), + wasi::ERRNO_NOTSUP, + "errno should ERRNO_NOTSUP", + ); + + // Try to renumber over a preopened directory handle. + assert_eq!( + wasi::fd_renumber(dir_fd, pre_fd) + .expect_err("renumbering over a preopened file descriptor") + .raw_error(), + wasi::ERRNO_NOTSUP, + "errno should be ERRNO_NOTSUP", + ); + + // Ensure that dir_fd is still open. + let dir_fdstat = wasi::fd_fdstat_get(dir_fd).expect("failed fd_fdstat_get"); + assert_eq!( + dir_fdstat.fs_filetype, + wasi::FILETYPE_DIRECTORY, + "expected the scratch directory to be a directory", + ); + + // Try to renumber a preopened directory handle. + assert_eq!( + wasi::fd_renumber(pre_fd, dir_fd) + .expect_err("renumbering over a preopened file descriptor") + .raw_error(), + wasi::ERRNO_NOTSUP, + "errno should be ERRNO_NOTSUP", + ); + + // Ensure that dir_fd is still open. + let dir_fdstat = wasi::fd_fdstat_get(dir_fd).expect("failed fd_fdstat_get"); + assert_eq!( + dir_fdstat.fs_filetype, + wasi::FILETYPE_DIRECTORY, + "expected the scratch directory to be a directory", + ); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_close_preopen(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/dangling_fd.rs b/crates/test-programs/wasi-tests/src/bin/dangling_fd.rs new file mode 100644 index 0000000000..e63ecbc8d7 --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/dangling_fd.rs @@ -0,0 +1,54 @@ +use more_asserts::assert_gt; +use std::{env, process}; +use wasi_tests::open_scratch_directory; + +unsafe fn test_dangling_fd(dir_fd: wasi::Fd) { + // Create a file, open it, delete it without closing the handle, + // and then try creating it again + let fd = wasi::path_open(dir_fd, 0, "file", wasi::OFLAGS_CREAT, 0, 0, 0).unwrap(); + wasi::fd_close(fd).unwrap(); + let file_fd = wasi::path_open(dir_fd, 0, "file", 0, 0, 0, 0).expect("failed to open"); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + wasi::path_unlink_file(dir_fd, "file").expect("failed to unlink"); + let fd = wasi::path_open(dir_fd, 0, "file", wasi::OFLAGS_CREAT, 0, 0, 0).unwrap(); + wasi::fd_close(fd).unwrap(); + + // Now, repeat the same process but for a directory + wasi::path_create_directory(dir_fd, "subdir").expect("failed to create dir"); + let subdir_fd = wasi::path_open(dir_fd, 0, "subdir", wasi::OFLAGS_DIRECTORY, 0, 0, 0) + .expect("failed to open dir"); + assert_gt!( + subdir_fd, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + wasi::path_remove_directory(dir_fd, "subdir").expect("failed to remove dir 2"); + wasi::path_create_directory(dir_fd, "subdir").expect("failed to create dir 2"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_dangling_fd(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/dangling_symlink.rs b/crates/test-programs/wasi-tests/src/bin/dangling_symlink.rs new file mode 100644 index 0000000000..3b67c5be10 --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/dangling_symlink.rs @@ -0,0 +1,42 @@ +use std::{env, process}; +use wasi_tests::open_scratch_directory; + +unsafe fn test_dangling_symlink(dir_fd: wasi::Fd) { + // First create a dangling symlink. + wasi::path_symlink("target", dir_fd, "symlink").expect("creating a symlink"); + + // Try to open it as a directory with O_NOFOLLOW. + assert_eq!( + wasi::path_open(dir_fd, 0, "symlink", wasi::OFLAGS_DIRECTORY, 0, 0, 0) + .expect_err("opening a dangling symlink as a directory") + .raw_error(), + wasi::ERRNO_LOOP, + "errno should be ERRNO_LOOP", + ); + + // Clean up. + wasi::path_unlink_file(dir_fd, "symlink").expect("failed to remove file"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_dangling_symlink(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/directory_seek.rs b/crates/test-programs/wasi-tests/src/bin/directory_seek.rs new file mode 100644 index 0000000000..3e1742dcb4 --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/directory_seek.rs @@ -0,0 +1,66 @@ +use more_asserts::assert_gt; +use std::{env, process}; +use wasi_tests::open_scratch_directory; + +unsafe fn test_directory_seek(dir_fd: wasi::Fd) { + // Create a directory in the scratch directory. + wasi::path_create_directory(dir_fd, "dir").expect("failed to make directory"); + + // Open the directory and attempt to request rights for seeking. + let fd = wasi::path_open(dir_fd, 0, "dir", 0, wasi::RIGHTS_FD_SEEK, 0, 0) + .expect("failed to open file"); + assert_gt!( + fd, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + + // Attempt to seek. + assert_eq!( + wasi::fd_seek(fd, 0, wasi::WHENCE_CUR) + .expect_err("seek on a directory") + .raw_error(), + wasi::ERRNO_NOTCAPABLE, + "errno should be ERRNO_NOTCAPABLE" + ); + + // Check if we obtained the right to seek. + let fdstat = wasi::fd_fdstat_get(fd).expect("failed to fdstat"); + assert_eq!( + fdstat.fs_filetype, + wasi::FILETYPE_DIRECTORY, + "expected the scratch directory to be a directory", + ); + assert_eq!( + (fdstat.fs_rights_base & wasi::RIGHTS_FD_SEEK), + 0, + "directory has the seek right", + ); + + // Clean up. + wasi::fd_close(fd).expect("failed to close fd"); + wasi::path_remove_directory(dir_fd, "dir").expect("failed to remove dir"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_directory_seek(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/fd_advise.rs b/crates/test-programs/wasi-tests/src/bin/fd_advise.rs new file mode 100644 index 0000000000..408edf3490 --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/fd_advise.rs @@ -0,0 +1,64 @@ +use more_asserts::assert_gt; +use std::{env, process}; +use wasi_tests::open_scratch_directory; + +unsafe fn test_fd_advise(dir_fd: wasi::Fd) { + // Create a file in the scratch directory. + let file_fd = wasi::path_open( + dir_fd, + 0, + "file", + wasi::OFLAGS_CREAT, + wasi::RIGHTS_FD_READ + | wasi::RIGHTS_FD_WRITE + | wasi::RIGHTS_FD_ADVISE + | wasi::RIGHTS_FD_FILESTAT_GET + | wasi::RIGHTS_FD_ALLOCATE, + 0, + 0, + ) + .expect("failed to open file"); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + + // Check file size + let stat = wasi::fd_filestat_get(file_fd).expect("failed to fdstat"); + assert_eq!(stat.size, 0, "file size should be 0"); + + // Allocate some size + wasi::fd_allocate(file_fd, 0, 100).expect("allocating size"); + + let stat = wasi::fd_filestat_get(file_fd).expect("failed to fdstat 2"); + assert_eq!(stat.size, 100, "file size should be 100"); + + // Advise the kernel + wasi::fd_advise(file_fd, 10, 50, wasi::ADVICE_NORMAL).expect("failed advise"); + + wasi::fd_close(file_fd).expect("failed to close"); + wasi::path_unlink_file(dir_fd, "file").expect("failed to unlink"); +} +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_fd_advise(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/fd_filestat_set.rs b/crates/test-programs/wasi-tests/src/bin/fd_filestat_set.rs new file mode 100644 index 0000000000..0f7cefcfe5 --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/fd_filestat_set.rs @@ -0,0 +1,75 @@ +use more_asserts::assert_gt; +use std::{env, process}; +use wasi_tests::open_scratch_directory; + +unsafe fn test_fd_filestat_set(dir_fd: wasi::Fd) { + // Create a file in the scratch directory. + let file_fd = wasi::path_open( + dir_fd, + 0, + "file", + wasi::OFLAGS_CREAT, + wasi::RIGHTS_FD_READ + | wasi::RIGHTS_FD_WRITE + | wasi::RIGHTS_FD_FILESTAT_GET + | wasi::RIGHTS_FD_FILESTAT_SET_SIZE + | wasi::RIGHTS_FD_FILESTAT_SET_TIMES, + 0, + 0, + ) + .expect("failed to create file"); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + + // Check file size + let stat = wasi::fd_filestat_get(file_fd).expect("failed filestat"); + assert_eq!(stat.size, 0, "file size should be 0"); + + // Check fd_filestat_set_size + wasi::fd_filestat_set_size(file_fd, 100).expect("fd_filestat_set_size"); + + let stat = wasi::fd_filestat_get(file_fd).expect("failed filestat 2"); + assert_eq!(stat.size, 100, "file size should be 100"); + + // Check fd_filestat_set_times + let old_atim = stat.atim; + let new_mtim = stat.mtim - 100; + wasi::fd_filestat_set_times(file_fd, new_mtim, new_mtim, wasi::FSTFLAGS_MTIM) + .expect("fd_filestat_set_times"); + + let stat = wasi::fd_filestat_get(file_fd).expect("failed filestat 3"); + assert_eq!(stat.size, 100, "file size should remain unchanged at 100"); + assert_eq!(stat.mtim, new_mtim, "mtim should change"); + assert_eq!(stat.atim, old_atim, "atim should not change"); + + // let status = wasi_fd_filestat_set_times(file_fd, new_mtim, new_mtim, wasi::FILESTAT_SET_MTIM | wasi::FILESTAT_SET_MTIM_NOW); + // assert_eq!(status, wasi::EINVAL, "ATIM & ATIM_NOW can't both be set"); + + wasi::fd_close(file_fd).expect("failed to close fd"); + wasi::path_unlink_file(dir_fd, "file").expect("failed to remove dir"); +} +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_fd_filestat_set(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/fd_flags_set.rs b/crates/test-programs/wasi-tests/src/bin/fd_flags_set.rs new file mode 100644 index 0000000000..e642ca19a5 --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/fd_flags_set.rs @@ -0,0 +1,165 @@ +use std::{env, process}; +use wasi; +use wasi_tests::open_scratch_directory; + +unsafe fn test_fd_fdstat_set_flags(dir_fd: wasi::Fd) { + const FILE_NAME: &str = "file"; + let data = &[0u8; 100]; + + let file_fd = wasi::path_open( + dir_fd, + 0, + FILE_NAME, + wasi::OFLAGS_CREAT, + wasi::RIGHTS_FD_READ + | wasi::RIGHTS_FD_WRITE + | wasi::RIGHTS_FD_SEEK + | wasi::RIGHTS_FD_TELL + | wasi::RIGHTS_FD_FDSTAT_SET_FLAGS, + 0, + wasi::FDFLAGS_APPEND, + ) + .expect("opening a file"); + + // Write some data and then verify the written data + assert_eq!( + wasi::fd_write( + file_fd, + &[wasi::Ciovec { + buf: data.as_ptr(), + buf_len: data.len(), + }], + ) + .expect("writing to a file"), + data.len(), + "should write {} bytes", + data.len(), + ); + + wasi::fd_seek(file_fd, 0, wasi::WHENCE_SET).expect("seeking file"); + + let buffer = &mut [0u8; 100]; + + assert_eq!( + wasi::fd_read( + file_fd, + &[wasi::Iovec { + buf: buffer.as_mut_ptr(), + buf_len: buffer.len(), + }] + ) + .expect("reading file"), + buffer.len(), + "shoudl read {} bytes", + buffer.len() + ); + + assert_eq!(&data[..], &buffer[..]); + + let data = &[1u8; 100]; + + // Seek back to the start to ensure we're in append-only mode + wasi::fd_seek(file_fd, 0, wasi::WHENCE_SET).expect("seeking file"); + + assert_eq!( + wasi::fd_write( + file_fd, + &[wasi::Ciovec { + buf: data.as_ptr(), + buf_len: data.len(), + }], + ) + .expect("writing to a file"), + data.len(), + "should write {} bytes", + data.len(), + ); + + wasi::fd_seek(file_fd, 100, wasi::WHENCE_SET).expect("seeking file"); + + assert_eq!( + wasi::fd_read( + file_fd, + &[wasi::Iovec { + buf: buffer.as_mut_ptr(), + buf_len: buffer.len(), + }] + ) + .expect("reading file"), + buffer.len(), + "shoudl read {} bytes", + buffer.len() + ); + + assert_eq!(&data[..], &buffer[..]); + + wasi::fd_fdstat_set_flags(file_fd, 0).expect("disabling flags"); + + // Overwrite some existing data to ensure the append mode is now off + wasi::fd_seek(file_fd, 0, wasi::WHENCE_SET).expect("seeking file"); + + let data = &[2u8; 100]; + + assert_eq!( + wasi::fd_write( + file_fd, + &[wasi::Ciovec { + buf: data.as_ptr(), + buf_len: data.len(), + }], + ) + .expect("writing to a file"), + data.len(), + "should write {} bytes", + data.len(), + ); + + wasi::fd_seek(file_fd, 0, wasi::WHENCE_SET).expect("seeking file"); + + assert_eq!( + wasi::fd_read( + file_fd, + &[wasi::Iovec { + buf: buffer.as_mut_ptr(), + buf_len: buffer.len(), + }] + ) + .expect("reading file"), + buffer.len(), + "shoudl read {} bytes", + buffer.len() + ); + + assert_eq!(&data[..], &buffer[..]); + + wasi::fd_close(file_fd).expect("close file"); + + let stat = wasi::path_filestat_get(dir_fd, 0, FILE_NAME).expect("stat path"); + + assert_eq!(stat.size, 200, "expected a file size of 200"); + + wasi::path_unlink_file(dir_fd, FILE_NAME).expect("unlinking file"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + unsafe { + test_fd_fdstat_set_flags(dir_fd); + } +} diff --git a/crates/test-programs/wasi-tests/src/bin/fd_readdir.rs b/crates/test-programs/wasi-tests/src/bin/fd_readdir.rs new file mode 100644 index 0000000000..8790ea0218 --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/fd_readdir.rs @@ -0,0 +1,159 @@ +use more_asserts::assert_gt; +use std::{cmp::min, env, mem, process, slice, str}; +use wasi_tests::open_scratch_directory; + +const BUF_LEN: usize = 256; + +struct DirEntry { + dirent: wasi::Dirent, + name: String, +} + +// Manually reading the output from fd_readdir is tedious and repetitive, +// so encapsulate it into an iterator +struct ReadDir<'a> { + buf: &'a [u8], +} + +impl<'a> ReadDir<'a> { + fn from_slice(buf: &'a [u8]) -> Self { + Self { buf } + } +} + +impl<'a> Iterator for ReadDir<'a> { + type Item = DirEntry; + + fn next(&mut self) -> Option { + unsafe { + if self.buf.is_empty() { + return None; + } + + // Read the data + let dirent_ptr = self.buf.as_ptr() as *const wasi::Dirent; + let dirent = dirent_ptr.read_unaligned(); + let name_ptr = dirent_ptr.offset(1) as *const u8; + // NOTE Linux syscall returns a NULL-terminated name, but WASI doesn't + let namelen = dirent.d_namlen as usize; + let slice = slice::from_raw_parts(name_ptr, namelen); + let name = str::from_utf8(slice).expect("invalid utf8").to_owned(); + + // Update the internal state + let delta = mem::size_of_val(&dirent) + namelen; + self.buf = &self.buf[delta..]; + + DirEntry { dirent, name }.into() + } + } +} + +unsafe fn exec_fd_readdir(fd: wasi::Fd, cookie: wasi::Dircookie) -> Vec { + let mut buf: [u8; BUF_LEN] = [0; BUF_LEN]; + let bufused = + wasi::fd_readdir(fd, buf.as_mut_ptr(), BUF_LEN, cookie).expect("failed fd_readdir"); + + let sl = slice::from_raw_parts(buf.as_ptr(), min(BUF_LEN, bufused)); + let dirs: Vec<_> = ReadDir::from_slice(sl).collect(); + dirs +} + +unsafe fn test_fd_readdir(dir_fd: wasi::Fd) { + let stat = wasi::fd_filestat_get(dir_fd).expect("failed filestat"); + + // Check the behavior in an empty directory + let mut dirs = exec_fd_readdir(dir_fd, 0); + dirs.sort_by_key(|d| d.name.clone()); + assert_eq!(dirs.len(), 2, "expected two entries in an empty directory"); + let mut dirs = dirs.into_iter(); + + // the first entry should be `.` + let dir = dirs.next().expect("first entry is None"); + assert_eq!(dir.name, ".", "first name"); + assert_eq!(dir.dirent.d_type, wasi::FILETYPE_DIRECTORY, "first type"); + assert_eq!(dir.dirent.d_ino, stat.ino); + assert_eq!(dir.dirent.d_namlen, 1); + + // the second entry should be `..` + let dir = dirs.next().expect("second entry is None"); + assert_eq!(dir.name, "..", "second name"); + assert_eq!(dir.dirent.d_type, wasi::FILETYPE_DIRECTORY, "second type"); + + assert!( + dirs.next().is_none(), + "the directory should be seen as empty" + ); + + // Add a file and check the behavior + let file_fd = wasi::path_open( + dir_fd, + 0, + "file", + wasi::OFLAGS_CREAT, + wasi::RIGHTS_FD_READ + | wasi::RIGHTS_FD_WRITE + | wasi::RIGHTS_FD_READDIR + | wasi::RIGHTS_FD_FILESTAT_GET, + 0, + 0, + ) + .expect("failed to create file"); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + + let stat = wasi::fd_filestat_get(file_fd).expect("failed filestat"); + + // Execute another readdir + let mut dirs = exec_fd_readdir(dir_fd, 0); + assert_eq!(dirs.len(), 3, "expected three entries"); + // Save the data about the last entry. We need to do it before sorting. + let lastfile_cookie = dirs[1].dirent.d_next; + let lastfile_name = dirs[2].name.clone(); + dirs.sort_by_key(|d| d.name.clone()); + let mut dirs = dirs.into_iter(); + + let dir = dirs.next().expect("first entry is None"); + assert_eq!(dir.name, ".", "first name"); + let dir = dirs.next().expect("second entry is None"); + assert_eq!(dir.name, "..", "second name"); + let dir = dirs.next().expect("third entry is None"); + // check the file info + assert_eq!(dir.name, "file", "file name doesn't match"); + assert_eq!( + dir.dirent.d_type, + wasi::FILETYPE_REGULAR_FILE, + "type for the real file" + ); + assert_eq!(dir.dirent.d_ino, stat.ino); + + // check if cookie works as expected + let dirs = exec_fd_readdir(dir_fd, lastfile_cookie); + assert_eq!(dirs.len(), 1, "expected one entry"); + assert_eq!(dirs[0].name, lastfile_name, "name of the only entry"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_fd_readdir(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/file_allocate.rs b/crates/test-programs/wasi-tests/src/bin/file_allocate.rs new file mode 100644 index 0000000000..cadee68ab5 --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/file_allocate.rs @@ -0,0 +1,70 @@ +use more_asserts::assert_gt; +use std::{env, process}; +use wasi_tests::open_scratch_directory; + +unsafe fn test_file_allocate(dir_fd: wasi::Fd) { + // Create a file in the scratch directory. + let file_fd = wasi::path_open( + dir_fd, + 0, + "file", + wasi::OFLAGS_CREAT, + wasi::RIGHTS_FD_READ + | wasi::RIGHTS_FD_WRITE + | wasi::RIGHTS_FD_ALLOCATE + | wasi::RIGHTS_FD_FILESTAT_GET, + 0, + 0, + ) + .expect("opening a file"); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + + // Check file size + let mut stat = wasi::fd_filestat_get(file_fd).expect("reading file stats"); + assert_eq!(stat.size, 0, "file size should be 0"); + + // Allocate some size + wasi::fd_allocate(file_fd, 0, 100).expect("allocating size"); + stat = wasi::fd_filestat_get(file_fd).expect("reading file stats"); + assert_eq!(stat.size, 100, "file size should be 100"); + + // Allocate should not modify if less than current size + wasi::fd_allocate(file_fd, 10, 10).expect("allocating size less than current size"); + stat = wasi::fd_filestat_get(file_fd).expect("reading file stats"); + assert_eq!(stat.size, 100, "file size should remain unchanged at 100"); + + // Allocate should modify if offset+len > current_len + wasi::fd_allocate(file_fd, 90, 20).expect("allocating size larger than current size"); + stat = wasi::fd_filestat_get(file_fd).expect("reading file stats"); + assert_eq!(stat.size, 110, "file size should increase from 100 to 110"); + + wasi::fd_close(file_fd).expect("closing a file"); + wasi::path_unlink_file(dir_fd, "file").expect("removing a file"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_file_allocate(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/file_pread_pwrite.rs b/crates/test-programs/wasi-tests/src/bin/file_pread_pwrite.rs new file mode 100644 index 0000000000..d039670c96 --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/file_pread_pwrite.rs @@ -0,0 +1,92 @@ +use more_asserts::assert_gt; +use std::{env, process}; +use wasi_tests::open_scratch_directory; + +unsafe fn test_file_pread_pwrite(dir_fd: wasi::Fd) { + // Create a file in the scratch directory. + let file_fd = wasi::path_open( + dir_fd, + 0, + "file", + wasi::OFLAGS_CREAT, + wasi::RIGHTS_FD_READ | wasi::RIGHTS_FD_SEEK | wasi::RIGHTS_FD_WRITE, + 0, + 0, + ) + .expect("opening a file"); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + + let contents = &[0u8, 1, 2, 3]; + let ciovec = wasi::Ciovec { + buf: contents.as_ptr() as *const _, + buf_len: contents.len(), + }; + let mut nwritten = + wasi::fd_pwrite(file_fd, &mut [ciovec], 0).expect("writing bytes at offset 0"); + assert_eq!(nwritten, 4, "nwritten bytes check"); + + let contents = &mut [0u8; 4]; + let iovec = wasi::Iovec { + buf: contents.as_mut_ptr() as *mut _, + buf_len: contents.len(), + }; + let mut nread = wasi::fd_pread(file_fd, &[iovec], 0).expect("reading bytes at offset 0"); + assert_eq!(nread, 4, "nread bytes check"); + assert_eq!(contents, &[0u8, 1, 2, 3], "written bytes equal read bytes"); + + let contents = &mut [0u8; 4]; + let iovec = wasi::Iovec { + buf: contents.as_mut_ptr() as *mut _, + buf_len: contents.len(), + }; + nread = wasi::fd_pread(file_fd, &[iovec], 2).expect("reading bytes at offset 2"); + assert_eq!(nread, 2, "nread bytes check"); + assert_eq!(contents, &[2u8, 3, 0, 0], "file cursor was overwritten"); + + let contents = &[1u8, 0]; + let ciovec = wasi::Ciovec { + buf: contents.as_ptr() as *const _, + buf_len: contents.len(), + }; + nwritten = wasi::fd_pwrite(file_fd, &mut [ciovec], 2).expect("writing bytes at offset 2"); + assert_eq!(nwritten, 2, "nwritten bytes check"); + + let contents = &mut [0u8; 4]; + let iovec = wasi::Iovec { + buf: contents.as_mut_ptr() as *mut _, + buf_len: contents.len(), + }; + nread = wasi::fd_pread(file_fd, &[iovec], 0).expect("reading bytes at offset 0"); + assert_eq!(nread, 4, "nread bytes check"); + assert_eq!(contents, &[0u8, 1, 1, 0], "file cursor was overwritten"); + + wasi::fd_close(file_fd).expect("closing a file"); + wasi::path_unlink_file(dir_fd, "file").expect("removing a file"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_file_pread_pwrite(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/file_seek_tell.rs b/crates/test-programs/wasi-tests/src/bin/file_seek_tell.rs new file mode 100644 index 0000000000..59357d3ad8 --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/file_seek_tell.rs @@ -0,0 +1,92 @@ +use more_asserts::assert_gt; +use std::{env, process}; +use wasi_tests::open_scratch_directory; + +unsafe fn test_file_seek_tell(dir_fd: wasi::Fd) { + // Create a file in the scratch directory. + let file_fd = wasi::path_open( + dir_fd, + 0, + "file", + wasi::OFLAGS_CREAT, + wasi::RIGHTS_FD_READ | wasi::RIGHTS_FD_WRITE | wasi::RIGHTS_FD_SEEK | wasi::RIGHTS_FD_TELL, + 0, + 0, + ) + .expect("opening a file"); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + + // Check current offset + let mut offset = wasi::fd_tell(file_fd).expect("getting initial file offset"); + assert_eq!(offset, 0, "current offset should be 0"); + + // Write to file + let buf = &[0u8; 100]; + let iov = wasi::Ciovec { + buf: buf.as_ptr() as *const _, + buf_len: buf.len(), + }; + let nwritten = wasi::fd_write(file_fd, &[iov]).expect("writing to a file"); + assert_eq!(nwritten, 100, "should write 100 bytes to file"); + + // Check current offset + offset = wasi::fd_tell(file_fd).expect("getting file offset after writing"); + assert_eq!(offset, 100, "offset after writing should be 100"); + + // Seek to middle of the file + let mut newoffset = + wasi::fd_seek(file_fd, -50, wasi::WHENCE_CUR).expect("seeking to the middle of a file"); + assert_eq!( + newoffset, 50, + "offset after seeking to the middle should be at 50" + ); + + // Seek to the beginning of the file + newoffset = + wasi::fd_seek(file_fd, 0, wasi::WHENCE_SET).expect("seeking to the beginning of the file"); + assert_eq!( + newoffset, 0, + "offset after seeking to the beginning of the file should be at 0" + ); + + // Seek beyond the file should be possible + wasi::fd_seek(file_fd, 1000, wasi::WHENCE_CUR).expect("seeking beyond the end of the file"); + + // Seek before byte 0 is an error though + assert_eq!( + wasi::fd_seek(file_fd, -2000, wasi::WHENCE_CUR) + .expect_err("seeking before byte 0 should be an error") + .raw_error(), + wasi::ERRNO_INVAL, + "errno should be ERRNO_INVAL", + ); + + wasi::fd_close(file_fd).expect("closing a file"); + wasi::path_unlink_file(dir_fd, "file").expect("deleting a file"); +} +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_file_seek_tell(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/file_unbuffered_write.rs b/crates/test-programs/wasi-tests/src/bin/file_unbuffered_write.rs new file mode 100644 index 0000000000..fd934c1f75 --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/file_unbuffered_write.rs @@ -0,0 +1,77 @@ +use more_asserts::assert_gt; +use std::{env, process}; +use wasi_tests::open_scratch_directory; + +unsafe fn test_file_unbuffered_write(dir_fd: wasi::Fd) { + // Create and open file for reading + let fd_read = wasi::path_open( + dir_fd, + 0, + "file", + wasi::OFLAGS_CREAT, + wasi::RIGHTS_FD_READ, + 0, + 0, + ) + .expect("create and open file for reading"); + assert_gt!( + fd_read, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + + // Open the same file but for writing + let fd_write = wasi::path_open(dir_fd, 0, "file", 0, wasi::RIGHTS_FD_WRITE, 0, 0) + .expect("opening file for writing"); + assert_gt!( + fd_write, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + + // Write to file + let contents = &[1u8]; + let ciovec = wasi::Ciovec { + buf: contents.as_ptr() as *const _, + buf_len: contents.len(), + }; + let nwritten = wasi::fd_write(fd_write, &[ciovec]).expect("writing byte to file"); + assert_eq!(nwritten, 1, "nwritten bytes check"); + + // Read from file + let contents = &mut [0u8; 1]; + let iovec = wasi::Iovec { + buf: contents.as_mut_ptr() as *mut _, + buf_len: contents.len(), + }; + let nread = wasi::fd_read(fd_read, &[iovec]).expect("reading bytes from file"); + assert_eq!(nread, 1, "nread bytes check"); + assert_eq!(contents, &[1u8], "written bytes equal read bytes"); + + // Clean up + wasi::fd_close(fd_write).expect("closing a file"); + wasi::fd_close(fd_read).expect("closing a file"); + wasi::path_unlink_file(dir_fd, "file").expect("removing a file"); +} +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_file_unbuffered_write(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/interesting_paths.rs b/crates/test-programs/wasi-tests/src/bin/interesting_paths.rs new file mode 100644 index 0000000000..e057778281 --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/interesting_paths.rs @@ -0,0 +1,127 @@ +use more_asserts::assert_gt; +use std::{env, process}; +use wasi_tests::{create_file, open_scratch_directory}; + +unsafe fn test_interesting_paths(dir_fd: wasi::Fd, arg: &str) { + // Create a directory in the scratch directory. + wasi::path_create_directory(dir_fd, "dir").expect("creating dir"); + + // Create a directory in the directory we just created. + wasi::path_create_directory(dir_fd, "dir/nested").expect("creating a nested dir"); + + // Create a file in the nested directory. + create_file(dir_fd, "dir/nested/file"); + + // Now open it with an absolute path. + assert_eq!( + wasi::path_open(dir_fd, 0, "/dir/nested/file", 0, 0, 0, 0) + .expect_err("opening a file with an absolute path") + .raw_error(), + wasi::ERRNO_NOTCAPABLE, + "errno should be ERRNO_NOTCAPABLE" + ); + + // Now open it with a path containing "..". + let mut file_fd = wasi::path_open( + dir_fd, + 0, + "dir/.//nested/../../dir/nested/../nested///./file", + 0, + 0, + 0, + 0, + ) + .expect("opening a file with \"..\" in the path"); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + wasi::fd_close(file_fd).expect("closing a file"); + + // Now open it with a trailing NUL. + assert_eq!( + wasi::path_open(dir_fd, 0, "dir/nested/file\0", 0, 0, 0, 0) + .expect_err("opening a file with a trailing NUL") + .raw_error(), + wasi::ERRNO_ILSEQ, + "errno should be ERRNO_ILSEQ", + ); + + // Now open it with a trailing slash. + assert_eq!( + wasi::path_open(dir_fd, 0, "dir/nested/file/", 0, 0, 0, 0) + .expect_err("opening a file with a trailing slash should fail") + .raw_error(), + wasi::ERRNO_NOTDIR, + "errno should be ERRNO_NOTDIR", + ); + + // Now open it with trailing slashes. + assert_eq!( + wasi::path_open(dir_fd, 0, "dir/nested/file///", 0, 0, 0, 0) + .expect_err("opening a file with trailing slashes should fail") + .raw_error(), + wasi::ERRNO_NOTDIR, + "errno should be ERRNO_NOTDIR", + ); + + // Now open the directory with a trailing slash. + file_fd = wasi::path_open(dir_fd, 0, "dir/nested/", 0, 0, 0, 0) + .expect("opening a directory with a trailing slash"); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + wasi::fd_close(file_fd).expect("closing a file"); + + // Now open the directory with trailing slashes. + file_fd = wasi::path_open(dir_fd, 0, "dir/nested///", 0, 0, 0, 0) + .expect("opening a directory with trailing slashes"); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + wasi::fd_close(file_fd).expect("closing a file"); + + // Now open it with a path containing too many ".."s. + let bad_path = format!("dir/nested/../../../{}/dir/nested/file", arg); + assert_eq!( + wasi::path_open(dir_fd, 0, &bad_path, 0, 0, 0, 0) + .expect_err("opening a file with too many \"..\"s in the path should fail") + .raw_error(), + wasi::ERRNO_NOTCAPABLE, + "errno should be ERRNO_NOTCAPABLE", + ); + wasi::path_unlink_file(dir_fd, "dir/nested/file") + .expect("unlink_file on a symlink should succeed"); + wasi::path_remove_directory(dir_fd, "dir/nested") + .expect("remove_directory on a directory should succeed"); + wasi::path_remove_directory(dir_fd, "dir") + .expect("remove_directory on a directory should succeed"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_interesting_paths(dir_fd, &arg) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/isatty.rs b/crates/test-programs/wasi-tests/src/bin/isatty.rs new file mode 100644 index 0000000000..da1ab7f337 --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/isatty.rs @@ -0,0 +1,44 @@ +use more_asserts::assert_gt; +use std::{env, process}; +use wasi_tests::open_scratch_directory; + +unsafe fn test_isatty(dir_fd: wasi::Fd) { + // Create a file in the scratch directory and test if it's a tty. + let file_fd = + wasi::path_open(dir_fd, 0, "file", wasi::OFLAGS_CREAT, 0, 0, 0).expect("opening a file"); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + assert_eq!( + libc::isatty(file_fd as std::os::raw::c_int), + 0, + "file is a tty" + ); + wasi::fd_close(file_fd).expect("closing a file"); + wasi::path_unlink_file(dir_fd, "file").expect("removing a file"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_isatty(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/nofollow_errors.rs b/crates/test-programs/wasi-tests/src/bin/nofollow_errors.rs new file mode 100644 index 0000000000..e0c1797bb2 --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/nofollow_errors.rs @@ -0,0 +1,120 @@ +use libc; +use more_asserts::assert_gt; +use std::{env, process}; +use wasi_tests::open_scratch_directory; + +unsafe fn test_nofollow_errors(dir_fd: wasi::Fd) { + // Create a directory for the symlink to point to. + wasi::path_create_directory(dir_fd, "target").expect("creating a dir"); + + // Create a symlink. + wasi::path_symlink("target", dir_fd, "symlink").expect("creating a symlink"); + + // Try to open it as a directory with O_NOFOLLOW again. + assert_eq!( + wasi::path_open(dir_fd, 0, "symlink", wasi::OFLAGS_DIRECTORY, 0, 0, 0) + .expect_err("opening a directory symlink as a directory should fail") + .raw_error(), + wasi::ERRNO_LOOP, + "errno should be ERRNO_LOOP", + ); + + // Try to open it with just O_NOFOLLOW. + assert_eq!( + wasi::path_open(dir_fd, 0, "symlink", 0, 0, 0, 0) + .expect_err("opening a symlink with O_NOFOLLOW should fail") + .raw_error(), + wasi::ERRNO_LOOP, + "errno should be ERRNO_LOOP", + ); + + // Try to open it as a directory without O_NOFOLLOW. + let file_fd = wasi::path_open( + dir_fd, + wasi::LOOKUPFLAGS_SYMLINK_FOLLOW, + "symlink", + wasi::OFLAGS_DIRECTORY, + 0, + 0, + 0, + ) + .expect("opening a symlink as a directory"); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + wasi::fd_close(file_fd).expect("closing a file"); + + // Replace the target directory with a file. + wasi::path_unlink_file(dir_fd, "symlink").expect("removing a file"); + wasi::path_remove_directory(dir_fd, "target") + .expect("remove_directory on a directory should succeed"); + + let file_fd = + wasi::path_open(dir_fd, 0, "target", wasi::OFLAGS_CREAT, 0, 0, 0).expect("creating a file"); + wasi::fd_close(file_fd).expect("closing a file"); + wasi::path_symlink("target", dir_fd, "symlink").expect("creating a symlink"); + + // Try to open it as a directory with O_NOFOLLOW again. + assert_eq!( + wasi::path_open(dir_fd, 0, "symlink", wasi::OFLAGS_DIRECTORY, 0, 0, 0) + .expect_err("opening a directory symlink as a directory should fail") + .raw_error(), + wasi::ERRNO_LOOP, + "errno should be ERRNO_LOOP", + ); + + // Try to open it with just O_NOFOLLOW. + assert_eq!( + wasi::path_open(dir_fd, 0, "symlink", 0, 0, 0, 0) + .expect_err("opening a symlink with NOFOLLOW should fail") + .raw_error(), + wasi::ERRNO_LOOP, + "errno should be ERRNO_LOOP", + ); + + // Try to open it as a directory without O_NOFOLLOW. + assert_eq!( + wasi::path_open( + dir_fd, + wasi::LOOKUPFLAGS_SYMLINK_FOLLOW, + "symlink", + wasi::OFLAGS_DIRECTORY, + 0, + 0, + 0, + ) + .expect_err("opening a symlink to a file as a directory") + .raw_error(), + wasi::ERRNO_NOTDIR, + "errno should be ERRNO_NOTDIR", + ); + + // Clean up. + wasi::path_unlink_file(dir_fd, "target").expect("removing a file"); + wasi::path_unlink_file(dir_fd, "symlink").expect("removing a file"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_nofollow_errors(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/path_filestat.rs b/crates/test-programs/wasi-tests/src/bin/path_filestat.rs new file mode 100644 index 0000000000..af9d19621a --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/path_filestat.rs @@ -0,0 +1,144 @@ +use more_asserts::assert_gt; +use std::{env, process}; +use wasi_tests::open_scratch_directory; + +unsafe fn test_path_filestat(dir_fd: wasi::Fd) { + let mut fdstat = wasi::fd_fdstat_get(dir_fd).expect("fd_fdstat_get"); + assert_ne!( + fdstat.fs_rights_base & wasi::RIGHTS_PATH_FILESTAT_GET, + 0, + "the scratch directory should have RIGHT_PATH_FILESTAT_GET as base right", + ); + assert_ne!( + fdstat.fs_rights_inheriting & wasi::RIGHTS_PATH_FILESTAT_GET, + 0, + "the scratch directory should have RIGHT_PATH_FILESTAT_GET as base right", + ); + + // Create a file in the scratch directory. + let file_fd = wasi::path_open( + dir_fd, + 0, + "file", + wasi::OFLAGS_CREAT, + wasi::RIGHTS_FD_READ | wasi::RIGHTS_FD_WRITE | wasi::RIGHTS_PATH_FILESTAT_GET, + 0, + // Pass some flags for later retrieval + wasi::FDFLAGS_APPEND | wasi::FDFLAGS_SYNC, + ) + .expect("opening a file"); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + + fdstat = wasi::fd_fdstat_get(file_fd).expect("fd_fdstat_get"); + assert_eq!( + fdstat.fs_rights_base & wasi::RIGHTS_PATH_FILESTAT_GET, + 0, + "files shouldn't have rights for path_* syscalls even if manually given", + ); + assert_eq!( + fdstat.fs_rights_inheriting & wasi::RIGHTS_PATH_FILESTAT_GET, + 0, + "files shouldn't have rights for path_* syscalls even if manually given", + ); + assert_ne!( + fdstat.fs_flags & (wasi::FDFLAGS_APPEND | wasi::FDFLAGS_SYNC), + 0, + "file should have the same flags used to create the file" + ); + + // Check file size + let mut stat = wasi::path_filestat_get(dir_fd, 0, "file").expect("reading file stats"); + assert_eq!(stat.size, 0, "file size should be 0"); + + // Check path_filestat_set_times + let old_atim = stat.atim; + let new_mtim = stat.mtim - 100; + wasi::path_filestat_set_times( + dir_fd, + 0, + "file", + // on purpose: the syscall should not touch atim, because + // neither of the ATIM flags is set + new_mtim, + new_mtim, + wasi::FSTFLAGS_MTIM, + ) + .expect("path_filestat_set_times should succeed"); + + stat = wasi::path_filestat_get(dir_fd, 0, "file") + .expect("reading file stats after path_filestat_set_times"); + assert_eq!(stat.mtim, new_mtim, "mtim should change"); + assert_eq!(stat.atim, old_atim, "atim should not change"); + + assert_eq!( + wasi::path_filestat_set_times( + dir_fd, + 0, + "file", + new_mtim, + new_mtim, + wasi::FSTFLAGS_MTIM | wasi::FSTFLAGS_MTIM_NOW, + ) + .expect_err("MTIM and MTIM_NOW can't both be set") + .raw_error(), + wasi::ERRNO_INVAL, + "errno should be ERRNO_INVAL" + ); + + // check if the times were untouched + stat = wasi::path_filestat_get(dir_fd, 0, "file") + .expect("reading file stats after ERRNO_INVAL fd_filestat_set_times"); + assert_eq!(stat.mtim, new_mtim, "mtim should not change"); + assert_eq!(stat.atim, old_atim, "atim should not change"); + + let new_atim = old_atim - 100; + assert_eq!( + wasi::path_filestat_set_times( + dir_fd, + 0, + "file", + new_atim, + new_atim, + wasi::FSTFLAGS_ATIM | wasi::FSTFLAGS_ATIM_NOW, + ) + .expect_err("ATIM & ATIM_NOW can't both be set") + .raw_error(), + wasi::ERRNO_INVAL, + "errno should be ERRNO_INVAL" + ); + + // check if the times were untouched + stat = wasi::path_filestat_get(dir_fd, 0, "file") + .expect("reading file stats after ERRNO_INVAL path_filestat_set_times"); + assert_eq!(stat.mtim, new_mtim, "mtim should not change"); + assert_eq!(stat.atim, old_atim, "atim should not change"); + + wasi::fd_close(file_fd).expect("closing a file"); + wasi::path_unlink_file(dir_fd, "file").expect("removing a file"); +} +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_path_filestat(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/path_link.rs b/crates/test-programs/wasi-tests/src/bin/path_link.rs new file mode 100644 index 0000000000..3e005012fd --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/path_link.rs @@ -0,0 +1,264 @@ +use more_asserts::assert_gt; +use std::{env, process}; +use wasi_tests::{create_file, open_scratch_directory}; + +const TEST_RIGHTS: wasi::Rights = wasi::RIGHTS_FD_READ + | wasi::RIGHTS_PATH_LINK_SOURCE + | wasi::RIGHTS_PATH_LINK_TARGET + | wasi::RIGHTS_FD_FILESTAT_GET + | wasi::RIGHTS_PATH_OPEN + | wasi::RIGHTS_PATH_UNLINK_FILE; + +unsafe fn create_or_open(dir_fd: wasi::Fd, name: &str, flags: wasi::Oflags) -> wasi::Fd { + let file_fd = wasi::path_open(dir_fd, 0, name, flags, TEST_RIGHTS, TEST_RIGHTS, 0) + .unwrap_or_else(|_| panic!("opening '{}'", name)); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + file_fd +} + +unsafe fn open_link(dir_fd: wasi::Fd, name: &str) -> wasi::Fd { + let file_fd = wasi::path_open(dir_fd, 0, name, 0, TEST_RIGHTS, TEST_RIGHTS, 0) + .unwrap_or_else(|_| panic!("opening a link '{}'", name)); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + file_fd +} + +// This is temporary until `wasi` implements `Debug` and `PartialEq` for +// `wasi::Filestat`. +fn filestats_assert_eq(left: wasi::Filestat, right: wasi::Filestat) { + assert_eq!(left.dev, right.dev, "dev should be equal"); + assert_eq!(left.ino, right.ino, "ino should be equal"); + assert_eq!(left.atim, right.atim, "atim should be equal"); + assert_eq!(left.ctim, right.ctim, "ctim should be equal"); + assert_eq!(left.mtim, right.mtim, "mtim should be equal"); + assert_eq!(left.size, right.size, "size should be equal"); + assert_eq!(left.nlink, right.nlink, "nlink should be equal"); + assert_eq!(left.filetype, right.filetype, "filetype should be equal"); +} + +// This is temporary until `wasi` implements `Debug` and `PartialEq` for +// `wasi::Fdstat`. +fn fdstats_assert_eq(left: wasi::Fdstat, right: wasi::Fdstat) { + assert_eq!(left.fs_flags, right.fs_flags, "fs_flags should be equal"); + assert_eq!( + left.fs_filetype, right.fs_filetype, + "fs_filetype should be equal" + ); + assert_eq!( + left.fs_rights_base, right.fs_rights_base, + "fs_rights_base should be equal" + ); + assert_eq!( + left.fs_rights_inheriting, right.fs_rights_inheriting, + "fs_rights_inheriting should be equal" + ); +} + +unsafe fn check_rights(orig_fd: wasi::Fd, link_fd: wasi::Fd) { + // Compare Filestats + let orig_filestat = wasi::fd_filestat_get(orig_fd).expect("reading filestat of the source"); + let link_filestat = wasi::fd_filestat_get(link_fd).expect("reading filestat of the link"); + filestats_assert_eq(orig_filestat, link_filestat); + + // Compare Fdstats + let orig_fdstat = wasi::fd_fdstat_get(orig_fd).expect("reading fdstat of the source"); + let link_fdstat = wasi::fd_fdstat_get(link_fd).expect("reading fdstat of the link"); + fdstats_assert_eq(orig_fdstat, link_fdstat); +} + +unsafe fn test_path_link(dir_fd: wasi::Fd) { + // Create a file + let file_fd = create_or_open(dir_fd, "file", wasi::OFLAGS_CREAT); + + // Create a link in the same directory and compare rights + wasi::path_link(dir_fd, 0, "file", dir_fd, "link") + .expect("creating a link in the same directory"); + let mut link_fd = open_link(dir_fd, "link"); + check_rights(file_fd, link_fd); + wasi::path_unlink_file(dir_fd, "link").expect("removing a link"); + + // Create a link in a different directory and compare rights + wasi::path_create_directory(dir_fd, "subdir").expect("creating a subdirectory"); + let subdir_fd = create_or_open(dir_fd, "subdir", wasi::OFLAGS_DIRECTORY); + wasi::path_link(dir_fd, 0, "file", subdir_fd, "link").expect("creating a link in subdirectory"); + link_fd = open_link(subdir_fd, "link"); + check_rights(file_fd, link_fd); + wasi::path_unlink_file(subdir_fd, "link").expect("removing a link"); + wasi::path_remove_directory(dir_fd, "subdir").expect("removing a subdirectory"); + + // Create a link to a path that already exists + create_file(dir_fd, "link"); + + assert_eq!( + wasi::path_link(dir_fd, 0, "file", dir_fd, "link") + .expect_err("creating a link to existing path should fail") + .raw_error(), + wasi::ERRNO_EXIST, + "errno should be ERRNO_EXIST" + ); + wasi::path_unlink_file(dir_fd, "link").expect("removing a file"); + + // Create a link to itself + assert_eq!( + wasi::path_link(dir_fd, 0, "file", dir_fd, "file") + .expect_err("creating a link to itself should fail") + .raw_error(), + wasi::ERRNO_EXIST, + "errno should be ERRNO_EXIST" + ); + + // Create a link where target is a directory + wasi::path_create_directory(dir_fd, "link").expect("creating a dir"); + + assert_eq!( + wasi::path_link(dir_fd, 0, "file", dir_fd, "link") + .expect_err("creating a link where target is a directory should fail") + .raw_error(), + wasi::ERRNO_EXIST, + "errno should be ERRNO_EXIST" + ); + wasi::path_remove_directory(dir_fd, "link").expect("removing a dir"); + + // Create a link to a directory + wasi::path_create_directory(dir_fd, "subdir").expect("creating a subdirectory"); + create_or_open(dir_fd, "subdir", wasi::OFLAGS_DIRECTORY); + + assert_eq!( + wasi::path_link(dir_fd, 0, "subdir", dir_fd, "link") + .expect_err("creating a link to a directory should fail") + .raw_error(), + wasi::ERRNO_PERM, + "errno should be ERRNO_PERM" + ); + wasi::path_remove_directory(dir_fd, "subdir").expect("removing a subdirectory"); + + // Create a link to a file with trailing slash + assert_eq!( + wasi::path_link(dir_fd, 0, "file", dir_fd, "link/") + .expect_err("creating a link to a file with trailing slash should fail") + .raw_error(), + wasi::ERRNO_NOENT, + "errno should be ERRNO_NOENT" + ); + + // Create a link to a dangling symlink + wasi::path_symlink("target", dir_fd, "symlink").expect("creating a dangling symlink"); + + assert_eq!( + wasi::path_link(dir_fd, 0, "symlink", dir_fd, "link") + .expect_err("creating a link to a dangling symlink should fail") + .raw_error(), + wasi::ERRNO_NOENT, + "errno should be ERRNO_NOENT" + ); + wasi::path_unlink_file(dir_fd, "symlink").expect("removing a symlink"); + + // Create a link to a symlink loop + wasi::path_symlink("symlink", dir_fd, "symlink").expect("creating a symlink loop"); + + assert_eq!( + wasi::path_link(dir_fd, 0, "symlink", dir_fd, "link") + .expect_err("creating a link to a symlink loop") + .raw_error(), + wasi::ERRNO_LOOP, + "errno should be ERRNO_LOOP" + ); + wasi::path_unlink_file(dir_fd, "symlink").expect("removing a symlink"); + + // Create a link where target is a dangling symlink + wasi::path_symlink("target", dir_fd, "symlink").expect("creating a dangling symlink"); + + assert_eq!( + wasi::path_link(dir_fd, 0, "file", dir_fd, "symlink") + .expect_err("creating a link where target is a dangling symlink") + .raw_error(), + wasi::ERRNO_EXIST, + "errno should be ERRNO_EXIST" + ); + wasi::path_unlink_file(dir_fd, "symlink").expect("removing a symlink"); + + // Create a link to a file following symlinks + wasi::path_symlink("file", dir_fd, "symlink").expect("creating a valid symlink"); + wasi::path_link( + dir_fd, + wasi::LOOKUPFLAGS_SYMLINK_FOLLOW, + "symlink", + dir_fd, + "link", + ) + .expect("creating a link to a file following symlinks"); + link_fd = open_link(dir_fd, "link"); + check_rights(file_fd, link_fd); + wasi::path_unlink_file(dir_fd, "link").expect("removing a link"); + wasi::path_unlink_file(dir_fd, "symlink").expect("removing a symlink"); + + // Create a link where target is a dangling symlink following symlinks + wasi::path_symlink("target", dir_fd, "symlink").expect("creating a dangling symlink"); + + assert_eq!( + wasi::path_link( + dir_fd, + wasi::LOOKUPFLAGS_SYMLINK_FOLLOW, + "symlink", + dir_fd, + "link", + ) + .expect_err("creating a link where target is a dangling symlink following symlinks") + .raw_error(), + wasi::ERRNO_NOENT, + "errno should be ERRNO_NOENT" + ); + wasi::path_unlink_file(dir_fd, "symlink").expect("removing a symlink"); + + // Create a link to a symlink loop following symlinks + wasi::path_symlink("symlink", dir_fd, "symlink").expect("creating a symlink loop"); + + assert_eq!( + wasi::path_link( + dir_fd, + wasi::LOOKUPFLAGS_SYMLINK_FOLLOW, + "symlink", + dir_fd, + "link", + ) + .expect_err("creating a link to a symlink loop following symlinks") + .raw_error(), + wasi::ERRNO_LOOP, + "errno should be ERRNO_LOOP" + ); + wasi::path_unlink_file(dir_fd, "symlink").expect("removing a symlink"); + + // Clean up. + wasi::path_unlink_file(dir_fd, "file").expect("removing a file"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_path_link(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/path_open_create_existing.rs b/crates/test-programs/wasi-tests/src/bin/path_open_create_existing.rs new file mode 100644 index 0000000000..11e7fc4c99 --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/path_open_create_existing.rs @@ -0,0 +1,45 @@ +use std::{env, process}; +use wasi_tests::{create_file, open_scratch_directory}; + +unsafe fn test_path_open_create_existing(dir_fd: wasi::Fd) { + create_file(dir_fd, "file"); + assert_eq!( + wasi::path_open( + dir_fd, + 0, + "file", + wasi::OFLAGS_CREAT | wasi::OFLAGS_EXCL, + 0, + 0, + 0, + ) + .expect_err("trying to create a file that already exists") + .raw_error(), + wasi::ERRNO_EXIST, + "errno should be ERRNO_EXIST" + ); + wasi::path_unlink_file(dir_fd, "file").expect("removing a file"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_path_open_create_existing(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/path_open_dirfd_not_dir.rs b/crates/test-programs/wasi-tests/src/bin/path_open_dirfd_not_dir.rs new file mode 100644 index 0000000000..dd6e502361 --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/path_open_dirfd_not_dir.rs @@ -0,0 +1,40 @@ +use std::{env, process}; +use wasi_tests::open_scratch_directory; + +unsafe fn test_dirfd_not_dir(dir_fd: wasi::Fd) { + // Open a file. + let file_fd = + wasi::path_open(dir_fd, 0, "file", wasi::OFLAGS_CREAT, 0, 0, 0).expect("opening a file"); + // Now try to open a file underneath it as if it were a directory. + assert_eq!( + wasi::path_open(file_fd, 0, "foo", wasi::OFLAGS_CREAT, 0, 0, 0) + .expect_err("non-directory base fd should get ERRNO_NOTDIR") + .raw_error(), + wasi::ERRNO_NOTDIR, + "errno should be ERRNO_NOTDIR" + ); + wasi::fd_close(file_fd).expect("closing a file"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_dirfd_not_dir(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/path_open_read_without_rights.rs b/crates/test-programs/wasi-tests/src/bin/path_open_read_without_rights.rs new file mode 100644 index 0000000000..26113c08c8 --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/path_open_read_without_rights.rs @@ -0,0 +1,79 @@ +use std::{env, process}; +use wasi_tests::open_scratch_directory; +use wasi_tests::{create_file, drop_rights, fd_get_rights}; + +const TEST_FILENAME: &'static str = "file"; + +unsafe fn try_read_file(dir_fd: wasi::Fd) { + let fd = wasi::path_open(dir_fd, 0, TEST_FILENAME, 0, 0, 0, 0).expect("opening the file"); + + // Check that we don't have the right to exeucute fd_read + let (rbase, rinher) = fd_get_rights(fd); + assert_eq!( + rbase & wasi::RIGHTS_FD_READ, + 0, + "should not have base RIGHTS_FD_READ" + ); + assert_eq!( + rinher & wasi::RIGHTS_FD_READ, + 0, + "should not have inheriting RIGHTS_FD_READ" + ); + + let contents = &mut [0u8; 1]; + let iovec = wasi::Iovec { + buf: contents.as_mut_ptr() as *mut _, + buf_len: contents.len(), + }; + // Since we no longer have the right to fd_read, trying to read a file + // should be an error. + assert_eq!( + wasi::fd_read(fd, &[iovec]) + .expect_err("reading bytes from file should fail") + .raw_error(), + wasi::ERRNO_NOTCAPABLE, + "the errno should be ENOTCAPABLE" + ); +} + +unsafe fn test_read_rights(dir_fd: wasi::Fd) { + create_file(dir_fd, TEST_FILENAME); + drop_rights(dir_fd, wasi::RIGHTS_FD_READ, wasi::RIGHTS_FD_READ); + + let (rbase, rinher) = fd_get_rights(dir_fd); + assert_eq!( + rbase & wasi::RIGHTS_FD_READ, + 0, + "dir should not have base RIGHTS_FD_READ" + ); + assert_eq!( + rinher & wasi::RIGHTS_FD_READ, + 0, + "dir should not have inheriting RIGHTS_FD_READ" + ); + + try_read_file(dir_fd); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_read_rights(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/path_rename.rs b/crates/test-programs/wasi-tests/src/bin/path_rename.rs new file mode 100644 index 0000000000..1acd276f08 --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/path_rename.rs @@ -0,0 +1,174 @@ +use more_asserts::assert_gt; +use std::{env, process}; +use wasi_tests::{create_file, open_scratch_directory}; + +unsafe fn test_path_rename(dir_fd: wasi::Fd) { + // First, try renaming a dir to nonexistent path + // Create source directory + wasi::path_create_directory(dir_fd, "source").expect("creating a directory"); + + // Try renaming the directory + wasi::path_rename(dir_fd, "source", dir_fd, "target").expect("renaming a directory"); + + // Check that source directory doesn't exist anymore + assert_eq!( + wasi::path_open(dir_fd, 0, "source", wasi::OFLAGS_DIRECTORY, 0, 0, 0) + .expect_err("opening a nonexistent path as a directory should fail") + .raw_error(), + wasi::ERRNO_NOENT, + "errno should be ERRNO_NOENT" + ); + + // Check that target directory exists + let mut fd = wasi::path_open(dir_fd, 0, "target", wasi::OFLAGS_DIRECTORY, 0, 0, 0) + .expect("opening renamed path as a directory"); + assert_gt!( + fd, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + + wasi::fd_close(fd).expect("closing a file"); + wasi::path_remove_directory(dir_fd, "target").expect("removing a directory"); + + // Now, try renaming renaming a dir to existing empty dir + wasi::path_create_directory(dir_fd, "source").expect("creating a directory"); + wasi::path_create_directory(dir_fd, "target").expect("creating a directory"); + wasi::path_rename(dir_fd, "source", dir_fd, "target").expect("renaming a directory"); + + // Check that source directory doesn't exist anymore + assert_eq!( + wasi::path_open(dir_fd, 0, "source", wasi::OFLAGS_DIRECTORY, 0, 0, 0) + .expect_err("opening a nonexistent path as a directory") + .raw_error(), + wasi::ERRNO_NOENT, + "errno should be ERRNO_NOENT" + ); + + // Check that target directory exists + fd = wasi::path_open(dir_fd, 0, "target", wasi::OFLAGS_DIRECTORY, 0, 0, 0) + .expect("opening renamed path as a directory"); + assert_gt!( + fd, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + + wasi::fd_close(fd).expect("closing a file"); + wasi::path_remove_directory(dir_fd, "target").expect("removing a directory"); + + // Now, try renaming a dir to existing non-empty dir + wasi::path_create_directory(dir_fd, "source").expect("creating a directory"); + wasi::path_create_directory(dir_fd, "target").expect("creating a directory"); + create_file(dir_fd, "target/file"); + + assert_eq!( + wasi::path_rename(dir_fd, "source", dir_fd, "target") + .expect_err("renaming directory to a nonempty directory") + .raw_error(), + wasi::ERRNO_NOTEMPTY, + "errno should be ERRNO_NOTEMPTY" + ); + + // Try renaming dir to a file + assert_eq!( + wasi::path_rename(dir_fd, "source", dir_fd, "target/file") + .expect_err("renaming a directory to a file") + .raw_error(), + wasi::ERRNO_NOTDIR, + "errno should be ERRNO_NOTDIR" + ); + + wasi::path_unlink_file(dir_fd, "target/file").expect("removing a file"); + wasi::path_remove_directory(dir_fd, "target").expect("removing a directory"); + wasi::path_remove_directory(dir_fd, "source").expect("removing a directory"); + + // Now, try renaming a file to a nonexistent path + create_file(dir_fd, "source"); + wasi::path_rename(dir_fd, "source", dir_fd, "target").expect("renaming a file"); + + // Check that source file doesn't exist anymore + assert_eq!( + wasi::path_open(dir_fd, 0, "source", 0, 0, 0, 0) + .expect_err("opening a nonexistent path should fail") + .raw_error(), + wasi::ERRNO_NOENT, + "errno should be ERRNO_NOENT" + ); + + // Check that target file exists + fd = wasi::path_open(dir_fd, 0, "target", 0, 0, 0, 0).expect("opening renamed path"); + assert_gt!( + fd, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + + wasi::fd_close(fd).expect("closing a file"); + wasi::path_unlink_file(dir_fd, "target").expect("removing a file"); + + // Now, try renaming file to an existing file + create_file(dir_fd, "source"); + create_file(dir_fd, "target"); + + wasi::path_rename(dir_fd, "source", dir_fd, "target") + .expect("renaming file to another existing file"); + + // Check that source file doesn't exist anymore + assert_eq!( + wasi::path_open(dir_fd, 0, "source", 0, 0, 0, 0) + .expect_err("opening a nonexistent path") + .raw_error(), + wasi::ERRNO_NOENT, + "errno should be ERRNO_NOENT" + ); + + // Check that target file exists + fd = wasi::path_open(dir_fd, 0, "target", 0, 0, 0, 0).expect("opening renamed path"); + assert_gt!( + fd, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + + wasi::fd_close(fd).expect("closing a file"); + wasi::path_unlink_file(dir_fd, "target").expect("removing a file"); + + // Try renaming to an (empty) directory instead + create_file(dir_fd, "source"); + wasi::path_create_directory(dir_fd, "target").expect("creating a directory"); + + assert_eq!( + wasi::path_rename(dir_fd, "source", dir_fd, "target") + .expect_err("renaming a file to existing directory should fail") + .raw_error(), + wasi::ERRNO_ISDIR, + "errno should be ERRNO_ISDIR" + ); + + wasi::path_remove_directory(dir_fd, "target").expect("removing a directory"); + wasi::path_unlink_file(dir_fd, "source").expect("removing a file"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_path_rename(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/path_rename_trailing_slashes.rs b/crates/test-programs/wasi-tests/src/bin/path_rename_trailing_slashes.rs new file mode 100644 index 0000000000..2ba1699bbb --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/path_rename_trailing_slashes.rs @@ -0,0 +1,63 @@ +use std::{env, process}; +use wasi_tests::{create_file, open_scratch_directory}; + +unsafe fn test_path_rename_trailing_slashes(dir_fd: wasi::Fd) { + // Test renaming a file with a trailing slash in the name. + create_file(dir_fd, "source"); + + assert_eq!( + wasi::path_rename(dir_fd, "source/", dir_fd, "target") + .expect_err("renaming a file with a trailing slash in the source name should fail") + .raw_error(), + wasi::ERRNO_NOTDIR, + "errno should be ERRNO_NOTDIR" + ); + assert_eq!( + wasi::path_rename(dir_fd, "source", dir_fd, "target/") + .expect_err("renaming a file with a trailing slash in the destination name should fail") + .raw_error(), + wasi::ERRNO_NOTDIR, + "errno should be ERRNO_NOTDIR" + ); + assert_eq!( + wasi::path_rename(dir_fd, "source/", dir_fd, "target/") + .expect_err("renaming a file with a trailing slash in the source and destination names should fail") + .raw_error(), + wasi::ERRNO_NOTDIR, + "errno should be ERRNO_NOTDIR" + ); + wasi::path_unlink_file(dir_fd, "source").expect("removing a file"); + + // Test renaming a directory with a trailing slash in the name. + wasi::path_create_directory(dir_fd, "source").expect("creating a directory"); + wasi::path_rename(dir_fd, "source/", dir_fd, "target") + .expect("renaming a directory with a trailing slash in the source name"); + wasi::path_rename(dir_fd, "target", dir_fd, "source/") + .expect("renaming a directory with a trailing slash in the destination name"); + wasi::path_rename(dir_fd, "source/", dir_fd, "target/") + .expect("renaming a directory with a trailing slash in the source and destination names"); + wasi::path_remove_directory(dir_fd, "target").expect("removing a directory"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_path_rename_trailing_slashes(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/path_symlink_trailing_slashes.rs b/crates/test-programs/wasi-tests/src/bin/path_symlink_trailing_slashes.rs new file mode 100644 index 0000000000..e8e1d7fb2f --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/path_symlink_trailing_slashes.rs @@ -0,0 +1,86 @@ +use std::{env, process}; +use wasi_tests::{create_file, open_scratch_directory}; + +unsafe fn test_path_symlink_trailing_slashes(dir_fd: wasi::Fd) { + // Link destination shouldn't end with a slash. + assert_eq!( + wasi::path_symlink("source", dir_fd, "target/") + .expect_err("link destination ending with a slash should fail") + .raw_error(), + wasi::ERRNO_NOENT, + "errno should be ERRNO_NOENT" + ); + + // Without the trailing slash, this should succeed. + wasi::path_symlink("source", dir_fd, "target").expect("link destination ending with a slash"); + wasi::path_unlink_file(dir_fd, "target").expect("removing a file"); + + // Link destination already exists, target has trailing slash. + wasi::path_create_directory(dir_fd, "target").expect("creating a directory"); + assert_eq!( + wasi::path_symlink("source", dir_fd, "target/") + .expect_err("link destination already exists") + .raw_error(), + wasi::ERRNO_EXIST, + "errno should be ERRNO_EXIST" + ); + wasi::path_remove_directory(dir_fd, "target").expect("removing a directory"); + + // Link destination already exists, target has no trailing slash. + wasi::path_create_directory(dir_fd, "target").expect("creating a directory"); + assert_eq!( + wasi::path_symlink("source", dir_fd, "target") + .expect_err("link destination already exists") + .raw_error(), + wasi::ERRNO_EXIST, + "errno should be ERRNO_EXIST" + ); + wasi::path_remove_directory(dir_fd, "target").expect("removing a directory"); + + // Link destination already exists, target has trailing slash. + create_file(dir_fd, "target"); + + assert_eq!( + wasi::path_symlink("source", dir_fd, "target/") + .expect_err("link destination already exists") + .raw_error(), + wasi::ERRNO_EXIST, + "errno should be ERRNO_EXIST" + ); + wasi::path_unlink_file(dir_fd, "target").expect("removing a file"); + + // Link destination already exists, target has no trailing slash. + create_file(dir_fd, "target"); + + assert_eq!( + wasi::path_symlink("source", dir_fd, "target") + .expect_err("link destination already exists") + .raw_error(), + wasi::ERRNO_EXIST, + "errno should be ERRNO_EXIST" + ); + wasi::path_unlink_file(dir_fd, "target").expect("removing a file"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_path_symlink_trailing_slashes(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/poll_oneoff.rs b/crates/test-programs/wasi-tests/src/bin/poll_oneoff.rs new file mode 100644 index 0000000000..d049c1b9fe --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/poll_oneoff.rs @@ -0,0 +1,265 @@ +use more_asserts::assert_gt; +use std::{env, mem::MaybeUninit, process}; +use wasi_tests::{open_scratch_directory, STDERR_FD, STDIN_FD, STDOUT_FD}; + +const CLOCK_ID: wasi::Userdata = 0x0123_45678; + +unsafe fn poll_oneoff_impl(r#in: &[wasi::Subscription], nexpected: usize) -> Vec { + let mut out: Vec = Vec::new(); + out.resize_with(r#in.len(), || { + MaybeUninit::::zeroed().assume_init() + }); + let size = wasi::poll_oneoff(r#in.as_ptr(), out.as_mut_ptr(), r#in.len()) + .expect("poll_oneoff should succeed"); + assert_eq!( + size, nexpected, + "poll_oneoff should return {} events", + nexpected + ); + out +} + +unsafe fn test_empty_poll() { + let r#in = []; + let mut out: Vec = Vec::new(); + let error = wasi::poll_oneoff(r#in.as_ptr(), out.as_mut_ptr(), r#in.len()) + .expect_err("empty poll_oneoff should fail"); + assert_eq!( + error.raw_error(), + wasi::ERRNO_INVAL, + "error should be EINVAL" + ); +} + +unsafe fn test_timeout() { + let clock = wasi::SubscriptionClock { + id: wasi::CLOCKID_MONOTONIC, + timeout: 5_000_000u64, // 5 milliseconds + precision: 0, + flags: 0, + }; + let r#in = [wasi::Subscription { + userdata: CLOCK_ID, + r#type: wasi::EVENTTYPE_CLOCK, + u: wasi::SubscriptionU { clock }, + }]; + let out = poll_oneoff_impl(&r#in, 1); + let event = &out[0]; + assert_eq!( + event.error, + wasi::ERRNO_SUCCESS, + "the event.error should be set to ESUCCESS" + ); + assert_eq!( + event.r#type, + wasi::EVENTTYPE_CLOCK, + "the event.type should equal clock" + ); + assert_eq!( + event.userdata, CLOCK_ID, + "the event.userdata should contain clock_id specified by the user" + ); +} + +unsafe fn test_stdin_read() { + let clock = wasi::SubscriptionClock { + id: wasi::CLOCKID_MONOTONIC, + timeout: 5_000_000u64, // 5 milliseconds + precision: 0, + flags: 0, + }; + let fd_readwrite = wasi::SubscriptionFdReadwrite { + file_descriptor: STDIN_FD, + }; + let r#in = [ + wasi::Subscription { + userdata: CLOCK_ID, + r#type: wasi::EVENTTYPE_CLOCK, + u: wasi::SubscriptionU { clock }, + }, + // Make sure that timeout is returned only once even if there are multiple read events + wasi::Subscription { + userdata: 1, + r#type: wasi::EVENTTYPE_FD_READ, + u: wasi::SubscriptionU { fd_readwrite }, + }, + ]; + let out = poll_oneoff_impl(&r#in, 1); + let event = &out[0]; + assert_eq!( + event.error, + wasi::ERRNO_SUCCESS, + "the event.error should be set to ESUCCESS" + ); + assert_eq!( + event.r#type, + wasi::EVENTTYPE_CLOCK, + "the event.type should equal clock" + ); + assert_eq!( + event.userdata, CLOCK_ID, + "the event.userdata should contain clock_id specified by the user" + ); +} + +unsafe fn test_stdout_stderr_write() { + let stdout_readwrite = wasi::SubscriptionFdReadwrite { + file_descriptor: STDOUT_FD, + }; + let stderr_readwrite = wasi::SubscriptionFdReadwrite { + file_descriptor: STDERR_FD, + }; + let r#in = [ + wasi::Subscription { + userdata: 1, + r#type: wasi::EVENTTYPE_FD_WRITE, + u: wasi::SubscriptionU { + fd_readwrite: stdout_readwrite, + }, + }, + wasi::Subscription { + userdata: 2, + r#type: wasi::EVENTTYPE_FD_WRITE, + u: wasi::SubscriptionU { + fd_readwrite: stderr_readwrite, + }, + }, + ]; + let out = poll_oneoff_impl(&r#in, 2); + assert_eq!( + out[0].userdata, 1, + "the event.userdata should contain fd userdata specified by the user" + ); + assert_eq!( + out[0].error, + wasi::ERRNO_SUCCESS, + "the event.error should be set to ERRNO_SUCCESS", + ); + assert_eq!( + out[0].r#type, + wasi::EVENTTYPE_FD_WRITE, + "the event.type should equal FD_WRITE" + ); + assert_eq!( + out[1].userdata, 2, + "the event.userdata should contain fd userdata specified by the user" + ); + assert_eq!( + out[1].error, + wasi::ERRNO_SUCCESS, + "the event.error should be set to ERRNO_SUCCESS", + ); + assert_eq!( + out[1].r#type, + wasi::EVENTTYPE_FD_WRITE, + "the event.type should equal FD_WRITE" + ); +} + +unsafe fn test_fd_readwrite(fd: wasi::Fd, error_code: wasi::Errno) { + let fd_readwrite = wasi::SubscriptionFdReadwrite { + file_descriptor: fd, + }; + let r#in = [ + wasi::Subscription { + userdata: 1, + r#type: wasi::EVENTTYPE_FD_READ, + u: wasi::SubscriptionU { fd_readwrite }, + }, + wasi::Subscription { + userdata: 2, + r#type: wasi::EVENTTYPE_FD_WRITE, + u: wasi::SubscriptionU { fd_readwrite }, + }, + ]; + let out = poll_oneoff_impl(&r#in, 2); + assert_eq!( + out[0].userdata, 1, + "the event.userdata should contain fd userdata specified by the user" + ); + assert_eq!( + out[0].error, error_code, + "the event.error should be set to {}", + error_code + ); + assert_eq!( + out[0].r#type, + wasi::EVENTTYPE_FD_READ, + "the event.type_ should equal FD_READ" + ); + assert_eq!( + out[1].userdata, 2, + "the event.userdata should contain fd userdata specified by the user" + ); + assert_eq!( + out[1].error, error_code, + "the event.error should be set to {}", + error_code + ); + assert_eq!( + out[1].r#type, + wasi::EVENTTYPE_FD_WRITE, + "the event.type_ should equal FD_WRITE" + ); +} + +unsafe fn test_fd_readwrite_valid_fd(dir_fd: wasi::Fd) { + // Create a file in the scratch directory. + let file_fd = wasi::path_open( + dir_fd, + 0, + "file", + wasi::OFLAGS_CREAT, + wasi::RIGHTS_FD_READ | wasi::RIGHTS_FD_WRITE | wasi::RIGHTS_POLL_FD_READWRITE, + 0, + 0, + ) + .expect("opening a file"); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + + test_fd_readwrite(file_fd, wasi::ERRNO_SUCCESS); + + wasi::fd_close(file_fd).expect("closing a file"); + wasi::path_unlink_file(dir_fd, "file").expect("removing a file"); +} + +unsafe fn test_fd_readwrite_invalid_fd() { + test_fd_readwrite(wasi::Fd::max_value(), wasi::ERRNO_BADF) +} + +unsafe fn test_poll_oneoff(dir_fd: wasi::Fd) { + test_timeout(); + test_empty_poll(); + // NB we assume that stdin/stdout/stderr are valid and open + // for the duration of the test case + test_stdin_read(); + test_stdout_stderr_write(); + test_fd_readwrite_valid_fd(dir_fd); + test_fd_readwrite_invalid_fd(); +} +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_poll_oneoff(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/readlink.rs b/crates/test-programs/wasi-tests/src/bin/readlink.rs new file mode 100644 index 0000000000..2257a89ddc --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/readlink.rs @@ -0,0 +1,56 @@ +use std::{env, process}; +use wasi_tests::{create_file, open_scratch_directory}; + +unsafe fn test_readlink(dir_fd: wasi::Fd) { + // Create a file in the scratch directory. + create_file(dir_fd, "target"); + + // Create a symlink + wasi::path_symlink("target", dir_fd, "symlink").expect("creating a symlink"); + + // Read link into the buffer + let buf = &mut [0u8; 10]; + let mut bufused = wasi::path_readlink(dir_fd, "symlink", buf.as_mut_ptr(), buf.len()) + .expect("readlink should succeed"); + assert_eq!(bufused, 6, "should use 6 bytes of the buffer"); + assert_eq!(&buf[..6], b"target", "buffer should contain 'target'"); + assert_eq!( + &buf[6..], + &[0u8; 4], + "the remaining bytes should be untouched" + ); + + // Read link into smaller buffer than the actual link's length + let buf = &mut [0u8; 4]; + bufused = wasi::path_readlink(dir_fd, "symlink", buf.as_mut_ptr(), buf.len()) + .expect("readlink should succeed"); + assert_eq!(bufused, 4, "should use all 4 bytes of the buffer"); + assert_eq!(buf, b"targ", "buffer should contain 'targ'"); + + // Clean up. + wasi::path_unlink_file(dir_fd, "target").expect("removing a file"); + wasi::path_unlink_file(dir_fd, "symlink").expect("removing a file"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_readlink(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/readlink_no_buffer.rs b/crates/test-programs/wasi-tests/src/bin/readlink_no_buffer.rs new file mode 100644 index 0000000000..b8a677b2b5 --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/readlink_no_buffer.rs @@ -0,0 +1,41 @@ +use std::{env, process}; +use wasi_tests::open_scratch_directory; + +unsafe fn test_readlink_no_buffer(dir_fd: wasi::Fd) { + // First create a dangling symlink. + wasi::path_symlink("target", dir_fd, "symlink").expect("creating a symlink"); + + // Readlink it into a non-existent buffer. + let bufused = wasi::path_readlink(dir_fd, "symlink", (&mut []).as_mut_ptr(), 0) + .expect("readlink with a 0-sized buffer should succeed"); + assert_eq!( + bufused, 0, + "readlink with a 0-sized buffer should return 'bufused' 0" + ); + + // Clean up. + wasi::path_unlink_file(dir_fd, "symlink").expect("removing a file"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_readlink_no_buffer(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/remove_directory_trailing_slashes.rs b/crates/test-programs/wasi-tests/src/bin/remove_directory_trailing_slashes.rs new file mode 100644 index 0000000000..7f05199dc3 --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/remove_directory_trailing_slashes.rs @@ -0,0 +1,63 @@ +use std::{env, process}; +use wasi_tests::{create_file, open_scratch_directory}; + +unsafe fn test_remove_directory_trailing_slashes(dir_fd: wasi::Fd) { + // Create a directory in the scratch directory. + wasi::path_create_directory(dir_fd, "dir").expect("creating a directory"); + + // Test that removing it succeeds. + wasi::path_remove_directory(dir_fd, "dir") + .expect("remove_directory on a directory should succeed"); + + wasi::path_create_directory(dir_fd, "dir").expect("creating a directory"); + + // Test that removing it with a trailing flash succeeds. + wasi::path_remove_directory(dir_fd, "dir/") + .expect("remove_directory with a trailing slash on a directory should succeed"); + + // Create a temporary file. + create_file(dir_fd, "file"); + + // Test that removing it with no trailing flash fails. + assert_eq!( + wasi::path_remove_directory(dir_fd, "file") + .expect_err("remove_directory without a trailing slash on a file should fail") + .raw_error(), + wasi::ERRNO_NOTDIR, + "errno should be ERRNO_NOTDIR" + ); + + // Test that removing it with a trailing flash fails. + assert_eq!( + wasi::path_remove_directory(dir_fd, "file/") + .expect_err("remove_directory with a trailing slash on a file should fail") + .raw_error(), + wasi::ERRNO_NOTDIR, + "errno should be ERRNO_NOTDIR" + ); + + wasi::path_unlink_file(dir_fd, "file").expect("removing a file"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_remove_directory_trailing_slashes(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/remove_nonempty_directory.rs b/crates/test-programs/wasi-tests/src/bin/remove_nonempty_directory.rs new file mode 100644 index 0000000000..daff21c2da --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/remove_nonempty_directory.rs @@ -0,0 +1,47 @@ +use std::{env, process}; +use wasi_tests::open_scratch_directory; + +unsafe fn test_remove_nonempty_directory(dir_fd: wasi::Fd) { + // Create a directory in the scratch directory. + wasi::path_create_directory(dir_fd, "dir").expect("creating a directory"); + + // Create a directory in the directory we just created. + wasi::path_create_directory(dir_fd, "dir/nested").expect("creating a subdirectory"); + + // Test that attempting to unlink the first directory returns the expected error code. + assert_eq!( + wasi::path_remove_directory(dir_fd, "dir") + .expect_err("remove_directory on a directory should return ENOTEMPTY") + .raw_error(), + wasi::ERRNO_NOTEMPTY, + "errno should be ERRNO_NOTEMPTY", + ); + + // Removing the directories. + wasi::path_remove_directory(dir_fd, "dir/nested") + .expect("remove_directory on a nested directory should succeed"); + wasi::path_remove_directory(dir_fd, "dir").expect("removing a directory"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_remove_nonempty_directory(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/renumber.rs b/crates/test-programs/wasi-tests/src/bin/renumber.rs new file mode 100644 index 0000000000..9e411cdbd6 --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/renumber.rs @@ -0,0 +1,104 @@ +use more_asserts::assert_gt; +use std::{env, process}; +use wasi_tests::open_scratch_directory; + +unsafe fn test_renumber(dir_fd: wasi::Fd) { + let pre_fd: wasi::Fd = (libc::STDERR_FILENO + 1) as wasi::Fd; + + assert_gt!(dir_fd, pre_fd, "dir_fd number"); + + // Create a file in the scratch directory. + let fd_from = wasi::path_open( + dir_fd, + 0, + "file1", + wasi::OFLAGS_CREAT, + wasi::RIGHTS_FD_READ | wasi::RIGHTS_FD_WRITE, + 0, + 0, + ) + .expect("opening a file"); + assert_gt!( + fd_from, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + + // Get fd_from fdstat attributes + let fdstat_from = + wasi::fd_fdstat_get(fd_from).expect("calling fd_fdstat on the open file descriptor"); + + // Create another file in the scratch directory. + let fd_to = wasi::path_open( + dir_fd, + 0, + "file2", + wasi::OFLAGS_CREAT, + wasi::RIGHTS_FD_READ | wasi::RIGHTS_FD_WRITE, + 0, + 0, + ) + .expect("opening a file"); + assert_gt!( + fd_to, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + + // Renumber fd of file1 into fd of file2 + wasi::fd_renumber(fd_from, fd_to).expect("renumbering two descriptors"); + + // Ensure that fd_from is closed + assert_eq!( + wasi::fd_close(fd_from) + .expect_err("closing already closed file descriptor") + .raw_error(), + wasi::ERRNO_BADF, + "errno should be ERRNO_BADF" + ); + + // Ensure that fd_to is still open. + let fdstat_to = + wasi::fd_fdstat_get(fd_to).expect("calling fd_fdstat on the open file descriptor"); + assert_eq!( + fdstat_from.fs_filetype, fdstat_to.fs_filetype, + "expected fd_to have the same fdstat as fd_from" + ); + assert_eq!( + fdstat_from.fs_flags, fdstat_to.fs_flags, + "expected fd_to have the same fdstat as fd_from" + ); + assert_eq!( + fdstat_from.fs_rights_base, fdstat_to.fs_rights_base, + "expected fd_to have the same fdstat as fd_from" + ); + assert_eq!( + fdstat_from.fs_rights_inheriting, fdstat_to.fs_rights_inheriting, + "expected fd_to have the same fdstat as fd_from" + ); + + wasi::fd_close(fd_to).expect("closing a file"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_renumber(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/sched_yield.rs b/crates/test-programs/wasi-tests/src/bin/sched_yield.rs new file mode 100644 index 0000000000..a00ab2991a --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/sched_yield.rs @@ -0,0 +1,8 @@ +unsafe fn test_sched_yield() { + wasi::sched_yield().expect("sched_yield"); +} + +fn main() { + // Run tests + unsafe { test_sched_yield() } +} diff --git a/crates/test-programs/wasi-tests/src/bin/stdio.rs b/crates/test-programs/wasi-tests/src/bin/stdio.rs new file mode 100644 index 0000000000..31b0a25b9b --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/stdio.rs @@ -0,0 +1,13 @@ +use wasi_tests::{STDERR_FD, STDIN_FD, STDOUT_FD}; + +unsafe fn test_stdio() { + for fd in &[STDIN_FD, STDOUT_FD, STDERR_FD] { + wasi::fd_fdstat_get(*fd).expect("fd_fdstat_get on stdio"); + wasi::fd_renumber(*fd, *fd + 100).expect("renumbering stdio"); + } +} + +fn main() { + // Run the tests. + unsafe { test_stdio() } +} diff --git a/crates/test-programs/wasi-tests/src/bin/symlink_loop.rs b/crates/test-programs/wasi-tests/src/bin/symlink_loop.rs new file mode 100644 index 0000000000..6422aad53a --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/symlink_loop.rs @@ -0,0 +1,42 @@ +use std::{env, process}; +use wasi_tests::open_scratch_directory; + +unsafe fn test_symlink_loop(dir_fd: wasi::Fd) { + // Create a self-referencing symlink. + wasi::path_symlink("symlink", dir_fd, "symlink").expect("creating a symlink"); + + // Try to open it. + assert_eq!( + wasi::path_open(dir_fd, 0, "symlink", 0, 0, 0, 0) + .expect_err("opening a self-referencing symlink") + .raw_error(), + wasi::ERRNO_LOOP, + "errno should be ERRNO_LOOP", + ); + + // Clean up. + wasi::path_unlink_file(dir_fd, "symlink").expect("removing a file"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_symlink_loop(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/truncation_rights.rs b/crates/test-programs/wasi-tests/src/bin/truncation_rights.rs new file mode 100644 index 0000000000..f846370728 --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/truncation_rights.rs @@ -0,0 +1,100 @@ +use std::{env, process}; +use wasi_tests::{create_file, open_scratch_directory}; + +unsafe fn test_truncation_rights(dir_fd: wasi::Fd) { + // Create a file in the scratch directory. + create_file(dir_fd, "file"); + + // Get the rights for the scratch directory. + let mut dir_fdstat = + wasi::fd_fdstat_get(dir_fd).expect("calling fd_fdstat on the scratch directory"); + assert_eq!( + dir_fdstat.fs_filetype, + wasi::FILETYPE_DIRECTORY, + "expected the scratch directory to be a directory", + ); + assert_eq!( + dir_fdstat.fs_flags, 0, + "expected the scratch directory to have no special flags", + ); + assert_eq!( + dir_fdstat.fs_rights_base & wasi::RIGHTS_FD_FILESTAT_SET_SIZE, + 0, + "directories shouldn't have the fd_filestat_set_size right", + ); + + // If we have the right to set sizes from paths, test that it works. + if (dir_fdstat.fs_rights_base & wasi::RIGHTS_PATH_FILESTAT_SET_SIZE) == 0 { + eprintln!("implementation doesn't support setting file sizes, skipping"); + } else { + // Test that we can truncate the file. + let mut file_fd = wasi::path_open(dir_fd, 0, "file", wasi::OFLAGS_TRUNC, 0, 0, 0) + .expect("truncating a file"); + wasi::fd_close(file_fd).expect("closing a file"); + + let mut rights_base: wasi::Rights = dir_fdstat.fs_rights_base; + let mut rights_inheriting: wasi::Rights = dir_fdstat.fs_rights_inheriting; + + if (rights_inheriting & wasi::RIGHTS_FD_FILESTAT_SET_SIZE) == 0 { + eprintln!("implementation doesn't support setting file sizes through file descriptors, skipping"); + } else { + rights_inheriting &= !wasi::RIGHTS_FD_FILESTAT_SET_SIZE; + wasi::fd_fdstat_set_rights(dir_fd, rights_base, rights_inheriting) + .expect("droping fd_filestat_set_size inheriting right on a directory"); + } + + // Test that we can truncate the file without the + // wasi_unstable::RIGHT_FD_FILESTAT_SET_SIZE right. + file_fd = wasi::path_open(dir_fd, 0, "file", wasi::OFLAGS_TRUNC, 0, 0, 0) + .expect("truncating a file without fd_filestat_set_size right"); + wasi::fd_close(file_fd).expect("closing a file"); + + rights_base &= !wasi::RIGHTS_PATH_FILESTAT_SET_SIZE; + wasi::fd_fdstat_set_rights(dir_fd, rights_base, rights_inheriting) + .expect("droping path_filestat_set_size base right on a directory"); + + // Test that clearing wasi_unstable::RIGHT_PATH_FILESTAT_SET_SIZE actually + // took effect. + dir_fdstat = wasi::fd_fdstat_get(dir_fd).expect("reading the fdstat from a directory"); + assert_eq!( + (dir_fdstat.fs_rights_base & wasi::RIGHTS_PATH_FILESTAT_SET_SIZE), + 0, + "reading the fdstat from a directory", + ); + + // Test that we can't truncate the file without the + // wasi_unstable::RIGHT_PATH_FILESTAT_SET_SIZE right. + assert_eq!( + wasi::path_open(dir_fd, 0, "file", wasi::OFLAGS_TRUNC, 0, 0, 0) + .expect_err("truncating a file without path_filestat_set_size right") + .raw_error(), + wasi::ERRNO_NOTCAPABLE, + "errno should be ERRNO_NOTCAPABLE", + ); + } + + wasi::path_unlink_file(dir_fd, "file").expect("removing a file"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_truncation_rights(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/bin/unlink_file_trailing_slashes.rs b/crates/test-programs/wasi-tests/src/bin/unlink_file_trailing_slashes.rs new file mode 100644 index 0000000000..495ae4f533 --- /dev/null +++ b/crates/test-programs/wasi-tests/src/bin/unlink_file_trailing_slashes.rs @@ -0,0 +1,67 @@ +use std::{env, process}; +use wasi_tests::{create_file, open_scratch_directory}; + +unsafe fn test_unlink_file_trailing_slashes(dir_fd: wasi::Fd) { + // Create a directory in the scratch directory. + wasi::path_create_directory(dir_fd, "dir").expect("creating a directory"); + + // Test that unlinking it fails. + assert_eq!( + wasi::path_unlink_file(dir_fd, "dir") + .expect_err("unlink_file on a directory should fail") + .raw_error(), + wasi::ERRNO_ISDIR, + "errno should be ERRNO_ISDIR" + ); + + // Test that unlinking it with a trailing flash fails. + assert_eq!( + wasi::path_unlink_file(dir_fd, "dir/") + .expect_err("unlink_file on a directory should fail") + .raw_error(), + wasi::ERRNO_ISDIR, + "errno should be ERRNO_ISDIR" + ); + + // Clean up. + wasi::path_remove_directory(dir_fd, "dir").expect("removing a directory"); + + // Create a temporary file. + create_file(dir_fd, "file"); + + // Test that unlinking it with a trailing flash fails. + assert_eq!( + wasi::path_unlink_file(dir_fd, "file/") + .expect_err("unlink_file with a trailing slash should fail") + .raw_error(), + wasi::ERRNO_NOTDIR, + "errno should be ERRNO_NOTDIR" + ); + + // Test that unlinking it with no trailing flash succeeds. + wasi::path_unlink_file(dir_fd, "file") + .expect("unlink_file with no trailing slash should succeed"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {} ", prog); + process::exit(1); + }; + + // Open scratch directory + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{}", err); + process::exit(1) + } + }; + + // Run the tests. + unsafe { test_unlink_file_trailing_slashes(dir_fd) } +} diff --git a/crates/test-programs/wasi-tests/src/lib.rs b/crates/test-programs/wasi-tests/src/lib.rs new file mode 100644 index 0000000000..0a9bee6f9d --- /dev/null +++ b/crates/test-programs/wasi-tests/src/lib.rs @@ -0,0 +1,63 @@ +use more_asserts::assert_gt; + +// The `wasi` crate version 0.9.0 and beyond, doesn't +// seem to define these constants, so we do it ourselves. +pub const STDIN_FD: wasi::Fd = 0x0; +pub const STDOUT_FD: wasi::Fd = 0x1; +pub const STDERR_FD: wasi::Fd = 0x2; + +/// Opens a fresh file descriptor for `path` where `path` should be a preopened +/// directory. +pub fn open_scratch_directory(path: &str) -> Result { + unsafe { + for i in 3.. { + let stat = match wasi::fd_prestat_get(i) { + Ok(s) => s, + Err(_) => break, + }; + if stat.pr_type != wasi::PREOPENTYPE_DIR { + continue; + } + let mut dst = Vec::with_capacity(stat.u.dir.pr_name_len); + if wasi::fd_prestat_dir_name(i, dst.as_mut_ptr(), dst.capacity()).is_err() { + continue; + } + dst.set_len(stat.u.dir.pr_name_len); + if dst == path.as_bytes() { + let (base, inherit) = fd_get_rights(i); + return Ok( + wasi::path_open(i, 0, ".", wasi::OFLAGS_DIRECTORY, base, inherit, 0) + .expect("failed to open dir"), + ); + } + } + + Err(format!("failed to find scratch dir")) + } +} + +pub unsafe fn create_file(dir_fd: wasi::Fd, filename: &str) { + let file_fd = + wasi::path_open(dir_fd, 0, filename, wasi::OFLAGS_CREAT, 0, 0, 0).expect("creating a file"); + assert_gt!( + file_fd, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + wasi::fd_close(file_fd).expect("closing a file"); +} + +// Returns: (rights_base, rights_inheriting) +pub unsafe fn fd_get_rights(fd: wasi::Fd) -> (wasi::Rights, wasi::Rights) { + let fdstat = wasi::fd_fdstat_get(fd).expect("fd_fdstat_get failed"); + (fdstat.fs_rights_base, fdstat.fs_rights_inheriting) +} + +pub unsafe fn drop_rights(fd: wasi::Fd, drop_base: wasi::Rights, drop_inheriting: wasi::Rights) { + let (current_base, current_inheriting) = fd_get_rights(fd); + + let new_base = current_base & !drop_base; + let new_inheriting = current_inheriting & !drop_inheriting; + + wasi::fd_fdstat_set_rights(fd, new_base, new_inheriting).expect("dropping fd rights"); +} diff --git a/crates/wasi-common/Cargo.toml b/crates/wasi-common/Cargo.toml new file mode 100644 index 0000000000..36c7017cbc --- /dev/null +++ b/crates/wasi-common/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "wasi-common" +version = "0.12.0" +authors = ["The Wasmtime Project Developers"] +description = "WASI implementation in Rust" +license = "Apache-2.0 WITH LLVM-exception" +categories = ["wasm"] +keywords = ["webassembly", "wasm"] +repository = "https://github.com/bytecodealliance/wasmtime" +readme = "README.md" +edition = "2018" + +[dependencies] +anyhow = "1.0" +thiserror = "1.0" +libc = "0.2" +getrandom = "0.1" +cfg-if = "0.1.9" +log = "0.4" +filetime = "0.2.7" +lazy_static = "1.4.0" +num = { version = "0.2.0", default-features = false } +wig = { path = "wig", version = "0.12.0" } + +[target.'cfg(unix)'.dependencies] +yanix = { path = "yanix", version = "0.12.0" } + +[target.'cfg(windows)'.dependencies] +winx = { path = "winx", version = "0.12.0" } +winapi = "0.3" +cpu-time = "1.0" + +[lib] +name = "wasi_common" +crate-type = ["rlib", "staticlib", "cdylib"] + +[badges] +maintenance = { status = "actively-developed" } diff --git a/crates/wasi-common/LICENSE b/crates/wasi-common/LICENSE new file mode 100644 index 0000000000..f9d81955f4 --- /dev/null +++ b/crates/wasi-common/LICENSE @@ -0,0 +1,220 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + diff --git a/crates/wasi-common/LICENSE.cloudabi-utils b/crates/wasi-common/LICENSE.cloudabi-utils new file mode 100644 index 0000000000..04c6f48a27 --- /dev/null +++ b/crates/wasi-common/LICENSE.cloudabi-utils @@ -0,0 +1,24 @@ +All code is distributed under the following license: + + Copyright (c) 2015 Nuxi, https://nuxi.nl/ + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. diff --git a/crates/wasi-common/README.md b/crates/wasi-common/README.md new file mode 100644 index 0000000000..06a17544ed --- /dev/null +++ b/crates/wasi-common/README.md @@ -0,0 +1,72 @@ +
+

wasi-common

+ +A
Bytecode Alliance project + +

+ A library providing a common implementation of WASI hostcalls for re-use in any WASI-enabled runtime. +

+ +

+ Crates.io version + Download + docs.rs docs +

+
+ +The `wasi-common` crate will ultimately serve as a library providing a common implementation of +WASI hostcalls for re-use in any WASI (and potentially non-WASI) runtimes +such as [Wasmtime] and [Lucet]. + +The library is an adaption of [lucet-wasi] crate from the [Lucet] project, and it is +currently based on [40ae1df][lucet-wasi-tracker] git revision. + +Please note that the library requires Rust compiler version at least 1.37.0. + +[Wasmtime]: https://github.com/bytecodealliance/wasmtime +[Lucet]: https://github.com/fastly/lucet +[lucet-wasi]: https://github.com/fastly/lucet/tree/master/lucet-wasi +[lucet-wasi-tracker]: https://github.com/fastly/lucet/commit/40ae1df64536250a2b6ab67e7f167d22f4aa7f94 + +## Supported syscalls + +### *nix +In our *nix implementation, we currently support the entire [WASI API] +with the exception of socket hostcalls: +- `sock_recv` +- `sock_send` +- `sock_shutdown` + +We expect these to be implemented when network access is standardised. + +We also currently do not support the `proc_raise` hostcall, as it is expected to +be dropped entirely from WASI. + +[WASI API]: https://github.com/bytecodealliance/wasmtime/blob/master/docs/WASI-api.md + +### Windows +In our Windows implementation, we currently support the minimal subset of [WASI API] +which allows for running the very basic "Hello world!" style WASM apps. More coming shortly, +so stay tuned! + +## Development hints +When testing the crate, you may want to enable and run full wasm32 integration testsuite. This +requires `wasm32-wasi` target installed which can be done as follows using [rustup] + +``` +rustup target add wasm32-wasi +``` + +[rustup]: https://rustup.rs + +Now, you should be able to run the integration testsuite by running `cargo test` on the +`test-programs` package with `test_programs` feature enabled: + +``` +cargo test --features test_programs --package test-programs +``` + +## Third-Party Code +Significant parts of our hostcall implementations are derived from the C implementations in +`cloudabi-utils`. See [LICENSE.cloudabi-utils](LICENSE.cloudabi-utils) for license information. + diff --git a/crates/wasi-common/src/ctx.rs b/crates/wasi-common/src/ctx.rs new file mode 100644 index 0000000000..e9e64a3866 --- /dev/null +++ b/crates/wasi-common/src/ctx.rs @@ -0,0 +1,380 @@ +use crate::fdentry::FdEntry; +use crate::{wasi, Error, Result}; +use std::borrow::Borrow; +use std::collections::HashMap; +use std::env; +use std::ffi::{CString, OsString}; +use std::fs::File; +use std::path::{Path, PathBuf}; + +enum PendingFdEntry { + Thunk(fn() -> Result), + File(File), +} + +impl std::fmt::Debug for PendingFdEntry { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Thunk(f) => write!( + fmt, + "PendingFdEntry::Thunk({:p})", + f as *const fn() -> Result + ), + Self::File(f) => write!(fmt, "PendingFdEntry::File({:?})", f), + } + } +} + +#[derive(Debug, Eq, Hash, PartialEq)] +enum PendingCString { + Bytes(Vec), + OsString(OsString), +} + +impl From> for PendingCString { + fn from(bytes: Vec) -> Self { + Self::Bytes(bytes) + } +} + +impl From for PendingCString { + fn from(s: OsString) -> Self { + Self::OsString(s) + } +} + +impl PendingCString { + fn into_string(self) -> Result { + match self { + Self::Bytes(v) => String::from_utf8(v).map_err(|_| Error::EILSEQ), + Self::OsString(s) => s.into_string().map_err(|_| Error::EILSEQ), + } + } + + /// Create a `CString` containing valid UTF-8, or fail with `Error::EILSEQ`. + fn into_utf8_cstring(self) -> Result { + self.into_string() + .and_then(|s| CString::new(s).map_err(|_| Error::EILSEQ)) + } +} + +/// A builder allowing customizable construction of `WasiCtx` instances. +pub struct WasiCtxBuilder { + fds: Option>, + preopens: Option>, + args: Option>, + env: Option>, +} + +impl WasiCtxBuilder { + /// Builder for a new `WasiCtx`. + pub fn new() -> Self { + let mut fds = HashMap::new(); + + fds.insert(0, PendingFdEntry::Thunk(FdEntry::null)); + fds.insert(1, PendingFdEntry::Thunk(FdEntry::null)); + fds.insert(2, PendingFdEntry::Thunk(FdEntry::null)); + + Self { + fds: Some(fds), + preopens: Some(Vec::new()), + args: Some(Vec::new()), + env: Some(HashMap::new()), + } + } + + /// Add arguments to the command-line arguments list. + /// + /// Arguments must be valid UTF-8 with no NUL bytes, or else `WasiCtxBuilder::build()` will fail + /// with `Error::EILSEQ`. + pub fn args>(&mut self, args: impl IntoIterator) -> &mut Self { + self.args + .as_mut() + .unwrap() + .extend(args.into_iter().map(|a| a.as_ref().to_vec().into())); + self + } + + /// Add an argument to the command-line arguments list. + /// + /// Arguments must be valid UTF-8 with no NUL bytes, or else `WasiCtxBuilder::build()` will fail + /// with `Error::EILSEQ`. + pub fn arg>(&mut self, arg: S) -> &mut Self { + self.args + .as_mut() + .unwrap() + .push(arg.as_ref().to_vec().into()); + self + } + + /// Inherit the command-line arguments from the host process. + /// + /// If any arguments from the host process contain invalid UTF-8, `WasiCtxBuilder::build()` will + /// fail with `Error::EILSEQ`. + pub fn inherit_args(&mut self) -> &mut Self { + let args = self.args.as_mut().unwrap(); + args.clear(); + args.extend(env::args_os().map(PendingCString::OsString)); + self + } + + /// Inherit stdin from the host process. + pub fn inherit_stdin(&mut self) -> &mut Self { + self.fds + .as_mut() + .unwrap() + .insert(0, PendingFdEntry::Thunk(FdEntry::duplicate_stdin)); + self + } + + /// Inherit stdout from the host process. + pub fn inherit_stdout(&mut self) -> &mut Self { + self.fds + .as_mut() + .unwrap() + .insert(1, PendingFdEntry::Thunk(FdEntry::duplicate_stdout)); + self + } + + /// Inherit stdout from the host process. + pub fn inherit_stderr(&mut self) -> &mut Self { + self.fds + .as_mut() + .unwrap() + .insert(2, PendingFdEntry::Thunk(FdEntry::duplicate_stderr)); + self + } + + /// Inherit the stdin, stdout, and stderr streams from the host process. + pub fn inherit_stdio(&mut self) -> &mut Self { + let fds = self.fds.as_mut().unwrap(); + fds.insert(0, PendingFdEntry::Thunk(FdEntry::duplicate_stdin)); + fds.insert(1, PendingFdEntry::Thunk(FdEntry::duplicate_stdout)); + fds.insert(2, PendingFdEntry::Thunk(FdEntry::duplicate_stderr)); + self + } + + /// Inherit the environment variables from the host process. + /// + /// If any environment variables from the host process contain invalid Unicode (UTF-16 for + /// Windows, UTF-8 for other platforms), `WasiCtxBuilder::build()` will fail with + /// `Error::EILSEQ`. + pub fn inherit_env(&mut self) -> &mut Self { + let env = self.env.as_mut().unwrap(); + env.clear(); + env.extend(std::env::vars_os().map(|(k, v)| (k.into(), v.into()))); + self + } + + /// Add an entry to the environment. + /// + /// Environment variable keys and values must be valid UTF-8 with no NUL bytes, or else + /// `WasiCtxBuilder::build()` will fail with `Error::EILSEQ`. + pub fn env>(&mut self, k: S, v: S) -> &mut Self { + self.env + .as_mut() + .unwrap() + .insert(k.as_ref().to_vec().into(), v.as_ref().to_vec().into()); + self + } + + /// Add entries to the environment. + /// + /// Environment variable keys and values must be valid UTF-8 with no NUL bytes, or else + /// `WasiCtxBuilder::build()` will fail with `Error::EILSEQ`. + pub fn envs, T: Borrow<(S, S)>>( + &mut self, + envs: impl IntoIterator, + ) -> &mut Self { + self.env.as_mut().unwrap().extend(envs.into_iter().map(|t| { + let (k, v) = t.borrow(); + (k.as_ref().to_vec().into(), v.as_ref().to_vec().into()) + })); + self + } + + /// Provide a File to use as stdin + pub fn stdin(&mut self, file: File) -> &mut Self { + self.fds + .as_mut() + .unwrap() + .insert(0, PendingFdEntry::File(file)); + self + } + + /// Provide a File to use as stdout + pub fn stdout(&mut self, file: File) -> &mut Self { + self.fds + .as_mut() + .unwrap() + .insert(1, PendingFdEntry::File(file)); + self + } + + /// Provide a File to use as stderr + pub fn stderr(&mut self, file: File) -> &mut Self { + self.fds + .as_mut() + .unwrap() + .insert(2, PendingFdEntry::File(file)); + self + } + + /// Add a preopened directory. + pub fn preopened_dir>(&mut self, dir: File, guest_path: P) -> &mut Self { + self.preopens + .as_mut() + .unwrap() + .push((guest_path.as_ref().to_owned(), dir)); + self + } + + /// Build a `WasiCtx`, consuming this `WasiCtxBuilder`. + /// + /// If any of the arguments or environment variables in this builder cannot be converted into + /// `CString`s, either due to NUL bytes or Unicode conversions, this returns `Error::EILSEQ`. + pub fn build(&mut self) -> Result { + // Process arguments and environment variables into `CString`s, failing quickly if they + // contain any NUL bytes, or if conversion from `OsString` fails. + let args = self + .args + .take() + .ok_or(Error::EINVAL)? + .into_iter() + .map(|arg| arg.into_utf8_cstring()) + .collect::>>()?; + + let env = self + .env + .take() + .ok_or(Error::EINVAL)? + .into_iter() + .map(|(k, v)| { + k.into_string().and_then(|mut pair| { + v.into_string().and_then(|v| { + pair.push('='); + pair.push_str(v.as_str()); + // We have valid UTF-8, but the keys and values have not yet been checked + // for NULs, so we do a final check here. + CString::new(pair).map_err(|_| Error::EILSEQ) + }) + }) + }) + .collect::>>()?; + + let mut fds: HashMap = HashMap::new(); + // Populate the non-preopen fds. + for (fd, pending) in self.fds.take().ok_or(Error::EINVAL)? { + log::debug!("WasiCtx inserting ({:?}, {:?})", fd, pending); + match pending { + PendingFdEntry::Thunk(f) => { + fds.insert(fd, f()?); + } + PendingFdEntry::File(f) => { + fds.insert(fd, FdEntry::from(f)?); + } + } + } + // Then add the preopen fds. Startup code in the guest starts looking at fd 3 for preopens, + // so we start from there. This variable is initially 2, though, because the loop + // immediately does the increment and check for overflow. + let mut preopen_fd: wasi::__wasi_fd_t = 2; + for (guest_path, dir) in self.preopens.take().ok_or(Error::EINVAL)? { + // We do the increment at the beginning of the loop body, so that we don't overflow + // unnecessarily if we have exactly the maximum number of file descriptors. + preopen_fd = preopen_fd.checked_add(1).ok_or(Error::ENFILE)?; + + if !dir.metadata()?.is_dir() { + return Err(Error::EBADF); + } + + // We don't currently allow setting file descriptors other than 0-2, but this will avoid + // collisions if we restore that functionality in the future. + while fds.contains_key(&preopen_fd) { + preopen_fd = preopen_fd.checked_add(1).ok_or(Error::ENFILE)?; + } + let mut fe = FdEntry::from(dir)?; + fe.preopen_path = Some(guest_path); + log::debug!("WasiCtx inserting ({:?}, {:?})", preopen_fd, fe); + fds.insert(preopen_fd, fe); + log::debug!("WasiCtx fds = {:?}", fds); + } + + Ok(WasiCtx { args, env, fds }) + } +} + +#[derive(Debug)] +pub struct WasiCtx { + fds: HashMap, + pub(crate) args: Vec, + pub(crate) env: Vec, +} + +impl WasiCtx { + /// Make a new `WasiCtx` with some default settings. + /// + /// - File descriptors 0, 1, and 2 inherit stdin, stdout, and stderr from the host process. + /// + /// - Environment variables are inherited from the host process. + /// + /// To override these behaviors, use `WasiCtxBuilder`. + pub fn new>(args: impl IntoIterator) -> Result { + WasiCtxBuilder::new() + .args(args) + .inherit_stdio() + .inherit_env() + .build() + } + + /// Check if `WasiCtx` contains the specified raw WASI `fd`. + pub(crate) unsafe fn contains_fd_entry(&self, fd: wasi::__wasi_fd_t) -> bool { + self.fds.contains_key(&fd) + } + + /// Get an immutable `FdEntry` corresponding to the specified raw WASI `fd`. + pub(crate) unsafe fn get_fd_entry(&self, fd: wasi::__wasi_fd_t) -> Result<&FdEntry> { + self.fds.get(&fd).ok_or(Error::EBADF) + } + + /// Get a mutable `FdEntry` corresponding to the specified raw WASI `fd`. + pub(crate) unsafe fn get_fd_entry_mut( + &mut self, + fd: wasi::__wasi_fd_t, + ) -> Result<&mut FdEntry> { + self.fds.get_mut(&fd).ok_or(Error::EBADF) + } + + /// Insert the specified `FdEntry` into the `WasiCtx` object. + /// + /// The `FdEntry` will automatically get another free raw WASI `fd` assigned. Note that + /// the two subsequent free raw WASI `fd`s do not have to be stored contiguously. + pub(crate) fn insert_fd_entry(&mut self, fe: FdEntry) -> Result { + // Never insert where stdio handles are expected to be. + let mut fd = 3; + while self.fds.contains_key(&fd) { + if let Some(next_fd) = fd.checked_add(1) { + fd = next_fd; + } else { + return Err(Error::EMFILE); + } + } + self.fds.insert(fd, fe); + Ok(fd) + } + + /// Insert the specified `FdEntry` with the specified raw WASI `fd` key into the `WasiCtx` + /// object. + pub(crate) fn insert_fd_entry_at( + &mut self, + fd: wasi::__wasi_fd_t, + fe: FdEntry, + ) -> Option { + self.fds.insert(fd, fe) + } + + /// Remove `FdEntry` corresponding to the specified raw WASI `fd` from the `WasiCtx` object. + pub(crate) fn remove_fd_entry(&mut self, fd: wasi::__wasi_fd_t) -> Result { + self.fds.remove(&fd).ok_or(Error::EBADF) + } +} diff --git a/crates/wasi-common/src/error.rs b/crates/wasi-common/src/error.rs new file mode 100644 index 0000000000..4ce56bbc57 --- /dev/null +++ b/crates/wasi-common/src/error.rs @@ -0,0 +1,262 @@ +// Due to https://github.com/rust-lang/rust/issues/64247 +#![allow(clippy::use_self)] +use crate::wasi; +use std::convert::Infallible; +use std::num::TryFromIntError; +use std::{ffi, str}; +use thiserror::Error; + +#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)] +#[repr(u16)] +#[error("{:?} ({})", self, wasi::strerror(*self as wasi::__wasi_errno_t))] +pub enum WasiError { + ESUCCESS = wasi::__WASI_ERRNO_SUCCESS, + E2BIG = wasi::__WASI_ERRNO_2BIG, + EACCES = wasi::__WASI_ERRNO_ACCES, + EADDRINUSE = wasi::__WASI_ERRNO_ADDRINUSE, + EADDRNOTAVAIL = wasi::__WASI_ERRNO_ADDRNOTAVAIL, + EAFNOSUPPORT = wasi::__WASI_ERRNO_AFNOSUPPORT, + EAGAIN = wasi::__WASI_ERRNO_AGAIN, + EALREADY = wasi::__WASI_ERRNO_ALREADY, + EBADF = wasi::__WASI_ERRNO_BADF, + EBADMSG = wasi::__WASI_ERRNO_BADMSG, + EBUSY = wasi::__WASI_ERRNO_BUSY, + ECANCELED = wasi::__WASI_ERRNO_CANCELED, + ECHILD = wasi::__WASI_ERRNO_CHILD, + ECONNABORTED = wasi::__WASI_ERRNO_CONNABORTED, + ECONNREFUSED = wasi::__WASI_ERRNO_CONNREFUSED, + ECONNRESET = wasi::__WASI_ERRNO_CONNRESET, + EDEADLK = wasi::__WASI_ERRNO_DEADLK, + EDESTADDRREQ = wasi::__WASI_ERRNO_DESTADDRREQ, + EDOM = wasi::__WASI_ERRNO_DOM, + EDQUOT = wasi::__WASI_ERRNO_DQUOT, + EEXIST = wasi::__WASI_ERRNO_EXIST, + EFAULT = wasi::__WASI_ERRNO_FAULT, + EFBIG = wasi::__WASI_ERRNO_FBIG, + EHOSTUNREACH = wasi::__WASI_ERRNO_HOSTUNREACH, + EIDRM = wasi::__WASI_ERRNO_IDRM, + EILSEQ = wasi::__WASI_ERRNO_ILSEQ, + EINPROGRESS = wasi::__WASI_ERRNO_INPROGRESS, + EINTR = wasi::__WASI_ERRNO_INTR, + EINVAL = wasi::__WASI_ERRNO_INVAL, + EIO = wasi::__WASI_ERRNO_IO, + EISCONN = wasi::__WASI_ERRNO_ISCONN, + EISDIR = wasi::__WASI_ERRNO_ISDIR, + ELOOP = wasi::__WASI_ERRNO_LOOP, + EMFILE = wasi::__WASI_ERRNO_MFILE, + EMLINK = wasi::__WASI_ERRNO_MLINK, + EMSGSIZE = wasi::__WASI_ERRNO_MSGSIZE, + EMULTIHOP = wasi::__WASI_ERRNO_MULTIHOP, + ENAMETOOLONG = wasi::__WASI_ERRNO_NAMETOOLONG, + ENETDOWN = wasi::__WASI_ERRNO_NETDOWN, + ENETRESET = wasi::__WASI_ERRNO_NETRESET, + ENETUNREACH = wasi::__WASI_ERRNO_NETUNREACH, + ENFILE = wasi::__WASI_ERRNO_NFILE, + ENOBUFS = wasi::__WASI_ERRNO_NOBUFS, + ENODEV = wasi::__WASI_ERRNO_NODEV, + ENOENT = wasi::__WASI_ERRNO_NOENT, + ENOEXEC = wasi::__WASI_ERRNO_NOEXEC, + ENOLCK = wasi::__WASI_ERRNO_NOLCK, + ENOLINK = wasi::__WASI_ERRNO_NOLINK, + ENOMEM = wasi::__WASI_ERRNO_NOMEM, + ENOMSG = wasi::__WASI_ERRNO_NOMSG, + ENOPROTOOPT = wasi::__WASI_ERRNO_NOPROTOOPT, + ENOSPC = wasi::__WASI_ERRNO_NOSPC, + ENOSYS = wasi::__WASI_ERRNO_NOSYS, + ENOTCONN = wasi::__WASI_ERRNO_NOTCONN, + ENOTDIR = wasi::__WASI_ERRNO_NOTDIR, + ENOTEMPTY = wasi::__WASI_ERRNO_NOTEMPTY, + ENOTRECOVERABLE = wasi::__WASI_ERRNO_NOTRECOVERABLE, + ENOTSOCK = wasi::__WASI_ERRNO_NOTSOCK, + ENOTSUP = wasi::__WASI_ERRNO_NOTSUP, + ENOTTY = wasi::__WASI_ERRNO_NOTTY, + ENXIO = wasi::__WASI_ERRNO_NXIO, + EOVERFLOW = wasi::__WASI_ERRNO_OVERFLOW, + EOWNERDEAD = wasi::__WASI_ERRNO_OWNERDEAD, + EPERM = wasi::__WASI_ERRNO_PERM, + EPIPE = wasi::__WASI_ERRNO_PIPE, + EPROTO = wasi::__WASI_ERRNO_PROTO, + EPROTONOSUPPORT = wasi::__WASI_ERRNO_PROTONOSUPPORT, + EPROTOTYPE = wasi::__WASI_ERRNO_PROTOTYPE, + ERANGE = wasi::__WASI_ERRNO_RANGE, + EROFS = wasi::__WASI_ERRNO_ROFS, + ESPIPE = wasi::__WASI_ERRNO_SPIPE, + ESRCH = wasi::__WASI_ERRNO_SRCH, + ESTALE = wasi::__WASI_ERRNO_STALE, + ETIMEDOUT = wasi::__WASI_ERRNO_TIMEDOUT, + ETXTBSY = wasi::__WASI_ERRNO_TXTBSY, + EXDEV = wasi::__WASI_ERRNO_XDEV, + ENOTCAPABLE = wasi::__WASI_ERRNO_NOTCAPABLE, +} + +impl WasiError { + pub fn as_raw_errno(self) -> wasi::__wasi_errno_t { + self as wasi::__wasi_errno_t + } +} + +#[derive(Debug, Error)] +pub enum Error { + #[error("WASI error code: {0}")] + Wasi(#[from] WasiError), + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[cfg(unix)] + #[error("Yanix error: {0}")] + Yanix(#[from] yanix::YanixError), +} + +impl From for Error { + fn from(_: TryFromIntError) -> Self { + Self::EOVERFLOW + } +} + +impl From for Error { + fn from(_: Infallible) -> Self { + unreachable!() + } +} + +impl From for Error { + fn from(_: str::Utf8Error) -> Self { + Self::EILSEQ + } +} + +impl From for Error { + fn from(_: ffi::NulError) -> Self { + Self::EILSEQ + } +} + +impl From<&ffi::NulError> for Error { + fn from(_: &ffi::NulError) -> Self { + Self::EILSEQ + } +} + +impl Error { + pub(crate) fn as_wasi_error(&self) -> WasiError { + match self { + Self::Wasi(err) => *err, + Self::Io(err) => { + let err = match err.raw_os_error() { + Some(code) => Self::from_raw_os_error(code), + None => { + log::debug!("Inconvertible OS error: {}", err); + Self::EIO + } + }; + err.as_wasi_error() + } + #[cfg(unix)] + Self::Yanix(err) => { + use yanix::YanixError::*; + let err: Self = match err { + Errno(errno) => (*errno).into(), + NulError(err) => err.into(), + TryFromIntError(err) => (*err).into(), + }; + err.as_wasi_error() + } + } + } + + pub const ESUCCESS: Self = Error::Wasi(WasiError::ESUCCESS); + pub const E2BIG: Self = Error::Wasi(WasiError::E2BIG); + pub const EACCES: Self = Error::Wasi(WasiError::EACCES); + pub const EADDRINUSE: Self = Error::Wasi(WasiError::EADDRINUSE); + pub const EADDRNOTAVAIL: Self = Error::Wasi(WasiError::EADDRNOTAVAIL); + pub const EAFNOSUPPORT: Self = Error::Wasi(WasiError::EAFNOSUPPORT); + pub const EAGAIN: Self = Error::Wasi(WasiError::EAGAIN); + pub const EALREADY: Self = Error::Wasi(WasiError::EALREADY); + pub const EBADF: Self = Error::Wasi(WasiError::EBADF); + pub const EBADMSG: Self = Error::Wasi(WasiError::EBADMSG); + pub const EBUSY: Self = Error::Wasi(WasiError::EBUSY); + pub const ECANCELED: Self = Error::Wasi(WasiError::ECANCELED); + pub const ECHILD: Self = Error::Wasi(WasiError::ECHILD); + pub const ECONNABORTED: Self = Error::Wasi(WasiError::ECONNABORTED); + pub const ECONNREFUSED: Self = Error::Wasi(WasiError::ECONNREFUSED); + pub const ECONNRESET: Self = Error::Wasi(WasiError::ECONNRESET); + pub const EDEADLK: Self = Error::Wasi(WasiError::EDEADLK); + pub const EDESTADDRREQ: Self = Error::Wasi(WasiError::EDESTADDRREQ); + pub const EDOM: Self = Error::Wasi(WasiError::EDOM); + pub const EDQUOT: Self = Error::Wasi(WasiError::EDQUOT); + pub const EEXIST: Self = Error::Wasi(WasiError::EEXIST); + pub const EFAULT: Self = Error::Wasi(WasiError::EFAULT); + pub const EFBIG: Self = Error::Wasi(WasiError::EFBIG); + pub const EHOSTUNREACH: Self = Error::Wasi(WasiError::EHOSTUNREACH); + pub const EIDRM: Self = Error::Wasi(WasiError::EIDRM); + pub const EILSEQ: Self = Error::Wasi(WasiError::EILSEQ); + pub const EINPROGRESS: Self = Error::Wasi(WasiError::EINPROGRESS); + pub const EINTR: Self = Error::Wasi(WasiError::EINTR); + pub const EINVAL: Self = Error::Wasi(WasiError::EINVAL); + pub const EIO: Self = Error::Wasi(WasiError::EIO); + pub const EISCONN: Self = Error::Wasi(WasiError::EISCONN); + pub const EISDIR: Self = Error::Wasi(WasiError::EISDIR); + pub const ELOOP: Self = Error::Wasi(WasiError::ELOOP); + pub const EMFILE: Self = Error::Wasi(WasiError::EMFILE); + pub const EMLINK: Self = Error::Wasi(WasiError::EMLINK); + pub const EMSGSIZE: Self = Error::Wasi(WasiError::EMSGSIZE); + pub const EMULTIHOP: Self = Error::Wasi(WasiError::EMULTIHOP); + pub const ENAMETOOLONG: Self = Error::Wasi(WasiError::ENAMETOOLONG); + pub const ENETDOWN: Self = Error::Wasi(WasiError::ENETDOWN); + pub const ENETRESET: Self = Error::Wasi(WasiError::ENETRESET); + pub const ENETUNREACH: Self = Error::Wasi(WasiError::ENETUNREACH); + pub const ENFILE: Self = Error::Wasi(WasiError::ENFILE); + pub const ENOBUFS: Self = Error::Wasi(WasiError::ENOBUFS); + pub const ENODEV: Self = Error::Wasi(WasiError::ENODEV); + pub const ENOENT: Self = Error::Wasi(WasiError::ENOENT); + pub const ENOEXEC: Self = Error::Wasi(WasiError::ENOEXEC); + pub const ENOLCK: Self = Error::Wasi(WasiError::ENOLCK); + pub const ENOLINK: Self = Error::Wasi(WasiError::ENOLINK); + pub const ENOMEM: Self = Error::Wasi(WasiError::ENOMEM); + pub const ENOMSG: Self = Error::Wasi(WasiError::ENOMSG); + pub const ENOPROTOOPT: Self = Error::Wasi(WasiError::ENOPROTOOPT); + pub const ENOSPC: Self = Error::Wasi(WasiError::ENOSPC); + pub const ENOSYS: Self = Error::Wasi(WasiError::ENOSYS); + pub const ENOTCONN: Self = Error::Wasi(WasiError::ENOTCONN); + pub const ENOTDIR: Self = Error::Wasi(WasiError::ENOTDIR); + pub const ENOTEMPTY: Self = Error::Wasi(WasiError::ENOTEMPTY); + pub const ENOTRECOVERABLE: Self = Error::Wasi(WasiError::ENOTRECOVERABLE); + pub const ENOTSOCK: Self = Error::Wasi(WasiError::ENOTSOCK); + pub const ENOTSUP: Self = Error::Wasi(WasiError::ENOTSUP); + pub const ENOTTY: Self = Error::Wasi(WasiError::ENOTTY); + pub const ENXIO: Self = Error::Wasi(WasiError::ENXIO); + pub const EOVERFLOW: Self = Error::Wasi(WasiError::EOVERFLOW); + pub const EOWNERDEAD: Self = Error::Wasi(WasiError::EOWNERDEAD); + pub const EPERM: Self = Error::Wasi(WasiError::EPERM); + pub const EPIPE: Self = Error::Wasi(WasiError::EPIPE); + pub const EPROTO: Self = Error::Wasi(WasiError::EPROTO); + pub const EPROTONOSUPPORT: Self = Error::Wasi(WasiError::EPROTONOSUPPORT); + pub const EPROTOTYPE: Self = Error::Wasi(WasiError::EPROTOTYPE); + pub const ERANGE: Self = Error::Wasi(WasiError::ERANGE); + pub const EROFS: Self = Error::Wasi(WasiError::EROFS); + pub const ESPIPE: Self = Error::Wasi(WasiError::ESPIPE); + pub const ESRCH: Self = Error::Wasi(WasiError::ESRCH); + pub const ESTALE: Self = Error::Wasi(WasiError::ESTALE); + pub const ETIMEDOUT: Self = Error::Wasi(WasiError::ETIMEDOUT); + pub const ETXTBSY: Self = Error::Wasi(WasiError::ETXTBSY); + pub const EXDEV: Self = Error::Wasi(WasiError::EXDEV); + pub const ENOTCAPABLE: Self = Error::Wasi(WasiError::ENOTCAPABLE); +} + +pub(crate) trait FromRawOsError { + fn from_raw_os_error(code: i32) -> Self; +} + +pub(crate) type Result = std::result::Result; + +pub(crate) trait AsWasiError { + fn as_wasi_error(&self) -> WasiError; +} + +impl AsWasiError for Result { + fn as_wasi_error(&self) -> WasiError { + self.as_ref() + .err() + .unwrap_or(&Error::ESUCCESS) + .as_wasi_error() + } +} diff --git a/crates/wasi-common/src/fdentry.rs b/crates/wasi-common/src/fdentry.rs new file mode 100644 index 0000000000..a86e6ce8b4 --- /dev/null +++ b/crates/wasi-common/src/fdentry.rs @@ -0,0 +1,218 @@ +use crate::sys::dev_null; +use crate::sys::fdentry_impl::{ + descriptor_as_oshandle, determine_type_and_access_rights, OsHandle, +}; +use crate::{wasi, Error, Result}; +use std::marker::PhantomData; +use std::mem::ManuallyDrop; +use std::ops::{Deref, DerefMut}; +use std::path::PathBuf; +use std::{fs, io}; + +#[derive(Debug)] +pub(crate) enum Descriptor { + OsHandle(OsHandle), + Stdin, + Stdout, + Stderr, +} + +impl Descriptor { + /// Return a reference to the `OsHandle` treating it as an actual file/dir, and + /// allowing operations which require an actual file and not just a stream or + /// socket file descriptor. + pub(crate) fn as_file(&self) -> Result<&OsHandle> { + match self { + Self::OsHandle(file) => Ok(file), + _ => Err(Error::EBADF), + } + } + + /// Like `as_file`, but return a mutable reference. + pub(crate) fn as_file_mut(&mut self) -> Result<&mut OsHandle> { + match self { + Self::OsHandle(file) => Ok(file), + _ => Err(Error::EBADF), + } + } + + /// Return an `OsHandle`, which may be a stream or socket file descriptor. + pub(crate) fn as_os_handle<'descriptor>(&'descriptor self) -> OsHandleRef<'descriptor> { + descriptor_as_oshandle(self) + } +} + +/// An abstraction struct serving as a wrapper for a host `Descriptor` object which requires +/// certain base rights `rights_base` and inheriting rights `rights_inheriting` in order to be +/// accessed correctly. +/// +/// Here, the `descriptor` field stores the host `Descriptor` object (such as a file descriptor, or +/// stdin handle), and accessing it can only be done via the provided `FdEntry::as_descriptor` and +/// `FdEntry::as_descriptor_mut` methods which require a set of base and inheriting rights to be +/// specified, verifying whether the stored `Descriptor` object is valid for the rights specified. +#[derive(Debug)] +pub(crate) struct FdEntry { + pub(crate) file_type: wasi::__wasi_filetype_t, + descriptor: Descriptor, + pub(crate) rights_base: wasi::__wasi_rights_t, + pub(crate) rights_inheriting: wasi::__wasi_rights_t, + pub(crate) preopen_path: Option, + // TODO: directories +} + +impl FdEntry { + pub(crate) fn from(file: fs::File) -> Result { + unsafe { determine_type_and_access_rights(&file) }.map( + |(file_type, rights_base, rights_inheriting)| Self { + file_type, + descriptor: Descriptor::OsHandle(OsHandle::from(file)), + rights_base, + rights_inheriting, + preopen_path: None, + }, + ) + } + + pub(crate) fn duplicate_stdin() -> Result { + unsafe { determine_type_and_access_rights(&io::stdin()) }.map( + |(file_type, rights_base, rights_inheriting)| Self { + file_type, + descriptor: Descriptor::Stdin, + rights_base, + rights_inheriting, + preopen_path: None, + }, + ) + } + + pub(crate) fn duplicate_stdout() -> Result { + unsafe { determine_type_and_access_rights(&io::stdout()) }.map( + |(file_type, rights_base, rights_inheriting)| Self { + file_type, + descriptor: Descriptor::Stdout, + rights_base, + rights_inheriting, + preopen_path: None, + }, + ) + } + + pub(crate) fn duplicate_stderr() -> Result { + unsafe { determine_type_and_access_rights(&io::stderr()) }.map( + |(file_type, rights_base, rights_inheriting)| Self { + file_type, + descriptor: Descriptor::Stderr, + rights_base, + rights_inheriting, + preopen_path: None, + }, + ) + } + + pub(crate) fn null() -> Result { + Self::from(dev_null()?) + } + + /// Convert this `FdEntry` into a host `Descriptor` object provided the specified + /// `rights_base` and `rights_inheriting` rights are set on this `FdEntry` object. + /// + /// The `FdEntry` can only be converted into a valid `Descriptor` object if + /// the specified set of base rights `rights_base`, and inheriting rights `rights_inheriting` + /// is a subset of rights attached to this `FdEntry`. The check is performed using + /// `FdEntry::validate_rights` method. If the check fails, `Error::ENOTCAPABLE` is returned. + pub(crate) fn as_descriptor( + &self, + rights_base: wasi::__wasi_rights_t, + rights_inheriting: wasi::__wasi_rights_t, + ) -> Result<&Descriptor> { + self.validate_rights(rights_base, rights_inheriting)?; + Ok(&self.descriptor) + } + + /// Convert this `FdEntry` into a mutable host `Descriptor` object provided the specified + /// `rights_base` and `rights_inheriting` rights are set on this `FdEntry` object. + /// + /// The `FdEntry` can only be converted into a valid `Descriptor` object if + /// the specified set of base rights `rights_base`, and inheriting rights `rights_inheriting` + /// is a subset of rights attached to this `FdEntry`. The check is performed using + /// `FdEntry::validate_rights` method. If the check fails, `Error::ENOTCAPABLE` is returned. + pub(crate) fn as_descriptor_mut( + &mut self, + rights_base: wasi::__wasi_rights_t, + rights_inheriting: wasi::__wasi_rights_t, + ) -> Result<&mut Descriptor> { + self.validate_rights(rights_base, rights_inheriting)?; + Ok(&mut self.descriptor) + } + + /// Check if this `FdEntry` object satisfies the specified base rights `rights_base`, and + /// inheriting rights `rights_inheriting`; i.e., if rights attached to this `FdEntry` object + /// are a superset. + /// + /// Upon unsuccessful check, `Error::ENOTCAPABLE` is returned. + fn validate_rights( + &self, + rights_base: wasi::__wasi_rights_t, + rights_inheriting: wasi::__wasi_rights_t, + ) -> Result<()> { + let missing_base = !self.rights_base & rights_base; + let missing_inheriting = !self.rights_inheriting & rights_inheriting; + if missing_base != 0 || missing_inheriting != 0 { + log::trace!( + " | validate_rights failed: required: \ + rights_base = {:#x}, rights_inheriting = {:#x}; \ + actual: rights_base = {:#x}, rights_inheriting = {:#x}; \ + missing_base = {:#x}, missing_inheriting = {:#x}", + rights_base, + rights_inheriting, + self.rights_base, + self.rights_inheriting, + missing_base, + missing_inheriting + ); + Err(Error::ENOTCAPABLE) + } else { + Ok(()) + } + } + + /// Test whether this descriptor is considered a tty within WASI. + /// Note that since WASI itself lacks an `isatty` syscall and relies + /// on a conservative approximation, we use the same approximation here. + pub(crate) fn isatty(&self) -> bool { + self.file_type == wasi::__WASI_FILETYPE_CHARACTER_DEVICE + && (self.rights_base & (wasi::__WASI_RIGHTS_FD_SEEK | wasi::__WASI_RIGHTS_FD_TELL)) == 0 + } +} + +/// This allows an `OsHandle` to be temporarily borrowed from a +/// `Descriptor`. The `Descriptor` continues to own the resource, +/// and `OsHandleRef`'s lifetime parameter ensures that it doesn't +/// outlive the `Descriptor`. +pub(crate) struct OsHandleRef<'descriptor> { + handle: ManuallyDrop, + _ref: PhantomData<&'descriptor Descriptor>, +} + +impl<'descriptor> OsHandleRef<'descriptor> { + pub(crate) fn new(handle: ManuallyDrop) -> Self { + OsHandleRef { + handle, + _ref: PhantomData, + } + } +} + +impl<'descriptor> Deref for OsHandleRef<'descriptor> { + type Target = fs::File; + + fn deref(&self) -> &Self::Target { + &self.handle + } +} + +impl<'descriptor> DerefMut for OsHandleRef<'descriptor> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.handle + } +} diff --git a/crates/wasi-common/src/fs/dir.rs b/crates/wasi-common/src/fs/dir.rs new file mode 100644 index 0000000000..0a543b4a15 --- /dev/null +++ b/crates/wasi-common/src/fs/dir.rs @@ -0,0 +1,216 @@ +use crate::fs::{File, OpenOptions, ReadDir}; +use crate::{host, hostcalls, wasi, WasiCtx}; +#[cfg(unix)] +use std::os::unix::ffi::OsStrExt; +use std::{io, path::Path}; + +/// A reference to an open directory on the filesystem. +/// +/// TODO: Implement `Dir`-using versions of `std::fs`'s free functions: +/// `copy`, `create_dir`, `create_dir_all`, `hard_link`, `metadata`, +/// `read_link`, `read_to_string`, `remove_dir`, `remove_dir_all`, +/// `remove_file`, `rename`, `set_permissions`, `symlink_metadata`, and +/// `write`. +/// +/// Unlike `std::fs`, this API has no `canonicalize`, because absolute paths +/// don't interoperate well with the capability-oriented security model. +pub struct Dir<'ctx> { + ctx: &'ctx mut WasiCtx, + fd: wasi::__wasi_fd_t, +} + +impl<'ctx> Dir<'ctx> { + /// Constructs a new instance of `Self` from the given raw WASI file descriptor. + pub unsafe fn from_raw_wasi_fd(ctx: &'ctx mut WasiCtx, fd: wasi::__wasi_fd_t) -> Self { + Self { ctx, fd } + } + + /// Attempts to open a file in read-only mode. + /// + /// This corresponds to [`std::fs::File::open`], but only accesses paths + /// relative to and within `self`. + /// + /// TODO: Not yet implemented. Refactor the hostcalls functions to split out the + /// encoding/decoding parts from the underlying functionality, so that we can call + /// into the underlying functionality directly. + /// + /// [`std::fs::File::open`]: https://doc.rust-lang.org/std/fs/struct.File.html#method.open + pub fn open_file>(&mut self, path: P) -> io::Result { + let path = path.as_ref(); + let mut fd = 0; + + // TODO: Refactor the hostcalls functions to split out the encoding/decoding + // parts from the underlying functionality, so that we can call into the + // underlying functionality directly. + // + // TODO: Set the requested rights to be readonly. + // + // TODO: Handle paths for non-Unix platforms which don't have `as_bytes()` + // on `OsStrExt`. + unimplemented!("Dir::open_file"); + /* + wasi_errno_to_io_error(hostcalls::path_open( + self.ctx, + self.fd, + wasi::__WASI_LOOKUPFLAGS_SYMLINK_FOLLOW, + path.as_os_str().as_bytes(), + path.as_os_str().len(), + 0, + !0, + !0, + 0, + &mut fd, + ))?; + */ + + let ctx = self.ctx; + Ok(unsafe { File::from_raw_wasi_fd(ctx, fd) }) + } + + /// Opens a file at `path` with the options specified by `self`. + /// + /// This corresponds to [`std::fs::OpenOptions::open`]. + /// + /// Instead of being a method on `OpenOptions`, this is a method on `Dir`, + /// and it only accesses functions relative to and within `self`. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::OpenOptions::open`]: https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.open + pub fn open_file_with>( + &mut self, + path: P, + options: &OpenOptions, + ) -> io::Result { + unimplemented!("Dir::open_file_with"); + } + + /// Attempts to open a directory. + /// + /// TODO: Not yet implemented. See the comment in `open_file`. + pub fn open_dir>(&mut self, path: P) -> io::Result { + let path = path.as_ref(); + let mut fd = 0; + + // TODO: See the comment in `open_file`. + unimplemented!("Dir::open_dir"); + /* + wasi_errno_to_io_error(hostcalls::path_open( + self.ctx, + self.fd, + wasi::__WASI_LOOKUPFLAGS_SYMLINK_FOLLOW, + path.as_os_str().as_bytes(), + wasi::__WASI_OFLAGS_DIRECTORY, + !0, + !0, + 0, + &mut fd, + ))?; + */ + + let ctx = self.ctx; + Ok(unsafe { Dir::from_raw_wasi_fd(ctx, fd) }) + } + + /// Opens a file in write-only mode. + /// + /// This corresponds to [`std::fs::File::create`], but only accesses paths + /// relative to and within `self`. + /// + /// TODO: Not yet implemented. See the comment in `open_file`. + /// + /// [`std::fs::File::create`]: https://doc.rust-lang.org/std/fs/struct.File.html#method.create + pub fn create_file>(&mut self, path: P) -> io::Result { + let path = path.as_ref(); + let mut fd = 0; + + // TODO: See the comments in `open_file`. + // + // TODO: Set the requested rights to be read+write. + unimplemented!("Dir::create_file"); + /* + wasi_errno_to_io_error(hostcalls::path_open( + self.ctx, + self.fd, + wasi::__WASI_LOOKUPFLAGS_SYMLINK_FOLLOW, + path.as_os_str().as_bytes(), + path.as_os_str().len(), + wasi::__WASI_OFLAGS_CREAT | wasi::__WASI_OFLAGS_TRUNC, + !0, + !0, + 0, + &mut fd, + ))?; + */ + + let ctx = self.ctx; + Ok(unsafe { File::from_raw_wasi_fd(ctx, fd) }) + } + + /// Returns an iterator over the entries within a directory. + /// + /// This corresponds to [`std::fs::read_dir`], but reads the directory + /// represented by `self`. + /// + /// TODO: Not yet implemented. We may need to wait until we have the ability + /// to duplicate file descriptors before we can implement read safely. For + /// now, use `into_read` instead. + /// + /// [`std::fs::read_dir`]: https://doc.rust-lang.org/std/fs/fn.read_dir.html + pub fn read(&mut self) -> io::Result { + unimplemented!("Dir::read") + } + + /// Consumes self and returns an iterator over the entries within a directory + /// in the manner of `read`. + pub fn into_read(self) -> ReadDir { + unsafe { ReadDir::from_raw_wasi_fd(self.fd) } + } + + /// Read the entire contents of a file into a bytes vector. + /// + /// This corresponds to [`std::fs::read`], but only accesses paths + /// relative to and within `self`. + /// + /// [`std::fs::read`]: https://doc.rust-lang.org/std/fs/fn.read.html + pub fn read_file>(&mut self, path: P) -> io::Result> { + use io::Read; + let mut file = self.open_file(path)?; + let mut bytes = Vec::with_capacity(initial_buffer_size(&file)); + file.read_to_end(&mut bytes)?; + Ok(bytes) + } + + /// Returns an iterator over the entries within a directory. + /// + /// This corresponds to [`std::fs::read_dir`], but only accesses paths + /// relative to and within `self`. + /// + /// [`std::fs::read_dir`]: https://doc.rust-lang.org/std/fs/fn.read_dir.html + pub fn read_dir>(&mut self, path: P) -> io::Result { + self.open_dir(path)?.read() + } +} + +impl<'ctx> Drop for Dir<'ctx> { + fn drop(&mut self) { + // Note that errors are ignored when closing a file descriptor. The + // reason for this is that if an error occurs we don't actually know if + // the file descriptor was closed or not, and if we retried (for + // something like EINTR), we might close another valid file descriptor + // opened after we closed ours. + let _ = unsafe { hostcalls::fd_close(self.ctx, &mut [], self.fd) }; + } +} + +/// Indicates how large a buffer to pre-allocate before reading the entire file. +/// +/// Derived from the function of the same name in libstd. +fn initial_buffer_size(file: &File) -> usize { + // Allocate one extra byte so the buffer doesn't need to grow before the + // final `read` call at the end of the file. Don't worry about `usize` + // overflow because reading will fail regardless in that case. + file.metadata().map(|m| m.len() as usize + 1).unwrap_or(0) +} + +// TODO: impl Debug for Dir diff --git a/crates/wasi-common/src/fs/dir_builder.rs b/crates/wasi-common/src/fs/dir_builder.rs new file mode 100644 index 0000000000..aaf44b7345 --- /dev/null +++ b/crates/wasi-common/src/fs/dir_builder.rs @@ -0,0 +1,49 @@ +use std::{io, path::Path}; + +/// A builder used to create directories in various manners. +/// +/// This corresponds to [`std::fs::DirBuilder`]. +/// +/// TODO: Not yet implemented. +/// +/// [`std::fs::DirBuilder`]: https://doc.rust-lang.org/std/fs/struct.DirBuilder.html +pub struct DirBuilder {} + +impl DirBuilder { + /// Creates a new set of options with default mode/security settings for all platforms and also non-recursive. + /// + /// This corresponds to [`std::fs::DirBuilder::new`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::DirBuilder::new`]: https://doc.rust-lang.org/std/fs/struct.DirBuilder.html#method.new + pub fn new() -> Self { + unimplemented!("DirBuilder::new"); + } + + /// Indicates that directories should be created recursively, creating all parent directories. + /// + /// This corresponds to [`std::fs::DirBuilder::recursive`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::DirBuilder::recursive`]: https://doc.rust-lang.org/std/fs/struct.DirBuilder.html#method.recursive + pub fn recursive(&mut self, recursive: bool) -> &mut Self { + unimplemented!("DirBuilder::recursive"); + } + + /// Creates the specified directory with the options configured in this builder. + /// + /// This corresponds to [`std::fs::DirBuilder::create`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::DirBuilder::create`]: https://doc.rust-lang.org/std/fs/struct.DirBuilder.html#method.create + pub fn create>(&self, path: P) -> io::Result<()> { + unimplemented!("DirBuilder::create"); + } +} + +// TODO: functions from DirBuilderExt? + +// TODO: impl Debug for DirBuilder diff --git a/crates/wasi-common/src/fs/dir_entry.rs b/crates/wasi-common/src/fs/dir_entry.rs new file mode 100644 index 0000000000..0c0c34cf80 --- /dev/null +++ b/crates/wasi-common/src/fs/dir_entry.rs @@ -0,0 +1,53 @@ +use crate::fs::{FileType, Metadata}; +use std::{ffi, io}; + +/// Entries returned by the ReadDir iterator. +/// +/// This corresponds to [`std::fs::DirEntry`]. +/// +/// Unlike `std::fs::DirEntry`, this API has no `DirEntry::path`, because +/// absolute paths don't interoperate well with the capability-oriented +/// security model. +/// +/// TODO: Not yet implemented. +/// +/// [`std::fs::DirEntry`]: https://doc.rust-lang.org/std/fs/struct.DirEntry.html +pub struct DirEntry {} + +impl DirEntry { + /// Returns the metadata for the file that this entry points at. + /// + /// This corresponds to [`std::fs::DirEntry::metadata`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::DirEntry::metadata`]: https://doc.rust-lang.org/std/fs/struct.DirEntry.html#method.metadata + pub fn metadata(&self) -> io::Result { + unimplemented!("DirEntry::metadata"); + } + + /// Returns the file type for the file that this entry points at. + /// + /// This to [`std::fs::DirEntry::file_type`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::DirEntry::file_type`]: https://doc.rust-lang.org/std/fs/struct.DirEntry.html#method.file_type + pub fn file_type(&self) -> io::Result { + unimplemented!("DirEntry::file_type"); + } + + /// Returns the bare file name of this directory entry without any other leading path component. + /// + /// This corresponds to [`std::fs::DirEntry::file_name`], though it returns + /// `String` rather than `OsString`. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::DirEntry::file_name`]: https://doc.rust-lang.org/std/fs/struct.DirEntry.html#method.file_name + pub fn file_name(&self) -> String { + unimplemented!("DirEntry::file_name"); + } +} + +// TODO: impl Debug for DirEntry diff --git a/crates/wasi-common/src/fs/file.rs b/crates/wasi-common/src/fs/file.rs new file mode 100644 index 0000000000..f87c6acfb9 --- /dev/null +++ b/crates/wasi-common/src/fs/file.rs @@ -0,0 +1,116 @@ +use crate::fs::Metadata; +use crate::{host, hostcalls, hostcalls_impl, wasi, Result, WasiCtx}; +use std::io; + +/// A reference to an open file on the filesystem. +/// +/// This corresponds to [`std::fs::File`]. +/// +/// Note that this `File` has no `open` or `create` methods. To open or create +/// a file, you must first obtain a [`Dir`] containing the file, and then call +/// [`Dir::open_file`] or [`Dir::create_file`]. +/// +/// [`std::fs::File`]: https://doc.rust-lang.org/std/fs/struct.File.html +/// [`Dir`]: struct.Dir.html +/// [`Dir::open_file`]: struct.Dir.html#method.open_file +/// [`Dir::create_file`]: struct.Dir.html#method.create_file +pub struct File<'ctx> { + ctx: &'ctx mut WasiCtx, + fd: wasi::__wasi_fd_t, +} + +impl<'ctx> File<'ctx> { + /// Constructs a new instance of `Self` from the given raw WASI file descriptor. + /// + /// This corresponds to [`std::fs::File::from_raw_fd`]. + /// + /// [`std::fs::File::from_raw_fd`]: https://doc.rust-lang.org/std/fs/struct.File.html#method.from_raw_fd + pub unsafe fn from_raw_wasi_fd(ctx: &'ctx mut WasiCtx, fd: wasi::__wasi_fd_t) -> Self { + Self { ctx, fd } + } + + /// Attempts to sync all OS-internal metadata to disk. + /// + /// This corresponds to [`std::fs::File::sync_all`]. + /// + /// [`std::fs::File::sync_all`]: https://doc.rust-lang.org/std/fs/struct.File.html#method.sync_all + pub fn sync_all(&self) -> Result<()> { + unsafe { + hostcalls_impl::fd_sync(self.ctx, &mut [], self.fd)?; + } + Ok(()) + } + + /// This function is similar to `sync_all`, except that it may not synchronize + /// file metadata to the filesystem. + /// + /// This corresponds to [`std::fs::File::sync_data`]. + /// + /// [`std::fs::File::sync_data`]: https://doc.rust-lang.org/std/fs/struct.File.html#method.sync_data + pub fn sync_data(&self) -> Result<()> { + unsafe { + hostcalls_impl::fd_datasync(self.ctx, &mut [], self.fd)?; + } + Ok(()) + } + + /// Truncates or extends the underlying file, updating the size of this file + /// to become size. + /// + /// This corresponds to [`std::fs::File::set_len`]. + /// + /// [`std::fs::File::set_len`]: https://doc.rust-lang.org/std/fs/struct.File.html#method.set_len + pub fn set_len(&self, size: u64) -> Result<()> { + unsafe { + hostcalls_impl::fd_filestat_set_size(self.ctx, &mut [], self.fd, size)?; + } + Ok(()) + } + + /// Queries metadata about the underlying file. + /// + /// This corresponds to [`std::fs::File::metadata`]. + /// + /// [`std::fs::File::metadata`]: https://doc.rust-lang.org/std/fs/struct.File.html#method.metadata + pub fn metadata(&self) -> Result { + Ok(Metadata {}) + } +} + +impl<'ctx> Drop for File<'ctx> { + fn drop(&mut self) { + // Note that errors are ignored when closing a file descriptor. The + // reason for this is that if an error occurs we don't actually know if + // the file descriptor was closed or not, and if we retried (for + // something like EINTR), we might close another valid file descriptor + // opened after we closed ours. + let _ = unsafe { hostcalls::fd_close(self.ctx, &mut [], self.fd) }; + } +} + +impl<'ctx> io::Read for File<'ctx> { + /// TODO: Not yet implemented. See the comment in `Dir::open_file`. + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let iov = [host::__wasi_iovec_t { + buf: buf.as_mut_ptr() as *mut u8, + buf_len: buf.len(), + }]; + let mut nread = 0; + + // TODO: See the comment in `Dir::open_file`. + unimplemented!("File::read"); + /* + wasi_errno_to_io_error(unsafe { + hostcalls::fd_read(self.ctx, self.fd, &iov, 1, &mut nread) + })?; + */ + + Ok(nread) + } +} + +// TODO: traits to implement: Write, Seek + +// TODO: functions from FileExt? + +// TODO: impl Debug for File diff --git a/crates/wasi-common/src/fs/file_type.rs b/crates/wasi-common/src/fs/file_type.rs new file mode 100644 index 0000000000..a97f53a8e1 --- /dev/null +++ b/crates/wasi-common/src/fs/file_type.rs @@ -0,0 +1,49 @@ +/// A structure representing a type of file with accessors for each file type. +/// It is returned by `Metadata::file_type` method. +/// +/// This corresponds to [`std::fs::FileType`]. +/// +/// TODO: Not yet implemented. +/// +/// [`std::fs::FileType`]: https://doc.rust-lang.org/std/fs/struct.FileType.html +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct FileType {} + +impl FileType { + /// Tests whether this file type represents a directory. + /// + /// This corresponds to [`std::fs::FileType::is_dir`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::FileType::is_dir`]: https://doc.rust-lang.org/std/fs/struct.FileType.html#method.is_dir + pub fn is_dir(&self) -> bool { + unimplemented!("FileType::is_dir"); + } + + /// Tests whether this file type represents a regular file. + /// + /// This corresponds to [`std::fs::FileType::is_file`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::FileType::is_file`]: https://doc.rust-lang.org/std/fs/struct.FileType.html#method.is_file + pub fn is_file(&self) -> bool { + unimplemented!("FileType::is_file"); + } + + /// Tests whether this file type represents a symbolic link. + /// + /// This corresponds to [`std::fs::FileType::is_symlink`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::FileType::is_symlink`]: https://doc.rust-lang.org/std/fs/struct.FileType.html#method.is_symlink + pub fn is_symlink(&self) -> bool { + unimplemented!("FileType::is_symlink"); + } +} + +// TODO: functions from FileTypeExt? + +// TODO: impl Debug for FileType diff --git a/crates/wasi-common/src/fs/metadata.rs b/crates/wasi-common/src/fs/metadata.rs new file mode 100644 index 0000000000..e55afb73a8 --- /dev/null +++ b/crates/wasi-common/src/fs/metadata.rs @@ -0,0 +1,106 @@ +use crate::fs::{FileType, Permissions}; +use std::{io, time::SystemTime}; + +/// Metadata information about a file. +/// +/// This corresponds to [`std::fs::Metadata`]. +/// +/// TODO: Not yet implemented. +/// +/// [`std::fs::Metadata`]: https://doc.rust-lang.org/std/fs/struct.Metadata.html +#[derive(Clone)] +pub struct Metadata {} + +impl Metadata { + /// Returns the file type for this metadata. + /// + /// This corresponds to [`std::fs::Metadata::file_type`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::Metadata::file_type`]: https://doc.rust-lang.org/std/fs/struct.Metadata.html#method.file_type + pub fn file_type(&self) -> FileType { + unimplemented!("Metadata::file_type"); + } + + /// Returns true if this metadata is for a directory. + /// + /// This corresponds to [`std::fs::Metadata::is_dir`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::Metadata::is_dir`]: https://doc.rust-lang.org/std/fs/struct.Metadata.html#method.is_dir + pub fn is_dir(&self) -> bool { + unimplemented!("Metadata::is_dir"); + } + + /// Returns true if this metadata is for a regular file. + /// + /// This corresponds to [`std::fs::Metadata::is_file`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::Metadata::is_file`]: https://doc.rust-lang.org/std/fs/struct.Metadata.html#method.is_file + pub fn is_file(&self) -> bool { + unimplemented!("Metadata::is_file"); + } + + /// Returns the size of the file, in bytes, this metadata is for. + /// + /// This corresponds to [`std::fs::Metadata::len`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::Metadata::len`]: https://doc.rust-lang.org/std/fs/struct.Metadata.html#method.len + pub fn len(&self) -> u64 { + unimplemented!("Metadata::len"); + } + + /// Returns the permissions of the file this metadata is for. + /// + /// This corresponds to [`std::fs::Metadata::permissions`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::Metadata::permissions`]: https://doc.rust-lang.org/std/fs/struct.Metadata.html#method.permissions + pub fn permissions(&self) -> Permissions { + unimplemented!("Metadata::permissions"); + } + + /// Returns the last modification time listed in this metadata. + /// + /// This corresponds to [`std::fs::Metadata::modified`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::Metadata::modified`]: https://doc.rust-lang.org/std/fs/struct.Metadata.html#method.modified + pub fn modified(&self) -> io::Result { + unimplemented!("Metadata::modified"); + } + + /// Returns the last access time of this metadata. + /// + /// This corresponds to [`std::fs::Metadata::accessed`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::Metadata::accessed`]: https://doc.rust-lang.org/std/fs/struct.Metadata.html#method.accessed + pub fn accessed(&self) -> io::Result { + unimplemented!("Metadata::accessed"); + } + + /// Returns the creation time listed in this metadata. + /// + /// This corresponds to [`std::fs::Metadata::created`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::Metadata::created`]: https://doc.rust-lang.org/std/fs/struct.Metadata.html#method.created + pub fn created(&self) -> io::Result { + unimplemented!("Metadata::created"); + } +} + +// TODO: Functions from MetadataExt? + +// TODO: impl Debug for Metadata diff --git a/crates/wasi-common/src/fs/mod.rs b/crates/wasi-common/src/fs/mod.rs new file mode 100644 index 0000000000..c406581431 --- /dev/null +++ b/crates/wasi-common/src/fs/mod.rs @@ -0,0 +1,50 @@ +//! A very experimental module modeled providing a high-level and safe +//! filesystem interface, modeled after `std::fs`, implemented on top of +//! WASI functions. +//! +//! Most functions in this API are not yet implemented! +//! +//! This corresponds to [`std::fs`]. +//! +//! Instead of [`std::fs`'s free functions] which operate on paths, this +//! crate has methods on [`Dir`] which operate on paths which must be +//! relative to and within the directory. +//! +//! Since all functions which expose raw file descriptors are `unsafe`, +//! I/O handles in this API are unforgeable (unsafe code notwithstanding). +//! This combined with WASI's lack of absolute paths provides a natural +//! capability-oriented interface. +//! +//! [`std::fs`]: https://doc.rust-lang.org/std/fs/index.html +//! [`std::fs`'s free functions]: https://doc.rust-lang.org/std/fs/index.html#functions +//! [`DIR`]: struct.Dir.html + +// TODO: When more things are implemented, remove these. +#![allow( + unused_imports, + unreachable_code, + unused_variables, + unused_mut, + unused_unsafe, + dead_code +)] + +mod dir; +mod dir_builder; +mod dir_entry; +mod file; +mod file_type; +mod metadata; +mod open_options; +mod permissions; +mod readdir; + +pub use dir::*; +pub use dir_builder::*; +pub use dir_entry::*; +pub use file::*; +pub use file_type::*; +pub use metadata::*; +pub use open_options::*; +pub use permissions::*; +pub use readdir::*; diff --git a/crates/wasi-common/src/fs/open_options.rs b/crates/wasi-common/src/fs/open_options.rs new file mode 100644 index 0000000000..0de1a43841 --- /dev/null +++ b/crates/wasi-common/src/fs/open_options.rs @@ -0,0 +1,99 @@ +/// Options and flags which can be used to configure how a file is opened. +/// +/// This corresponds to [`std::fs::OpenOptions`]. +/// +/// Note that this `OpenOptions` has no `open` method. To open a file with +/// an `OptionOptions`, you must first obtain a [`Dir`] containing the file, and +/// then call [`Dir::open_file_with`]. +/// +/// [`std::fs::OpenOptions`]: https://doc.rust-lang.org/std/fs/struct.OpenOptions.html +/// [`Dir`]: struct.Dir.html +/// [`Dir::open_file_with`]: struct.Dir.html#method.open_file_with +pub struct OpenOptions { + pub(crate) read: bool, + pub(crate) write: bool, + pub(crate) append: bool, + pub(crate) truncate: bool, + pub(crate) create: bool, + pub(crate) create_new: bool, +} + +impl OpenOptions { + /// Creates a blank new set of options ready for configuration. + /// + /// This corresponds to [`std::fs::OpenOptions::new`]. + /// + /// [`std::fs::OpenOptions::new`]: https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.new + pub fn new() -> Self { + Self { + read: false, + write: false, + append: false, + truncate: false, + create: false, + create_new: false, + } + } + + /// Sets the option for read access. + /// + /// This corresponds to [`std::fs::OpenOptions::read`]. + /// + /// [`std::fs::OpenOptions::read`]: https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.read + pub fn read(&mut self, read: bool) -> &mut Self { + self.read = read; + self + } + + /// Sets the option for write access. + /// + /// This corresponds to [`std::fs::OpenOptions::write`]. + /// + /// [`std::fs::OpenOptions::write`]: https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.write + pub fn write(&mut self, write: bool) -> &mut Self { + self.write = write; + self + } + + /// Sets the option for the append mode. + /// + /// This corresponds to [`std::fs::OpenOptions::append`]. + /// + /// [`std::fs::OpenOptions::append`]: https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.append + pub fn append(&mut self, append: bool) -> &mut Self { + self.append = append; + self + } + + /// Sets the option for truncating a previous file. + /// + /// This corresponds to [`std::fs::OpenOptions::truncate`]. + /// + /// [`std::fs::OpenOptions::truncate`]: https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.truncate + pub fn truncate(&mut self, truncate: bool) -> &mut Self { + self.truncate = truncate; + self + } + + /// Sets the option to create a new file. + /// + /// This corresponds to [`std::fs::OpenOptions::create`]. + /// + /// [`std::fs::OpenOptions::create`]: https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.create + pub fn create(&mut self, create: bool) -> &mut Self { + self.create = create; + self + } + + /// Sets the option to always create a new file. + /// + /// This corresponds to [`std::fs::OpenOptions::create_new`]. + /// + /// [`std::fs::OpenOptions::create_new`]: https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.create_new + pub fn create_new(&mut self, create_new: bool) -> &mut Self { + self.create_new = create_new; + self + } +} + +// TODO: Functions from OpenOptionsExt? diff --git a/crates/wasi-common/src/fs/permissions.rs b/crates/wasi-common/src/fs/permissions.rs new file mode 100644 index 0000000000..498a1898ff --- /dev/null +++ b/crates/wasi-common/src/fs/permissions.rs @@ -0,0 +1,37 @@ +/// Representation of the various permissions on a file. +/// +/// This corresponds to [`std::fs::Permissions`]. +/// +/// TODO: Not yet implemented. +/// +/// [`std::fs::Permissions`]: https://doc.rust-lang.org/std/fs/struct.Permissions.html +#[derive(Eq, PartialEq, Clone)] +pub struct Permissions {} + +impl Permissions { + /// Returns true if these permissions describe a readonly (unwritable) file. + /// + /// This corresponds to [`std::fs::Permissions::readonly`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::Permissions::readonly`]: https://doc.rust-lang.org/std/fs/struct.Permissions.html#method.readonly + pub fn readonly(&self) -> bool { + unimplemented!("Permissions::readonly"); + } + + /// Modifies the readonly flag for this set of permissions. + /// + /// This corresponds to [`std::fs::Permissions::set_readonly`]. + /// + /// TODO: Not yet implemented. + /// + /// [`std::fs::Permissions::set_readonly`]: https://doc.rust-lang.org/std/fs/struct.Permissions.html#method.set_readonly + pub fn set_readonly(&mut self, readonly: bool) { + unimplemented!("Permissions::set_readonly"); + } +} + +// TODO: functions from PermissionsExt? + +// TODO: impl Debug for Permissions diff --git a/crates/wasi-common/src/fs/readdir.rs b/crates/wasi-common/src/fs/readdir.rs new file mode 100644 index 0000000000..4a5ce7196c --- /dev/null +++ b/crates/wasi-common/src/fs/readdir.rs @@ -0,0 +1,32 @@ +use crate::fs::DirEntry; +use crate::{hostcalls, wasi}; + +/// Iterator over the entries in a directory. +/// +/// This corresponds to [`std::fs::ReadDir`]. +/// +/// TODO: Not yet implemented. +/// +/// [`std::fs::ReadDir`]: https://doc.rust-lang.org/std/fs/struct.ReadDir.html +pub struct ReadDir { + fd: wasi::__wasi_fd_t, +} + +impl ReadDir { + /// Constructs a new instance of `Self` from the given raw WASI file descriptor. + pub unsafe fn from_raw_wasi_fd(fd: wasi::__wasi_fd_t) -> Self { + Self { fd } + } +} + +/// TODO: Not yet implemented. +impl Iterator for ReadDir { + type Item = DirEntry; + + /// TODO: Not yet implemented. + fn next(&mut self) -> Option { + unimplemented!("ReadDir::next"); + } +} + +// TODO: impl Debug for ReadDir diff --git a/crates/wasi-common/src/helpers.rs b/crates/wasi-common/src/helpers.rs new file mode 100644 index 0000000000..f176b8d1fe --- /dev/null +++ b/crates/wasi-common/src/helpers.rs @@ -0,0 +1,10 @@ +use crate::{Error, Result}; +use std::str; + +/// Creates not-owned WASI path from byte slice. +/// +/// NB WASI spec requires bytes to be valid UTF-8. Otherwise, +/// `__WASI_ERRNO_ILSEQ` error is returned. +pub(crate) fn path_from_slice<'a>(s: &'a [u8]) -> Result<&'a str> { + str::from_utf8(s).map_err(|_| Error::EILSEQ) +} diff --git a/crates/wasi-common/src/host.rs b/crates/wasi-common/src/host.rs new file mode 100644 index 0000000000..946d7feb12 --- /dev/null +++ b/crates/wasi-common/src/host.rs @@ -0,0 +1,80 @@ +//! WASI host types. These are types that contain raw pointers and `usize` +//! values, and so are platform-specific. + +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] + +use crate::wasi::*; +use crate::{Error, Result}; +use std::{convert::TryInto, io, mem, slice}; +use wig::witx_host_types; + +witx_host_types!("snapshot" "wasi_snapshot_preview1"); + +pub(crate) unsafe fn ciovec_to_host(ciovec: &__wasi_ciovec_t) -> io::IoSlice { + let slice = slice::from_raw_parts(ciovec.buf as *const u8, ciovec.buf_len); + io::IoSlice::new(slice) +} + +pub(crate) unsafe fn iovec_to_host_mut(iovec: &mut __wasi_iovec_t) -> io::IoSliceMut { + let slice = slice::from_raw_parts_mut(iovec.buf as *mut u8, iovec.buf_len); + io::IoSliceMut::new(slice) +} + +#[allow(dead_code)] // trouble with sockets +#[derive(Clone, Copy, Debug)] +#[repr(u8)] +pub(crate) enum FileType { + Unknown = __WASI_FILETYPE_UNKNOWN, + BlockDevice = __WASI_FILETYPE_BLOCK_DEVICE, + CharacterDevice = __WASI_FILETYPE_CHARACTER_DEVICE, + Directory = __WASI_FILETYPE_DIRECTORY, + RegularFile = __WASI_FILETYPE_REGULAR_FILE, + SocketDgram = __WASI_FILETYPE_SOCKET_DGRAM, + SocketStream = __WASI_FILETYPE_SOCKET_STREAM, + Symlink = __WASI_FILETYPE_SYMBOLIC_LINK, +} + +impl FileType { + pub(crate) fn to_wasi(&self) -> __wasi_filetype_t { + *self as __wasi_filetype_t + } +} + +#[derive(Debug, Clone)] +pub(crate) struct Dirent { + pub name: String, + pub ftype: FileType, + pub ino: u64, + pub cookie: __wasi_dircookie_t, +} + +impl Dirent { + /// Serialize the directory entry to the format define by `__wasi_fd_readdir`, + /// so that the serialized entries can be concatenated by the implementation. + pub fn to_wasi_raw(&self) -> Result> { + let name = self.name.as_bytes(); + let namlen = name.len(); + let dirent_size = mem::size_of::<__wasi_dirent_t>(); + let offset = dirent_size.checked_add(namlen).ok_or(Error::EOVERFLOW)?; + + let mut raw = Vec::::with_capacity(offset); + raw.resize(offset, 0); + + let sys_dirent = raw.as_mut_ptr() as *mut __wasi_dirent_t; + unsafe { + sys_dirent.write_unaligned(__wasi_dirent_t { + d_namlen: namlen.try_into()?, + d_ino: self.ino, + d_next: self.cookie, + d_type: self.ftype.to_wasi(), + }); + } + + let sys_name = unsafe { sys_dirent.offset(1) as *mut u8 }; + let sys_name = unsafe { slice::from_raw_parts_mut(sys_name, namlen) }; + sys_name.copy_from_slice(&name); + + Ok(raw) + } +} diff --git a/crates/wasi-common/src/hostcalls_impl/fs.rs b/crates/wasi-common/src/hostcalls_impl/fs.rs new file mode 100644 index 0000000000..d5ecb8bc69 --- /dev/null +++ b/crates/wasi-common/src/hostcalls_impl/fs.rs @@ -0,0 +1,1092 @@ +#![allow(non_camel_case_types)] +use super::fs_helpers::path_get; +use crate::ctx::WasiCtx; +use crate::fdentry::{Descriptor, FdEntry}; +use crate::helpers::*; +use crate::memory::*; +use crate::sandboxed_tty_writer::SandboxedTTYWriter; +use crate::sys::hostcalls_impl::fs_helpers::path_open_rights; +use crate::sys::{host_impl, hostcalls_impl}; +use crate::{helpers, host, wasi, wasi32, Error, Result}; +use filetime::{set_file_handle_times, FileTime}; +use log::trace; +use std::fs::File; +use std::io::{self, Read, Seek, SeekFrom, Write}; +use std::ops::DerefMut; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +pub(crate) unsafe fn fd_close( + wasi_ctx: &mut WasiCtx, + _memory: &mut [u8], + fd: wasi::__wasi_fd_t, +) -> Result<()> { + trace!("fd_close(fd={:?})", fd); + + if let Ok(fe) = wasi_ctx.get_fd_entry(fd) { + // can't close preopened files + if fe.preopen_path.is_some() { + return Err(Error::ENOTSUP); + } + } + + wasi_ctx.remove_fd_entry(fd)?; + Ok(()) +} + +pub(crate) unsafe fn fd_datasync( + wasi_ctx: &WasiCtx, + _memory: &mut [u8], + fd: wasi::__wasi_fd_t, +) -> Result<()> { + trace!("fd_datasync(fd={:?})", fd); + + let fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor(wasi::__WASI_RIGHTS_FD_DATASYNC, 0)? + .as_file()?; + + fd.sync_data().map_err(Into::into) +} + +pub(crate) unsafe fn fd_pread( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + iovs_ptr: wasi32::uintptr_t, + iovs_len: wasi32::size_t, + offset: wasi::__wasi_filesize_t, + nread: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "fd_pread(fd={:?}, iovs_ptr={:#x?}, iovs_len={:?}, offset={}, nread={:#x?})", + fd, + iovs_ptr, + iovs_len, + offset, + nread + ); + + let fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor(wasi::__WASI_RIGHTS_FD_READ | wasi::__WASI_RIGHTS_FD_SEEK, 0)? + .as_file()?; + + let iovs = dec_iovec_slice(memory, iovs_ptr, iovs_len)?; + + if offset > i64::max_value() as u64 { + return Err(Error::EIO); + } + let buf_size = iovs.iter().map(|v| v.buf_len).sum(); + let mut buf = vec![0; buf_size]; + let host_nread = hostcalls_impl::fd_pread(fd, &mut buf, offset)?; + let mut buf_offset = 0; + let mut left = host_nread; + for iov in &iovs { + if left == 0 { + break; + } + let vec_len = std::cmp::min(iov.buf_len, left); + std::slice::from_raw_parts_mut(iov.buf as *mut u8, vec_len) + .copy_from_slice(&buf[buf_offset..buf_offset + vec_len]); + buf_offset += vec_len; + left -= vec_len; + } + + trace!(" | *nread={:?}", host_nread); + + enc_usize_byref(memory, nread, host_nread) +} + +pub(crate) unsafe fn fd_pwrite( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + iovs_ptr: wasi32::uintptr_t, + iovs_len: wasi32::size_t, + offset: wasi::__wasi_filesize_t, + nwritten: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "fd_pwrite(fd={:?}, iovs_ptr={:#x?}, iovs_len={:?}, offset={}, nwritten={:#x?})", + fd, + iovs_ptr, + iovs_len, + offset, + nwritten + ); + + let fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor( + wasi::__WASI_RIGHTS_FD_WRITE | wasi::__WASI_RIGHTS_FD_SEEK, + 0, + )? + .as_file()?; + let iovs = dec_ciovec_slice(memory, iovs_ptr, iovs_len)?; + + if offset > i64::max_value() as u64 { + return Err(Error::EIO); + } + let buf_size = iovs.iter().map(|v| v.buf_len).sum(); + let mut buf = Vec::with_capacity(buf_size); + for iov in &iovs { + buf.extend_from_slice(std::slice::from_raw_parts( + iov.buf as *const u8, + iov.buf_len, + )); + } + let host_nwritten = hostcalls_impl::fd_pwrite(fd, &buf, offset)?; + + trace!(" | *nwritten={:?}", host_nwritten); + + enc_usize_byref(memory, nwritten, host_nwritten) +} + +pub(crate) unsafe fn fd_read( + wasi_ctx: &mut WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + iovs_ptr: wasi32::uintptr_t, + iovs_len: wasi32::size_t, + nread: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "fd_read(fd={:?}, iovs_ptr={:#x?}, iovs_len={:?}, nread={:#x?})", + fd, + iovs_ptr, + iovs_len, + nread + ); + + let mut iovs = dec_iovec_slice(memory, iovs_ptr, iovs_len)?; + let mut iovs: Vec = iovs + .iter_mut() + .map(|vec| host::iovec_to_host_mut(vec)) + .collect(); + + let maybe_host_nread = match wasi_ctx + .get_fd_entry_mut(fd)? + .as_descriptor_mut(wasi::__WASI_RIGHTS_FD_READ, 0)? + { + Descriptor::OsHandle(file) => file.read_vectored(&mut iovs), + Descriptor::Stdin => io::stdin().read_vectored(&mut iovs), + _ => return Err(Error::EBADF), + }; + + let host_nread = maybe_host_nread?; + + trace!(" | *nread={:?}", host_nread); + + enc_usize_byref(memory, nread, host_nread) +} + +pub(crate) unsafe fn fd_renumber( + wasi_ctx: &mut WasiCtx, + _memory: &mut [u8], + from: wasi::__wasi_fd_t, + to: wasi::__wasi_fd_t, +) -> Result<()> { + trace!("fd_renumber(from={:?}, to={:?})", from, to); + + if !wasi_ctx.contains_fd_entry(from) { + return Err(Error::EBADF); + } + + // Don't allow renumbering over a pre-opened resource. + // TODO: Eventually, we do want to permit this, once libpreopen in + // userspace is capable of removing entries from its tables as well. + let from_fe = wasi_ctx.get_fd_entry(from)?; + if from_fe.preopen_path.is_some() { + return Err(Error::ENOTSUP); + } + if let Ok(to_fe) = wasi_ctx.get_fd_entry(to) { + if to_fe.preopen_path.is_some() { + return Err(Error::ENOTSUP); + } + } + + let fe = wasi_ctx.remove_fd_entry(from)?; + wasi_ctx.insert_fd_entry_at(to, fe); + + Ok(()) +} + +pub(crate) unsafe fn fd_seek( + wasi_ctx: &mut WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + offset: wasi::__wasi_filedelta_t, + whence: wasi::__wasi_whence_t, + newoffset: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "fd_seek(fd={:?}, offset={:?}, whence={}, newoffset={:#x?})", + fd, + offset, + wasi::whence_to_str(whence), + newoffset + ); + + let rights = if offset == 0 && whence == wasi::__WASI_WHENCE_CUR { + wasi::__WASI_RIGHTS_FD_TELL + } else { + wasi::__WASI_RIGHTS_FD_SEEK | wasi::__WASI_RIGHTS_FD_TELL + }; + let fd = wasi_ctx + .get_fd_entry_mut(fd)? + .as_descriptor_mut(rights, 0)? + .as_file_mut()?; + + let pos = match whence { + wasi::__WASI_WHENCE_CUR => SeekFrom::Current(offset), + wasi::__WASI_WHENCE_END => SeekFrom::End(offset), + wasi::__WASI_WHENCE_SET => SeekFrom::Start(offset as u64), + _ => return Err(Error::EINVAL), + }; + let host_newoffset = fd.seek(pos)?; + + trace!(" | *newoffset={:?}", host_newoffset); + + enc_filesize_byref(memory, newoffset, host_newoffset) +} + +pub(crate) unsafe fn fd_tell( + wasi_ctx: &mut WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + newoffset: wasi32::uintptr_t, +) -> Result<()> { + trace!("fd_tell(fd={:?}, newoffset={:#x?})", fd, newoffset); + + let fd = wasi_ctx + .get_fd_entry_mut(fd)? + .as_descriptor_mut(wasi::__WASI_RIGHTS_FD_TELL, 0)? + .as_file_mut()?; + + let host_offset = fd.seek(SeekFrom::Current(0))?; + + trace!(" | *newoffset={:?}", host_offset); + + enc_filesize_byref(memory, newoffset, host_offset) +} + +pub(crate) unsafe fn fd_fdstat_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + fdstat_ptr: wasi32::uintptr_t, // *mut wasi::__wasi_fdstat_t +) -> Result<()> { + trace!("fd_fdstat_get(fd={:?}, fdstat_ptr={:#x?})", fd, fdstat_ptr); + + let mut fdstat = dec_fdstat_byref(memory, fdstat_ptr)?; + let host_fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor(0, 0)? + .as_os_handle(); + + let fs_flags = hostcalls_impl::fd_fdstat_get(&host_fd)?; + + let fe = wasi_ctx.get_fd_entry(fd)?; + fdstat.fs_filetype = fe.file_type; + fdstat.fs_rights_base = fe.rights_base; + fdstat.fs_rights_inheriting = fe.rights_inheriting; + fdstat.fs_flags = fs_flags; + + trace!(" | *buf={:?}", fdstat); + + enc_fdstat_byref(memory, fdstat_ptr, fdstat) +} + +pub(crate) unsafe fn fd_fdstat_set_flags( + wasi_ctx: &mut WasiCtx, + _memory: &mut [u8], + fd: wasi::__wasi_fd_t, + fdflags: wasi::__wasi_fdflags_t, +) -> Result<()> { + trace!("fd_fdstat_set_flags(fd={:?}, fdflags={:#x?})", fd, fdflags); + + let descriptor = wasi_ctx + .get_fd_entry_mut(fd)? + .as_descriptor_mut(wasi::__WASI_RIGHTS_FD_FDSTAT_SET_FLAGS, 0)?; + + if let Some(new_handle) = + hostcalls_impl::fd_fdstat_set_flags(&descriptor.as_os_handle(), fdflags)? + { + *descriptor = Descriptor::OsHandle(new_handle); + } + + Ok(()) +} + +pub(crate) unsafe fn fd_fdstat_set_rights( + wasi_ctx: &mut WasiCtx, + _memory: &mut [u8], + fd: wasi::__wasi_fd_t, + fs_rights_base: wasi::__wasi_rights_t, + fs_rights_inheriting: wasi::__wasi_rights_t, +) -> Result<()> { + trace!( + "fd_fdstat_set_rights(fd={:?}, fs_rights_base={:#x?}, fs_rights_inheriting={:#x?})", + fd, + fs_rights_base, + fs_rights_inheriting + ); + + let fe = wasi_ctx.get_fd_entry_mut(fd)?; + if fe.rights_base & fs_rights_base != fs_rights_base + || fe.rights_inheriting & fs_rights_inheriting != fs_rights_inheriting + { + return Err(Error::ENOTCAPABLE); + } + fe.rights_base = fs_rights_base; + fe.rights_inheriting = fs_rights_inheriting; + + Ok(()) +} + +pub(crate) unsafe fn fd_sync( + wasi_ctx: &WasiCtx, + _memory: &mut [u8], + fd: wasi::__wasi_fd_t, +) -> Result<()> { + trace!("fd_sync(fd={:?})", fd); + + let fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor(wasi::__WASI_RIGHTS_FD_SYNC, 0)? + .as_file()?; + fd.sync_all().map_err(Into::into) +} + +pub(crate) unsafe fn fd_write( + wasi_ctx: &mut WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + iovs_ptr: wasi32::uintptr_t, + iovs_len: wasi32::size_t, + nwritten: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "fd_write(fd={:?}, iovs_ptr={:#x?}, iovs_len={:?}, nwritten={:#x?})", + fd, + iovs_ptr, + iovs_len, + nwritten + ); + + let iovs = dec_ciovec_slice(memory, iovs_ptr, iovs_len)?; + let iovs: Vec = iovs.iter().map(|vec| host::ciovec_to_host(vec)).collect(); + + // perform unbuffered writes + let entry = wasi_ctx.get_fd_entry_mut(fd)?; + let isatty = entry.isatty(); + let desc = entry.as_descriptor_mut(wasi::__WASI_RIGHTS_FD_WRITE, 0)?; + let host_nwritten = match desc { + Descriptor::OsHandle(file) => { + if isatty { + SandboxedTTYWriter::new(file.deref_mut()).write_vectored(&iovs)? + } else { + file.write_vectored(&iovs)? + } + } + Descriptor::Stdin => return Err(Error::EBADF), + Descriptor::Stdout => { + // lock for the duration of the scope + let stdout = io::stdout(); + let mut stdout = stdout.lock(); + let nwritten = if isatty { + SandboxedTTYWriter::new(&mut stdout).write_vectored(&iovs)? + } else { + stdout.write_vectored(&iovs)? + }; + stdout.flush()?; + nwritten + } + // Always sanitize stderr, even if it's not directly connected to a tty, + // because stderr is meant for diagnostics rather than binary output, + // and may be redirected to a file which could end up being displayed + // on a tty later. + Descriptor::Stderr => SandboxedTTYWriter::new(&mut io::stderr()).write_vectored(&iovs)?, + }; + + trace!(" | *nwritten={:?}", host_nwritten); + + enc_usize_byref(memory, nwritten, host_nwritten) +} + +pub(crate) unsafe fn fd_advise( + wasi_ctx: &WasiCtx, + _memory: &mut [u8], + fd: wasi::__wasi_fd_t, + offset: wasi::__wasi_filesize_t, + len: wasi::__wasi_filesize_t, + advice: wasi::__wasi_advice_t, +) -> Result<()> { + trace!( + "fd_advise(fd={:?}, offset={}, len={}, advice={:?})", + fd, + offset, + len, + advice + ); + + let fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor(wasi::__WASI_RIGHTS_FD_ADVISE, 0)? + .as_file()?; + + hostcalls_impl::fd_advise(fd, advice, offset, len) +} + +pub(crate) unsafe fn fd_allocate( + wasi_ctx: &WasiCtx, + _memory: &mut [u8], + fd: wasi::__wasi_fd_t, + offset: wasi::__wasi_filesize_t, + len: wasi::__wasi_filesize_t, +) -> Result<()> { + trace!("fd_allocate(fd={:?}, offset={}, len={})", fd, offset, len); + + let fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor(wasi::__WASI_RIGHTS_FD_ALLOCATE, 0)? + .as_file()?; + + let metadata = fd.metadata()?; + + let current_size = metadata.len(); + let wanted_size = offset.checked_add(len).ok_or(Error::E2BIG)?; + // This check will be unnecessary when rust-lang/rust#63326 is fixed + if wanted_size > i64::max_value() as u64 { + return Err(Error::E2BIG); + } + + if wanted_size > current_size { + fd.set_len(wanted_size).map_err(Into::into) + } else { + Ok(()) + } +} + +pub(crate) unsafe fn path_create_directory( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, +) -> Result<()> { + trace!( + "path_create_directory(dirfd={:?}, path_ptr={:#x?}, path_len={})", + dirfd, + path_ptr, + path_len, + ); + + let path = dec_slice_of_u8(memory, path_ptr, path_len).and_then(helpers::path_from_slice)?; + + trace!(" | (path_ptr,path_len)='{}'", path); + + let rights = wasi::__WASI_RIGHTS_PATH_OPEN | wasi::__WASI_RIGHTS_PATH_CREATE_DIRECTORY; + let fe = wasi_ctx.get_fd_entry(dirfd)?; + let resolved = path_get(fe, rights, 0, 0, path, false)?; + + hostcalls_impl::path_create_directory(resolved) +} + +pub(crate) unsafe fn path_link( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + old_dirfd: wasi::__wasi_fd_t, + old_flags: wasi::__wasi_lookupflags_t, + old_path_ptr: wasi32::uintptr_t, + old_path_len: wasi32::size_t, + new_dirfd: wasi::__wasi_fd_t, + new_path_ptr: wasi32::uintptr_t, + new_path_len: wasi32::size_t, +) -> Result<()> { + trace!( + "path_link(old_dirfd={:?}, old_flags={:?}, old_path_ptr={:#x?}, old_path_len={}, new_dirfd={:?}, new_path_ptr={:#x?}, new_path_len={})", + old_dirfd, + old_flags, + old_path_ptr, + old_path_len, + new_dirfd, + new_path_ptr, + new_path_len, + ); + + let old_path = dec_slice_of_u8(memory, old_path_ptr, old_path_len).and_then(path_from_slice)?; + let new_path = dec_slice_of_u8(memory, new_path_ptr, new_path_len).and_then(path_from_slice)?; + + trace!(" | (old_path_ptr,old_path_len)='{}'", old_path); + trace!(" | (new_path_ptr,new_path_len)='{}'", new_path); + + let old_fe = wasi_ctx.get_fd_entry(old_dirfd)?; + let new_fe = wasi_ctx.get_fd_entry(new_dirfd)?; + let resolved_old = path_get( + old_fe, + wasi::__WASI_RIGHTS_PATH_LINK_SOURCE, + 0, + 0, + old_path, + false, + )?; + let resolved_new = path_get( + new_fe, + wasi::__WASI_RIGHTS_PATH_LINK_TARGET, + 0, + 0, + new_path, + false, + )?; + + hostcalls_impl::path_link(resolved_old, resolved_new) +} + +pub(crate) unsafe fn path_open( + wasi_ctx: &mut WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + dirflags: wasi::__wasi_lookupflags_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, + oflags: wasi::__wasi_oflags_t, + fs_rights_base: wasi::__wasi_rights_t, + fs_rights_inheriting: wasi::__wasi_rights_t, + fs_flags: wasi::__wasi_fdflags_t, + fd_out_ptr: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "path_open(dirfd={:?}, dirflags={:?}, path_ptr={:#x?}, path_len={:?}, oflags={:#x?}, fs_rights_base={:#x?}, fs_rights_inheriting={:#x?}, fs_flags={:#x?}, fd_out_ptr={:#x?})", + dirfd, + dirflags, + path_ptr, + path_len, + oflags, + fs_rights_base, + fs_rights_inheriting, + fs_flags, + fd_out_ptr + ); + + // pre-encode fd_out_ptr to -1 in case of error in opening a path + enc_fd_byref(memory, fd_out_ptr, wasi::__wasi_fd_t::max_value())?; + + let path = dec_slice_of_u8(memory, path_ptr, path_len).and_then(path_from_slice)?; + + trace!(" | (path_ptr,path_len)='{}'", path); + + let (needed_base, needed_inheriting) = + path_open_rights(fs_rights_base, fs_rights_inheriting, oflags, fs_flags); + trace!( + " | needed_base = {}, needed_inheriting = {}", + needed_base, + needed_inheriting + ); + let fe = wasi_ctx.get_fd_entry(dirfd)?; + let resolved = path_get( + fe, + needed_base, + needed_inheriting, + dirflags, + path, + oflags & wasi::__WASI_OFLAGS_CREAT != 0, + )?; + + // which open mode do we need? + let read = fs_rights_base & (wasi::__WASI_RIGHTS_FD_READ | wasi::__WASI_RIGHTS_FD_READDIR) != 0; + let write = fs_rights_base + & (wasi::__WASI_RIGHTS_FD_DATASYNC + | wasi::__WASI_RIGHTS_FD_WRITE + | wasi::__WASI_RIGHTS_FD_ALLOCATE + | wasi::__WASI_RIGHTS_FD_FILESTAT_SET_SIZE) + != 0; + + trace!( + " | calling path_open impl: read={}, write={}", + read, + write + ); + let fd = hostcalls_impl::path_open(resolved, read, write, oflags, fs_flags)?; + + let mut fe = FdEntry::from(fd)?; + // We need to manually deny the rights which are not explicitly requested + // because FdEntry::from will assign maximal consistent rights. + fe.rights_base &= fs_rights_base; + fe.rights_inheriting &= fs_rights_inheriting; + let guest_fd = wasi_ctx.insert_fd_entry(fe)?; + + trace!(" | *fd={:?}", guest_fd); + + enc_fd_byref(memory, fd_out_ptr, guest_fd) +} + +pub(crate) unsafe fn path_readlink( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, + buf_ptr: wasi32::uintptr_t, + buf_len: wasi32::size_t, + buf_used: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "path_readlink(dirfd={:?}, path_ptr={:#x?}, path_len={:?}, buf_ptr={:#x?}, buf_len={}, buf_used={:#x?})", + dirfd, + path_ptr, + path_len, + buf_ptr, + buf_len, + buf_used, + ); + + enc_usize_byref(memory, buf_used, 0)?; + + let path = dec_slice_of_u8(memory, path_ptr, path_len).and_then(helpers::path_from_slice)?; + + trace!(" | (path_ptr,path_len)='{}'", &path); + + let fe = wasi_ctx.get_fd_entry(dirfd)?; + let resolved = path_get(fe, wasi::__WASI_RIGHTS_PATH_READLINK, 0, 0, &path, false)?; + + let mut buf = dec_slice_of_mut_u8(memory, buf_ptr, buf_len)?; + + let host_bufused = hostcalls_impl::path_readlink(resolved, &mut buf)?; + + trace!(" | (buf_ptr,*buf_used)={:?}", buf); + trace!(" | *buf_used={:?}", host_bufused); + + enc_usize_byref(memory, buf_used, host_bufused) +} + +pub(crate) unsafe fn path_rename( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + old_dirfd: wasi::__wasi_fd_t, + old_path_ptr: wasi32::uintptr_t, + old_path_len: wasi32::size_t, + new_dirfd: wasi::__wasi_fd_t, + new_path_ptr: wasi32::uintptr_t, + new_path_len: wasi32::size_t, +) -> Result<()> { + trace!( + "path_rename(old_dirfd={:?}, old_path_ptr={:#x?}, old_path_len={:?}, new_dirfd={:?}, new_path_ptr={:#x?}, new_path_len={:?})", + old_dirfd, + old_path_ptr, + old_path_len, + new_dirfd, + new_path_ptr, + new_path_len, + ); + + let old_path = dec_slice_of_u8(memory, old_path_ptr, old_path_len).and_then(path_from_slice)?; + let new_path = dec_slice_of_u8(memory, new_path_ptr, new_path_len).and_then(path_from_slice)?; + + trace!(" | (old_path_ptr,old_path_len)='{}'", old_path); + trace!(" | (new_path_ptr,new_path_len)='{}'", new_path); + + let old_fe = wasi_ctx.get_fd_entry(old_dirfd)?; + let new_fe = wasi_ctx.get_fd_entry(new_dirfd)?; + let resolved_old = path_get( + old_fe, + wasi::__WASI_RIGHTS_PATH_RENAME_SOURCE, + 0, + 0, + old_path, + true, + )?; + let resolved_new = path_get( + new_fe, + wasi::__WASI_RIGHTS_PATH_RENAME_TARGET, + 0, + 0, + new_path, + true, + )?; + + log::debug!("path_rename resolved_old={:?}", resolved_old); + log::debug!("path_rename resolved_new={:?}", resolved_new); + + hostcalls_impl::path_rename(resolved_old, resolved_new) +} + +pub(crate) unsafe fn fd_filestat_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + filestat_ptr: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "fd_filestat_get(fd={:?}, filestat_ptr={:#x?})", + fd, + filestat_ptr + ); + + let fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor(wasi::__WASI_RIGHTS_FD_FILESTAT_GET, 0)? + .as_file()?; + let host_filestat = hostcalls_impl::fd_filestat_get(fd)?; + + trace!(" | *filestat_ptr={:?}", host_filestat); + + enc_filestat_byref(memory, filestat_ptr, host_filestat) +} + +pub(crate) unsafe fn fd_filestat_set_times( + wasi_ctx: &WasiCtx, + _memory: &mut [u8], + fd: wasi::__wasi_fd_t, + st_atim: wasi::__wasi_timestamp_t, + st_mtim: wasi::__wasi_timestamp_t, + fst_flags: wasi::__wasi_fstflags_t, +) -> Result<()> { + trace!( + "fd_filestat_set_times(fd={:?}, st_atim={}, st_mtim={}, fst_flags={:#x?})", + fd, + st_atim, + st_mtim, + fst_flags + ); + + let fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor(wasi::__WASI_RIGHTS_FD_FILESTAT_SET_TIMES, 0)? + .as_file()?; + + fd_filestat_set_times_impl(fd, st_atim, st_mtim, fst_flags) +} + +pub(crate) fn fd_filestat_set_times_impl( + fd: &File, + st_atim: wasi::__wasi_timestamp_t, + st_mtim: wasi::__wasi_timestamp_t, + fst_flags: wasi::__wasi_fstflags_t, +) -> Result<()> { + let set_atim = fst_flags & wasi::__WASI_FSTFLAGS_ATIM != 0; + let set_atim_now = fst_flags & wasi::__WASI_FSTFLAGS_ATIM_NOW != 0; + let set_mtim = fst_flags & wasi::__WASI_FSTFLAGS_MTIM != 0; + let set_mtim_now = fst_flags & wasi::__WASI_FSTFLAGS_MTIM_NOW != 0; + + if (set_atim && set_atim_now) || (set_mtim && set_mtim_now) { + return Err(Error::EINVAL); + } + let atim = if set_atim { + let time = UNIX_EPOCH + Duration::from_nanos(st_atim); + Some(FileTime::from_system_time(time)) + } else if set_atim_now { + let time = SystemTime::now(); + Some(FileTime::from_system_time(time)) + } else { + None + }; + + let mtim = if set_mtim { + let time = UNIX_EPOCH + Duration::from_nanos(st_mtim); + Some(FileTime::from_system_time(time)) + } else if set_mtim_now { + let time = SystemTime::now(); + Some(FileTime::from_system_time(time)) + } else { + None + }; + set_file_handle_times(fd, atim, mtim).map_err(Into::into) +} + +pub(crate) unsafe fn fd_filestat_set_size( + wasi_ctx: &WasiCtx, + _memory: &mut [u8], + fd: wasi::__wasi_fd_t, + st_size: wasi::__wasi_filesize_t, +) -> Result<()> { + trace!("fd_filestat_set_size(fd={:?}, st_size={})", fd, st_size); + + let fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor(wasi::__WASI_RIGHTS_FD_FILESTAT_SET_SIZE, 0)? + .as_file()?; + + // This check will be unnecessary when rust-lang/rust#63326 is fixed + if st_size > i64::max_value() as u64 { + return Err(Error::E2BIG); + } + fd.set_len(st_size).map_err(Into::into) +} + +pub(crate) unsafe fn path_filestat_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + dirflags: wasi::__wasi_lookupflags_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, + filestat_ptr: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "path_filestat_get(dirfd={:?}, dirflags={:?}, path_ptr={:#x?}, path_len={}, filestat_ptr={:#x?})", + dirfd, + dirflags, + path_ptr, + path_len, + filestat_ptr + ); + + let path = dec_slice_of_u8(memory, path_ptr, path_len).and_then(path_from_slice)?; + + trace!(" | (path_ptr,path_len)='{}'", path); + + let fe = wasi_ctx.get_fd_entry(dirfd)?; + let resolved = path_get( + fe, + wasi::__WASI_RIGHTS_PATH_FILESTAT_GET, + 0, + dirflags, + path, + false, + )?; + let host_filestat = hostcalls_impl::path_filestat_get(resolved, dirflags)?; + + trace!(" | *filestat_ptr={:?}", host_filestat); + + enc_filestat_byref(memory, filestat_ptr, host_filestat) +} + +pub(crate) unsafe fn path_filestat_set_times( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + dirflags: wasi::__wasi_lookupflags_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, + st_atim: wasi::__wasi_timestamp_t, + st_mtim: wasi::__wasi_timestamp_t, + fst_flags: wasi::__wasi_fstflags_t, +) -> Result<()> { + trace!( + "path_filestat_set_times(dirfd={:?}, dirflags={:?}, path_ptr={:#x?}, path_len={}, st_atim={}, st_mtim={}, fst_flags={:#x?})", + dirfd, + dirflags, + path_ptr, + path_len, + st_atim, st_mtim, + fst_flags + ); + + let path = dec_slice_of_u8(memory, path_ptr, path_len).and_then(path_from_slice)?; + + trace!(" | (path_ptr,path_len)='{}'", path); + + let fe = wasi_ctx.get_fd_entry(dirfd)?; + let resolved = path_get( + fe, + wasi::__WASI_RIGHTS_PATH_FILESTAT_SET_TIMES, + 0, + dirflags, + path, + false, + )?; + + hostcalls_impl::path_filestat_set_times(resolved, dirflags, st_atim, st_mtim, fst_flags) +} + +pub(crate) unsafe fn path_symlink( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + old_path_ptr: wasi32::uintptr_t, + old_path_len: wasi32::size_t, + dirfd: wasi::__wasi_fd_t, + new_path_ptr: wasi32::uintptr_t, + new_path_len: wasi32::size_t, +) -> Result<()> { + trace!( + "path_symlink(old_path_ptr={:#x?}, old_path_len={}, dirfd={:?}, new_path_ptr={:#x?}, new_path_len={})", + old_path_ptr, + old_path_len, + dirfd, + new_path_ptr, + new_path_len + ); + + let old_path = dec_slice_of_u8(memory, old_path_ptr, old_path_len).and_then(path_from_slice)?; + let new_path = dec_slice_of_u8(memory, new_path_ptr, new_path_len).and_then(path_from_slice)?; + + trace!(" | (old_path_ptr,old_path_len)='{}'", old_path); + trace!(" | (new_path_ptr,new_path_len)='{}'", new_path); + + let fe = wasi_ctx.get_fd_entry(dirfd)?; + let resolved_new = path_get(fe, wasi::__WASI_RIGHTS_PATH_SYMLINK, 0, 0, new_path, true)?; + + hostcalls_impl::path_symlink(old_path, resolved_new) +} + +pub(crate) unsafe fn path_unlink_file( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, +) -> Result<()> { + trace!( + "path_unlink_file(dirfd={:?}, path_ptr={:#x?}, path_len={})", + dirfd, + path_ptr, + path_len + ); + + let path = dec_slice_of_u8(memory, path_ptr, path_len).and_then(path_from_slice)?; + + trace!(" | (path_ptr,path_len)='{}'", path); + + let fe = wasi_ctx.get_fd_entry(dirfd)?; + let resolved = path_get(fe, wasi::__WASI_RIGHTS_PATH_UNLINK_FILE, 0, 0, path, false)?; + + hostcalls_impl::path_unlink_file(resolved) +} + +pub(crate) unsafe fn path_remove_directory( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, +) -> Result<()> { + trace!( + "path_remove_directory(dirfd={:?}, path_ptr={:#x?}, path_len={})", + dirfd, + path_ptr, + path_len + ); + + let path = dec_slice_of_u8(memory, path_ptr, path_len).and_then(path_from_slice)?; + + trace!(" | (path_ptr,path_len)='{}'", path); + + let fe = wasi_ctx.get_fd_entry(dirfd)?; + let resolved = path_get( + fe, + wasi::__WASI_RIGHTS_PATH_REMOVE_DIRECTORY, + 0, + 0, + path, + true, + )?; + + log::debug!("path_remove_directory resolved={:?}", resolved); + + hostcalls_impl::path_remove_directory(resolved) +} + +pub(crate) unsafe fn fd_prestat_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + prestat_ptr: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "fd_prestat_get(fd={:?}, prestat_ptr={:#x?})", + fd, + prestat_ptr + ); + + // TODO: should we validate any rights here? + let fe = wasi_ctx.get_fd_entry(fd)?; + let po_path = fe.preopen_path.as_ref().ok_or(Error::ENOTSUP)?; + if fe.file_type != wasi::__WASI_FILETYPE_DIRECTORY { + return Err(Error::ENOTDIR); + } + + let path = host_impl::path_from_host(po_path.as_os_str())?; + + enc_prestat_byref( + memory, + prestat_ptr, + host::__wasi_prestat_t { + tag: wasi::__WASI_PREOPENTYPE_DIR, + u: host::__wasi_prestat_u_t { + dir: host::__wasi_prestat_dir_t { + pr_name_len: path.len(), + }, + }, + }, + ) +} + +pub(crate) unsafe fn fd_prestat_dir_name( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, +) -> Result<()> { + trace!( + "fd_prestat_dir_name(fd={:?}, path_ptr={:#x?}, path_len={})", + fd, + path_ptr, + path_len + ); + + // TODO: should we validate any rights here? + let fe = wasi_ctx.get_fd_entry(fd)?; + let po_path = fe.preopen_path.as_ref().ok_or(Error::ENOTSUP)?; + if fe.file_type != wasi::__WASI_FILETYPE_DIRECTORY { + return Err(Error::ENOTDIR); + } + + let path = host_impl::path_from_host(po_path.as_os_str())?; + + if path.len() > dec_usize(path_len) { + return Err(Error::ENAMETOOLONG); + } + + trace!(" | (path_ptr,path_len)='{}'", path); + + enc_slice_of_u8(memory, path.as_bytes(), path_ptr) +} + +pub(crate) unsafe fn fd_readdir( + wasi_ctx: &mut WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + buf: wasi32::uintptr_t, + buf_len: wasi32::size_t, + cookie: wasi::__wasi_dircookie_t, + buf_used: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "fd_readdir(fd={:?}, buf={:#x?}, buf_len={}, cookie={:#x?}, buf_used={:#x?})", + fd, + buf, + buf_len, + cookie, + buf_used, + ); + + enc_usize_byref(memory, buf_used, 0)?; + + let file = wasi_ctx + .get_fd_entry_mut(fd)? + .as_descriptor_mut(wasi::__WASI_RIGHTS_FD_READDIR, 0)? + .as_file_mut()?; + let mut host_buf = dec_slice_of_mut_u8(memory, buf, buf_len)?; + + trace!(" | (buf,buf_len)={:?}", host_buf); + + let iter = hostcalls_impl::fd_readdir(file, cookie)?; + let mut host_bufused = 0; + for dirent in iter { + let dirent_raw = dirent?.to_wasi_raw()?; + let offset = dirent_raw.len(); + if host_buf.len() < offset { + break; + } else { + host_buf[0..offset].copy_from_slice(&dirent_raw); + host_bufused += offset; + host_buf = &mut host_buf[offset..]; + } + } + + trace!(" | *buf_used={:?}", host_bufused); + + enc_usize_byref(memory, buf_used, host_bufused) +} diff --git a/crates/wasi-common/src/hostcalls_impl/fs_helpers.rs b/crates/wasi-common/src/hostcalls_impl/fs_helpers.rs new file mode 100644 index 0000000000..23a06c23fc --- /dev/null +++ b/crates/wasi-common/src/hostcalls_impl/fs_helpers.rs @@ -0,0 +1,213 @@ +#![allow(non_camel_case_types)] +use crate::sys::host_impl; +use crate::sys::hostcalls_impl::fs_helpers::*; +use crate::{error::WasiError, fdentry::FdEntry, wasi, Error, Result}; +use std::fs::File; +use std::path::{Component, Path}; + +#[derive(Debug)] +pub(crate) struct PathGet { + dirfd: File, + path: String, +} + +impl PathGet { + pub(crate) fn dirfd(&self) -> &File { + &self.dirfd + } + + pub(crate) fn path(&self) -> &str { + &self.path + } +} + +/// Normalizes a path to ensure that the target path is located under the directory provided. +/// +/// This is a workaround for not having Capsicum support in the OS. +pub(crate) fn path_get( + fe: &FdEntry, + rights_base: wasi::__wasi_rights_t, + rights_inheriting: wasi::__wasi_rights_t, + dirflags: wasi::__wasi_lookupflags_t, + path: &str, + needs_final_component: bool, +) -> Result { + const MAX_SYMLINK_EXPANSIONS: usize = 128; + + if path.contains('\0') { + // if contains NUL, return EILSEQ + return Err(Error::EILSEQ); + } + + if fe.file_type != wasi::__WASI_FILETYPE_DIRECTORY { + // if `dirfd` doesn't refer to a directory, return `ENOTDIR`. + return Err(Error::ENOTDIR); + } + + let dirfd = fe + .as_descriptor(rights_base, rights_inheriting)? + .as_file()? + .try_clone()?; + + // Stack of directory file descriptors. Index 0 always corresponds with the directory provided + // to this function. Entering a directory causes a file descriptor to be pushed, while handling + // ".." entries causes an entry to be popped. Index 0 cannot be popped, as this would imply + // escaping the base directory. + let mut dir_stack = vec![dirfd]; + + // Stack of paths left to process. This is initially the `path` argument to this function, but + // any symlinks we encounter are processed by pushing them on the stack. + let mut path_stack = vec![path.to_owned()]; + + // Track the number of symlinks we've expanded, so we can return `ELOOP` after too many. + let mut symlink_expansions = 0; + + // TODO: rewrite this using a custom posix path type, with a component iterator that respects + // trailing slashes. This version does way too much allocation, and is way too fiddly. + loop { + match path_stack.pop() { + Some(cur_path) => { + log::debug!("path_get cur_path = {:?}", cur_path); + + let ends_with_slash = cur_path.ends_with('/'); + let mut components = Path::new(&cur_path).components(); + let head = match components.next() { + None => return Err(Error::ENOENT), + Some(p) => p, + }; + let tail = components.as_path(); + + if tail.components().next().is_some() { + let mut tail = host_impl::path_from_host(tail.as_os_str())?; + if ends_with_slash { + tail.push('/'); + } + path_stack.push(tail); + } + + log::debug!("path_get path_stack = {:?}", path_stack); + + match head { + Component::Prefix(_) | Component::RootDir => { + // path is absolute! + return Err(Error::ENOTCAPABLE); + } + Component::CurDir => { + // "." so skip + } + Component::ParentDir => { + // ".." so pop a dir + let _ = dir_stack.pop().ok_or(Error::ENOTCAPABLE)?; + + // we're not allowed to pop past the original directory + if dir_stack.is_empty() { + return Err(Error::ENOTCAPABLE); + } + } + Component::Normal(head) => { + let mut head = host_impl::path_from_host(head)?; + if ends_with_slash { + // preserve trailing slash + head.push('/'); + } + + if !path_stack.is_empty() || (ends_with_slash && !needs_final_component) { + match openat(dir_stack.last().ok_or(Error::ENOTCAPABLE)?, &head) { + Ok(new_dir) => { + dir_stack.push(new_dir); + } + Err(e) => { + match e.as_wasi_error() { + WasiError::ELOOP + | WasiError::EMLINK + | WasiError::ENOTDIR => + // Check to see if it was a symlink. Linux indicates + // this with ENOTDIR because of the O_DIRECTORY flag. + { + // attempt symlink expansion + let mut link_path = readlinkat( + dir_stack.last().ok_or(Error::ENOTCAPABLE)?, + &head, + )?; + + symlink_expansions += 1; + if symlink_expansions > MAX_SYMLINK_EXPANSIONS { + return Err(Error::ELOOP); + } + + if head.ends_with('/') { + link_path.push('/'); + } + + log::debug!( + "attempted symlink expansion link_path={:?}", + link_path + ); + + path_stack.push(link_path); + } + _ => { + return Err(e); + } + } + } + } + + continue; + } else if ends_with_slash + || (dirflags & wasi::__WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) != 0 + { + // if there's a trailing slash, or if `LOOKUP_SYMLINK_FOLLOW` is set, attempt + // symlink expansion + match readlinkat(dir_stack.last().ok_or(Error::ENOTCAPABLE)?, &head) { + Ok(mut link_path) => { + symlink_expansions += 1; + if symlink_expansions > MAX_SYMLINK_EXPANSIONS { + return Err(Error::ELOOP); + } + + if head.ends_with('/') { + link_path.push('/'); + } + + log::debug!( + "attempted symlink expansion link_path={:?}", + link_path + ); + + path_stack.push(link_path); + continue; + } + Err(e) => { + if e.as_wasi_error() != WasiError::EINVAL + && e.as_wasi_error() != WasiError::ENOENT + // this handles the cases when trying to link to + // a destination that already exists, and the target + // path contains a slash + && e.as_wasi_error() != WasiError::ENOTDIR + { + return Err(e); + } + } + } + } + + // not a symlink, so we're done; + return Ok(PathGet { + dirfd: dir_stack.pop().ok_or(Error::ENOTCAPABLE)?, + path: head, + }); + } + } + } + None => { + // no further components to process. means we've hit a case like "." or "a/..", or if the + // input path has trailing slashes and `needs_final_component` is not set + return Ok(PathGet { + dirfd: dir_stack.pop().ok_or(Error::ENOTCAPABLE)?, + path: String::from("."), + }); + } + } + } +} diff --git a/crates/wasi-common/src/hostcalls_impl/misc.rs b/crates/wasi-common/src/hostcalls_impl/misc.rs new file mode 100644 index 0000000000..ca3225e0a7 --- /dev/null +++ b/crates/wasi-common/src/hostcalls_impl/misc.rs @@ -0,0 +1,362 @@ +#![allow(non_camel_case_types)] +use crate::ctx::WasiCtx; +use crate::fdentry::Descriptor; +use crate::memory::*; +use crate::sys::hostcalls_impl; +use crate::{wasi, wasi32, Error, Result}; +use log::{error, trace}; +use std::convert::TryFrom; + +pub(crate) fn args_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + argv_ptr: wasi32::uintptr_t, + argv_buf: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "args_get(argv_ptr={:#x?}, argv_buf={:#x?})", + argv_ptr, + argv_buf, + ); + + let mut argv_buf_offset = 0; + let mut argv = vec![]; + + for arg in &wasi_ctx.args { + let arg_bytes = arg.as_bytes_with_nul(); + let arg_ptr = argv_buf + argv_buf_offset; + + enc_slice_of_u8(memory, arg_bytes, arg_ptr)?; + + argv.push(arg_ptr); + + let len = wasi32::uintptr_t::try_from(arg_bytes.len())?; + argv_buf_offset = argv_buf_offset.checked_add(len).ok_or(Error::EOVERFLOW)?; + } + + enc_slice_of_wasi32_uintptr(memory, argv.as_slice(), argv_ptr) +} + +pub(crate) fn args_sizes_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + argc_ptr: wasi32::uintptr_t, + argv_buf_size_ptr: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "args_sizes_get(argc_ptr={:#x?}, argv_buf_size_ptr={:#x?})", + argc_ptr, + argv_buf_size_ptr, + ); + + let argc = wasi_ctx.args.len(); + let argv_size = wasi_ctx + .args + .iter() + .map(|arg| arg.as_bytes_with_nul().len()) + .sum(); + + trace!(" | *argc_ptr={:?}", argc); + + enc_usize_byref(memory, argc_ptr, argc)?; + + trace!(" | *argv_buf_size_ptr={:?}", argv_size); + + enc_usize_byref(memory, argv_buf_size_ptr, argv_size) +} + +pub(crate) fn environ_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + environ_ptr: wasi32::uintptr_t, + environ_buf: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "environ_get(environ_ptr={:#x?}, environ_buf={:#x?})", + environ_ptr, + environ_buf, + ); + + let mut environ_buf_offset = 0; + let mut environ = vec![]; + + for pair in &wasi_ctx.env { + let env_bytes = pair.as_bytes_with_nul(); + let env_ptr = environ_buf + environ_buf_offset; + + enc_slice_of_u8(memory, env_bytes, env_ptr)?; + + environ.push(env_ptr); + + let len = wasi32::uintptr_t::try_from(env_bytes.len())?; + environ_buf_offset = environ_buf_offset + .checked_add(len) + .ok_or(Error::EOVERFLOW)?; + } + + enc_slice_of_wasi32_uintptr(memory, environ.as_slice(), environ_ptr) +} + +pub(crate) fn environ_sizes_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + environ_count_ptr: wasi32::uintptr_t, + environ_size_ptr: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "environ_sizes_get(environ_count_ptr={:#x?}, environ_size_ptr={:#x?})", + environ_count_ptr, + environ_size_ptr, + ); + + let environ_count = wasi_ctx.env.len(); + let environ_size = wasi_ctx + .env + .iter() + .try_fold(0, |acc: u32, pair| { + acc.checked_add(pair.as_bytes_with_nul().len() as u32) + }) + .ok_or(Error::EOVERFLOW)?; + + trace!(" | *environ_count_ptr={:?}", environ_count); + + enc_usize_byref(memory, environ_count_ptr, environ_count)?; + + trace!(" | *environ_size_ptr={:?}", environ_size); + + enc_usize_byref(memory, environ_size_ptr, environ_size as usize) +} + +pub(crate) fn random_get( + _wasi_ctx: &WasiCtx, + memory: &mut [u8], + buf_ptr: wasi32::uintptr_t, + buf_len: wasi32::size_t, +) -> Result<()> { + trace!("random_get(buf_ptr={:#x?}, buf_len={:?})", buf_ptr, buf_len); + + let buf = dec_slice_of_mut_u8(memory, buf_ptr, buf_len)?; + + getrandom::getrandom(buf).map_err(|err| { + error!("getrandom failure: {:?}", err); + Error::EIO + }) +} + +pub(crate) fn clock_res_get( + _wasi_ctx: &WasiCtx, + memory: &mut [u8], + clock_id: wasi::__wasi_clockid_t, + resolution_ptr: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "clock_res_get(clock_id={:?}, resolution_ptr={:#x?})", + clock_id, + resolution_ptr, + ); + + let resolution = hostcalls_impl::clock_res_get(clock_id)?; + + trace!(" | *resolution_ptr={:?}", resolution); + + enc_timestamp_byref(memory, resolution_ptr, resolution) +} + +pub(crate) fn clock_time_get( + _wasi_ctx: &WasiCtx, + memory: &mut [u8], + clock_id: wasi::__wasi_clockid_t, + precision: wasi::__wasi_timestamp_t, + time_ptr: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "clock_time_get(clock_id={:?}, precision={:?}, time_ptr={:#x?})", + clock_id, + precision, + time_ptr, + ); + + let time = hostcalls_impl::clock_time_get(clock_id)?; + + trace!(" | *time_ptr={:?}", time); + + enc_timestamp_byref(memory, time_ptr, time) +} + +pub(crate) fn sched_yield(_wasi_ctx: &WasiCtx, _memory: &mut [u8]) -> Result<()> { + trace!("sched_yield()"); + + std::thread::yield_now(); + + Ok(()) +} + +pub(crate) fn poll_oneoff( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + input: wasi32::uintptr_t, + output: wasi32::uintptr_t, + nsubscriptions: wasi32::size_t, + nevents: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "poll_oneoff(input={:#x?}, output={:#x?}, nsubscriptions={}, nevents={:#x?})", + input, + output, + nsubscriptions, + nevents, + ); + + if u64::from(nsubscriptions) > wasi::__wasi_filesize_t::max_value() { + return Err(Error::EINVAL); + } + + enc_int_byref(memory, nevents, 0)?; + + let subscriptions = dec_subscriptions(memory, input, nsubscriptions)?; + let mut events = Vec::new(); + + let mut timeout: Option = None; + let mut fd_events = Vec::new(); + + // As mandated by the WASI spec: + // > If `nsubscriptions` is 0, returns `errno::inval`. + if subscriptions.is_empty() { + return Err(Error::EINVAL); + } + for subscription in subscriptions { + match subscription.u.tag { + wasi::__WASI_EVENTTYPE_CLOCK => { + let clock = unsafe { subscription.u.u.clock }; + let delay = wasi_clock_to_relative_ns_delay(clock)?; + + log::debug!("poll_oneoff event.u.clock = {:?}", clock); + log::debug!("poll_oneoff delay = {:?}ns", delay); + + let current = ClockEventData { + delay, + userdata: subscription.userdata, + }; + let timeout = timeout.get_or_insert(current); + if current.delay < timeout.delay { + *timeout = current; + } + } + wasi::__WASI_EVENTTYPE_FD_READ => { + let wasi_fd = unsafe { subscription.u.u.fd_read.file_descriptor }; + let rights = wasi::__WASI_RIGHTS_FD_READ | wasi::__WASI_RIGHTS_POLL_FD_READWRITE; + match unsafe { + wasi_ctx + .get_fd_entry(wasi_fd) + .and_then(|fe| fe.as_descriptor(rights, 0)) + } { + Ok(descriptor) => fd_events.push(FdEventData { + descriptor, + r#type: wasi::__WASI_EVENTTYPE_FD_READ, + userdata: subscription.userdata, + }), + Err(err) => { + let event = wasi::__wasi_event_t { + userdata: subscription.userdata, + error: err.as_wasi_error().as_raw_errno(), + r#type: wasi::__WASI_EVENTTYPE_FD_READ, + fd_readwrite: wasi::__wasi_event_fd_readwrite_t { + nbytes: 0, + flags: 0, + }, + }; + events.push(event); + } + }; + } + + wasi::__WASI_EVENTTYPE_FD_WRITE => { + let wasi_fd = unsafe { subscription.u.u.fd_write.file_descriptor }; + let rights = wasi::__WASI_RIGHTS_FD_WRITE | wasi::__WASI_RIGHTS_POLL_FD_READWRITE; + match unsafe { + wasi_ctx + .get_fd_entry(wasi_fd) + .and_then(|fe| fe.as_descriptor(rights, 0)) + } { + Ok(descriptor) => fd_events.push(FdEventData { + descriptor, + r#type: wasi::__WASI_EVENTTYPE_FD_WRITE, + userdata: subscription.userdata, + }), + Err(err) => { + let event = wasi::__wasi_event_t { + userdata: subscription.userdata, + error: err.as_wasi_error().as_raw_errno(), + r#type: wasi::__WASI_EVENTTYPE_FD_WRITE, + fd_readwrite: wasi::__wasi_event_fd_readwrite_t { + nbytes: 0, + flags: 0, + }, + }; + events.push(event); + } + }; + } + + _ => unreachable!(), + } + } + + log::debug!("poll_oneoff timeout = {:?}", timeout); + log::debug!("poll_oneoff fd_events = {:?}", fd_events); + + // The underlying implementation should successfully and immediately return + // if no events have been passed. Such situation may occur if all provided + // events have been filtered out as errors in the code above. + hostcalls_impl::poll_oneoff(timeout, fd_events, &mut events)?; + + let events_count = u32::try_from(events.len()).map_err(|_| Error::EOVERFLOW)?; + + enc_events(memory, output, nsubscriptions, events)?; + + trace!(" | *nevents={:?}", events_count); + + enc_int_byref(memory, nevents, events_count) +} + +fn wasi_clock_to_relative_ns_delay(wasi_clock: wasi::__wasi_subscription_clock_t) -> Result { + use std::time::SystemTime; + + if wasi_clock.flags != wasi::__WASI_SUBCLOCKFLAGS_SUBSCRIPTION_CLOCK_ABSTIME { + return Ok(u128::from(wasi_clock.timeout)); + } + let now: u128 = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .map_err(|_| Error::ENOTCAPABLE)? + .as_nanos(); + let deadline = u128::from(wasi_clock.timeout); + Ok(deadline.saturating_sub(now)) +} + +#[derive(Debug, Copy, Clone)] +pub(crate) struct ClockEventData { + pub(crate) delay: u128, // delay is expressed in nanoseconds + pub(crate) userdata: wasi::__wasi_userdata_t, +} + +#[derive(Debug)] +pub(crate) struct FdEventData<'a> { + pub(crate) descriptor: &'a Descriptor, + pub(crate) r#type: wasi::__wasi_eventtype_t, + pub(crate) userdata: wasi::__wasi_userdata_t, +} + +pub(crate) fn proc_exit(_wasi_ctx: &WasiCtx, _memory: &mut [u8], rval: wasi::__wasi_exitcode_t) { + trace!("proc_exit(rval={:?})", rval); + // TODO: Rather than call std::process::exit here, we should trigger a + // stack unwind similar to a trap. + std::process::exit(rval as i32); +} + +pub(crate) fn proc_raise( + _wasi_ctx: &WasiCtx, + _memory: &mut [u8], + _sig: wasi::__wasi_signal_t, +) -> Result<()> { + unimplemented!("proc_raise") +} diff --git a/crates/wasi-common/src/hostcalls_impl/mod.rs b/crates/wasi-common/src/hostcalls_impl/mod.rs new file mode 100644 index 0000000000..924c8c1ba9 --- /dev/null +++ b/crates/wasi-common/src/hostcalls_impl/mod.rs @@ -0,0 +1,9 @@ +mod fs; +mod fs_helpers; +mod misc; +mod sock; + +pub(crate) use self::fs::*; +pub(crate) use self::fs_helpers::PathGet; +pub(crate) use self::misc::*; +pub(crate) use self::sock::*; diff --git a/crates/wasi-common/src/hostcalls_impl/sock.rs b/crates/wasi-common/src/hostcalls_impl/sock.rs new file mode 100644 index 0000000000..9089e421f9 --- /dev/null +++ b/crates/wasi-common/src/hostcalls_impl/sock.rs @@ -0,0 +1,36 @@ +use crate::ctx::WasiCtx; +use crate::{wasi, wasi32, Result}; + +pub fn sock_recv( + _wasi_ctx: &WasiCtx, + _memory: &mut [u8], + _sock: wasi::__wasi_fd_t, + _ri_data: wasi32::uintptr_t, + _ri_data_len: wasi32::size_t, + _ri_flags: wasi::__wasi_riflags_t, + _ro_datalen: wasi32::uintptr_t, + _ro_flags: wasi32::uintptr_t, +) -> Result<()> { + unimplemented!("sock_recv") +} + +pub fn sock_send( + _wasi_ctx: &WasiCtx, + _memory: &mut [u8], + _sock: wasi::__wasi_fd_t, + _si_data: wasi32::uintptr_t, + _si_data_len: wasi32::size_t, + _si_flags: wasi::__wasi_siflags_t, + _so_datalen: wasi32::uintptr_t, +) -> Result<()> { + unimplemented!("sock_send") +} + +pub fn sock_shutdown( + _wasi_ctx: &WasiCtx, + _memory: &mut [u8], + _sock: wasi::__wasi_fd_t, + _how: wasi::__wasi_sdflags_t, +) -> Result<()> { + unimplemented!("sock_shutdown") +} diff --git a/crates/wasi-common/src/lib.rs b/crates/wasi-common/src/lib.rs new file mode 100644 index 0000000000..9e197fa74d --- /dev/null +++ b/crates/wasi-common/src/lib.rs @@ -0,0 +1,46 @@ +#![deny( + // missing_docs, + trivial_numeric_casts, + unused_extern_crates, + unstable_features, + clippy::use_self +)] +#![warn(unused_import_braces)] +#![cfg_attr(feature = "clippy", plugin(clippy(conf_file = "../clippy.toml")))] +#![cfg_attr(feature = "cargo-clippy", allow(clippy::new_without_default))] +#![cfg_attr( + feature = "cargo-clippy", + warn( + clippy::float_arithmetic, + clippy::mut_mut, + clippy::nonminimal_bool, + clippy::option_map_unwrap_or, + clippy::option_map_unwrap_or_else, + clippy::unicode_not_nfc, + clippy::use_self + ) +)] + +mod ctx; +mod error; +mod fdentry; +pub mod fs; +mod helpers; +mod host; +mod hostcalls_impl; +mod memory; +pub mod old; +mod sandboxed_tty_writer; +mod sys; +pub mod wasi; +pub mod wasi32; + +pub mod hostcalls { + wig::define_hostcalls!("snapshot" "wasi_snapshot_preview1"); +} + +pub use ctx::{WasiCtx, WasiCtxBuilder}; +pub use sys::preopen_dir; + +pub use error::Error; +pub(crate) use error::Result; diff --git a/crates/wasi-common/src/memory.rs b/crates/wasi-common/src/memory.rs new file mode 100644 index 0000000000..fa6752002f --- /dev/null +++ b/crates/wasi-common/src/memory.rs @@ -0,0 +1,480 @@ +//! Functions to store and load data to and from wasm linear memory, +//! transforming them from and to host data types. +//! +//! Endianness concerns are completely encapsulated in this file, so +//! that users outside this file holding a `wasi::*` value never need +//! to consider what endianness it's in. Inside this file, +//! wasm linear-memory-ordered values are called "raw" values, and +//! are not held for long durations. + +#![allow(unused)] +use crate::{host, wasi, wasi32, Error, Result}; +use num::PrimInt; +use std::convert::TryFrom; +use std::mem::{align_of, size_of}; +use std::{ptr, slice}; + +fn dec_ptr(memory: &[u8], ptr: wasi32::uintptr_t, len: usize) -> Result<*const u8> { + // check for overflow + let checked_len = (ptr as usize).checked_add(len).ok_or(Error::EFAULT)?; + + // translate the pointer + memory + .get(ptr as usize..checked_len) + .ok_or(Error::EFAULT) + .map(|mem| mem.as_ptr()) +} + +fn dec_ptr_mut(memory: &mut [u8], ptr: wasi32::uintptr_t, len: usize) -> Result<*mut u8> { + // check for overflow + let checked_len = (ptr as usize).checked_add(len).ok_or(Error::EFAULT)?; + + // translate the pointer + memory + .get_mut(ptr as usize..checked_len) + .ok_or(Error::EFAULT) + .map(|mem| mem.as_mut_ptr()) +} + +fn dec_ptr_to<'memory, T>(memory: &'memory [u8], ptr: wasi32::uintptr_t) -> Result<&'memory T> { + // check that the ptr is aligned + if ptr as usize % align_of::() != 0 { + return Err(Error::EINVAL); + } + + dec_ptr(memory, ptr, size_of::()).map(|p| unsafe { &*(p as *const T) }) +} + +fn dec_ptr_to_mut<'memory, T>( + memory: &'memory mut [u8], + ptr: wasi32::uintptr_t, +) -> Result<&'memory mut T> { + // check that the ptr is aligned + if ptr as usize % align_of::() != 0 { + return Err(Error::EINVAL); + } + + dec_ptr_mut(memory, ptr, size_of::()).map(|p| unsafe { &mut *(p as *mut T) }) +} + +/// This function does not perform endianness conversions! +fn dec_raw_byref(memory: &[u8], ptr: wasi32::uintptr_t) -> Result { + dec_ptr_to::(memory, ptr).map(|p| unsafe { ptr::read(p) }) +} + +/// This function does not perform endianness conversions! +fn enc_raw_byref(memory: &mut [u8], ptr: wasi32::uintptr_t, t: T) -> Result<()> { + dec_ptr_to_mut::(memory, ptr).map(|p| unsafe { ptr::write(p, t) }) +} + +pub(crate) fn dec_int_byref(memory: &[u8], ptr: wasi32::uintptr_t) -> Result +where + T: PrimInt, +{ + dec_raw_byref::(memory, ptr).map(|i| PrimInt::from_le(i)) +} + +pub(crate) fn enc_int_byref(memory: &mut [u8], ptr: wasi32::uintptr_t, t: T) -> Result<()> +where + T: PrimInt, +{ + enc_raw_byref::(memory, ptr, PrimInt::to_le(t)) +} + +fn check_slice_of(ptr: wasi32::uintptr_t, len: wasi32::size_t) -> Result<(usize, usize)> { + // check alignment, and that length doesn't overflow + if ptr as usize % align_of::() != 0 { + return Err(Error::EINVAL); + } + let len = dec_usize(len); + let len_bytes = if let Some(len) = size_of::().checked_mul(len) { + len + } else { + return Err(Error::EOVERFLOW); + }; + + Ok((len, len_bytes)) +} + +fn dec_raw_slice_of<'memory, T>( + memory: &'memory [u8], + ptr: wasi32::uintptr_t, + len: wasi32::size_t, +) -> Result<&'memory [T]> { + let (len, len_bytes) = check_slice_of::(ptr, len)?; + let ptr = dec_ptr(memory, ptr, len_bytes)? as *const T; + Ok(unsafe { slice::from_raw_parts(ptr, len) }) +} + +fn dec_raw_slice_of_mut<'memory, T>( + memory: &'memory mut [u8], + ptr: wasi32::uintptr_t, + len: wasi32::size_t, +) -> Result<&'memory mut [T]> { + let (len, len_bytes) = check_slice_of::(ptr, len)?; + let ptr = dec_ptr_mut(memory, ptr, len_bytes)? as *mut T; + Ok(unsafe { slice::from_raw_parts_mut(ptr, len) }) +} + +fn raw_slice_for_enc<'memory, T>( + memory: &'memory mut [u8], + slice: &[T], + ptr: wasi32::uintptr_t, +) -> Result<&'memory mut [T]> { + // check alignment + if ptr as usize % align_of::() != 0 { + return Err(Error::EINVAL); + } + // check that length doesn't overflow + let len_bytes = if let Some(len) = size_of::().checked_mul(slice.len()) { + len + } else { + return Err(Error::EOVERFLOW); + }; + + // get the pointer into guest memory + let ptr = dec_ptr_mut(memory, ptr, len_bytes)? as *mut T; + + Ok(unsafe { slice::from_raw_parts_mut(ptr, slice.len()) }) +} + +pub(crate) fn dec_slice_of_u8<'memory>( + memory: &'memory [u8], + ptr: wasi32::uintptr_t, + len: wasi32::size_t, +) -> Result<&'memory [u8]> { + dec_raw_slice_of::(memory, ptr, len) +} + +pub(crate) fn dec_slice_of_mut_u8<'memory>( + memory: &'memory mut [u8], + ptr: wasi32::uintptr_t, + len: wasi32::size_t, +) -> Result<&'memory mut [u8]> { + dec_raw_slice_of_mut::(memory, ptr, len) +} + +pub(crate) fn enc_slice_of_u8( + memory: &mut [u8], + slice: &[u8], + ptr: wasi32::uintptr_t, +) -> Result<()> { + let output = raw_slice_for_enc::(memory, slice, ptr)?; + + output.copy_from_slice(slice); + + Ok(()) +} + +pub(crate) fn enc_slice_of_wasi32_uintptr( + memory: &mut [u8], + slice: &[wasi32::uintptr_t], + ptr: wasi32::uintptr_t, +) -> Result<()> { + let mut output_iter = raw_slice_for_enc::(memory, slice, ptr)?.into_iter(); + + for p in slice { + *output_iter.next().unwrap() = PrimInt::to_le(*p); + } + + Ok(()) +} + +macro_rules! dec_enc_scalar { + ($ty:ident, $dec_byref:ident, $enc_byref:ident) => { + pub(crate) fn $dec_byref(memory: &mut [u8], ptr: wasi32::uintptr_t) -> Result { + dec_int_byref::(memory, ptr) + } + + pub(crate) fn $enc_byref( + memory: &mut [u8], + ptr: wasi32::uintptr_t, + x: wasi::$ty, + ) -> Result<()> { + enc_int_byref::(memory, ptr, x) + } + }; +} + +pub(crate) fn dec_ciovec_slice( + memory: &[u8], + ptr: wasi32::uintptr_t, + len: wasi32::size_t, +) -> Result> { + let raw_slice = dec_raw_slice_of::(memory, ptr, len)?; + + raw_slice + .iter() + .map(|raw_iov| { + let len = dec_usize(PrimInt::from_le(raw_iov.buf_len)); + let buf = PrimInt::from_le(raw_iov.buf); + Ok(host::__wasi_ciovec_t { + buf: dec_ptr(memory, buf, len)? as *const u8, + buf_len: len, + }) + }) + .collect() +} + +pub(crate) fn dec_iovec_slice( + memory: &[u8], + ptr: wasi32::uintptr_t, + len: wasi32::size_t, +) -> Result> { + let raw_slice = dec_raw_slice_of::(memory, ptr, len)?; + + raw_slice + .iter() + .map(|raw_iov| { + let len = dec_usize(PrimInt::from_le(raw_iov.buf_len)); + let buf = PrimInt::from_le(raw_iov.buf); + Ok(host::__wasi_iovec_t { + buf: dec_ptr(memory, buf, len)? as *mut u8, + buf_len: len, + }) + }) + .collect() +} + +dec_enc_scalar!(__wasi_clockid_t, dec_clockid_byref, enc_clockid_byref); +dec_enc_scalar!(__wasi_errno_t, dec_errno_byref, enc_errno_byref); +dec_enc_scalar!(__wasi_exitcode_t, dec_exitcode_byref, enc_exitcode_byref); +dec_enc_scalar!(__wasi_fd_t, dec_fd_byref, enc_fd_byref); +dec_enc_scalar!(__wasi_fdflags_t, dec_fdflags_byref, enc_fdflags_byref); +dec_enc_scalar!(__wasi_device_t, dev_device_byref, enc_device_byref); +dec_enc_scalar!(__wasi_inode_t, dev_inode_byref, enc_inode_byref); +dec_enc_scalar!(__wasi_linkcount_t, dev_linkcount_byref, enc_linkcount_byref); + +pub(crate) fn dec_filestat_byref( + memory: &mut [u8], + filestat_ptr: wasi32::uintptr_t, +) -> Result { + let raw = dec_raw_byref::(memory, filestat_ptr)?; + + Ok(wasi::__wasi_filestat_t { + dev: PrimInt::from_le(raw.dev), + ino: PrimInt::from_le(raw.ino), + filetype: PrimInt::from_le(raw.filetype), + nlink: PrimInt::from_le(raw.nlink), + size: PrimInt::from_le(raw.size), + atim: PrimInt::from_le(raw.atim), + mtim: PrimInt::from_le(raw.mtim), + ctim: PrimInt::from_le(raw.ctim), + }) +} + +pub(crate) fn enc_filestat_byref( + memory: &mut [u8], + filestat_ptr: wasi32::uintptr_t, + filestat: wasi::__wasi_filestat_t, +) -> Result<()> { + let raw = wasi::__wasi_filestat_t { + dev: PrimInt::to_le(filestat.dev), + ino: PrimInt::to_le(filestat.ino), + filetype: PrimInt::to_le(filestat.filetype), + nlink: PrimInt::to_le(filestat.nlink), + size: PrimInt::to_le(filestat.size), + atim: PrimInt::to_le(filestat.atim), + mtim: PrimInt::to_le(filestat.mtim), + ctim: PrimInt::to_le(filestat.ctim), + }; + + enc_raw_byref::(memory, filestat_ptr, raw) +} + +pub(crate) fn dec_fdstat_byref( + memory: &mut [u8], + fdstat_ptr: wasi32::uintptr_t, +) -> Result { + let raw = dec_raw_byref::(memory, fdstat_ptr)?; + + Ok(wasi::__wasi_fdstat_t { + fs_filetype: PrimInt::from_le(raw.fs_filetype), + fs_flags: PrimInt::from_le(raw.fs_flags), + fs_rights_base: PrimInt::from_le(raw.fs_rights_base), + fs_rights_inheriting: PrimInt::from_le(raw.fs_rights_inheriting), + }) +} + +pub(crate) fn enc_fdstat_byref( + memory: &mut [u8], + fdstat_ptr: wasi32::uintptr_t, + fdstat: wasi::__wasi_fdstat_t, +) -> Result<()> { + let raw = wasi::__wasi_fdstat_t { + fs_filetype: PrimInt::to_le(fdstat.fs_filetype), + fs_flags: PrimInt::to_le(fdstat.fs_flags), + fs_rights_base: PrimInt::to_le(fdstat.fs_rights_base), + fs_rights_inheriting: PrimInt::to_le(fdstat.fs_rights_inheriting), + }; + + enc_raw_byref::(memory, fdstat_ptr, raw) +} + +dec_enc_scalar!(__wasi_filedelta_t, dec_filedelta_byref, enc_filedelta_byref); +dec_enc_scalar!(__wasi_filesize_t, dec_filesize_byref, enc_filesize_byref); +dec_enc_scalar!(__wasi_filetype_t, dec_filetype_byref, enc_filetype_byref); + +dec_enc_scalar!( + __wasi_lookupflags_t, + dec_lookupflags_byref, + enc_lookupflags_byref +); + +dec_enc_scalar!(__wasi_oflags_t, dec_oflags_byref, enc_oflags_byref); + +pub(crate) fn dec_prestat_byref( + memory: &mut [u8], + prestat_ptr: wasi32::uintptr_t, +) -> Result { + let raw = dec_raw_byref::(memory, prestat_ptr)?; + + match PrimInt::from_le(raw.tag) { + wasi::__WASI_PREOPENTYPE_DIR => Ok(host::__wasi_prestat_t { + tag: wasi::__WASI_PREOPENTYPE_DIR, + u: host::__wasi_prestat_u_t { + dir: host::__wasi_prestat_dir_t { + pr_name_len: dec_usize(PrimInt::from_le(unsafe { raw.u.dir.pr_name_len })), + }, + }, + }), + _ => Err(Error::EINVAL), + } +} + +pub(crate) fn enc_prestat_byref( + memory: &mut [u8], + prestat_ptr: wasi32::uintptr_t, + prestat: host::__wasi_prestat_t, +) -> Result<()> { + let raw = match prestat.tag { + wasi::__WASI_PREOPENTYPE_DIR => Ok(wasi32::__wasi_prestat_t { + tag: PrimInt::to_le(wasi::__WASI_PREOPENTYPE_DIR), + u: wasi32::__wasi_prestat_u_t { + dir: wasi32::__wasi_prestat_dir_t { + pr_name_len: enc_usize(unsafe { prestat.u.dir.pr_name_len }), + }, + }, + }), + _ => Err(Error::EINVAL), + }?; + + enc_raw_byref::(memory, prestat_ptr, raw) +} + +dec_enc_scalar!(__wasi_rights_t, dec_rights_byref, enc_rights_byref); +dec_enc_scalar!(__wasi_timestamp_t, dec_timestamp_byref, enc_timestamp_byref); + +pub(crate) fn dec_usize(size: wasi32::size_t) -> usize { + usize::try_from(size).unwrap() +} + +pub(crate) fn enc_usize(size: usize) -> wasi32::size_t { + wasi32::size_t::try_from(size).unwrap() +} + +pub(crate) fn enc_usize_byref( + memory: &mut [u8], + usize_ptr: wasi32::uintptr_t, + host_usize: usize, +) -> Result<()> { + enc_int_byref::(memory, usize_ptr, enc_usize(host_usize)) +} + +dec_enc_scalar!(__wasi_whence_t, dec_whence_byref, enc_whence_byref); + +dec_enc_scalar!( + __wasi_subclockflags_t, + dec_subclockflags_byref, + enc_subclockflags_byref +); + +dec_enc_scalar!( + __wasi_eventrwflags_t, + dec_eventrwflags_byref, + enc_eventrwflags_byref +); + +dec_enc_scalar!(__wasi_eventtype_t, dec_eventtype_byref, enc_eventtype_byref); +dec_enc_scalar!(__wasi_userdata_t, dec_userdata_byref, enc_userdata_byref); + +pub(crate) fn dec_subscriptions( + memory: &mut [u8], + input: wasi32::uintptr_t, + nsubscriptions: wasi32::size_t, +) -> Result> { + let raw_input_slice = + dec_raw_slice_of::(memory, input, nsubscriptions)?; + + raw_input_slice + .into_iter() + .map(|raw_subscription| { + let userdata = PrimInt::from_le(raw_subscription.userdata); + let tag = PrimInt::from_le(raw_subscription.u.tag); + let raw_u = raw_subscription.u.u; + let u = match tag { + wasi::__WASI_EVENTTYPE_CLOCK => wasi::__wasi_subscription_u_u_t { + clock: unsafe { + wasi::__wasi_subscription_clock_t { + id: PrimInt::from_le(raw_u.clock.id), + timeout: PrimInt::from_le(raw_u.clock.timeout), + precision: PrimInt::from_le(raw_u.clock.precision), + flags: PrimInt::from_le(raw_u.clock.flags), + } + }, + }, + wasi::__WASI_EVENTTYPE_FD_READ => wasi::__wasi_subscription_u_u_t { + fd_read: wasi::__wasi_subscription_fd_readwrite_t { + file_descriptor: PrimInt::from_le(unsafe { raw_u.fd_read.file_descriptor }), + }, + }, + wasi::__WASI_EVENTTYPE_FD_WRITE => wasi::__wasi_subscription_u_u_t { + fd_write: wasi::__wasi_subscription_fd_readwrite_t { + file_descriptor: PrimInt::from_le(unsafe { + raw_u.fd_write.file_descriptor + }), + }, + }, + _ => return Err(Error::EINVAL), + }; + Ok(wasi::__wasi_subscription_t { + userdata, + u: wasi::__wasi_subscription_u_t { tag, u }, + }) + }) + .collect::>>() +} + +pub(crate) fn enc_events( + memory: &mut [u8], + output: wasi32::uintptr_t, + nsubscriptions: wasi32::size_t, + events: Vec, +) -> Result<()> { + let mut raw_output_iter = + dec_raw_slice_of_mut::(memory, output, nsubscriptions)?.into_iter(); + + for event in events.iter() { + *raw_output_iter + .next() + .expect("the number of events cannot exceed the number of subscriptions") = { + let userdata = PrimInt::to_le(event.userdata); + let error = PrimInt::to_le(event.error); + let r#type = PrimInt::to_le(event.r#type); + let flags = PrimInt::to_le(event.fd_readwrite.flags); + let nbytes = PrimInt::to_le(event.fd_readwrite.nbytes); + wasi::__wasi_event_t { + userdata, + error, + r#type, + fd_readwrite: wasi::__wasi_event_fd_readwrite_t { flags, nbytes }, + } + }; + } + + Ok(()) +} + +dec_enc_scalar!(__wasi_advice_t, dec_advice_byref, enc_advice_byref); +dec_enc_scalar!(__wasi_fstflags_t, dec_fstflags_byref, enc_fstflags_byref); +dec_enc_scalar!(__wasi_dircookie_t, dec_dircookie_byref, enc_dircookie_byref); diff --git a/crates/wasi-common/src/old/mod.rs b/crates/wasi-common/src/old/mod.rs new file mode 100644 index 0000000000..5d4d33030a --- /dev/null +++ b/crates/wasi-common/src/old/mod.rs @@ -0,0 +1 @@ +pub mod snapshot_0; diff --git a/crates/wasi-common/src/old/snapshot_0/ctx.rs b/crates/wasi-common/src/old/snapshot_0/ctx.rs new file mode 100644 index 0000000000..d6da885165 --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/ctx.rs @@ -0,0 +1,335 @@ +use crate::old::snapshot_0::fdentry::FdEntry; +use crate::old::snapshot_0::{wasi, Error, Result}; +use std::borrow::Borrow; +use std::collections::HashMap; +use std::env; +use std::ffi::{CString, OsString}; +use std::fs::File; +use std::path::{Path, PathBuf}; + +enum PendingFdEntry { + Thunk(fn() -> Result), + File(File), +} + +impl std::fmt::Debug for PendingFdEntry { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Thunk(f) => write!( + fmt, + "PendingFdEntry::Thunk({:p})", + f as *const fn() -> Result + ), + Self::File(f) => write!(fmt, "PendingFdEntry::File({:?})", f), + } + } +} + +#[derive(Debug, Eq, Hash, PartialEq)] +enum PendingCString { + Bytes(Vec), + OsString(OsString), +} + +impl From> for PendingCString { + fn from(bytes: Vec) -> Self { + Self::Bytes(bytes) + } +} + +impl From for PendingCString { + fn from(s: OsString) -> Self { + Self::OsString(s) + } +} + +impl PendingCString { + fn into_string(self) -> Result { + match self { + Self::Bytes(v) => String::from_utf8(v).map_err(|_| Error::EILSEQ), + Self::OsString(s) => s.into_string().map_err(|_| Error::EILSEQ), + } + } + + /// Create a `CString` containing valid UTF-8, or fail with `Error::EILSEQ`. + fn into_utf8_cstring(self) -> Result { + self.into_string() + .and_then(|s| CString::new(s).map_err(|_| Error::EILSEQ)) + } +} + +/// A builder allowing customizable construction of `WasiCtx` instances. +pub struct WasiCtxBuilder { + fds: HashMap, + preopens: Vec<(PathBuf, File)>, + args: Vec, + env: HashMap, +} + +impl WasiCtxBuilder { + /// Builder for a new `WasiCtx`. + pub fn new() -> Self { + let mut builder = Self { + fds: HashMap::new(), + preopens: Vec::new(), + args: vec![], + env: HashMap::new(), + }; + + builder.fds.insert(0, PendingFdEntry::Thunk(FdEntry::null)); + builder.fds.insert(1, PendingFdEntry::Thunk(FdEntry::null)); + builder.fds.insert(2, PendingFdEntry::Thunk(FdEntry::null)); + + builder + } + + /// Add arguments to the command-line arguments list. + /// + /// Arguments must be valid UTF-8 with no NUL bytes, or else `WasiCtxBuilder::build()` will fail + /// with `Error::EILSEQ`. + pub fn args>(mut self, args: impl IntoIterator) -> Self { + self.args = args + .into_iter() + .map(|arg| arg.as_ref().to_vec().into()) + .collect(); + self + } + + /// Add an argument to the command-line arguments list. + /// + /// Arguments must be valid UTF-8 with no NUL bytes, or else `WasiCtxBuilder::build()` will fail + /// with `Error::EILSEQ`. + pub fn arg>(mut self, arg: S) -> Self { + self.args.push(arg.as_ref().to_vec().into()); + self + } + + /// Inherit the command-line arguments from the host process. + /// + /// If any arguments from the host process contain invalid UTF-8, `WasiCtxBuilder::build()` will + /// fail with `Error::EILSEQ`. + pub fn inherit_args(mut self) -> Self { + self.args = env::args_os().map(PendingCString::OsString).collect(); + self + } + + /// Inherit the stdin, stdout, and stderr streams from the host process. + pub fn inherit_stdio(mut self) -> Self { + self.fds + .insert(0, PendingFdEntry::Thunk(FdEntry::duplicate_stdin)); + self.fds + .insert(1, PendingFdEntry::Thunk(FdEntry::duplicate_stdout)); + self.fds + .insert(2, PendingFdEntry::Thunk(FdEntry::duplicate_stderr)); + self + } + + /// Inherit the environment variables from the host process. + /// + /// If any environment variables from the host process contain invalid Unicode (UTF-16 for + /// Windows, UTF-8 for other platforms), `WasiCtxBuilder::build()` will fail with + /// `Error::EILSEQ`. + pub fn inherit_env(mut self) -> Self { + self.env = std::env::vars_os() + .map(|(k, v)| (k.into(), v.into())) + .collect(); + self + } + + /// Add an entry to the environment. + /// + /// Environment variable keys and values must be valid UTF-8 with no NUL bytes, or else + /// `WasiCtxBuilder::build()` will fail with `Error::EILSEQ`. + pub fn env>(mut self, k: S, v: S) -> Self { + self.env + .insert(k.as_ref().to_vec().into(), v.as_ref().to_vec().into()); + self + } + + /// Add entries to the environment. + /// + /// Environment variable keys and values must be valid UTF-8 with no NUL bytes, or else + /// `WasiCtxBuilder::build()` will fail with `Error::EILSEQ`. + pub fn envs, T: Borrow<(S, S)>>( + mut self, + envs: impl IntoIterator, + ) -> Self { + self.env = envs + .into_iter() + .map(|t| { + let (k, v) = t.borrow(); + (k.as_ref().to_vec().into(), v.as_ref().to_vec().into()) + }) + .collect(); + self + } + + /// Provide a File to use as stdin + pub fn stdin(mut self, file: File) -> Self { + self.fds.insert(0, PendingFdEntry::File(file)); + self + } + + /// Provide a File to use as stdout + pub fn stdout(mut self, file: File) -> Self { + self.fds.insert(1, PendingFdEntry::File(file)); + self + } + + /// Provide a File to use as stderr + pub fn stderr(mut self, file: File) -> Self { + self.fds.insert(2, PendingFdEntry::File(file)); + self + } + + /// Add a preopened directory. + pub fn preopened_dir>(mut self, dir: File, guest_path: P) -> Self { + self.preopens.push((guest_path.as_ref().to_owned(), dir)); + self + } + + /// Build a `WasiCtx`, consuming this `WasiCtxBuilder`. + /// + /// If any of the arguments or environment variables in this builder cannot be converted into + /// `CString`s, either due to NUL bytes or Unicode conversions, this returns `Error::EILSEQ`. + pub fn build(self) -> Result { + // Process arguments and environment variables into `CString`s, failing quickly if they + // contain any NUL bytes, or if conversion from `OsString` fails. + let args = self + .args + .into_iter() + .map(|arg| arg.into_utf8_cstring()) + .collect::>>()?; + + let env = self + .env + .into_iter() + .map(|(k, v)| { + k.into_string().and_then(|mut pair| { + v.into_string().and_then(|v| { + pair.push('='); + pair.push_str(v.as_str()); + // We have valid UTF-8, but the keys and values have not yet been checked + // for NULs, so we do a final check here. + CString::new(pair).map_err(|_| Error::EILSEQ) + }) + }) + }) + .collect::>>()?; + + let mut fds: HashMap = HashMap::new(); + // Populate the non-preopen fds. + for (fd, pending) in self.fds { + log::debug!("WasiCtx inserting ({:?}, {:?})", fd, pending); + match pending { + PendingFdEntry::Thunk(f) => { + fds.insert(fd, f()?); + } + PendingFdEntry::File(f) => { + fds.insert(fd, FdEntry::from(f)?); + } + } + } + // Then add the preopen fds. Startup code in the guest starts looking at fd 3 for preopens, + // so we start from there. This variable is initially 2, though, because the loop + // immediately does the increment and check for overflow. + let mut preopen_fd: wasi::__wasi_fd_t = 2; + for (guest_path, dir) in self.preopens { + // We do the increment at the beginning of the loop body, so that we don't overflow + // unnecessarily if we have exactly the maximum number of file descriptors. + preopen_fd = preopen_fd.checked_add(1).ok_or(Error::ENFILE)?; + + if !dir.metadata()?.is_dir() { + return Err(Error::EBADF); + } + + // We don't currently allow setting file descriptors other than 0-2, but this will avoid + // collisions if we restore that functionality in the future. + while fds.contains_key(&preopen_fd) { + preopen_fd = preopen_fd.checked_add(1).ok_or(Error::ENFILE)?; + } + let mut fe = FdEntry::from(dir)?; + fe.preopen_path = Some(guest_path); + log::debug!("WasiCtx inserting ({:?}, {:?})", preopen_fd, fe); + fds.insert(preopen_fd, fe); + log::debug!("WasiCtx fds = {:?}", fds); + } + + Ok(WasiCtx { args, env, fds }) + } +} + +#[derive(Debug)] +pub struct WasiCtx { + fds: HashMap, + pub(crate) args: Vec, + pub(crate) env: Vec, +} + +impl WasiCtx { + /// Make a new `WasiCtx` with some default settings. + /// + /// - File descriptors 0, 1, and 2 inherit stdin, stdout, and stderr from the host process. + /// + /// - Environment variables are inherited from the host process. + /// + /// To override these behaviors, use `WasiCtxBuilder`. + pub fn new>(args: impl IntoIterator) -> Result { + WasiCtxBuilder::new() + .args(args) + .inherit_stdio() + .inherit_env() + .build() + } + + /// Check if `WasiCtx` contains the specified raw WASI `fd`. + pub(crate) unsafe fn contains_fd_entry(&self, fd: wasi::__wasi_fd_t) -> bool { + self.fds.contains_key(&fd) + } + + /// Get an immutable `FdEntry` corresponding to the specified raw WASI `fd`. + pub(crate) unsafe fn get_fd_entry(&self, fd: wasi::__wasi_fd_t) -> Result<&FdEntry> { + self.fds.get(&fd).ok_or(Error::EBADF) + } + + /// Get a mutable `FdEntry` corresponding to the specified raw WASI `fd`. + pub(crate) unsafe fn get_fd_entry_mut( + &mut self, + fd: wasi::__wasi_fd_t, + ) -> Result<&mut FdEntry> { + self.fds.get_mut(&fd).ok_or(Error::EBADF) + } + + /// Insert the specified `FdEntry` into the `WasiCtx` object. + /// + /// The `FdEntry` will automatically get another free raw WASI `fd` assigned. Note that + /// the two subsequent free raw WASI `fd`s do not have to be stored contiguously. + pub(crate) fn insert_fd_entry(&mut self, fe: FdEntry) -> Result { + // Never insert where stdio handles are expected to be. + let mut fd = 3; + while self.fds.contains_key(&fd) { + if let Some(next_fd) = fd.checked_add(1) { + fd = next_fd; + } else { + return Err(Error::EMFILE); + } + } + self.fds.insert(fd, fe); + Ok(fd) + } + + /// Insert the specified `FdEntry` with the specified raw WASI `fd` key into the `WasiCtx` + /// object. + pub(crate) fn insert_fd_entry_at( + &mut self, + fd: wasi::__wasi_fd_t, + fe: FdEntry, + ) -> Option { + self.fds.insert(fd, fe) + } + + /// Remove `FdEntry` corresponding to the specified raw WASI `fd` from the `WasiCtx` object. + pub(crate) fn remove_fd_entry(&mut self, fd: wasi::__wasi_fd_t) -> Result { + self.fds.remove(&fd).ok_or(Error::EBADF) + } +} diff --git a/crates/wasi-common/src/old/snapshot_0/error.rs b/crates/wasi-common/src/old/snapshot_0/error.rs new file mode 100644 index 0000000000..ae634520d3 --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/error.rs @@ -0,0 +1,247 @@ +// Due to https://github.com/rust-lang/rust/issues/64247 +#![allow(clippy::use_self)] +use crate::old::snapshot_0::wasi; +use std::convert::Infallible; +use std::num::TryFromIntError; +use std::{ffi, str}; +use thiserror::Error; + +#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)] +#[repr(u16)] +#[error("{:?} ({})", self, wasi::strerror(*self as wasi::__wasi_errno_t))] +pub enum WasiError { + ESUCCESS = wasi::__WASI_ERRNO_SUCCESS, + E2BIG = wasi::__WASI_ERRNO_2BIG, + EACCES = wasi::__WASI_ERRNO_ACCES, + EADDRINUSE = wasi::__WASI_ERRNO_ADDRINUSE, + EADDRNOTAVAIL = wasi::__WASI_ERRNO_ADDRNOTAVAIL, + EAFNOSUPPORT = wasi::__WASI_ERRNO_AFNOSUPPORT, + EAGAIN = wasi::__WASI_ERRNO_AGAIN, + EALREADY = wasi::__WASI_ERRNO_ALREADY, + EBADF = wasi::__WASI_ERRNO_BADF, + EBADMSG = wasi::__WASI_ERRNO_BADMSG, + EBUSY = wasi::__WASI_ERRNO_BUSY, + ECANCELED = wasi::__WASI_ERRNO_CANCELED, + ECHILD = wasi::__WASI_ERRNO_CHILD, + ECONNABORTED = wasi::__WASI_ERRNO_CONNABORTED, + ECONNREFUSED = wasi::__WASI_ERRNO_CONNREFUSED, + ECONNRESET = wasi::__WASI_ERRNO_CONNRESET, + EDEADLK = wasi::__WASI_ERRNO_DEADLK, + EDESTADDRREQ = wasi::__WASI_ERRNO_DESTADDRREQ, + EDOM = wasi::__WASI_ERRNO_DOM, + EDQUOT = wasi::__WASI_ERRNO_DQUOT, + EEXIST = wasi::__WASI_ERRNO_EXIST, + EFAULT = wasi::__WASI_ERRNO_FAULT, + EFBIG = wasi::__WASI_ERRNO_FBIG, + EHOSTUNREACH = wasi::__WASI_ERRNO_HOSTUNREACH, + EIDRM = wasi::__WASI_ERRNO_IDRM, + EILSEQ = wasi::__WASI_ERRNO_ILSEQ, + EINPROGRESS = wasi::__WASI_ERRNO_INPROGRESS, + EINTR = wasi::__WASI_ERRNO_INTR, + EINVAL = wasi::__WASI_ERRNO_INVAL, + EIO = wasi::__WASI_ERRNO_IO, + EISCONN = wasi::__WASI_ERRNO_ISCONN, + EISDIR = wasi::__WASI_ERRNO_ISDIR, + ELOOP = wasi::__WASI_ERRNO_LOOP, + EMFILE = wasi::__WASI_ERRNO_MFILE, + EMLINK = wasi::__WASI_ERRNO_MLINK, + EMSGSIZE = wasi::__WASI_ERRNO_MSGSIZE, + EMULTIHOP = wasi::__WASI_ERRNO_MULTIHOP, + ENAMETOOLONG = wasi::__WASI_ERRNO_NAMETOOLONG, + ENETDOWN = wasi::__WASI_ERRNO_NETDOWN, + ENETRESET = wasi::__WASI_ERRNO_NETRESET, + ENETUNREACH = wasi::__WASI_ERRNO_NETUNREACH, + ENFILE = wasi::__WASI_ERRNO_NFILE, + ENOBUFS = wasi::__WASI_ERRNO_NOBUFS, + ENODEV = wasi::__WASI_ERRNO_NODEV, + ENOENT = wasi::__WASI_ERRNO_NOENT, + ENOEXEC = wasi::__WASI_ERRNO_NOEXEC, + ENOLCK = wasi::__WASI_ERRNO_NOLCK, + ENOLINK = wasi::__WASI_ERRNO_NOLINK, + ENOMEM = wasi::__WASI_ERRNO_NOMEM, + ENOMSG = wasi::__WASI_ERRNO_NOMSG, + ENOPROTOOPT = wasi::__WASI_ERRNO_NOPROTOOPT, + ENOSPC = wasi::__WASI_ERRNO_NOSPC, + ENOSYS = wasi::__WASI_ERRNO_NOSYS, + ENOTCONN = wasi::__WASI_ERRNO_NOTCONN, + ENOTDIR = wasi::__WASI_ERRNO_NOTDIR, + ENOTEMPTY = wasi::__WASI_ERRNO_NOTEMPTY, + ENOTRECOVERABLE = wasi::__WASI_ERRNO_NOTRECOVERABLE, + ENOTSOCK = wasi::__WASI_ERRNO_NOTSOCK, + ENOTSUP = wasi::__WASI_ERRNO_NOTSUP, + ENOTTY = wasi::__WASI_ERRNO_NOTTY, + ENXIO = wasi::__WASI_ERRNO_NXIO, + EOVERFLOW = wasi::__WASI_ERRNO_OVERFLOW, + EOWNERDEAD = wasi::__WASI_ERRNO_OWNERDEAD, + EPERM = wasi::__WASI_ERRNO_PERM, + EPIPE = wasi::__WASI_ERRNO_PIPE, + EPROTO = wasi::__WASI_ERRNO_PROTO, + EPROTONOSUPPORT = wasi::__WASI_ERRNO_PROTONOSUPPORT, + EPROTOTYPE = wasi::__WASI_ERRNO_PROTOTYPE, + ERANGE = wasi::__WASI_ERRNO_RANGE, + EROFS = wasi::__WASI_ERRNO_ROFS, + ESPIPE = wasi::__WASI_ERRNO_SPIPE, + ESRCH = wasi::__WASI_ERRNO_SRCH, + ESTALE = wasi::__WASI_ERRNO_STALE, + ETIMEDOUT = wasi::__WASI_ERRNO_TIMEDOUT, + ETXTBSY = wasi::__WASI_ERRNO_TXTBSY, + EXDEV = wasi::__WASI_ERRNO_XDEV, + ENOTCAPABLE = wasi::__WASI_ERRNO_NOTCAPABLE, +} + +impl WasiError { + pub fn as_raw_errno(self) -> wasi::__wasi_errno_t { + self as wasi::__wasi_errno_t + } +} + +#[derive(Debug, Error)] +pub enum Error { + #[error("WASI error code: {0}")] + Wasi(#[from] WasiError), + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[cfg(unix)] + #[error("Yanix error: {0}")] + Yanix(#[from] yanix::YanixError), +} + +impl From for Error { + fn from(_: TryFromIntError) -> Self { + Self::EOVERFLOW + } +} + +impl From for Error { + fn from(_: Infallible) -> Self { + unreachable!() + } +} + +impl From for Error { + fn from(_: str::Utf8Error) -> Self { + Self::EILSEQ + } +} + +impl From for Error { + fn from(_: ffi::NulError) -> Self { + Self::EILSEQ + } +} + +impl From<&ffi::NulError> for Error { + fn from(_: &ffi::NulError) -> Self { + Self::EILSEQ + } +} + +impl Error { + pub(crate) fn as_wasi_error(&self) -> WasiError { + match self { + Self::Wasi(err) => *err, + Self::Io(err) => { + let err = match err.raw_os_error() { + Some(code) => Self::from_raw_os_error(code), + None => { + log::debug!("Inconvertible OS error: {}", err); + Self::EIO + } + }; + err.as_wasi_error() + } + #[cfg(unix)] + Self::Yanix(err) => { + use yanix::YanixError::*; + let err: Self = match err { + Errno(errno) => (*errno).into(), + NulError(err) => err.into(), + TryFromIntError(err) => (*err).into(), + }; + err.as_wasi_error() + } + } + } + + pub const ESUCCESS: Self = Error::Wasi(WasiError::ESUCCESS); + pub const E2BIG: Self = Error::Wasi(WasiError::E2BIG); + pub const EACCES: Self = Error::Wasi(WasiError::EACCES); + pub const EADDRINUSE: Self = Error::Wasi(WasiError::EADDRINUSE); + pub const EADDRNOTAVAIL: Self = Error::Wasi(WasiError::EADDRNOTAVAIL); + pub const EAFNOSUPPORT: Self = Error::Wasi(WasiError::EAFNOSUPPORT); + pub const EAGAIN: Self = Error::Wasi(WasiError::EAGAIN); + pub const EALREADY: Self = Error::Wasi(WasiError::EALREADY); + pub const EBADF: Self = Error::Wasi(WasiError::EBADF); + pub const EBADMSG: Self = Error::Wasi(WasiError::EBADMSG); + pub const EBUSY: Self = Error::Wasi(WasiError::EBUSY); + pub const ECANCELED: Self = Error::Wasi(WasiError::ECANCELED); + pub const ECHILD: Self = Error::Wasi(WasiError::ECHILD); + pub const ECONNABORTED: Self = Error::Wasi(WasiError::ECONNABORTED); + pub const ECONNREFUSED: Self = Error::Wasi(WasiError::ECONNREFUSED); + pub const ECONNRESET: Self = Error::Wasi(WasiError::ECONNRESET); + pub const EDEADLK: Self = Error::Wasi(WasiError::EDEADLK); + pub const EDESTADDRREQ: Self = Error::Wasi(WasiError::EDESTADDRREQ); + pub const EDOM: Self = Error::Wasi(WasiError::EDOM); + pub const EDQUOT: Self = Error::Wasi(WasiError::EDQUOT); + pub const EEXIST: Self = Error::Wasi(WasiError::EEXIST); + pub const EFAULT: Self = Error::Wasi(WasiError::EFAULT); + pub const EFBIG: Self = Error::Wasi(WasiError::EFBIG); + pub const EHOSTUNREACH: Self = Error::Wasi(WasiError::EHOSTUNREACH); + pub const EIDRM: Self = Error::Wasi(WasiError::EIDRM); + pub const EILSEQ: Self = Error::Wasi(WasiError::EILSEQ); + pub const EINPROGRESS: Self = Error::Wasi(WasiError::EINPROGRESS); + pub const EINTR: Self = Error::Wasi(WasiError::EINTR); + pub const EINVAL: Self = Error::Wasi(WasiError::EINVAL); + pub const EIO: Self = Error::Wasi(WasiError::EIO); + pub const EISCONN: Self = Error::Wasi(WasiError::EISCONN); + pub const EISDIR: Self = Error::Wasi(WasiError::EISDIR); + pub const ELOOP: Self = Error::Wasi(WasiError::ELOOP); + pub const EMFILE: Self = Error::Wasi(WasiError::EMFILE); + pub const EMLINK: Self = Error::Wasi(WasiError::EMLINK); + pub const EMSGSIZE: Self = Error::Wasi(WasiError::EMSGSIZE); + pub const EMULTIHOP: Self = Error::Wasi(WasiError::EMULTIHOP); + pub const ENAMETOOLONG: Self = Error::Wasi(WasiError::ENAMETOOLONG); + pub const ENETDOWN: Self = Error::Wasi(WasiError::ENETDOWN); + pub const ENETRESET: Self = Error::Wasi(WasiError::ENETRESET); + pub const ENETUNREACH: Self = Error::Wasi(WasiError::ENETUNREACH); + pub const ENFILE: Self = Error::Wasi(WasiError::ENFILE); + pub const ENOBUFS: Self = Error::Wasi(WasiError::ENOBUFS); + pub const ENODEV: Self = Error::Wasi(WasiError::ENODEV); + pub const ENOENT: Self = Error::Wasi(WasiError::ENOENT); + pub const ENOEXEC: Self = Error::Wasi(WasiError::ENOEXEC); + pub const ENOLCK: Self = Error::Wasi(WasiError::ENOLCK); + pub const ENOLINK: Self = Error::Wasi(WasiError::ENOLINK); + pub const ENOMEM: Self = Error::Wasi(WasiError::ENOMEM); + pub const ENOMSG: Self = Error::Wasi(WasiError::ENOMSG); + pub const ENOPROTOOPT: Self = Error::Wasi(WasiError::ENOPROTOOPT); + pub const ENOSPC: Self = Error::Wasi(WasiError::ENOSPC); + pub const ENOSYS: Self = Error::Wasi(WasiError::ENOSYS); + pub const ENOTCONN: Self = Error::Wasi(WasiError::ENOTCONN); + pub const ENOTDIR: Self = Error::Wasi(WasiError::ENOTDIR); + pub const ENOTEMPTY: Self = Error::Wasi(WasiError::ENOTEMPTY); + pub const ENOTRECOVERABLE: Self = Error::Wasi(WasiError::ENOTRECOVERABLE); + pub const ENOTSOCK: Self = Error::Wasi(WasiError::ENOTSOCK); + pub const ENOTSUP: Self = Error::Wasi(WasiError::ENOTSUP); + pub const ENOTTY: Self = Error::Wasi(WasiError::ENOTTY); + pub const ENXIO: Self = Error::Wasi(WasiError::ENXIO); + pub const EOVERFLOW: Self = Error::Wasi(WasiError::EOVERFLOW); + pub const EOWNERDEAD: Self = Error::Wasi(WasiError::EOWNERDEAD); + pub const EPERM: Self = Error::Wasi(WasiError::EPERM); + pub const EPIPE: Self = Error::Wasi(WasiError::EPIPE); + pub const EPROTO: Self = Error::Wasi(WasiError::EPROTO); + pub const EPROTONOSUPPORT: Self = Error::Wasi(WasiError::EPROTONOSUPPORT); + pub const EPROTOTYPE: Self = Error::Wasi(WasiError::EPROTOTYPE); + pub const ERANGE: Self = Error::Wasi(WasiError::ERANGE); + pub const EROFS: Self = Error::Wasi(WasiError::EROFS); + pub const ESPIPE: Self = Error::Wasi(WasiError::ESPIPE); + pub const ESRCH: Self = Error::Wasi(WasiError::ESRCH); + pub const ESTALE: Self = Error::Wasi(WasiError::ESTALE); + pub const ETIMEDOUT: Self = Error::Wasi(WasiError::ETIMEDOUT); + pub const ETXTBSY: Self = Error::Wasi(WasiError::ETXTBSY); + pub const EXDEV: Self = Error::Wasi(WasiError::EXDEV); + pub const ENOTCAPABLE: Self = Error::Wasi(WasiError::ENOTCAPABLE); +} + +pub(crate) trait FromRawOsError { + fn from_raw_os_error(code: i32) -> Self; +} diff --git a/crates/wasi-common/src/old/snapshot_0/fdentry.rs b/crates/wasi-common/src/old/snapshot_0/fdentry.rs new file mode 100644 index 0000000000..aab0178b93 --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/fdentry.rs @@ -0,0 +1,208 @@ +use crate::old::snapshot_0::sys::dev_null; +use crate::old::snapshot_0::sys::fdentry_impl::{ + descriptor_as_oshandle, determine_type_and_access_rights, OsHandle, +}; +use crate::old::snapshot_0::{wasi, Error, Result}; +use std::marker::PhantomData; +use std::mem::ManuallyDrop; +use std::ops::{Deref, DerefMut}; +use std::path::PathBuf; +use std::{fs, io}; + +#[derive(Debug)] +pub(crate) enum Descriptor { + OsHandle(OsHandle), + Stdin, + Stdout, + Stderr, +} + +impl Descriptor { + /// Return a reference to the `OsHandle` treating it as an actual file/dir, and + /// allowing operations which require an actual file and not just a stream or + /// socket file descriptor. + pub(crate) fn as_file(&self) -> Result<&OsHandle> { + match self { + Self::OsHandle(file) => Ok(file), + _ => Err(Error::EBADF), + } + } + + /// Like `as_file`, but return a mutable reference. + pub(crate) fn as_file_mut(&mut self) -> Result<&mut OsHandle> { + match self { + Self::OsHandle(file) => Ok(file), + _ => Err(Error::EBADF), + } + } + + /// Return an `OsHandle`, which may be a stream or socket file descriptor. + pub(crate) fn as_os_handle<'descriptor>(&'descriptor self) -> OsHandleRef<'descriptor> { + descriptor_as_oshandle(self) + } +} + +/// An abstraction struct serving as a wrapper for a host `Descriptor` object which requires +/// certain base rights `rights_base` and inheriting rights `rights_inheriting` in order to be +/// accessed correctly. +/// +/// Here, the `descriptor` field stores the host `Descriptor` object (such as a file descriptor, or +/// stdin handle), and accessing it can only be done via the provided `FdEntry::as_descriptor` and +/// `FdEntry::as_descriptor_mut` methods which require a set of base and inheriting rights to be +/// specified, verifying whether the stored `Descriptor` object is valid for the rights specified. +#[derive(Debug)] +pub(crate) struct FdEntry { + pub(crate) file_type: wasi::__wasi_filetype_t, + descriptor: Descriptor, + pub(crate) rights_base: wasi::__wasi_rights_t, + pub(crate) rights_inheriting: wasi::__wasi_rights_t, + pub(crate) preopen_path: Option, + // TODO: directories +} + +impl FdEntry { + /// Create an FdEntry with *maximal* possible rights from a given `File`. + /// If this is not desired, the rights of the resulting `FdEntry` should + /// be manually restricted. + pub(crate) fn from(file: fs::File) -> Result { + unsafe { determine_type_and_access_rights(&file) }.map( + |(file_type, rights_base, rights_inheriting)| Self { + file_type, + descriptor: Descriptor::OsHandle(OsHandle::from(file)), + rights_base, + rights_inheriting, + preopen_path: None, + }, + ) + } + + pub(crate) fn duplicate_stdin() -> Result { + unsafe { determine_type_and_access_rights(&io::stdin()) }.map( + |(file_type, rights_base, rights_inheriting)| Self { + file_type, + descriptor: Descriptor::Stdin, + rights_base, + rights_inheriting, + preopen_path: None, + }, + ) + } + + pub(crate) fn duplicate_stdout() -> Result { + unsafe { determine_type_and_access_rights(&io::stdout()) }.map( + |(file_type, rights_base, rights_inheriting)| Self { + file_type, + descriptor: Descriptor::Stdout, + rights_base, + rights_inheriting, + preopen_path: None, + }, + ) + } + + pub(crate) fn duplicate_stderr() -> Result { + unsafe { determine_type_and_access_rights(&io::stderr()) }.map( + |(file_type, rights_base, rights_inheriting)| Self { + file_type, + descriptor: Descriptor::Stderr, + rights_base, + rights_inheriting, + preopen_path: None, + }, + ) + } + + pub(crate) fn null() -> Result { + Self::from(dev_null()?) + } + + /// Convert this `FdEntry` into a host `Descriptor` object provided the specified + /// `rights_base` and `rights_inheriting` rights are set on this `FdEntry` object. + /// + /// The `FdEntry` can only be converted into a valid `Descriptor` object if + /// the specified set of base rights `rights_base`, and inheriting rights `rights_inheriting` + /// is a subset of rights attached to this `FdEntry`. The check is performed using + /// `FdEntry::validate_rights` method. If the check fails, `Error::ENOTCAPABLE` is returned. + pub(crate) fn as_descriptor( + &self, + rights_base: wasi::__wasi_rights_t, + rights_inheriting: wasi::__wasi_rights_t, + ) -> Result<&Descriptor> { + self.validate_rights(rights_base, rights_inheriting)?; + Ok(&self.descriptor) + } + + /// Convert this `FdEntry` into a mutable host `Descriptor` object provided the specified + /// `rights_base` and `rights_inheriting` rights are set on this `FdEntry` object. + /// + /// The `FdEntry` can only be converted into a valid `Descriptor` object if + /// the specified set of base rights `rights_base`, and inheriting rights `rights_inheriting` + /// is a subset of rights attached to this `FdEntry`. The check is performed using + /// `FdEntry::validate_rights` method. If the check fails, `Error::ENOTCAPABLE` is returned. + pub(crate) fn as_descriptor_mut( + &mut self, + rights_base: wasi::__wasi_rights_t, + rights_inheriting: wasi::__wasi_rights_t, + ) -> Result<&mut Descriptor> { + self.validate_rights(rights_base, rights_inheriting)?; + Ok(&mut self.descriptor) + } + + /// Check if this `FdEntry` object satisfies the specified base rights `rights_base`, and + /// inheriting rights `rights_inheriting`; i.e., if rights attached to this `FdEntry` object + /// are a superset. + /// + /// Upon unsuccessful check, `Error::ENOTCAPABLE` is returned. + fn validate_rights( + &self, + rights_base: wasi::__wasi_rights_t, + rights_inheriting: wasi::__wasi_rights_t, + ) -> Result<()> { + if !self.rights_base & rights_base != 0 || !self.rights_inheriting & rights_inheriting != 0 + { + Err(Error::ENOTCAPABLE) + } else { + Ok(()) + } + } + + /// Test whether this descriptor is considered a tty within WASI. + /// Note that since WASI itself lacks an `isatty` syscall and relies + /// on a conservative approximation, we use the same approximation here. + pub(crate) fn isatty(&self) -> bool { + self.file_type == wasi::__WASI_FILETYPE_CHARACTER_DEVICE + && (self.rights_base & (wasi::__WASI_RIGHTS_FD_SEEK | wasi::__WASI_RIGHTS_FD_TELL)) == 0 + } +} + +/// This allows an `OsHandle` to be temporarily borrowed from a +/// `Descriptor`. The `Descriptor` continues to own the resource, +/// and `OsHandleRef`'s lifetime parameter ensures that it doesn't +/// outlive the `Descriptor`. +pub(crate) struct OsHandleRef<'descriptor> { + handle: ManuallyDrop, + _ref: PhantomData<&'descriptor Descriptor>, +} + +impl<'descriptor> OsHandleRef<'descriptor> { + pub(crate) fn new(handle: ManuallyDrop) -> Self { + OsHandleRef { + handle, + _ref: PhantomData, + } + } +} + +impl<'descriptor> Deref for OsHandleRef<'descriptor> { + type Target = fs::File; + + fn deref(&self) -> &Self::Target { + &self.handle + } +} + +impl<'descriptor> DerefMut for OsHandleRef<'descriptor> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.handle + } +} diff --git a/crates/wasi-common/src/old/snapshot_0/helpers.rs b/crates/wasi-common/src/old/snapshot_0/helpers.rs new file mode 100644 index 0000000000..f540eb8f12 --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/helpers.rs @@ -0,0 +1,10 @@ +use crate::old::snapshot_0::{Error, Result}; +use std::str; + +/// Creates not-owned WASI path from byte slice. +/// +/// NB WASI spec requires bytes to be valid UTF-8. Otherwise, +/// `__WASI_ERRNO_ILSEQ` error is returned. +pub(crate) fn path_from_slice<'a>(s: &'a [u8]) -> Result<&'a str> { + str::from_utf8(s).map_err(|_| Error::EILSEQ) +} diff --git a/crates/wasi-common/src/old/snapshot_0/host.rs b/crates/wasi-common/src/old/snapshot_0/host.rs new file mode 100644 index 0000000000..b029fcf10e --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/host.rs @@ -0,0 +1,80 @@ +//! WASI host types. These are types that contain raw pointers and `usize` +//! values, and so are platform-specific. + +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] + +use crate::old::snapshot_0::wasi::*; +use crate::old::snapshot_0::{Error, Result}; +use std::{convert::TryInto, io, mem, slice}; +use wig::witx_host_types; + +witx_host_types!("old/snapshot_0" "wasi_unstable"); + +pub(crate) unsafe fn ciovec_to_host(ciovec: &__wasi_ciovec_t) -> io::IoSlice { + let slice = slice::from_raw_parts(ciovec.buf as *const u8, ciovec.buf_len); + io::IoSlice::new(slice) +} + +pub(crate) unsafe fn iovec_to_host_mut(iovec: &mut __wasi_iovec_t) -> io::IoSliceMut { + let slice = slice::from_raw_parts_mut(iovec.buf as *mut u8, iovec.buf_len); + io::IoSliceMut::new(slice) +} + +#[allow(dead_code)] // trouble with sockets +#[derive(Clone, Copy, Debug)] +#[repr(u8)] +pub(crate) enum FileType { + Unknown = __WASI_FILETYPE_UNKNOWN, + BlockDevice = __WASI_FILETYPE_BLOCK_DEVICE, + CharacterDevice = __WASI_FILETYPE_CHARACTER_DEVICE, + Directory = __WASI_FILETYPE_DIRECTORY, + RegularFile = __WASI_FILETYPE_REGULAR_FILE, + SocketDgram = __WASI_FILETYPE_SOCKET_DGRAM, + SocketStream = __WASI_FILETYPE_SOCKET_STREAM, + Symlink = __WASI_FILETYPE_SYMBOLIC_LINK, +} + +impl FileType { + pub(crate) fn to_wasi(&self) -> __wasi_filetype_t { + *self as __wasi_filetype_t + } +} + +#[derive(Debug, Clone)] +pub(crate) struct Dirent { + pub name: String, + pub ftype: FileType, + pub ino: u64, + pub cookie: __wasi_dircookie_t, +} + +impl Dirent { + /// Serialize the directory entry to the format define by `__wasi_fd_readdir`, + /// so that the serialized entries can be concatenated by the implementation. + pub fn to_wasi_raw(&self) -> Result> { + let name = self.name.as_bytes(); + let namlen = name.len(); + let dirent_size = mem::size_of::<__wasi_dirent_t>(); + let offset = dirent_size.checked_add(namlen).ok_or(Error::EOVERFLOW)?; + + let mut raw = Vec::::with_capacity(offset); + raw.resize(offset, 0); + + let sys_dirent = raw.as_mut_ptr() as *mut __wasi_dirent_t; + unsafe { + sys_dirent.write_unaligned(__wasi_dirent_t { + d_namlen: namlen.try_into()?, + d_ino: self.ino, + d_next: self.cookie, + d_type: self.ftype.to_wasi(), + }); + } + + let sys_name = unsafe { sys_dirent.offset(1) as *mut u8 }; + let sys_name = unsafe { slice::from_raw_parts_mut(sys_name, namlen) }; + sys_name.copy_from_slice(&name); + + Ok(raw) + } +} diff --git a/crates/wasi-common/src/old/snapshot_0/hostcalls_impl/fs.rs b/crates/wasi-common/src/old/snapshot_0/hostcalls_impl/fs.rs new file mode 100644 index 0000000000..ff23ae0a5e --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/hostcalls_impl/fs.rs @@ -0,0 +1,1075 @@ +#![allow(non_camel_case_types)] +use super::fs_helpers::path_get; +use crate::old::snapshot_0::ctx::WasiCtx; +use crate::old::snapshot_0::fdentry::{Descriptor, FdEntry}; +use crate::old::snapshot_0::helpers::*; +use crate::old::snapshot_0::memory::*; +use crate::old::snapshot_0::sys::fdentry_impl::determine_type_rights; +use crate::old::snapshot_0::sys::hostcalls_impl::fs_helpers::path_open_rights; +use crate::old::snapshot_0::sys::{host_impl, hostcalls_impl}; +use crate::old::snapshot_0::{helpers, host, wasi, wasi32, Error, Result}; +use crate::sandboxed_tty_writer::SandboxedTTYWriter; +use filetime::{set_file_handle_times, FileTime}; +use log::trace; +use std::fs::File; +use std::io::{self, Read, Seek, SeekFrom, Write}; +use std::ops::DerefMut; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +pub(crate) unsafe fn fd_close( + wasi_ctx: &mut WasiCtx, + _mem: &mut [u8], + fd: wasi::__wasi_fd_t, +) -> Result<()> { + trace!("fd_close(fd={:?})", fd); + + if let Ok(fe) = wasi_ctx.get_fd_entry(fd) { + // can't close preopened files + if fe.preopen_path.is_some() { + return Err(Error::ENOTSUP); + } + } + + wasi_ctx.remove_fd_entry(fd)?; + Ok(()) +} + +pub(crate) unsafe fn fd_datasync( + wasi_ctx: &WasiCtx, + _mem: &mut [u8], + fd: wasi::__wasi_fd_t, +) -> Result<()> { + trace!("fd_datasync(fd={:?})", fd); + + let fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor(wasi::__WASI_RIGHTS_FD_DATASYNC, 0)? + .as_file()?; + + fd.sync_data().map_err(Into::into) +} + +pub(crate) unsafe fn fd_pread( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + iovs_ptr: wasi32::uintptr_t, + iovs_len: wasi32::size_t, + offset: wasi::__wasi_filesize_t, + nread: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "fd_pread(fd={:?}, iovs_ptr={:#x?}, iovs_len={:?}, offset={}, nread={:#x?})", + fd, + iovs_ptr, + iovs_len, + offset, + nread + ); + + let fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor(wasi::__WASI_RIGHTS_FD_READ | wasi::__WASI_RIGHTS_FD_SEEK, 0)? + .as_file()?; + + let iovs = dec_iovec_slice(memory, iovs_ptr, iovs_len)?; + + if offset > i64::max_value() as u64 { + return Err(Error::EIO); + } + let buf_size = iovs.iter().map(|v| v.buf_len).sum(); + let mut buf = vec![0; buf_size]; + let host_nread = hostcalls_impl::fd_pread(fd, &mut buf, offset)?; + let mut buf_offset = 0; + let mut left = host_nread; + for iov in &iovs { + if left == 0 { + break; + } + let vec_len = std::cmp::min(iov.buf_len, left); + std::slice::from_raw_parts_mut(iov.buf as *mut u8, vec_len) + .copy_from_slice(&buf[buf_offset..buf_offset + vec_len]); + buf_offset += vec_len; + left -= vec_len; + } + + trace!(" | *nread={:?}", host_nread); + + enc_usize_byref(memory, nread, host_nread) +} + +pub(crate) unsafe fn fd_pwrite( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + iovs_ptr: wasi32::uintptr_t, + iovs_len: wasi32::size_t, + offset: wasi::__wasi_filesize_t, + nwritten: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "fd_pwrite(fd={:?}, iovs_ptr={:#x?}, iovs_len={:?}, offset={}, nwritten={:#x?})", + fd, + iovs_ptr, + iovs_len, + offset, + nwritten + ); + + let fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor( + wasi::__WASI_RIGHTS_FD_WRITE | wasi::__WASI_RIGHTS_FD_SEEK, + 0, + )? + .as_file()?; + let iovs = dec_ciovec_slice(memory, iovs_ptr, iovs_len)?; + + if offset > i64::max_value() as u64 { + return Err(Error::EIO); + } + let buf_size = iovs.iter().map(|v| v.buf_len).sum(); + let mut buf = Vec::with_capacity(buf_size); + for iov in &iovs { + buf.extend_from_slice(std::slice::from_raw_parts( + iov.buf as *const u8, + iov.buf_len, + )); + } + let host_nwritten = hostcalls_impl::fd_pwrite(fd, &buf, offset)?; + + trace!(" | *nwritten={:?}", host_nwritten); + + enc_usize_byref(memory, nwritten, host_nwritten) +} + +pub(crate) unsafe fn fd_read( + wasi_ctx: &mut WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + iovs_ptr: wasi32::uintptr_t, + iovs_len: wasi32::size_t, + nread: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "fd_read(fd={:?}, iovs_ptr={:#x?}, iovs_len={:?}, nread={:#x?})", + fd, + iovs_ptr, + iovs_len, + nread + ); + + let mut iovs = dec_iovec_slice(memory, iovs_ptr, iovs_len)?; + let mut iovs: Vec = iovs + .iter_mut() + .map(|vec| host::iovec_to_host_mut(vec)) + .collect(); + + let maybe_host_nread = match wasi_ctx + .get_fd_entry_mut(fd)? + .as_descriptor_mut(wasi::__WASI_RIGHTS_FD_READ, 0)? + { + Descriptor::OsHandle(file) => file.read_vectored(&mut iovs), + Descriptor::Stdin => io::stdin().read_vectored(&mut iovs), + _ => return Err(Error::EBADF), + }; + + let host_nread = maybe_host_nread?; + + trace!(" | *nread={:?}", host_nread); + + enc_usize_byref(memory, nread, host_nread) +} + +pub(crate) unsafe fn fd_renumber( + wasi_ctx: &mut WasiCtx, + _mem: &mut [u8], + from: wasi::__wasi_fd_t, + to: wasi::__wasi_fd_t, +) -> Result<()> { + trace!("fd_renumber(from={:?}, to={:?})", from, to); + + if !wasi_ctx.contains_fd_entry(from) { + return Err(Error::EBADF); + } + + // Don't allow renumbering over a pre-opened resource. + // TODO: Eventually, we do want to permit this, once libpreopen in + // userspace is capable of removing entries from its tables as well. + let from_fe = wasi_ctx.get_fd_entry(from)?; + if from_fe.preopen_path.is_some() { + return Err(Error::ENOTSUP); + } + if let Ok(to_fe) = wasi_ctx.get_fd_entry(to) { + if to_fe.preopen_path.is_some() { + return Err(Error::ENOTSUP); + } + } + + let fe = wasi_ctx.remove_fd_entry(from)?; + wasi_ctx.insert_fd_entry_at(to, fe); + + Ok(()) +} + +pub(crate) unsafe fn fd_seek( + wasi_ctx: &mut WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + offset: wasi::__wasi_filedelta_t, + whence: wasi::__wasi_whence_t, + newoffset: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "fd_seek(fd={:?}, offset={:?}, whence={}, newoffset={:#x?})", + fd, + offset, + wasi::whence_to_str(whence), + newoffset + ); + + let rights = if offset == 0 && whence == wasi::__WASI_WHENCE_CUR { + wasi::__WASI_RIGHTS_FD_TELL + } else { + wasi::__WASI_RIGHTS_FD_SEEK | wasi::__WASI_RIGHTS_FD_TELL + }; + let fd = wasi_ctx + .get_fd_entry_mut(fd)? + .as_descriptor_mut(rights, 0)? + .as_file_mut()?; + + let pos = match whence { + wasi::__WASI_WHENCE_CUR => SeekFrom::Current(offset), + wasi::__WASI_WHENCE_END => SeekFrom::End(offset), + wasi::__WASI_WHENCE_SET => SeekFrom::Start(offset as u64), + _ => return Err(Error::EINVAL), + }; + let host_newoffset = fd.seek(pos)?; + + trace!(" | *newoffset={:?}", host_newoffset); + + enc_filesize_byref(memory, newoffset, host_newoffset) +} + +pub(crate) unsafe fn fd_tell( + wasi_ctx: &mut WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + newoffset: wasi32::uintptr_t, +) -> Result<()> { + trace!("fd_tell(fd={:?}, newoffset={:#x?})", fd, newoffset); + + let fd = wasi_ctx + .get_fd_entry_mut(fd)? + .as_descriptor_mut(wasi::__WASI_RIGHTS_FD_TELL, 0)? + .as_file_mut()?; + + let host_offset = fd.seek(SeekFrom::Current(0))?; + + trace!(" | *newoffset={:?}", host_offset); + + enc_filesize_byref(memory, newoffset, host_offset) +} + +pub(crate) unsafe fn fd_fdstat_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + fdstat_ptr: wasi32::uintptr_t, // *mut wasi::__wasi_fdstat_t +) -> Result<()> { + trace!("fd_fdstat_get(fd={:?}, fdstat_ptr={:#x?})", fd, fdstat_ptr); + + let mut fdstat = dec_fdstat_byref(memory, fdstat_ptr)?; + let host_fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor(0, 0)? + .as_os_handle(); + + let fs_flags = hostcalls_impl::fd_fdstat_get(&host_fd)?; + + let fe = wasi_ctx.get_fd_entry(fd)?; + fdstat.fs_filetype = fe.file_type; + fdstat.fs_rights_base = fe.rights_base; + fdstat.fs_rights_inheriting = fe.rights_inheriting; + fdstat.fs_flags = fs_flags; + + trace!(" | *buf={:?}", fdstat); + + enc_fdstat_byref(memory, fdstat_ptr, fdstat) +} + +pub(crate) unsafe fn fd_fdstat_set_flags( + wasi_ctx: &WasiCtx, + _mem: &mut [u8], + fd: wasi::__wasi_fd_t, + fdflags: wasi::__wasi_fdflags_t, +) -> Result<()> { + trace!("fd_fdstat_set_flags(fd={:?}, fdflags={:#x?})", fd, fdflags); + + let fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor(0, 0)? + .as_os_handle(); + + hostcalls_impl::fd_fdstat_set_flags(&fd, fdflags) +} + +pub(crate) unsafe fn fd_fdstat_set_rights( + wasi_ctx: &mut WasiCtx, + _mem: &mut [u8], + fd: wasi::__wasi_fd_t, + fs_rights_base: wasi::__wasi_rights_t, + fs_rights_inheriting: wasi::__wasi_rights_t, +) -> Result<()> { + trace!( + "fd_fdstat_set_rights(fd={:?}, fs_rights_base={:#x?}, fs_rights_inheriting={:#x?})", + fd, + fs_rights_base, + fs_rights_inheriting + ); + + let fe = wasi_ctx.get_fd_entry_mut(fd)?; + if fe.rights_base & fs_rights_base != fs_rights_base + || fe.rights_inheriting & fs_rights_inheriting != fs_rights_inheriting + { + return Err(Error::ENOTCAPABLE); + } + fe.rights_base = fs_rights_base; + fe.rights_inheriting = fs_rights_inheriting; + + Ok(()) +} + +pub(crate) unsafe fn fd_sync( + wasi_ctx: &WasiCtx, + _mem: &mut [u8], + fd: wasi::__wasi_fd_t, +) -> Result<()> { + trace!("fd_sync(fd={:?})", fd); + + let fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor(wasi::__WASI_RIGHTS_FD_SYNC, 0)? + .as_file()?; + fd.sync_all().map_err(Into::into) +} + +pub(crate) unsafe fn fd_write( + wasi_ctx: &mut WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + iovs_ptr: wasi32::uintptr_t, + iovs_len: wasi32::size_t, + nwritten: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "fd_write(fd={:?}, iovs_ptr={:#x?}, iovs_len={:?}, nwritten={:#x?})", + fd, + iovs_ptr, + iovs_len, + nwritten + ); + + let iovs = dec_ciovec_slice(memory, iovs_ptr, iovs_len)?; + let iovs: Vec = iovs.iter().map(|vec| host::ciovec_to_host(vec)).collect(); + + // perform unbuffered writes + let entry = wasi_ctx.get_fd_entry_mut(fd)?; + let isatty = entry.isatty(); + let desc = entry.as_descriptor_mut(wasi::__WASI_RIGHTS_FD_WRITE, 0)?; + let host_nwritten = match desc { + Descriptor::OsHandle(file) => { + if isatty { + SandboxedTTYWriter::new(file.deref_mut()).write_vectored(&iovs)? + } else { + file.write_vectored(&iovs)? + } + } + Descriptor::Stdin => return Err(Error::EBADF), + Descriptor::Stdout => { + // lock for the duration of the scope + let stdout = io::stdout(); + let mut stdout = stdout.lock(); + let nwritten = if isatty { + SandboxedTTYWriter::new(&mut stdout).write_vectored(&iovs)? + } else { + stdout.write_vectored(&iovs)? + }; + stdout.flush()?; + nwritten + } + // Always sanitize stderr, even if it's not directly connected to a tty, + // because stderr is meant for diagnostics rather than binary output, + // and may be redirected to a file which could end up being displayed + // on a tty later. + Descriptor::Stderr => SandboxedTTYWriter::new(&mut io::stderr()).write_vectored(&iovs)?, + }; + + trace!(" | *nwritten={:?}", host_nwritten); + + enc_usize_byref(memory, nwritten, host_nwritten) +} + +pub(crate) unsafe fn fd_advise( + wasi_ctx: &WasiCtx, + _mem: &mut [u8], + fd: wasi::__wasi_fd_t, + offset: wasi::__wasi_filesize_t, + len: wasi::__wasi_filesize_t, + advice: wasi::__wasi_advice_t, +) -> Result<()> { + trace!( + "fd_advise(fd={:?}, offset={}, len={}, advice={:?})", + fd, + offset, + len, + advice + ); + + let fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor(wasi::__WASI_RIGHTS_FD_ADVISE, 0)? + .as_file()?; + + hostcalls_impl::fd_advise(fd, advice, offset, len) +} + +pub(crate) unsafe fn fd_allocate( + wasi_ctx: &WasiCtx, + _mem: &mut [u8], + fd: wasi::__wasi_fd_t, + offset: wasi::__wasi_filesize_t, + len: wasi::__wasi_filesize_t, +) -> Result<()> { + trace!("fd_allocate(fd={:?}, offset={}, len={})", fd, offset, len); + + let fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor(wasi::__WASI_RIGHTS_FD_ALLOCATE, 0)? + .as_file()?; + + let metadata = fd.metadata()?; + + let current_size = metadata.len(); + let wanted_size = offset.checked_add(len).ok_or(Error::E2BIG)?; + // This check will be unnecessary when rust-lang/rust#63326 is fixed + if wanted_size > i64::max_value() as u64 { + return Err(Error::E2BIG); + } + + if wanted_size > current_size { + fd.set_len(wanted_size).map_err(Into::into) + } else { + Ok(()) + } +} + +pub(crate) unsafe fn path_create_directory( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, +) -> Result<()> { + trace!( + "path_create_directory(dirfd={:?}, path_ptr={:#x?}, path_len={})", + dirfd, + path_ptr, + path_len, + ); + + let path = dec_slice_of_u8(memory, path_ptr, path_len).and_then(helpers::path_from_slice)?; + + trace!(" | (path_ptr,path_len)='{}'", path); + + let rights = wasi::__WASI_RIGHTS_PATH_OPEN | wasi::__WASI_RIGHTS_PATH_CREATE_DIRECTORY; + let fe = wasi_ctx.get_fd_entry(dirfd)?; + let resolved = path_get(fe, rights, 0, 0, path, false)?; + + hostcalls_impl::path_create_directory(resolved) +} + +pub(crate) unsafe fn path_link( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + old_dirfd: wasi::__wasi_fd_t, + old_flags: wasi::__wasi_lookupflags_t, + old_path_ptr: wasi32::uintptr_t, + old_path_len: wasi32::size_t, + new_dirfd: wasi::__wasi_fd_t, + new_path_ptr: wasi32::uintptr_t, + new_path_len: wasi32::size_t, +) -> Result<()> { + trace!( + "path_link(old_dirfd={:?}, old_flags={:?}, old_path_ptr={:#x?}, old_path_len={}, new_dirfd={:?}, new_path_ptr={:#x?}, new_path_len={})", + old_dirfd, + old_flags, + old_path_ptr, + old_path_len, + new_dirfd, + new_path_ptr, + new_path_len, + ); + + let old_path = dec_slice_of_u8(memory, old_path_ptr, old_path_len).and_then(path_from_slice)?; + let new_path = dec_slice_of_u8(memory, new_path_ptr, new_path_len).and_then(path_from_slice)?; + + trace!(" | (old_path_ptr,old_path_len)='{}'", old_path); + trace!(" | (new_path_ptr,new_path_len)='{}'", new_path); + + let old_fe = wasi_ctx.get_fd_entry(old_dirfd)?; + let new_fe = wasi_ctx.get_fd_entry(new_dirfd)?; + let resolved_old = path_get( + old_fe, + wasi::__WASI_RIGHTS_PATH_LINK_SOURCE, + 0, + 0, + old_path, + false, + )?; + let resolved_new = path_get( + new_fe, + wasi::__WASI_RIGHTS_PATH_LINK_TARGET, + 0, + 0, + new_path, + false, + )?; + + hostcalls_impl::path_link(resolved_old, resolved_new) +} + +pub(crate) unsafe fn path_open( + wasi_ctx: &mut WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + dirflags: wasi::__wasi_lookupflags_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, + oflags: wasi::__wasi_oflags_t, + fs_rights_base: wasi::__wasi_rights_t, + fs_rights_inheriting: wasi::__wasi_rights_t, + fs_flags: wasi::__wasi_fdflags_t, + fd_out_ptr: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "path_open(dirfd={:?}, dirflags={:?}, path_ptr={:#x?}, path_len={:?}, oflags={:#x?}, fs_rights_base={:#x?}, fs_rights_inheriting={:#x?}, fs_flags={:#x?}, fd_out_ptr={:#x?})", + dirfd, + dirflags, + path_ptr, + path_len, + oflags, + fs_rights_base, + fs_rights_inheriting, + fs_flags, + fd_out_ptr + ); + + // pre-encode fd_out_ptr to -1 in case of error in opening a path + enc_fd_byref(memory, fd_out_ptr, wasi::__wasi_fd_t::max_value())?; + + let path = dec_slice_of_u8(memory, path_ptr, path_len).and_then(path_from_slice)?; + + trace!(" | (path_ptr,path_len)='{}'", path); + + let (needed_base, needed_inheriting) = + path_open_rights(fs_rights_base, fs_rights_inheriting, oflags, fs_flags); + let fe = wasi_ctx.get_fd_entry(dirfd)?; + let resolved = path_get( + fe, + needed_base, + needed_inheriting, + dirflags, + path, + oflags & wasi::__WASI_OFLAGS_CREAT != 0, + )?; + + // which open mode do we need? + let read = fs_rights_base & (wasi::__WASI_RIGHTS_FD_READ | wasi::__WASI_RIGHTS_FD_READDIR) != 0; + let write = fs_rights_base + & (wasi::__WASI_RIGHTS_FD_DATASYNC + | wasi::__WASI_RIGHTS_FD_WRITE + | wasi::__WASI_RIGHTS_FD_ALLOCATE + | wasi::__WASI_RIGHTS_FD_FILESTAT_SET_SIZE) + != 0; + + let fd = hostcalls_impl::path_open(resolved, read, write, oflags, fs_flags)?; + + // Determine the type of the new file descriptor and which rights contradict with this type + let (_ty, max_base, max_inheriting) = determine_type_rights(&fd)?; + let mut fe = FdEntry::from(fd)?; + fe.rights_base &= max_base; + fe.rights_inheriting &= max_inheriting; + let guest_fd = wasi_ctx.insert_fd_entry(fe)?; + + trace!(" | *fd={:?}", guest_fd); + + enc_fd_byref(memory, fd_out_ptr, guest_fd) +} + +pub(crate) unsafe fn path_readlink( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, + buf_ptr: wasi32::uintptr_t, + buf_len: wasi32::size_t, + buf_used: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "path_readlink(dirfd={:?}, path_ptr={:#x?}, path_len={:?}, buf_ptr={:#x?}, buf_len={}, buf_used={:#x?})", + dirfd, + path_ptr, + path_len, + buf_ptr, + buf_len, + buf_used, + ); + + enc_usize_byref(memory, buf_used, 0)?; + + let path = dec_slice_of_u8(memory, path_ptr, path_len).and_then(helpers::path_from_slice)?; + + trace!(" | (path_ptr,path_len)='{}'", &path); + + let fe = wasi_ctx.get_fd_entry(dirfd)?; + let resolved = path_get(fe, wasi::__WASI_RIGHTS_PATH_READLINK, 0, 0, &path, false)?; + + let mut buf = dec_slice_of_mut_u8(memory, buf_ptr, buf_len)?; + + let host_bufused = hostcalls_impl::path_readlink(resolved, &mut buf)?; + + trace!(" | (buf_ptr,*buf_used)={:?}", buf); + trace!(" | *buf_used={:?}", host_bufused); + + enc_usize_byref(memory, buf_used, host_bufused) +} + +pub(crate) unsafe fn path_rename( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + old_dirfd: wasi::__wasi_fd_t, + old_path_ptr: wasi32::uintptr_t, + old_path_len: wasi32::size_t, + new_dirfd: wasi::__wasi_fd_t, + new_path_ptr: wasi32::uintptr_t, + new_path_len: wasi32::size_t, +) -> Result<()> { + trace!( + "path_rename(old_dirfd={:?}, old_path_ptr={:#x?}, old_path_len={:?}, new_dirfd={:?}, new_path_ptr={:#x?}, new_path_len={:?})", + old_dirfd, + old_path_ptr, + old_path_len, + new_dirfd, + new_path_ptr, + new_path_len, + ); + + let old_path = dec_slice_of_u8(memory, old_path_ptr, old_path_len).and_then(path_from_slice)?; + let new_path = dec_slice_of_u8(memory, new_path_ptr, new_path_len).and_then(path_from_slice)?; + + trace!(" | (old_path_ptr,old_path_len)='{}'", old_path); + trace!(" | (new_path_ptr,new_path_len)='{}'", new_path); + + let old_fe = wasi_ctx.get_fd_entry(old_dirfd)?; + let new_fe = wasi_ctx.get_fd_entry(new_dirfd)?; + let resolved_old = path_get( + old_fe, + wasi::__WASI_RIGHTS_PATH_RENAME_SOURCE, + 0, + 0, + old_path, + true, + )?; + let resolved_new = path_get( + new_fe, + wasi::__WASI_RIGHTS_PATH_RENAME_TARGET, + 0, + 0, + new_path, + true, + )?; + + log::debug!("path_rename resolved_old={:?}", resolved_old); + log::debug!("path_rename resolved_new={:?}", resolved_new); + + hostcalls_impl::path_rename(resolved_old, resolved_new) +} + +pub(crate) unsafe fn fd_filestat_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + filestat_ptr: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "fd_filestat_get(fd={:?}, filestat_ptr={:#x?})", + fd, + filestat_ptr + ); + + let fd = wasi_ctx.get_fd_entry(fd)?.as_descriptor(0, 0)?.as_file()?; + let host_filestat = hostcalls_impl::fd_filestat_get(fd)?; + + trace!(" | *filestat_ptr={:?}", host_filestat); + + enc_filestat_byref(memory, filestat_ptr, host_filestat) +} + +pub(crate) unsafe fn fd_filestat_set_times( + wasi_ctx: &WasiCtx, + _mem: &mut [u8], + fd: wasi::__wasi_fd_t, + st_atim: wasi::__wasi_timestamp_t, + st_mtim: wasi::__wasi_timestamp_t, + fst_flags: wasi::__wasi_fstflags_t, +) -> Result<()> { + trace!( + "fd_filestat_set_times(fd={:?}, st_atim={}, st_mtim={}, fst_flags={:#x?})", + fd, + st_atim, + st_mtim, + fst_flags + ); + + let fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor(wasi::__WASI_RIGHTS_FD_FILESTAT_SET_TIMES, 0)? + .as_file()?; + + fd_filestat_set_times_impl(fd, st_atim, st_mtim, fst_flags) +} + +pub(crate) fn fd_filestat_set_times_impl( + fd: &File, + st_atim: wasi::__wasi_timestamp_t, + st_mtim: wasi::__wasi_timestamp_t, + fst_flags: wasi::__wasi_fstflags_t, +) -> Result<()> { + let set_atim = fst_flags & wasi::__WASI_FSTFLAGS_ATIM != 0; + let set_atim_now = fst_flags & wasi::__WASI_FSTFLAGS_ATIM_NOW != 0; + let set_mtim = fst_flags & wasi::__WASI_FSTFLAGS_MTIM != 0; + let set_mtim_now = fst_flags & wasi::__WASI_FSTFLAGS_MTIM_NOW != 0; + + if (set_atim && set_atim_now) || (set_mtim && set_mtim_now) { + return Err(Error::EINVAL); + } + let atim = if set_atim { + let time = UNIX_EPOCH + Duration::from_nanos(st_atim); + Some(FileTime::from_system_time(time)) + } else if set_atim_now { + let time = SystemTime::now(); + Some(FileTime::from_system_time(time)) + } else { + None + }; + + let mtim = if set_mtim { + let time = UNIX_EPOCH + Duration::from_nanos(st_mtim); + Some(FileTime::from_system_time(time)) + } else if set_mtim_now { + let time = SystemTime::now(); + Some(FileTime::from_system_time(time)) + } else { + None + }; + set_file_handle_times(fd, atim, mtim).map_err(Into::into) +} + +pub(crate) unsafe fn fd_filestat_set_size( + wasi_ctx: &WasiCtx, + _mem: &mut [u8], + fd: wasi::__wasi_fd_t, + st_size: wasi::__wasi_filesize_t, +) -> Result<()> { + trace!("fd_filestat_set_size(fd={:?}, st_size={})", fd, st_size); + + let fd = wasi_ctx + .get_fd_entry(fd)? + .as_descriptor(wasi::__WASI_RIGHTS_FD_FILESTAT_SET_SIZE, 0)? + .as_file()?; + + // This check will be unnecessary when rust-lang/rust#63326 is fixed + if st_size > i64::max_value() as u64 { + return Err(Error::E2BIG); + } + fd.set_len(st_size).map_err(Into::into) +} + +pub(crate) unsafe fn path_filestat_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + dirflags: wasi::__wasi_lookupflags_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, + filestat_ptr: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "path_filestat_get(dirfd={:?}, dirflags={:?}, path_ptr={:#x?}, path_len={}, filestat_ptr={:#x?})", + dirfd, + dirflags, + path_ptr, + path_len, + filestat_ptr + ); + + let path = dec_slice_of_u8(memory, path_ptr, path_len).and_then(path_from_slice)?; + + trace!(" | (path_ptr,path_len)='{}'", path); + + let fe = wasi_ctx.get_fd_entry(dirfd)?; + let resolved = path_get( + fe, + wasi::__WASI_RIGHTS_PATH_FILESTAT_GET, + 0, + dirflags, + path, + false, + )?; + let host_filestat = hostcalls_impl::path_filestat_get(resolved, dirflags)?; + + trace!(" | *filestat_ptr={:?}", host_filestat); + + enc_filestat_byref(memory, filestat_ptr, host_filestat) +} + +pub(crate) unsafe fn path_filestat_set_times( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + dirflags: wasi::__wasi_lookupflags_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, + st_atim: wasi::__wasi_timestamp_t, + st_mtim: wasi::__wasi_timestamp_t, + fst_flags: wasi::__wasi_fstflags_t, +) -> Result<()> { + trace!( + "path_filestat_set_times(dirfd={:?}, dirflags={:?}, path_ptr={:#x?}, path_len={}, st_atim={}, st_mtim={}, fst_flags={:#x?})", + dirfd, + dirflags, + path_ptr, + path_len, + st_atim, st_mtim, + fst_flags + ); + + let path = dec_slice_of_u8(memory, path_ptr, path_len).and_then(path_from_slice)?; + + trace!(" | (path_ptr,path_len)='{}'", path); + + let fe = wasi_ctx.get_fd_entry(dirfd)?; + let resolved = path_get( + fe, + wasi::__WASI_RIGHTS_PATH_FILESTAT_SET_TIMES, + 0, + dirflags, + path, + false, + )?; + + hostcalls_impl::path_filestat_set_times(resolved, dirflags, st_atim, st_mtim, fst_flags) +} + +pub(crate) unsafe fn path_symlink( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + old_path_ptr: wasi32::uintptr_t, + old_path_len: wasi32::size_t, + dirfd: wasi::__wasi_fd_t, + new_path_ptr: wasi32::uintptr_t, + new_path_len: wasi32::size_t, +) -> Result<()> { + trace!( + "path_symlink(old_path_ptr={:#x?}, old_path_len={}, dirfd={:?}, new_path_ptr={:#x?}, new_path_len={})", + old_path_ptr, + old_path_len, + dirfd, + new_path_ptr, + new_path_len + ); + + let old_path = dec_slice_of_u8(memory, old_path_ptr, old_path_len).and_then(path_from_slice)?; + let new_path = dec_slice_of_u8(memory, new_path_ptr, new_path_len).and_then(path_from_slice)?; + + trace!(" | (old_path_ptr,old_path_len)='{}'", old_path); + trace!(" | (new_path_ptr,new_path_len)='{}'", new_path); + + let fe = wasi_ctx.get_fd_entry(dirfd)?; + let resolved_new = path_get(fe, wasi::__WASI_RIGHTS_PATH_SYMLINK, 0, 0, new_path, true)?; + + hostcalls_impl::path_symlink(old_path, resolved_new) +} + +pub(crate) unsafe fn path_unlink_file( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, +) -> Result<()> { + trace!( + "path_unlink_file(dirfd={:?}, path_ptr={:#x?}, path_len={})", + dirfd, + path_ptr, + path_len + ); + + let path = dec_slice_of_u8(memory, path_ptr, path_len).and_then(path_from_slice)?; + + trace!(" | (path_ptr,path_len)='{}'", path); + + let fe = wasi_ctx.get_fd_entry(dirfd)?; + let resolved = path_get(fe, wasi::__WASI_RIGHTS_PATH_UNLINK_FILE, 0, 0, path, false)?; + + hostcalls_impl::path_unlink_file(resolved) +} + +pub(crate) unsafe fn path_remove_directory( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + dirfd: wasi::__wasi_fd_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, +) -> Result<()> { + trace!( + "path_remove_directory(dirfd={:?}, path_ptr={:#x?}, path_len={})", + dirfd, + path_ptr, + path_len + ); + + let path = dec_slice_of_u8(memory, path_ptr, path_len).and_then(path_from_slice)?; + + trace!(" | (path_ptr,path_len)='{}'", path); + + let fe = wasi_ctx.get_fd_entry(dirfd)?; + let resolved = path_get( + fe, + wasi::__WASI_RIGHTS_PATH_REMOVE_DIRECTORY, + 0, + 0, + path, + true, + )?; + + log::debug!("path_remove_directory resolved={:?}", resolved); + + hostcalls_impl::path_remove_directory(resolved) +} + +pub(crate) unsafe fn fd_prestat_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + prestat_ptr: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "fd_prestat_get(fd={:?}, prestat_ptr={:#x?})", + fd, + prestat_ptr + ); + + // TODO: should we validate any rights here? + let fe = wasi_ctx.get_fd_entry(fd)?; + let po_path = fe.preopen_path.as_ref().ok_or(Error::ENOTSUP)?; + if fe.file_type != wasi::__WASI_FILETYPE_DIRECTORY { + return Err(Error::ENOTDIR); + } + + let path = host_impl::path_from_host(po_path.as_os_str())?; + + enc_prestat_byref( + memory, + prestat_ptr, + host::__wasi_prestat_t { + tag: wasi::__WASI_PREOPENTYPE_DIR, + u: host::__wasi_prestat_u_t { + dir: host::__wasi_prestat_dir_t { + pr_name_len: path.len(), + }, + }, + }, + ) +} + +pub(crate) unsafe fn fd_prestat_dir_name( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + path_ptr: wasi32::uintptr_t, + path_len: wasi32::size_t, +) -> Result<()> { + trace!( + "fd_prestat_dir_name(fd={:?}, path_ptr={:#x?}, path_len={})", + fd, + path_ptr, + path_len + ); + + // TODO: should we validate any rights here? + let fe = wasi_ctx.get_fd_entry(fd)?; + let po_path = fe.preopen_path.as_ref().ok_or(Error::ENOTSUP)?; + if fe.file_type != wasi::__WASI_FILETYPE_DIRECTORY { + return Err(Error::ENOTDIR); + } + + let path = host_impl::path_from_host(po_path.as_os_str())?; + + if path.len() > dec_usize(path_len) { + return Err(Error::ENAMETOOLONG); + } + + trace!(" | (path_ptr,path_len)='{}'", path); + + enc_slice_of_u8(memory, path.as_bytes(), path_ptr) +} + +pub(crate) unsafe fn fd_readdir( + wasi_ctx: &mut WasiCtx, + memory: &mut [u8], + fd: wasi::__wasi_fd_t, + buf: wasi32::uintptr_t, + buf_len: wasi32::size_t, + cookie: wasi::__wasi_dircookie_t, + buf_used: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "fd_readdir(fd={:?}, buf={:#x?}, buf_len={}, cookie={:#x?}, buf_used={:#x?})", + fd, + buf, + buf_len, + cookie, + buf_used, + ); + + enc_usize_byref(memory, buf_used, 0)?; + + let file = wasi_ctx + .get_fd_entry_mut(fd)? + .as_descriptor_mut(wasi::__WASI_RIGHTS_FD_READDIR, 0)? + .as_file_mut()?; + let mut host_buf = dec_slice_of_mut_u8(memory, buf, buf_len)?; + + trace!(" | (buf,buf_len)={:?}", host_buf); + + let iter = hostcalls_impl::fd_readdir(file, cookie)?; + let mut host_bufused = 0; + for dirent in iter { + let dirent_raw = dirent?.to_wasi_raw()?; + let offset = dirent_raw.len(); + if host_buf.len() < offset { + break; + } else { + host_buf[0..offset].copy_from_slice(&dirent_raw); + host_bufused += offset; + host_buf = &mut host_buf[offset..]; + } + } + + trace!(" | *buf_used={:?}", host_bufused); + + enc_usize_byref(memory, buf_used, host_bufused) +} diff --git a/crates/wasi-common/src/old/snapshot_0/hostcalls_impl/fs_helpers.rs b/crates/wasi-common/src/old/snapshot_0/hostcalls_impl/fs_helpers.rs new file mode 100644 index 0000000000..ff41bbc3a9 --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/hostcalls_impl/fs_helpers.rs @@ -0,0 +1,213 @@ +#![allow(non_camel_case_types)] +use crate::old::snapshot_0::sys::host_impl; +use crate::old::snapshot_0::sys::hostcalls_impl::fs_helpers::*; +use crate::old::snapshot_0::{error::WasiError, fdentry::FdEntry, wasi, Error, Result}; +use std::fs::File; +use std::path::{Component, Path}; + +#[derive(Debug)] +pub(crate) struct PathGet { + dirfd: File, + path: String, +} + +impl PathGet { + pub(crate) fn dirfd(&self) -> &File { + &self.dirfd + } + + pub(crate) fn path(&self) -> &str { + &self.path + } +} + +/// Normalizes a path to ensure that the target path is located under the directory provided. +/// +/// This is a workaround for not having Capsicum support in the OS. +pub(crate) fn path_get( + fe: &FdEntry, + rights_base: wasi::__wasi_rights_t, + rights_inheriting: wasi::__wasi_rights_t, + dirflags: wasi::__wasi_lookupflags_t, + path: &str, + needs_final_component: bool, +) -> Result { + const MAX_SYMLINK_EXPANSIONS: usize = 128; + + if path.contains('\0') { + // if contains NUL, return EILSEQ + return Err(Error::EILSEQ); + } + + if fe.file_type != wasi::__WASI_FILETYPE_DIRECTORY { + // if `dirfd` doesn't refer to a directory, return `ENOTDIR`. + return Err(Error::ENOTDIR); + } + + let dirfd = fe + .as_descriptor(rights_base, rights_inheriting)? + .as_file()? + .try_clone()?; + + // Stack of directory file descriptors. Index 0 always corresponds with the directory provided + // to this function. Entering a directory causes a file descriptor to be pushed, while handling + // ".." entries causes an entry to be popped. Index 0 cannot be popped, as this would imply + // escaping the base directory. + let mut dir_stack = vec![dirfd]; + + // Stack of paths left to process. This is initially the `path` argument to this function, but + // any symlinks we encounter are processed by pushing them on the stack. + let mut path_stack = vec![path.to_owned()]; + + // Track the number of symlinks we've expanded, so we can return `ELOOP` after too many. + let mut symlink_expansions = 0; + + // TODO: rewrite this using a custom posix path type, with a component iterator that respects + // trailing slashes. This version does way too much allocation, and is way too fiddly. + loop { + match path_stack.pop() { + Some(cur_path) => { + log::debug!("path_get cur_path = {:?}", cur_path); + + let ends_with_slash = cur_path.ends_with('/'); + let mut components = Path::new(&cur_path).components(); + let head = match components.next() { + None => return Err(Error::ENOENT), + Some(p) => p, + }; + let tail = components.as_path(); + + if tail.components().next().is_some() { + let mut tail = host_impl::path_from_host(tail.as_os_str())?; + if ends_with_slash { + tail.push('/'); + } + path_stack.push(tail); + } + + log::debug!("path_get path_stack = {:?}", path_stack); + + match head { + Component::Prefix(_) | Component::RootDir => { + // path is absolute! + return Err(Error::ENOTCAPABLE); + } + Component::CurDir => { + // "." so skip + } + Component::ParentDir => { + // ".." so pop a dir + let _ = dir_stack.pop().ok_or(Error::ENOTCAPABLE)?; + + // we're not allowed to pop past the original directory + if dir_stack.is_empty() { + return Err(Error::ENOTCAPABLE); + } + } + Component::Normal(head) => { + let mut head = host_impl::path_from_host(head)?; + if ends_with_slash { + // preserve trailing slash + head.push('/'); + } + + if !path_stack.is_empty() || (ends_with_slash && !needs_final_component) { + match openat(dir_stack.last().ok_or(Error::ENOTCAPABLE)?, &head) { + Ok(new_dir) => { + dir_stack.push(new_dir); + } + Err(e) => { + match e.as_wasi_error() { + WasiError::ELOOP + | WasiError::EMLINK + | WasiError::ENOTDIR => + // Check to see if it was a symlink. Linux indicates + // this with ENOTDIR because of the O_DIRECTORY flag. + { + // attempt symlink expansion + let mut link_path = readlinkat( + dir_stack.last().ok_or(Error::ENOTCAPABLE)?, + &head, + )?; + + symlink_expansions += 1; + if symlink_expansions > MAX_SYMLINK_EXPANSIONS { + return Err(Error::ELOOP); + } + + if head.ends_with('/') { + link_path.push('/'); + } + + log::debug!( + "attempted symlink expansion link_path={:?}", + link_path + ); + + path_stack.push(link_path); + } + _ => { + return Err(e); + } + } + } + } + + continue; + } else if ends_with_slash + || (dirflags & wasi::__WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) != 0 + { + // if there's a trailing slash, or if `LOOKUP_SYMLINK_FOLLOW` is set, attempt + // symlink expansion + match readlinkat(dir_stack.last().ok_or(Error::ENOTCAPABLE)?, &head) { + Ok(mut link_path) => { + symlink_expansions += 1; + if symlink_expansions > MAX_SYMLINK_EXPANSIONS { + return Err(Error::ELOOP); + } + + if head.ends_with('/') { + link_path.push('/'); + } + + log::debug!( + "attempted symlink expansion link_path={:?}", + link_path + ); + + path_stack.push(link_path); + continue; + } + Err(e) => { + if e.as_wasi_error() != WasiError::EINVAL + && e.as_wasi_error() != WasiError::ENOENT + // this handles the cases when trying to link to + // a destination that already exists, and the target + // path contains a slash + && e.as_wasi_error() != WasiError::ENOTDIR + { + return Err(e); + } + } + } + } + + // not a symlink, so we're done; + return Ok(PathGet { + dirfd: dir_stack.pop().ok_or(Error::ENOTCAPABLE)?, + path: head, + }); + } + } + } + None => { + // no further components to process. means we've hit a case like "." or "a/..", or if the + // input path has trailing slashes and `needs_final_component` is not set + return Ok(PathGet { + dirfd: dir_stack.pop().ok_or(Error::ENOTCAPABLE)?, + path: String::from("."), + }); + } + } + } +} diff --git a/crates/wasi-common/src/old/snapshot_0/hostcalls_impl/misc.rs b/crates/wasi-common/src/old/snapshot_0/hostcalls_impl/misc.rs new file mode 100644 index 0000000000..14820a3e66 --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/hostcalls_impl/misc.rs @@ -0,0 +1,353 @@ +#![allow(non_camel_case_types)] +use crate::old::snapshot_0::ctx::WasiCtx; +use crate::old::snapshot_0::fdentry::Descriptor; +use crate::old::snapshot_0::memory::*; +use crate::old::snapshot_0::sys::hostcalls_impl; +use crate::old::snapshot_0::{wasi, wasi32, Error, Result}; +use log::{error, trace}; +use std::convert::TryFrom; + +pub(crate) fn args_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + argv_ptr: wasi32::uintptr_t, + argv_buf: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "args_get(argv_ptr={:#x?}, argv_buf={:#x?})", + argv_ptr, + argv_buf, + ); + + let mut argv_buf_offset = 0; + let mut argv = vec![]; + + for arg in &wasi_ctx.args { + let arg_bytes = arg.as_bytes_with_nul(); + let arg_ptr = argv_buf + argv_buf_offset; + + enc_slice_of_u8(memory, arg_bytes, arg_ptr)?; + + argv.push(arg_ptr); + + let len = wasi32::uintptr_t::try_from(arg_bytes.len())?; + argv_buf_offset = argv_buf_offset.checked_add(len).ok_or(Error::EOVERFLOW)?; + } + + enc_slice_of_wasi32_uintptr(memory, argv.as_slice(), argv_ptr) +} + +pub(crate) fn args_sizes_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + argc_ptr: wasi32::uintptr_t, + argv_buf_size_ptr: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "args_sizes_get(argc_ptr={:#x?}, argv_buf_size_ptr={:#x?})", + argc_ptr, + argv_buf_size_ptr, + ); + + let argc = wasi_ctx.args.len(); + let argv_size = wasi_ctx + .args + .iter() + .map(|arg| arg.as_bytes_with_nul().len()) + .sum(); + + trace!(" | *argc_ptr={:?}", argc); + + enc_usize_byref(memory, argc_ptr, argc)?; + + trace!(" | *argv_buf_size_ptr={:?}", argv_size); + + enc_usize_byref(memory, argv_buf_size_ptr, argv_size) +} + +pub(crate) fn environ_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + environ_ptr: wasi32::uintptr_t, + environ_buf: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "environ_get(environ_ptr={:#x?}, environ_buf={:#x?})", + environ_ptr, + environ_buf, + ); + + let mut environ_buf_offset = 0; + let mut environ = vec![]; + + for pair in &wasi_ctx.env { + let env_bytes = pair.as_bytes_with_nul(); + let env_ptr = environ_buf + environ_buf_offset; + + enc_slice_of_u8(memory, env_bytes, env_ptr)?; + + environ.push(env_ptr); + + let len = wasi32::uintptr_t::try_from(env_bytes.len())?; + environ_buf_offset = environ_buf_offset + .checked_add(len) + .ok_or(Error::EOVERFLOW)?; + } + + enc_slice_of_wasi32_uintptr(memory, environ.as_slice(), environ_ptr) +} + +pub(crate) fn environ_sizes_get( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + environ_count_ptr: wasi32::uintptr_t, + environ_size_ptr: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "environ_sizes_get(environ_count_ptr={:#x?}, environ_size_ptr={:#x?})", + environ_count_ptr, + environ_size_ptr, + ); + + let environ_count = wasi_ctx.env.len(); + let environ_size = wasi_ctx + .env + .iter() + .try_fold(0, |acc: u32, pair| { + acc.checked_add(pair.as_bytes_with_nul().len() as u32) + }) + .ok_or(Error::EOVERFLOW)?; + + trace!(" | *environ_count_ptr={:?}", environ_count); + + enc_usize_byref(memory, environ_count_ptr, environ_count)?; + + trace!(" | *environ_size_ptr={:?}", environ_size); + + enc_usize_byref(memory, environ_size_ptr, environ_size as usize) +} + +pub(crate) fn random_get( + _wasi_ctx: &WasiCtx, + memory: &mut [u8], + buf_ptr: wasi32::uintptr_t, + buf_len: wasi32::size_t, +) -> Result<()> { + trace!("random_get(buf_ptr={:#x?}, buf_len={:?})", buf_ptr, buf_len); + + let buf = dec_slice_of_mut_u8(memory, buf_ptr, buf_len)?; + + getrandom::getrandom(buf).map_err(|err| { + error!("getrandom failure: {:?}", err); + Error::EIO + }) +} + +pub(crate) fn clock_res_get( + _wasi_ctx: &WasiCtx, + memory: &mut [u8], + clock_id: wasi::__wasi_clockid_t, + resolution_ptr: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "clock_res_get(clock_id={:?}, resolution_ptr={:#x?})", + clock_id, + resolution_ptr, + ); + + let resolution = hostcalls_impl::clock_res_get(clock_id)?; + + trace!(" | *resolution_ptr={:?}", resolution); + + enc_timestamp_byref(memory, resolution_ptr, resolution) +} + +pub(crate) fn clock_time_get( + _wasi_ctx: &WasiCtx, + memory: &mut [u8], + clock_id: wasi::__wasi_clockid_t, + precision: wasi::__wasi_timestamp_t, + time_ptr: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "clock_time_get(clock_id={:?}, precision={:?}, time_ptr={:#x?})", + clock_id, + precision, + time_ptr, + ); + + let time = hostcalls_impl::clock_time_get(clock_id)?; + + trace!(" | *time_ptr={:?}", time); + + enc_timestamp_byref(memory, time_ptr, time) +} + +pub(crate) fn sched_yield(_wasi_ctx: &WasiCtx, _memory: &mut [u8]) -> Result<()> { + trace!("sched_yield()"); + + std::thread::yield_now(); + + Ok(()) +} + +pub(crate) fn poll_oneoff( + wasi_ctx: &WasiCtx, + memory: &mut [u8], + input: wasi32::uintptr_t, + output: wasi32::uintptr_t, + nsubscriptions: wasi32::size_t, + nevents: wasi32::uintptr_t, +) -> Result<()> { + trace!( + "poll_oneoff(input={:#x?}, output={:#x?}, nsubscriptions={}, nevents={:#x?})", + input, + output, + nsubscriptions, + nevents, + ); + + if u64::from(nsubscriptions) > wasi::__wasi_filesize_t::max_value() { + return Err(Error::EINVAL); + } + + enc_int_byref(memory, nevents, 0)?; + + let subscriptions = dec_subscriptions(memory, input, nsubscriptions)?; + let mut events = Vec::new(); + + let mut timeout: Option = None; + let mut fd_events = Vec::new(); + for subscription in subscriptions { + match subscription.u.tag { + wasi::__WASI_EVENTTYPE_CLOCK => { + let clock = unsafe { subscription.u.u.clock }; + let delay = wasi_clock_to_relative_ns_delay(clock)?; + + log::debug!("poll_oneoff event.u.clock = {:?}", clock); + log::debug!("poll_oneoff delay = {:?}ns", delay); + + let current = ClockEventData { + delay, + userdata: subscription.userdata, + }; + let timeout = timeout.get_or_insert(current); + if current.delay < timeout.delay { + *timeout = current; + } + } + + wasi::__WASI_EVENTTYPE_FD_READ => { + let wasi_fd = unsafe { subscription.u.u.fd_read.file_descriptor }; + let rights = wasi::__WASI_RIGHTS_FD_READ | wasi::__WASI_RIGHTS_POLL_FD_READWRITE; + match unsafe { + wasi_ctx + .get_fd_entry(wasi_fd) + .and_then(|fe| fe.as_descriptor(rights, 0)) + } { + Ok(descriptor) => fd_events.push(FdEventData { + descriptor, + r#type: wasi::__WASI_EVENTTYPE_FD_READ, + userdata: subscription.userdata, + }), + Err(err) => { + let event = wasi::__wasi_event_t { + userdata: subscription.userdata, + error: err.as_wasi_error().as_raw_errno(), + r#type: wasi::__WASI_EVENTTYPE_FD_READ, + fd_readwrite: wasi::__wasi_event_fd_readwrite_t { + nbytes: 0, + flags: 0, + }, + }; + events.push(event); + } + }; + } + + wasi::__WASI_EVENTTYPE_FD_WRITE => { + let wasi_fd = unsafe { subscription.u.u.fd_write.file_descriptor }; + let rights = wasi::__WASI_RIGHTS_FD_WRITE | wasi::__WASI_RIGHTS_POLL_FD_READWRITE; + match unsafe { + wasi_ctx + .get_fd_entry(wasi_fd) + .and_then(|fe| fe.as_descriptor(rights, 0)) + } { + Ok(descriptor) => fd_events.push(FdEventData { + descriptor, + r#type: wasi::__WASI_EVENTTYPE_FD_WRITE, + userdata: subscription.userdata, + }), + Err(err) => { + let event = wasi::__wasi_event_t { + userdata: subscription.userdata, + error: err.as_wasi_error().as_raw_errno(), + r#type: wasi::__WASI_EVENTTYPE_FD_WRITE, + fd_readwrite: wasi::__wasi_event_fd_readwrite_t { + nbytes: 0, + flags: 0, + }, + }; + events.push(event); + } + }; + } + _ => unreachable!(), + } + } + + log::debug!("poll_oneoff timeout = {:?}", timeout); + log::debug!("poll_oneoff fd_events = {:?}", fd_events); + + hostcalls_impl::poll_oneoff(timeout, fd_events, &mut events)?; + + let events_count = u32::try_from(events.len()).map_err(|_| Error::EOVERFLOW)?; + + enc_events(memory, output, nsubscriptions, events)?; + + trace!(" | *nevents={:?}", events_count); + + enc_int_byref(memory, nevents, events_count) +} + +fn wasi_clock_to_relative_ns_delay(wasi_clock: wasi::__wasi_subscription_clock_t) -> Result { + use std::time::SystemTime; + + if wasi_clock.flags != wasi::__WASI_SUBCLOCKFLAGS_SUBSCRIPTION_CLOCK_ABSTIME { + return Ok(u128::from(wasi_clock.timeout)); + } + let now: u128 = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .map_err(|_| Error::ENOTCAPABLE)? + .as_nanos(); + let deadline = u128::from(wasi_clock.timeout); + Ok(deadline.saturating_sub(now)) +} + +#[derive(Debug, Copy, Clone)] +pub(crate) struct ClockEventData { + pub(crate) delay: u128, // delay is expressed in nanoseconds + pub(crate) userdata: wasi::__wasi_userdata_t, +} + +#[derive(Debug)] +pub(crate) struct FdEventData<'a> { + pub(crate) descriptor: &'a Descriptor, + pub(crate) r#type: wasi::__wasi_eventtype_t, + pub(crate) userdata: wasi::__wasi_userdata_t, +} + +pub(crate) fn proc_exit(_wasi_ctx: &WasiCtx, _memory: &mut [u8], rval: wasi::__wasi_exitcode_t) { + trace!("proc_exit(rval={:?})", rval); + // TODO: Rather than call std::process::exit here, we should trigger a + // stack unwind similar to a trap. + std::process::exit(rval as i32); +} + +pub(crate) fn proc_raise( + _wasi_ctx: &WasiCtx, + _memory: &mut [u8], + _sig: wasi::__wasi_signal_t, +) -> Result<()> { + unimplemented!("proc_raise") +} diff --git a/crates/wasi-common/src/old/snapshot_0/hostcalls_impl/mod.rs b/crates/wasi-common/src/old/snapshot_0/hostcalls_impl/mod.rs new file mode 100644 index 0000000000..924c8c1ba9 --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/hostcalls_impl/mod.rs @@ -0,0 +1,9 @@ +mod fs; +mod fs_helpers; +mod misc; +mod sock; + +pub(crate) use self::fs::*; +pub(crate) use self::fs_helpers::PathGet; +pub(crate) use self::misc::*; +pub(crate) use self::sock::*; diff --git a/crates/wasi-common/src/old/snapshot_0/hostcalls_impl/sock.rs b/crates/wasi-common/src/old/snapshot_0/hostcalls_impl/sock.rs new file mode 100644 index 0000000000..ac485413ad --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/hostcalls_impl/sock.rs @@ -0,0 +1,35 @@ +use crate::old::snapshot_0::{wasi, wasi32, Result, WasiCtx}; + +pub fn sock_recv( + _wasi_ctx: &WasiCtx, + _memory: &mut [u8], + _sock: wasi::__wasi_fd_t, + _ri_data: wasi32::uintptr_t, + _ri_data_len: wasi32::size_t, + _ri_flags: wasi::__wasi_riflags_t, + _ro_datalen: wasi32::uintptr_t, + _ro_flags: wasi32::uintptr_t, +) -> Result<()> { + unimplemented!("sock_recv") +} + +pub fn sock_send( + _wasi_ctx: &WasiCtx, + _memory: &mut [u8], + _sock: wasi::__wasi_fd_t, + _si_data: wasi32::uintptr_t, + _si_data_len: wasi32::size_t, + _si_flags: wasi::__wasi_siflags_t, + _so_datalen: wasi32::uintptr_t, +) -> Result<()> { + unimplemented!("sock_send") +} + +pub fn sock_shutdown( + _wasi_ctx: &WasiCtx, + _memory: &mut [u8], + _sock: wasi::__wasi_fd_t, + _how: wasi::__wasi_sdflags_t, +) -> Result<()> { + unimplemented!("sock_shutdown") +} diff --git a/crates/wasi-common/src/old/snapshot_0/memory.rs b/crates/wasi-common/src/old/snapshot_0/memory.rs new file mode 100644 index 0000000000..c4f1d007ec --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/memory.rs @@ -0,0 +1,481 @@ +//! Functions to store and load data to and from wasm linear memory, +//! transforming them from and to host data types. +//! +//! Endianness concerns are completely encapsulated in this file, so +//! that users outside this file holding a `wasi::*` value never need +//! to consider what endianness it's in. Inside this file, +//! wasm linear-memory-ordered values are called "raw" values, and +//! are not held for long durations. + +#![allow(unused)] +use crate::old::snapshot_0::{host, wasi, wasi32, Error, Result}; +use num::PrimInt; +use std::convert::TryFrom; +use std::mem::{align_of, size_of}; +use std::{ptr, slice}; + +fn dec_ptr(memory: &[u8], ptr: wasi32::uintptr_t, len: usize) -> Result<*const u8> { + // check for overflow + let checked_len = (ptr as usize).checked_add(len).ok_or(Error::EFAULT)?; + + // translate the pointer + memory + .get(ptr as usize..checked_len) + .ok_or(Error::EFAULT) + .map(|mem| mem.as_ptr()) +} + +fn dec_ptr_mut(memory: &mut [u8], ptr: wasi32::uintptr_t, len: usize) -> Result<*mut u8> { + // check for overflow + let checked_len = (ptr as usize).checked_add(len).ok_or(Error::EFAULT)?; + + // translate the pointer + memory + .get_mut(ptr as usize..checked_len) + .ok_or(Error::EFAULT) + .map(|mem| mem.as_mut_ptr()) +} + +fn dec_ptr_to<'memory, T>(memory: &'memory [u8], ptr: wasi32::uintptr_t) -> Result<&'memory T> { + // check that the ptr is aligned + if ptr as usize % align_of::() != 0 { + return Err(Error::EINVAL); + } + + dec_ptr(memory, ptr, size_of::()).map(|p| unsafe { &*(p as *const T) }) +} + +fn dec_ptr_to_mut<'memory, T>( + memory: &'memory mut [u8], + ptr: wasi32::uintptr_t, +) -> Result<&'memory mut T> { + // check that the ptr is aligned + if ptr as usize % align_of::() != 0 { + return Err(Error::EINVAL); + } + + dec_ptr_mut(memory, ptr, size_of::()).map(|p| unsafe { &mut *(p as *mut T) }) +} + +/// This function does not perform endianness conversions! +fn dec_raw_byref(memory: &[u8], ptr: wasi32::uintptr_t) -> Result { + dec_ptr_to::(memory, ptr).map(|p| unsafe { ptr::read(p) }) +} + +/// This function does not perform endianness conversions! +fn enc_raw_byref(memory: &mut [u8], ptr: wasi32::uintptr_t, t: T) -> Result<()> { + dec_ptr_to_mut::(memory, ptr).map(|p| unsafe { ptr::write(p, t) }) +} + +pub(crate) fn dec_int_byref(memory: &[u8], ptr: wasi32::uintptr_t) -> Result +where + T: PrimInt, +{ + dec_raw_byref::(memory, ptr).map(|i| PrimInt::from_le(i)) +} + +pub(crate) fn enc_int_byref(memory: &mut [u8], ptr: wasi32::uintptr_t, t: T) -> Result<()> +where + T: PrimInt, +{ + enc_raw_byref::(memory, ptr, PrimInt::to_le(t)) +} + +fn check_slice_of(ptr: wasi32::uintptr_t, len: wasi32::size_t) -> Result<(usize, usize)> { + // check alignment, and that length doesn't overflow + if ptr as usize % align_of::() != 0 { + return Err(Error::EINVAL); + } + let len = dec_usize(len); + let len_bytes = if let Some(len) = size_of::().checked_mul(len) { + len + } else { + return Err(Error::EOVERFLOW); + }; + + Ok((len, len_bytes)) +} + +fn dec_raw_slice_of<'memory, T>( + memory: &'memory [u8], + ptr: wasi32::uintptr_t, + len: wasi32::size_t, +) -> Result<&'memory [T]> { + let (len, len_bytes) = check_slice_of::(ptr, len)?; + let ptr = dec_ptr(memory, ptr, len_bytes)? as *const T; + Ok(unsafe { slice::from_raw_parts(ptr, len) }) +} + +fn dec_raw_slice_of_mut<'memory, T>( + memory: &'memory mut [u8], + ptr: wasi32::uintptr_t, + len: wasi32::size_t, +) -> Result<&'memory mut [T]> { + let (len, len_bytes) = check_slice_of::(ptr, len)?; + let ptr = dec_ptr_mut(memory, ptr, len_bytes)? as *mut T; + Ok(unsafe { slice::from_raw_parts_mut(ptr, len) }) +} + +fn raw_slice_for_enc<'memory, T>( + memory: &'memory mut [u8], + slice: &[T], + ptr: wasi32::uintptr_t, +) -> Result<&'memory mut [T]> { + // check alignment + if ptr as usize % align_of::() != 0 { + return Err(Error::EINVAL); + } + // check that length doesn't overflow + let len_bytes = if let Some(len) = size_of::().checked_mul(slice.len()) { + len + } else { + return Err(Error::EOVERFLOW); + }; + + // get the pointer into guest memory + let ptr = dec_ptr_mut(memory, ptr, len_bytes)? as *mut T; + + Ok(unsafe { slice::from_raw_parts_mut(ptr, slice.len()) }) +} + +pub(crate) fn dec_slice_of_u8<'memory>( + memory: &'memory [u8], + ptr: wasi32::uintptr_t, + len: wasi32::size_t, +) -> Result<&'memory [u8]> { + dec_raw_slice_of::(memory, ptr, len) +} + +pub(crate) fn dec_slice_of_mut_u8<'memory>( + memory: &'memory mut [u8], + ptr: wasi32::uintptr_t, + len: wasi32::size_t, +) -> Result<&'memory mut [u8]> { + dec_raw_slice_of_mut::(memory, ptr, len) +} + +pub(crate) fn enc_slice_of_u8( + memory: &mut [u8], + slice: &[u8], + ptr: wasi32::uintptr_t, +) -> Result<()> { + let output = raw_slice_for_enc::(memory, slice, ptr)?; + + output.copy_from_slice(slice); + + Ok(()) +} + +pub(crate) fn enc_slice_of_wasi32_uintptr( + memory: &mut [u8], + slice: &[wasi32::uintptr_t], + ptr: wasi32::uintptr_t, +) -> Result<()> { + let mut output_iter = raw_slice_for_enc::(memory, slice, ptr)?.into_iter(); + + for p in slice { + *output_iter.next().unwrap() = PrimInt::to_le(*p); + } + + Ok(()) +} + +macro_rules! dec_enc_scalar { + ($ty:ident, $dec_byref:ident, $enc_byref:ident) => { + pub(crate) fn $dec_byref(memory: &mut [u8], ptr: wasi32::uintptr_t) -> Result { + dec_int_byref::(memory, ptr) + } + + pub(crate) fn $enc_byref( + memory: &mut [u8], + ptr: wasi32::uintptr_t, + x: wasi::$ty, + ) -> Result<()> { + enc_int_byref::(memory, ptr, x) + } + }; +} + +pub(crate) fn dec_ciovec_slice( + memory: &[u8], + ptr: wasi32::uintptr_t, + len: wasi32::size_t, +) -> Result> { + let raw_slice = dec_raw_slice_of::(memory, ptr, len)?; + + raw_slice + .iter() + .map(|raw_iov| { + let len = dec_usize(PrimInt::from_le(raw_iov.buf_len)); + let buf = PrimInt::from_le(raw_iov.buf); + Ok(host::__wasi_ciovec_t { + buf: dec_ptr(memory, buf, len)? as *const u8, + buf_len: len, + }) + }) + .collect() +} + +pub(crate) fn dec_iovec_slice( + memory: &[u8], + ptr: wasi32::uintptr_t, + len: wasi32::size_t, +) -> Result> { + let raw_slice = dec_raw_slice_of::(memory, ptr, len)?; + + raw_slice + .iter() + .map(|raw_iov| { + let len = dec_usize(PrimInt::from_le(raw_iov.buf_len)); + let buf = PrimInt::from_le(raw_iov.buf); + Ok(host::__wasi_iovec_t { + buf: dec_ptr(memory, buf, len)? as *mut u8, + buf_len: len, + }) + }) + .collect() +} + +dec_enc_scalar!(__wasi_clockid_t, dec_clockid_byref, enc_clockid_byref); +dec_enc_scalar!(__wasi_errno_t, dec_errno_byref, enc_errno_byref); +dec_enc_scalar!(__wasi_exitcode_t, dec_exitcode_byref, enc_exitcode_byref); +dec_enc_scalar!(__wasi_fd_t, dec_fd_byref, enc_fd_byref); +dec_enc_scalar!(__wasi_fdflags_t, dec_fdflags_byref, enc_fdflags_byref); +dec_enc_scalar!(__wasi_device_t, dev_device_byref, enc_device_byref); +dec_enc_scalar!(__wasi_inode_t, dev_inode_byref, enc_inode_byref); +dec_enc_scalar!(__wasi_linkcount_t, dev_linkcount_byref, enc_linkcount_byref); + +pub(crate) fn dec_filestat_byref( + memory: &mut [u8], + filestat_ptr: wasi32::uintptr_t, +) -> Result { + let raw = dec_raw_byref::(memory, filestat_ptr)?; + + Ok(wasi::__wasi_filestat_t { + dev: PrimInt::from_le(raw.dev), + ino: PrimInt::from_le(raw.ino), + filetype: PrimInt::from_le(raw.filetype), + nlink: PrimInt::from_le(raw.nlink), + size: PrimInt::from_le(raw.size), + atim: PrimInt::from_le(raw.atim), + mtim: PrimInt::from_le(raw.mtim), + ctim: PrimInt::from_le(raw.ctim), + }) +} + +pub(crate) fn enc_filestat_byref( + memory: &mut [u8], + filestat_ptr: wasi32::uintptr_t, + filestat: wasi::__wasi_filestat_t, +) -> Result<()> { + let raw = wasi::__wasi_filestat_t { + dev: PrimInt::to_le(filestat.dev), + ino: PrimInt::to_le(filestat.ino), + filetype: PrimInt::to_le(filestat.filetype), + nlink: PrimInt::to_le(filestat.nlink), + size: PrimInt::to_le(filestat.size), + atim: PrimInt::to_le(filestat.atim), + mtim: PrimInt::to_le(filestat.mtim), + ctim: PrimInt::to_le(filestat.ctim), + }; + + enc_raw_byref::(memory, filestat_ptr, raw) +} + +pub(crate) fn dec_fdstat_byref( + memory: &mut [u8], + fdstat_ptr: wasi32::uintptr_t, +) -> Result { + let raw = dec_raw_byref::(memory, fdstat_ptr)?; + + Ok(wasi::__wasi_fdstat_t { + fs_filetype: PrimInt::from_le(raw.fs_filetype), + fs_flags: PrimInt::from_le(raw.fs_flags), + fs_rights_base: PrimInt::from_le(raw.fs_rights_base), + fs_rights_inheriting: PrimInt::from_le(raw.fs_rights_inheriting), + }) +} + +pub(crate) fn enc_fdstat_byref( + memory: &mut [u8], + fdstat_ptr: wasi32::uintptr_t, + fdstat: wasi::__wasi_fdstat_t, +) -> Result<()> { + let raw = wasi::__wasi_fdstat_t { + fs_filetype: PrimInt::to_le(fdstat.fs_filetype), + fs_flags: PrimInt::to_le(fdstat.fs_flags), + fs_rights_base: PrimInt::to_le(fdstat.fs_rights_base), + fs_rights_inheriting: PrimInt::to_le(fdstat.fs_rights_inheriting), + }; + + enc_raw_byref::(memory, fdstat_ptr, raw) +} + +dec_enc_scalar!(__wasi_filedelta_t, dec_filedelta_byref, enc_filedelta_byref); +dec_enc_scalar!(__wasi_filesize_t, dec_filesize_byref, enc_filesize_byref); +dec_enc_scalar!(__wasi_filetype_t, dec_filetype_byref, enc_filetype_byref); + +dec_enc_scalar!( + __wasi_lookupflags_t, + dec_lookupflags_byref, + enc_lookupflags_byref +); + +dec_enc_scalar!(__wasi_oflags_t, dec_oflags_byref, enc_oflags_byref); + +pub(crate) fn dec_prestat_byref( + memory: &mut [u8], + prestat_ptr: wasi32::uintptr_t, +) -> Result { + let raw = dec_raw_byref::(memory, prestat_ptr)?; + + match PrimInt::from_le(raw.tag) { + wasi::__WASI_PREOPENTYPE_DIR => Ok(host::__wasi_prestat_t { + tag: wasi::__WASI_PREOPENTYPE_DIR, + u: host::__wasi_prestat_u_t { + dir: host::__wasi_prestat_dir_t { + pr_name_len: dec_usize(PrimInt::from_le(unsafe { raw.u.dir.pr_name_len })), + }, + }, + }), + _ => Err(Error::EINVAL), + } +} + +pub(crate) fn enc_prestat_byref( + memory: &mut [u8], + prestat_ptr: wasi32::uintptr_t, + prestat: host::__wasi_prestat_t, +) -> Result<()> { + let raw = match prestat.tag { + wasi::__WASI_PREOPENTYPE_DIR => Ok(wasi32::__wasi_prestat_t { + tag: PrimInt::to_le(wasi::__WASI_PREOPENTYPE_DIR), + u: wasi32::__wasi_prestat_u_t { + dir: wasi32::__wasi_prestat_dir_t { + pr_name_len: enc_usize(unsafe { prestat.u.dir.pr_name_len }), + }, + }, + }), + _ => Err(Error::EINVAL), + }?; + + enc_raw_byref::(memory, prestat_ptr, raw) +} + +dec_enc_scalar!(__wasi_rights_t, dec_rights_byref, enc_rights_byref); +dec_enc_scalar!(__wasi_timestamp_t, dec_timestamp_byref, enc_timestamp_byref); + +pub(crate) fn dec_usize(size: wasi32::size_t) -> usize { + usize::try_from(size).unwrap() +} + +pub(crate) fn enc_usize(size: usize) -> wasi32::size_t { + wasi32::size_t::try_from(size).unwrap() +} + +pub(crate) fn enc_usize_byref( + memory: &mut [u8], + usize_ptr: wasi32::uintptr_t, + host_usize: usize, +) -> Result<()> { + enc_int_byref::(memory, usize_ptr, enc_usize(host_usize)) +} + +dec_enc_scalar!(__wasi_whence_t, dec_whence_byref, enc_whence_byref); + +dec_enc_scalar!( + __wasi_subclockflags_t, + dec_subclockflags_byref, + enc_subclockflags_byref +); + +dec_enc_scalar!( + __wasi_eventrwflags_t, + dec_eventrwflags_byref, + enc_eventrwflags_byref +); + +dec_enc_scalar!(__wasi_eventtype_t, dec_eventtype_byref, enc_eventtype_byref); +dec_enc_scalar!(__wasi_userdata_t, dec_userdata_byref, enc_userdata_byref); + +pub(crate) fn dec_subscriptions( + memory: &mut [u8], + input: wasi32::uintptr_t, + nsubscriptions: wasi32::size_t, +) -> Result> { + let raw_input_slice = + dec_raw_slice_of::(memory, input, nsubscriptions)?; + + raw_input_slice + .into_iter() + .map(|raw_subscription| { + let userdata = PrimInt::from_le(raw_subscription.userdata); + let tag = PrimInt::from_le(raw_subscription.u.tag); + let raw_u = raw_subscription.u.u; + let u = match tag { + wasi::__WASI_EVENTTYPE_CLOCK => wasi::__wasi_subscription_u_u_t { + clock: unsafe { + wasi::__wasi_subscription_clock_t { + identifier: PrimInt::from_le(raw_u.clock.identifier), + id: PrimInt::from_le(raw_u.clock.id), + timeout: PrimInt::from_le(raw_u.clock.timeout), + precision: PrimInt::from_le(raw_u.clock.precision), + flags: PrimInt::from_le(raw_u.clock.flags), + } + }, + }, + wasi::__WASI_EVENTTYPE_FD_READ => wasi::__wasi_subscription_u_u_t { + fd_read: wasi::__wasi_subscription_fd_readwrite_t { + file_descriptor: PrimInt::from_le(unsafe { raw_u.fd_read.file_descriptor }), + }, + }, + wasi::__WASI_EVENTTYPE_FD_WRITE => wasi::__wasi_subscription_u_u_t { + fd_write: wasi::__wasi_subscription_fd_readwrite_t { + file_descriptor: PrimInt::from_le(unsafe { + raw_u.fd_write.file_descriptor + }), + }, + }, + _ => return Err(Error::EINVAL), + }; + Ok(wasi::__wasi_subscription_t { + userdata, + u: wasi::__wasi_subscription_u_t { tag, u }, + }) + }) + .collect::>>() +} + +pub(crate) fn enc_events( + memory: &mut [u8], + output: wasi32::uintptr_t, + nsubscriptions: wasi32::size_t, + events: Vec, +) -> Result<()> { + let mut raw_output_iter = + dec_raw_slice_of_mut::(memory, output, nsubscriptions)?.into_iter(); + + for event in events.iter() { + *raw_output_iter + .next() + .expect("the number of events cannot exceed the number of subscriptions") = { + let userdata = PrimInt::to_le(event.userdata); + let error = PrimInt::to_le(event.error); + let r#type = PrimInt::to_le(event.r#type); + let flags = PrimInt::to_le(event.fd_readwrite.flags); + let nbytes = PrimInt::to_le(event.fd_readwrite.nbytes); + wasi::__wasi_event_t { + userdata, + error, + r#type, + fd_readwrite: wasi::__wasi_event_fd_readwrite_t { flags, nbytes }, + } + }; + } + + Ok(()) +} + +dec_enc_scalar!(__wasi_advice_t, dec_advice_byref, enc_advice_byref); +dec_enc_scalar!(__wasi_fstflags_t, dec_fstflags_byref, enc_fstflags_byref); +dec_enc_scalar!(__wasi_dircookie_t, dec_dircookie_byref, enc_dircookie_byref); diff --git a/crates/wasi-common/src/old/snapshot_0/mod.rs b/crates/wasi-common/src/old/snapshot_0/mod.rs new file mode 100644 index 0000000000..8226d731b0 --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/mod.rs @@ -0,0 +1,19 @@ +mod ctx; +mod error; +mod fdentry; +mod helpers; +mod host; +mod hostcalls_impl; +mod memory; +mod sys; +pub mod wasi; +pub mod wasi32; + +pub mod hostcalls { + wig::define_hostcalls!("old/snapshot_0" "wasi_unstable"); +} + +pub use ctx::{WasiCtx, WasiCtxBuilder}; + +pub type Error = error::Error; +pub type Result = std::result::Result; diff --git a/crates/wasi-common/src/old/snapshot_0/sys/mod.rs b/crates/wasi-common/src/old/snapshot_0/sys/mod.rs new file mode 100644 index 0000000000..6d8b3fd82f --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/mod.rs @@ -0,0 +1,13 @@ +use cfg_if::cfg_if; + +cfg_if! { + if #[cfg(unix)] { + mod unix; + pub(crate) use self::unix::*; + } else if #[cfg(windows)] { + mod windows; + pub(crate) use self::windows::*; + } else { + compile_error!("wasi-common doesn't compile for this platform yet"); + } +} diff --git a/crates/wasi-common/src/old/snapshot_0/sys/unix/bsd/filetime.rs b/crates/wasi-common/src/old/snapshot_0/sys/unix/bsd/filetime.rs new file mode 100644 index 0000000000..943aba9f7b --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/unix/bsd/filetime.rs @@ -0,0 +1,103 @@ +//! This internal module consists of helper types and functions for dealing +//! with setting the file times specific to BSD-style *nixes. +use crate::old::snapshot_0::{sys::unix::filetime::FileTime, Result}; +use cfg_if::cfg_if; +use std::ffi::CStr; +use std::fs::File; +use std::io; +use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; + +cfg_if! { + if #[cfg(any( + target_os = "macos", + target_os = "freebsd", + target_os = "ios", + target_os = "dragonfly" + ))] { + pub(crate) const UTIME_NOW: i64 = -1; + pub(crate) const UTIME_OMIT: i64 = -2; + } else if #[cfg(target_os = "openbsd")] { + // These are swapped compared to macos, freebsd, ios, and dragonfly. + // https://github.com/openbsd/src/blob/master/sys/sys/stat.h#L187 + pub(crate) const UTIME_NOW: i64 = -2; + pub(crate) const UTIME_OMIT: i64 = -1; + } else if #[cfg(target_os = "netbsd" )] { + // These are the same as for Linux. + // http://cvsweb.netbsd.org/bsdweb.cgi/src/sys/sys/stat.h?rev=1.69&content-type=text/x-cvsweb-markup&only_with_tag=MAIN + pub(crate) const UTIME_NOW: i64 = 1_073_741_823; + pub(crate) const UTIME_OMIT: i64 = 1_073_741_822; + } +} + +/// Wrapper for `utimensat` syscall, however, with an added twist such that `utimensat` symbol +/// is firstly resolved (i.e., we check whether it exists on the host), and only used if that is +/// the case. Otherwise, the syscall resorts to a less accurate `utimesat` emulated syscall. +/// The original implementation can be found here: [filetime::unix::macos::set_times] +/// +/// [filetime::unix::macos::set_times]: https://github.com/alexcrichton/filetime/blob/master/src/unix/macos.rs#L49 +pub(crate) fn utimensat( + dirfd: &File, + path: &str, + atime: FileTime, + mtime: FileTime, + symlink_nofollow: bool, +) -> Result<()> { + use crate::old::snapshot_0::sys::unix::filetime::to_timespec; + use std::ffi::CString; + use std::os::unix::prelude::*; + + // Attempt to use the `utimensat` syscall, but if it's not supported by the + // current kernel then fall back to an older syscall. + if let Some(func) = fetch_utimensat() { + let flags = if symlink_nofollow { + libc::AT_SYMLINK_NOFOLLOW + } else { + 0 + }; + + let p = CString::new(path.as_bytes())?; + let times = [to_timespec(&atime)?, to_timespec(&mtime)?]; + let rc = unsafe { func(dirfd.as_raw_fd(), p.as_ptr(), times.as_ptr(), flags) }; + if rc == 0 { + return Ok(()); + } else { + return Err(io::Error::last_os_error().into()); + } + } + + super::utimesat::utimesat(dirfd, path, atime, mtime, symlink_nofollow) +} + +/// Wraps `fetch` specifically targetting `utimensat` symbol. If the symbol exists +/// on the host, then returns an `Some(unsafe fn)`. +fn fetch_utimensat() -> Option< + unsafe extern "C" fn( + libc::c_int, + *const libc::c_char, + *const libc::timespec, + libc::c_int, + ) -> libc::c_int, +> { + static ADDR: AtomicUsize = AtomicUsize::new(0); + unsafe { + fetch(&ADDR, CStr::from_bytes_with_nul_unchecked(b"utimensat\0")) + .map(|sym| std::mem::transmute(sym)) + } +} + +/// Fetches a symbol by `name` and stores it in `cache`. +fn fetch(cache: &AtomicUsize, name: &CStr) -> Option { + match cache.load(SeqCst) { + 0 => {} + 1 => return None, + n => return Some(n), + } + let sym = unsafe { libc::dlsym(libc::RTLD_DEFAULT, name.as_ptr() as *const _) }; + let (val, ret) = if sym.is_null() { + (1, None) + } else { + (sym as usize, Some(sym as usize)) + }; + cache.store(val, SeqCst); + return ret; +} diff --git a/crates/wasi-common/src/old/snapshot_0/sys/unix/bsd/host_impl.rs b/crates/wasi-common/src/old/snapshot_0/sys/unix/bsd/host_impl.rs new file mode 100644 index 0000000000..b151e703e1 --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/unix/bsd/host_impl.rs @@ -0,0 +1,16 @@ +use crate::old::snapshot_0::{wasi, Result}; +use std::convert::TryFrom; + +pub(crate) const O_RSYNC: yanix::file::OFlag = yanix::file::OFlag::SYNC; + +pub(crate) fn stdev_from_nix(dev: libc::dev_t) -> Result { + wasi::__wasi_device_t::try_from(dev).map_err(Into::into) +} + +pub(crate) fn stino_from_nix(ino: libc::ino_t) -> Result { + wasi::__wasi_device_t::try_from(ino).map_err(Into::into) +} + +pub(crate) fn stnlink_from_nix(nlink: libc::nlink_t) -> Result { + Ok(nlink.into()) +} diff --git a/crates/wasi-common/src/old/snapshot_0/sys/unix/bsd/hostcalls_impl.rs b/crates/wasi-common/src/old/snapshot_0/sys/unix/bsd/hostcalls_impl.rs new file mode 100644 index 0000000000..48736ac7df --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/unix/bsd/hostcalls_impl.rs @@ -0,0 +1,169 @@ +use crate::old::snapshot_0::hostcalls_impl::PathGet; +use crate::old::snapshot_0::{Error, Result}; +use std::os::unix::prelude::AsRawFd; + +pub(crate) fn path_unlink_file(resolved: PathGet) -> Result<()> { + use yanix::{ + file::{unlinkat, AtFlag}, + Errno, YanixError, + }; + unsafe { + unlinkat( + resolved.dirfd().as_raw_fd(), + resolved.path(), + AtFlag::empty(), + ) + } + .map_err(|err| { + if let YanixError::Errno(mut errno) = err { + // Non-Linux implementations may return EPERM when attempting to remove a + // directory without REMOVEDIR. While that's what POSIX specifies, it's + // less useful. Adjust this to EISDIR. It doesn't matter that this is not + // atomic with the unlinkat, because if the file is removed and a directory + // is created before fstatat sees it, we're racing with that change anyway + // and unlinkat could have legitimately seen the directory if the race had + // turned out differently. + use yanix::file::{fstatat, FileType}; + + if errno == Errno::EPERM { + if let Ok(stat) = unsafe { + fstatat( + resolved.dirfd().as_raw_fd(), + resolved.path(), + AtFlag::SYMLINK_NOFOLLOW, + ) + } { + if FileType::from_stat_st_mode(stat.st_mode) == FileType::Directory { + errno = Errno::EISDIR; + } + } else { + errno = Errno::last(); + } + } + errno.into() + } else { + err + } + }) + .map_err(Into::into) +} + +pub(crate) fn path_symlink(old_path: &str, resolved: PathGet) -> Result<()> { + use yanix::{ + file::{fstatat, symlinkat, AtFlag}, + Errno, YanixError, + }; + + log::debug!("path_symlink old_path = {:?}", old_path); + log::debug!("path_symlink resolved = {:?}", resolved); + + unsafe { symlinkat(old_path, resolved.dirfd().as_raw_fd(), resolved.path()) }.or_else(|err| { + if let YanixError::Errno(errno) = err { + match errno { + Errno::ENOTDIR => { + // On BSD, symlinkat returns ENOTDIR when it should in fact + // return a EEXIST. It seems that it gets confused with by + // the trailing slash in the target path. Thus, we strip + // the trailing slash and check if the path exists, and + // adjust the error code appropriately. + let new_path = resolved.path().trim_end_matches('/'); + if let Ok(_) = unsafe { + fstatat( + resolved.dirfd().as_raw_fd(), + new_path, + AtFlag::SYMLINK_NOFOLLOW, + ) + } { + Err(Error::EEXIST) + } else { + Err(Error::ENOTDIR) + } + } + x => Err(x.into()), + } + } else { + Err(err.into()) + } + }) +} + +pub(crate) fn path_rename(resolved_old: PathGet, resolved_new: PathGet) -> Result<()> { + use yanix::{ + file::{fstatat, renameat, AtFlag}, + Errno, YanixError, + }; + unsafe { + renameat( + resolved_old.dirfd().as_raw_fd(), + resolved_old.path(), + resolved_new.dirfd().as_raw_fd(), + resolved_new.path(), + ) + } + .or_else(|err| { + // Currently, this is verified to be correct on macOS, where + // ENOENT can be returned in case when we try to rename a file + // into a name with a trailing slash. On macOS, if the latter does + // not exist, an ENOENT is thrown, whereas on Linux we observe the + // correct behaviour of throwing an ENOTDIR since the destination is + // indeed not a directory. + // + // TODO + // Verify on other BSD-based OSes. + if let YanixError::Errno(errno) = err { + match errno { + Errno::ENOENT => { + // check if the source path exists + if let Ok(_) = unsafe { + fstatat( + resolved_old.dirfd().as_raw_fd(), + resolved_old.path(), + AtFlag::SYMLINK_NOFOLLOW, + ) + } { + // check if destination contains a trailing slash + if resolved_new.path().contains('/') { + Err(Error::ENOTDIR) + } else { + Err(Error::ENOENT) + } + } else { + Err(Error::ENOENT) + } + } + x => Err(x.into()), + } + } else { + Err(err.into()) + } + }) +} + +pub(crate) mod fd_readdir_impl { + use crate::old::snapshot_0::sys::fdentry_impl::OsHandle; + use crate::old::snapshot_0::Result; + use std::sync::{Mutex, MutexGuard}; + use yanix::dir::Dir; + + pub(crate) fn get_dir_from_os_handle<'a>( + os_handle: &'a mut OsHandle, + ) -> Result> { + let dir = match os_handle.dir { + Some(ref mut dir) => dir, + None => { + // We need to duplicate the fd, because `opendir(3)`: + // Upon successful return from fdopendir(), the file descriptor is under + // control of the system, and if any attempt is made to close the file + // descriptor, or to modify the state of the associated description other + // than by means of closedir(), readdir(), readdir_r(), or rewinddir(), + // the behaviour is undefined. + let fd = (*os_handle).try_clone()?; + let dir = Dir::from(fd)?; + os_handle.dir.get_or_insert(Mutex::new(dir)) + } + }; + // Note that from this point on, until the end of the parent scope (i.e., enclosing this + // function), we're locking the `Dir` member of this `OsHandle`. + Ok(dir.lock().unwrap()) + } +} diff --git a/crates/wasi-common/src/old/snapshot_0/sys/unix/bsd/mod.rs b/crates/wasi-common/src/old/snapshot_0/sys/unix/bsd/mod.rs new file mode 100644 index 0000000000..39a4046a74 --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/unix/bsd/mod.rs @@ -0,0 +1,6 @@ +pub(crate) mod filetime; +pub(crate) mod host_impl; +pub(crate) mod hostcalls_impl; +pub(crate) mod oshandle; +#[path = "../linux/utimesat.rs"] +pub(crate) mod utimesat; diff --git a/crates/wasi-common/src/old/snapshot_0/sys/unix/bsd/oshandle.rs b/crates/wasi-common/src/old/snapshot_0/sys/unix/bsd/oshandle.rs new file mode 100644 index 0000000000..70d1a8c4e8 --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/unix/bsd/oshandle.rs @@ -0,0 +1,48 @@ +use std::fs; +use std::ops::{Deref, DerefMut}; +use std::os::unix::prelude::{AsRawFd, RawFd}; +use std::sync::Mutex; +use yanix::dir::Dir; + +#[derive(Debug)] +pub(crate) struct OsHandle { + pub(crate) file: fs::File, + // In case that this `OsHandle` actually refers to a directory, + // when the client makes a `fd_readdir` syscall on this descriptor, + // we will need to cache the `libc::DIR` pointer manually in order + // to be able to seek on it later. While on Linux, this is handled + // by the OS, BSD Unixes require the client to do this caching. + // + // This comes directly from the BSD man pages on `readdir`: + // > Values returned by telldir() are good only for the lifetime + // > of the DIR pointer, dirp, from which they are derived. + // > If the directory is closed and then reopened, prior values + // > returned by telldir() will no longer be valid. + pub(crate) dir: Option>, +} + +impl From for OsHandle { + fn from(file: fs::File) -> Self { + Self { file, dir: None } + } +} + +impl AsRawFd for OsHandle { + fn as_raw_fd(&self) -> RawFd { + self.file.as_raw_fd() + } +} + +impl Deref for OsHandle { + type Target = fs::File; + + fn deref(&self) -> &Self::Target { + &self.file + } +} + +impl DerefMut for OsHandle { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.file + } +} diff --git a/crates/wasi-common/src/old/snapshot_0/sys/unix/emscripten/filetime.rs b/crates/wasi-common/src/old/snapshot_0/sys/unix/emscripten/filetime.rs new file mode 100644 index 0000000000..adc48a3493 --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/unix/emscripten/filetime.rs @@ -0,0 +1,36 @@ +//! This internal module consists of helper types and functions for dealing +//! with setting the file times specific to Emscripten. +use crate::old::snapshot_0::{sys::unix::filetime::FileTime, Result}; +use std::fs::File; +use std::io; + +pub(crate) const UTIME_NOW: i32 = 1_073_741_823; +pub(crate) const UTIME_OMIT: i32 = 1_073_741_822; + +/// Wrapper for `utimensat` syscall. In Emscripten, there is no point in dynamically resolving +/// if `utimensat` is available as it always was and will be. +pub(crate) fn utimensat( + dirfd: &File, + path: &str, + atime: FileTime, + mtime: FileTime, + symlink_nofollow: bool, +) -> Result<()> { + use crate::old::snapshot_0::sys::unix::filetime::to_timespec; + use std::ffi::CString; + use std::os::unix::prelude::*; + + let flags = if symlink_nofollow { + libc::AT_SYMLINK_NOFOLLOW + } else { + 0 + }; + let p = CString::new(path.as_bytes())?; + let times = [to_timespec(&atime)?, to_timespec(&mtime)?]; + let rc = unsafe { libc::utimensat(dirfd.as_raw_fd(), p.as_ptr(), times.as_ptr(), flags) }; + if rc == 0 { + Ok(()) + } else { + Err(io::Error::last_os_error().into()) + } +} diff --git a/crates/wasi-common/src/old/snapshot_0/sys/unix/emscripten/host_impl.rs b/crates/wasi-common/src/old/snapshot_0/sys/unix/emscripten/host_impl.rs new file mode 100644 index 0000000000..412280a519 --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/unix/emscripten/host_impl.rs @@ -0,0 +1,15 @@ +use crate::old::snapshot_0::{wasi, Result}; + +pub(crate) const O_RSYNC: yanix::file::OFlag = yanix::file::OFlag::RSYNC; + +pub(crate) fn stdev_from_nix(dev: libc::dev_t) -> Result { + Ok(wasi::__wasi_device_t::from(dev)) +} + +pub(crate) fn stino_from_nix(ino: libc::ino_t) -> Result { + Ok(wasi::__wasi_device_t::from(ino)) +} + +pub(crate) fn stnlink_from_nix(nlink: libc::nlink_t) -> Result { + Ok(nlink) +} diff --git a/crates/wasi-common/src/old/snapshot_0/sys/unix/emscripten/mod.rs b/crates/wasi-common/src/old/snapshot_0/sys/unix/emscripten/mod.rs new file mode 100644 index 0000000000..c9c3841b9a --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/unix/emscripten/mod.rs @@ -0,0 +1,6 @@ +pub(crate) mod filetime; +pub(crate) mod host_impl; +#[path = "../linux/hostcalls_impl.rs"] +pub(crate) mod hostcalls_impl; +#[path = "../linux/oshandle.rs"] +pub(crate) mod oshandle; diff --git a/crates/wasi-common/src/old/snapshot_0/sys/unix/fdentry_impl.rs b/crates/wasi-common/src/old/snapshot_0/sys/unix/fdentry_impl.rs new file mode 100644 index 0000000000..a46177c3c5 --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/unix/fdentry_impl.rs @@ -0,0 +1,130 @@ +use crate::old::snapshot_0::fdentry::{Descriptor, OsHandleRef}; +use crate::old::snapshot_0::{sys::unix::sys_impl, wasi, Error, Result}; +use std::fs::File; +use std::io; +use std::mem::ManuallyDrop; +use std::os::unix::prelude::{AsRawFd, FileTypeExt, FromRawFd, RawFd}; + +pub(crate) use sys_impl::oshandle::*; + +impl AsRawFd for Descriptor { + fn as_raw_fd(&self) -> RawFd { + match self { + Self::OsHandle(file) => file.as_raw_fd(), + Self::Stdin => io::stdin().as_raw_fd(), + Self::Stdout => io::stdout().as_raw_fd(), + Self::Stderr => io::stderr().as_raw_fd(), + } + } +} + +pub(crate) fn descriptor_as_oshandle<'lifetime>( + desc: &'lifetime Descriptor, +) -> OsHandleRef<'lifetime> { + OsHandleRef::new(ManuallyDrop::new(OsHandle::from(unsafe { + File::from_raw_fd(desc.as_raw_fd()) + }))) +} + +/// This function is unsafe because it operates on a raw file descriptor. +pub(crate) unsafe fn determine_type_and_access_rights( + fd: &Fd, +) -> Result<( + wasi::__wasi_filetype_t, + wasi::__wasi_rights_t, + wasi::__wasi_rights_t, +)> { + let (file_type, mut rights_base, rights_inheriting) = determine_type_rights(fd)?; + + use yanix::{fcntl, file::OFlag}; + let flags = fcntl::get_status_flags(fd.as_raw_fd())?; + let accmode = flags & OFlag::ACCMODE; + if accmode == OFlag::RDONLY { + rights_base &= !wasi::__WASI_RIGHTS_FD_WRITE; + } else if accmode == OFlag::WRONLY { + rights_base &= !wasi::__WASI_RIGHTS_FD_READ; + } + + Ok((file_type, rights_base, rights_inheriting)) +} + +/// This function is unsafe because it operates on a raw file descriptor. +pub(crate) unsafe fn determine_type_rights( + fd: &Fd, +) -> Result<( + wasi::__wasi_filetype_t, + wasi::__wasi_rights_t, + wasi::__wasi_rights_t, +)> { + let (file_type, rights_base, rights_inheriting) = { + // we just make a `File` here for convenience; we don't want it to close when it drops + let file = std::mem::ManuallyDrop::new(std::fs::File::from_raw_fd(fd.as_raw_fd())); + let ft = file.metadata()?.file_type(); + if ft.is_block_device() { + log::debug!("Host fd {:?} is a block device", fd.as_raw_fd()); + ( + wasi::__WASI_FILETYPE_BLOCK_DEVICE, + wasi::RIGHTS_BLOCK_DEVICE_BASE, + wasi::RIGHTS_BLOCK_DEVICE_INHERITING, + ) + } else if ft.is_char_device() { + log::debug!("Host fd {:?} is a char device", fd.as_raw_fd()); + use yanix::file::isatty; + if isatty(fd.as_raw_fd())? { + ( + wasi::__WASI_FILETYPE_CHARACTER_DEVICE, + wasi::RIGHTS_TTY_BASE, + wasi::RIGHTS_TTY_BASE, + ) + } else { + ( + wasi::__WASI_FILETYPE_CHARACTER_DEVICE, + wasi::RIGHTS_CHARACTER_DEVICE_BASE, + wasi::RIGHTS_CHARACTER_DEVICE_INHERITING, + ) + } + } else if ft.is_dir() { + log::debug!("Host fd {:?} is a directory", fd.as_raw_fd()); + ( + wasi::__WASI_FILETYPE_DIRECTORY, + wasi::RIGHTS_DIRECTORY_BASE, + wasi::RIGHTS_DIRECTORY_INHERITING, + ) + } else if ft.is_file() { + log::debug!("Host fd {:?} is a file", fd.as_raw_fd()); + ( + wasi::__WASI_FILETYPE_REGULAR_FILE, + wasi::RIGHTS_REGULAR_FILE_BASE, + wasi::RIGHTS_REGULAR_FILE_INHERITING, + ) + } else if ft.is_socket() { + log::debug!("Host fd {:?} is a socket", fd.as_raw_fd()); + use yanix::socket::{get_socket_type, SockType}; + match get_socket_type(fd.as_raw_fd())? { + SockType::Datagram => ( + wasi::__WASI_FILETYPE_SOCKET_DGRAM, + wasi::RIGHTS_SOCKET_BASE, + wasi::RIGHTS_SOCKET_INHERITING, + ), + SockType::Stream => ( + wasi::__WASI_FILETYPE_SOCKET_STREAM, + wasi::RIGHTS_SOCKET_BASE, + wasi::RIGHTS_SOCKET_INHERITING, + ), + _ => return Err(Error::EINVAL), + } + } else if ft.is_fifo() { + log::debug!("Host fd {:?} is a fifo", fd.as_raw_fd()); + ( + wasi::__WASI_FILETYPE_UNKNOWN, + wasi::RIGHTS_REGULAR_FILE_BASE, + wasi::RIGHTS_REGULAR_FILE_INHERITING, + ) + } else { + log::debug!("Host fd {:?} is unknown", fd.as_raw_fd()); + return Err(Error::EINVAL); + } + }; + + Ok((file_type, rights_base, rights_inheriting)) +} diff --git a/crates/wasi-common/src/old/snapshot_0/sys/unix/filetime.rs b/crates/wasi-common/src/old/snapshot_0/sys/unix/filetime.rs new file mode 100644 index 0000000000..15530e1f0c --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/unix/filetime.rs @@ -0,0 +1,67 @@ +//! This internal module consists of helper types and functions for dealing +//! with setting the file times (mainly in `path_filestat_set_times` syscall for now). +//! +//! The vast majority of the code contained within and in platform-specific implementations +//! (`super::linux::filetime` and `super::bsd::filetime`) is based on the [filetime] crate. +//! Kudos @alexcrichton! +//! +//! [filetime]: https://github.com/alexcrichton/filetime +use crate::old::snapshot_0::Result; +use std::convert::TryInto; + +pub(crate) use super::sys_impl::filetime::*; + +cfg_if::cfg_if! { + if #[cfg(not(target_os = "emscripten"))] { + fn filetime_to_timespec(ft: &filetime::FileTime) -> Result { + Ok( + libc::timespec { + tv_sec: ft.seconds(), + tv_nsec: ft.nanoseconds().try_into()?, + } + ) + } + } else { + fn filetime_to_timespec(ft: &filetime::FileTime) -> Result { + Ok( + libc::timespec { + tv_sec: ft.seconds().try_into()?, + tv_nsec: ft.nanoseconds().try_into()?, + } + ) + } + } +} + +/// A wrapper `enum` around `filetime::FileTime` struct, but unlike the original, this +/// type allows the possibility of specifying `FileTime::Now` as a valid enumeration which, +/// in turn, if `utimensat` is available on the host, will use a special const setting +/// `UTIME_NOW`. +#[derive(Debug, Copy, Clone)] +pub(crate) enum FileTime { + Now, + Omit, + FileTime(filetime::FileTime), +} + +/// Converts `FileTime` to `libc::timespec`. If `FileTime::Now` variant is specified, this +/// resolves to `UTIME_NOW` special const, `FileTime::Omit` variant resolves to `UTIME_OMIT`, and +/// `FileTime::FileTime(ft)` where `ft := filetime::FileTime` uses [filetime] crate's original +/// implementation which can be found here: [filetime::unix::to_timespec]. +/// +/// [filetime]: https://github.com/alexcrichton/filetime +/// [filetime::unix::to_timespec]: https://github.com/alexcrichton/filetime/blob/master/src/unix/mod.rs#L30 +pub(crate) fn to_timespec(ft: &FileTime) -> Result { + let ts = match ft { + FileTime::Now => libc::timespec { + tv_sec: 0, + tv_nsec: UTIME_NOW, + }, + FileTime::Omit => libc::timespec { + tv_sec: 0, + tv_nsec: UTIME_OMIT, + }, + FileTime::FileTime(ft) => filetime_to_timespec(ft)?, + }; + Ok(ts) +} diff --git a/crates/wasi-common/src/old/snapshot_0/sys/unix/host_impl.rs b/crates/wasi-common/src/old/snapshot_0/sys/unix/host_impl.rs new file mode 100644 index 0000000000..63f92d5ce3 --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/unix/host_impl.rs @@ -0,0 +1,222 @@ +//! WASI host types specific to *nix host. +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(dead_code)] +use crate::old::snapshot_0::host::FileType; +use crate::old::snapshot_0::{ + error::FromRawOsError, helpers, sys::unix::sys_impl, wasi, Error, Result, +}; +use std::ffi::OsStr; +use std::os::unix::prelude::OsStrExt; +use yanix::{file::OFlag, Errno}; + +pub(crate) use sys_impl::host_impl::*; + +impl FromRawOsError for Error { + fn from_raw_os_error(code: i32) -> Self { + Self::from(Errno::from_i32(code)) + } +} + +impl From for Error { + fn from(errno: Errno) -> Self { + match errno { + Errno::EPERM => Self::EPERM, + Errno::ENOENT => Self::ENOENT, + Errno::ESRCH => Self::ESRCH, + Errno::EINTR => Self::EINTR, + Errno::EIO => Self::EIO, + Errno::ENXIO => Self::ENXIO, + Errno::E2BIG => Self::E2BIG, + Errno::ENOEXEC => Self::ENOEXEC, + Errno::EBADF => Self::EBADF, + Errno::ECHILD => Self::ECHILD, + Errno::EAGAIN => Self::EAGAIN, + Errno::ENOMEM => Self::ENOMEM, + Errno::EACCES => Self::EACCES, + Errno::EFAULT => Self::EFAULT, + Errno::EBUSY => Self::EBUSY, + Errno::EEXIST => Self::EEXIST, + Errno::EXDEV => Self::EXDEV, + Errno::ENODEV => Self::ENODEV, + Errno::ENOTDIR => Self::ENOTDIR, + Errno::EISDIR => Self::EISDIR, + Errno::EINVAL => Self::EINVAL, + Errno::ENFILE => Self::ENFILE, + Errno::EMFILE => Self::EMFILE, + Errno::ENOTTY => Self::ENOTTY, + Errno::ETXTBSY => Self::ETXTBSY, + Errno::EFBIG => Self::EFBIG, + Errno::ENOSPC => Self::ENOSPC, + Errno::ESPIPE => Self::ESPIPE, + Errno::EROFS => Self::EROFS, + Errno::EMLINK => Self::EMLINK, + Errno::EPIPE => Self::EPIPE, + Errno::EDOM => Self::EDOM, + Errno::ERANGE => Self::ERANGE, + Errno::EDEADLK => Self::EDEADLK, + Errno::ENAMETOOLONG => Self::ENAMETOOLONG, + Errno::ENOLCK => Self::ENOLCK, + Errno::ENOSYS => Self::ENOSYS, + Errno::ENOTEMPTY => Self::ENOTEMPTY, + Errno::ELOOP => Self::ELOOP, + Errno::ENOMSG => Self::ENOMSG, + Errno::EIDRM => Self::EIDRM, + Errno::ENOLINK => Self::ENOLINK, + Errno::EPROTO => Self::EPROTO, + Errno::EMULTIHOP => Self::EMULTIHOP, + Errno::EBADMSG => Self::EBADMSG, + Errno::EOVERFLOW => Self::EOVERFLOW, + Errno::EILSEQ => Self::EILSEQ, + Errno::ENOTSOCK => Self::ENOTSOCK, + Errno::EDESTADDRREQ => Self::EDESTADDRREQ, + Errno::EMSGSIZE => Self::EMSGSIZE, + Errno::EPROTOTYPE => Self::EPROTOTYPE, + Errno::ENOPROTOOPT => Self::ENOPROTOOPT, + Errno::EPROTONOSUPPORT => Self::EPROTONOSUPPORT, + Errno::EAFNOSUPPORT => Self::EAFNOSUPPORT, + Errno::EADDRINUSE => Self::EADDRINUSE, + Errno::EADDRNOTAVAIL => Self::EADDRNOTAVAIL, + Errno::ENETDOWN => Self::ENETDOWN, + Errno::ENETUNREACH => Self::ENETUNREACH, + Errno::ENETRESET => Self::ENETRESET, + Errno::ECONNABORTED => Self::ECONNABORTED, + Errno::ECONNRESET => Self::ECONNRESET, + Errno::ENOBUFS => Self::ENOBUFS, + Errno::EISCONN => Self::EISCONN, + Errno::ENOTCONN => Self::ENOTCONN, + Errno::ETIMEDOUT => Self::ETIMEDOUT, + Errno::ECONNREFUSED => Self::ECONNREFUSED, + Errno::EHOSTUNREACH => Self::EHOSTUNREACH, + Errno::EALREADY => Self::EALREADY, + Errno::EINPROGRESS => Self::EINPROGRESS, + Errno::ESTALE => Self::ESTALE, + Errno::EDQUOT => Self::EDQUOT, + Errno::ECANCELED => Self::ECANCELED, + Errno::EOWNERDEAD => Self::EOWNERDEAD, + Errno::ENOTRECOVERABLE => Self::ENOTRECOVERABLE, + } + } +} + +pub(crate) fn nix_from_fdflags(fdflags: wasi::__wasi_fdflags_t) -> OFlag { + let mut nix_flags = OFlag::empty(); + if fdflags & wasi::__WASI_FDFLAGS_APPEND != 0 { + nix_flags.insert(OFlag::APPEND); + } + if fdflags & wasi::__WASI_FDFLAGS_DSYNC != 0 { + nix_flags.insert(OFlag::DSYNC); + } + if fdflags & wasi::__WASI_FDFLAGS_NONBLOCK != 0 { + nix_flags.insert(OFlag::NONBLOCK); + } + if fdflags & wasi::__WASI_FDFLAGS_RSYNC != 0 { + nix_flags.insert(O_RSYNC); + } + if fdflags & wasi::__WASI_FDFLAGS_SYNC != 0 { + nix_flags.insert(OFlag::SYNC); + } + nix_flags +} + +pub(crate) fn fdflags_from_nix(oflags: OFlag) -> wasi::__wasi_fdflags_t { + let mut fdflags = 0; + if oflags.contains(OFlag::APPEND) { + fdflags |= wasi::__WASI_FDFLAGS_APPEND; + } + if oflags.contains(OFlag::DSYNC) { + fdflags |= wasi::__WASI_FDFLAGS_DSYNC; + } + if oflags.contains(OFlag::NONBLOCK) { + fdflags |= wasi::__WASI_FDFLAGS_NONBLOCK; + } + if oflags.contains(O_RSYNC) { + fdflags |= wasi::__WASI_FDFLAGS_RSYNC; + } + if oflags.contains(OFlag::SYNC) { + fdflags |= wasi::__WASI_FDFLAGS_SYNC; + } + fdflags +} + +pub(crate) fn nix_from_oflags(oflags: wasi::__wasi_oflags_t) -> OFlag { + let mut nix_flags = OFlag::empty(); + if oflags & wasi::__WASI_OFLAGS_CREAT != 0 { + nix_flags.insert(OFlag::CREAT); + } + if oflags & wasi::__WASI_OFLAGS_DIRECTORY != 0 { + nix_flags.insert(OFlag::DIRECTORY); + } + if oflags & wasi::__WASI_OFLAGS_EXCL != 0 { + nix_flags.insert(OFlag::EXCL); + } + if oflags & wasi::__WASI_OFLAGS_TRUNC != 0 { + nix_flags.insert(OFlag::TRUNC); + } + nix_flags +} + +pub(crate) fn filestat_from_nix(filestat: libc::stat) -> Result { + use std::convert::TryInto; + + fn filestat_to_timestamp(secs: u64, nsecs: u64) -> Result { + secs.checked_mul(1_000_000_000) + .and_then(|sec_nsec| sec_nsec.checked_add(nsecs)) + .ok_or(Error::EOVERFLOW) + } + + let filetype = yanix::file::FileType::from_stat_st_mode(filestat.st_mode); + let dev = stdev_from_nix(filestat.st_dev)?; + let ino = stino_from_nix(filestat.st_ino)?; + let atim = filestat_to_timestamp( + filestat.st_atime.try_into()?, + filestat.st_atime_nsec.try_into()?, + )?; + let ctim = filestat_to_timestamp( + filestat.st_ctime.try_into()?, + filestat.st_ctime_nsec.try_into()?, + )?; + let mtim = filestat_to_timestamp( + filestat.st_mtime.try_into()?, + filestat.st_mtime_nsec.try_into()?, + )?; + + Ok(wasi::__wasi_filestat_t { + dev, + ino, + nlink: stnlink_from_nix(filestat.st_nlink)?, + size: filestat.st_size as wasi::__wasi_filesize_t, + atim, + ctim, + mtim, + filetype: FileType::from(filetype).to_wasi(), + }) +} + +/// Creates owned WASI path from OS string. +/// +/// NB WASI spec requires OS string to be valid UTF-8. Otherwise, +/// `__WASI_ERRNO_ILSEQ` error is returned. +pub(crate) fn path_from_host>(s: S) -> Result { + helpers::path_from_slice(s.as_ref().as_bytes()).map(String::from) +} + +impl From for FileType { + fn from(ft: yanix::file::FileType) -> Self { + use yanix::file::FileType::*; + match ft { + RegularFile => Self::RegularFile, + Symlink => Self::Symlink, + Directory => Self::Directory, + BlockDevice => Self::BlockDevice, + CharacterDevice => Self::CharacterDevice, + /* Unknown | Socket | Fifo */ + _ => Self::Unknown, + // TODO how to discriminate between STREAM and DGRAM? + // Perhaps, we should create a more general WASI filetype + // such as __WASI_FILETYPE_SOCKET, and then it would be + // up to the client to check whether it's actually + // STREAM or DGRAM? + } + } +} diff --git a/crates/wasi-common/src/old/snapshot_0/sys/unix/hostcalls_impl/fs.rs b/crates/wasi-common/src/old/snapshot_0/sys/unix/hostcalls_impl/fs.rs new file mode 100644 index 0000000000..c8a3d59734 --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/unix/hostcalls_impl/fs.rs @@ -0,0 +1,314 @@ +#![allow(non_camel_case_types)] +#![allow(unused_unsafe)] +use crate::old::snapshot_0::host::Dirent; +use crate::old::snapshot_0::hostcalls_impl::PathGet; +use crate::old::snapshot_0::sys::{fdentry_impl::OsHandle, host_impl, unix::sys_impl}; +use crate::old::snapshot_0::{wasi, Error, Result}; +use std::convert::TryInto; +use std::fs::File; +use std::os::unix::fs::FileExt; +use std::os::unix::prelude::{AsRawFd, FromRawFd}; + +pub(crate) use sys_impl::hostcalls_impl::*; + +pub(crate) fn fd_pread( + file: &File, + buf: &mut [u8], + offset: wasi::__wasi_filesize_t, +) -> Result { + file.read_at(buf, offset).map_err(Into::into) +} + +pub(crate) fn fd_pwrite(file: &File, buf: &[u8], offset: wasi::__wasi_filesize_t) -> Result { + file.write_at(buf, offset).map_err(Into::into) +} + +pub(crate) fn fd_fdstat_get(fd: &File) -> Result { + unsafe { yanix::fcntl::get_status_flags(fd.as_raw_fd()) } + .map(host_impl::fdflags_from_nix) + .map_err(Into::into) +} + +pub(crate) fn fd_fdstat_set_flags(fd: &File, fdflags: wasi::__wasi_fdflags_t) -> Result<()> { + let nix_flags = host_impl::nix_from_fdflags(fdflags); + unsafe { yanix::fcntl::set_status_flags(fd.as_raw_fd(), nix_flags) }.map_err(Into::into) +} + +pub(crate) fn fd_advise( + file: &File, + advice: wasi::__wasi_advice_t, + offset: wasi::__wasi_filesize_t, + len: wasi::__wasi_filesize_t, +) -> Result<()> { + use yanix::fadvise::{posix_fadvise, PosixFadviseAdvice}; + let offset = offset.try_into()?; + let len = len.try_into()?; + let host_advice = match advice { + wasi::__WASI_ADVICE_DONTNEED => PosixFadviseAdvice::DontNeed, + wasi::__WASI_ADVICE_SEQUENTIAL => PosixFadviseAdvice::Sequential, + wasi::__WASI_ADVICE_WILLNEED => PosixFadviseAdvice::WillNeed, + wasi::__WASI_ADVICE_NOREUSE => PosixFadviseAdvice::NoReuse, + wasi::__WASI_ADVICE_RANDOM => PosixFadviseAdvice::Random, + wasi::__WASI_ADVICE_NORMAL => PosixFadviseAdvice::Normal, + _ => return Err(Error::EINVAL), + }; + unsafe { posix_fadvise(file.as_raw_fd(), offset, len, host_advice) }.map_err(Into::into) +} + +pub(crate) fn path_create_directory(resolved: PathGet) -> Result<()> { + use yanix::file::{mkdirat, Mode}; + unsafe { + mkdirat( + resolved.dirfd().as_raw_fd(), + resolved.path(), + Mode::from_bits_truncate(0o777), + ) + } + .map_err(Into::into) +} + +pub(crate) fn path_link(resolved_old: PathGet, resolved_new: PathGet) -> Result<()> { + use yanix::file::{linkat, AtFlag}; + unsafe { + linkat( + resolved_old.dirfd().as_raw_fd(), + resolved_old.path(), + resolved_new.dirfd().as_raw_fd(), + resolved_new.path(), + AtFlag::SYMLINK_FOLLOW, + ) + } + .map_err(Into::into) +} + +pub(crate) fn path_open( + resolved: PathGet, + read: bool, + write: bool, + oflags: wasi::__wasi_oflags_t, + fs_flags: wasi::__wasi_fdflags_t, +) -> Result { + use yanix::{ + file::{fstatat, openat, AtFlag, FileType, Mode, OFlag}, + Errno, + }; + + let mut nix_all_oflags = if read && write { + OFlag::RDWR + } else if write { + OFlag::WRONLY + } else { + OFlag::RDONLY + }; + + // on non-Capsicum systems, we always want nofollow + nix_all_oflags.insert(OFlag::NOFOLLOW); + + // convert open flags + nix_all_oflags.insert(host_impl::nix_from_oflags(oflags)); + + // convert file descriptor flags + nix_all_oflags.insert(host_impl::nix_from_fdflags(fs_flags)); + + // Call openat. Use mode 0o666 so that we follow whatever the user's + // umask is, but don't set the executable flag, because it isn't yet + // meaningful for WASI programs to create executable files. + + log::debug!("path_open resolved = {:?}", resolved); + log::debug!("path_open oflags = {:?}", nix_all_oflags); + + let new_fd = match unsafe { + openat( + resolved.dirfd().as_raw_fd(), + resolved.path(), + nix_all_oflags, + Mode::from_bits_truncate(0o666), + ) + } { + Ok(fd) => fd, + Err(e) => { + if let yanix::YanixError::Errno(errno) = e { + match errno { + // Linux returns ENXIO instead of EOPNOTSUPP when opening a socket + Errno::ENXIO => { + if let Ok(stat) = unsafe { + fstatat( + resolved.dirfd().as_raw_fd(), + resolved.path(), + AtFlag::SYMLINK_NOFOLLOW, + ) + } { + if FileType::from_stat_st_mode(stat.st_mode) == FileType::Socket { + return Err(Error::ENOTSUP); + } else { + return Err(Error::ENXIO); + } + } else { + return Err(Error::ENXIO); + } + } + // Linux returns ENOTDIR instead of ELOOP when using O_NOFOLLOW|O_DIRECTORY + // on a symlink. + Errno::ENOTDIR + if !(nix_all_oflags & (OFlag::NOFOLLOW | OFlag::DIRECTORY)).is_empty() => + { + if let Ok(stat) = unsafe { + fstatat( + resolved.dirfd().as_raw_fd(), + resolved.path(), + AtFlag::SYMLINK_NOFOLLOW, + ) + } { + if FileType::from_stat_st_mode(stat.st_mode) == FileType::Symlink { + return Err(Error::ELOOP); + } + } + return Err(Error::ENOTDIR); + } + // FreeBSD returns EMLINK instead of ELOOP when using O_NOFOLLOW on + // a symlink. + Errno::EMLINK if !(nix_all_oflags & OFlag::NOFOLLOW).is_empty() => { + return Err(Error::ELOOP); + } + errno => return Err(errno.into()), + } + } else { + return Err(e.into()); + } + } + }; + + log::debug!("path_open (host) new_fd = {:?}", new_fd); + + // Determine the type of the new file descriptor and which rights contradict with this type + Ok(unsafe { File::from_raw_fd(new_fd) }) +} + +pub(crate) fn path_readlink(resolved: PathGet, buf: &mut [u8]) -> Result { + use std::cmp::min; + use yanix::file::readlinkat; + let read_link = unsafe { readlinkat(resolved.dirfd().as_raw_fd(), resolved.path()) } + .map_err(Into::into) + .and_then(host_impl::path_from_host)?; + let copy_len = min(read_link.len(), buf.len()); + if copy_len > 0 { + buf[..copy_len].copy_from_slice(&read_link.as_bytes()[..copy_len]); + } + Ok(copy_len) +} + +pub(crate) fn fd_filestat_get(file: &std::fs::File) -> Result { + use yanix::file::fstat; + unsafe { fstat(file.as_raw_fd()) } + .map_err(Into::into) + .and_then(host_impl::filestat_from_nix) +} + +pub(crate) fn path_filestat_get( + resolved: PathGet, + dirflags: wasi::__wasi_lookupflags_t, +) -> Result { + use yanix::file::{fstatat, AtFlag}; + let atflags = match dirflags { + 0 => AtFlag::empty(), + _ => AtFlag::SYMLINK_NOFOLLOW, + }; + unsafe { fstatat(resolved.dirfd().as_raw_fd(), resolved.path(), atflags) } + .map_err(Into::into) + .and_then(host_impl::filestat_from_nix) +} + +pub(crate) fn path_filestat_set_times( + resolved: PathGet, + dirflags: wasi::__wasi_lookupflags_t, + st_atim: wasi::__wasi_timestamp_t, + st_mtim: wasi::__wasi_timestamp_t, + fst_flags: wasi::__wasi_fstflags_t, +) -> Result<()> { + use super::super::filetime::*; + use std::time::{Duration, UNIX_EPOCH}; + + let set_atim = fst_flags & wasi::__WASI_FSTFLAGS_ATIM != 0; + let set_atim_now = fst_flags & wasi::__WASI_FSTFLAGS_ATIM_NOW != 0; + let set_mtim = fst_flags & wasi::__WASI_FSTFLAGS_MTIM != 0; + let set_mtim_now = fst_flags & wasi::__WASI_FSTFLAGS_MTIM_NOW != 0; + + if (set_atim && set_atim_now) || (set_mtim && set_mtim_now) { + return Err(Error::EINVAL); + } + + let symlink_nofollow = wasi::__WASI_LOOKUPFLAGS_SYMLINK_FOLLOW != dirflags; + let atim = if set_atim { + let time = UNIX_EPOCH + Duration::from_nanos(st_atim); + FileTime::FileTime(filetime::FileTime::from_system_time(time)) + } else if set_atim_now { + FileTime::Now + } else { + FileTime::Omit + }; + let mtim = if set_mtim { + let time = UNIX_EPOCH + Duration::from_nanos(st_mtim); + FileTime::FileTime(filetime::FileTime::from_system_time(time)) + } else if set_mtim_now { + FileTime::Now + } else { + FileTime::Omit + }; + + utimensat( + resolved.dirfd(), + resolved.path(), + atim, + mtim, + symlink_nofollow, + ) + .map_err(Into::into) +} + +pub(crate) fn path_remove_directory(resolved: PathGet) -> Result<()> { + use yanix::file::{unlinkat, AtFlag}; + unsafe { + unlinkat( + resolved.dirfd().as_raw_fd(), + resolved.path(), + AtFlag::REMOVEDIR, + ) + } + .map_err(Into::into) +} + +pub(crate) fn fd_readdir<'a>( + os_handle: &'a mut OsHandle, + cookie: wasi::__wasi_dircookie_t, +) -> Result> + 'a> { + use yanix::dir::{DirIter, Entry, EntryExt, SeekLoc}; + + // Get an instance of `Dir`; this is host-specific due to intricasies + // of managing a dir stream between Linux and BSD *nixes + let mut dir = fd_readdir_impl::get_dir_from_os_handle(os_handle)?; + + // Seek if needed. Unless cookie is wasi::__WASI_DIRCOOKIE_START, + // new items may not be returned to the caller. + if cookie == wasi::__WASI_DIRCOOKIE_START { + log::trace!(" | fd_readdir: doing rewinddir"); + dir.rewind(); + } else { + log::trace!(" | fd_readdir: doing seekdir to {}", cookie); + let loc = unsafe { SeekLoc::from_raw(cookie as i64)? }; + dir.seek(loc); + } + + Ok(DirIter::new(dir).map(|entry| { + let entry: Entry = entry?; + Ok(Dirent { + name: entry + // TODO can we reuse path_from_host for CStr? + .file_name() + .to_str()? + .to_owned(), + ino: entry.ino(), + ftype: entry.file_type().into(), + cookie: entry.seek_loc()?.to_raw().try_into()?, + }) + })) +} diff --git a/crates/wasi-common/src/old/snapshot_0/sys/unix/hostcalls_impl/fs_helpers.rs b/crates/wasi-common/src/old/snapshot_0/sys/unix/hostcalls_impl/fs_helpers.rs new file mode 100644 index 0000000000..609e42dc5c --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/unix/hostcalls_impl/fs_helpers.rs @@ -0,0 +1,66 @@ +#![allow(non_camel_case_types)] +#![allow(unused_unsafe)] +use crate::old::snapshot_0::sys::host_impl; +use crate::old::snapshot_0::{wasi, Result}; +use std::fs::File; +use yanix::file::OFlag; + +pub(crate) fn path_open_rights( + rights_base: wasi::__wasi_rights_t, + rights_inheriting: wasi::__wasi_rights_t, + oflags: wasi::__wasi_oflags_t, + fs_flags: wasi::__wasi_fdflags_t, +) -> (wasi::__wasi_rights_t, wasi::__wasi_rights_t) { + // which rights are needed on the dirfd? + let mut needed_base = wasi::__WASI_RIGHTS_PATH_OPEN; + let mut needed_inheriting = rights_base | rights_inheriting; + + // convert open flags + let oflags = host_impl::nix_from_oflags(oflags); + if oflags.contains(OFlag::CREAT) { + needed_base |= wasi::__WASI_RIGHTS_PATH_CREATE_FILE; + } + if oflags.contains(OFlag::TRUNC) { + needed_base |= wasi::__WASI_RIGHTS_PATH_FILESTAT_SET_SIZE; + } + + // convert file descriptor flags + let fdflags = host_impl::nix_from_fdflags(fs_flags); + if fdflags.contains(OFlag::DSYNC) { + needed_inheriting |= wasi::__WASI_RIGHTS_FD_DATASYNC; + } + if fdflags.intersects(host_impl::O_RSYNC | OFlag::SYNC) { + needed_inheriting |= wasi::__WASI_RIGHTS_FD_SYNC; + } + + (needed_base, needed_inheriting) +} + +pub(crate) fn openat(dirfd: &File, path: &str) -> Result { + use std::os::unix::prelude::{AsRawFd, FromRawFd}; + use yanix::file::{openat, Mode}; + + log::debug!("path_get openat path = {:?}", path); + + unsafe { + openat( + dirfd.as_raw_fd(), + path, + OFlag::RDONLY | OFlag::DIRECTORY | OFlag::NOFOLLOW, + Mode::empty(), + ) + } + .map(|new_fd| unsafe { File::from_raw_fd(new_fd) }) + .map_err(Into::into) +} + +pub(crate) fn readlinkat(dirfd: &File, path: &str) -> Result { + use std::os::unix::prelude::AsRawFd; + use yanix::file::readlinkat; + + log::debug!("path_get readlinkat path = {:?}", path); + + unsafe { readlinkat(dirfd.as_raw_fd(), path) } + .map_err(Into::into) + .and_then(host_impl::path_from_host) +} diff --git a/crates/wasi-common/src/old/snapshot_0/sys/unix/hostcalls_impl/misc.rs b/crates/wasi-common/src/old/snapshot_0/sys/unix/hostcalls_impl/misc.rs new file mode 100644 index 0000000000..f6dfbe96ea --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/unix/hostcalls_impl/misc.rs @@ -0,0 +1,195 @@ +#![allow(non_camel_case_types)] +#![allow(unused_unsafe)] +use crate::old::snapshot_0::hostcalls_impl::{ClockEventData, FdEventData}; +use crate::old::snapshot_0::{wasi, Error, Result}; +use yanix::clock::{clock_getres, clock_gettime, ClockId}; + +fn wasi_clock_id_to_unix(clock_id: wasi::__wasi_clockid_t) -> Result { + // convert the supported clocks to libc types, or return EINVAL + match clock_id { + wasi::__WASI_CLOCKID_REALTIME => Ok(ClockId::Realtime), + wasi::__WASI_CLOCKID_MONOTONIC => Ok(ClockId::Monotonic), + wasi::__WASI_CLOCKID_PROCESS_CPUTIME_ID => Ok(ClockId::ProcessCPUTime), + wasi::__WASI_CLOCKID_THREAD_CPUTIME_ID => Ok(ClockId::ThreadCPUTime), + _ => Err(Error::EINVAL), + } +} + +pub(crate) fn clock_res_get(clock_id: wasi::__wasi_clockid_t) -> Result { + let clock_id = wasi_clock_id_to_unix(clock_id)?; + let timespec = clock_getres(clock_id)?; + + // convert to nanoseconds, returning EOVERFLOW in case of overflow; + // this is freelancing a bit from the spec but seems like it'll + // be an unusual situation to hit + (timespec.tv_sec as wasi::__wasi_timestamp_t) + .checked_mul(1_000_000_000) + .and_then(|sec_ns| sec_ns.checked_add(timespec.tv_nsec as wasi::__wasi_timestamp_t)) + .map_or(Err(Error::EOVERFLOW), |resolution| { + // a supported clock can never return zero; this case will probably never get hit, but + // make sure we follow the spec + if resolution == 0 { + Err(Error::EINVAL) + } else { + Ok(resolution) + } + }) +} + +pub(crate) fn clock_time_get(clock_id: wasi::__wasi_clockid_t) -> Result { + let clock_id = wasi_clock_id_to_unix(clock_id)?; + let timespec = clock_gettime(clock_id)?; + + // convert to nanoseconds, returning EOVERFLOW in case of overflow; this is freelancing a bit + // from the spec but seems like it'll be an unusual situation to hit + (timespec.tv_sec as wasi::__wasi_timestamp_t) + .checked_mul(1_000_000_000) + .and_then(|sec_ns| sec_ns.checked_add(timespec.tv_nsec as wasi::__wasi_timestamp_t)) + .map_or(Err(Error::EOVERFLOW), Ok) +} + +pub(crate) fn poll_oneoff( + timeout: Option, + fd_events: Vec, + events: &mut Vec, +) -> Result<()> { + use std::{convert::TryInto, os::unix::prelude::AsRawFd}; + use yanix::{ + poll::{poll, PollFd, PollFlags}, + Errno, + }; + + if fd_events.is_empty() && timeout.is_none() { + return Ok(()); + } + + let mut poll_fds: Vec<_> = fd_events + .iter() + .map(|event| { + let mut flags = PollFlags::empty(); + match event.r#type { + wasi::__WASI_EVENTTYPE_FD_READ => flags.insert(PollFlags::POLLIN), + wasi::__WASI_EVENTTYPE_FD_WRITE => flags.insert(PollFlags::POLLOUT), + // An event on a file descriptor can currently only be of type FD_READ or FD_WRITE + // Nothing else has been defined in the specification, and these are also the only two + // events we filtered before. If we get something else here, the code has a serious bug. + _ => unreachable!(), + }; + unsafe { PollFd::new(event.descriptor.as_raw_fd(), flags) } + }) + .collect(); + + let poll_timeout = timeout.map_or(-1, |timeout| { + let delay = timeout.delay / 1_000_000; // poll syscall requires delay to expressed in milliseconds + delay.try_into().unwrap_or(libc::c_int::max_value()) + }); + log::debug!("poll_oneoff poll_timeout = {:?}", poll_timeout); + + let ready = loop { + match poll(&mut poll_fds, poll_timeout) { + Err(_) => { + if Errno::last() == Errno::EINTR { + continue; + } + return Err(Errno::last().into()); + } + Ok(ready) => break ready, + } + }; + + Ok(if ready == 0 { + poll_oneoff_handle_timeout_event(timeout.expect("timeout should not be None"), events) + } else { + let ready_events = fd_events.into_iter().zip(poll_fds.into_iter()).take(ready); + poll_oneoff_handle_fd_event(ready_events, events)? + }) +} + +fn poll_oneoff_handle_timeout_event( + timeout: ClockEventData, + events: &mut Vec, +) { + events.push(wasi::__wasi_event_t { + userdata: timeout.userdata, + error: wasi::__WASI_ERRNO_SUCCESS, + r#type: wasi::__WASI_EVENTTYPE_CLOCK, + fd_readwrite: wasi::__wasi_event_fd_readwrite_t { + flags: 0, + nbytes: 0, + }, + }); +} + +fn poll_oneoff_handle_fd_event<'a>( + ready_events: impl Iterator, yanix::poll::PollFd)>, + events: &mut Vec, +) -> Result<()> { + use std::{convert::TryInto, os::unix::prelude::AsRawFd}; + use yanix::{file::fionread, poll::PollFlags}; + + for (fd_event, poll_fd) in ready_events { + log::debug!("poll_oneoff_handle_fd_event fd_event = {:?}", fd_event); + log::debug!("poll_oneoff_handle_fd_event poll_fd = {:?}", poll_fd); + + let revents = match poll_fd.revents() { + Some(revents) => revents, + None => continue, + }; + + log::debug!("poll_oneoff_handle_fd_event revents = {:?}", revents); + + let nbytes = if fd_event.r#type == wasi::__WASI_EVENTTYPE_FD_READ { + unsafe { fionread(fd_event.descriptor.as_raw_fd())? } + } else { + 0 + }; + + let output_event = if revents.contains(PollFlags::POLLNVAL) { + wasi::__wasi_event_t { + userdata: fd_event.userdata, + error: wasi::__WASI_ERRNO_BADF, + r#type: fd_event.r#type, + fd_readwrite: wasi::__wasi_event_fd_readwrite_t { + nbytes: 0, + flags: wasi::__WASI_EVENTRWFLAGS_FD_READWRITE_HANGUP, + }, + } + } else if revents.contains(PollFlags::POLLERR) { + wasi::__wasi_event_t { + userdata: fd_event.userdata, + error: wasi::__WASI_ERRNO_IO, + r#type: fd_event.r#type, + fd_readwrite: wasi::__wasi_event_fd_readwrite_t { + nbytes: 0, + flags: wasi::__WASI_EVENTRWFLAGS_FD_READWRITE_HANGUP, + }, + } + } else if revents.contains(PollFlags::POLLHUP) { + wasi::__wasi_event_t { + userdata: fd_event.userdata, + error: wasi::__WASI_ERRNO_SUCCESS, + r#type: fd_event.r#type, + fd_readwrite: wasi::__wasi_event_fd_readwrite_t { + nbytes: 0, + flags: wasi::__WASI_EVENTRWFLAGS_FD_READWRITE_HANGUP, + }, + } + } else if revents.contains(PollFlags::POLLIN) | revents.contains(PollFlags::POLLOUT) { + wasi::__wasi_event_t { + userdata: fd_event.userdata, + error: wasi::__WASI_ERRNO_SUCCESS, + r#type: fd_event.r#type, + fd_readwrite: wasi::__wasi_event_fd_readwrite_t { + nbytes: nbytes.try_into()?, + flags: 0, + }, + } + } else { + continue; + }; + + events.push(output_event); + } + + Ok(()) +} diff --git a/crates/wasi-common/src/old/snapshot_0/sys/unix/hostcalls_impl/mod.rs b/crates/wasi-common/src/old/snapshot_0/sys/unix/hostcalls_impl/mod.rs new file mode 100644 index 0000000000..ba18086104 --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/unix/hostcalls_impl/mod.rs @@ -0,0 +1,8 @@ +//! Unix-specific hostcalls that implement +//! [WASI](https://github.com/bytecodealliance/wasmtime-wasi/blob/wasi/docs/WASI-overview.md). +mod fs; +pub(crate) mod fs_helpers; +mod misc; + +pub(crate) use self::fs::*; +pub(crate) use self::misc::*; diff --git a/crates/wasi-common/src/old/snapshot_0/sys/unix/linux/filetime.rs b/crates/wasi-common/src/old/snapshot_0/sys/unix/linux/filetime.rs new file mode 100644 index 0000000000..8162b391fa --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/unix/linux/filetime.rs @@ -0,0 +1,61 @@ +//! This internal module consists of helper types and functions for dealing +//! with setting the file times specific to Linux. +use crate::old::snapshot_0::{sys::unix::filetime::FileTime, Result}; +use std::fs::File; +use std::io; +use std::sync::atomic::{AtomicBool, Ordering::Relaxed}; + +pub(crate) const UTIME_NOW: i64 = 1_073_741_823; +pub(crate) const UTIME_OMIT: i64 = 1_073_741_822; + +/// Wrapper for `utimensat` syscall, however, with an added twist such that `utimensat` symbol +/// is firstly resolved (i.e., we check whether it exists on the host), and only used if that is +/// the case. Otherwise, the syscall resorts to a less accurate `utimesat` emulated syscall. +/// The original implementation can be found here: [filetime::unix::linux::set_times] +/// +/// [filetime::unix::linux::set_times]: https://github.com/alexcrichton/filetime/blob/master/src/unix/linux.rs#L64 +pub(crate) fn utimensat( + dirfd: &File, + path: &str, + atime: FileTime, + mtime: FileTime, + symlink_nofollow: bool, +) -> Result<()> { + use crate::old::snapshot_0::sys::unix::filetime::to_timespec; + use std::ffi::CString; + use std::os::unix::prelude::*; + + let flags = if symlink_nofollow { + libc::AT_SYMLINK_NOFOLLOW + } else { + 0 + }; + + // Attempt to use the `utimensat` syscall, but if it's not supported by the + // current kernel then fall back to an older syscall. + static INVALID: AtomicBool = AtomicBool::new(false); + if !INVALID.load(Relaxed) { + let p = CString::new(path.as_bytes())?; + let times = [to_timespec(&atime)?, to_timespec(&mtime)?]; + let rc = unsafe { + libc::syscall( + libc::SYS_utimensat, + dirfd.as_raw_fd(), + p.as_ptr(), + times.as_ptr(), + flags, + ) + }; + if rc == 0 { + return Ok(()); + } + let err = io::Error::last_os_error(); + if err.raw_os_error() == Some(libc::ENOSYS) { + INVALID.store(true, Relaxed); + } else { + return Err(err.into()); + } + } + + super::utimesat::utimesat(dirfd, path, atime, mtime, symlink_nofollow) +} diff --git a/crates/wasi-common/src/old/snapshot_0/sys/unix/linux/host_impl.rs b/crates/wasi-common/src/old/snapshot_0/sys/unix/linux/host_impl.rs new file mode 100644 index 0000000000..d867e51b6f --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/unix/linux/host_impl.rs @@ -0,0 +1,16 @@ +use crate::old::snapshot_0::{wasi, Result}; +use std::convert::TryInto; + +pub(crate) const O_RSYNC: yanix::file::OFlag = yanix::file::OFlag::RSYNC; + +pub(crate) fn stdev_from_nix(dev: libc::dev_t) -> Result { + Ok(wasi::__wasi_device_t::from(dev)) +} + +pub(crate) fn stino_from_nix(ino: libc::ino_t) -> Result { + Ok(wasi::__wasi_device_t::from(ino)) +} + +pub(crate) fn stnlink_from_nix(nlink: libc::nlink_t) -> Result { + nlink.try_into().map_err(Into::into) +} diff --git a/crates/wasi-common/src/old/snapshot_0/sys/unix/linux/hostcalls_impl.rs b/crates/wasi-common/src/old/snapshot_0/sys/unix/linux/hostcalls_impl.rs new file mode 100644 index 0000000000..86b1ec443e --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/unix/linux/hostcalls_impl.rs @@ -0,0 +1,60 @@ +use crate::old::snapshot_0::hostcalls_impl::PathGet; +use crate::old::snapshot_0::Result; +use std::os::unix::prelude::AsRawFd; + +pub(crate) fn path_unlink_file(resolved: PathGet) -> Result<()> { + use yanix::file::{unlinkat, AtFlag}; + unsafe { + unlinkat( + resolved.dirfd().as_raw_fd(), + resolved.path(), + AtFlag::empty(), + ) + } + .map_err(Into::into) +} + +pub(crate) fn path_symlink(old_path: &str, resolved: PathGet) -> Result<()> { + use yanix::file::symlinkat; + + log::debug!("path_symlink old_path = {:?}", old_path); + log::debug!("path_symlink resolved = {:?}", resolved); + + unsafe { symlinkat(old_path, resolved.dirfd().as_raw_fd(), resolved.path()) } + .map_err(Into::into) +} + +pub(crate) fn path_rename(resolved_old: PathGet, resolved_new: PathGet) -> Result<()> { + use yanix::file::renameat; + unsafe { + renameat( + resolved_old.dirfd().as_raw_fd(), + resolved_old.path(), + resolved_new.dirfd().as_raw_fd(), + resolved_new.path(), + ) + } + .map_err(Into::into) +} + +pub(crate) mod fd_readdir_impl { + use crate::old::snapshot_0::sys::fdentry_impl::OsHandle; + use crate::old::snapshot_0::Result; + use yanix::dir::Dir; + + pub(crate) fn get_dir_from_os_handle(os_handle: &mut OsHandle) -> Result> { + // We need to duplicate the fd, because `opendir(3)`: + // After a successful call to fdopendir(), fd is used internally by the implementation, + // and should not otherwise be used by the application. + // `opendir(3p)` also says that it's undefined behavior to + // modify the state of the fd in a different way than by accessing DIR*. + // + // Still, rewinddir will be needed because the two file descriptors + // share progress. But we can safely execute closedir now. + let fd = os_handle.try_clone()?; + // TODO This doesn't look very clean. Can we do something about it? + // Boxing is needed here in order to satisfy `yanix`'s trait requirement for the `DirIter` + // where `T: Deref`. + Ok(Box::new(Dir::from(fd)?)) + } +} diff --git a/crates/wasi-common/src/old/snapshot_0/sys/unix/linux/mod.rs b/crates/wasi-common/src/old/snapshot_0/sys/unix/linux/mod.rs new file mode 100644 index 0000000000..0b4b8fd0b9 --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/unix/linux/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod filetime; +pub(crate) mod host_impl; +pub(crate) mod hostcalls_impl; +pub(crate) mod oshandle; +pub(crate) mod utimesat; diff --git a/crates/wasi-common/src/old/snapshot_0/sys/unix/linux/oshandle.rs b/crates/wasi-common/src/old/snapshot_0/sys/unix/linux/oshandle.rs new file mode 100644 index 0000000000..8104d9d5e3 --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/unix/linux/oshandle.rs @@ -0,0 +1,32 @@ +use std::fs; +use std::ops::{Deref, DerefMut}; +use std::os::unix::prelude::{AsRawFd, RawFd}; + +#[derive(Debug)] +pub(crate) struct OsHandle(fs::File); + +impl From for OsHandle { + fn from(file: fs::File) -> Self { + Self(file) + } +} + +impl AsRawFd for OsHandle { + fn as_raw_fd(&self) -> RawFd { + self.0.as_raw_fd() + } +} + +impl Deref for OsHandle { + type Target = fs::File; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for OsHandle { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/crates/wasi-common/src/old/snapshot_0/sys/unix/linux/utimesat.rs b/crates/wasi-common/src/old/snapshot_0/sys/unix/linux/utimesat.rs new file mode 100644 index 0000000000..155d994578 --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/unix/linux/utimesat.rs @@ -0,0 +1,88 @@ +use crate::old::snapshot_0::sys::unix::filetime::FileTime; +use crate::old::snapshot_0::Result; +use std::{fs, io}; + +/// Combines `openat` with `utimes` to emulate `utimensat` on platforms where it is +/// not available. The logic for setting file times is based on [filetime::unix::set_file_handles_times]. +/// +/// [filetime::unix::set_file_handles_times]: https://github.com/alexcrichton/filetime/blob/master/src/unix/utimes.rs#L24 +pub(crate) fn utimesat( + dirfd: &fs::File, + path: &str, + atime: FileTime, + mtime: FileTime, + symlink_nofollow: bool, +) -> Result<()> { + use std::ffi::CString; + use std::os::unix::prelude::*; + // emulate *at syscall by reading the path from a combination of + // (fd, path) + let p = CString::new(path.as_bytes())?; + let mut flags = libc::O_RDWR; + if symlink_nofollow { + flags |= libc::O_NOFOLLOW; + } + let fd = unsafe { libc::openat(dirfd.as_raw_fd(), p.as_ptr(), flags) }; + let f = unsafe { fs::File::from_raw_fd(fd) }; + let (atime, mtime) = get_times(atime, mtime, || f.metadata().map_err(Into::into))?; + let times = [to_timeval(atime), to_timeval(mtime)]; + let rc = unsafe { libc::futimes(f.as_raw_fd(), times.as_ptr()) }; + if rc == 0 { + Ok(()) + } else { + Err(io::Error::last_os_error().into()) + } +} + +/// Converts `filetime::FileTime` to `libc::timeval`. This function was taken directly from +/// [filetime] crate. +/// +/// [filetime]: https://github.com/alexcrichton/filetime/blob/master/src/unix/utimes.rs#L93 +fn to_timeval(ft: filetime::FileTime) -> libc::timeval { + libc::timeval { + tv_sec: ft.seconds(), + tv_usec: (ft.nanoseconds() / 1000) as libc::suseconds_t, + } +} + +/// For a provided pair of access and modified `FileTime`s, converts the input to +/// `filetime::FileTime` used later in `utimensat` function. For variants `FileTime::Now` +/// and `FileTime::Omit`, this function will make two syscalls: either accessing current +/// system time, or accessing the file's metadata. +/// +/// The original implementation can be found here: [filetime::unix::get_times]. +/// +/// [filetime::unix::get_times]: https://github.com/alexcrichton/filetime/blob/master/src/unix/utimes.rs#L42 +fn get_times( + atime: FileTime, + mtime: FileTime, + current: impl Fn() -> Result, +) -> Result<(filetime::FileTime, filetime::FileTime)> { + use std::time::SystemTime; + + let atime = match atime { + FileTime::Now => { + let time = SystemTime::now(); + filetime::FileTime::from_system_time(time) + } + FileTime::Omit => { + let meta = current()?; + filetime::FileTime::from_last_access_time(&meta) + } + FileTime::FileTime(ft) => ft, + }; + + let mtime = match mtime { + FileTime::Now => { + let time = SystemTime::now(); + filetime::FileTime::from_system_time(time) + } + FileTime::Omit => { + let meta = current()?; + filetime::FileTime::from_last_modification_time(&meta) + } + FileTime::FileTime(ft) => ft, + }; + + Ok((atime, mtime)) +} diff --git a/crates/wasi-common/src/old/snapshot_0/sys/unix/mod.rs b/crates/wasi-common/src/old/snapshot_0/sys/unix/mod.rs new file mode 100644 index 0000000000..723c878950 --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/unix/mod.rs @@ -0,0 +1,34 @@ +pub(crate) mod fdentry_impl; +pub(crate) mod host_impl; +pub(crate) mod hostcalls_impl; + +mod filetime; + +cfg_if::cfg_if! { + if #[cfg(target_os = "linux")] { + mod linux; + use self::linux as sys_impl; + } else if #[cfg(target_os = "emscripten")] { + mod emscripten; + use self::emscripten as sys_impl; + } else if #[cfg(any(target_os = "macos", + target_os = "netbsd", + target_os = "freebsd", + target_os = "openbsd", + target_os = "ios", + target_os = "dragonfly"))] { + mod bsd; + use self::bsd as sys_impl; + } +} + +use crate::old::snapshot_0::Result; +use std::fs::{File, OpenOptions}; + +pub(crate) fn dev_null() -> Result { + OpenOptions::new() + .read(true) + .write(true) + .open("/dev/null") + .map_err(Into::into) +} diff --git a/crates/wasi-common/src/old/snapshot_0/sys/windows/fdentry_impl.rs b/crates/wasi-common/src/old/snapshot_0/sys/windows/fdentry_impl.rs new file mode 100644 index 0000000000..4b1c6ba39e --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/windows/fdentry_impl.rs @@ -0,0 +1,138 @@ +use crate::old::snapshot_0::fdentry::{Descriptor, OsHandleRef}; +use crate::old::snapshot_0::{wasi, Error, Result}; +use std::fs::File; +use std::io; +use std::mem::ManuallyDrop; +use std::ops::{Deref, DerefMut}; +use std::os::windows::prelude::{AsRawHandle, FromRawHandle, RawHandle}; + +#[derive(Debug)] +pub(crate) struct OsHandle(File); + +impl From for OsHandle { + fn from(file: File) -> Self { + Self(file) + } +} + +impl AsRawHandle for OsHandle { + fn as_raw_handle(&self) -> RawHandle { + self.0.as_raw_handle() + } +} + +impl Deref for OsHandle { + type Target = File; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for OsHandle { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl AsRawHandle for Descriptor { + fn as_raw_handle(&self) -> RawHandle { + match self { + Self::OsHandle(file) => file.as_raw_handle(), + Self::Stdin => io::stdin().as_raw_handle(), + Self::Stdout => io::stdout().as_raw_handle(), + Self::Stderr => io::stderr().as_raw_handle(), + } + } +} + +pub(crate) fn descriptor_as_oshandle<'lifetime>( + desc: &'lifetime Descriptor, +) -> OsHandleRef<'lifetime> { + OsHandleRef::new(ManuallyDrop::new(OsHandle::from(unsafe { + File::from_raw_handle(desc.as_raw_handle()) + }))) +} + +/// This function is unsafe because it operates on a raw file handle. +pub(crate) unsafe fn determine_type_and_access_rights( + handle: &Handle, +) -> Result<( + wasi::__wasi_filetype_t, + wasi::__wasi_rights_t, + wasi::__wasi_rights_t, +)> { + use winx::file::{query_access_information, AccessMode}; + + let (file_type, mut rights_base, rights_inheriting) = determine_type_rights(handle)?; + + match file_type { + wasi::__WASI_FILETYPE_DIRECTORY | wasi::__WASI_FILETYPE_REGULAR_FILE => { + let mode = query_access_information(handle.as_raw_handle())?; + if mode.contains(AccessMode::FILE_GENERIC_READ) { + rights_base |= wasi::__WASI_RIGHTS_FD_READ; + } + if mode.contains(AccessMode::FILE_GENERIC_WRITE) { + rights_base |= wasi::__WASI_RIGHTS_FD_WRITE; + } + } + _ => { + // TODO: is there a way around this? On windows, it seems + // we cannot check access rights for anything but dirs and regular files + } + } + + Ok((file_type, rights_base, rights_inheriting)) +} + +/// This function is unsafe because it operates on a raw file handle. +pub(crate) unsafe fn determine_type_rights( + handle: &Handle, +) -> Result<( + wasi::__wasi_filetype_t, + wasi::__wasi_rights_t, + wasi::__wasi_rights_t, +)> { + let (file_type, rights_base, rights_inheriting) = { + let file_type = winx::file::get_file_type(handle.as_raw_handle())?; + if file_type.is_char() { + // character file: LPT device or console + // TODO: rule out LPT device + ( + wasi::__WASI_FILETYPE_CHARACTER_DEVICE, + wasi::RIGHTS_TTY_BASE, + wasi::RIGHTS_TTY_BASE, + ) + } else if file_type.is_disk() { + // disk file: file, dir or disk device + let file = std::mem::ManuallyDrop::new(File::from_raw_handle(handle.as_raw_handle())); + let meta = file.metadata().map_err(|_| Error::EINVAL)?; + if meta.is_dir() { + ( + wasi::__WASI_FILETYPE_DIRECTORY, + wasi::RIGHTS_DIRECTORY_BASE, + wasi::RIGHTS_DIRECTORY_INHERITING, + ) + } else if meta.is_file() { + ( + wasi::__WASI_FILETYPE_REGULAR_FILE, + wasi::RIGHTS_REGULAR_FILE_BASE, + wasi::RIGHTS_REGULAR_FILE_INHERITING, + ) + } else { + return Err(Error::EINVAL); + } + } else if file_type.is_pipe() { + // pipe object: socket, named pipe or anonymous pipe + // TODO: what about pipes, etc? + ( + wasi::__WASI_FILETYPE_SOCKET_STREAM, + wasi::RIGHTS_SOCKET_BASE, + wasi::RIGHTS_SOCKET_INHERITING, + ) + } else { + return Err(Error::EINVAL); + } + }; + Ok((file_type, rights_base, rights_inheriting)) +} diff --git a/crates/wasi-common/src/old/snapshot_0/sys/windows/host_impl.rs b/crates/wasi-common/src/old/snapshot_0/sys/windows/host_impl.rs new file mode 100644 index 0000000000..5fcebb052e --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/windows/host_impl.rs @@ -0,0 +1,176 @@ +//! WASI host types specific to Windows host. +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(unused)] +use crate::old::snapshot_0::host::FileType; +use crate::old::snapshot_0::{error::FromRawOsError, wasi, Error, Result}; +use std::convert::TryInto; +use std::ffi::OsStr; +use std::fs::OpenOptions; +use std::fs::{self, File}; +use std::io; +use std::os::windows::ffi::OsStrExt; +use std::os::windows::fs::OpenOptionsExt; +use std::time::{SystemTime, UNIX_EPOCH}; +use winx::file::{AccessMode, Attributes, CreationDisposition, Flags}; +use winx::winerror::WinError; + +impl FromRawOsError for Error { + fn from_raw_os_error(code: i32) -> Self { + Self::from(WinError::from_u32(code as u32)) + } +} + +impl From for Error { + fn from(err: WinError) -> Self { + // TODO: implement error mapping between Windows and WASI + use winx::winerror::WinError::*; + match err { + ERROR_SUCCESS => Self::ESUCCESS, + ERROR_BAD_ENVIRONMENT => Self::E2BIG, + ERROR_FILE_NOT_FOUND => Self::ENOENT, + ERROR_PATH_NOT_FOUND => Self::ENOENT, + ERROR_TOO_MANY_OPEN_FILES => Self::ENFILE, + ERROR_ACCESS_DENIED => Self::EACCES, + ERROR_SHARING_VIOLATION => Self::EACCES, + ERROR_PRIVILEGE_NOT_HELD => Self::ENOTCAPABLE, // TODO is this the correct mapping? + ERROR_INVALID_HANDLE => Self::EBADF, + ERROR_INVALID_NAME => Self::ENOENT, + ERROR_NOT_ENOUGH_MEMORY => Self::ENOMEM, + ERROR_OUTOFMEMORY => Self::ENOMEM, + ERROR_DIR_NOT_EMPTY => Self::ENOTEMPTY, + ERROR_NOT_READY => Self::EBUSY, + ERROR_BUSY => Self::EBUSY, + ERROR_NOT_SUPPORTED => Self::ENOTSUP, + ERROR_FILE_EXISTS => Self::EEXIST, + ERROR_BROKEN_PIPE => Self::EPIPE, + ERROR_BUFFER_OVERFLOW => Self::ENAMETOOLONG, + ERROR_NOT_A_REPARSE_POINT => Self::EINVAL, + ERROR_NEGATIVE_SEEK => Self::EINVAL, + ERROR_DIRECTORY => Self::ENOTDIR, + ERROR_ALREADY_EXISTS => Self::EEXIST, + _ => Self::ENOTSUP, + } + } +} + +pub(crate) fn fdflags_from_win(mode: AccessMode) -> wasi::__wasi_fdflags_t { + let mut fdflags = 0; + // TODO verify this! + if mode.contains(AccessMode::FILE_APPEND_DATA) { + fdflags |= wasi::__WASI_FDFLAGS_APPEND; + } + if mode.contains(AccessMode::SYNCHRONIZE) { + fdflags |= wasi::__WASI_FDFLAGS_DSYNC; + fdflags |= wasi::__WASI_FDFLAGS_RSYNC; + fdflags |= wasi::__WASI_FDFLAGS_SYNC; + } + // The NONBLOCK equivalent is FILE_FLAG_OVERLAPPED + // but it seems winapi doesn't provide a mechanism + // for checking whether the handle supports async IO. + // On the contrary, I've found some dicsussion online + // which suggests that on Windows all handles should + // generally be assumed to be opened with async support + // and then the program should fallback should that **not** + // be the case at the time of the operation. + // TODO: this requires further investigation + fdflags +} + +pub(crate) fn win_from_fdflags(fdflags: wasi::__wasi_fdflags_t) -> (AccessMode, Flags) { + let mut access_mode = AccessMode::empty(); + let mut flags = Flags::empty(); + + // TODO verify this! + if fdflags & wasi::__WASI_FDFLAGS_NONBLOCK != 0 { + flags.insert(Flags::FILE_FLAG_OVERLAPPED); + } + if fdflags & wasi::__WASI_FDFLAGS_APPEND != 0 { + access_mode.insert(AccessMode::FILE_APPEND_DATA); + } + if fdflags & wasi::__WASI_FDFLAGS_DSYNC != 0 + || fdflags & wasi::__WASI_FDFLAGS_RSYNC != 0 + || fdflags & wasi::__WASI_FDFLAGS_SYNC != 0 + { + access_mode.insert(AccessMode::SYNCHRONIZE); + } + + (access_mode, flags) +} + +pub(crate) fn win_from_oflags(oflags: wasi::__wasi_oflags_t) -> CreationDisposition { + if oflags & wasi::__WASI_OFLAGS_CREAT != 0 { + if oflags & wasi::__WASI_OFLAGS_EXCL != 0 { + CreationDisposition::CREATE_NEW + } else { + CreationDisposition::CREATE_ALWAYS + } + } else if oflags & wasi::__WASI_OFLAGS_TRUNC != 0 { + CreationDisposition::TRUNCATE_EXISTING + } else { + CreationDisposition::OPEN_EXISTING + } +} + +pub(crate) fn filetype_from_std(ftype: &fs::FileType) -> FileType { + if ftype.is_file() { + FileType::RegularFile + } else if ftype.is_dir() { + FileType::Directory + } else if ftype.is_symlink() { + FileType::Symlink + } else { + FileType::Unknown + } +} + +fn num_hardlinks(file: &File) -> io::Result { + Ok(winx::file::get_fileinfo(file)?.nNumberOfLinks.into()) +} + +fn device_id(file: &File) -> io::Result { + Ok(winx::file::get_fileinfo(file)?.dwVolumeSerialNumber.into()) +} + +pub(crate) fn file_serial_no(file: &File) -> io::Result { + let info = winx::file::get_fileinfo(file)?; + let high = info.nFileIndexHigh; + let low = info.nFileIndexLow; + let no = (u64::from(high) << 32) | u64::from(low); + Ok(no) +} + +fn change_time(file: &File) -> io::Result { + winx::file::change_time(file) +} + +fn systemtime_to_timestamp(st: SystemTime) -> Result { + st.duration_since(UNIX_EPOCH) + .map_err(|_| Error::EINVAL)? // date earlier than UNIX_EPOCH + .as_nanos() + .try_into() + .map_err(Into::into) // u128 doesn't fit into u64 +} + +pub(crate) fn filestat_from_win(file: &File) -> Result { + let metadata = file.metadata()?; + Ok(wasi::__wasi_filestat_t { + dev: device_id(file)?, + ino: file_serial_no(file)?, + nlink: num_hardlinks(file)?.try_into()?, // u64 doesn't fit into u32 + size: metadata.len(), + atim: systemtime_to_timestamp(metadata.accessed()?)?, + ctim: change_time(file)?.try_into()?, // i64 doesn't fit into u64 + mtim: systemtime_to_timestamp(metadata.modified()?)?, + filetype: filetype_from_std(&metadata.file_type()).to_wasi(), + }) +} + +/// Creates owned WASI path from OS string. +/// +/// NB WASI spec requires OS string to be valid UTF-8. Otherwise, +/// `__WASI_ERRNO_ILSEQ` error is returned. +pub(crate) fn path_from_host>(s: S) -> Result { + let vec: Vec = s.as_ref().encode_wide().collect(); + String::from_utf16(&vec).map_err(|_| Error::EILSEQ) +} diff --git a/crates/wasi-common/src/old/snapshot_0/sys/windows/hostcalls_impl/fs.rs b/crates/wasi-common/src/old/snapshot_0/sys/windows/hostcalls_impl/fs.rs new file mode 100644 index 0000000000..173b8aa2b2 --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/windows/hostcalls_impl/fs.rs @@ -0,0 +1,549 @@ +#![allow(non_camel_case_types)] +#![allow(unused)] +use super::fs_helpers::*; +use crate::old::snapshot_0::ctx::WasiCtx; +use crate::old::snapshot_0::fdentry::FdEntry; +use crate::old::snapshot_0::host::{Dirent, FileType}; +use crate::old::snapshot_0::hostcalls_impl::{fd_filestat_set_times_impl, PathGet}; +use crate::old::snapshot_0::sys::fdentry_impl::determine_type_rights; +use crate::old::snapshot_0::sys::host_impl::{self, path_from_host}; +use crate::old::snapshot_0::sys::hostcalls_impl::fs_helpers::PathGetExt; +use crate::old::snapshot_0::{wasi, Error, Result}; +use log::{debug, trace}; +use std::convert::TryInto; +use std::fs::{File, Metadata, OpenOptions}; +use std::io::{self, Seek, SeekFrom}; +use std::os::windows::fs::{FileExt, OpenOptionsExt}; +use std::os::windows::prelude::{AsRawHandle, FromRawHandle}; +use std::path::{Path, PathBuf}; +use winx::file::{AccessMode, CreationDisposition, FileModeInformation, Flags}; + +fn read_at(mut file: &File, buf: &mut [u8], offset: u64) -> io::Result { + // get current cursor position + let cur_pos = file.seek(SeekFrom::Current(0))?; + // perform a seek read by a specified offset + let nread = file.seek_read(buf, offset)?; + // rewind the cursor back to the original position + file.seek(SeekFrom::Start(cur_pos))?; + Ok(nread) +} + +fn write_at(mut file: &File, buf: &[u8], offset: u64) -> io::Result { + // get current cursor position + let cur_pos = file.seek(SeekFrom::Current(0))?; + // perform a seek write by a specified offset + let nwritten = file.seek_write(buf, offset)?; + // rewind the cursor back to the original position + file.seek(SeekFrom::Start(cur_pos))?; + Ok(nwritten) +} + +// TODO refactor common code with unix +pub(crate) fn fd_pread( + file: &File, + buf: &mut [u8], + offset: wasi::__wasi_filesize_t, +) -> Result { + read_at(file, buf, offset).map_err(Into::into) +} + +// TODO refactor common code with unix +pub(crate) fn fd_pwrite(file: &File, buf: &[u8], offset: wasi::__wasi_filesize_t) -> Result { + write_at(file, buf, offset).map_err(Into::into) +} + +pub(crate) fn fd_fdstat_get(fd: &File) -> Result { + let mut fdflags = 0; + + let handle = unsafe { fd.as_raw_handle() }; + + let access_mode = winx::file::query_access_information(handle)?; + let mode = winx::file::query_mode_information(handle)?; + + // Append without write implies append-only (__WASI_FDFLAGS_APPEND) + if access_mode.contains(AccessMode::FILE_APPEND_DATA) + && !access_mode.contains(AccessMode::FILE_WRITE_DATA) + { + fdflags |= wasi::__WASI_FDFLAGS_APPEND; + } + + if mode.contains(FileModeInformation::FILE_WRITE_THROUGH) { + // Only report __WASI_FDFLAGS_SYNC + // This is technically the only one of the O_?SYNC flags Windows supports. + fdflags |= wasi::__WASI_FDFLAGS_SYNC; + } + + // Files do not support the `__WASI_FDFLAGS_NONBLOCK` flag + + Ok(fdflags) +} + +pub(crate) fn fd_fdstat_set_flags(fd: &File, fdflags: wasi::__wasi_fdflags_t) -> Result<()> { + unimplemented!("fd_fdstat_set_flags") +} + +pub(crate) fn fd_advise( + _file: &File, + advice: wasi::__wasi_advice_t, + _offset: wasi::__wasi_filesize_t, + _len: wasi::__wasi_filesize_t, +) -> Result<()> { + match advice { + wasi::__WASI_ADVICE_DONTNEED + | wasi::__WASI_ADVICE_SEQUENTIAL + | wasi::__WASI_ADVICE_WILLNEED + | wasi::__WASI_ADVICE_NOREUSE + | wasi::__WASI_ADVICE_RANDOM + | wasi::__WASI_ADVICE_NORMAL => {} + _ => return Err(Error::EINVAL), + } + + Ok(()) +} + +pub(crate) fn path_create_directory(resolved: PathGet) -> Result<()> { + let path = resolved.concatenate()?; + std::fs::create_dir(&path).map_err(Into::into) +} + +pub(crate) fn path_link(resolved_old: PathGet, resolved_new: PathGet) -> Result<()> { + unimplemented!("path_link") +} + +pub(crate) fn path_open( + resolved: PathGet, + read: bool, + write: bool, + oflags: wasi::__wasi_oflags_t, + fdflags: wasi::__wasi_fdflags_t, +) -> Result { + use winx::file::{AccessMode, CreationDisposition, Flags}; + + // convert open flags + // note: the calls to `write(true)` are to bypass an internal OpenOption check + // the write flag will ultimately be ignored when `access_mode` is called below. + let mut opts = OpenOptions::new(); + match creation_disposition_from_oflags(oflags) { + CreationDisposition::CREATE_ALWAYS => { + opts.create(true).write(true); + } + CreationDisposition::CREATE_NEW => { + opts.create_new(true).write(true); + } + CreationDisposition::TRUNCATE_EXISTING => { + opts.truncate(true).write(true); + } + _ => {} + } + + let path = resolved.concatenate()?; + + match path.symlink_metadata().map(|metadata| metadata.file_type()) { + Ok(file_type) => { + // check if we are trying to open a symlink + if file_type.is_symlink() { + return Err(Error::ELOOP); + } + // check if we are trying to open a file as a dir + if file_type.is_file() && oflags & wasi::__WASI_OFLAGS_DIRECTORY != 0 { + return Err(Error::ENOTDIR); + } + } + Err(e) => match e.raw_os_error() { + Some(e) => { + use winx::winerror::WinError; + log::debug!("path_open at symlink_metadata error code={:?}", e); + let e = WinError::from_u32(e as u32); + + if e != WinError::ERROR_FILE_NOT_FOUND { + return Err(e.into()); + } + // file not found, let it proceed to actually + // trying to open it + } + None => { + log::debug!("Inconvertible OS error: {}", e); + return Err(Error::EIO); + } + }, + } + + opts.access_mode(file_access_mode_from_fdflags(fdflags, read, write).bits()) + .custom_flags(file_flags_from_fdflags(fdflags).bits()) + .open(&path) + .map_err(Into::into) +} + +fn creation_disposition_from_oflags(oflags: wasi::__wasi_oflags_t) -> CreationDisposition { + if oflags & wasi::__WASI_OFLAGS_CREAT != 0 { + if oflags & wasi::__WASI_OFLAGS_EXCL != 0 { + CreationDisposition::CREATE_NEW + } else { + CreationDisposition::CREATE_ALWAYS + } + } else if oflags & wasi::__WASI_OFLAGS_TRUNC != 0 { + CreationDisposition::TRUNCATE_EXISTING + } else { + CreationDisposition::OPEN_EXISTING + } +} + +fn file_access_mode_from_fdflags( + fdflags: wasi::__wasi_fdflags_t, + read: bool, + write: bool, +) -> AccessMode { + let mut access_mode = AccessMode::READ_CONTROL; + + if read { + access_mode.insert(AccessMode::GENERIC_READ); + } + + if write { + access_mode.insert(AccessMode::GENERIC_WRITE); + } + + // For append, grant the handle FILE_APPEND_DATA access but *not* FILE_WRITE_DATA. + // This makes the handle "append only". + // Changes to the file pointer will be ignored (like POSIX's O_APPEND behavior). + if fdflags & wasi::__WASI_FDFLAGS_APPEND != 0 { + access_mode.insert(AccessMode::FILE_APPEND_DATA); + access_mode.remove(AccessMode::FILE_WRITE_DATA); + } + + access_mode +} + +fn file_flags_from_fdflags(fdflags: wasi::__wasi_fdflags_t) -> Flags { + // Enable backup semantics so directories can be opened as files + let mut flags = Flags::FILE_FLAG_BACKUP_SEMANTICS; + + // Note: __WASI_FDFLAGS_NONBLOCK is purposely being ignored for files + // While Windows does inherently support a non-blocking mode on files, the WASI API will + // treat I/O operations on files as synchronous. WASI might have an async-io API in the future. + + // Technically, Windows only supports __WASI_FDFLAGS_SYNC, but treat all the flags as the same. + if fdflags & wasi::__WASI_FDFLAGS_DSYNC != 0 + || fdflags & wasi::__WASI_FDFLAGS_RSYNC != 0 + || fdflags & wasi::__WASI_FDFLAGS_SYNC != 0 + { + flags.insert(Flags::FILE_FLAG_WRITE_THROUGH); + } + + flags +} + +fn dirent_from_path>( + path: P, + name: &str, + cookie: wasi::__wasi_dircookie_t, +) -> Result { + let path = path.as_ref(); + trace!("dirent_from_path: opening {}", path.to_string_lossy()); + + // To open a directory on Windows, FILE_FLAG_BACKUP_SEMANTICS flag must be used + let file = OpenOptions::new() + .custom_flags(Flags::FILE_FLAG_BACKUP_SEMANTICS.bits()) + .read(true) + .open(path)?; + let ty = file.metadata()?.file_type(); + Ok(Dirent { + ftype: host_impl::filetype_from_std(&ty), + name: name.to_owned(), + cookie, + ino: host_impl::file_serial_no(&file)?, + }) +} + +// On Windows there is apparently no support for seeking the directory stream in the OS. +// cf. https://github.com/WebAssembly/WASI/issues/61 +// +// The implementation here may perform in O(n^2) if the host buffer is O(1) +// and the number of directory entries is O(n). +// TODO: Add a heuristic optimization to achieve O(n) time in the most common case +// where fd_readdir is resumed where it previously finished +// +// Correctness of this approach relies upon one assumption: that the order of entries +// returned by `FindNextFileW` is stable, i.e. doesn't change if the directory +// contents stay the same. This invariant is crucial to be able to implement +// any kind of seeking whatsoever without having to read the whole directory at once +// and then return the data from cache. (which leaks memory) +// +// The MSDN documentation explicitly says that the order in which the search returns the files +// is not guaranteed, and is dependent on the file system. +// cf. https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findnextfilew +// +// This stackoverflow post suggests that `FindNextFileW` is indeed stable and that +// the order of directory entries depends **only** on the filesystem used, but the +// MSDN documentation is not clear about this. +// cf. https://stackoverflow.com/questions/47380739/is-findfirstfile-and-findnextfile-order-random-even-for-dvd +// +// Implementation details: +// Cookies for the directory entries start from 1. (0 is reserved by wasi::__WASI_DIRCOOKIE_START) +// . gets cookie = 1 +// .. gets cookie = 2 +// other entries, in order they were returned by FindNextFileW get subsequent integers as their cookies +pub(crate) fn fd_readdir( + fd: &File, + cookie: wasi::__wasi_dircookie_t, +) -> Result>> { + use winx::file::get_file_path; + + let cookie = cookie.try_into()?; + let path = get_file_path(fd)?; + // std::fs::ReadDir doesn't return . and .., so we need to emulate it + let path = Path::new(&path); + // The directory /.. is the same as / on Unix (at least on ext4), so emulate this behavior too + let parent = path.parent().unwrap_or(path); + let dot = dirent_from_path(path, ".", 1)?; + let dotdot = dirent_from_path(parent, "..", 2)?; + + trace!(" | fd_readdir impl: executing std::fs::ReadDir"); + let iter = path.read_dir()?.zip(3..).map(|(dir, no)| { + let dir: std::fs::DirEntry = dir?; + + Ok(Dirent { + name: path_from_host(dir.file_name())?, + ftype: host_impl::filetype_from_std(&dir.file_type()?), + ino: File::open(dir.path()).and_then(|f| host_impl::file_serial_no(&f))?, + cookie: no, + }) + }); + + // into_iter for arrays is broken and returns references instead of values, + // so we need to use vec![...] and do heap allocation + // See https://github.com/rust-lang/rust/issues/25725 + let iter = vec![dot, dotdot].into_iter().map(Ok).chain(iter); + + // Emulate seekdir(). This may give O(n^2) complexity if used with a + // small host_buf, but this is difficult to implement efficiently. + // + // See https://github.com/WebAssembly/WASI/issues/61 for more details. + Ok(iter.skip(cookie)) +} + +pub(crate) fn path_readlink(resolved: PathGet, buf: &mut [u8]) -> Result { + use winx::file::get_file_path; + + let path = resolved.concatenate()?; + let target_path = path.read_link()?; + + // since on Windows we are effectively emulating 'at' syscalls + // we need to strip the prefix from the absolute path + // as otherwise we will error out since WASI is not capable + // of dealing with absolute paths + let dir_path = get_file_path(resolved.dirfd())?; + let dir_path = PathBuf::from(strip_extended_prefix(dir_path)); + let target_path = target_path + .strip_prefix(dir_path) + .map_err(|_| Error::ENOTCAPABLE) + .and_then(|path| path.to_str().map(String::from).ok_or(Error::EILSEQ))?; + + if buf.len() > 0 { + let mut chars = target_path.chars(); + let mut nread = 0usize; + + for i in 0..buf.len() { + match chars.next() { + Some(ch) => { + buf[i] = ch as u8; + nread += 1; + } + None => break, + } + } + + Ok(nread) + } else { + Ok(0) + } +} + +fn strip_trailing_slashes_and_concatenate(resolved: &PathGet) -> Result> { + if resolved.path().ends_with('/') { + let suffix = resolved.path().trim_end_matches('/'); + concatenate(resolved.dirfd(), Path::new(suffix)).map(Some) + } else { + Ok(None) + } +} + +pub(crate) fn path_rename(resolved_old: PathGet, resolved_new: PathGet) -> Result<()> { + use std::fs; + + let old_path = resolved_old.concatenate()?; + let new_path = resolved_new.concatenate()?; + + // First sanity check: check we're not trying to rename dir to file or vice versa. + // NB on Windows, the former is actually permitted [std::fs::rename]. + // + // [std::fs::rename]: https://doc.rust-lang.org/std/fs/fn.rename.html + if old_path.is_dir() && new_path.is_file() { + return Err(Error::ENOTDIR); + } + // Second sanity check: check we're not trying to rename a file into a path + // ending in a trailing slash. + if old_path.is_file() && resolved_new.path().ends_with('/') { + return Err(Error::ENOTDIR); + } + + // TODO handle symlinks + + fs::rename(&old_path, &new_path).or_else(|e| match e.raw_os_error() { + Some(e) => { + use winx::winerror::WinError; + + log::debug!("path_rename at rename error code={:?}", e); + match WinError::from_u32(e as u32) { + WinError::ERROR_ACCESS_DENIED => { + // So most likely dealing with new_path == dir. + // Eliminate case old_path == file first. + if old_path.is_file() { + Err(Error::EISDIR) + } else { + // Ok, let's try removing an empty dir at new_path if it exists + // and is a nonempty dir. + fs::remove_dir(&new_path) + .and_then(|()| fs::rename(old_path, new_path)) + .map_err(Into::into) + } + } + WinError::ERROR_INVALID_NAME => { + // If source contains trailing slashes, check if we are dealing with + // a file instead of a dir, and if so, throw ENOTDIR. + if let Some(path) = strip_trailing_slashes_and_concatenate(&resolved_old)? { + if path.is_file() { + return Err(Error::ENOTDIR); + } + } + Err(WinError::ERROR_INVALID_NAME.into()) + } + e => Err(e.into()), + } + } + None => { + log::debug!("Inconvertible OS error: {}", e); + Err(Error::EIO) + } + }) +} + +pub(crate) fn fd_filestat_get(file: &std::fs::File) -> Result { + host_impl::filestat_from_win(file) +} + +pub(crate) fn path_filestat_get( + resolved: PathGet, + dirflags: wasi::__wasi_lookupflags_t, +) -> Result { + let path = resolved.concatenate()?; + let file = File::open(path)?; + host_impl::filestat_from_win(&file) +} + +pub(crate) fn path_filestat_set_times( + resolved: PathGet, + dirflags: wasi::__wasi_lookupflags_t, + st_atim: wasi::__wasi_timestamp_t, + mut st_mtim: wasi::__wasi_timestamp_t, + fst_flags: wasi::__wasi_fstflags_t, +) -> Result<()> { + use winx::file::AccessMode; + let path = resolved.concatenate()?; + let file = OpenOptions::new() + .access_mode(AccessMode::FILE_WRITE_ATTRIBUTES.bits()) + .open(path)?; + fd_filestat_set_times_impl(&file, st_atim, st_mtim, fst_flags) +} + +pub(crate) fn path_symlink(old_path: &str, resolved: PathGet) -> Result<()> { + use std::os::windows::fs::{symlink_dir, symlink_file}; + use winx::winerror::WinError; + + let old_path = concatenate(resolved.dirfd(), Path::new(old_path))?; + let new_path = resolved.concatenate()?; + + // try creating a file symlink + symlink_file(&old_path, &new_path).or_else(|e| { + match e.raw_os_error() { + Some(e) => { + log::debug!("path_symlink at symlink_file error code={:?}", e); + match WinError::from_u32(e as u32) { + WinError::ERROR_NOT_A_REPARSE_POINT => { + // try creating a dir symlink instead + symlink_dir(old_path, new_path).map_err(Into::into) + } + WinError::ERROR_ACCESS_DENIED => { + // does the target exist? + if new_path.exists() { + Err(Error::EEXIST) + } else { + Err(WinError::ERROR_ACCESS_DENIED.into()) + } + } + WinError::ERROR_INVALID_NAME => { + // does the target without trailing slashes exist? + if let Some(path) = strip_trailing_slashes_and_concatenate(&resolved)? { + if path.exists() { + return Err(Error::EEXIST); + } + } + Err(WinError::ERROR_INVALID_NAME.into()) + } + e => Err(e.into()), + } + } + None => { + log::debug!("Inconvertible OS error: {}", e); + Err(Error::EIO) + } + } + }) +} + +pub(crate) fn path_unlink_file(resolved: PathGet) -> Result<()> { + use std::fs; + use winx::winerror::WinError; + + let path = resolved.concatenate()?; + let file_type = path + .symlink_metadata() + .map(|metadata| metadata.file_type())?; + + // check if we're unlinking a symlink + // NB this will get cleaned up a lot when [std::os::windows::fs::FileTypeExt] + // stabilises + // + // [std::os::windows::fs::FileTypeExt]: https://doc.rust-lang.org/std/os/windows/fs/trait.FileTypeExt.html + if file_type.is_symlink() { + fs::remove_file(&path).or_else(|e| { + match e.raw_os_error() { + Some(e) => { + log::debug!("path_unlink_file at symlink_file error code={:?}", e); + match WinError::from_u32(e as u32) { + WinError::ERROR_ACCESS_DENIED => { + // try unlinking a dir symlink instead + fs::remove_dir(path).map_err(Into::into) + } + e => Err(e.into()), + } + } + None => { + log::debug!("Inconvertible OS error: {}", e); + Err(Error::EIO) + } + } + }) + } else if file_type.is_dir() { + Err(Error::EISDIR) + } else if file_type.is_file() { + fs::remove_file(path).map_err(Into::into) + } else { + Err(Error::EINVAL) + } +} + +pub(crate) fn path_remove_directory(resolved: PathGet) -> Result<()> { + let path = resolved.concatenate()?; + std::fs::remove_dir(&path).map_err(Into::into) +} diff --git a/crates/wasi-common/src/old/snapshot_0/sys/windows/hostcalls_impl/fs_helpers.rs b/crates/wasi-common/src/old/snapshot_0/sys/windows/hostcalls_impl/fs_helpers.rs new file mode 100644 index 0000000000..f05721a8dd --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/windows/hostcalls_impl/fs_helpers.rs @@ -0,0 +1,149 @@ +#![allow(non_camel_case_types)] +use crate::old::snapshot_0::hostcalls_impl::PathGet; +use crate::old::snapshot_0::{wasi, Error, Result}; +use std::ffi::{OsStr, OsString}; +use std::fs::File; +use std::os::windows::ffi::{OsStrExt, OsStringExt}; +use std::path::{Path, PathBuf}; + +pub(crate) trait PathGetExt { + fn concatenate(&self) -> Result; +} + +impl PathGetExt for PathGet { + fn concatenate(&self) -> Result { + concatenate(self.dirfd(), Path::new(self.path())) + } +} + +pub(crate) fn path_open_rights( + rights_base: wasi::__wasi_rights_t, + rights_inheriting: wasi::__wasi_rights_t, + oflags: wasi::__wasi_oflags_t, + fdflags: wasi::__wasi_fdflags_t, +) -> (wasi::__wasi_rights_t, wasi::__wasi_rights_t) { + // which rights are needed on the dirfd? + let mut needed_base = wasi::__WASI_RIGHTS_PATH_OPEN; + let mut needed_inheriting = rights_base | rights_inheriting; + + // convert open flags + if oflags & wasi::__WASI_OFLAGS_CREAT != 0 { + needed_base |= wasi::__WASI_RIGHTS_PATH_CREATE_FILE; + } else if oflags & wasi::__WASI_OFLAGS_TRUNC != 0 { + needed_base |= wasi::__WASI_RIGHTS_PATH_FILESTAT_SET_SIZE; + } + + // convert file descriptor flags + if fdflags & wasi::__WASI_FDFLAGS_DSYNC != 0 + || fdflags & wasi::__WASI_FDFLAGS_RSYNC != 0 + || fdflags & wasi::__WASI_FDFLAGS_SYNC != 0 + { + needed_inheriting |= wasi::__WASI_RIGHTS_FD_DATASYNC; + needed_inheriting |= wasi::__WASI_RIGHTS_FD_SYNC; + } + + (needed_base, needed_inheriting) +} + +pub(crate) fn openat(dirfd: &File, path: &str) -> Result { + use std::fs::OpenOptions; + use std::os::windows::fs::OpenOptionsExt; + use winx::file::Flags; + use winx::winerror::WinError; + + let path = concatenate(dirfd, Path::new(path))?; + OpenOptions::new() + .read(true) + .custom_flags(Flags::FILE_FLAG_BACKUP_SEMANTICS.bits()) + .open(&path) + .map_err(|e| match e.raw_os_error() { + Some(e) => { + log::debug!("openat error={:?}", e); + match WinError::from_u32(e as u32) { + WinError::ERROR_INVALID_NAME => Error::ENOTDIR, + e => e.into(), + } + } + None => { + log::debug!("Inconvertible OS error: {}", e); + Error::EIO + } + }) +} + +pub(crate) fn readlinkat(dirfd: &File, s_path: &str) -> Result { + use winx::file::get_file_path; + use winx::winerror::WinError; + + let path = concatenate(dirfd, Path::new(s_path))?; + match path.read_link() { + Ok(target_path) => { + // since on Windows we are effectively emulating 'at' syscalls + // we need to strip the prefix from the absolute path + // as otherwise we will error out since WASI is not capable + // of dealing with absolute paths + let dir_path = get_file_path(dirfd)?; + let dir_path = PathBuf::from(strip_extended_prefix(dir_path)); + target_path + .strip_prefix(dir_path) + .map_err(|_| Error::ENOTCAPABLE) + .and_then(|path| path.to_str().map(String::from).ok_or(Error::EILSEQ)) + } + Err(e) => match e.raw_os_error() { + Some(e) => { + log::debug!("readlinkat error={:?}", e); + match WinError::from_u32(e as u32) { + WinError::ERROR_INVALID_NAME => { + if s_path.ends_with('/') { + // strip "/" and check if exists + let path = concatenate(dirfd, Path::new(s_path.trim_end_matches('/')))?; + if path.exists() && !path.is_dir() { + Err(Error::ENOTDIR) + } else { + Err(Error::ENOENT) + } + } else { + Err(Error::ENOENT) + } + } + e => Err(e.into()), + } + } + None => { + log::debug!("Inconvertible OS error: {}", e); + Err(Error::EIO) + } + }, + } +} + +pub(crate) fn strip_extended_prefix>(path: P) -> OsString { + let path: Vec = path.as_ref().encode_wide().collect(); + if &[92, 92, 63, 92] == &path[0..4] { + OsString::from_wide(&path[4..]) + } else { + OsString::from_wide(&path) + } +} + +pub(crate) fn concatenate>(dirfd: &File, path: P) -> Result { + use winx::file::get_file_path; + + // WASI is not able to deal with absolute paths + // so error out if absolute + if path.as_ref().is_absolute() { + return Err(Error::ENOTCAPABLE); + } + + let dir_path = get_file_path(dirfd)?; + // concatenate paths + let mut out_path = PathBuf::from(dir_path); + out_path.push(path.as_ref()); + // strip extended prefix; otherwise we will error out on any relative + // components with `out_path` + let out_path = PathBuf::from(strip_extended_prefix(out_path)); + + log::debug!("out_path={:?}", out_path); + + Ok(out_path) +} diff --git a/crates/wasi-common/src/old/snapshot_0/sys/windows/hostcalls_impl/misc.rs b/crates/wasi-common/src/old/snapshot_0/sys/windows/hostcalls_impl/misc.rs new file mode 100644 index 0000000000..bf8df626cd --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/windows/hostcalls_impl/misc.rs @@ -0,0 +1,118 @@ +#![allow(non_camel_case_types)] +#![allow(unused_unsafe)] +#![allow(unused)] +use crate::old::snapshot_0::hostcalls_impl::{ClockEventData, FdEventData}; +use crate::old::snapshot_0::memory::*; +use crate::old::snapshot_0::sys::host_impl; +use crate::old::snapshot_0::{wasi, wasi32, Error, Result}; +use cpu_time::{ProcessTime, ThreadTime}; +use lazy_static::lazy_static; +use std::convert::TryInto; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +lazy_static! { + static ref START_MONOTONIC: Instant = Instant::now(); + static ref PERF_COUNTER_RES: u64 = get_perf_counter_resolution_ns(); +} + +// Timer resolution on Windows is really hard. We may consider exposing the resolution of the respective +// timers as an associated function in the future. +pub(crate) fn clock_res_get(clock_id: wasi::__wasi_clockid_t) -> Result { + Ok(match clock_id { + // This is the best that we can do with std::time::SystemTime. + // Rust uses GetSystemTimeAsFileTime, which is said to have the resolution of + // 10ms or 55ms, [1] but MSDN doesn't confirm this in any way. + // Even the MSDN article on high resolution timestamps doesn't even mention the precision + // for this method. [3] + // + // The timer resolution can be queried using one of the functions: [2, 5] + // * NtQueryTimerResolution, which is undocumented and thus not exposed by the winapi crate + // * timeGetDevCaps, which returns the upper and lower bound for the precision, in ms. + // While the upper bound seems like something we could use, it's typically too high to be meaningful. + // For instance, the intervals return by the syscall are: + // * [1, 65536] on Wine + // * [1, 1000000] on Windows 10, which is up to (sic) 1000 seconds. + // + // It's possible to manually set the timer resolution, but this sounds like something which should + // only be done temporarily. [5] + // + // Alternatively, we could possibly use GetSystemTimePreciseAsFileTime in clock_time_get, but + // this syscall is only available starting from Windows 8. + // (we could possibly emulate it on earlier versions of Windows, see [4]) + // The MSDN are not clear on the resolution of GetSystemTimePreciseAsFileTime either, but a + // Microsoft devblog entry [1] suggests that it kind of combines GetSystemTimeAsFileTime with + // QueryPeformanceCounter, which probably means that those two should have the same resolution. + // + // See also this discussion about the use of GetSystemTimePreciseAsFileTime in Python stdlib, + // which in particular contains some resolution benchmarks. + // + // [1] https://devblogs.microsoft.com/oldnewthing/20170921-00/?p=97057 + // [2] http://www.windowstimestamp.com/description + // [3] https://docs.microsoft.com/en-us/windows/win32/sysinfo/acquiring-high-resolution-time-stamps?redirectedfrom=MSDN + // [4] https://www.codeproject.com/Tips/1011902/High-Resolution-Time-For-Windows + // [5] https://stackoverflow.com/questions/7685762/windows-7-timing-functions-how-to-use-getsystemtimeadjustment-correctly + // [6] https://bugs.python.org/issue19007 + wasi::__WASI_CLOCKID_REALTIME => 55_000_000, + // std::time::Instant uses QueryPerformanceCounter & QueryPerformanceFrequency internally + wasi::__WASI_CLOCKID_MONOTONIC => *PERF_COUNTER_RES, + // The best we can do is to hardcode the value from the docs. + // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getprocesstimes + wasi::__WASI_CLOCKID_PROCESS_CPUTIME_ID => 100, + // The best we can do is to hardcode the value from the docs. + // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getthreadtimes + wasi::__WASI_CLOCKID_THREAD_CPUTIME_ID => 100, + _ => return Err(Error::EINVAL), + }) +} + +pub(crate) fn clock_time_get(clock_id: wasi::__wasi_clockid_t) -> Result { + let duration = match clock_id { + wasi::__WASI_CLOCKID_REALTIME => get_monotonic_time(), + wasi::__WASI_CLOCKID_MONOTONIC => get_realtime_time()?, + wasi::__WASI_CLOCKID_PROCESS_CPUTIME_ID => get_proc_cputime()?, + wasi::__WASI_CLOCKID_THREAD_CPUTIME_ID => get_thread_cputime()?, + _ => return Err(Error::EINVAL), + }; + duration.as_nanos().try_into().map_err(Into::into) +} + +pub(crate) fn poll_oneoff( + timeout: Option, + fd_events: Vec, + events: &mut Vec, +) -> Result> { + unimplemented!("poll_oneoff") +} + +fn get_monotonic_time() -> Duration { + // We're circumventing the fact that we can't get a Duration from an Instant + // The epoch of __WASI_CLOCKID_MONOTONIC is undefined, so we fix a time point once + // and count relative to this time point. + // + // The alternative would be to copy over the implementation of std::time::Instant + // to our source tree and add a conversion to std::time::Duration + START_MONOTONIC.elapsed() +} + +fn get_realtime_time() -> Result { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| Error::EFAULT) +} + +fn get_proc_cputime() -> Result { + Ok(ProcessTime::try_now()?.as_duration()) +} + +fn get_thread_cputime() -> Result { + Ok(ThreadTime::try_now()?.as_duration()) +} + +fn get_perf_counter_resolution_ns() -> u64 { + use winx::time::perf_counter_frequency; + const NANOS_PER_SEC: u64 = 1_000_000_000; + // This should always succeed starting from Windows XP, so it's fine to panic in case of an error. + let freq = perf_counter_frequency().expect("QueryPerformanceFrequency returned an error"); + let epsilon = NANOS_PER_SEC / freq; + epsilon +} diff --git a/crates/wasi-common/src/old/snapshot_0/sys/windows/hostcalls_impl/mod.rs b/crates/wasi-common/src/old/snapshot_0/sys/windows/hostcalls_impl/mod.rs new file mode 100644 index 0000000000..fdbf448f80 --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/windows/hostcalls_impl/mod.rs @@ -0,0 +1,8 @@ +//! Windows-specific hostcalls that implement +//! [WASI](https://github.com/bytecodealliance/wasmtime-wasi/blob/wasi/docs/WASI-overview.md). +mod fs; +pub(crate) mod fs_helpers; +mod misc; + +pub(crate) use self::fs::*; +pub(crate) use self::misc::*; diff --git a/crates/wasi-common/src/old/snapshot_0/sys/windows/mod.rs b/crates/wasi-common/src/old/snapshot_0/sys/windows/mod.rs new file mode 100644 index 0000000000..35d00ac935 --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/sys/windows/mod.rs @@ -0,0 +1,14 @@ +pub(crate) mod fdentry_impl; +pub(crate) mod host_impl; +pub(crate) mod hostcalls_impl; + +use crate::old::snapshot_0::Result; +use std::fs::{File, OpenOptions}; + +pub(crate) fn dev_null() -> Result { + OpenOptions::new() + .read(true) + .write(true) + .open("NUL") + .map_err(Into::into) +} diff --git a/crates/wasi-common/src/old/snapshot_0/wasi.rs b/crates/wasi-common/src/old/snapshot_0/wasi.rs new file mode 100644 index 0000000000..63a08e8c5e --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/wasi.rs @@ -0,0 +1,119 @@ +//! Types and constants shared between 32-bit and 64-bit wasi. Types involving +//! pointer or `usize`-sized data are excluded here, so this file only contains +//! fixed-size types, so it's host/target independent. + +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(dead_code)] + +use wig::witx_wasi_types; + +witx_wasi_types!("old/snapshot_0" "wasi_unstable"); + +pub(crate) const RIGHTS_ALL: __wasi_rights_t = __WASI_RIGHTS_FD_DATASYNC + | __WASI_RIGHTS_FD_READ + | __WASI_RIGHTS_FD_SEEK + | __WASI_RIGHTS_FD_FDSTAT_SET_FLAGS + | __WASI_RIGHTS_FD_SYNC + | __WASI_RIGHTS_FD_TELL + | __WASI_RIGHTS_FD_WRITE + | __WASI_RIGHTS_FD_ADVISE + | __WASI_RIGHTS_FD_ALLOCATE + | __WASI_RIGHTS_PATH_CREATE_DIRECTORY + | __WASI_RIGHTS_PATH_CREATE_FILE + | __WASI_RIGHTS_PATH_LINK_SOURCE + | __WASI_RIGHTS_PATH_LINK_TARGET + | __WASI_RIGHTS_PATH_OPEN + | __WASI_RIGHTS_FD_READDIR + | __WASI_RIGHTS_PATH_READLINK + | __WASI_RIGHTS_PATH_RENAME_SOURCE + | __WASI_RIGHTS_PATH_RENAME_TARGET + | __WASI_RIGHTS_PATH_FILESTAT_GET + | __WASI_RIGHTS_PATH_FILESTAT_SET_SIZE + | __WASI_RIGHTS_PATH_FILESTAT_SET_TIMES + | __WASI_RIGHTS_FD_FILESTAT_GET + | __WASI_RIGHTS_FD_FILESTAT_SET_SIZE + | __WASI_RIGHTS_FD_FILESTAT_SET_TIMES + | __WASI_RIGHTS_PATH_SYMLINK + | __WASI_RIGHTS_PATH_UNLINK_FILE + | __WASI_RIGHTS_PATH_REMOVE_DIRECTORY + | __WASI_RIGHTS_POLL_FD_READWRITE + | __WASI_RIGHTS_SOCK_SHUTDOWN; + +// Block and character device interaction is outside the scope of +// WASI. Simply allow everything. +pub(crate) const RIGHTS_BLOCK_DEVICE_BASE: __wasi_rights_t = RIGHTS_ALL; +pub(crate) const RIGHTS_BLOCK_DEVICE_INHERITING: __wasi_rights_t = RIGHTS_ALL; +pub(crate) const RIGHTS_CHARACTER_DEVICE_BASE: __wasi_rights_t = RIGHTS_ALL; +pub(crate) const RIGHTS_CHARACTER_DEVICE_INHERITING: __wasi_rights_t = RIGHTS_ALL; + +// Only allow directory operations on directories. Directories can only +// yield file descriptors to other directories and files. +pub(crate) const RIGHTS_DIRECTORY_BASE: __wasi_rights_t = __WASI_RIGHTS_FD_FDSTAT_SET_FLAGS + | __WASI_RIGHTS_FD_SYNC + | __WASI_RIGHTS_FD_ADVISE + | __WASI_RIGHTS_PATH_CREATE_DIRECTORY + | __WASI_RIGHTS_PATH_CREATE_FILE + | __WASI_RIGHTS_PATH_LINK_SOURCE + | __WASI_RIGHTS_PATH_LINK_TARGET + | __WASI_RIGHTS_PATH_OPEN + | __WASI_RIGHTS_FD_READDIR + | __WASI_RIGHTS_PATH_READLINK + | __WASI_RIGHTS_PATH_RENAME_SOURCE + | __WASI_RIGHTS_PATH_RENAME_TARGET + | __WASI_RIGHTS_PATH_FILESTAT_GET + | __WASI_RIGHTS_PATH_FILESTAT_SET_SIZE + | __WASI_RIGHTS_PATH_FILESTAT_SET_TIMES + | __WASI_RIGHTS_FD_FILESTAT_GET + | __WASI_RIGHTS_FD_FILESTAT_SET_TIMES + | __WASI_RIGHTS_PATH_SYMLINK + | __WASI_RIGHTS_PATH_UNLINK_FILE + | __WASI_RIGHTS_PATH_REMOVE_DIRECTORY + | __WASI_RIGHTS_POLL_FD_READWRITE; +pub(crate) const RIGHTS_DIRECTORY_INHERITING: __wasi_rights_t = + RIGHTS_DIRECTORY_BASE | RIGHTS_REGULAR_FILE_BASE; + +// Operations that apply to regular files. +pub(crate) const RIGHTS_REGULAR_FILE_BASE: __wasi_rights_t = __WASI_RIGHTS_FD_DATASYNC + | __WASI_RIGHTS_FD_READ + | __WASI_RIGHTS_FD_SEEK + | __WASI_RIGHTS_FD_FDSTAT_SET_FLAGS + | __WASI_RIGHTS_FD_SYNC + | __WASI_RIGHTS_FD_TELL + | __WASI_RIGHTS_FD_WRITE + | __WASI_RIGHTS_FD_ADVISE + | __WASI_RIGHTS_FD_ALLOCATE + | __WASI_RIGHTS_FD_FILESTAT_GET + | __WASI_RIGHTS_FD_FILESTAT_SET_SIZE + | __WASI_RIGHTS_FD_FILESTAT_SET_TIMES + | __WASI_RIGHTS_POLL_FD_READWRITE; +pub(crate) const RIGHTS_REGULAR_FILE_INHERITING: __wasi_rights_t = 0; + +// Operations that apply to sockets and socket pairs. +pub(crate) const RIGHTS_SOCKET_BASE: __wasi_rights_t = __WASI_RIGHTS_FD_READ + | __WASI_RIGHTS_FD_FDSTAT_SET_FLAGS + | __WASI_RIGHTS_FD_WRITE + | __WASI_RIGHTS_FD_FILESTAT_GET + | __WASI_RIGHTS_POLL_FD_READWRITE + | __WASI_RIGHTS_SOCK_SHUTDOWN; +pub(crate) const RIGHTS_SOCKET_INHERITING: __wasi_rights_t = RIGHTS_ALL; + +// Operations that apply to TTYs. +pub(crate) const RIGHTS_TTY_BASE: __wasi_rights_t = __WASI_RIGHTS_FD_READ + | __WASI_RIGHTS_FD_FDSTAT_SET_FLAGS + | __WASI_RIGHTS_FD_WRITE + | __WASI_RIGHTS_FD_FILESTAT_GET + | __WASI_RIGHTS_POLL_FD_READWRITE; +#[allow(unused)] +pub(crate) const RIGHTS_TTY_INHERITING: __wasi_rights_t = 0; + +pub fn whence_to_str(whence: __wasi_whence_t) -> &'static str { + match whence { + __WASI_WHENCE_CUR => "__WASI_WHENCE_CUR", + __WASI_WHENCE_END => "__WASI_WHENCE_END", + __WASI_WHENCE_SET => "__WASI_WHENCE_SET", + other => panic!("Undefined whence value {:?}", other), + } +} + +pub const __WASI_DIRCOOKIE_START: __wasi_dircookie_t = 0; diff --git a/crates/wasi-common/src/old/snapshot_0/wasi32.rs b/crates/wasi-common/src/old/snapshot_0/wasi32.rs new file mode 100644 index 0000000000..9afc0fc5e9 --- /dev/null +++ b/crates/wasi-common/src/old/snapshot_0/wasi32.rs @@ -0,0 +1,15 @@ +//! Types and constants specific to 32-bit wasi. These are similar to the types +//! in the `host` module, but pointers and `usize` values are replaced with +//! `u32`-sized types. + +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(dead_code)] + +use crate::old::snapshot_0::wasi::*; +use wig::witx_wasi32_types; + +pub type uintptr_t = u32; +pub type size_t = u32; + +witx_wasi32_types!("old/snapshot_0" "wasi_unstable"); diff --git a/crates/wasi-common/src/sandboxed_tty_writer.rs b/crates/wasi-common/src/sandboxed_tty_writer.rs new file mode 100644 index 0000000000..3152926157 --- /dev/null +++ b/crates/wasi-common/src/sandboxed_tty_writer.rs @@ -0,0 +1,199 @@ +use std::io::{IoSlice, Result, Write}; + +/// An adapter around a `Write` stream that guarantees that its output +/// is valid UTF-8 and contains no control characters. It does this by +/// replacing characters with inert control pictures and replacement +/// characters. +pub(crate) struct SandboxedTTYWriter<'writer, Writer> +where + Writer: Write, +{ + inner: &'writer mut Writer, +} + +impl<'writer, Writer> SandboxedTTYWriter<'writer, Writer> +where + Writer: Write, +{ + /// Construct a new `SandboxedTTYWriter` with the given inner `Writer`. + pub(crate) fn new(inner: &'writer mut Writer) -> Self { + Self { inner } + } + + /// Write a single character to the output. + pub(crate) fn write_char(&mut self, c: char) -> Result<()> { + self.inner.write( + match c { + '\u{0000}' => '␀', + '\u{0001}' => '␁', + '\u{0002}' => '␂', + '\u{0003}' => '␃', + '\u{0004}' => '␄', + '\u{0005}' => '␅', + '\u{0006}' => '␆', + '\u{0007}' => '␇', + '\u{0008}' => '␈', + '\u{0009}' => '\t', + '\u{000A}' => '\n', + '\u{000B}' => '␋', + '\u{000C}' => '␌', + '\u{000D}' => '\r', + '\u{000E}' => '␎', + '\u{000F}' => '␏', + '\u{0010}' => '␐', + '\u{0011}' => '␑', + '\u{0012}' => '␒', + '\u{0013}' => '␓', + '\u{0014}' => '␔', + '\u{0015}' => '␕', + '\u{0016}' => '␖', + '\u{0017}' => '␗', + '\u{0018}' => '␘', + '\u{0019}' => '␙', + '\u{001A}' => '␚', + '\u{001B}' => '␛', + '\u{001C}' => '␜', + '\u{001D}' => '␝', + '\u{001E}' => '␞', + '\u{001F}' => '␟', + '\u{007F}' => '␡', + x if x.is_control() => '�', + x => x, + } + .encode_utf8(&mut [0; 4]) // UTF-8 encoding of a `char` is at most 4 bytes. + .as_bytes(), + )?; + + Ok(()) + } + + /// Write a string to the output. + pub(crate) fn write_str(&mut self, s: &str) -> Result { + let mut result = 0; + + for c in s.chars() { + self.write_char(c)?; + // Note that we use the encoding length of the given char, rather than + // how many bytes we actually wrote, because our users don't know about + // what's really being written. + result += c.len_utf8(); + } + + Ok(result) + } +} + +impl<'writer, Writer> Write for SandboxedTTYWriter<'writer, Writer> +where + Writer: Write, +{ + fn write(&mut self, buf: &[u8]) -> Result { + let mut input = buf; + let mut result = 0; + + // Decode the string without heap-allocating it. See the example here + // for more details: + // https://doc.rust-lang.org/std/str/struct.Utf8Error.html#examples + loop { + match std::str::from_utf8(input) { + Ok(valid) => { + result += self.write_str(valid)?; + break; + } + Err(error) => { + let (valid, after_valid) = input.split_at(error.valid_up_to()); + result += self.write_str(unsafe { std::str::from_utf8_unchecked(valid) })?; + self.write_char('�')?; + + if let Some(invalid_sequence_length) = error.error_len() { + // An invalid sequence was encountered. Tell the application we've + // written those bytes (though actually, we replaced them with U+FFFD). + result += invalid_sequence_length; + // Set up `input` to resume writing after the end of the sequence. + input = &after_valid[invalid_sequence_length..]; + } else { + // The end of the buffer was encountered unexpectedly. Tell the application + // we've written out the remainder of the buffer. + result += after_valid.len(); + break; + } + } + } + } + + return Ok(result); + } + + fn write_vectored(&mut self, bufs: &[IoSlice]) -> Result { + // Terminal output is [not expected to be atomic], so just write all the + // individual buffers in sequence. + // + // [not expected to be atomic]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/read.html#tag_16_474_08 + let mut total_written = 0; + + for buf in bufs { + let written = self.write(buf)?; + + total_written += written; + + // Stop at the first point where the OS writes less than we asked. + if written < buf.len() { + break; + } + } + + Ok(total_written) + } + + fn flush(&mut self) -> Result<()> { + self.inner.flush() + } +} + +#[cfg(test)] +mod tests { + use super::SandboxedTTYWriter; + use std::io::{Result, Write}; + + #[test] + fn basic() -> Result<()> { + let mut buffer = Vec::new(); + let mut safe = SandboxedTTYWriter::new(&mut buffer); + safe.write_str("a\0b\u{0080}")?; + safe.write_char('\u{0007}')?; + safe.write(&[0x80])?; + safe.write(&[0xed, 0xa0, 0x80, 0xff, 0xfe])?; + assert_eq!( + buffer, + "a\u{2400}b\u{FFFD}\u{2407}\u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}".as_bytes() + ); + Ok(()) + } + + #[test] + fn how_many_replacements() -> Result<()> { + // See https://hsivonen.fi/broken-utf-8/ for background. + + let mut buffer = Vec::new(); + let mut safe = SandboxedTTYWriter::new(&mut buffer); + safe.write(&[0x80, 0x80, 0x80, 0x80])?; + assert_eq!(buffer, "\u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}".as_bytes()); + + let mut buffer = Vec::new(); + let mut safe = SandboxedTTYWriter::new(&mut buffer); + safe.write(&[0xF0, 0x80, 0x80, 0x41])?; + assert_eq!(buffer, "\u{FFFD}\u{FFFD}\u{FFFD}A".as_bytes()); + + let mut buffer = Vec::new(); + let mut safe = SandboxedTTYWriter::new(&mut buffer); + safe.write(&[0xF0, 0x80, 0x80])?; + assert_eq!(buffer, "\u{FFFD}\u{FFFD}\u{FFFD}".as_bytes()); + + let mut buffer = Vec::new(); + let mut safe = SandboxedTTYWriter::new(&mut buffer); + safe.write(&[0xF4, 0x80, 0x80, 0xC0])?; + assert_eq!(buffer, "\u{FFFD}\u{FFFD}".as_bytes()); + + Ok(()) + } +} diff --git a/crates/wasi-common/src/sys/mod.rs b/crates/wasi-common/src/sys/mod.rs new file mode 100644 index 0000000000..849fb7ee16 --- /dev/null +++ b/crates/wasi-common/src/sys/mod.rs @@ -0,0 +1,15 @@ +use cfg_if::cfg_if; + +cfg_if! { + if #[cfg(unix)] { + mod unix; + pub(crate) use unix::*; + pub use unix::preopen_dir; + } else if #[cfg(windows)] { + mod windows; + pub(crate) use windows::*; + pub use windows::preopen_dir; + } else { + compile_error!("wasi-common doesn't compile for this platform yet"); + } +} diff --git a/crates/wasi-common/src/sys/unix/bsd/filetime.rs b/crates/wasi-common/src/sys/unix/bsd/filetime.rs new file mode 100644 index 0000000000..4fb40c926b --- /dev/null +++ b/crates/wasi-common/src/sys/unix/bsd/filetime.rs @@ -0,0 +1,103 @@ +//! This internal module consists of helper types and functions for dealing +//! with setting the file times specific to BSD-style *nixes. +use crate::{sys::unix::filetime::FileTime, Result}; +use cfg_if::cfg_if; +use std::ffi::CStr; +use std::fs::File; +use std::io; +use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; + +cfg_if! { + if #[cfg(any( + target_os = "macos", + target_os = "freebsd", + target_os = "ios", + target_os = "dragonfly" + ))] { + pub(crate) const UTIME_NOW: i64 = -1; + pub(crate) const UTIME_OMIT: i64 = -2; + } else if #[cfg(target_os = "openbsd")] { + // These are swapped compared to macos, freebsd, ios, and dragonfly. + // https://github.com/openbsd/src/blob/master/sys/sys/stat.h#L187 + pub(crate) const UTIME_NOW: i64 = -2; + pub(crate) const UTIME_OMIT: i64 = -1; + } else if #[cfg(target_os = "netbsd" )] { + // These are the same as for Linux. + // http://cvsweb.netbsd.org/bsdweb.cgi/src/sys/sys/stat.h?rev=1.69&content-type=text/x-cvsweb-markup&only_with_tag=MAIN + pub(crate) const UTIME_NOW: i64 = 1_073_741_823; + pub(crate) const UTIME_OMIT: i64 = 1_073_741_822; + } +} + +/// Wrapper for `utimensat` syscall, however, with an added twist such that `utimensat` symbol +/// is firstly resolved (i.e., we check whether it exists on the host), and only used if that is +/// the case. Otherwise, the syscall resorts to a less accurate `utimesat` emulated syscall. +/// The original implementation can be found here: [filetime::unix::macos::set_times] +/// +/// [filetime::unix::macos::set_times]: https://github.com/alexcrichton/filetime/blob/master/src/unix/macos.rs#L49 +pub(crate) fn utimensat( + dirfd: &File, + path: &str, + atime: FileTime, + mtime: FileTime, + symlink_nofollow: bool, +) -> Result<()> { + use crate::sys::unix::filetime::to_timespec; + use std::ffi::CString; + use std::os::unix::prelude::*; + + // Attempt to use the `utimensat` syscall, but if it's not supported by the + // current kernel then fall back to an older syscall. + if let Some(func) = fetch_utimensat() { + let flags = if symlink_nofollow { + libc::AT_SYMLINK_NOFOLLOW + } else { + 0 + }; + + let p = CString::new(path.as_bytes())?; + let times = [to_timespec(&atime)?, to_timespec(&mtime)?]; + let rc = unsafe { func(dirfd.as_raw_fd(), p.as_ptr(), times.as_ptr(), flags) }; + if rc == 0 { + return Ok(()); + } else { + return Err(io::Error::last_os_error().into()); + } + } + + super::utimesat::utimesat(dirfd, path, atime, mtime, symlink_nofollow) +} + +/// Wraps `fetch` specifically targetting `utimensat` symbol. If the symbol exists +/// on the host, then returns an `Some(unsafe fn)`. +fn fetch_utimensat() -> Option< + unsafe extern "C" fn( + libc::c_int, + *const libc::c_char, + *const libc::timespec, + libc::c_int, + ) -> libc::c_int, +> { + static ADDR: AtomicUsize = AtomicUsize::new(0); + unsafe { + fetch(&ADDR, CStr::from_bytes_with_nul_unchecked(b"utimensat\0")) + .map(|sym| std::mem::transmute(sym)) + } +} + +/// Fetches a symbol by `name` and stores it in `cache`. +fn fetch(cache: &AtomicUsize, name: &CStr) -> Option { + match cache.load(SeqCst) { + 0 => {} + 1 => return None, + n => return Some(n), + } + let sym = unsafe { libc::dlsym(libc::RTLD_DEFAULT, name.as_ptr() as *const _) }; + let (val, ret) = if sym.is_null() { + (1, None) + } else { + (sym as usize, Some(sym as usize)) + }; + cache.store(val, SeqCst); + return ret; +} diff --git a/crates/wasi-common/src/sys/unix/bsd/host_impl.rs b/crates/wasi-common/src/sys/unix/bsd/host_impl.rs new file mode 100644 index 0000000000..53f0782deb --- /dev/null +++ b/crates/wasi-common/src/sys/unix/bsd/host_impl.rs @@ -0,0 +1,12 @@ +use crate::{wasi, Result}; +use std::convert::TryFrom; + +pub(crate) const O_RSYNC: yanix::file::OFlag = yanix::file::OFlag::SYNC; + +pub(crate) fn stdev_from_nix(dev: libc::dev_t) -> Result { + wasi::__wasi_device_t::try_from(dev).map_err(Into::into) +} + +pub(crate) fn stino_from_nix(ino: libc::ino_t) -> Result { + wasi::__wasi_device_t::try_from(ino).map_err(Into::into) +} diff --git a/crates/wasi-common/src/sys/unix/bsd/hostcalls_impl.rs b/crates/wasi-common/src/sys/unix/bsd/hostcalls_impl.rs new file mode 100644 index 0000000000..b939e8257a --- /dev/null +++ b/crates/wasi-common/src/sys/unix/bsd/hostcalls_impl.rs @@ -0,0 +1,169 @@ +use crate::hostcalls_impl::PathGet; +use crate::{Error, Result}; +use std::os::unix::prelude::AsRawFd; + +pub(crate) fn path_unlink_file(resolved: PathGet) -> Result<()> { + use yanix::{ + file::{unlinkat, AtFlag}, + Errno, YanixError, + }; + unsafe { + unlinkat( + resolved.dirfd().as_raw_fd(), + resolved.path(), + AtFlag::empty(), + ) + } + .map_err(|err| { + if let YanixError::Errno(mut errno) = err { + // Non-Linux implementations may return EPERM when attempting to remove a + // directory without REMOVEDIR. While that's what POSIX specifies, it's + // less useful. Adjust this to EISDIR. It doesn't matter that this is not + // atomic with the unlinkat, because if the file is removed and a directory + // is created before fstatat sees it, we're racing with that change anyway + // and unlinkat could have legitimately seen the directory if the race had + // turned out differently. + use yanix::file::{fstatat, FileType}; + + if errno == Errno::EPERM { + if let Ok(stat) = unsafe { + fstatat( + resolved.dirfd().as_raw_fd(), + resolved.path(), + AtFlag::SYMLINK_NOFOLLOW, + ) + } { + if FileType::from_stat_st_mode(stat.st_mode) == FileType::Directory { + errno = Errno::EISDIR; + } + } else { + errno = Errno::last(); + } + } + errno.into() + } else { + err + } + }) + .map_err(Into::into) +} + +pub(crate) fn path_symlink(old_path: &str, resolved: PathGet) -> Result<()> { + use yanix::{ + file::{fstatat, symlinkat, AtFlag}, + Errno, YanixError, + }; + + log::debug!("path_symlink old_path = {:?}", old_path); + log::debug!("path_symlink resolved = {:?}", resolved); + + unsafe { symlinkat(old_path, resolved.dirfd().as_raw_fd(), resolved.path()) }.or_else(|err| { + if let YanixError::Errno(errno) = err { + match errno { + Errno::ENOTDIR => { + // On BSD, symlinkat returns ENOTDIR when it should in fact + // return a EEXIST. It seems that it gets confused with by + // the trailing slash in the target path. Thus, we strip + // the trailing slash and check if the path exists, and + // adjust the error code appropriately. + let new_path = resolved.path().trim_end_matches('/'); + if let Ok(_) = unsafe { + fstatat( + resolved.dirfd().as_raw_fd(), + new_path, + AtFlag::SYMLINK_NOFOLLOW, + ) + } { + Err(Error::EEXIST) + } else { + Err(Error::ENOTDIR) + } + } + x => Err(x.into()), + } + } else { + Err(err.into()) + } + }) +} + +pub(crate) fn path_rename(resolved_old: PathGet, resolved_new: PathGet) -> Result<()> { + use yanix::{ + file::{fstatat, renameat, AtFlag}, + Errno, YanixError, + }; + unsafe { + renameat( + resolved_old.dirfd().as_raw_fd(), + resolved_old.path(), + resolved_new.dirfd().as_raw_fd(), + resolved_new.path(), + ) + } + .or_else(|err| { + // Currently, this is verified to be correct on macOS, where + // ENOENT can be returned in case when we try to rename a file + // into a name with a trailing slash. On macOS, if the latter does + // not exist, an ENOENT is thrown, whereas on Linux we observe the + // correct behaviour of throwing an ENOTDIR since the destination is + // indeed not a directory. + // + // TODO + // Verify on other BSD-based OSes. + if let YanixError::Errno(errno) = err { + match errno { + Errno::ENOENT => { + // check if the source path exists + if let Ok(_) = unsafe { + fstatat( + resolved_old.dirfd().as_raw_fd(), + resolved_old.path(), + AtFlag::SYMLINK_NOFOLLOW, + ) + } { + // check if destination contains a trailing slash + if resolved_new.path().contains('/') { + Err(Error::ENOTDIR) + } else { + Err(Error::ENOENT) + } + } else { + Err(Error::ENOENT) + } + } + x => Err(x.into()), + } + } else { + Err(err.into()) + } + }) +} + +pub(crate) mod fd_readdir_impl { + use crate::sys::fdentry_impl::OsHandle; + use crate::Result; + use std::sync::{Mutex, MutexGuard}; + use yanix::dir::Dir; + + pub(crate) fn get_dir_from_os_handle<'a>( + os_handle: &'a mut OsHandle, + ) -> Result> { + let dir = match os_handle.dir { + Some(ref mut dir) => dir, + None => { + // We need to duplicate the fd, because `opendir(3)`: + // Upon successful return from fdopendir(), the file descriptor is under + // control of the system, and if any attempt is made to close the file + // descriptor, or to modify the state of the associated description other + // than by means of closedir(), readdir(), readdir_r(), or rewinddir(), + // the behaviour is undefined. + let fd = (*os_handle).try_clone()?; + let dir = Dir::from(fd)?; + os_handle.dir.get_or_insert(Mutex::new(dir)) + } + }; + // Note that from this point on, until the end of the parent scope (i.e., enclosing this + // function), we're locking the `Dir` member of this `OsHandle`. + Ok(dir.lock().unwrap()) + } +} diff --git a/crates/wasi-common/src/sys/unix/bsd/mod.rs b/crates/wasi-common/src/sys/unix/bsd/mod.rs new file mode 100644 index 0000000000..39a4046a74 --- /dev/null +++ b/crates/wasi-common/src/sys/unix/bsd/mod.rs @@ -0,0 +1,6 @@ +pub(crate) mod filetime; +pub(crate) mod host_impl; +pub(crate) mod hostcalls_impl; +pub(crate) mod oshandle; +#[path = "../linux/utimesat.rs"] +pub(crate) mod utimesat; diff --git a/crates/wasi-common/src/sys/unix/bsd/oshandle.rs b/crates/wasi-common/src/sys/unix/bsd/oshandle.rs new file mode 100644 index 0000000000..70d1a8c4e8 --- /dev/null +++ b/crates/wasi-common/src/sys/unix/bsd/oshandle.rs @@ -0,0 +1,48 @@ +use std::fs; +use std::ops::{Deref, DerefMut}; +use std::os::unix::prelude::{AsRawFd, RawFd}; +use std::sync::Mutex; +use yanix::dir::Dir; + +#[derive(Debug)] +pub(crate) struct OsHandle { + pub(crate) file: fs::File, + // In case that this `OsHandle` actually refers to a directory, + // when the client makes a `fd_readdir` syscall on this descriptor, + // we will need to cache the `libc::DIR` pointer manually in order + // to be able to seek on it later. While on Linux, this is handled + // by the OS, BSD Unixes require the client to do this caching. + // + // This comes directly from the BSD man pages on `readdir`: + // > Values returned by telldir() are good only for the lifetime + // > of the DIR pointer, dirp, from which they are derived. + // > If the directory is closed and then reopened, prior values + // > returned by telldir() will no longer be valid. + pub(crate) dir: Option>, +} + +impl From for OsHandle { + fn from(file: fs::File) -> Self { + Self { file, dir: None } + } +} + +impl AsRawFd for OsHandle { + fn as_raw_fd(&self) -> RawFd { + self.file.as_raw_fd() + } +} + +impl Deref for OsHandle { + type Target = fs::File; + + fn deref(&self) -> &Self::Target { + &self.file + } +} + +impl DerefMut for OsHandle { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.file + } +} diff --git a/crates/wasi-common/src/sys/unix/emscripten/filetime.rs b/crates/wasi-common/src/sys/unix/emscripten/filetime.rs new file mode 100644 index 0000000000..08f7c24a79 --- /dev/null +++ b/crates/wasi-common/src/sys/unix/emscripten/filetime.rs @@ -0,0 +1,36 @@ +//! This internal module consists of helper types and functions for dealing +//! with setting the file times specific to Emscripten. +use crate::{sys::unix::filetime::FileTime, Result}; +use std::fs::File; +use std::io; + +pub(crate) const UTIME_NOW: i32 = 1_073_741_823; +pub(crate) const UTIME_OMIT: i32 = 1_073_741_822; + +/// Wrapper for `utimensat` syscall. In Emscripten, there is no point in dynamically resolving +/// if `utimensat` is available as it always was and will be. +pub(crate) fn utimensat( + dirfd: &File, + path: &str, + atime: FileTime, + mtime: FileTime, + symlink_nofollow: bool, +) -> Result<()> { + use crate::sys::unix::filetime::to_timespec; + use std::ffi::CString; + use std::os::unix::prelude::*; + + let flags = if symlink_nofollow { + libc::AT_SYMLINK_NOFOLLOW + } else { + 0 + }; + let p = CString::new(path.as_bytes())?; + let times = [to_timespec(&atime)?, to_timespec(&mtime)?]; + let rc = unsafe { libc::utimensat(dirfd.as_raw_fd(), p.as_ptr(), times.as_ptr(), flags) }; + if rc == 0 { + Ok(()) + } else { + Err(io::Error::last_os_error().into()) + } +} diff --git a/crates/wasi-common/src/sys/unix/emscripten/mod.rs b/crates/wasi-common/src/sys/unix/emscripten/mod.rs new file mode 100644 index 0000000000..82a7ed93a8 --- /dev/null +++ b/crates/wasi-common/src/sys/unix/emscripten/mod.rs @@ -0,0 +1,7 @@ +pub(crate) mod filetime; +#[path = "../linux/host_impl.rs"] +pub(crate) mod host_impl; +#[path = "../linux/hostcalls_impl.rs"] +pub(crate) mod hostcalls_impl; +#[path = "../linux/oshandle.rs"] +pub(crate) mod oshandle; diff --git a/crates/wasi-common/src/sys/unix/fdentry_impl.rs b/crates/wasi-common/src/sys/unix/fdentry_impl.rs new file mode 100644 index 0000000000..6efd681cb2 --- /dev/null +++ b/crates/wasi-common/src/sys/unix/fdentry_impl.rs @@ -0,0 +1,135 @@ +use crate::fdentry::{Descriptor, OsHandleRef}; +use crate::{sys::unix::sys_impl, wasi, Error, Result}; +use std::fs::File; +use std::io; +use std::mem::ManuallyDrop; +use std::os::unix::prelude::{AsRawFd, FileTypeExt, FromRawFd, RawFd}; + +pub(crate) use sys_impl::oshandle::*; + +impl AsRawFd for Descriptor { + fn as_raw_fd(&self) -> RawFd { + match self { + Self::OsHandle(file) => file.as_raw_fd(), + Self::Stdin => io::stdin().as_raw_fd(), + Self::Stdout => io::stdout().as_raw_fd(), + Self::Stderr => io::stderr().as_raw_fd(), + } + } +} + +pub(crate) fn descriptor_as_oshandle<'lifetime>( + desc: &'lifetime Descriptor, +) -> OsHandleRef<'lifetime> { + OsHandleRef::new(ManuallyDrop::new(OsHandle::from(unsafe { + File::from_raw_fd(desc.as_raw_fd()) + }))) +} + +/// Returns the set of all possible rights that are both relevant for the file +/// type and consistent with the open mode. +/// +/// This function is unsafe because it operates on a raw file descriptor. +pub(crate) unsafe fn determine_type_and_access_rights( + fd: &Fd, +) -> Result<( + wasi::__wasi_filetype_t, + wasi::__wasi_rights_t, + wasi::__wasi_rights_t, +)> { + let (file_type, mut rights_base, rights_inheriting) = determine_type_rights(fd)?; + + use yanix::{fcntl, file::OFlag}; + let flags = fcntl::get_status_flags(fd.as_raw_fd())?; + let accmode = flags & OFlag::ACCMODE; + if accmode == OFlag::RDONLY { + rights_base &= !wasi::__WASI_RIGHTS_FD_WRITE; + } else if accmode == OFlag::WRONLY { + rights_base &= !wasi::__WASI_RIGHTS_FD_READ; + } + + Ok((file_type, rights_base, rights_inheriting)) +} + +/// Returns the set of all possible rights that are relevant for file type. +/// +/// This function is unsafe because it operates on a raw file descriptor. +pub(crate) unsafe fn determine_type_rights( + fd: &Fd, +) -> Result<( + wasi::__wasi_filetype_t, + wasi::__wasi_rights_t, + wasi::__wasi_rights_t, +)> { + let (file_type, rights_base, rights_inheriting) = { + // we just make a `File` here for convenience; we don't want it to close when it drops + let file = std::mem::ManuallyDrop::new(std::fs::File::from_raw_fd(fd.as_raw_fd())); + let ft = file.metadata()?.file_type(); + if ft.is_block_device() { + log::debug!("Host fd {:?} is a block device", fd.as_raw_fd()); + ( + wasi::__WASI_FILETYPE_BLOCK_DEVICE, + wasi::RIGHTS_BLOCK_DEVICE_BASE, + wasi::RIGHTS_BLOCK_DEVICE_INHERITING, + ) + } else if ft.is_char_device() { + log::debug!("Host fd {:?} is a char device", fd.as_raw_fd()); + use yanix::file::isatty; + if isatty(fd.as_raw_fd())? { + ( + wasi::__WASI_FILETYPE_CHARACTER_DEVICE, + wasi::RIGHTS_TTY_BASE, + wasi::RIGHTS_TTY_BASE, + ) + } else { + ( + wasi::__WASI_FILETYPE_CHARACTER_DEVICE, + wasi::RIGHTS_CHARACTER_DEVICE_BASE, + wasi::RIGHTS_CHARACTER_DEVICE_INHERITING, + ) + } + } else if ft.is_dir() { + log::debug!("Host fd {:?} is a directory", fd.as_raw_fd()); + ( + wasi::__WASI_FILETYPE_DIRECTORY, + wasi::RIGHTS_DIRECTORY_BASE, + wasi::RIGHTS_DIRECTORY_INHERITING, + ) + } else if ft.is_file() { + log::debug!("Host fd {:?} is a file", fd.as_raw_fd()); + ( + wasi::__WASI_FILETYPE_REGULAR_FILE, + wasi::RIGHTS_REGULAR_FILE_BASE, + wasi::RIGHTS_REGULAR_FILE_INHERITING, + ) + } else if ft.is_socket() { + log::debug!("Host fd {:?} is a socket", fd.as_raw_fd()); + use yanix::socket::{get_socket_type, SockType}; + match get_socket_type(fd.as_raw_fd())? { + SockType::Datagram => ( + wasi::__WASI_FILETYPE_SOCKET_DGRAM, + wasi::RIGHTS_SOCKET_BASE, + wasi::RIGHTS_SOCKET_INHERITING, + ), + SockType::Stream => ( + wasi::__WASI_FILETYPE_SOCKET_STREAM, + wasi::RIGHTS_SOCKET_BASE, + wasi::RIGHTS_SOCKET_INHERITING, + ), + _ => return Err(Error::EINVAL), + } + } else if ft.is_fifo() { + log::debug!("Host fd {:?} is a fifo", fd.as_raw_fd()); + ( + wasi::__WASI_FILETYPE_UNKNOWN, + wasi::RIGHTS_REGULAR_FILE_BASE, + wasi::RIGHTS_REGULAR_FILE_INHERITING, + ) + } else { + log::debug!("Host fd {:?} is unknown", fd.as_raw_fd()); + return Err(Error::EINVAL); + } + }; + + Ok((file_type, rights_base, rights_inheriting)) +} diff --git a/crates/wasi-common/src/sys/unix/filetime.rs b/crates/wasi-common/src/sys/unix/filetime.rs new file mode 100644 index 0000000000..9a70740a36 --- /dev/null +++ b/crates/wasi-common/src/sys/unix/filetime.rs @@ -0,0 +1,67 @@ +//! This internal module consists of helper types and functions for dealing +//! with setting the file times (mainly in `path_filestat_set_times` syscall for now). +//! +//! The vast majority of the code contained within and in platform-specific implementations +//! (`super::linux::filetime` and `super::bsd::filetime`) is based on the [filetime] crate. +//! Kudos @alexcrichton! +//! +//! [filetime]: https://github.com/alexcrichton/filetime +use crate::Result; +use std::convert::TryInto; + +pub(crate) use super::sys_impl::filetime::*; + +cfg_if::cfg_if! { + if #[cfg(not(target_os = "emscripten"))] { + fn filetime_to_timespec(ft: &filetime::FileTime) -> Result { + Ok( + libc::timespec { + tv_sec: ft.seconds(), + tv_nsec: ft.nanoseconds().try_into()?, + } + ) + } + } else { + fn filetime_to_timespec(ft: &filetime::FileTime) -> Result { + Ok( + libc::timespec { + tv_sec: ft.seconds().try_into()?, + tv_nsec: ft.nanoseconds().try_into()?, + } + ) + } + } +} + +/// A wrapper `enum` around `filetime::FileTime` struct, but unlike the original, this +/// type allows the possibility of specifying `FileTime::Now` as a valid enumeration which, +/// in turn, if `utimensat` is available on the host, will use a special const setting +/// `UTIME_NOW`. +#[derive(Debug, Copy, Clone)] +pub(crate) enum FileTime { + Now, + Omit, + FileTime(filetime::FileTime), +} + +/// Converts `FileTime` to `libc::timespec`. If `FileTime::Now` variant is specified, this +/// resolves to `UTIME_NOW` special const, `FileTime::Omit` variant resolves to `UTIME_OMIT`, and +/// `FileTime::FileTime(ft)` where `ft := filetime::FileTime` uses [filetime] crate's original +/// implementation which can be found here: [filetime::unix::to_timespec]. +/// +/// [filetime]: https://github.com/alexcrichton/filetime +/// [filetime::unix::to_timespec]: https://github.com/alexcrichton/filetime/blob/master/src/unix/mod.rs#L30 +pub(crate) fn to_timespec(ft: &FileTime) -> Result { + let ts = match ft { + FileTime::Now => libc::timespec { + tv_sec: 0, + tv_nsec: UTIME_NOW, + }, + FileTime::Omit => libc::timespec { + tv_sec: 0, + tv_nsec: UTIME_OMIT, + }, + FileTime::FileTime(ft) => filetime_to_timespec(ft)?, + }; + Ok(ts) +} diff --git a/crates/wasi-common/src/sys/unix/host_impl.rs b/crates/wasi-common/src/sys/unix/host_impl.rs new file mode 100644 index 0000000000..f06727fc5f --- /dev/null +++ b/crates/wasi-common/src/sys/unix/host_impl.rs @@ -0,0 +1,220 @@ +//! WASI host types specific to *nix host. +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(dead_code)] +use crate::host::FileType; +use crate::{error::FromRawOsError, helpers, sys::unix::sys_impl, wasi, Error, Result}; +use std::ffi::OsStr; +use std::os::unix::prelude::OsStrExt; +use yanix::{file::OFlag, Errno}; + +pub(crate) use sys_impl::host_impl::*; + +impl FromRawOsError for Error { + fn from_raw_os_error(code: i32) -> Self { + Self::from(Errno::from_i32(code)) + } +} + +impl From for Error { + fn from(errno: Errno) -> Self { + match errno { + Errno::EPERM => Self::EPERM, + Errno::ENOENT => Self::ENOENT, + Errno::ESRCH => Self::ESRCH, + Errno::EINTR => Self::EINTR, + Errno::EIO => Self::EIO, + Errno::ENXIO => Self::ENXIO, + Errno::E2BIG => Self::E2BIG, + Errno::ENOEXEC => Self::ENOEXEC, + Errno::EBADF => Self::EBADF, + Errno::ECHILD => Self::ECHILD, + Errno::EAGAIN => Self::EAGAIN, + Errno::ENOMEM => Self::ENOMEM, + Errno::EACCES => Self::EACCES, + Errno::EFAULT => Self::EFAULT, + Errno::EBUSY => Self::EBUSY, + Errno::EEXIST => Self::EEXIST, + Errno::EXDEV => Self::EXDEV, + Errno::ENODEV => Self::ENODEV, + Errno::ENOTDIR => Self::ENOTDIR, + Errno::EISDIR => Self::EISDIR, + Errno::EINVAL => Self::EINVAL, + Errno::ENFILE => Self::ENFILE, + Errno::EMFILE => Self::EMFILE, + Errno::ENOTTY => Self::ENOTTY, + Errno::ETXTBSY => Self::ETXTBSY, + Errno::EFBIG => Self::EFBIG, + Errno::ENOSPC => Self::ENOSPC, + Errno::ESPIPE => Self::ESPIPE, + Errno::EROFS => Self::EROFS, + Errno::EMLINK => Self::EMLINK, + Errno::EPIPE => Self::EPIPE, + Errno::EDOM => Self::EDOM, + Errno::ERANGE => Self::ERANGE, + Errno::EDEADLK => Self::EDEADLK, + Errno::ENAMETOOLONG => Self::ENAMETOOLONG, + Errno::ENOLCK => Self::ENOLCK, + Errno::ENOSYS => Self::ENOSYS, + Errno::ENOTEMPTY => Self::ENOTEMPTY, + Errno::ELOOP => Self::ELOOP, + Errno::ENOMSG => Self::ENOMSG, + Errno::EIDRM => Self::EIDRM, + Errno::ENOLINK => Self::ENOLINK, + Errno::EPROTO => Self::EPROTO, + Errno::EMULTIHOP => Self::EMULTIHOP, + Errno::EBADMSG => Self::EBADMSG, + Errno::EOVERFLOW => Self::EOVERFLOW, + Errno::EILSEQ => Self::EILSEQ, + Errno::ENOTSOCK => Self::ENOTSOCK, + Errno::EDESTADDRREQ => Self::EDESTADDRREQ, + Errno::EMSGSIZE => Self::EMSGSIZE, + Errno::EPROTOTYPE => Self::EPROTOTYPE, + Errno::ENOPROTOOPT => Self::ENOPROTOOPT, + Errno::EPROTONOSUPPORT => Self::EPROTONOSUPPORT, + Errno::EAFNOSUPPORT => Self::EAFNOSUPPORT, + Errno::EADDRINUSE => Self::EADDRINUSE, + Errno::EADDRNOTAVAIL => Self::EADDRNOTAVAIL, + Errno::ENETDOWN => Self::ENETDOWN, + Errno::ENETUNREACH => Self::ENETUNREACH, + Errno::ENETRESET => Self::ENETRESET, + Errno::ECONNABORTED => Self::ECONNABORTED, + Errno::ECONNRESET => Self::ECONNRESET, + Errno::ENOBUFS => Self::ENOBUFS, + Errno::EISCONN => Self::EISCONN, + Errno::ENOTCONN => Self::ENOTCONN, + Errno::ETIMEDOUT => Self::ETIMEDOUT, + Errno::ECONNREFUSED => Self::ECONNREFUSED, + Errno::EHOSTUNREACH => Self::EHOSTUNREACH, + Errno::EALREADY => Self::EALREADY, + Errno::EINPROGRESS => Self::EINPROGRESS, + Errno::ESTALE => Self::ESTALE, + Errno::EDQUOT => Self::EDQUOT, + Errno::ECANCELED => Self::ECANCELED, + Errno::EOWNERDEAD => Self::EOWNERDEAD, + Errno::ENOTRECOVERABLE => Self::ENOTRECOVERABLE, + } + } +} + +pub(crate) fn nix_from_fdflags(fdflags: wasi::__wasi_fdflags_t) -> OFlag { + let mut nix_flags = OFlag::empty(); + if fdflags & wasi::__WASI_FDFLAGS_APPEND != 0 { + nix_flags.insert(OFlag::APPEND); + } + if fdflags & wasi::__WASI_FDFLAGS_DSYNC != 0 { + nix_flags.insert(OFlag::DSYNC); + } + if fdflags & wasi::__WASI_FDFLAGS_NONBLOCK != 0 { + nix_flags.insert(OFlag::NONBLOCK); + } + if fdflags & wasi::__WASI_FDFLAGS_RSYNC != 0 { + nix_flags.insert(O_RSYNC); + } + if fdflags & wasi::__WASI_FDFLAGS_SYNC != 0 { + nix_flags.insert(OFlag::SYNC); + } + nix_flags +} + +pub(crate) fn fdflags_from_nix(oflags: OFlag) -> wasi::__wasi_fdflags_t { + let mut fdflags = 0; + if oflags.contains(OFlag::APPEND) { + fdflags |= wasi::__WASI_FDFLAGS_APPEND; + } + if oflags.contains(OFlag::DSYNC) { + fdflags |= wasi::__WASI_FDFLAGS_DSYNC; + } + if oflags.contains(OFlag::NONBLOCK) { + fdflags |= wasi::__WASI_FDFLAGS_NONBLOCK; + } + if oflags.contains(O_RSYNC) { + fdflags |= wasi::__WASI_FDFLAGS_RSYNC; + } + if oflags.contains(OFlag::SYNC) { + fdflags |= wasi::__WASI_FDFLAGS_SYNC; + } + fdflags +} + +pub(crate) fn nix_from_oflags(oflags: wasi::__wasi_oflags_t) -> OFlag { + let mut nix_flags = OFlag::empty(); + if oflags & wasi::__WASI_OFLAGS_CREAT != 0 { + nix_flags.insert(OFlag::CREAT); + } + if oflags & wasi::__WASI_OFLAGS_DIRECTORY != 0 { + nix_flags.insert(OFlag::DIRECTORY); + } + if oflags & wasi::__WASI_OFLAGS_EXCL != 0 { + nix_flags.insert(OFlag::EXCL); + } + if oflags & wasi::__WASI_OFLAGS_TRUNC != 0 { + nix_flags.insert(OFlag::TRUNC); + } + nix_flags +} + +pub(crate) fn filestat_from_nix(filestat: libc::stat) -> Result { + use std::convert::TryInto; + + fn filestat_to_timestamp(secs: u64, nsecs: u64) -> Result { + secs.checked_mul(1_000_000_000) + .and_then(|sec_nsec| sec_nsec.checked_add(nsecs)) + .ok_or(Error::EOVERFLOW) + } + + let filetype = yanix::file::FileType::from_stat_st_mode(filestat.st_mode); + let dev = stdev_from_nix(filestat.st_dev)?; + let ino = stino_from_nix(filestat.st_ino)?; + let atim = filestat_to_timestamp( + filestat.st_atime.try_into()?, + filestat.st_atime_nsec.try_into()?, + )?; + let ctim = filestat_to_timestamp( + filestat.st_ctime.try_into()?, + filestat.st_ctime_nsec.try_into()?, + )?; + let mtim = filestat_to_timestamp( + filestat.st_mtime.try_into()?, + filestat.st_mtime_nsec.try_into()?, + )?; + + Ok(wasi::__wasi_filestat_t { + dev, + ino, + nlink: wasi::__wasi_linkcount_t::from(filestat.st_nlink), + size: filestat.st_size as wasi::__wasi_filesize_t, + atim, + ctim, + mtim, + filetype: FileType::from(filetype).to_wasi(), + }) +} + +/// Creates owned WASI path from OS string. +/// +/// NB WASI spec requires OS string to be valid UTF-8. Otherwise, +/// `__WASI_ERRNO_ILSEQ` error is returned. +pub(crate) fn path_from_host>(s: S) -> Result { + helpers::path_from_slice(s.as_ref().as_bytes()).map(String::from) +} + +impl From for FileType { + fn from(ft: yanix::file::FileType) -> Self { + use yanix::file::FileType::*; + match ft { + RegularFile => Self::RegularFile, + Symlink => Self::Symlink, + Directory => Self::Directory, + BlockDevice => Self::BlockDevice, + CharacterDevice => Self::CharacterDevice, + /* Unknown | Socket | Fifo */ + _ => Self::Unknown, + // TODO how to discriminate between STREAM and DGRAM? + // Perhaps, we should create a more general WASI filetype + // such as __WASI_FILETYPE_SOCKET, and then it would be + // up to the client to check whether it's actually + // STREAM or DGRAM? + } + } +} diff --git a/crates/wasi-common/src/sys/unix/hostcalls_impl/fs.rs b/crates/wasi-common/src/sys/unix/hostcalls_impl/fs.rs new file mode 100644 index 0000000000..a4f1fe9dd0 --- /dev/null +++ b/crates/wasi-common/src/sys/unix/hostcalls_impl/fs.rs @@ -0,0 +1,319 @@ +#![allow(non_camel_case_types)] +#![allow(unused_unsafe)] +use crate::host::Dirent; +use crate::hostcalls_impl::PathGet; +use crate::sys::{fdentry_impl::OsHandle, host_impl, unix::sys_impl}; +use crate::{wasi, Error, Result}; +use std::convert::TryInto; +use std::fs::File; +use std::os::unix::fs::FileExt; +use std::os::unix::prelude::{AsRawFd, FromRawFd}; + +pub(crate) use sys_impl::hostcalls_impl::*; + +pub(crate) fn fd_pread( + file: &File, + buf: &mut [u8], + offset: wasi::__wasi_filesize_t, +) -> Result { + file.read_at(buf, offset).map_err(Into::into) +} + +pub(crate) fn fd_pwrite(file: &File, buf: &[u8], offset: wasi::__wasi_filesize_t) -> Result { + file.write_at(buf, offset).map_err(Into::into) +} + +pub(crate) fn fd_fdstat_get(fd: &File) -> Result { + unsafe { yanix::fcntl::get_status_flags(fd.as_raw_fd()) } + .map(host_impl::fdflags_from_nix) + .map_err(Into::into) +} + +pub(crate) fn fd_fdstat_set_flags( + fd: &File, + fdflags: wasi::__wasi_fdflags_t, +) -> Result> { + let nix_flags = host_impl::nix_from_fdflags(fdflags); + unsafe { yanix::fcntl::set_status_flags(fd.as_raw_fd(), nix_flags) } + .map(|_| None) + .map_err(Into::into) +} + +pub(crate) fn fd_advise( + file: &File, + advice: wasi::__wasi_advice_t, + offset: wasi::__wasi_filesize_t, + len: wasi::__wasi_filesize_t, +) -> Result<()> { + use yanix::fadvise::{posix_fadvise, PosixFadviseAdvice}; + let offset = offset.try_into()?; + let len = len.try_into()?; + let host_advice = match advice { + wasi::__WASI_ADVICE_DONTNEED => PosixFadviseAdvice::DontNeed, + wasi::__WASI_ADVICE_SEQUENTIAL => PosixFadviseAdvice::Sequential, + wasi::__WASI_ADVICE_WILLNEED => PosixFadviseAdvice::WillNeed, + wasi::__WASI_ADVICE_NOREUSE => PosixFadviseAdvice::NoReuse, + wasi::__WASI_ADVICE_RANDOM => PosixFadviseAdvice::Random, + wasi::__WASI_ADVICE_NORMAL => PosixFadviseAdvice::Normal, + _ => return Err(Error::EINVAL), + }; + unsafe { posix_fadvise(file.as_raw_fd(), offset, len, host_advice) }.map_err(Into::into) +} + +pub(crate) fn path_create_directory(resolved: PathGet) -> Result<()> { + use yanix::file::{mkdirat, Mode}; + unsafe { + mkdirat( + resolved.dirfd().as_raw_fd(), + resolved.path(), + Mode::from_bits_truncate(0o777), + ) + } + .map_err(Into::into) +} + +pub(crate) fn path_link(resolved_old: PathGet, resolved_new: PathGet) -> Result<()> { + use yanix::file::{linkat, AtFlag}; + unsafe { + linkat( + resolved_old.dirfd().as_raw_fd(), + resolved_old.path(), + resolved_new.dirfd().as_raw_fd(), + resolved_new.path(), + AtFlag::SYMLINK_FOLLOW, + ) + } + .map_err(Into::into) +} + +pub(crate) fn path_open( + resolved: PathGet, + read: bool, + write: bool, + oflags: wasi::__wasi_oflags_t, + fs_flags: wasi::__wasi_fdflags_t, +) -> Result { + use yanix::{ + file::{fstatat, openat, AtFlag, FileType, Mode, OFlag}, + Errno, + }; + + let mut nix_all_oflags = if read && write { + OFlag::RDWR + } else if write { + OFlag::WRONLY + } else { + OFlag::RDONLY + }; + + // on non-Capsicum systems, we always want nofollow + nix_all_oflags.insert(OFlag::NOFOLLOW); + + // convert open flags + nix_all_oflags.insert(host_impl::nix_from_oflags(oflags)); + + // convert file descriptor flags + nix_all_oflags.insert(host_impl::nix_from_fdflags(fs_flags)); + + // Call openat. Use mode 0o666 so that we follow whatever the user's + // umask is, but don't set the executable flag, because it isn't yet + // meaningful for WASI programs to create executable files. + + log::debug!("path_open resolved = {:?}", resolved); + log::debug!("path_open oflags = {:?}", nix_all_oflags); + + let new_fd = match unsafe { + openat( + resolved.dirfd().as_raw_fd(), + resolved.path(), + nix_all_oflags, + Mode::from_bits_truncate(0o666), + ) + } { + Ok(fd) => fd, + Err(e) => { + if let yanix::YanixError::Errno(errno) = e { + match errno { + // Linux returns ENXIO instead of EOPNOTSUPP when opening a socket + Errno::ENXIO => { + if let Ok(stat) = unsafe { + fstatat( + resolved.dirfd().as_raw_fd(), + resolved.path(), + AtFlag::SYMLINK_NOFOLLOW, + ) + } { + if FileType::from_stat_st_mode(stat.st_mode) == FileType::Socket { + return Err(Error::ENOTSUP); + } else { + return Err(Error::ENXIO); + } + } else { + return Err(Error::ENXIO); + } + } + // Linux returns ENOTDIR instead of ELOOP when using O_NOFOLLOW|O_DIRECTORY + // on a symlink. + Errno::ENOTDIR + if !(nix_all_oflags & (OFlag::NOFOLLOW | OFlag::DIRECTORY)).is_empty() => + { + if let Ok(stat) = unsafe { + fstatat( + resolved.dirfd().as_raw_fd(), + resolved.path(), + AtFlag::SYMLINK_NOFOLLOW, + ) + } { + if FileType::from_stat_st_mode(stat.st_mode) == FileType::Symlink { + return Err(Error::ELOOP); + } + } + return Err(Error::ENOTDIR); + } + // FreeBSD returns EMLINK instead of ELOOP when using O_NOFOLLOW on + // a symlink. + Errno::EMLINK if !(nix_all_oflags & OFlag::NOFOLLOW).is_empty() => { + return Err(Error::ELOOP); + } + errno => return Err(errno.into()), + } + } else { + return Err(e.into()); + } + } + }; + + log::debug!("path_open (host) new_fd = {:?}", new_fd); + + // Determine the type of the new file descriptor and which rights contradict with this type + Ok(unsafe { File::from_raw_fd(new_fd) }) +} + +pub(crate) fn path_readlink(resolved: PathGet, buf: &mut [u8]) -> Result { + use std::cmp::min; + use yanix::file::readlinkat; + let read_link = unsafe { readlinkat(resolved.dirfd().as_raw_fd(), resolved.path()) } + .map_err(Into::into) + .and_then(host_impl::path_from_host)?; + let copy_len = min(read_link.len(), buf.len()); + if copy_len > 0 { + buf[..copy_len].copy_from_slice(&read_link.as_bytes()[..copy_len]); + } + Ok(copy_len) +} + +pub(crate) fn fd_filestat_get(file: &std::fs::File) -> Result { + use yanix::file::fstat; + unsafe { fstat(file.as_raw_fd()) } + .map_err(Into::into) + .and_then(host_impl::filestat_from_nix) +} + +pub(crate) fn path_filestat_get( + resolved: PathGet, + dirflags: wasi::__wasi_lookupflags_t, +) -> Result { + use yanix::file::{fstatat, AtFlag}; + let atflags = match dirflags { + 0 => AtFlag::empty(), + _ => AtFlag::SYMLINK_NOFOLLOW, + }; + unsafe { fstatat(resolved.dirfd().as_raw_fd(), resolved.path(), atflags) } + .map_err(Into::into) + .and_then(host_impl::filestat_from_nix) +} + +pub(crate) fn path_filestat_set_times( + resolved: PathGet, + dirflags: wasi::__wasi_lookupflags_t, + st_atim: wasi::__wasi_timestamp_t, + st_mtim: wasi::__wasi_timestamp_t, + fst_flags: wasi::__wasi_fstflags_t, +) -> Result<()> { + use super::super::filetime::*; + use std::time::{Duration, UNIX_EPOCH}; + + let set_atim = fst_flags & wasi::__WASI_FSTFLAGS_ATIM != 0; + let set_atim_now = fst_flags & wasi::__WASI_FSTFLAGS_ATIM_NOW != 0; + let set_mtim = fst_flags & wasi::__WASI_FSTFLAGS_MTIM != 0; + let set_mtim_now = fst_flags & wasi::__WASI_FSTFLAGS_MTIM_NOW != 0; + + if (set_atim && set_atim_now) || (set_mtim && set_mtim_now) { + return Err(Error::EINVAL); + } + + let symlink_nofollow = wasi::__WASI_LOOKUPFLAGS_SYMLINK_FOLLOW != dirflags; + let atim = if set_atim { + let time = UNIX_EPOCH + Duration::from_nanos(st_atim); + FileTime::FileTime(filetime::FileTime::from_system_time(time)) + } else if set_atim_now { + FileTime::Now + } else { + FileTime::Omit + }; + let mtim = if set_mtim { + let time = UNIX_EPOCH + Duration::from_nanos(st_mtim); + FileTime::FileTime(filetime::FileTime::from_system_time(time)) + } else if set_mtim_now { + FileTime::Now + } else { + FileTime::Omit + }; + + utimensat( + resolved.dirfd(), + resolved.path(), + atim, + mtim, + symlink_nofollow, + ) + .map_err(Into::into) +} + +pub(crate) fn path_remove_directory(resolved: PathGet) -> Result<()> { + use yanix::file::{unlinkat, AtFlag}; + unsafe { + unlinkat( + resolved.dirfd().as_raw_fd(), + resolved.path(), + AtFlag::REMOVEDIR, + ) + } + .map_err(Into::into) +} + +pub(crate) fn fd_readdir<'a>( + os_handle: &'a mut OsHandle, + cookie: wasi::__wasi_dircookie_t, +) -> Result> + 'a> { + use yanix::dir::{DirIter, Entry, EntryExt, SeekLoc}; + + // Get an instance of `Dir`; this is host-specific due to intricasies + // of managing a dir stream between Linux and BSD *nixes + let mut dir = fd_readdir_impl::get_dir_from_os_handle(os_handle)?; + + // Seek if needed. Unless cookie is wasi::__WASI_DIRCOOKIE_START, + // new items may not be returned to the caller. + if cookie == wasi::__WASI_DIRCOOKIE_START { + log::trace!(" | fd_readdir: doing rewinddir"); + dir.rewind(); + } else { + log::trace!(" | fd_readdir: doing seekdir to {}", cookie); + let loc = unsafe { SeekLoc::from_raw(cookie as i64)? }; + dir.seek(loc); + } + + Ok(DirIter::new(dir).map(|entry| { + let entry: Entry = entry?; + Ok(Dirent { + name: entry + // TODO can we reuse path_from_host for CStr? + .file_name() + .to_str()? + .to_owned(), + ino: entry.ino(), + ftype: entry.file_type().into(), + cookie: entry.seek_loc()?.to_raw().try_into()?, + }) + })) +} diff --git a/crates/wasi-common/src/sys/unix/hostcalls_impl/fs_helpers.rs b/crates/wasi-common/src/sys/unix/hostcalls_impl/fs_helpers.rs new file mode 100644 index 0000000000..785b553d96 --- /dev/null +++ b/crates/wasi-common/src/sys/unix/hostcalls_impl/fs_helpers.rs @@ -0,0 +1,66 @@ +#![allow(non_camel_case_types)] +#![allow(unused_unsafe)] +use crate::sys::host_impl; +use crate::{wasi, Result}; +use std::fs::File; +use yanix::file::OFlag; + +pub(crate) fn path_open_rights( + rights_base: wasi::__wasi_rights_t, + rights_inheriting: wasi::__wasi_rights_t, + oflags: wasi::__wasi_oflags_t, + fs_flags: wasi::__wasi_fdflags_t, +) -> (wasi::__wasi_rights_t, wasi::__wasi_rights_t) { + // which rights are needed on the dirfd? + let mut needed_base = wasi::__WASI_RIGHTS_PATH_OPEN; + let mut needed_inheriting = rights_base | rights_inheriting; + + // convert open flags + let oflags = host_impl::nix_from_oflags(oflags); + if oflags.contains(OFlag::CREAT) { + needed_base |= wasi::__WASI_RIGHTS_PATH_CREATE_FILE; + } + if oflags.contains(OFlag::TRUNC) { + needed_base |= wasi::__WASI_RIGHTS_PATH_FILESTAT_SET_SIZE; + } + + // convert file descriptor flags + let fdflags = host_impl::nix_from_fdflags(fs_flags); + if fdflags.contains(OFlag::DSYNC) { + needed_inheriting |= wasi::__WASI_RIGHTS_FD_DATASYNC; + } + if fdflags.intersects(host_impl::O_RSYNC | OFlag::SYNC) { + needed_inheriting |= wasi::__WASI_RIGHTS_FD_SYNC; + } + + (needed_base, needed_inheriting) +} + +pub(crate) fn openat(dirfd: &File, path: &str) -> Result { + use std::os::unix::prelude::{AsRawFd, FromRawFd}; + use yanix::file::{openat, Mode}; + + log::debug!("path_get openat path = {:?}", path); + + unsafe { + openat( + dirfd.as_raw_fd(), + path, + OFlag::RDONLY | OFlag::DIRECTORY | OFlag::NOFOLLOW, + Mode::empty(), + ) + } + .map(|new_fd| unsafe { File::from_raw_fd(new_fd) }) + .map_err(Into::into) +} + +pub(crate) fn readlinkat(dirfd: &File, path: &str) -> Result { + use std::os::unix::prelude::AsRawFd; + use yanix::file::readlinkat; + + log::debug!("path_get readlinkat path = {:?}", path); + + unsafe { readlinkat(dirfd.as_raw_fd(), path) } + .map_err(Into::into) + .and_then(host_impl::path_from_host) +} diff --git a/crates/wasi-common/src/sys/unix/hostcalls_impl/misc.rs b/crates/wasi-common/src/sys/unix/hostcalls_impl/misc.rs new file mode 100644 index 0000000000..3e6748fbc5 --- /dev/null +++ b/crates/wasi-common/src/sys/unix/hostcalls_impl/misc.rs @@ -0,0 +1,210 @@ +#![allow(non_camel_case_types)] +#![allow(unused_unsafe)] +use crate::hostcalls_impl::{ClockEventData, FdEventData}; +use crate::{wasi, Error, Result}; +use yanix::clock::{clock_getres, clock_gettime, ClockId}; + +fn wasi_clock_id_to_unix(clock_id: wasi::__wasi_clockid_t) -> Result { + // convert the supported clocks to libc types, or return EINVAL + match clock_id { + wasi::__WASI_CLOCKID_REALTIME => Ok(ClockId::Realtime), + wasi::__WASI_CLOCKID_MONOTONIC => Ok(ClockId::Monotonic), + wasi::__WASI_CLOCKID_PROCESS_CPUTIME_ID => Ok(ClockId::ProcessCPUTime), + wasi::__WASI_CLOCKID_THREAD_CPUTIME_ID => Ok(ClockId::ThreadCPUTime), + _ => Err(Error::EINVAL), + } +} + +pub(crate) fn clock_res_get(clock_id: wasi::__wasi_clockid_t) -> Result { + let clock_id = wasi_clock_id_to_unix(clock_id)?; + let timespec = clock_getres(clock_id)?; + + // convert to nanoseconds, returning EOVERFLOW in case of overflow; + // this is freelancing a bit from the spec but seems like it'll + // be an unusual situation to hit + (timespec.tv_sec as wasi::__wasi_timestamp_t) + .checked_mul(1_000_000_000) + .and_then(|sec_ns| sec_ns.checked_add(timespec.tv_nsec as wasi::__wasi_timestamp_t)) + .map_or(Err(Error::EOVERFLOW), |resolution| { + // a supported clock can never return zero; this case will probably never get hit, but + // make sure we follow the spec + if resolution == 0 { + Err(Error::EINVAL) + } else { + Ok(resolution) + } + }) +} + +pub(crate) fn clock_time_get(clock_id: wasi::__wasi_clockid_t) -> Result { + let clock_id = wasi_clock_id_to_unix(clock_id)?; + let timespec = clock_gettime(clock_id)?; + + // convert to nanoseconds, returning EOVERFLOW in case of overflow; this is freelancing a bit + // from the spec but seems like it'll be an unusual situation to hit + (timespec.tv_sec as wasi::__wasi_timestamp_t) + .checked_mul(1_000_000_000) + .and_then(|sec_ns| sec_ns.checked_add(timespec.tv_nsec as wasi::__wasi_timestamp_t)) + .map_or(Err(Error::EOVERFLOW), Ok) +} + +pub(crate) fn poll_oneoff( + timeout: Option, + fd_events: Vec, + events: &mut Vec, +) -> Result<()> { + use std::{convert::TryInto, os::unix::prelude::AsRawFd}; + use yanix::{ + poll::{poll, PollFd, PollFlags}, + Errno, + }; + + if fd_events.is_empty() && timeout.is_none() { + return Ok(()); + } + + let mut poll_fds: Vec<_> = fd_events + .iter() + .map(|event| { + let mut flags = PollFlags::empty(); + match event.r#type { + wasi::__WASI_EVENTTYPE_FD_READ => flags.insert(PollFlags::POLLIN), + wasi::__WASI_EVENTTYPE_FD_WRITE => flags.insert(PollFlags::POLLOUT), + // An event on a file descriptor can currently only be of type FD_READ or FD_WRITE + // Nothing else has been defined in the specification, and these are also the only two + // events we filtered before. If we get something else here, the code has a serious bug. + _ => unreachable!(), + }; + unsafe { PollFd::new(event.descriptor.as_raw_fd(), flags) } + }) + .collect(); + + let poll_timeout = timeout.map_or(-1, |timeout| { + let delay = timeout.delay / 1_000_000; // poll syscall requires delay to expressed in milliseconds + delay.try_into().unwrap_or(libc::c_int::max_value()) + }); + log::debug!("poll_oneoff poll_timeout = {:?}", poll_timeout); + + let ready = loop { + match poll(&mut poll_fds, poll_timeout) { + Err(_) => { + if Errno::last() == Errno::EINTR { + continue; + } + return Err(Errno::last().into()); + } + Ok(ready) => break ready, + } + }; + + Ok(if ready == 0 { + poll_oneoff_handle_timeout_event(timeout.expect("timeout should not be None"), events) + } else { + let ready_events = fd_events.into_iter().zip(poll_fds.into_iter()).take(ready); + poll_oneoff_handle_fd_event(ready_events, events)? + }) +} + +fn poll_oneoff_handle_timeout_event( + timeout: ClockEventData, + events: &mut Vec, +) { + events.push(wasi::__wasi_event_t { + userdata: timeout.userdata, + error: wasi::__WASI_ERRNO_SUCCESS, + r#type: wasi::__WASI_EVENTTYPE_CLOCK, + fd_readwrite: wasi::__wasi_event_fd_readwrite_t { + flags: 0, + nbytes: 0, + }, + }); +} + +fn poll_oneoff_handle_fd_event<'a>( + ready_events: impl Iterator, yanix::poll::PollFd)>, + events: &mut Vec, +) -> Result<()> { + use crate::fdentry::Descriptor; + use std::{convert::TryInto, os::unix::prelude::AsRawFd}; + use yanix::{file::fionread, poll::PollFlags}; + + fn query_nbytes(fd: &Descriptor) -> Result { + // fionread may overflow for large files, so use another way for regular files. + if let Descriptor::OsHandle(os_handle) = fd { + let meta = os_handle.metadata()?; + if meta.file_type().is_file() { + use yanix::file::tell; + let len = meta.len(); + let host_offset = unsafe { tell(os_handle.as_raw_fd())? }; + return Ok(len - host_offset); + } + } + unsafe { Ok(fionread(fd.as_raw_fd())?.into()) } + } + + for (fd_event, poll_fd) in ready_events { + log::debug!("poll_oneoff_handle_fd_event fd_event = {:?}", fd_event); + log::debug!("poll_oneoff_handle_fd_event poll_fd = {:?}", poll_fd); + + let revents = match poll_fd.revents() { + Some(revents) => revents, + None => continue, + }; + + log::debug!("poll_oneoff_handle_fd_event revents = {:?}", revents); + + let nbytes = if fd_event.r#type == wasi::__WASI_EVENTTYPE_FD_READ { + query_nbytes(fd_event.descriptor)? + } else { + 0 + }; + + let output_event = if revents.contains(PollFlags::POLLNVAL) { + wasi::__wasi_event_t { + userdata: fd_event.userdata, + error: wasi::__WASI_ERRNO_BADF, + r#type: fd_event.r#type, + fd_readwrite: wasi::__wasi_event_fd_readwrite_t { + nbytes: 0, + flags: wasi::__WASI_EVENTRWFLAGS_FD_READWRITE_HANGUP, + }, + } + } else if revents.contains(PollFlags::POLLERR) { + wasi::__wasi_event_t { + userdata: fd_event.userdata, + error: wasi::__WASI_ERRNO_IO, + r#type: fd_event.r#type, + fd_readwrite: wasi::__wasi_event_fd_readwrite_t { + nbytes: 0, + flags: wasi::__WASI_EVENTRWFLAGS_FD_READWRITE_HANGUP, + }, + } + } else if revents.contains(PollFlags::POLLHUP) { + wasi::__wasi_event_t { + userdata: fd_event.userdata, + error: wasi::__WASI_ERRNO_SUCCESS, + r#type: fd_event.r#type, + fd_readwrite: wasi::__wasi_event_fd_readwrite_t { + nbytes: 0, + flags: wasi::__WASI_EVENTRWFLAGS_FD_READWRITE_HANGUP, + }, + } + } else if revents.contains(PollFlags::POLLIN) | revents.contains(PollFlags::POLLOUT) { + wasi::__wasi_event_t { + userdata: fd_event.userdata, + error: wasi::__WASI_ERRNO_SUCCESS, + r#type: fd_event.r#type, + fd_readwrite: wasi::__wasi_event_fd_readwrite_t { + nbytes: nbytes.try_into()?, + flags: 0, + }, + } + } else { + continue; + }; + + events.push(output_event); + } + + Ok(()) +} diff --git a/crates/wasi-common/src/sys/unix/hostcalls_impl/mod.rs b/crates/wasi-common/src/sys/unix/hostcalls_impl/mod.rs new file mode 100644 index 0000000000..ba18086104 --- /dev/null +++ b/crates/wasi-common/src/sys/unix/hostcalls_impl/mod.rs @@ -0,0 +1,8 @@ +//! Unix-specific hostcalls that implement +//! [WASI](https://github.com/bytecodealliance/wasmtime-wasi/blob/wasi/docs/WASI-overview.md). +mod fs; +pub(crate) mod fs_helpers; +mod misc; + +pub(crate) use self::fs::*; +pub(crate) use self::misc::*; diff --git a/crates/wasi-common/src/sys/unix/linux/filetime.rs b/crates/wasi-common/src/sys/unix/linux/filetime.rs new file mode 100644 index 0000000000..6ffd6b8ddf --- /dev/null +++ b/crates/wasi-common/src/sys/unix/linux/filetime.rs @@ -0,0 +1,61 @@ +//! This internal module consists of helper types and functions for dealing +//! with setting the file times specific to Linux. +use crate::{sys::unix::filetime::FileTime, Result}; +use std::fs::File; +use std::io; +use std::sync::atomic::{AtomicBool, Ordering::Relaxed}; + +pub(crate) const UTIME_NOW: i64 = 1_073_741_823; +pub(crate) const UTIME_OMIT: i64 = 1_073_741_822; + +/// Wrapper for `utimensat` syscall, however, with an added twist such that `utimensat` symbol +/// is firstly resolved (i.e., we check whether it exists on the host), and only used if that is +/// the case. Otherwise, the syscall resorts to a less accurate `utimesat` emulated syscall. +/// The original implementation can be found here: [filetime::unix::linux::set_times] +/// +/// [filetime::unix::linux::set_times]: https://github.com/alexcrichton/filetime/blob/master/src/unix/linux.rs#L64 +pub(crate) fn utimensat( + dirfd: &File, + path: &str, + atime: FileTime, + mtime: FileTime, + symlink_nofollow: bool, +) -> Result<()> { + use crate::sys::unix::filetime::to_timespec; + use std::ffi::CString; + use std::os::unix::prelude::*; + + let flags = if symlink_nofollow { + libc::AT_SYMLINK_NOFOLLOW + } else { + 0 + }; + + // Attempt to use the `utimensat` syscall, but if it's not supported by the + // current kernel then fall back to an older syscall. + static INVALID: AtomicBool = AtomicBool::new(false); + if !INVALID.load(Relaxed) { + let p = CString::new(path.as_bytes())?; + let times = [to_timespec(&atime)?, to_timespec(&mtime)?]; + let rc = unsafe { + libc::syscall( + libc::SYS_utimensat, + dirfd.as_raw_fd(), + p.as_ptr(), + times.as_ptr(), + flags, + ) + }; + if rc == 0 { + return Ok(()); + } + let err = io::Error::last_os_error(); + if err.raw_os_error() == Some(libc::ENOSYS) { + INVALID.store(true, Relaxed); + } else { + return Err(err.into()); + } + } + + super::utimesat::utimesat(dirfd, path, atime, mtime, symlink_nofollow) +} diff --git a/crates/wasi-common/src/sys/unix/linux/host_impl.rs b/crates/wasi-common/src/sys/unix/linux/host_impl.rs new file mode 100644 index 0000000000..d7a3e46fdb --- /dev/null +++ b/crates/wasi-common/src/sys/unix/linux/host_impl.rs @@ -0,0 +1,11 @@ +use crate::{wasi, Result}; + +pub(crate) const O_RSYNC: yanix::file::OFlag = yanix::file::OFlag::RSYNC; + +pub(crate) fn stdev_from_nix(dev: libc::dev_t) -> Result { + Ok(wasi::__wasi_device_t::from(dev)) +} + +pub(crate) fn stino_from_nix(ino: libc::ino_t) -> Result { + Ok(wasi::__wasi_device_t::from(ino)) +} diff --git a/crates/wasi-common/src/sys/unix/linux/hostcalls_impl.rs b/crates/wasi-common/src/sys/unix/linux/hostcalls_impl.rs new file mode 100644 index 0000000000..0ddac0f41a --- /dev/null +++ b/crates/wasi-common/src/sys/unix/linux/hostcalls_impl.rs @@ -0,0 +1,60 @@ +use crate::hostcalls_impl::PathGet; +use crate::Result; +use std::os::unix::prelude::AsRawFd; + +pub(crate) fn path_unlink_file(resolved: PathGet) -> Result<()> { + use yanix::file::{unlinkat, AtFlag}; + unsafe { + unlinkat( + resolved.dirfd().as_raw_fd(), + resolved.path(), + AtFlag::empty(), + ) + } + .map_err(Into::into) +} + +pub(crate) fn path_symlink(old_path: &str, resolved: PathGet) -> Result<()> { + use yanix::file::symlinkat; + + log::debug!("path_symlink old_path = {:?}", old_path); + log::debug!("path_symlink resolved = {:?}", resolved); + + unsafe { symlinkat(old_path, resolved.dirfd().as_raw_fd(), resolved.path()) } + .map_err(Into::into) +} + +pub(crate) fn path_rename(resolved_old: PathGet, resolved_new: PathGet) -> Result<()> { + use yanix::file::renameat; + unsafe { + renameat( + resolved_old.dirfd().as_raw_fd(), + resolved_old.path(), + resolved_new.dirfd().as_raw_fd(), + resolved_new.path(), + ) + } + .map_err(Into::into) +} + +pub(crate) mod fd_readdir_impl { + use crate::sys::fdentry_impl::OsHandle; + use crate::Result; + use yanix::dir::Dir; + + pub(crate) fn get_dir_from_os_handle(os_handle: &mut OsHandle) -> Result> { + // We need to duplicate the fd, because `opendir(3)`: + // After a successful call to fdopendir(), fd is used internally by the implementation, + // and should not otherwise be used by the application. + // `opendir(3p)` also says that it's undefined behavior to + // modify the state of the fd in a different way than by accessing DIR*. + // + // Still, rewinddir will be needed because the two file descriptors + // share progress. But we can safely execute closedir now. + let fd = os_handle.try_clone()?; + // TODO This doesn't look very clean. Can we do something about it? + // Boxing is needed here in order to satisfy `yanix`'s trait requirement for the `DirIter` + // where `T: Deref`. + Ok(Box::new(Dir::from(fd)?)) + } +} diff --git a/crates/wasi-common/src/sys/unix/linux/mod.rs b/crates/wasi-common/src/sys/unix/linux/mod.rs new file mode 100644 index 0000000000..0b4b8fd0b9 --- /dev/null +++ b/crates/wasi-common/src/sys/unix/linux/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod filetime; +pub(crate) mod host_impl; +pub(crate) mod hostcalls_impl; +pub(crate) mod oshandle; +pub(crate) mod utimesat; diff --git a/crates/wasi-common/src/sys/unix/linux/oshandle.rs b/crates/wasi-common/src/sys/unix/linux/oshandle.rs new file mode 100644 index 0000000000..8104d9d5e3 --- /dev/null +++ b/crates/wasi-common/src/sys/unix/linux/oshandle.rs @@ -0,0 +1,32 @@ +use std::fs; +use std::ops::{Deref, DerefMut}; +use std::os::unix::prelude::{AsRawFd, RawFd}; + +#[derive(Debug)] +pub(crate) struct OsHandle(fs::File); + +impl From for OsHandle { + fn from(file: fs::File) -> Self { + Self(file) + } +} + +impl AsRawFd for OsHandle { + fn as_raw_fd(&self) -> RawFd { + self.0.as_raw_fd() + } +} + +impl Deref for OsHandle { + type Target = fs::File; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for OsHandle { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/crates/wasi-common/src/sys/unix/linux/utimesat.rs b/crates/wasi-common/src/sys/unix/linux/utimesat.rs new file mode 100644 index 0000000000..c99e6e0610 --- /dev/null +++ b/crates/wasi-common/src/sys/unix/linux/utimesat.rs @@ -0,0 +1,88 @@ +use crate::sys::unix::filetime::FileTime; +use crate::Result; +use std::{fs, io}; + +/// Combines `openat` with `utimes` to emulate `utimensat` on platforms where it is +/// not available. The logic for setting file times is based on [filetime::unix::set_file_handles_times]. +/// +/// [filetime::unix::set_file_handles_times]: https://github.com/alexcrichton/filetime/blob/master/src/unix/utimes.rs#L24 +pub(crate) fn utimesat( + dirfd: &fs::File, + path: &str, + atime: FileTime, + mtime: FileTime, + symlink_nofollow: bool, +) -> Result<()> { + use std::ffi::CString; + use std::os::unix::prelude::*; + // emulate *at syscall by reading the path from a combination of + // (fd, path) + let p = CString::new(path.as_bytes())?; + let mut flags = libc::O_RDWR; + if symlink_nofollow { + flags |= libc::O_NOFOLLOW; + } + let fd = unsafe { libc::openat(dirfd.as_raw_fd(), p.as_ptr(), flags) }; + let f = unsafe { fs::File::from_raw_fd(fd) }; + let (atime, mtime) = get_times(atime, mtime, || f.metadata().map_err(Into::into))?; + let times = [to_timeval(atime), to_timeval(mtime)]; + let rc = unsafe { libc::futimes(f.as_raw_fd(), times.as_ptr()) }; + if rc == 0 { + Ok(()) + } else { + Err(io::Error::last_os_error().into()) + } +} + +/// Converts `filetime::FileTime` to `libc::timeval`. This function was taken directly from +/// [filetime] crate. +/// +/// [filetime]: https://github.com/alexcrichton/filetime/blob/master/src/unix/utimes.rs#L93 +fn to_timeval(ft: filetime::FileTime) -> libc::timeval { + libc::timeval { + tv_sec: ft.seconds(), + tv_usec: (ft.nanoseconds() / 1000) as libc::suseconds_t, + } +} + +/// For a provided pair of access and modified `FileTime`s, converts the input to +/// `filetime::FileTime` used later in `utimensat` function. For variants `FileTime::Now` +/// and `FileTime::Omit`, this function will make two syscalls: either accessing current +/// system time, or accessing the file's metadata. +/// +/// The original implementation can be found here: [filetime::unix::get_times]. +/// +/// [filetime::unix::get_times]: https://github.com/alexcrichton/filetime/blob/master/src/unix/utimes.rs#L42 +fn get_times( + atime: FileTime, + mtime: FileTime, + current: impl Fn() -> Result, +) -> Result<(filetime::FileTime, filetime::FileTime)> { + use std::time::SystemTime; + + let atime = match atime { + FileTime::Now => { + let time = SystemTime::now(); + filetime::FileTime::from_system_time(time) + } + FileTime::Omit => { + let meta = current()?; + filetime::FileTime::from_last_access_time(&meta) + } + FileTime::FileTime(ft) => ft, + }; + + let mtime = match mtime { + FileTime::Now => { + let time = SystemTime::now(); + filetime::FileTime::from_system_time(time) + } + FileTime::Omit => { + let meta = current()?; + filetime::FileTime::from_last_modification_time(&meta) + } + FileTime::FileTime(ft) => ft, + }; + + Ok((atime, mtime)) +} diff --git a/crates/wasi-common/src/sys/unix/mod.rs b/crates/wasi-common/src/sys/unix/mod.rs new file mode 100644 index 0000000000..60b08ab355 --- /dev/null +++ b/crates/wasi-common/src/sys/unix/mod.rs @@ -0,0 +1,39 @@ +pub(crate) mod fdentry_impl; +pub(crate) mod host_impl; +pub(crate) mod hostcalls_impl; + +mod filetime; + +cfg_if::cfg_if! { + if #[cfg(target_os = "linux")] { + mod linux; + use self::linux as sys_impl; + } else if #[cfg(target_os = "emscripten")] { + mod emscripten; + use self::emscripten as sys_impl; + } else if #[cfg(any(target_os = "macos", + target_os = "netbsd", + target_os = "freebsd", + target_os = "openbsd", + target_os = "ios", + target_os = "dragonfly"))] { + mod bsd; + use self::bsd as sys_impl; + } +} + +use crate::Result; +use std::fs::{File, OpenOptions}; +use std::path::Path; + +pub(crate) fn dev_null() -> Result { + OpenOptions::new() + .read(true) + .write(true) + .open("/dev/null") + .map_err(Into::into) +} + +pub fn preopen_dir>(path: P) -> Result { + File::open(path).map_err(Into::into) +} diff --git a/crates/wasi-common/src/sys/windows/fdentry_impl.rs b/crates/wasi-common/src/sys/windows/fdentry_impl.rs new file mode 100644 index 0000000000..8700638004 --- /dev/null +++ b/crates/wasi-common/src/sys/windows/fdentry_impl.rs @@ -0,0 +1,143 @@ +use crate::fdentry::{Descriptor, OsHandleRef}; +use crate::{wasi, Error, Result}; +use std::fs::File; +use std::io; +use std::mem::ManuallyDrop; +use std::ops::{Deref, DerefMut}; +use std::os::windows::prelude::{AsRawHandle, FromRawHandle, RawHandle}; + +#[derive(Debug)] +pub(crate) struct OsHandle(File); + +impl From for OsHandle { + fn from(file: File) -> Self { + Self(file) + } +} + +impl AsRawHandle for OsHandle { + fn as_raw_handle(&self) -> RawHandle { + self.0.as_raw_handle() + } +} + +impl Deref for OsHandle { + type Target = File; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for OsHandle { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl AsRawHandle for Descriptor { + fn as_raw_handle(&self) -> RawHandle { + match self { + Self::OsHandle(file) => file.as_raw_handle(), + Self::Stdin => io::stdin().as_raw_handle(), + Self::Stdout => io::stdout().as_raw_handle(), + Self::Stderr => io::stderr().as_raw_handle(), + } + } +} + +pub(crate) fn descriptor_as_oshandle<'lifetime>( + desc: &'lifetime Descriptor, +) -> OsHandleRef<'lifetime> { + OsHandleRef::new(ManuallyDrop::new(OsHandle::from(unsafe { + File::from_raw_handle(desc.as_raw_handle()) + }))) +} + +/// Returns the set of all possible rights that are both relevant for the file +/// type and consistent with the open mode. +/// +/// This function is unsafe because it operates on a raw file descriptor. +pub(crate) unsafe fn determine_type_and_access_rights( + handle: &Handle, +) -> Result<( + wasi::__wasi_filetype_t, + wasi::__wasi_rights_t, + wasi::__wasi_rights_t, +)> { + use winx::file::{query_access_information, AccessMode}; + + let (file_type, mut rights_base, rights_inheriting) = determine_type_rights(handle)?; + + match file_type { + wasi::__WASI_FILETYPE_DIRECTORY | wasi::__WASI_FILETYPE_REGULAR_FILE => { + let mode = query_access_information(handle.as_raw_handle())?; + if mode.contains(AccessMode::FILE_GENERIC_READ) { + rights_base |= wasi::__WASI_RIGHTS_FD_READ; + } + if mode.contains(AccessMode::FILE_GENERIC_WRITE) { + rights_base |= wasi::__WASI_RIGHTS_FD_WRITE; + } + } + _ => { + // TODO: is there a way around this? On windows, it seems + // we cannot check access rights for anything but dirs and regular files + } + } + + Ok((file_type, rights_base, rights_inheriting)) +} + +/// Returns the set of all possible rights that are relevant for file type. +/// +/// This function is unsafe because it operates on a raw file descriptor. +pub(crate) unsafe fn determine_type_rights( + handle: &Handle, +) -> Result<( + wasi::__wasi_filetype_t, + wasi::__wasi_rights_t, + wasi::__wasi_rights_t, +)> { + let (file_type, rights_base, rights_inheriting) = { + let file_type = winx::file::get_file_type(handle.as_raw_handle())?; + if file_type.is_char() { + // character file: LPT device or console + // TODO: rule out LPT device + ( + wasi::__WASI_FILETYPE_CHARACTER_DEVICE, + wasi::RIGHTS_TTY_BASE, + wasi::RIGHTS_TTY_BASE, + ) + } else if file_type.is_disk() { + // disk file: file, dir or disk device + let file = std::mem::ManuallyDrop::new(File::from_raw_handle(handle.as_raw_handle())); + let meta = file.metadata().map_err(|_| Error::EINVAL)?; + if meta.is_dir() { + ( + wasi::__WASI_FILETYPE_DIRECTORY, + wasi::RIGHTS_DIRECTORY_BASE, + wasi::RIGHTS_DIRECTORY_INHERITING, + ) + } else if meta.is_file() { + ( + wasi::__WASI_FILETYPE_REGULAR_FILE, + wasi::RIGHTS_REGULAR_FILE_BASE, + wasi::RIGHTS_REGULAR_FILE_INHERITING, + ) + } else { + return Err(Error::EINVAL); + } + } else if file_type.is_pipe() { + // pipe object: socket, named pipe or anonymous pipe + // TODO: what about pipes, etc? + ( + wasi::__WASI_FILETYPE_SOCKET_STREAM, + wasi::RIGHTS_SOCKET_BASE, + wasi::RIGHTS_SOCKET_INHERITING, + ) + } else { + return Err(Error::EINVAL); + } + }; + Ok((file_type, rights_base, rights_inheriting)) +} diff --git a/crates/wasi-common/src/sys/windows/host_impl.rs b/crates/wasi-common/src/sys/windows/host_impl.rs new file mode 100644 index 0000000000..ba6717e5e6 --- /dev/null +++ b/crates/wasi-common/src/sys/windows/host_impl.rs @@ -0,0 +1,112 @@ +//! WASI host types specific to Windows host. +use crate::host::FileType; +use crate::{error::FromRawOsError, wasi, Error, Result}; +use std::convert::TryInto; +use std::ffi::OsStr; +use std::fs::{self, File}; +use std::io; +use std::os::windows::ffi::OsStrExt; +use std::time::{SystemTime, UNIX_EPOCH}; +use winx::winerror::WinError; + +impl FromRawOsError for Error { + fn from_raw_os_error(code: i32) -> Self { + Self::from(WinError::from_u32(code as u32)) + } +} + +impl From for Error { + fn from(err: WinError) -> Self { + // TODO: implement error mapping between Windows and WASI + use winx::winerror::WinError::*; + match err { + ERROR_SUCCESS => Self::ESUCCESS, + ERROR_BAD_ENVIRONMENT => Self::E2BIG, + ERROR_FILE_NOT_FOUND => Self::ENOENT, + ERROR_PATH_NOT_FOUND => Self::ENOENT, + ERROR_TOO_MANY_OPEN_FILES => Self::ENFILE, + ERROR_ACCESS_DENIED => Self::EACCES, + ERROR_SHARING_VIOLATION => Self::EACCES, + ERROR_PRIVILEGE_NOT_HELD => Self::ENOTCAPABLE, // TODO is this the correct mapping? + ERROR_INVALID_HANDLE => Self::EBADF, + ERROR_INVALID_NAME => Self::ENOENT, + ERROR_NOT_ENOUGH_MEMORY => Self::ENOMEM, + ERROR_OUTOFMEMORY => Self::ENOMEM, + ERROR_DIR_NOT_EMPTY => Self::ENOTEMPTY, + ERROR_NOT_READY => Self::EBUSY, + ERROR_BUSY => Self::EBUSY, + ERROR_NOT_SUPPORTED => Self::ENOTSUP, + ERROR_FILE_EXISTS => Self::EEXIST, + ERROR_BROKEN_PIPE => Self::EPIPE, + ERROR_BUFFER_OVERFLOW => Self::ENAMETOOLONG, + ERROR_NOT_A_REPARSE_POINT => Self::EINVAL, + ERROR_NEGATIVE_SEEK => Self::EINVAL, + ERROR_DIRECTORY => Self::ENOTDIR, + ERROR_ALREADY_EXISTS => Self::EEXIST, + _ => Self::ENOTSUP, + } + } +} + +pub(crate) fn filetype_from_std(ftype: &fs::FileType) -> FileType { + if ftype.is_file() { + FileType::RegularFile + } else if ftype.is_dir() { + FileType::Directory + } else if ftype.is_symlink() { + FileType::Symlink + } else { + FileType::Unknown + } +} + +fn num_hardlinks(file: &File) -> io::Result { + Ok(winx::file::get_fileinfo(file)?.nNumberOfLinks.into()) +} + +fn device_id(file: &File) -> io::Result { + Ok(winx::file::get_fileinfo(file)?.dwVolumeSerialNumber.into()) +} + +pub(crate) fn file_serial_no(file: &File) -> io::Result { + let info = winx::file::get_fileinfo(file)?; + let high = info.nFileIndexHigh; + let low = info.nFileIndexLow; + let no = (u64::from(high) << 32) | u64::from(low); + Ok(no) +} + +fn change_time(file: &File) -> io::Result { + winx::file::change_time(file) +} + +fn systemtime_to_timestamp(st: SystemTime) -> Result { + st.duration_since(UNIX_EPOCH) + .map_err(|_| Error::EINVAL)? // date earlier than UNIX_EPOCH + .as_nanos() + .try_into() + .map_err(Into::into) // u128 doesn't fit into u64 +} + +pub(crate) fn filestat_from_win(file: &File) -> Result { + let metadata = file.metadata()?; + Ok(wasi::__wasi_filestat_t { + dev: device_id(file)?, + ino: file_serial_no(file)?, + nlink: num_hardlinks(file)?.try_into()?, // u64 doesn't fit into u32 + size: metadata.len(), + atim: systemtime_to_timestamp(metadata.accessed()?)?, + ctim: change_time(file)?.try_into()?, // i64 doesn't fit into u64 + mtim: systemtime_to_timestamp(metadata.modified()?)?, + filetype: filetype_from_std(&metadata.file_type()).to_wasi(), + }) +} + +/// Creates owned WASI path from OS string. +/// +/// NB WASI spec requires OS string to be valid UTF-8. Otherwise, +/// `__WASI_ERRNO_ILSEQ` error is returned. +pub(crate) fn path_from_host>(s: S) -> Result { + let vec: Vec = s.as_ref().encode_wide().collect(); + String::from_utf16(&vec).map_err(|_| Error::EILSEQ) +} diff --git a/crates/wasi-common/src/sys/windows/hostcalls_impl/fs.rs b/crates/wasi-common/src/sys/windows/hostcalls_impl/fs.rs new file mode 100644 index 0000000000..1baacb5e05 --- /dev/null +++ b/crates/wasi-common/src/sys/windows/hostcalls_impl/fs.rs @@ -0,0 +1,588 @@ +#![allow(non_camel_case_types)] +#![allow(unused)] +use super::fs_helpers::*; +use crate::ctx::WasiCtx; +use crate::fdentry::FdEntry; +use crate::host::{Dirent, FileType}; +use crate::hostcalls_impl::{fd_filestat_set_times_impl, PathGet}; +use crate::sys::fdentry_impl::{determine_type_rights, OsHandle}; +use crate::sys::host_impl::{self, path_from_host}; +use crate::sys::hostcalls_impl::fs_helpers::PathGetExt; +use crate::{wasi, Error, Result}; +use log::{debug, trace}; +use std::convert::TryInto; +use std::fs::{File, Metadata, OpenOptions}; +use std::io::{self, Seek, SeekFrom}; +use std::os::windows::fs::{FileExt, OpenOptionsExt}; +use std::os::windows::prelude::{AsRawHandle, FromRawHandle}; +use std::path::{Path, PathBuf}; +use winx::file::{AccessMode, CreationDisposition, FileModeInformation, Flags}; + +fn read_at(mut file: &File, buf: &mut [u8], offset: u64) -> io::Result { + // get current cursor position + let cur_pos = file.seek(SeekFrom::Current(0))?; + // perform a seek read by a specified offset + let nread = file.seek_read(buf, offset)?; + // rewind the cursor back to the original position + file.seek(SeekFrom::Start(cur_pos))?; + Ok(nread) +} + +fn write_at(mut file: &File, buf: &[u8], offset: u64) -> io::Result { + // get current cursor position + let cur_pos = file.seek(SeekFrom::Current(0))?; + // perform a seek write by a specified offset + let nwritten = file.seek_write(buf, offset)?; + // rewind the cursor back to the original position + file.seek(SeekFrom::Start(cur_pos))?; + Ok(nwritten) +} + +// TODO refactor common code with unix +pub(crate) fn fd_pread( + file: &File, + buf: &mut [u8], + offset: wasi::__wasi_filesize_t, +) -> Result { + read_at(file, buf, offset).map_err(Into::into) +} + +// TODO refactor common code with unix +pub(crate) fn fd_pwrite(file: &File, buf: &[u8], offset: wasi::__wasi_filesize_t) -> Result { + write_at(file, buf, offset).map_err(Into::into) +} + +pub(crate) fn fd_fdstat_get(fd: &File) -> Result { + let mut fdflags = 0; + + let handle = unsafe { fd.as_raw_handle() }; + + let access_mode = winx::file::query_access_information(handle)?; + let mode = winx::file::query_mode_information(handle)?; + + // Append without write implies append-only (__WASI_FDFLAGS_APPEND) + if access_mode.contains(AccessMode::FILE_APPEND_DATA) + && !access_mode.contains(AccessMode::FILE_WRITE_DATA) + { + fdflags |= wasi::__WASI_FDFLAGS_APPEND; + } + + if mode.contains(FileModeInformation::FILE_WRITE_THROUGH) { + // Only report __WASI_FDFLAGS_SYNC + // This is technically the only one of the O_?SYNC flags Windows supports. + fdflags |= wasi::__WASI_FDFLAGS_SYNC; + } + + // Files do not support the `__WASI_FDFLAGS_NONBLOCK` flag + + Ok(fdflags) +} + +pub(crate) fn fd_fdstat_set_flags( + fd: &File, + fdflags: wasi::__wasi_fdflags_t, +) -> Result> { + let handle = unsafe { fd.as_raw_handle() }; + + let access_mode = winx::file::query_access_information(handle)?; + + let new_access_mode = file_access_mode_from_fdflags( + fdflags, + access_mode.contains(AccessMode::FILE_READ_DATA), + access_mode.contains(AccessMode::FILE_WRITE_DATA) + | access_mode.contains(AccessMode::FILE_APPEND_DATA), + ); + + unsafe { + Ok(Some(OsHandle::from(File::from_raw_handle( + winx::file::reopen_file(handle, new_access_mode, file_flags_from_fdflags(fdflags))?, + )))) + } +} + +pub(crate) fn fd_advise( + _file: &File, + advice: wasi::__wasi_advice_t, + _offset: wasi::__wasi_filesize_t, + _len: wasi::__wasi_filesize_t, +) -> Result<()> { + match advice { + wasi::__WASI_ADVICE_DONTNEED + | wasi::__WASI_ADVICE_SEQUENTIAL + | wasi::__WASI_ADVICE_WILLNEED + | wasi::__WASI_ADVICE_NOREUSE + | wasi::__WASI_ADVICE_RANDOM + | wasi::__WASI_ADVICE_NORMAL => {} + _ => return Err(Error::EINVAL), + } + + Ok(()) +} + +pub(crate) fn path_create_directory(resolved: PathGet) -> Result<()> { + let path = resolved.concatenate()?; + std::fs::create_dir(&path).map_err(Into::into) +} + +pub(crate) fn path_link(resolved_old: PathGet, resolved_new: PathGet) -> Result<()> { + unimplemented!("path_link") +} + +pub(crate) fn path_open( + resolved: PathGet, + read: bool, + write: bool, + oflags: wasi::__wasi_oflags_t, + fdflags: wasi::__wasi_fdflags_t, +) -> Result { + use winx::file::{AccessMode, CreationDisposition, Flags}; + + let is_trunc = oflags & wasi::__WASI_OFLAGS_TRUNC != 0; + + if is_trunc { + // Windows does not support append mode when opening for truncation + // This is because truncation requires `GENERIC_WRITE` access, which will override the removal + // of the `FILE_WRITE_DATA` permission. + if fdflags & wasi::__WASI_FDFLAGS_APPEND != 0 { + return Err(Error::ENOTSUP); + } + } + + // convert open flags + // note: the calls to `write(true)` are to bypass an internal OpenOption check + // the write flag will ultimately be ignored when `access_mode` is calculated below. + let mut opts = OpenOptions::new(); + match creation_disposition_from_oflags(oflags) { + CreationDisposition::CREATE_ALWAYS => { + opts.create(true).write(true); + } + CreationDisposition::CREATE_NEW => { + opts.create_new(true).write(true); + } + CreationDisposition::TRUNCATE_EXISTING => { + opts.truncate(true).write(true); + } + _ => {} + } + + let path = resolved.concatenate()?; + + match path.symlink_metadata().map(|metadata| metadata.file_type()) { + Ok(file_type) => { + // check if we are trying to open a symlink + if file_type.is_symlink() { + return Err(Error::ELOOP); + } + // check if we are trying to open a file as a dir + if file_type.is_file() && oflags & wasi::__WASI_OFLAGS_DIRECTORY != 0 { + return Err(Error::ENOTDIR); + } + } + Err(e) => match e.raw_os_error() { + Some(e) => { + use winx::winerror::WinError; + log::debug!("path_open at symlink_metadata error code={:?}", e); + let e = WinError::from_u32(e as u32); + + if e != WinError::ERROR_FILE_NOT_FOUND { + return Err(e.into()); + } + // file not found, let it proceed to actually + // trying to open it + } + None => { + log::debug!("Inconvertible OS error: {}", e); + return Err(Error::EIO); + } + }, + } + + let mut access_mode = file_access_mode_from_fdflags(fdflags, read, write); + + // Truncation requires the special `GENERIC_WRITE` bit set (this is why it doesn't work with append-only mode) + if is_trunc { + access_mode |= AccessMode::GENERIC_WRITE; + } + + opts.access_mode(access_mode.bits()) + .custom_flags(file_flags_from_fdflags(fdflags).bits()) + .open(&path) + .map_err(Into::into) +} + +fn creation_disposition_from_oflags(oflags: wasi::__wasi_oflags_t) -> CreationDisposition { + if oflags & wasi::__WASI_OFLAGS_CREAT != 0 { + if oflags & wasi::__WASI_OFLAGS_EXCL != 0 { + CreationDisposition::CREATE_NEW + } else { + CreationDisposition::CREATE_ALWAYS + } + } else if oflags & wasi::__WASI_OFLAGS_TRUNC != 0 { + CreationDisposition::TRUNCATE_EXISTING + } else { + CreationDisposition::OPEN_EXISTING + } +} + +fn file_access_mode_from_fdflags( + fdflags: wasi::__wasi_fdflags_t, + read: bool, + write: bool, +) -> AccessMode { + let mut access_mode = AccessMode::READ_CONTROL; + + // Note that `GENERIC_READ` and `GENERIC_WRITE` cannot be used to properly support append-only mode + // The file-specific flags `FILE_GENERIC_READ` and `FILE_GENERIC_WRITE` are used here instead + // These flags have the same semantic meaning for file objects, but allow removal of specific permissions (see below) + if read { + access_mode.insert(AccessMode::FILE_GENERIC_READ); + } + + if write { + access_mode.insert(AccessMode::FILE_GENERIC_WRITE); + } + + // For append, grant the handle FILE_APPEND_DATA access but *not* FILE_WRITE_DATA. + // This makes the handle "append only". + // Changes to the file pointer will be ignored (like POSIX's O_APPEND behavior). + if fdflags & wasi::__WASI_FDFLAGS_APPEND != 0 { + access_mode.insert(AccessMode::FILE_APPEND_DATA); + access_mode.remove(AccessMode::FILE_WRITE_DATA); + } + + access_mode +} + +fn file_flags_from_fdflags(fdflags: wasi::__wasi_fdflags_t) -> Flags { + // Enable backup semantics so directories can be opened as files + let mut flags = Flags::FILE_FLAG_BACKUP_SEMANTICS; + + // Note: __WASI_FDFLAGS_NONBLOCK is purposely being ignored for files + // While Windows does inherently support a non-blocking mode on files, the WASI API will + // treat I/O operations on files as synchronous. WASI might have an async-io API in the future. + + // Technically, Windows only supports __WASI_FDFLAGS_SYNC, but treat all the flags as the same. + if fdflags & wasi::__WASI_FDFLAGS_DSYNC != 0 + || fdflags & wasi::__WASI_FDFLAGS_RSYNC != 0 + || fdflags & wasi::__WASI_FDFLAGS_SYNC != 0 + { + flags.insert(Flags::FILE_FLAG_WRITE_THROUGH); + } + + flags +} + +fn dirent_from_path>( + path: P, + name: &str, + cookie: wasi::__wasi_dircookie_t, +) -> Result { + let path = path.as_ref(); + trace!("dirent_from_path: opening {}", path.to_string_lossy()); + + // To open a directory on Windows, FILE_FLAG_BACKUP_SEMANTICS flag must be used + let file = OpenOptions::new() + .custom_flags(Flags::FILE_FLAG_BACKUP_SEMANTICS.bits()) + .read(true) + .open(path)?; + let ty = file.metadata()?.file_type(); + Ok(Dirent { + ftype: host_impl::filetype_from_std(&ty), + name: name.to_owned(), + cookie, + ino: host_impl::file_serial_no(&file)?, + }) +} + +// On Windows there is apparently no support for seeking the directory stream in the OS. +// cf. https://github.com/WebAssembly/WASI/issues/61 +// +// The implementation here may perform in O(n^2) if the host buffer is O(1) +// and the number of directory entries is O(n). +// TODO: Add a heuristic optimization to achieve O(n) time in the most common case +// where fd_readdir is resumed where it previously finished +// +// Correctness of this approach relies upon one assumption: that the order of entries +// returned by `FindNextFileW` is stable, i.e. doesn't change if the directory +// contents stay the same. This invariant is crucial to be able to implement +// any kind of seeking whatsoever without having to read the whole directory at once +// and then return the data from cache. (which leaks memory) +// +// The MSDN documentation explicitly says that the order in which the search returns the files +// is not guaranteed, and is dependent on the file system. +// cf. https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findnextfilew +// +// This stackoverflow post suggests that `FindNextFileW` is indeed stable and that +// the order of directory entries depends **only** on the filesystem used, but the +// MSDN documentation is not clear about this. +// cf. https://stackoverflow.com/questions/47380739/is-findfirstfile-and-findnextfile-order-random-even-for-dvd +// +// Implementation details: +// Cookies for the directory entries start from 1. (0 is reserved by wasi::__WASI_DIRCOOKIE_START) +// . gets cookie = 1 +// .. gets cookie = 2 +// other entries, in order they were returned by FindNextFileW get subsequent integers as their cookies +pub(crate) fn fd_readdir( + fd: &File, + cookie: wasi::__wasi_dircookie_t, +) -> Result>> { + use winx::file::get_file_path; + + let cookie = cookie.try_into()?; + let path = get_file_path(fd)?; + // std::fs::ReadDir doesn't return . and .., so we need to emulate it + let path = Path::new(&path); + // The directory /.. is the same as / on Unix (at least on ext4), so emulate this behavior too + let parent = path.parent().unwrap_or(path); + let dot = dirent_from_path(path, ".", 1)?; + let dotdot = dirent_from_path(parent, "..", 2)?; + + trace!(" | fd_readdir impl: executing std::fs::ReadDir"); + let iter = path.read_dir()?.zip(3..).map(|(dir, no)| { + let dir: std::fs::DirEntry = dir?; + + Ok(Dirent { + name: path_from_host(dir.file_name())?, + ftype: host_impl::filetype_from_std(&dir.file_type()?), + ino: File::open(dir.path()).and_then(|f| host_impl::file_serial_no(&f))?, + cookie: no, + }) + }); + + // into_iter for arrays is broken and returns references instead of values, + // so we need to use vec![...] and do heap allocation + // See https://github.com/rust-lang/rust/issues/25725 + let iter = vec![dot, dotdot].into_iter().map(Ok).chain(iter); + + // Emulate seekdir(). This may give O(n^2) complexity if used with a + // small host_buf, but this is difficult to implement efficiently. + // + // See https://github.com/WebAssembly/WASI/issues/61 for more details. + Ok(iter.skip(cookie)) +} + +pub(crate) fn path_readlink(resolved: PathGet, buf: &mut [u8]) -> Result { + use winx::file::get_file_path; + + let path = resolved.concatenate()?; + let target_path = path.read_link()?; + + // since on Windows we are effectively emulating 'at' syscalls + // we need to strip the prefix from the absolute path + // as otherwise we will error out since WASI is not capable + // of dealing with absolute paths + let dir_path = get_file_path(resolved.dirfd())?; + let dir_path = PathBuf::from(strip_extended_prefix(dir_path)); + let target_path = target_path + .strip_prefix(dir_path) + .map_err(|_| Error::ENOTCAPABLE) + .and_then(|path| path.to_str().map(String::from).ok_or(Error::EILSEQ))?; + + if buf.len() > 0 { + let mut chars = target_path.chars(); + let mut nread = 0usize; + + for i in 0..buf.len() { + match chars.next() { + Some(ch) => { + buf[i] = ch as u8; + nread += 1; + } + None => break, + } + } + + Ok(nread) + } else { + Ok(0) + } +} + +fn strip_trailing_slashes_and_concatenate(resolved: &PathGet) -> Result> { + if resolved.path().ends_with('/') { + let suffix = resolved.path().trim_end_matches('/'); + concatenate(resolved.dirfd(), Path::new(suffix)).map(Some) + } else { + Ok(None) + } +} + +pub(crate) fn path_rename(resolved_old: PathGet, resolved_new: PathGet) -> Result<()> { + use std::fs; + + let old_path = resolved_old.concatenate()?; + let new_path = resolved_new.concatenate()?; + + // First sanity check: check we're not trying to rename dir to file or vice versa. + // NB on Windows, the former is actually permitted [std::fs::rename]. + // + // [std::fs::rename]: https://doc.rust-lang.org/std/fs/fn.rename.html + if old_path.is_dir() && new_path.is_file() { + return Err(Error::ENOTDIR); + } + // Second sanity check: check we're not trying to rename a file into a path + // ending in a trailing slash. + if old_path.is_file() && resolved_new.path().ends_with('/') { + return Err(Error::ENOTDIR); + } + + // TODO handle symlinks + + fs::rename(&old_path, &new_path).or_else(|e| match e.raw_os_error() { + Some(e) => { + use winx::winerror::WinError; + + log::debug!("path_rename at rename error code={:?}", e); + match WinError::from_u32(e as u32) { + WinError::ERROR_ACCESS_DENIED => { + // So most likely dealing with new_path == dir. + // Eliminate case old_path == file first. + if old_path.is_file() { + Err(Error::EISDIR) + } else { + // Ok, let's try removing an empty dir at new_path if it exists + // and is a nonempty dir. + fs::remove_dir(&new_path) + .and_then(|()| fs::rename(old_path, new_path)) + .map_err(Into::into) + } + } + WinError::ERROR_INVALID_NAME => { + // If source contains trailing slashes, check if we are dealing with + // a file instead of a dir, and if so, throw ENOTDIR. + if let Some(path) = strip_trailing_slashes_and_concatenate(&resolved_old)? { + if path.is_file() { + return Err(Error::ENOTDIR); + } + } + Err(WinError::ERROR_INVALID_NAME.into()) + } + e => Err(e.into()), + } + } + None => { + log::debug!("Inconvertible OS error: {}", e); + Err(Error::EIO) + } + }) +} + +pub(crate) fn fd_filestat_get(file: &std::fs::File) -> Result { + host_impl::filestat_from_win(file) +} + +pub(crate) fn path_filestat_get( + resolved: PathGet, + dirflags: wasi::__wasi_lookupflags_t, +) -> Result { + let path = resolved.concatenate()?; + let file = File::open(path)?; + host_impl::filestat_from_win(&file) +} + +pub(crate) fn path_filestat_set_times( + resolved: PathGet, + dirflags: wasi::__wasi_lookupflags_t, + st_atim: wasi::__wasi_timestamp_t, + mut st_mtim: wasi::__wasi_timestamp_t, + fst_flags: wasi::__wasi_fstflags_t, +) -> Result<()> { + use winx::file::AccessMode; + let path = resolved.concatenate()?; + let file = OpenOptions::new() + .access_mode(AccessMode::FILE_WRITE_ATTRIBUTES.bits()) + .open(path)?; + fd_filestat_set_times_impl(&file, st_atim, st_mtim, fst_flags) +} + +pub(crate) fn path_symlink(old_path: &str, resolved: PathGet) -> Result<()> { + use std::os::windows::fs::{symlink_dir, symlink_file}; + use winx::winerror::WinError; + + let old_path = concatenate(resolved.dirfd(), Path::new(old_path))?; + let new_path = resolved.concatenate()?; + + // try creating a file symlink + symlink_file(&old_path, &new_path).or_else(|e| { + match e.raw_os_error() { + Some(e) => { + log::debug!("path_symlink at symlink_file error code={:?}", e); + match WinError::from_u32(e as u32) { + WinError::ERROR_NOT_A_REPARSE_POINT => { + // try creating a dir symlink instead + symlink_dir(old_path, new_path).map_err(Into::into) + } + WinError::ERROR_ACCESS_DENIED => { + // does the target exist? + if new_path.exists() { + Err(Error::EEXIST) + } else { + Err(WinError::ERROR_ACCESS_DENIED.into()) + } + } + WinError::ERROR_INVALID_NAME => { + // does the target without trailing slashes exist? + if let Some(path) = strip_trailing_slashes_and_concatenate(&resolved)? { + if path.exists() { + return Err(Error::EEXIST); + } + } + Err(WinError::ERROR_INVALID_NAME.into()) + } + e => Err(e.into()), + } + } + None => { + log::debug!("Inconvertible OS error: {}", e); + Err(Error::EIO) + } + } + }) +} + +pub(crate) fn path_unlink_file(resolved: PathGet) -> Result<()> { + use std::fs; + use winx::winerror::WinError; + + let path = resolved.concatenate()?; + let file_type = path + .symlink_metadata() + .map(|metadata| metadata.file_type())?; + + // check if we're unlinking a symlink + // NB this will get cleaned up a lot when [std::os::windows::fs::FileTypeExt] + // stabilises + // + // [std::os::windows::fs::FileTypeExt]: https://doc.rust-lang.org/std/os/windows/fs/trait.FileTypeExt.html + if file_type.is_symlink() { + fs::remove_file(&path).or_else(|e| { + match e.raw_os_error() { + Some(e) => { + log::debug!("path_unlink_file at symlink_file error code={:?}", e); + match WinError::from_u32(e as u32) { + WinError::ERROR_ACCESS_DENIED => { + // try unlinking a dir symlink instead + fs::remove_dir(path).map_err(Into::into) + } + e => Err(e.into()), + } + } + None => { + log::debug!("Inconvertible OS error: {}", e); + Err(Error::EIO) + } + } + }) + } else if file_type.is_dir() { + Err(Error::EISDIR) + } else if file_type.is_file() { + fs::remove_file(path).map_err(Into::into) + } else { + Err(Error::EINVAL) + } +} + +pub(crate) fn path_remove_directory(resolved: PathGet) -> Result<()> { + let path = resolved.concatenate()?; + std::fs::remove_dir(&path).map_err(Into::into) +} diff --git a/crates/wasi-common/src/sys/windows/hostcalls_impl/fs_helpers.rs b/crates/wasi-common/src/sys/windows/hostcalls_impl/fs_helpers.rs new file mode 100644 index 0000000000..8fe392ccba --- /dev/null +++ b/crates/wasi-common/src/sys/windows/hostcalls_impl/fs_helpers.rs @@ -0,0 +1,149 @@ +#![allow(non_camel_case_types)] +use crate::hostcalls_impl::PathGet; +use crate::{wasi, Error, Result}; +use std::ffi::{OsStr, OsString}; +use std::fs::File; +use std::os::windows::ffi::{OsStrExt, OsStringExt}; +use std::path::{Path, PathBuf}; + +pub(crate) trait PathGetExt { + fn concatenate(&self) -> Result; +} + +impl PathGetExt for PathGet { + fn concatenate(&self) -> Result { + concatenate(self.dirfd(), Path::new(self.path())) + } +} + +pub(crate) fn path_open_rights( + rights_base: wasi::__wasi_rights_t, + rights_inheriting: wasi::__wasi_rights_t, + oflags: wasi::__wasi_oflags_t, + fdflags: wasi::__wasi_fdflags_t, +) -> (wasi::__wasi_rights_t, wasi::__wasi_rights_t) { + // which rights are needed on the dirfd? + let mut needed_base = wasi::__WASI_RIGHTS_PATH_OPEN; + let mut needed_inheriting = rights_base | rights_inheriting; + + // convert open flags + if oflags & wasi::__WASI_OFLAGS_CREAT != 0 { + needed_base |= wasi::__WASI_RIGHTS_PATH_CREATE_FILE; + } else if oflags & wasi::__WASI_OFLAGS_TRUNC != 0 { + needed_base |= wasi::__WASI_RIGHTS_PATH_FILESTAT_SET_SIZE; + } + + // convert file descriptor flags + if fdflags & wasi::__WASI_FDFLAGS_DSYNC != 0 + || fdflags & wasi::__WASI_FDFLAGS_RSYNC != 0 + || fdflags & wasi::__WASI_FDFLAGS_SYNC != 0 + { + needed_inheriting |= wasi::__WASI_RIGHTS_FD_DATASYNC; + needed_inheriting |= wasi::__WASI_RIGHTS_FD_SYNC; + } + + (needed_base, needed_inheriting) +} + +pub(crate) fn openat(dirfd: &File, path: &str) -> Result { + use std::fs::OpenOptions; + use std::os::windows::fs::OpenOptionsExt; + use winx::file::Flags; + use winx::winerror::WinError; + + let path = concatenate(dirfd, Path::new(path))?; + OpenOptions::new() + .read(true) + .custom_flags(Flags::FILE_FLAG_BACKUP_SEMANTICS.bits()) + .open(&path) + .map_err(|e| match e.raw_os_error() { + Some(e) => { + log::debug!("openat error={:?}", e); + match WinError::from_u32(e as u32) { + WinError::ERROR_INVALID_NAME => Error::ENOTDIR, + e => e.into(), + } + } + None => { + log::debug!("Inconvertible OS error: {}", e); + Error::EIO + } + }) +} + +pub(crate) fn readlinkat(dirfd: &File, s_path: &str) -> Result { + use winx::file::get_file_path; + use winx::winerror::WinError; + + let path = concatenate(dirfd, Path::new(s_path))?; + match path.read_link() { + Ok(target_path) => { + // since on Windows we are effectively emulating 'at' syscalls + // we need to strip the prefix from the absolute path + // as otherwise we will error out since WASI is not capable + // of dealing with absolute paths + let dir_path = get_file_path(dirfd)?; + let dir_path = PathBuf::from(strip_extended_prefix(dir_path)); + target_path + .strip_prefix(dir_path) + .map_err(|_| Error::ENOTCAPABLE) + .and_then(|path| path.to_str().map(String::from).ok_or(Error::EILSEQ)) + } + Err(e) => match e.raw_os_error() { + Some(e) => { + log::debug!("readlinkat error={:?}", e); + match WinError::from_u32(e as u32) { + WinError::ERROR_INVALID_NAME => { + if s_path.ends_with('/') { + // strip "/" and check if exists + let path = concatenate(dirfd, Path::new(s_path.trim_end_matches('/')))?; + if path.exists() && !path.is_dir() { + Err(Error::ENOTDIR) + } else { + Err(Error::ENOENT) + } + } else { + Err(Error::ENOENT) + } + } + e => Err(e.into()), + } + } + None => { + log::debug!("Inconvertible OS error: {}", e); + Err(Error::EIO) + } + }, + } +} + +pub(crate) fn strip_extended_prefix>(path: P) -> OsString { + let path: Vec = path.as_ref().encode_wide().collect(); + if &[92, 92, 63, 92] == &path[0..4] { + OsString::from_wide(&path[4..]) + } else { + OsString::from_wide(&path) + } +} + +pub(crate) fn concatenate>(dirfd: &File, path: P) -> Result { + use winx::file::get_file_path; + + // WASI is not able to deal with absolute paths + // so error out if absolute + if path.as_ref().is_absolute() { + return Err(Error::ENOTCAPABLE); + } + + let dir_path = get_file_path(dirfd)?; + // concatenate paths + let mut out_path = PathBuf::from(dir_path); + out_path.push(path.as_ref()); + // strip extended prefix; otherwise we will error out on any relative + // components with `out_path` + let out_path = PathBuf::from(strip_extended_prefix(out_path)); + + log::debug!("out_path={:?}", out_path); + + Ok(out_path) +} diff --git a/crates/wasi-common/src/sys/windows/hostcalls_impl/misc.rs b/crates/wasi-common/src/sys/windows/hostcalls_impl/misc.rs new file mode 100644 index 0000000000..7c5330127b --- /dev/null +++ b/crates/wasi-common/src/sys/windows/hostcalls_impl/misc.rs @@ -0,0 +1,401 @@ +#![allow(non_camel_case_types)] +#![allow(unused_unsafe)] +#![allow(unused)] +use crate::fdentry::Descriptor; +use crate::hostcalls_impl::{ClockEventData, FdEventData}; +use crate::memory::*; +use crate::sys::host_impl; +use crate::{error::WasiError, wasi, wasi32, Error, Result}; +use cpu_time::{ProcessTime, ThreadTime}; +use lazy_static::lazy_static; +use log::{debug, error, trace, warn}; +use std::convert::TryInto; +use std::io; +use std::os::windows::io::AsRawHandle; +use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender, TryRecvError}; +use std::sync::Mutex; +use std::thread; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +struct StdinPoll { + request_tx: Sender<()>, + notify_rx: Receiver, +} + +enum PollState { + Ready, + NotReady, // it's not ready, but we didn't wait + TimedOut, // it's not ready and a timeout has occurred + Error(WasiError), // not using the top-lever Error because it's not Clone +} + +enum WaitMode { + Timeout(Duration), + Infinite, + Immediate, +} + +impl StdinPoll { + // This function should not be used directly + // Correctness of this function crucially depends on the fact that + // mpsc::Receiver is !Sync. + fn poll(&self, wait_mode: WaitMode) -> PollState { + // Clean up possible unread result from the previous poll + match self.notify_rx.try_recv() { + Ok(_) | Err(TryRecvError::Empty) => {} + Err(TryRecvError::Disconnected) => panic!("notify_rx channel closed"), + } + + // Notify the worker thread that we want to poll stdin + self.request_tx.send(()).expect("request_tx channel closed"); + + // Wait for the worker thread to send a readiness notification + let pollret = match wait_mode { + WaitMode::Timeout(timeout) => { + self.notify_rx + .recv_timeout(timeout) + .unwrap_or_else(|e| match e { + RecvTimeoutError::Disconnected => panic!("notify_rx channel closed"), + RecvTimeoutError::Timeout => PollState::TimedOut, + }) + } + WaitMode::Infinite => self.notify_rx.recv().expect("notify_rx channel closed"), + WaitMode::Immediate => self.notify_rx.try_recv().unwrap_or_else(|e| match e { + TryRecvError::Disconnected => panic!("notify_rx channel closed"), + TryRecvError::Empty => PollState::NotReady, + }), + }; + + pollret + } + + fn event_loop(request_rx: Receiver<()>, notify_tx: Sender) -> ! { + use std::io::BufRead; + loop { + // Wait for the request to poll stdin + request_rx.recv().expect("request_rx channel closed"); + + // Wait for data to appear in stdin. + // If `fill_buf` returns any slice, then it means that either + // (a) there some data in stdin, if it's non-empty + // (b) EOF was received, if it's empty + // Linux returns `POLLIN` in both cases, and we imitate this behavior. + let resp = match std::io::stdin().lock().fill_buf() { + Ok(_) => PollState::Ready, + Err(e) => PollState::Error(Error::from(e).as_wasi_error()), + }; + + // Notify the requestor about data in stdin. They may have already timed out, + // then the next requestor will have to clean the channel. + notify_tx.send(resp).expect("notify_tx channel closed"); + } + } +} + +lazy_static! { + static ref START_MONOTONIC: Instant = Instant::now(); + static ref PERF_COUNTER_RES: u64 = get_perf_counter_resolution_ns(); + static ref STDIN_POLL: Mutex = { + let (request_tx, request_rx) = mpsc::channel(); + let (notify_tx, notify_rx) = mpsc::channel(); + thread::spawn(move || StdinPoll::event_loop(request_rx, notify_tx)); + Mutex::new(StdinPoll { + request_tx, + notify_rx, + }) + }; +} + +// Timer resolution on Windows is really hard. We may consider exposing the resolution of the respective +// timers as an associated function in the future. +pub(crate) fn clock_res_get(clock_id: wasi::__wasi_clockid_t) -> Result { + Ok(match clock_id { + // This is the best that we can do with std::time::SystemTime. + // Rust uses GetSystemTimeAsFileTime, which is said to have the resolution of + // 10ms or 55ms, [1] but MSDN doesn't confirm this in any way. + // Even the MSDN article on high resolution timestamps doesn't even mention the precision + // for this method. [3] + // + // The timer resolution can be queried using one of the functions: [2, 5] + // * NtQueryTimerResolution, which is undocumented and thus not exposed by the winapi crate + // * timeGetDevCaps, which returns the upper and lower bound for the precision, in ms. + // While the upper bound seems like something we could use, it's typically too high to be meaningful. + // For instance, the intervals return by the syscall are: + // * [1, 65536] on Wine + // * [1, 1000000] on Windows 10, which is up to (sic) 1000 seconds. + // + // It's possible to manually set the timer resolution, but this sounds like something which should + // only be done temporarily. [5] + // + // Alternatively, we could possibly use GetSystemTimePreciseAsFileTime in clock_time_get, but + // this syscall is only available starting from Windows 8. + // (we could possibly emulate it on earlier versions of Windows, see [4]) + // The MSDN are not clear on the resolution of GetSystemTimePreciseAsFileTime either, but a + // Microsoft devblog entry [1] suggests that it kind of combines GetSystemTimeAsFileTime with + // QueryPeformanceCounter, which probably means that those two should have the same resolution. + // + // See also this discussion about the use of GetSystemTimePreciseAsFileTime in Python stdlib, + // which in particular contains some resolution benchmarks. + // + // [1] https://devblogs.microsoft.com/oldnewthing/20170921-00/?p=97057 + // [2] http://www.windowstimestamp.com/description + // [3] https://docs.microsoft.com/en-us/windows/win32/sysinfo/acquiring-high-resolution-time-stamps?redirectedfrom=MSDN + // [4] https://www.codeproject.com/Tips/1011902/High-Resolution-Time-For-Windows + // [5] https://stackoverflow.com/questions/7685762/windows-7-timing-functions-how-to-use-getsystemtimeadjustment-correctly + // [6] https://bugs.python.org/issue19007 + wasi::__WASI_CLOCKID_REALTIME => 55_000_000, + // std::time::Instant uses QueryPerformanceCounter & QueryPerformanceFrequency internally + wasi::__WASI_CLOCKID_MONOTONIC => *PERF_COUNTER_RES, + // The best we can do is to hardcode the value from the docs. + // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getprocesstimes + wasi::__WASI_CLOCKID_PROCESS_CPUTIME_ID => 100, + // The best we can do is to hardcode the value from the docs. + // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getthreadtimes + wasi::__WASI_CLOCKID_THREAD_CPUTIME_ID => 100, + _ => return Err(Error::EINVAL), + }) +} + +pub(crate) fn clock_time_get(clock_id: wasi::__wasi_clockid_t) -> Result { + let duration = match clock_id { + wasi::__WASI_CLOCKID_REALTIME => get_monotonic_time(), + wasi::__WASI_CLOCKID_MONOTONIC => get_realtime_time()?, + wasi::__WASI_CLOCKID_PROCESS_CPUTIME_ID => get_proc_cputime()?, + wasi::__WASI_CLOCKID_THREAD_CPUTIME_ID => get_thread_cputime()?, + _ => return Err(Error::EINVAL), + }; + duration.as_nanos().try_into().map_err(Into::into) +} + +fn make_rw_event(event: &FdEventData, nbytes: Result) -> wasi::__wasi_event_t { + use crate::error::AsWasiError; + let error = nbytes.as_wasi_error(); + let nbytes = nbytes.unwrap_or_default(); + wasi::__wasi_event_t { + userdata: event.userdata, + r#type: event.r#type, + error: error.as_raw_errno(), + fd_readwrite: wasi::__wasi_event_fd_readwrite_t { nbytes, flags: 0 }, + } +} + +fn make_timeout_event(timeout: &ClockEventData) -> wasi::__wasi_event_t { + wasi::__wasi_event_t { + userdata: timeout.userdata, + r#type: wasi::__WASI_EVENTTYPE_CLOCK, + error: wasi::__WASI_ERRNO_SUCCESS, + fd_readwrite: wasi::__wasi_event_fd_readwrite_t { + nbytes: 0, + flags: 0, + }, + } +} + +fn handle_timeout( + timeout_event: ClockEventData, + timeout: Duration, + events: &mut Vec, +) { + thread::sleep(timeout); + handle_timeout_event(timeout_event, events); +} + +fn handle_timeout_event(timeout_event: ClockEventData, events: &mut Vec) { + let new_event = make_timeout_event(&timeout_event); + events.push(new_event); +} + +fn handle_rw_event(event: FdEventData, out_events: &mut Vec) { + let size = match event.descriptor { + Descriptor::OsHandle(os_handle) => { + if event.r#type == wasi::__WASI_EVENTTYPE_FD_READ { + os_handle.metadata().map(|m| m.len()).map_err(Into::into) + } else { + // The spec is unclear what nbytes should actually be for __WASI_EVENTTYPE_FD_WRITE and + // the implementation on Unix just returns 0 here, so it's probably fine + // to do the same on Windows for now. + // cf. https://github.com/WebAssembly/WASI/issues/148 + Ok(0) + } + } + // We return the only universally correct lower bound, see the comment later in the function. + Descriptor::Stdin => Ok(1), + // On Unix, ioctl(FIONREAD) will return 0 for stdout/stderr. Emulate the same behavior on Windows. + Descriptor::Stdout | Descriptor::Stderr => Ok(0), + }; + + let new_event = make_rw_event(&event, size); + out_events.push(new_event); +} + +fn handle_error_event( + event: FdEventData, + error: Error, + out_events: &mut Vec, +) { + let new_event = make_rw_event(&event, Err(error)); + out_events.push(new_event); +} + +pub(crate) fn poll_oneoff( + timeout: Option, + fd_events: Vec, + events: &mut Vec, +) -> Result<()> { + use std::fs::Metadata; + use std::thread; + + let timeout = timeout + .map(|event| { + event + .delay + .try_into() + .map(Duration::from_nanos) + .map(|dur| (event, dur)) + }) + .transpose()?; + + // With no events to listen, poll_oneoff just becomes a sleep. + if fd_events.is_empty() { + match timeout { + Some((event, dur)) => return Ok(handle_timeout(event, dur, events)), + // The implementation has to return Ok(()) in this case, + // cf. the comment in src/hostcalls_impl/misc.rs + None => return Ok(()), + } + } + + let mut stdin_events = vec![]; + let mut immediate_events = vec![]; + let mut pipe_events = vec![]; + + for event in fd_events { + match event.descriptor { + Descriptor::Stdin if event.r#type == wasi::__WASI_EVENTTYPE_FD_READ => { + stdin_events.push(event) + } + // stdout/stderr are always considered ready to write because there seems to + // be no way of checking if a write to stdout would block. + // + // If stdin is polled for anything else then reading, then it is also + // considered immediately ready, following the behavior on Linux. + Descriptor::Stdin | Descriptor::Stderr | Descriptor::Stdout => { + immediate_events.push(event) + } + Descriptor::OsHandle(os_handle) => { + let ftype = unsafe { winx::file::get_file_type(os_handle.as_raw_handle()) }?; + if ftype.is_unknown() || ftype.is_char() { + debug!("poll_oneoff: unsupported file type: {:?}", ftype); + handle_error_event(event, Error::ENOTSUP, events); + } else if ftype.is_disk() { + immediate_events.push(event); + } else if ftype.is_pipe() { + pipe_events.push(event); + } else { + unreachable!(); + } + } + } + } + + let immediate = !immediate_events.is_empty(); + // Process all the events that do not require waiting. + if immediate { + trace!(" | have immediate events, will return immediately"); + for mut event in immediate_events { + handle_rw_event(event, events); + } + } + if !stdin_events.is_empty() { + // During the firt request to poll stdin, we spin up a separate thread to + // waiting for data to arrive on stdin. This thread will not terminate. + // + // We'd like to do the following: + // (1) wait in a non-blocking way for data to be available in stdin, with timeout + // (2) find out, how many bytes are there available to be read. + // + // One issue is that we are currently relying on the Rust libstd for interaction + // with stdin. More precisely, `io::stdin` is used via the `BufRead` trait, + // in the `fd_read` function, which always does buffering on the libstd side. [1] + // This means that even if there's still some unread data in stdin, + // the lower-level Windows system calls may return false negatives, + // claiming that stdin is empty. + // + // Theoretically, one could use `WaitForSingleObject` on the stdin handle + // to achieve (1). Unfortunately, this function doesn't seem to honor the + // requested timeout and to misbehaves after the stdin is closed. + // + // There appears to be no way of achieving (2) on Windows. + // [1]: https://github.com/rust-lang/rust/pull/12422 + let waitmode = if immediate { + trace!(" | tentatively checking stdin"); + WaitMode::Immediate + } else { + trace!(" | passively waiting on stdin"); + match timeout { + Some((event, dur)) => WaitMode::Timeout(dur), + None => WaitMode::Infinite, + } + }; + let state = STDIN_POLL.lock().unwrap().poll(waitmode); + for event in stdin_events { + match state { + PollState::Ready => handle_rw_event(event, events), + PollState::NotReady => {} // not immediately available, so just ignore + PollState::TimedOut => handle_timeout_event(timeout.unwrap().0, events), + PollState::Error(e) => handle_error_event(event, Error::Wasi(e), events), + } + } + } + + if !immediate && !pipe_events.is_empty() { + trace!(" | actively polling pipes"); + match timeout { + Some((event, dur)) => { + // In the tests stdin is replaced with a dummy pipe, so for now + // we just time out. Support for pipes will be decided later on. + warn!("Polling pipes not supported on Windows, will just time out."); + handle_timeout(event, dur, events); + } + None => { + error!("Polling only pipes with no timeout not supported on Windows."); + return Err(Error::ENOTSUP); + } + } + } + + Ok(()) +} + +fn get_monotonic_time() -> Duration { + // We're circumventing the fact that we can't get a Duration from an Instant + // The epoch of __WASI_CLOCKID_MONOTONIC is undefined, so we fix a time point once + // and count relative to this time point. + // + // The alternative would be to copy over the implementation of std::time::Instant + // to our source tree and add a conversion to std::time::Duration + START_MONOTONIC.elapsed() +} + +fn get_realtime_time() -> Result { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| Error::EFAULT) +} + +fn get_proc_cputime() -> Result { + Ok(ProcessTime::try_now()?.as_duration()) +} + +fn get_thread_cputime() -> Result { + Ok(ThreadTime::try_now()?.as_duration()) +} + +fn get_perf_counter_resolution_ns() -> u64 { + use winx::time::perf_counter_frequency; + const NANOS_PER_SEC: u64 = 1_000_000_000; + // This should always succeed starting from Windows XP, so it's fine to panic in case of an error. + let freq = perf_counter_frequency().expect("QueryPerformanceFrequency returned an error"); + let epsilon = NANOS_PER_SEC / freq; + epsilon +} diff --git a/crates/wasi-common/src/sys/windows/hostcalls_impl/mod.rs b/crates/wasi-common/src/sys/windows/hostcalls_impl/mod.rs new file mode 100644 index 0000000000..fdbf448f80 --- /dev/null +++ b/crates/wasi-common/src/sys/windows/hostcalls_impl/mod.rs @@ -0,0 +1,8 @@ +//! Windows-specific hostcalls that implement +//! [WASI](https://github.com/bytecodealliance/wasmtime-wasi/blob/wasi/docs/WASI-overview.md). +mod fs; +pub(crate) mod fs_helpers; +mod misc; + +pub(crate) use self::fs::*; +pub(crate) use self::misc::*; diff --git a/crates/wasi-common/src/sys/windows/mod.rs b/crates/wasi-common/src/sys/windows/mod.rs new file mode 100644 index 0000000000..398d945323 --- /dev/null +++ b/crates/wasi-common/src/sys/windows/mod.rs @@ -0,0 +1,32 @@ +pub(crate) mod fdentry_impl; +pub(crate) mod host_impl; +pub(crate) mod hostcalls_impl; + +use crate::Result; +use std::fs::{File, OpenOptions}; +use std::path::Path; + +pub(crate) fn dev_null() -> Result { + OpenOptions::new() + .read(true) + .write(true) + .open("NUL") + .map_err(Into::into) +} + +pub fn preopen_dir>(path: P) -> Result { + use std::fs::OpenOptions; + use std::os::windows::fs::OpenOptionsExt; + use winapi::um::winbase::FILE_FLAG_BACKUP_SEMANTICS; + + // To open a directory using CreateFile, specify the + // FILE_FLAG_BACKUP_SEMANTICS flag as part of dwFileFlags... + // cf. https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-createfile2 + OpenOptions::new() + .create(false) + .write(true) + .read(true) + .attributes(FILE_FLAG_BACKUP_SEMANTICS) + .open(path) + .map_err(Into::into) +} diff --git a/crates/wasi-common/src/wasi.rs b/crates/wasi-common/src/wasi.rs new file mode 100644 index 0000000000..e418546a07 --- /dev/null +++ b/crates/wasi-common/src/wasi.rs @@ -0,0 +1,119 @@ +//! Types and constants shared between 32-bit and 64-bit wasi. Types involving +//! pointer or `usize`-sized data are excluded here, so this file only contains +//! fixed-size types, so it's host/target independent. + +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(dead_code)] + +use wig::witx_wasi_types; + +witx_wasi_types!("snapshot" "wasi_snapshot_preview1"); + +pub(crate) const RIGHTS_ALL: __wasi_rights_t = __WASI_RIGHTS_FD_DATASYNC + | __WASI_RIGHTS_FD_READ + | __WASI_RIGHTS_FD_SEEK + | __WASI_RIGHTS_FD_FDSTAT_SET_FLAGS + | __WASI_RIGHTS_FD_SYNC + | __WASI_RIGHTS_FD_TELL + | __WASI_RIGHTS_FD_WRITE + | __WASI_RIGHTS_FD_ADVISE + | __WASI_RIGHTS_FD_ALLOCATE + | __WASI_RIGHTS_PATH_CREATE_DIRECTORY + | __WASI_RIGHTS_PATH_CREATE_FILE + | __WASI_RIGHTS_PATH_LINK_SOURCE + | __WASI_RIGHTS_PATH_LINK_TARGET + | __WASI_RIGHTS_PATH_OPEN + | __WASI_RIGHTS_FD_READDIR + | __WASI_RIGHTS_PATH_READLINK + | __WASI_RIGHTS_PATH_RENAME_SOURCE + | __WASI_RIGHTS_PATH_RENAME_TARGET + | __WASI_RIGHTS_PATH_FILESTAT_GET + | __WASI_RIGHTS_PATH_FILESTAT_SET_SIZE + | __WASI_RIGHTS_PATH_FILESTAT_SET_TIMES + | __WASI_RIGHTS_FD_FILESTAT_GET + | __WASI_RIGHTS_FD_FILESTAT_SET_SIZE + | __WASI_RIGHTS_FD_FILESTAT_SET_TIMES + | __WASI_RIGHTS_PATH_SYMLINK + | __WASI_RIGHTS_PATH_UNLINK_FILE + | __WASI_RIGHTS_PATH_REMOVE_DIRECTORY + | __WASI_RIGHTS_POLL_FD_READWRITE + | __WASI_RIGHTS_SOCK_SHUTDOWN; + +// Block and character device interaction is outside the scope of +// WASI. Simply allow everything. +pub(crate) const RIGHTS_BLOCK_DEVICE_BASE: __wasi_rights_t = RIGHTS_ALL; +pub(crate) const RIGHTS_BLOCK_DEVICE_INHERITING: __wasi_rights_t = RIGHTS_ALL; +pub(crate) const RIGHTS_CHARACTER_DEVICE_BASE: __wasi_rights_t = RIGHTS_ALL; +pub(crate) const RIGHTS_CHARACTER_DEVICE_INHERITING: __wasi_rights_t = RIGHTS_ALL; + +// Only allow directory operations on directories. Directories can only +// yield file descriptors to other directories and files. +pub(crate) const RIGHTS_DIRECTORY_BASE: __wasi_rights_t = __WASI_RIGHTS_FD_FDSTAT_SET_FLAGS + | __WASI_RIGHTS_FD_SYNC + | __WASI_RIGHTS_FD_ADVISE + | __WASI_RIGHTS_PATH_CREATE_DIRECTORY + | __WASI_RIGHTS_PATH_CREATE_FILE + | __WASI_RIGHTS_PATH_LINK_SOURCE + | __WASI_RIGHTS_PATH_LINK_TARGET + | __WASI_RIGHTS_PATH_OPEN + | __WASI_RIGHTS_FD_READDIR + | __WASI_RIGHTS_PATH_READLINK + | __WASI_RIGHTS_PATH_RENAME_SOURCE + | __WASI_RIGHTS_PATH_RENAME_TARGET + | __WASI_RIGHTS_PATH_FILESTAT_GET + | __WASI_RIGHTS_PATH_FILESTAT_SET_SIZE + | __WASI_RIGHTS_PATH_FILESTAT_SET_TIMES + | __WASI_RIGHTS_FD_FILESTAT_GET + | __WASI_RIGHTS_FD_FILESTAT_SET_TIMES + | __WASI_RIGHTS_PATH_SYMLINK + | __WASI_RIGHTS_PATH_UNLINK_FILE + | __WASI_RIGHTS_PATH_REMOVE_DIRECTORY + | __WASI_RIGHTS_POLL_FD_READWRITE; +pub(crate) const RIGHTS_DIRECTORY_INHERITING: __wasi_rights_t = + RIGHTS_DIRECTORY_BASE | RIGHTS_REGULAR_FILE_BASE; + +// Operations that apply to regular files. +pub(crate) const RIGHTS_REGULAR_FILE_BASE: __wasi_rights_t = __WASI_RIGHTS_FD_DATASYNC + | __WASI_RIGHTS_FD_READ + | __WASI_RIGHTS_FD_SEEK + | __WASI_RIGHTS_FD_FDSTAT_SET_FLAGS + | __WASI_RIGHTS_FD_SYNC + | __WASI_RIGHTS_FD_TELL + | __WASI_RIGHTS_FD_WRITE + | __WASI_RIGHTS_FD_ADVISE + | __WASI_RIGHTS_FD_ALLOCATE + | __WASI_RIGHTS_FD_FILESTAT_GET + | __WASI_RIGHTS_FD_FILESTAT_SET_SIZE + | __WASI_RIGHTS_FD_FILESTAT_SET_TIMES + | __WASI_RIGHTS_POLL_FD_READWRITE; +pub(crate) const RIGHTS_REGULAR_FILE_INHERITING: __wasi_rights_t = 0; + +// Operations that apply to sockets and socket pairs. +pub(crate) const RIGHTS_SOCKET_BASE: __wasi_rights_t = __WASI_RIGHTS_FD_READ + | __WASI_RIGHTS_FD_FDSTAT_SET_FLAGS + | __WASI_RIGHTS_FD_WRITE + | __WASI_RIGHTS_FD_FILESTAT_GET + | __WASI_RIGHTS_POLL_FD_READWRITE + | __WASI_RIGHTS_SOCK_SHUTDOWN; +pub(crate) const RIGHTS_SOCKET_INHERITING: __wasi_rights_t = RIGHTS_ALL; + +// Operations that apply to TTYs. +pub(crate) const RIGHTS_TTY_BASE: __wasi_rights_t = __WASI_RIGHTS_FD_READ + | __WASI_RIGHTS_FD_FDSTAT_SET_FLAGS + | __WASI_RIGHTS_FD_WRITE + | __WASI_RIGHTS_FD_FILESTAT_GET + | __WASI_RIGHTS_POLL_FD_READWRITE; +#[allow(unused)] +pub(crate) const RIGHTS_TTY_INHERITING: __wasi_rights_t = 0; + +pub fn whence_to_str(whence: __wasi_whence_t) -> &'static str { + match whence { + __WASI_WHENCE_CUR => "__WASI_WHENCE_CUR", + __WASI_WHENCE_END => "__WASI_WHENCE_END", + __WASI_WHENCE_SET => "__WASI_WHENCE_SET", + other => panic!("Undefined whence value {:?}", other), + } +} + +pub const __WASI_DIRCOOKIE_START: __wasi_dircookie_t = 0; diff --git a/crates/wasi-common/src/wasi32.rs b/crates/wasi-common/src/wasi32.rs new file mode 100644 index 0000000000..11460010c8 --- /dev/null +++ b/crates/wasi-common/src/wasi32.rs @@ -0,0 +1,15 @@ +//! Types and constants specific to 32-bit wasi. These are similar to the types +//! in the `host` module, but pointers and `usize` values are replaced with +//! `u32`-sized types. + +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(dead_code)] + +use crate::wasi::*; +use wig::witx_wasi32_types; + +pub type uintptr_t = u32; +pub type size_t = u32; + +witx_wasi32_types!("snapshot" "wasi_snapshot_preview1"); diff --git a/crates/wasi-common/wig/Cargo.toml b/crates/wasi-common/wig/Cargo.toml new file mode 100644 index 0000000000..a46335ba24 --- /dev/null +++ b/crates/wasi-common/wig/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "wig" +version = "0.12.0" +authors = ["Dan Gohman "] +description = "WebAssembly Interface Generator" +license = "Apache-2.0 WITH LLVM-exception" +categories = ["wasm"] +keywords = ["webassembly", "wasm"] +repository = "https://github.com/bytecodealliance/wasmtime" +edition = "2018" +include = ["src/**/*", "LICENSE", "WASI"] + +[lib] +proc-macro = true + +[dependencies] +quote = "1.0.2" +proc-macro2 = "1.0.6" +heck = "0.3.1" +witx = { path = "WASI/tools/witx", version = "0.8.0" } + +[badges] +maintenance = { status = "actively-developed" } diff --git a/crates/wasi-common/wig/LICENSE b/crates/wasi-common/wig/LICENSE new file mode 100644 index 0000000000..f9d81955f4 --- /dev/null +++ b/crates/wasi-common/wig/LICENSE @@ -0,0 +1,220 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + diff --git a/crates/wasi-common/wig/WASI b/crates/wasi-common/wig/WASI new file mode 160000 index 0000000000..85df508517 --- /dev/null +++ b/crates/wasi-common/wig/WASI @@ -0,0 +1 @@ +Subproject commit 85df5085172a75f2a490a89833821c85cdbfaba7 diff --git a/crates/wasi-common/wig/src/hostcalls.rs b/crates/wasi-common/wig/src/hostcalls.rs new file mode 100644 index 0000000000..df7d813f1d --- /dev/null +++ b/crates/wasi-common/wig/src/hostcalls.rs @@ -0,0 +1,131 @@ +use crate::utils; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; + +pub fn define(args: TokenStream) -> TokenStream { + let (path, phase) = utils::witx_path_from_args(args); + let doc = match witx::load(&[&path]) { + Ok(doc) => doc, + Err(e) => { + panic!("error opening file {}: {}", path, e); + } + }; + + let mut ret = TokenStream::new(); + + let old = match phase.as_str() { + "snapshot" => false, + "old/snapshot_0" => true, + s => panic!("unsupported phase: {}", s), + }; + + for module in doc.modules() { + for func in module.funcs() { + ret.extend(generate_wrappers(&func, old)); + } + } + + return ret; +} + +fn generate_wrappers(func: &witx::InterfaceFunc, old: bool) -> TokenStream { + let name = format_ident!("{}", func.name.as_str()); + let mut arg_declarations = Vec::new(); + let mut arg_names = Vec::new(); + + for param in func.params.iter() { + let name = utils::param_name(param); + + if let witx::TypePassedBy::PointerLengthPair = param.tref.type_().passed_by() { + let ptr = format_ident!("{}_ptr", name); + let len = format_ident!("{}_len", name); + arg_declarations.push(quote! { #ptr: super::wasi32::uintptr_t }); + arg_declarations.push(quote! { #len: super::wasi32::size_t }); + arg_names.push(ptr); + arg_names.push(len); + continue; + } + + match ¶m.tref { + witx::TypeRef::Name(n) => { + if n.name.as_str() == "size" { + arg_declarations.push(quote! { #name: super::wasi32::size_t }); + } else { + let ty_name = format_ident!("__wasi_{}_t", n.name.as_str()); + arg_declarations.push(quote! { #name: super::wasi::#ty_name }); + } + } + witx::TypeRef::Value(v) => match &**v { + witx::Type::ConstPointer(_) | witx::Type::Pointer(_) => { + arg_declarations.push(quote! { #name: super::wasi32::uintptr_t }); + } + _ => panic!("unexpected value type"), + }, + } + arg_names.push(name); + } + + let mut ret = quote!(()); + + for (i, result) in func.results.iter().enumerate() { + if i == 0 { + match &result.tref { + witx::TypeRef::Name(n) => { + let ty_name = format_ident!("__wasi_{}_t", n.name.as_str()); + ret = quote! { super::wasi::#ty_name }; + } + witx::TypeRef::Value(_) => panic!("unexpected value type"), + } + continue; + } + let name = utils::param_name(result); + arg_declarations.push(quote! { #name: super::wasi32::uintptr_t }); + arg_names.push(name); + } + + let call = quote! { + super::hostcalls_impl::#name(wasi_ctx, memory, #(#arg_names,)*) + }; + let body = if func.results.len() == 0 { + call + } else { + quote! { + let ret = #call + .err() + .unwrap_or(super::Error::ESUCCESS) + .as_wasi_error(); + log::trace!(" | errno={}", ret); + ret.as_raw_errno() + } + }; + + let c_abi_name = if old { + format_ident!("old_wasi_common_{}", name) + } else { + format_ident!("wasi_common_{}", name) + }; + + quote! { + pub unsafe fn #name( + wasi_ctx: &mut super::WasiCtx, + memory: &mut [u8], + #(#arg_declarations,)* + ) -> #ret { + #body + } + + #[no_mangle] + pub unsafe fn #c_abi_name( + wasi_ctx: *mut super::WasiCtx, + memory: *mut u8, + memory_len: usize, + #(#arg_declarations,)* + ) -> #ret { + #name( + &mut *wasi_ctx, + std::slice::from_raw_parts_mut(memory, memory_len), + #(#arg_names,)* + ) + } + } +} diff --git a/crates/wasi-common/wig/src/lib.rs b/crates/wasi-common/wig/src/lib.rs new file mode 100644 index 0000000000..d0fd3f41e0 --- /dev/null +++ b/crates/wasi-common/wig/src/lib.rs @@ -0,0 +1,44 @@ +extern crate proc_macro; + +mod hostcalls; +mod raw_types; +mod utils; +mod wasi; + +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; + +#[proc_macro] +pub fn witx_host_types(args: TokenStream) -> TokenStream { + TokenStream::from(raw_types::gen( + TokenStream2::from(args), + raw_types::Mode::Host, + )) +} + +#[proc_macro] +pub fn witx_wasi_types(args: TokenStream) -> TokenStream { + TokenStream::from(raw_types::gen( + TokenStream2::from(args), + raw_types::Mode::Wasi, + )) +} + +#[proc_macro] +pub fn witx_wasi32_types(args: TokenStream) -> TokenStream { + TokenStream::from(raw_types::gen( + TokenStream2::from(args), + raw_types::Mode::Wasi32, + )) +} + +/// A single-use macro in the `wasmtime-wasi` crate. +#[proc_macro] +pub fn define_wasi_struct(args: TokenStream) -> TokenStream { + wasi::define_struct(args.into()).into() +} + +#[proc_macro] +pub fn define_hostcalls(args: TokenStream) -> TokenStream { + hostcalls::define(args.into()).into() +} diff --git a/crates/wasi-common/wig/src/raw_types.rs b/crates/wasi-common/wig/src/raw_types.rs new file mode 100644 index 0000000000..04cbd26fc6 --- /dev/null +++ b/crates/wasi-common/wig/src/raw_types.rs @@ -0,0 +1,359 @@ +//! Translate witx types to Rust. + +use crate::utils; +use heck::ShoutySnakeCase; +use proc_macro2::{Delimiter, Group, Literal, TokenStream, TokenTree}; +use quote::{format_ident, quote}; +use std::convert::TryFrom; + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Mode { + Host, + Wasi32, + Wasi, +} + +impl Mode { + pub fn include_target_types(&self) -> bool { + match self { + Mode::Host | Mode::Wasi32 => true, + Mode::Wasi => false, + } + } +} + +pub fn gen(args: TokenStream, mode: Mode) -> TokenStream { + let mut output = TokenStream::new(); + + let (path, _phase) = utils::witx_path_from_args(args); + let doc = match witx::load(&[&path]) { + Ok(doc) => doc, + Err(e) => { + panic!("error opening file {}: {}", path, e); + } + }; + + gen_datatypes(&mut output, &doc, mode); + + output +} + +fn gen_datatypes(output: &mut TokenStream, doc: &witx::Document, mode: Mode) { + let mut test_contents = TokenStream::new(); + for namedtype in doc.typenames() { + if mode.include_target_types() != namedtype_has_target_size(&namedtype) { + continue; + } + gen_datatype(output, &mut test_contents, mode, &namedtype); + } + match mode { + Mode::Wasi | Mode::Wasi32 => output.extend(quote! { + #[cfg(test)] + mod test { + use super::*; + #test_contents + } + }), + Mode::Host => {} // Don't emit tests for host reprs - the layout is different + } +} + +fn gen_datatype( + output: &mut TokenStream, + test_contents: &mut TokenStream, + mode: Mode, + namedtype: &witx::NamedType, +) { + let wasi_name = format_ident!("__wasi_{}_t", namedtype.name.as_str()); + let (size, align) = { + use witx::Layout; + let sa = namedtype.type_().mem_size_align(); + (sa.size, sa.align) + }; + let mut test_code = quote! { + assert_eq!(::std::mem::size_of::<#wasi_name>(), #size, concat!("Size of: ", stringify!(#wasi_name))); + assert_eq!(::std::mem::align_of::<#wasi_name>(), #align, concat!("Align of: ", stringify!(#wasi_name))); + }; + match &namedtype.tref { + witx::TypeRef::Name(alias_to) => { + let to = tref_tokens(mode, &alias_to.tref); + output.extend(quote!(pub type #wasi_name = #to;)); + } + witx::TypeRef::Value(v) => match &**v { + witx::Type::Int(_) => panic!("unsupported int datatype"), + witx::Type::Enum(e) => { + let repr = int_repr_tokens(e.repr); + output.extend(quote!(pub type #wasi_name = #repr;)); + for (index, variant) in e.variants.iter().enumerate() { + let value_name = format_ident!( + "__WASI_{}_{}", + namedtype.name.as_str().to_shouty_snake_case(), + variant.name.as_str().to_shouty_snake_case() + ); + let index_name = Literal::usize_unsuffixed(index); + output.extend(quote!(pub const #value_name: #wasi_name = #index_name;)); + } + } + witx::Type::Flags(f) => { + let repr = int_repr_tokens(f.repr); + output.extend(quote!(pub type #wasi_name = #repr;)); + for (index, flag) in f.flags.iter().enumerate() { + let value_name = format_ident!( + "__WASI_{}_{}", + namedtype.name.as_str().to_shouty_snake_case(), + flag.name.as_str().to_shouty_snake_case() + ); + let flag_value = Literal::u128_unsuffixed( + 1u128 + .checked_shl(u32::try_from(index).expect("flag value overflow")) + .expect("flag value overflow"), + ); + output.extend(quote!(pub const #value_name: #wasi_name = #flag_value;)); + } + } + witx::Type::Struct(s) => { + output.extend(quote!(#[repr(C)])); + // Types which contain unions can't trivially implement Debug, + // Hash, or Eq, because the type itself doesn't record which + // union member is active. + if struct_has_union(&s) { + output.extend(quote!(#[derive(Copy, Clone)])); + output.extend(quote!(#[allow(missing_debug_implementations)])); + } else { + output.extend(quote!(#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)])); + } + + output.extend(quote!(pub struct #wasi_name)); + + let mut inner = TokenStream::new(); + for ml in s.member_layout().iter() { + let member_name = format_ident!("r#{}", ml.member.name.as_str()); + let member_type = tref_tokens(mode, &ml.member.tref); + let offset = ml.offset; + inner.extend(quote!(pub #member_name: #member_type,)); + test_code.extend(quote!{ + assert_eq!( + unsafe { &(*(::std::ptr::null::<#wasi_name>())).#member_name as *const _ as usize }, + #offset, + concat!( + "Offset of field: ", + stringify!(#wasi_name), + "::", + stringify!(#member_name), + ) + ); + }); + } + let braced = Group::new(Delimiter::Brace, inner); + output.extend(TokenStream::from(TokenTree::Group(braced))); + } + witx::Type::Union(u) => { + let u_name = format_ident!("__wasi_{}_u_t", namedtype.name.as_str()); + output.extend(quote!(#[repr(C)])); + output.extend(quote!(#[derive(Copy, Clone)])); + output.extend(quote!(#[allow(missing_debug_implementations)])); + + output.extend(quote!(pub union #u_name)); + + let mut inner = TokenStream::new(); + for variant in &u.variants { + let variant_name = format_ident!("r#{}", variant.name.as_str()); + if let Some(ref tref) = variant.tref { + let variant_type = tref_tokens(mode, tref); + inner.extend(quote!(pub #variant_name: #variant_type,)); + } else { + inner.extend(quote!(pub #variant_name: (),)); + } + } + let braced = Group::new(Delimiter::Brace, inner); + output.extend(TokenStream::from(TokenTree::Group(braced))); + + output.extend(quote!(#[repr(C)])); + output.extend(quote!(#[derive(Copy, Clone)])); + output.extend(quote!(#[allow(missing_debug_implementations)])); + + output.extend(quote!(pub struct #wasi_name)); + let tag_name = format_ident!("__wasi_{}_t", u.tag.name.as_str()); + let inner = quote!(pub tag: #tag_name, pub u: #u_name,); + output.extend(TokenStream::from(TokenTree::Group(Group::new( + Delimiter::Brace, + inner, + )))); + } + witx::Type::Handle(_h) => { + output.extend(quote!(pub type #wasi_name = u32;)); + } + witx::Type::Builtin(b) => { + if namedtype.name.as_str() == "size" { + match mode { + Mode::Host => output.extend(quote!(pub type #wasi_name = usize;)), + Mode::Wasi => panic!("size has target-specific size"), + Mode::Wasi32 => output.extend(quote!(pub type #wasi_name = u32;)), + } + } else { + let b_type = builtin_tokens(mode, *b); + output.extend(quote!(pub type #wasi_name = #b_type;)); + } + } + witx::Type::Pointer { .. } + | witx::Type::ConstPointer { .. } + | witx::Type::Array { .. } => { + let tref_tokens = tref_tokens(mode, &namedtype.tref); + output.extend(quote!(pub type #wasi_name = #tref_tokens;)); + } + }, + } + + if namedtype.name.as_str() == "errno" { + // Generate strerror for errno type + gen_errno_strerror(output, namedtype); + } + + let test_func_name = format_ident!("wig_test_layout_{}", namedtype.name.as_str()); + test_contents.extend(quote! { + #[test] + fn #test_func_name() { + #test_code + } + }); +} + +fn gen_errno_strerror(output: &mut TokenStream, namedtype: &witx::NamedType) { + let inner = match &namedtype.tref { + witx::TypeRef::Value(v) => match &**v { + witx::Type::Enum(e) => e, + x => panic!("expected Enum('errno'), instead received {:?}", x), + }, + x => panic!("expected Enum('errno'), instead received {:?}", x), + }; + let mut inner_group = TokenStream::new(); + for variant in &inner.variants { + let value_name = format_ident!( + "__WASI_ERRNO_{}", + variant.name.as_str().to_shouty_snake_case() + ); + let docs = variant.docs.trim(); + inner_group.extend(quote!(#value_name => #docs,)); + } + output.extend( + quote!(pub fn strerror(errno: __wasi_errno_t) -> &'static str { + match errno { + #inner_group + other => panic!("Undefined errno value {:?}", other), + } + }), + ); +} + +fn int_repr_tokens(int_repr: witx::IntRepr) -> TokenStream { + match int_repr { + witx::IntRepr::U8 => quote!(u8), + witx::IntRepr::U16 => quote!(u16), + witx::IntRepr::U32 => quote!(u32), + witx::IntRepr::U64 => quote!(u64), + } +} + +fn builtin_tokens(mode: Mode, builtin: witx::BuiltinType) -> TokenStream { + match builtin { + witx::BuiltinType::String => match mode { + Mode::Host => quote!((*const u8, usize)), + Mode::Wasi => panic!("strings have target-specific size"), + Mode::Wasi32 => quote!((u32, u32)), + }, + witx::BuiltinType::Char8 => quote!(i8), + witx::BuiltinType::U8 => quote!(u8), + witx::BuiltinType::U16 => quote!(u16), + witx::BuiltinType::U32 => quote!(u32), + witx::BuiltinType::U64 => quote!(u64), + witx::BuiltinType::S8 => quote!(i8), + witx::BuiltinType::S16 => quote!(i16), + witx::BuiltinType::S32 => quote!(i32), + witx::BuiltinType::S64 => quote!(i64), + witx::BuiltinType::F32 => quote!(f32), + witx::BuiltinType::F64 => quote!(f64), + witx::BuiltinType::USize => match mode { + Mode::Host => quote!(usize), + Mode::Wasi => panic!("usize has target-specific size"), + Mode::Wasi32 => quote!(u32), + }, + } +} + +fn tref_tokens(mode: Mode, tref: &witx::TypeRef) -> TokenStream { + match tref { + witx::TypeRef::Name(n) => TokenStream::from(TokenTree::Ident(format_ident!( + "__wasi_{}_t", + n.name.as_str() + ))), + witx::TypeRef::Value(v) => match &**v { + witx::Type::Builtin(b) => builtin_tokens(mode, *b), + witx::Type::Pointer(pointee) => { + let pointee = tref_tokens(mode, pointee); + match mode { + Mode::Host => quote!(*mut #pointee), + Mode::Wasi => panic!("pointers have target-specific size"), + Mode::Wasi32 => quote!(u32), + } + } + witx::Type::ConstPointer(pointee) => { + let pointee = tref_tokens(mode, pointee); + match mode { + Mode::Host => quote!(*const #pointee), + Mode::Wasi => panic!("pointers have target-specific size"), + Mode::Wasi32 => quote!(u32), + } + } + witx::Type::Array(element) => { + let element_name = tref_tokens(mode, element); + match mode { + Mode::Host => quote!((*const #element_name, usize)), + Mode::Wasi => panic!("arrays have target-specific size"), + Mode::Wasi32 => quote!((u32, u32)), + } + } + t => panic!("cannot give name to anonymous type {:?}", t), + }, + } +} + +/// Test whether the given struct contains any union members. +fn struct_has_union(s: &witx::StructDatatype) -> bool { + s.members.iter().any(|member| match &*member.tref.type_() { + witx::Type::Union { .. } => true, + witx::Type::Struct(s) => struct_has_union(&s), + _ => false, + }) +} + +/// Test whether the type referred to has a target-specific size. +fn tref_has_target_size(tref: &witx::TypeRef) -> bool { + match tref { + witx::TypeRef::Name(nt) => namedtype_has_target_size(&nt), + witx::TypeRef::Value(t) => type_has_target_size(&t), + } +} + +/// Test whether the given named type has a target-specific size. +fn namedtype_has_target_size(nt: &witx::NamedType) -> bool { + if nt.name.as_str() == "size" { + true + } else { + tref_has_target_size(&nt.tref) + } +} + +/// Test whether the given type has a target-specific size. +fn type_has_target_size(ty: &witx::Type) -> bool { + match ty { + witx::Type::Builtin(witx::BuiltinType::String) => true, + witx::Type::Pointer { .. } | witx::Type::ConstPointer { .. } => true, + witx::Type::Array(elem) => tref_has_target_size(elem), + witx::Type::Struct(s) => s.members.iter().any(|m| tref_has_target_size(&m.tref)), + witx::Type::Union(u) => u + .variants + .iter() + .any(|v| v.tref.as_ref().map(tref_has_target_size).unwrap_or(false)), + _ => false, + } +} diff --git a/crates/wasi-common/wig/src/utils.rs b/crates/wasi-common/wig/src/utils.rs new file mode 100644 index 0000000000..1e36026738 --- /dev/null +++ b/crates/wasi-common/wig/src/utils.rs @@ -0,0 +1,65 @@ +use proc_macro2::{Ident, Literal, TokenStream, TokenTree}; + +/// Given the input tokens to a macro invocation, return the path to the +/// witx file to process. +pub(crate) fn witx_path_from_args(args: TokenStream) -> (String, String) { + let mut strings = Vec::new(); + + for arg in args { + if let TokenTree::Literal(literal) = arg { + let parsed = parse_string_literal(literal); + + strings.push(parsed); + } else { + panic!("arguments must be string literals"); + } + } + + if strings.len() != 2 { + panic!("expected two string literals"); + } + + let phase = &strings[0]; + let id = &strings[1]; + let path = witx_path(phase, id); + + (path, phase.clone()) +} + +fn witx_path(phase: &str, id: &str) -> String { + let root = env!("CARGO_MANIFEST_DIR"); + format!("{}/WASI/phases/{}/witx/{}.witx", root, phase, id) +} + +// Convert a `Literal` holding a string literal into the `String`. +// +// FIXME: It feels like there should be an easier way to do this. +fn parse_string_literal(literal: Literal) -> String { + let s = literal.to_string(); + assert!( + s.starts_with('"') && s.ends_with('"'), + "string literal must be enclosed in double-quotes" + ); + + let trimmed = s[1..s.len() - 1].to_owned(); + assert!( + !trimmed.contains('"'), + "string literal must not contain embedded quotes for now" + ); + assert!( + !trimmed.contains('\\'), + "string literal must not contain embedded backslashes for now" + ); + + trimmed +} + +pub fn param_name(param: &witx::InterfaceFuncParam) -> Ident { + quote::format_ident!( + "{}", + match param.name.as_str() { + "in" | "type" => format!("r#{}", param.name.as_str()), + s => s.to_string(), + } + ) +} diff --git a/crates/wasi-common/wig/src/wasi.rs b/crates/wasi-common/wig/src/wasi.rs new file mode 100644 index 0000000000..4e681bfc47 --- /dev/null +++ b/crates/wasi-common/wig/src/wasi.rs @@ -0,0 +1,252 @@ +use crate::utils; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::{format_ident, quote}; + +enum Abi { + I32, + I64, + F32, + F64, +} + +/// This is a single-use macro intended to be used in the `wasmtime-wasi` crate. +/// +/// This macro will generate a structure, `Wasi`, which will create all the +/// functions necessary to bind wasi and hook everything up via the `wasmtime` +/// crate. +/// +/// The generated shim functions here will also `trace!` their arguments for +/// logging purposes. Otherwise this is hopefully somewhat straightforward! +/// +/// I'd recommend using `cargo +nightly expand` to explore the output of this +/// macro some more. +pub fn define_struct(args: TokenStream) -> TokenStream { + let (path, _phase) = utils::witx_path_from_args(args); + let doc = match witx::load(&[&path]) { + Ok(doc) => doc, + Err(e) => { + panic!("error opening file {}: {}", path, e); + } + }; + + let mut fields = Vec::new(); + let mut get_exports = Vec::new(); + let mut ctor_externs = Vec::new(); + let mut ctor_fields = Vec::new(); + + for module in doc.modules() { + for func in module.funcs() { + let name = func.name.as_str(); + let name_ident = Ident::new(func.name.as_str(), Span::call_site()); + fields.push(quote! { pub #name_ident: wasmtime::Func }); + get_exports.push(quote! { #name => Some(&self.#name_ident) }); + ctor_fields.push(name_ident.clone()); + + let mut shim_arg_decls = Vec::new(); + let mut params = Vec::new(); + let mut formats = Vec::new(); + let mut format_args = Vec::new(); + let mut hostcall_args = Vec::new(); + + for param in func.params.iter() { + let name = utils::param_name(param); + + // Registers a new parameter to the shim we're making with the + // given `name`, the `abi_ty` wasm type and `hex` defines + // whether it's debug-printed in a hex format or not. + // + // This will register a whole bunch of things: + // + // * The cranelift type for the parameter + // * Syntax to specify the actual function parameter + // * How to log the parameter value in a call to `trace!` + // * How to actually pass this argument to the host + // implementation, converting as necessary. + let mut add_param = |name: &Ident, abi_ty: Abi, hex: bool| { + match abi_ty { + Abi::I32 => { + params.push(quote! { types::I32 }); + shim_arg_decls.push(quote! { #name: i32 }); + } + Abi::I64 => { + params.push(quote! { types::I64 }); + shim_arg_decls.push(quote! { #name: i64 }); + } + Abi::F32 => { + params.push(quote! { types::F32 }); + shim_arg_decls.push(quote! { #name: f32 }); + } + Abi::F64 => { + params.push(quote! { types::F64 }); + shim_arg_decls.push(quote! { #name: f64 }); + } + } + formats.push(format!("{}={}", name, if hex { "{:#x}" } else { "{}" },)); + format_args.push(name.clone()); + hostcall_args.push(quote! { #name as _ }); + }; + + match &*param.tref.type_() { + witx::Type::Int(e) => match e.repr { + witx::IntRepr::U64 => add_param(&name, Abi::I64, false), + _ => add_param(&name, Abi::I32, false), + }, + + witx::Type::Enum(e) => match e.repr { + witx::IntRepr::U64 => add_param(&name, Abi::I64, false), + _ => add_param(&name, Abi::I32, false), + }, + + witx::Type::Flags(f) => match f.repr { + witx::IntRepr::U64 => add_param(&name, Abi::I64, true), + _ => add_param(&name, Abi::I32, true), + }, + + witx::Type::Builtin(witx::BuiltinType::Char8) + | witx::Type::Builtin(witx::BuiltinType::S8) + | witx::Type::Builtin(witx::BuiltinType::U8) + | witx::Type::Builtin(witx::BuiltinType::S16) + | witx::Type::Builtin(witx::BuiltinType::U16) + | witx::Type::Builtin(witx::BuiltinType::S32) + | witx::Type::Builtin(witx::BuiltinType::U32) + | witx::Type::Builtin(witx::BuiltinType::USize) => { + add_param(&name, Abi::I32, false); + } + + witx::Type::Builtin(witx::BuiltinType::S64) + | witx::Type::Builtin(witx::BuiltinType::U64) => { + add_param(&name, Abi::I64, false); + } + + witx::Type::Builtin(witx::BuiltinType::F32) => { + add_param(&name, Abi::F32, false); + } + + witx::Type::Builtin(witx::BuiltinType::F64) => { + add_param(&name, Abi::F64, false); + } + + // strings/arrays have an extra ABI parameter for the length + // of the array passed. + witx::Type::Builtin(witx::BuiltinType::String) | witx::Type::Array(_) => { + add_param(&name, Abi::I32, true); + let len = format_ident!("{}_len", name); + add_param(&len, Abi::I32, false); + } + + witx::Type::ConstPointer(_) + | witx::Type::Handle(_) + | witx::Type::Pointer(_) => { + add_param(&name, Abi::I32, true); + } + + witx::Type::Struct(_) | witx::Type::Union(_) => { + panic!("unsupported argument type") + } + } + } + + let mut results = func.results.iter(); + let mut ret_ty = quote! { () }; + let mut cvt_ret = quote! {}; + let mut returns = Vec::new(); + let mut handle_early_error = quote! { panic!("error: {:?}", e) }; + + // The first result is returned bare right now... + if let Some(ret) = results.next() { + handle_early_error = quote! { return e.into() }; + match &*ret.tref.type_() { + // Eventually we'll want to add support for more returned + // types, but for now let's just conform to what `*.witx` + // definitions currently use. + witx::Type::Enum(e) => match e.repr { + witx::IntRepr::U16 => { + returns.push(quote! { types::I32 }); + ret_ty = quote! { i32 }; + cvt_ret = quote! { .into() } + } + other => panic!("unsupported ret enum repr {:?}", other), + }, + other => panic!("unsupported first return {:?}", other), + } + } + + // ... and all remaining results are returned via out-poiners + for result in results { + let name = format_ident!("{}", result.name.as_str()); + params.push(quote! { types::I32 }); + shim_arg_decls.push(quote! { #name: i32 }); + formats.push(format!("{}={{:#x}}", name)); + format_args.push(name.clone()); + hostcall_args.push(quote! { #name as u32 }); + } + + let format_str = format!("{}({})", name, formats.join(", ")); + let wrap = format_ident!("wrap{}", shim_arg_decls.len() + 1); + ctor_externs.push(quote! { + let my_cx = cx.clone(); + let #name_ident = wasmtime::Func::#wrap( + store, + move |mem: crate::WasiCallerMemory #(,#shim_arg_decls)*| -> #ret_ty { + log::trace!( + #format_str, + #(#format_args),* + ); + unsafe { + let memory = match mem.get() { + Ok(e) => e, + Err(e) => #handle_early_error, + }; + hostcalls::#name_ident( + &mut my_cx.borrow_mut(), + memory, + #(#hostcall_args),* + ) #cvt_ret + } + } + ); + }); + } + } + + quote! { + /// An instantiated instance of the wasi exports. + /// + /// This represents a wasi module which can be used to instantiate other + /// wasm modules. This structure exports all that various fields of the + /// wasi instance as fields which can be used to implement your own + /// instantiation logic, if necessary. Additionally [`Wasi::get_export`] + /// can be used to do name-based resolution. + pub struct Wasi { + #(#fields,)* + } + + impl Wasi { + /// Creates a new [`Wasi`] instance. + /// + /// External values are allocated into the `store` provided and + /// configuration of the wasi instance itself should be all + /// contained in the `cx` parameter. + pub fn new(store: &wasmtime::Store, cx: WasiCtx) -> Wasi { + let cx = std::rc::Rc::new(std::cell::RefCell::new(cx)); + #(#ctor_externs)* + + Wasi { + #(#ctor_fields,)* + } + } + + /// Looks up a field called `name` in this structure, returning it + /// if found. + /// + /// This is often useful when instantiating a `wasmtime` instance + /// where name resolution often happens with strings. + pub fn get_export(&self, name: &str) -> Option<&wasmtime::Func> { + match name { + #(#get_exports,)* + _ => None, + } + } + } + } +} diff --git a/crates/wasi-common/winx/Cargo.toml b/crates/wasi-common/winx/Cargo.toml new file mode 100644 index 0000000000..7c1ab74423 --- /dev/null +++ b/crates/wasi-common/winx/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "winx" +version = "0.12.0" +authors = ["Jakub Konka "] +description = "Windows API helper library" +license = "Apache-2.0 WITH LLVM-exception" +repository = "https://github.com/bytecodealliance/wasmtime" +edition = "2018" + +[dependencies] +bitflags = "1.0" +cvt = "0.1" + +[dependencies.winapi] +version = "^0.3" +features = [ + "std", + "errhandlingapi", + "handleapi", + "processthreadsapi", + "profileapi", + "securitybaseapi", + "winbase", + "winerror", + "ws2def", + "fileapi", + "aclapi", +] + +[badges] +maintenance = { status = "actively-developed" } diff --git a/crates/wasi-common/winx/LICENSE b/crates/wasi-common/winx/LICENSE new file mode 100644 index 0000000000..f9d81955f4 --- /dev/null +++ b/crates/wasi-common/winx/LICENSE @@ -0,0 +1,220 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + diff --git a/crates/wasi-common/winx/src/file.rs b/crates/wasi-common/winx/src/file.rs new file mode 100644 index 0000000000..2a1bee7cfd --- /dev/null +++ b/crates/wasi-common/winx/src/file.rs @@ -0,0 +1,455 @@ +#![allow(non_camel_case_types)] + +use crate::ntdll::{ + NtQueryInformationFile, RtlNtStatusToDosError, FILE_ACCESS_INFORMATION, FILE_INFORMATION_CLASS, + FILE_MODE_INFORMATION, IO_STATUS_BLOCK, +}; +use crate::{winerror, Result}; +use bitflags::bitflags; +use cvt::cvt; +use std::ffi::{c_void, OsString}; +use std::fs::File; +use std::io; +use std::os::windows::prelude::{AsRawHandle, OsStringExt, RawHandle}; +use winapi::shared::{ + minwindef::{self, DWORD}, + ntstatus, +}; +use winapi::um::{fileapi, fileapi::GetFileType, minwinbase, winbase, winnt}; + +/// Maximum total path length for Unicode in Windows. +/// [Maximum path length limitation]: https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file#maximum-path-length-limitation +pub const WIDE_MAX_PATH: DWORD = 0x7fff; + +#[derive(Debug, Copy, Clone)] +pub struct FileType(minwindef::DWORD); + +// possible types are: +// * FILE_TYPE_CHAR +// * FILE_TYPE_DISK +// * FILE_TYPE_PIPE +// * FILE_TYPE_REMOTE +// * FILE_TYPE_UNKNOWN +// +// FILE_TYPE_REMOTE is unused +// https://technet.microsoft.com/en-us/evalcenter/aa364960(v=vs.100) +impl FileType { + /// Returns true if character device such as LPT device or console + pub fn is_char(&self) -> bool { + self.0 == winbase::FILE_TYPE_CHAR + } + + /// Returns true if disk device such as file or dir + pub fn is_disk(&self) -> bool { + self.0 == winbase::FILE_TYPE_DISK + } + + /// Returns true if pipe device such as socket, named pipe or anonymous pipe + pub fn is_pipe(&self) -> bool { + self.0 == winbase::FILE_TYPE_PIPE + } + + /// Returns true if unknown device + pub fn is_unknown(&self) -> bool { + self.0 == winbase::FILE_TYPE_UNKNOWN + } +} + +pub unsafe fn get_file_type(handle: RawHandle) -> Result { + let file_type = FileType(GetFileType(handle)); + let err = winerror::WinError::last(); + if file_type.is_unknown() && err != winerror::WinError::ERROR_SUCCESS { + Err(err) + } else { + Ok(file_type) + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +#[repr(u32)] +pub enum CreationDisposition { + NO_DISPOSITION = 0, + /// Creates a new file, only if it does not already exist. + /// If the specified file exists, the function fails and the last-error code is + /// set to ERROR_FILE_EXISTS (80). + /// + /// If the specified file does not exist and is a valid path to a writable location, + /// a new file is created. + CREATE_NEW = fileapi::CREATE_NEW, + /// Creates a new file, always. + /// If the specified file exists and is writable, the function overwrites the file, + /// the function succeeds, and last-error code is set to ERROR_ALREADY_EXISTS (183). + /// + /// If the specified file does not exist and is a valid path, a new file is created, + /// the function succeeds, and the last-error code is set to zero. + CREATE_ALWAYS = fileapi::CREATE_ALWAYS, + /// Opens a file or device, only if it exists. + /// If the specified file or device does not exist, the function fails and the + /// last-error code is set to ERROR_FILE_NOT_FOUND (2). + OPEN_EXISTING = fileapi::OPEN_EXISTING, + /// Opens a file, always. + /// If the specified file exists, the function succeeds and the last-error code is + /// set to ERROR_ALREADY_EXISTS (183). + /// + /// If the specified file does not exist and is a valid path to a writable location, + /// the function creates a file and the last-error code is set to zero. + OPEN_ALWAYS = fileapi::OPEN_ALWAYS, + /// Opens a file and truncates it so that its size is zero bytes, only if it exists. + /// If the specified file does not exist, the function fails and the last-error code + /// is set to ERROR_FILE_NOT_FOUND (2). + /// + /// The calling process must open the file with the GENERIC_WRITE bit set as part + /// of the dwDesiredAccess parameter. + TRUNCATE_EXISTING = fileapi::TRUNCATE_EXISTING, +} + +impl CreationDisposition { + pub fn from_u32(disp: u32) -> Self { + use CreationDisposition::*; + match disp { + fileapi::CREATE_NEW => CREATE_NEW, + fileapi::CREATE_ALWAYS => CREATE_ALWAYS, + fileapi::OPEN_EXISTING => OPEN_EXISTING, + fileapi::OPEN_ALWAYS => OPEN_ALWAYS, + fileapi::TRUNCATE_EXISTING => TRUNCATE_EXISTING, + _ => NO_DISPOSITION, + } + } +} + +bitflags! { + pub struct Attributes: minwindef::DWORD { + /// A file or directory that is an archive file or directory. + /// Applications typically use this attribute to mark files for backup or removal. + const FILE_ATTRIBUTE_ARCHIVE = winnt::FILE_ATTRIBUTE_ARCHIVE; + /// A file or directory that is compressed. For a file, all of the data in the file is compressed. + /// For a directory, compression is the default for newly created files and subdirectories. + const FILE_ATTRIBUTE_COMPRESSED = winnt::FILE_ATTRIBUTE_COMPRESSED; + /// This value is reserved for system use. + const FILE_ATTRIBUTE_DEVICE = winnt::FILE_ATTRIBUTE_DEVICE; + /// The handle that identifies a directory. + const FILE_ATTRIBUTE_DIRECTORY = winnt::FILE_ATTRIBUTE_DIRECTORY; + /// A file or directory that is encrypted. For a file, all data streams in the file are encrypted. + /// For a directory, encryption is the default for newly created files and subdirectories. + const FILE_ATTRIBUTE_ENCRYPTED = winnt::FILE_ATTRIBUTE_ENCRYPTED; + /// The file or directory is hidden. It is not included in an ordinary directory listing. + const FILE_ATTRIBUTE_HIDDEN = winnt::FILE_ATTRIBUTE_HIDDEN; + /// The directory or user data stream is configured with integrity (only supported on ReFS volumes). + /// It is not included in an ordinary directory listing. The integrity setting persists with the file if it's renamed. + /// If a file is copied the destination file will have integrity set if either the source file or destination directory have integrity set. + const FILE_ATTRIBUTE_INTEGRITY_STREAM = winnt::FILE_ATTRIBUTE_INTEGRITY_STREAM; + /// A file that does not have other attributes set. This attribute is valid only when used alone. + const FILE_ATTRIBUTE_NORMAL = winnt::FILE_ATTRIBUTE_NORMAL; + /// The file or directory is not to be indexed by the content indexing service. + const FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = winnt::FILE_ATTRIBUTE_NOT_CONTENT_INDEXED; + /// The user data stream not to be read by the background data integrity scanner (AKA scrubber). + /// When set on a directory it only provides inheritance. This flag is only supported on Storage Spaces and ReFS volumes. + /// It is not included in an ordinary directory listing. + const FILE_ATTRIBUTE_NO_SCRUB_DATA = winnt::FILE_ATTRIBUTE_NO_SCRUB_DATA; + /// The data of a file is not available immediately. + /// This attribute indicates that the file data is physically moved to offline storage. + /// This attribute is used by Remote Storage, which is the hierarchical storage management software. + /// Applications should not arbitrarily change this attribute. + const FILE_ATTRIBUTE_OFFLINE = winnt::FILE_ATTRIBUTE_OFFLINE; + /// A file that is read-only. Applications can read the file, but cannot write to it or delete it. + /// This attribute is not honored on directories. + const FILE_ATTRIBUTE_READONLY = winnt::FILE_ATTRIBUTE_READONLY; + /// When this attribute is set, it means that the file or directory is not fully present locally. + /// For a file that means that not all of its data is on local storage (e.g. it may be sparse with some data still in remote storage). + /// For a directory it means that some of the directory contents are being virtualized from another location. + /// Reading the file / enumerating the directory will be more expensive than normal, e.g. it will cause at least some of the + /// file/directory content to be fetched from a remote store. Only kernel-mode callers can set this bit. + const FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS = winnt::FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS; + /// This attribute only appears in directory enumeration classes (FILE_DIRECTORY_INFORMATION, FILE_BOTH_DIR_INFORMATION, etc.). + /// When this attribute is set, it means that the file or directory has no physical representation on the local system; the item is virtual. + /// Opening the item will be more expensive than normal, e.g. it will cause at least some of it to be fetched from a remote store. + const FILE_ATTRIBUTE_RECALL_ON_OPEN = winnt::FILE_ATTRIBUTE_RECALL_ON_OPEN; + /// A file or directory that has an associated reparse point, or a file that is a symbolic link. + const FILE_ATTRIBUTE_REPARSE_POINT = winnt::FILE_ATTRIBUTE_REPARSE_POINT; + /// A file that is a sparse file. + const FILE_ATTRIBUTE_SPARSE_FILE = winnt::FILE_ATTRIBUTE_SPARSE_FILE; + /// A file or directory that the operating system uses a part of, or uses exclusively. + const FILE_ATTRIBUTE_SYSTEM = winnt::FILE_ATTRIBUTE_SYSTEM; + /// A file that is being used for temporary storage. + /// File systems avoid writing data back to mass storage if sufficient cache memory is available, because typically, + /// an application deletes a temporary file after the handle is closed. In that scenario, the system can entirely + /// avoid writing the data. Otherwise, the data is written after the handle is closed. + const FILE_ATTRIBUTE_TEMPORARY = winnt::FILE_ATTRIBUTE_TEMPORARY; + /// This value is reserved for system use. + const FILE_ATTRIBUTE_VIRTUAL = winnt::FILE_ATTRIBUTE_VIRTUAL; + } +} + +bitflags! { + pub struct Flags: minwindef::DWORD { + /// The file is being opened or created for a backup or restore operation. + /// The system ensures that the calling process overrides file security checks when the process has SE_BACKUP_NAME and SE_RESTORE_NAME privileges. + /// You must set this flag to obtain a handle to a directory. A directory handle can be passed to some functions instead of a file handle. + const FILE_FLAG_BACKUP_SEMANTICS = winbase::FILE_FLAG_BACKUP_SEMANTICS; + /// The file is to be deleted immediately after all of its handles are closed, which includes the specified handle and any other open or duplicated handles. + /// If there are existing open handles to a file, the call fails unless they were all opened with the FILE_SHARE_DELETE share mode. + /// Subsequent open requests for the file fail, unless the FILE_SHARE_DELETE share mode is specified. + const FILE_FLAG_DELETE_ON_CLOSE = winbase::FILE_FLAG_DELETE_ON_CLOSE; + /// The file or device is being opened with no system caching for data reads and writes. + /// This flag does not affect hard disk caching or memory mapped files. + /// There are strict requirements for successfully working with files opened with + /// CreateFile using the FILE_FLAG_NO_BUFFERING flag. + const FILE_FLAG_NO_BUFFERING = winbase::FILE_FLAG_NO_BUFFERING; + /// The file data is requested, but it should continue to be located in remote storage. + /// It should not be transported back to local storage. This flag is for use by remote storage systems. + const FILE_FLAG_OPEN_NO_RECALL = winbase::FILE_FLAG_OPEN_NO_RECALL; + /// Normal reparse point processing will not occur; CreateFile will attempt to open the reparse point. + /// When a file is opened, a file handle is returned, whether or not the filter that controls the reparse point is operational. + /// This flag cannot be used with the CREATE_ALWAYS flag. + /// If the file is not a reparse point, then this flag is ignored. + const FILE_FLAG_OPEN_REPARSE_POINT = winbase::FILE_FLAG_OPEN_REPARSE_POINT; + /// The file or device is being opened or created for asynchronous I/O. + /// When subsequent I/O operations are completed on this handle, the event specified in the OVERLAPPED structure will be set to the signaled state. + /// If this flag is specified, the file can be used for simultaneous read and write operations. + /// If this flag is not specified, then I/O operations are serialized, even if the calls to the read and write functions specify an OVERLAPPED structure. + const FILE_FLAG_OVERLAPPED = winbase::FILE_FLAG_OVERLAPPED; + /// Access will occur according to POSIX rules. This includes allowing multiple files with names, + /// differing only in case, for file systems that support that naming. Use care when using this option, + /// because files created with this flag may not be accessible by applications that are written for MS-DOS or 16-bit Windows. + const FILE_FLAG_POSIX_SEMANTICS = winbase::FILE_FLAG_POSIX_SEMANTICS; + /// Access is intended to be random. The system can use this as a hint to optimize file caching. + /// This flag has no effect if the file system does not support cached I/O and FILE_FLAG_NO_BUFFERING. + const FILE_FLAG_RANDOM_ACCESS = winbase::FILE_FLAG_RANDOM_ACCESS; + /// The file or device is being opened with session awareness. + /// If this flag is not specified, then per-session devices (such as a device using RemoteFX USB Redirection) + /// cannot be opened by processes running in session 0. This flag has no effect for callers not in session 0. + /// This flag is supported only on server editions of Windows. + const FILE_FLAG_SESSION_AWARE = winbase::FILE_FLAG_SESSION_AWARE; + /// Access is intended to be sequential from beginning to end. The system can use this as a hint to optimize file caching. + /// This flag should not be used if read-behind (that is, reverse scans) will be used. + /// This flag has no effect if the file system does not support cached I/O and FILE_FLAG_NO_BUFFERING. + const FILE_FLAG_SEQUENTIAL_SCAN = winbase::FILE_FLAG_SEQUENTIAL_SCAN; + /// Write operations will not go through any intermediate cache, they will go directly to disk. + const FILE_FLAG_WRITE_THROUGH = winbase::FILE_FLAG_WRITE_THROUGH; + } +} + +bitflags! { + /// [Access mask]: https://docs.microsoft.com/en-us/windows/desktop/SecAuthZ/access-mask + pub struct AccessMode: minwindef::DWORD { + /// For a file object, the right to read the corresponding file data. + /// For a directory object, the right to read the corresponding directory data. + const FILE_READ_DATA = winnt::FILE_READ_DATA; + const FILE_LIST_DIRECTORY = winnt::FILE_LIST_DIRECTORY; + /// For a file object, the right to write data to the file. + /// For a directory object, the right to create a file in the directory. + const FILE_WRITE_DATA = winnt::FILE_WRITE_DATA; + const FILE_ADD_FILE = winnt::FILE_ADD_FILE; + /// For a file object, the right to append data to the file. + /// (For local files, write operations will not overwrite existing data + /// if this flag is specified without FILE_WRITE_DATA.) + /// For a directory object, the right to create a subdirectory. + /// For a named pipe, the right to create a pipe. + const FILE_APPEND_DATA = winnt::FILE_APPEND_DATA; + const FILE_ADD_SUBDIRECTORY = winnt::FILE_ADD_SUBDIRECTORY; + const FILE_CREATE_PIPE_INSTANCE = winnt::FILE_CREATE_PIPE_INSTANCE; + /// The right to read extended file attributes. + const FILE_READ_EA = winnt::FILE_READ_EA; + /// The right to write extended file attributes. + const FILE_WRITE_EA = winnt::FILE_WRITE_EA; + /// For a file, the right to execute FILE_EXECUTE. + /// For a directory, the right to traverse the directory. + /// By default, users are assigned the BYPASS_TRAVERSE_CHECKING privilege, + /// which ignores the FILE_TRAVERSE access right. + const FILE_EXECUTE = winnt::FILE_EXECUTE; + const FILE_TRAVERSE = winnt::FILE_TRAVERSE; + /// For a directory, the right to delete a directory and all + /// the files it contains, including read-only files. + const FILE_DELETE_CHILD = winnt::FILE_DELETE_CHILD; + /// The right to read file attributes. + const FILE_READ_ATTRIBUTES = winnt::FILE_READ_ATTRIBUTES; + /// The right to write file attributes. + const FILE_WRITE_ATTRIBUTES = winnt::FILE_WRITE_ATTRIBUTES; + /// The right to delete the object. + const DELETE = winnt::DELETE; + /// The right to read the information in the object's security descriptor, + /// not including the information in the system access control list (SACL). + const READ_CONTROL = winnt::READ_CONTROL; + /// The right to use the object for synchronization. This enables a thread + /// to wait until the object is in the signaled state. Some object types + /// do not support this access right. + const SYNCHRONIZE = winnt::SYNCHRONIZE; + /// The right to modify the discretionary access control list (DACL) in + /// the object's security descriptor. + const WRITE_DAC = winnt::WRITE_DAC; + /// The right to change the owner in the object's security descriptor. + const WRITE_OWNER = winnt::WRITE_OWNER; + /// It is used to indicate access to a system access control list (SACL). + const ACCESS_SYSTEM_SECURITY = winnt::ACCESS_SYSTEM_SECURITY; + /// Maximum allowed. + const MAXIMUM_ALLOWED = winnt::MAXIMUM_ALLOWED; + /// Reserved + const RESERVED1 = 0x4000000; + /// Reserved + const RESERVED2 = 0x8000000; + /// Provides all possible access rights. + /// This is convenience flag which is translated by the OS into actual [`FILE_GENERIC_ALL`] union. + const GENERIC_ALL = winnt::GENERIC_ALL; + /// Provides execute access. + const GENERIC_EXECUTE = winnt::GENERIC_EXECUTE; + /// Provides write access. + /// This is convenience flag which is translated by the OS into actual [`FILE_GENERIC_WRITE`] union. + const GENERIC_WRITE = winnt::GENERIC_WRITE; + /// Provides read access. + /// This is convenience flag which is translated by the OS into actual [`FILE_GENERIC_READ`] union. + const GENERIC_READ = winnt::GENERIC_READ; + /// Provides read access. + const FILE_GENERIC_READ = winnt::FILE_GENERIC_READ; + /// Provides write access. + const FILE_GENERIC_WRITE = winnt::FILE_GENERIC_WRITE; + /// Provides execute access. + const FILE_GENERIC_EXECUTE = winnt::FILE_GENERIC_EXECUTE; + /// Provides all accesses. + const FILE_ALL_ACCESS = winnt::FILE_ALL_ACCESS; + } +} + +bitflags! { + // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/52df7798-8330-474b-ac31-9afe8075640c + pub struct FileModeInformation: minwindef::DWORD { + /// When set, any system services, file system drivers (FSDs), and drivers that write data to + /// the file are required to actually transfer the data into the file before any requested write + /// operation is considered complete. + const FILE_WRITE_THROUGH = 0x2; + /// This is a hint that informs the cache that it SHOULD optimize for sequential access. + /// Non-sequential access of the file can result in performance degradation. + const FILE_SEQUENTIAL_ONLY = 0x4; + /// When set, the file cannot be cached or buffered in a driver's internal buffers. + const FILE_NO_INTERMEDIATE_BUFFERING = 0x8; + /// When set, all operations on the file are performed synchronously. + /// Any wait on behalf of the caller is subject to premature termination from alerts. + /// This flag also causes the I/O system to maintain the file position context. + const FILE_SYNCHRONOUS_IO_ALERT = 0x10; + /// When set, all operations on the file are performed synchronously. + /// Wait requests in the system to synchronize I/O queuing and completion are not subject to alerts. + /// This flag also causes the I/O system to maintain the file position context. + const FILE_SYNCHRONOUS_IO_NONALERT = 0x20; + /// This flag is not implemented and is always returned as not set. + const FILE_DELETE_ON_CLOSE = 0x1000; + } +} + +pub fn get_file_path(file: &File) -> Result { + use winapi::um::fileapi::GetFinalPathNameByHandleW; + + let mut raw_path: Vec = vec![0; WIDE_MAX_PATH as usize]; + + let handle = file.as_raw_handle(); + let read_len = + unsafe { GetFinalPathNameByHandleW(handle, raw_path.as_mut_ptr(), WIDE_MAX_PATH, 0) }; + + if read_len == 0 { + // failed to read + return Err(winerror::WinError::last()); + } + + // obtain a slice containing the written bytes, and check for it being too long + // (practically probably impossible) + let written_bytes = raw_path + .get(..read_len as usize) + .ok_or(winerror::WinError::ERROR_BUFFER_OVERFLOW)?; + + Ok(OsString::from_wide(written_bytes)) +} + +pub fn get_fileinfo(file: &File) -> io::Result { + use fileapi::{GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION}; + use std::mem; + + let handle = file.as_raw_handle(); + let info = unsafe { + let mut info: BY_HANDLE_FILE_INFORMATION = mem::zeroed(); + cvt(GetFileInformationByHandle(handle, &mut info))?; + info + }; + + Ok(info) +} + +pub fn change_time(file: &File) -> io::Result { + use fileapi::FILE_BASIC_INFO; + use minwinbase::FileBasicInfo; + use std::mem; + use winbase::GetFileInformationByHandleEx; + + let handle = file.as_raw_handle(); + let tm = unsafe { + let mut info: FILE_BASIC_INFO = mem::zeroed(); + let infosize = mem::size_of_val(&info); + cvt(GetFileInformationByHandleEx( + handle, + FileBasicInfo, + &mut info as *mut FILE_BASIC_INFO as *mut c_void, + infosize as u32, + ))?; + *info.ChangeTime.QuadPart() + }; + + Ok(tm) +} + +pub fn query_access_information(handle: RawHandle) -> Result { + let mut io_status_block = IO_STATUS_BLOCK::default(); + let mut info = FILE_ACCESS_INFORMATION::default(); + + unsafe { + let status = NtQueryInformationFile( + handle, + &mut io_status_block, + &mut info as *mut _ as *mut c_void, + std::mem::size_of::() as u32, + FILE_INFORMATION_CLASS::FileAccessInformation, + ); + + if status != ntstatus::STATUS_SUCCESS { + return Err(winerror::WinError::from_u32(RtlNtStatusToDosError(status))); + } + } + + Ok(AccessMode::from_bits_truncate(info.AccessFlags)) +} + +pub fn query_mode_information(handle: RawHandle) -> Result { + let mut io_status_block = IO_STATUS_BLOCK::default(); + let mut info = FILE_MODE_INFORMATION::default(); + + unsafe { + let status = NtQueryInformationFile( + handle, + &mut io_status_block, + &mut info as *mut _ as *mut c_void, + std::mem::size_of::() as u32, + FILE_INFORMATION_CLASS::FileModeInformation, + ); + + if status != ntstatus::STATUS_SUCCESS { + return Err(winerror::WinError::from_u32(RtlNtStatusToDosError(status))); + } + } + + Ok(FileModeInformation::from_bits_truncate(info.Mode)) +} + +pub fn reopen_file(handle: RawHandle, access_mode: AccessMode, flags: Flags) -> Result { + // Files on Windows are opened with DELETE, READ, and WRITE share mode by default (see OpenOptions in stdlib) + // This keeps the same share mode when reopening the file handle + let new_handle = unsafe { + winbase::ReOpenFile( + handle, + access_mode.bits(), + winnt::FILE_SHARE_DELETE | winnt::FILE_SHARE_READ | winnt::FILE_SHARE_WRITE, + flags.bits(), + ) + }; + + if new_handle == winapi::um::handleapi::INVALID_HANDLE_VALUE { + return Err(winerror::WinError::last()); + } + + Ok(new_handle) +} diff --git a/crates/wasi-common/winx/src/lib.rs b/crates/wasi-common/winx/src/lib.rs new file mode 100644 index 0000000000..779447e786 --- /dev/null +++ b/crates/wasi-common/winx/src/lib.rs @@ -0,0 +1,31 @@ +#![deny( + // missing_docs, + trivial_numeric_casts, + unused_extern_crates, + unstable_features +)] +#![warn(unused_import_braces)] +#![cfg_attr(feature = "clippy", plugin(clippy(conf_file = "../clippy.toml")))] +#![cfg_attr(feature = "cargo-clippy", allow(clippy::new_without_default))] +#![cfg_attr( + feature = "cargo-clippy", + warn( + clippy::float_arithmetic, + clippy::mut_mut, + clippy::nonminimal_bool, + clippy::option_map_unwrap_or, + clippy::option_map_unwrap_or_else, + clippy::unicode_not_nfc, + clippy::use_self + ) +)] +#![cfg(windows)] + +pub mod file; +mod ntdll; +pub mod time; +pub mod winerror; + +use winerror::WinError; + +pub type Result = std::result::Result; diff --git a/crates/wasi-common/winx/src/ntdll.rs b/crates/wasi-common/winx/src/ntdll.rs new file mode 100644 index 0000000000..2948aee79a --- /dev/null +++ b/crates/wasi-common/winx/src/ntdll.rs @@ -0,0 +1,65 @@ +//! Module for importing functions from ntdll.dll. +//! The winapi crate does not expose these Windows API functions. + +#![allow(nonstandard_style)] + +use std::ffi::c_void; +use std::os::raw::c_ulong; +use std::os::windows::prelude::RawHandle; +use winapi::shared::ntdef::NTSTATUS; +use winapi::um::winnt::ACCESS_MASK; + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) enum FILE_INFORMATION_CLASS { + FileAccessInformation = 8, + FileModeInformation = 16, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) union IO_STATUS_BLOCK_u { + pub Status: NTSTATUS, + pub Pointer: *mut c_void, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct IO_STATUS_BLOCK { + pub u: IO_STATUS_BLOCK_u, + pub Information: *mut c_void, +} + +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub(crate) struct FILE_ACCESS_INFORMATION { + pub AccessFlags: ACCESS_MASK, +} + +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub(crate) struct FILE_MODE_INFORMATION { + pub Mode: c_ulong, +} + +impl Default for IO_STATUS_BLOCK { + #[inline] + fn default() -> Self { + unsafe { std::mem::zeroed() } + } +} + +#[link(name = "ntdll")] +extern "C" { + // https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntqueryinformationfile + pub(crate) fn NtQueryInformationFile( + FileHandle: RawHandle, + IoStatusBlock: *mut IO_STATUS_BLOCK, + FileInformation: *mut c_void, + Length: c_ulong, + FileInformationClass: FILE_INFORMATION_CLASS, + ) -> NTSTATUS; + + // https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-rtlntstatustodoserror + pub(crate) fn RtlNtStatusToDosError(status: NTSTATUS) -> c_ulong; +} diff --git a/crates/wasi-common/winx/src/time.rs b/crates/wasi-common/winx/src/time.rs new file mode 100644 index 0000000000..28803e1fb6 --- /dev/null +++ b/crates/wasi-common/winx/src/time.rs @@ -0,0 +1,10 @@ +use cvt::cvt; +use winapi::um::{profileapi::QueryPerformanceFrequency, winnt::LARGE_INTEGER}; + +pub fn perf_counter_frequency() -> std::io::Result { + unsafe { + let mut frequency: LARGE_INTEGER = std::mem::zeroed(); + cvt(QueryPerformanceFrequency(&mut frequency))?; + Ok(*frequency.QuadPart() as u64) + } +} diff --git a/crates/wasi-common/winx/src/winerror.rs b/crates/wasi-common/winx/src/winerror.rs new file mode 100644 index 0000000000..bc4e39b331 --- /dev/null +++ b/crates/wasi-common/winx/src/winerror.rs @@ -0,0 +1,127 @@ +#![allow(non_camel_case_types)] +use winapi::shared::winerror; +use winapi::um::errhandlingapi::GetLastError; + +macro_rules! win_error_expand { + { + $( + #[doc=$doc:literal] + $error:ident, + )* + } => { + /// Wraps WINAPI error code as enum. + #[derive(Debug, Clone, Copy, Eq, PartialEq)] + #[repr(u32)] + pub enum WinError { + /// Unknown error occurred. + UnknownError = std::u32::MAX, + $( + #[doc=$doc] + $error = winerror::$error, + )* + } + + fn desc(err: WinError) -> &'static str { + use WinError::*; + match err { + UnknownError => r" Unknown error occurred.", + $($error => $doc,)* + } + } + + fn from_u32(err: u32) -> WinError { + use WinError::*; + match err { + $(winerror::$error => $error,)* + _ => UnknownError, + } + } + } +} + +win_error_expand! { + /// The operation completed successfully. + ERROR_SUCCESS, + /// The system cannot find the file specified. + ERROR_FILE_NOT_FOUND, + /// The system cannot find the path specified. + ERROR_PATH_NOT_FOUND, + /// The system cannot open the file. + ERROR_TOO_MANY_OPEN_FILES, + /// Access is denied. + ERROR_ACCESS_DENIED, + /// The handle is invalid. + ERROR_INVALID_HANDLE, + /// Not enough storage is available to process this command. + ERROR_NOT_ENOUGH_MEMORY, + /// The environment is incorrect. + ERROR_BAD_ENVIRONMENT, + /// Not enough storage is available to complete this operation. + ERROR_OUTOFMEMORY, + /// The device is not ready. + ERROR_NOT_READY, + /// The request is not supported. + ERROR_NOT_SUPPORTED, + /// The file exists. + ERROR_FILE_EXISTS, + /// The pipe has been ended. + ERROR_BROKEN_PIPE, + /// The file name is too long. + ERROR_BUFFER_OVERFLOW, + /// The directory is not empty. + ERROR_DIR_NOT_EMPTY, + /// The volume label you entered exceeds the label character limit of the destination file system. + ERROR_LABEL_TOO_LONG, + /// The requested resource is in use. + ERROR_BUSY, + /// The file name, directory name, or volume label syntax is incorrect. + ERROR_INVALID_NAME, + /// The process cannot access the file because it is being used by another process. + ERROR_SHARING_VIOLATION, + /// A required privilege is not held by the client. + ERROR_PRIVILEGE_NOT_HELD, + /// The file or directory is not a reparse point. + ERROR_NOT_A_REPARSE_POINT, + /// An attempt was made to move the file pointer before the beginning of the file. + ERROR_NEGATIVE_SEEK, + /// The directory name is invalid. + ERROR_DIRECTORY, + /// Cannot create a file when that file already exists. + ERROR_ALREADY_EXISTS, +} + +impl WinError { + /// Returns the last error as WinError. + pub fn last() -> Self { + Self::from_u32(unsafe { GetLastError() }) + } + + /// Constructs WinError from error code. + pub fn from_u32(err: u32) -> Self { + from_u32(err) + } + + /// Returns error's description string. This description matches + /// the docs for the error. + pub fn desc(self) -> &'static str { + desc(self) + } +} + +impl std::error::Error for WinError { + fn description(&self) -> &str { + self.desc() + } +} + +impl std::fmt::Display for WinError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{:?}: {}", self, self.desc()) + } +} + +impl From for std::io::Error { + fn from(err: WinError) -> Self { + Self::from_raw_os_error(err as i32) + } +} diff --git a/crates/wasi-common/yanix/Cargo.toml b/crates/wasi-common/yanix/Cargo.toml new file mode 100644 index 0000000000..c1e77c9459 --- /dev/null +++ b/crates/wasi-common/yanix/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "yanix" +version = "0.12.0" +authors = ["The Wasmtime Project Developers"] +description = "Yet Another Nix crate: a Unix API helper library" +license = "Apache-2.0 WITH LLVM-exception" +repository = "https://github.com/bytecodealliance/wasmtime" +edition = "2018" + +[dependencies] +log = "0.4" +libc = { version = "0.2", features = ["extra_traits"] } +thiserror = "1.0" +bitflags = "1.2" +cfg-if = "0.1.9" + +[badges] +maintenance = { status = "actively-developed" } diff --git a/crates/wasi-common/yanix/LICENSE b/crates/wasi-common/yanix/LICENSE new file mode 100644 index 0000000000..f9d81955f4 --- /dev/null +++ b/crates/wasi-common/yanix/LICENSE @@ -0,0 +1,220 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + diff --git a/crates/wasi-common/yanix/src/clock.rs b/crates/wasi-common/yanix/src/clock.rs new file mode 100644 index 0000000000..1f70d18893 --- /dev/null +++ b/crates/wasi-common/yanix/src/clock.rs @@ -0,0 +1,37 @@ +use crate::{Errno, Result}; +use std::mem::MaybeUninit; + +#[derive(Debug, Copy, Clone)] +pub enum ClockId { + Realtime, + Monotonic, + ProcessCPUTime, + ThreadCPUTime, +} + +impl ClockId { + pub fn as_raw(&self) -> libc::clockid_t { + match self { + Self::Realtime => libc::CLOCK_REALTIME, + Self::Monotonic => libc::CLOCK_MONOTONIC, + Self::ProcessCPUTime => libc::CLOCK_PROCESS_CPUTIME_ID, + Self::ThreadCPUTime => libc::CLOCK_THREAD_CPUTIME_ID, + } + } +} + +pub fn clock_getres(clock_id: ClockId) -> Result { + let mut timespec = MaybeUninit::::uninit(); + Errno::from_success_code(unsafe { + libc::clock_getres(clock_id.as_raw(), timespec.as_mut_ptr()) + })?; + Ok(unsafe { timespec.assume_init() }) +} + +pub fn clock_gettime(clock_id: ClockId) -> Result { + let mut timespec = MaybeUninit::::uninit(); + Errno::from_success_code(unsafe { + libc::clock_gettime(clock_id.as_raw(), timespec.as_mut_ptr()) + })?; + Ok(unsafe { timespec.assume_init() }) +} diff --git a/crates/wasi-common/yanix/src/dir.rs b/crates/wasi-common/yanix/src/dir.rs new file mode 100644 index 0000000000..fc13afc029 --- /dev/null +++ b/crates/wasi-common/yanix/src/dir.rs @@ -0,0 +1,124 @@ +use crate::{ + file::FileType, + sys::dir::{iter_impl, EntryImpl}, + Errno, Result, +}; +use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd}; +use std::{ffi::CStr, ops::Deref, ptr}; + +pub use crate::sys::EntryExt; + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct Dir(ptr::NonNull); + +impl Dir { + /// Takes the ownership of the passed-in descriptor-based object, + /// and creates a new instance of `Dir`. + #[inline] + pub fn from(fd: F) -> Result { + let fd = fd.into_raw_fd(); + unsafe { Self::from_fd(fd) } + } + + unsafe fn from_fd(fd: RawFd) -> Result { + let d = libc::fdopendir(fd); + if let Some(d) = ptr::NonNull::new(d) { + Ok(Self(d)) + } else { + let e = Errno::last(); + libc::close(fd); + Err(e.into()) + } + } + + /// Set the position of the directory stream, see `seekdir(3)`. + #[cfg(not(target_os = "android"))] + pub fn seek(&mut self, loc: SeekLoc) { + unsafe { libc::seekdir(self.0.as_ptr(), loc.0) } + } + + /// Reset directory stream, see `rewinddir(3)`. + pub fn rewind(&mut self) { + unsafe { libc::rewinddir(self.0.as_ptr()) } + } + + /// Get the current position in the directory stream. + /// + /// If this location is given to `Dir::seek`, the entries up to the previously returned + /// will be omitted and the iteration will start from the currently pending directory entry. + #[cfg(not(target_os = "android"))] + #[allow(dead_code)] + pub fn tell(&self) -> SeekLoc { + let loc = unsafe { libc::telldir(self.0.as_ptr()) }; + SeekLoc(loc) + } + + /// For use by platform-specific implementation code. Returns the raw + /// underlying state. + pub(crate) fn as_raw(&self) -> ptr::NonNull { + self.0 + } +} + +unsafe impl Send for Dir {} + +impl AsRawFd for Dir { + fn as_raw_fd(&self) -> RawFd { + unsafe { libc::dirfd(self.0.as_ptr()) } + } +} + +impl Drop for Dir { + fn drop(&mut self) { + unsafe { libc::closedir(self.0.as_ptr()) }; + } +} + +#[derive(Debug, Copy, Clone)] +pub struct Entry(pub(crate) EntryImpl); + +impl Entry { + /// Returns the file name of this directory entry. + pub fn file_name(&self) -> &CStr { + unsafe { CStr::from_ptr(self.0.d_name.as_ptr()) } + } + + /// Returns the type of this directory entry. + pub fn file_type(&self) -> FileType { + FileType::from_dirent_d_type(self.0.d_type) + } +} + +#[cfg(not(target_os = "android"))] +#[derive(Clone, Copy, Debug)] +pub struct SeekLoc(pub(crate) libc::c_long); + +#[cfg(not(target_os = "android"))] +impl SeekLoc { + pub fn to_raw(&self) -> i64 { + self.0.into() + } +} + +#[derive(Debug)] +pub struct DirIter>(T); + +impl DirIter +where + T: Deref, +{ + pub fn new(dir: T) -> Self { + Self(dir) + } +} + +impl Iterator for DirIter +where + T: Deref, +{ + type Item = Result; + + fn next(&mut self) -> Option { + iter_impl(&self.0).map(|x| x.map(Entry)) + } +} diff --git a/crates/wasi-common/yanix/src/errno.rs b/crates/wasi-common/yanix/src/errno.rs new file mode 100644 index 0000000000..8f8be64dc8 --- /dev/null +++ b/crates/wasi-common/yanix/src/errno.rs @@ -0,0 +1,227 @@ +//! Errno-specific for different Unix platforms +use crate::Result; +use std::{fmt, io}; +use thiserror::Error; + +#[derive(Debug, Copy, Clone, Error, PartialEq, Eq, Hash)] +#[repr(i32)] +pub enum Errno { + EPERM = libc::EPERM, + ENOENT = libc::ENOENT, + ESRCH = libc::ESRCH, + EINTR = libc::EINTR, + EIO = libc::EIO, + ENXIO = libc::ENXIO, + E2BIG = libc::E2BIG, + ENOEXEC = libc::ENOEXEC, + EBADF = libc::EBADF, + ECHILD = libc::ECHILD, + EAGAIN = libc::EAGAIN, + ENOMEM = libc::ENOMEM, + EACCES = libc::EACCES, + EFAULT = libc::EFAULT, + EBUSY = libc::EBUSY, + EEXIST = libc::EEXIST, + EXDEV = libc::EXDEV, + ENODEV = libc::ENODEV, + ENOTDIR = libc::ENOTDIR, + EISDIR = libc::EISDIR, + EINVAL = libc::EINVAL, + ENFILE = libc::ENFILE, + EMFILE = libc::EMFILE, + ENOTTY = libc::ENOTTY, + ETXTBSY = libc::ETXTBSY, + EFBIG = libc::EFBIG, + ENOSPC = libc::ENOSPC, + ESPIPE = libc::ESPIPE, + EROFS = libc::EROFS, + EMLINK = libc::EMLINK, + EPIPE = libc::EPIPE, + EDOM = libc::EDOM, + ERANGE = libc::ERANGE, + EDEADLK = libc::EDEADLK, + ENAMETOOLONG = libc::ENAMETOOLONG, + ENOLCK = libc::ENOLCK, + ENOSYS = libc::ENOSYS, + ENOTEMPTY = libc::ENOTEMPTY, + ELOOP = libc::ELOOP, + ENOMSG = libc::ENOMSG, + EIDRM = libc::EIDRM, + ENOLINK = libc::ENOLINK, + EPROTO = libc::EPROTO, + EMULTIHOP = libc::EMULTIHOP, + EBADMSG = libc::EBADMSG, + EOVERFLOW = libc::EOVERFLOW, + EILSEQ = libc::EILSEQ, + ENOTSOCK = libc::ENOTSOCK, + EDESTADDRREQ = libc::EDESTADDRREQ, + EMSGSIZE = libc::EMSGSIZE, + EPROTOTYPE = libc::EPROTOTYPE, + ENOPROTOOPT = libc::ENOPROTOOPT, + EPROTONOSUPPORT = libc::EPROTONOSUPPORT, + EAFNOSUPPORT = libc::EAFNOSUPPORT, + EADDRINUSE = libc::EADDRINUSE, + EADDRNOTAVAIL = libc::EADDRNOTAVAIL, + ENETDOWN = libc::ENETDOWN, + ENETUNREACH = libc::ENETUNREACH, + ENETRESET = libc::ENETRESET, + ECONNABORTED = libc::ECONNABORTED, + ECONNRESET = libc::ECONNRESET, + ENOBUFS = libc::ENOBUFS, + EISCONN = libc::EISCONN, + ENOTCONN = libc::ENOTCONN, + ETIMEDOUT = libc::ETIMEDOUT, + ECONNREFUSED = libc::ECONNREFUSED, + EHOSTUNREACH = libc::EHOSTUNREACH, + EALREADY = libc::EALREADY, + EINPROGRESS = libc::EINPROGRESS, + ESTALE = libc::ESTALE, + EDQUOT = libc::EDQUOT, + ECANCELED = libc::ECANCELED, + EOWNERDEAD = libc::EOWNERDEAD, + ENOTRECOVERABLE = libc::ENOTRECOVERABLE, +} + +impl Errno { + pub fn from_i32(err: i32) -> Self { + match err { + libc::EPERM => Self::EPERM, + libc::ENOENT => Self::ENOENT, + libc::ESRCH => Self::ESRCH, + libc::EINTR => Self::EINTR, + libc::EIO => Self::EIO, + libc::ENXIO => Self::ENXIO, + libc::E2BIG => Self::E2BIG, + libc::ENOEXEC => Self::ENOEXEC, + libc::EBADF => Self::EBADF, + libc::ECHILD => Self::ECHILD, + libc::EAGAIN => Self::EAGAIN, + libc::ENOMEM => Self::ENOMEM, + libc::EACCES => Self::EACCES, + libc::EFAULT => Self::EFAULT, + libc::EBUSY => Self::EBUSY, + libc::EEXIST => Self::EEXIST, + libc::EXDEV => Self::EXDEV, + libc::ENODEV => Self::ENODEV, + libc::ENOTDIR => Self::ENOTDIR, + libc::EISDIR => Self::EISDIR, + libc::EINVAL => Self::EINVAL, + libc::ENFILE => Self::ENFILE, + libc::EMFILE => Self::EMFILE, + libc::ENOTTY => Self::ENOTTY, + libc::ETXTBSY => Self::ETXTBSY, + libc::EFBIG => Self::EFBIG, + libc::ENOSPC => Self::ENOSPC, + libc::ESPIPE => Self::ESPIPE, + libc::EROFS => Self::EROFS, + libc::EMLINK => Self::EMLINK, + libc::EPIPE => Self::EPIPE, + libc::EDOM => Self::EDOM, + libc::ERANGE => Self::ERANGE, + libc::EDEADLK => Self::EDEADLK, + libc::ENAMETOOLONG => Self::ENAMETOOLONG, + libc::ENOLCK => Self::ENOLCK, + libc::ENOSYS => Self::ENOSYS, + libc::ENOTEMPTY => Self::ENOTEMPTY, + libc::ELOOP => Self::ELOOP, + libc::ENOMSG => Self::ENOMSG, + libc::EIDRM => Self::EIDRM, + libc::ENOLINK => Self::ENOLINK, + libc::EPROTO => Self::EPROTO, + libc::EMULTIHOP => Self::EMULTIHOP, + libc::EBADMSG => Self::EBADMSG, + libc::EOVERFLOW => Self::EOVERFLOW, + libc::EILSEQ => Self::EILSEQ, + libc::ENOTSOCK => Self::ENOTSOCK, + libc::EDESTADDRREQ => Self::EDESTADDRREQ, + libc::EMSGSIZE => Self::EMSGSIZE, + libc::EPROTOTYPE => Self::EPROTOTYPE, + libc::ENOPROTOOPT => Self::ENOPROTOOPT, + libc::EPROTONOSUPPORT => Self::EPROTONOSUPPORT, + libc::EAFNOSUPPORT => Self::EAFNOSUPPORT, + libc::EADDRINUSE => Self::EADDRINUSE, + libc::EADDRNOTAVAIL => Self::EADDRNOTAVAIL, + libc::ENETDOWN => Self::ENETDOWN, + libc::ENETUNREACH => Self::ENETUNREACH, + libc::ENETRESET => Self::ENETRESET, + libc::ECONNABORTED => Self::ECONNABORTED, + libc::ECONNRESET => Self::ECONNRESET, + libc::ENOBUFS => Self::ENOBUFS, + libc::EISCONN => Self::EISCONN, + libc::ENOTCONN => Self::ENOTCONN, + libc::ETIMEDOUT => Self::ETIMEDOUT, + libc::ECONNREFUSED => Self::ECONNREFUSED, + libc::EHOSTUNREACH => Self::EHOSTUNREACH, + libc::EALREADY => Self::EALREADY, + libc::EINPROGRESS => Self::EINPROGRESS, + libc::ESTALE => Self::ESTALE, + libc::EDQUOT => Self::EDQUOT, + libc::ECANCELED => Self::ECANCELED, + libc::EOWNERDEAD => Self::EOWNERDEAD, + libc::ENOTRECOVERABLE => Self::ENOTRECOVERABLE, + other => { + log::warn!("Unknown errno: {}", other); + Self::ENOSYS + } + } + } + + pub fn last() -> Self { + let errno = io::Error::last_os_error() + .raw_os_error() + .unwrap_or(libc::ENOSYS); + Self::from_i32(errno) + } + + pub fn from_success_code(t: T) -> Result<()> { + if t.is_zero() { + Ok(()) + } else { + Err(Self::last().into()) + } + } + + pub fn from_result(t: T) -> Result { + if t.is_minus_one() { + Err(Self::last().into()) + } else { + Ok(t) + } + } +} + +impl fmt::Display for Errno { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Errno code: {}", self) + } +} + +#[doc(hidden)] +pub trait IsZero { + fn is_zero(&self) -> bool; +} + +macro_rules! impl_is_zero { + ($($t:ident)*) => ($(impl IsZero for $t { + fn is_zero(&self) -> bool { + *self == 0 + } + })*) +} + +impl_is_zero! { i32 i64 isize } + +#[doc(hidden)] +pub trait IsMinusOne { + fn is_minus_one(&self) -> bool; +} + +macro_rules! impl_is_minus_one { + ($($t:ident)*) => ($(impl IsMinusOne for $t { + fn is_minus_one(&self) -> bool { + *self == -1 + } + })*) +} + +impl_is_minus_one! { i32 i64 isize } diff --git a/crates/wasi-common/yanix/src/fcntl.rs b/crates/wasi-common/yanix/src/fcntl.rs new file mode 100644 index 0000000000..87d9827af3 --- /dev/null +++ b/crates/wasi-common/yanix/src/fcntl.rs @@ -0,0 +1,33 @@ +use crate::{ + file::{FdFlag, OFlag}, + Errno, Result, +}; +use std::os::unix::prelude::*; + +pub unsafe fn dup_fd(fd: RawFd, close_on_exec: bool) -> Result { + // Both fcntl commands expect a RawFd arg which will specify + // the minimum duplicated RawFd number. In our case, I don't + // think we have to worry about this that much, so passing in + // the RawFd descriptor we want duplicated + Errno::from_result(if close_on_exec { + libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, fd) + } else { + libc::fcntl(fd, libc::F_DUPFD, fd) + }) +} + +pub unsafe fn get_fd_flags(fd: RawFd) -> Result { + Errno::from_result(libc::fcntl(fd, libc::F_GETFD)).map(FdFlag::from_bits_truncate) +} + +pub unsafe fn set_fd_flags(fd: RawFd, flags: FdFlag) -> Result<()> { + Errno::from_success_code(libc::fcntl(fd, libc::F_SETFD, flags.bits())) +} + +pub unsafe fn get_status_flags(fd: RawFd) -> Result { + Errno::from_result(libc::fcntl(fd, libc::F_GETFL)).map(OFlag::from_bits_truncate) +} + +pub unsafe fn set_status_flags(fd: RawFd, flags: OFlag) -> Result<()> { + Errno::from_success_code(libc::fcntl(fd, libc::F_SETFL, flags.bits())) +} diff --git a/crates/wasi-common/yanix/src/file.rs b/crates/wasi-common/yanix/src/file.rs new file mode 100644 index 0000000000..1124dcd95e --- /dev/null +++ b/crates/wasi-common/yanix/src/file.rs @@ -0,0 +1,249 @@ +use crate::{Errno, Result}; +use bitflags::bitflags; +use cfg_if::cfg_if; +use std::{ + convert::TryInto, + ffi::{CString, OsStr, OsString}, + os::unix::prelude::*, +}; + +pub use crate::sys::file::*; + +bitflags! { + pub struct FdFlag: libc::c_int { + const CLOEXEC = libc::FD_CLOEXEC; + } +} + +bitflags! { + pub struct AtFlag: libc::c_int { + const REMOVEDIR = libc::AT_REMOVEDIR; + const SYMLINK_FOLLOW = libc::AT_SYMLINK_FOLLOW; + const SYMLINK_NOFOLLOW = libc::AT_SYMLINK_NOFOLLOW; + } +} + +bitflags! { + pub struct Mode: libc::mode_t { + const IRWXU = libc::S_IRWXU; + const IRUSR = libc::S_IRUSR; + const IWUSR = libc::S_IWUSR; + const IXUSR = libc::S_IXUSR; + const IRWXG = libc::S_IRWXG; + const IRGRP = libc::S_IRGRP; + const IWGRP = libc::S_IWGRP; + const IXGRP = libc::S_IXGRP; + const IRWXO = libc::S_IRWXO; + const IROTH = libc::S_IROTH; + const IWOTH = libc::S_IWOTH; + const IXOTH = libc::S_IXOTH; + const ISUID = libc::S_ISUID as libc::mode_t; + const ISGID = libc::S_ISGID as libc::mode_t; + const ISVTX = libc::S_ISVTX as libc::mode_t; + } +} + +bitflags! { + pub struct OFlag: libc::c_int { + const ACCMODE = libc::O_ACCMODE; + const APPEND = libc::O_APPEND; + const CREAT = libc::O_CREAT; + const DIRECTORY = libc::O_DIRECTORY; + const DSYNC = { + // Have to use cfg_if: https://github.com/bitflags/bitflags/issues/137 + cfg_if! { + if #[cfg(any(target_os = "android", + target_os = "ios", + target_os = "linux", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd", + target_os = "emscripten"))] { + libc::O_DSYNC + } else if #[cfg(target_os = "freebsd")] { + // https://github.com/bytecodealliance/wasmtime/pull/756 + libc::O_SYNC + } + } + }; + const EXCL = libc::O_EXCL; + #[cfg(any(target_os = "dragonfly", + target_os = "freebsd", + target_os = "ios", + all(target_os = "linux", not(target_env = "musl")), + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd"))] + const FSYNC = libc::O_FSYNC; + const NOFOLLOW = libc::O_NOFOLLOW; + const NONBLOCK = libc::O_NONBLOCK; + const RDONLY = libc::O_RDONLY; + const WRONLY = libc::O_WRONLY; + const RDWR = libc::O_RDWR; + #[cfg(any(target_os = "linux", + target_os = "netbsd", + target_os = "openbsd", + target_os = "emscripten"))] + const RSYNC = libc::O_RSYNC; + const SYNC = libc::O_SYNC; + const TRUNC = libc::O_TRUNC; + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FileType { + CharacterDevice, + Directory, + BlockDevice, + RegularFile, + Symlink, + Fifo, + Socket, + Unknown, +} + +impl FileType { + pub fn from_stat_st_mode(st_mode: libc::mode_t) -> Self { + match st_mode & libc::S_IFMT { + libc::S_IFIFO => Self::Fifo, + libc::S_IFCHR => Self::CharacterDevice, + libc::S_IFDIR => Self::Directory, + libc::S_IFBLK => Self::BlockDevice, + libc::S_IFREG => Self::RegularFile, + libc::S_IFLNK => Self::Symlink, + libc::S_IFSOCK => Self::Socket, + _ => Self::Unknown, // Should we actually panic here since this one *should* never happen? + } + } + + pub fn from_dirent_d_type(d_type: u8) -> Self { + match d_type { + libc::DT_CHR => Self::CharacterDevice, + libc::DT_DIR => Self::Directory, + libc::DT_BLK => Self::BlockDevice, + libc::DT_REG => Self::RegularFile, + libc::DT_LNK => Self::Symlink, + libc::DT_SOCK => Self::Socket, + libc::DT_FIFO => Self::Fifo, + /* libc::DT_UNKNOWN */ _ => Self::Unknown, + } + } +} + +pub unsafe fn openat>( + dirfd: RawFd, + path: P, + oflag: OFlag, + mode: Mode, +) -> Result { + let path = CString::new(path.as_ref().as_bytes())?; + Errno::from_result(libc::openat( + dirfd, + path.as_ptr(), + oflag.bits(), + libc::c_uint::from(mode.bits()), + )) +} + +pub unsafe fn readlinkat>(dirfd: RawFd, path: P) -> Result { + let path = CString::new(path.as_ref().as_bytes())?; + let buffer = &mut [0u8; libc::PATH_MAX as usize + 1]; + Errno::from_result(libc::readlinkat( + dirfd, + path.as_ptr(), + buffer.as_mut_ptr() as *mut _, + buffer.len(), + )) + .and_then(|nread| { + let link = OsStr::from_bytes(&buffer[0..nread.try_into()?]); + Ok(link.into()) + }) +} + +pub unsafe fn mkdirat>(dirfd: RawFd, path: P, mode: Mode) -> Result<()> { + let path = CString::new(path.as_ref().as_bytes())?; + Errno::from_success_code(libc::mkdirat(dirfd, path.as_ptr(), mode.bits())) +} + +pub unsafe fn linkat>( + old_dirfd: RawFd, + old_path: P, + new_dirfd: RawFd, + new_path: P, + flags: AtFlag, +) -> Result<()> { + let old_path = CString::new(old_path.as_ref().as_bytes())?; + let new_path = CString::new(new_path.as_ref().as_bytes())?; + Errno::from_success_code(libc::linkat( + old_dirfd, + old_path.as_ptr(), + new_dirfd, + new_path.as_ptr(), + flags.bits(), + )) +} + +pub unsafe fn unlinkat>(dirfd: RawFd, path: P, flags: AtFlag) -> Result<()> { + let path = CString::new(path.as_ref().as_bytes())?; + Errno::from_success_code(libc::unlinkat(dirfd, path.as_ptr(), flags.bits())) +} + +pub unsafe fn renameat>( + old_dirfd: RawFd, + old_path: P, + new_dirfd: RawFd, + new_path: P, +) -> Result<()> { + let old_path = CString::new(old_path.as_ref().as_bytes())?; + let new_path = CString::new(new_path.as_ref().as_bytes())?; + Errno::from_success_code(libc::renameat( + old_dirfd, + old_path.as_ptr(), + new_dirfd, + new_path.as_ptr(), + )) +} + +pub unsafe fn symlinkat>(old_path: P, new_dirfd: RawFd, new_path: P) -> Result<()> { + let old_path = CString::new(old_path.as_ref().as_bytes())?; + let new_path = CString::new(new_path.as_ref().as_bytes())?; + Errno::from_success_code(libc::symlinkat( + old_path.as_ptr(), + new_dirfd, + new_path.as_ptr(), + )) +} + +pub unsafe fn fstatat>(dirfd: RawFd, path: P, flags: AtFlag) -> Result { + use std::mem::MaybeUninit; + let path = CString::new(path.as_ref().as_bytes())?; + let mut filestat = MaybeUninit::::uninit(); + Errno::from_result(libc::fstatat( + dirfd, + path.as_ptr(), + filestat.as_mut_ptr(), + flags.bits(), + ))?; + Ok(filestat.assume_init()) +} + +pub unsafe fn fstat(fd: RawFd) -> Result { + use std::mem::MaybeUninit; + let mut filestat = MaybeUninit::::uninit(); + Errno::from_result(libc::fstat(fd, filestat.as_mut_ptr()))?; + Ok(filestat.assume_init()) +} + +/// `fionread()` function, equivalent to `ioctl(fd, FIONREAD, *bytes)`. +pub unsafe fn fionread(fd: RawFd) -> Result { + let mut nread: libc::c_int = 0; + Errno::from_result(libc::ioctl(fd, libc::FIONREAD, &mut nread as *mut _))?; + Ok(nread.try_into()?) +} + +/// This function is unsafe because it operates on a raw file descriptor. +/// It's provided, because std::io::Seek requires a mutable borrow. +pub unsafe fn tell(fd: RawFd) -> Result { + let offset: i64 = Errno::from_result(libc::lseek(fd, 0, libc::SEEK_CUR))?; + Ok(offset.try_into()?) +} diff --git a/crates/wasi-common/yanix/src/lib.rs b/crates/wasi-common/yanix/src/lib.rs new file mode 100644 index 0000000000..e97e4e0b0f --- /dev/null +++ b/crates/wasi-common/yanix/src/lib.rs @@ -0,0 +1,40 @@ +//! `yanix` stands for Yet Another Nix crate, and, well, it is simply +//! a yet another crate in the spirit of the [nix] crate. As such, +//! this crate is inspired by the original `nix` crate, however, +//! it takes a different approach, using lower-level interfaces with +//! less abstraction, so that it fits better with its main use case +//! which is our WASI implementation, [wasi-common]. +//! +//! [nix]: https://github.com/nix-rust/nix +//! [wasi-common]: https://github.com/bytecodealliance/wasmtime/tree/master/crates/wasi-common +#![cfg(unix)] + +pub mod clock; +pub mod dir; +pub mod fcntl; +pub mod file; +pub mod poll; +pub mod socket; + +mod errno; +mod sys; + +pub mod fadvise { + pub use super::sys::fadvise::*; +} + +pub use errno::Errno; +use std::{ffi, num}; +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Debug, Error)] +pub enum YanixError { + #[error("raw os error {0}")] + Errno(#[from] Errno), + #[error("a nul byte was not found in the expected position")] + NulError(#[from] ffi::NulError), + #[error("integral type conversion failed")] + TryFromIntError(#[from] num::TryFromIntError), +} diff --git a/crates/wasi-common/yanix/src/poll.rs b/crates/wasi-common/yanix/src/poll.rs new file mode 100644 index 0000000000..db33b584b4 --- /dev/null +++ b/crates/wasi-common/yanix/src/poll.rs @@ -0,0 +1,47 @@ +use crate::{Errno, Result}; +use bitflags::bitflags; +use std::{convert::TryInto, os::unix::prelude::*}; + +bitflags! { + pub struct PollFlags: libc::c_short { + const POLLIN = libc::POLLIN; + const POLLPRI = libc::POLLPRI; + const POLLOUT = libc::POLLOUT; + const POLLRDNORM = libc::POLLRDNORM; + const POLLWRNORM = libc::POLLWRNORM; + const POLLRDBAND = libc::POLLRDBAND; + const POLLWRBAND = libc::POLLWRBAND; + const POLLERR = libc::POLLERR; + const POLLHUP = libc::POLLHUP; + const POLLNVAL = libc::POLLNVAL; + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[repr(C)] +pub struct PollFd(libc::pollfd); + +impl PollFd { + pub unsafe fn new(fd: RawFd, events: PollFlags) -> Self { + Self(libc::pollfd { + fd, + events: events.bits(), + revents: PollFlags::empty().bits(), + }) + } + + pub fn revents(self) -> Option { + PollFlags::from_bits(self.0.revents) + } +} + +pub fn poll(fds: &mut [PollFd], timeout: libc::c_int) -> Result { + Errno::from_result(unsafe { + libc::poll( + fds.as_mut_ptr() as *mut libc::pollfd, + fds.len() as libc::nfds_t, + timeout, + ) + }) + .and_then(|nready| nready.try_into().map_err(Into::into)) +} diff --git a/crates/wasi-common/yanix/src/socket.rs b/crates/wasi-common/yanix/src/socket.rs new file mode 100644 index 0000000000..f0f1364e13 --- /dev/null +++ b/crates/wasi-common/yanix/src/socket.rs @@ -0,0 +1,31 @@ +use crate::{Errno, Result}; +use std::os::unix::prelude::*; + +#[derive(Debug, Clone, Copy)] +#[repr(i32)] +pub enum SockType { + Stream = libc::SOCK_STREAM, + Datagram = libc::SOCK_DGRAM, + SeqPacket = libc::SOCK_SEQPACKET, + Raw = libc::SOCK_RAW, + Rdm = libc::SOCK_RDM, +} + +pub unsafe fn get_socket_type(fd: RawFd) -> Result { + use std::mem::{self, MaybeUninit}; + let mut buffer = MaybeUninit::::zeroed().assume_init(); + let mut out_len = mem::size_of::() as libc::socklen_t; + Errno::from_success_code(libc::getsockopt( + fd, + libc::SOL_SOCKET, + libc::SO_TYPE, + &mut buffer as *mut SockType as *mut _, + &mut out_len, + ))?; + assert_eq!( + out_len as usize, + mem::size_of::(), + "invalid SockType value" + ); + Ok(buffer) +} diff --git a/crates/wasi-common/yanix/src/sys/bsd/dir.rs b/crates/wasi-common/yanix/src/sys/bsd/dir.rs new file mode 100644 index 0000000000..2a0b4b3a0e --- /dev/null +++ b/crates/wasi-common/yanix/src/sys/bsd/dir.rs @@ -0,0 +1,65 @@ +use crate::{ + dir::{Dir, Entry, EntryExt, SeekLoc}, + Errno, Result, +}; +use std::ops::Deref; + +#[derive(Copy, Clone, Debug)] +pub(crate) struct EntryImpl { + dirent: libc::dirent, + loc: SeekLoc, +} + +impl Deref for EntryImpl { + type Target = libc::dirent; + + fn deref(&self) -> &Self::Target { + &self.dirent + } +} + +pub(crate) fn iter_impl(dir: &Dir) -> Option> { + let errno = Errno::last(); + let dirent = unsafe { libc::readdir(dir.as_raw().as_ptr()) }; + if dirent.is_null() { + if errno != Errno::last() { + // TODO This should be verified on different BSD-flavours. + // + // According to 4.3BSD/POSIX.1-2001 man pages, there was an error + // if the errno value has changed at some point during the sequence + // of readdir calls. + Some(Err(Errno::last().into())) + } else { + // Not an error. We've simply reached the end of the stream. + None + } + } else { + Some(Ok(EntryImpl { + dirent: unsafe { *dirent }, + loc: dir.tell(), + })) + } +} + +impl EntryExt for Entry { + #[cfg(target_os = "freebsd")] + fn ino(&self) -> u64 { + self.0.d_fileno.into() + } + + #[cfg(not(target_os = "freebsd"))] + fn ino(&self) -> u64 { + self.0.d_ino.into() + } + + fn seek_loc(&self) -> Result { + Ok(self.0.loc) + } +} + +impl SeekLoc { + pub unsafe fn from_raw(loc: i64) -> Result { + let loc = loc.into(); + Ok(Self(loc)) + } +} diff --git a/crates/wasi-common/yanix/src/sys/bsd/fadvise.rs b/crates/wasi-common/yanix/src/sys/bsd/fadvise.rs new file mode 100644 index 0000000000..0f60dcfb32 --- /dev/null +++ b/crates/wasi-common/yanix/src/sys/bsd/fadvise.rs @@ -0,0 +1,78 @@ +use crate::{Errno, Result}; +use std::{convert::TryInto, os::unix::prelude::*}; + +#[cfg(not(any(target_os = "freebsd", target_os = "netbsd")))] +#[derive(Debug, Copy, Clone)] +#[repr(i32)] +pub enum PosixFadviseAdvice { + Normal, + Sequential, + Random, + NoReuse, + WillNeed, + DontNeed, +} + +#[cfg(any(target_os = "freebsd", target_os = "netbsd"))] +#[derive(Debug, Copy, Clone)] +#[repr(i32)] +pub enum PosixFadviseAdvice { + Normal = libc::POSIX_FADV_NORMAL, + Sequential = libc::POSIX_FADV_SEQUENTIAL, + Random = libc::POSIX_FADV_RANDOM, + NoReuse = libc::POSIX_FADV_NOREUSE, + WillNeed = libc::POSIX_FADV_WILLNEED, + DontNeed = libc::POSIX_FADV_DONTNEED, +} + +// There's no posix_fadvise on macOS but we can use fcntl with F_RDADVISE +// command instead to achieve the same +#[cfg(any(target_os = "macos", target_os = "ios"))] +pub unsafe fn posix_fadvise( + fd: RawFd, + offset: libc::off_t, + len: libc::off_t, + _advice: PosixFadviseAdvice, +) -> Result<()> { + // From macOS man pages: + // F_RDADVISE Issue an advisory read async with no copy to user. + // + // The F_RDADVISE command operates on the following structure which holds information passed from + // the user to the system: + // + // struct radvisory { + // off_t ra_offset; /* offset into the file */ + // int ra_count; /* size of the read */ + // }; + let advisory = libc::radvisory { + ra_offset: offset, + ra_count: len.try_into()?, + }; + Errno::from_success_code(libc::fcntl(fd, libc::F_RDADVISE, &advisory)) +} + +#[cfg(any(target_os = "freebsd", target_os = "netbsd"))] +pub unsafe fn posix_fadvise( + fd: RawFd, + offset: libc::off_t, + len: libc::off_t, + advice: PosixFadviseAdvice, +) -> Result<()> { + Errno::from_success_code(libc::posix_fadvise(fd, offset, len, advice as libc::c_int)) +} + +// On BSDs without support we leave it as no-op +#[cfg(not(any( + target_os = "macos", + target_os = "ios", + target_os = "freebsd", + target_os = "netbsd" +)))] +pub unsafe fn posix_fadvise( + _fd: RawFd, + _offset: libc::off_t, + _len: libc::off_t, + _advice: PosixFadviseAdvice, +) -> Result<()> { + Ok(()) +} diff --git a/crates/wasi-common/yanix/src/sys/bsd/file.rs b/crates/wasi-common/yanix/src/sys/bsd/file.rs new file mode 100644 index 0000000000..b367154812 --- /dev/null +++ b/crates/wasi-common/yanix/src/sys/bsd/file.rs @@ -0,0 +1,18 @@ +use crate::{Errno, Result}; +use std::os::unix::prelude::*; + +pub unsafe fn isatty(fd: RawFd) -> Result { + let res = libc::isatty(fd); + if res == 1 { + // isatty() returns 1 if fd is an open file descriptor referring to a terminal... + Ok(true) + } else { + // ... otherwise 0 is returned, and errno is set to indicate the error. + let errno = Errno::last(); + if errno == Errno::ENOTTY { + Ok(false) + } else { + Err(errno.into()) + } + } +} diff --git a/crates/wasi-common/yanix/src/sys/bsd/mod.rs b/crates/wasi-common/yanix/src/sys/bsd/mod.rs new file mode 100644 index 0000000000..3e6611f7b9 --- /dev/null +++ b/crates/wasi-common/yanix/src/sys/bsd/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod dir; +pub(crate) mod fadvise; +pub(crate) mod file; diff --git a/crates/wasi-common/yanix/src/sys/emscripten/mod.rs b/crates/wasi-common/yanix/src/sys/emscripten/mod.rs new file mode 100644 index 0000000000..b9edc97e32 --- /dev/null +++ b/crates/wasi-common/yanix/src/sys/emscripten/mod.rs @@ -0,0 +1,16 @@ +#[path = "../linux/dir.rs"] +pub(crate) mod dir; +#[path = "../linux/fadvise.rs"] +pub(crate) mod fadvise; +#[path = "../linux/file.rs"] +pub(crate) mod file; + +use crate::{dir::SeekLoc, Result}; +use std::convert::TryInto; + +impl SeekLoc { + pub unsafe fn from_raw(loc: i64) -> Result { + let loc = loc.try_into()?; + Ok(Self(loc)) + } +} diff --git a/crates/wasi-common/yanix/src/sys/linux/dir.rs b/crates/wasi-common/yanix/src/sys/linux/dir.rs new file mode 100644 index 0000000000..aa2598f070 --- /dev/null +++ b/crates/wasi-common/yanix/src/sys/linux/dir.rs @@ -0,0 +1,46 @@ +use crate::{ + dir::{Dir, Entry, EntryExt, SeekLoc}, + Errno, Result, +}; +use std::ops::Deref; + +#[derive(Copy, Clone, Debug)] +pub(crate) struct EntryImpl(libc::dirent64); + +impl Deref for EntryImpl { + type Target = libc::dirent64; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl EntryExt for Entry { + fn ino(&self) -> u64 { + self.0.d_ino.into() + } + + fn seek_loc(&self) -> Result { + unsafe { SeekLoc::from_raw(self.0.d_off) } + } +} + +pub(crate) fn iter_impl(dir: &Dir) -> Option> { + let errno = Errno::last(); + let dirent = unsafe { libc::readdir64(dir.as_raw().as_ptr()) }; + if dirent.is_null() { + if errno != Errno::last() { + // TODO This should be verified on different BSD-flavours. + // + // According to 4.3BSD/POSIX.1-2001 man pages, there was an error + // if the errno value has changed at some point during the sequence + // of readdir calls. + Some(Err(Errno::last().into())) + } else { + // Not an error. We've simply reached the end of the stream. + None + } + } else { + Some(Ok(EntryImpl(unsafe { *dirent }))) + } +} diff --git a/crates/wasi-common/yanix/src/sys/linux/fadvise.rs b/crates/wasi-common/yanix/src/sys/linux/fadvise.rs new file mode 100644 index 0000000000..999525e1c7 --- /dev/null +++ b/crates/wasi-common/yanix/src/sys/linux/fadvise.rs @@ -0,0 +1,22 @@ +use crate::{Errno, Result}; +use std::os::unix::prelude::*; + +#[derive(Debug, Copy, Clone)] +#[repr(i32)] +pub enum PosixFadviseAdvice { + Normal = libc::POSIX_FADV_NORMAL, + Sequential = libc::POSIX_FADV_SEQUENTIAL, + Random = libc::POSIX_FADV_RANDOM, + NoReuse = libc::POSIX_FADV_NOREUSE, + WillNeed = libc::POSIX_FADV_WILLNEED, + DontNeed = libc::POSIX_FADV_DONTNEED, +} + +pub unsafe fn posix_fadvise( + fd: RawFd, + offset: libc::off_t, + len: libc::off_t, + advice: PosixFadviseAdvice, +) -> Result<()> { + Errno::from_success_code(libc::posix_fadvise(fd, offset, len, advice as libc::c_int)) +} diff --git a/crates/wasi-common/yanix/src/sys/linux/file.rs b/crates/wasi-common/yanix/src/sys/linux/file.rs new file mode 100644 index 0000000000..c5f88f457b --- /dev/null +++ b/crates/wasi-common/yanix/src/sys/linux/file.rs @@ -0,0 +1,23 @@ +use crate::{Errno, Result}; +use std::os::unix::prelude::*; + +pub unsafe fn isatty(fd: RawFd) -> Result { + let res = libc::isatty(fd); + if res == 1 { + // isatty() returns 1 if fd is an open file descriptor referring to a terminal... + Ok(true) + } else { + // ... otherwise 0 is returned, and errno is set to indicate the error. + let errno = Errno::last(); + // While POSIX specifies ENOTTY if the passed + // fd is *not* a tty, on Linux, some implementations + // may return EINVAL instead. + // + // https://linux.die.net/man/3/isatty + if errno == Errno::ENOTTY || errno == Errno::EINVAL { + Ok(false) + } else { + Err(errno.into()) + } + } +} diff --git a/crates/wasi-common/yanix/src/sys/linux/mod.rs b/crates/wasi-common/yanix/src/sys/linux/mod.rs new file mode 100644 index 0000000000..d7beaa7cbe --- /dev/null +++ b/crates/wasi-common/yanix/src/sys/linux/mod.rs @@ -0,0 +1,12 @@ +pub(crate) mod dir; +pub(crate) mod fadvise; +pub(crate) mod file; + +use crate::{dir::SeekLoc, Result}; + +impl SeekLoc { + pub unsafe fn from_raw(loc: i64) -> Result { + let loc = loc.into(); + Ok(Self(loc)) + } +} diff --git a/crates/wasi-common/yanix/src/sys/mod.rs b/crates/wasi-common/yanix/src/sys/mod.rs new file mode 100644 index 0000000000..3743f6f190 --- /dev/null +++ b/crates/wasi-common/yanix/src/sys/mod.rs @@ -0,0 +1,28 @@ +use crate::{dir::SeekLoc, Result}; +use cfg_if::cfg_if; + +cfg_if! { + if #[cfg(any(target_os = "linux", + target_os = "android"))] { + mod linux; + pub(crate) use linux::*; + } else if #[cfg(target_os = "emscripten")] { + mod emscripten; + pub(crate) use emscripten::*; + } else if #[cfg(any(target_os = "macos", + target_os = "ios", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + target_os = "dragonfly"))] { + mod bsd; + pub(crate) use bsd::*; + } else { + compile_error!("yanix doesn't compile for this platform yet"); + } +} + +pub trait EntryExt { + fn ino(&self) -> u64; + fn seek_loc(&self) -> Result; +} diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml new file mode 100644 index 0000000000..47fae58778 --- /dev/null +++ b/crates/wasi/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "wasmtime-wasi" +version = "0.12.0" +authors = ["The Cranelift Project Developers"] +description = "WASI API support for Wasmtime" +license = "Apache-2.0 WITH LLVM-exception" +categories = ["wasm"] +keywords = ["webassembly", "wasm"] +repository = "https://github.com/bytecodealliance/wasmtime" +readme = "README.md" +edition = "2018" + +[dependencies] +anyhow = "1.0" +log = { version = "0.4.8", default-features = false } +wasi-common = { path = "../wasi-common", version = "0.12.0" } +wasmtime = { path = "../api", version = "0.12.0" } +wasmtime-runtime = { path = "../runtime", version = "0.12.0" } +wig = { path = "../wasi-common/wig", version = "0.12.0" } + +[badges] +maintenance = { status = "actively-developed" } diff --git a/crates/wasi/LICENSE b/crates/wasi/LICENSE new file mode 100644 index 0000000000..f9d81955f4 --- /dev/null +++ b/crates/wasi/LICENSE @@ -0,0 +1,220 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + diff --git a/crates/wasi/README.md b/crates/wasi/README.md new file mode 100644 index 0000000000..7f2eaad27f --- /dev/null +++ b/crates/wasi/README.md @@ -0,0 +1,8 @@ +This is the `wasmtime-wasi` crate, which implements the +WebAssembly System Interface (WASI) API in Rust. + +WASI is greatly inspired by and directly derived from [CloudABI]. +It differs in that it has aspirations to expand to a greater +scope, and to better support the needs of WebAssembly engines. + +[CloudABI]: https://cloudabi.org/ diff --git a/crates/wasi/src/lib.rs b/crates/wasi/src/lib.rs new file mode 100644 index 0000000000..5a6d29c199 --- /dev/null +++ b/crates/wasi/src/lib.rs @@ -0,0 +1,75 @@ +pub mod old; + +use wasi_common::hostcalls; + +pub use wasi_common::{WasiCtx, WasiCtxBuilder}; + +// Defines a `struct Wasi` with member fields and appropriate APIs for dealing +// with all the various WASI exports. +wig::define_wasi_struct!( + "snapshot" "wasi_snapshot_preview1" +); + +pub fn is_wasi_module(name: &str) -> bool { + // FIXME: this should be more conservative, but while WASI is in flux and + // we're figuring out how to support multiple revisions, this should do the + // trick. + name.starts_with("wasi") +} + +/// This is an internal structure used to acquire a handle on the caller's +/// wasm memory buffer. +/// +/// This exploits how we can implement `WasmTy` for ourselves locally even +/// though crates in general should not be doing that. This is a crate in +/// the wasmtime project, however, so we should be able to keep up with our own +/// changes. +/// +/// In general this type is wildly unsafe. We need to update the wasi crates to +/// probably work with more `wasmtime`-like APIs to grip with the unsafety +/// around dealing with caller memory. +struct WasiCallerMemory { + base: *mut u8, + len: usize, +} + +impl wasmtime::WasmTy for WasiCallerMemory { + type Abi = (); + + fn push(_dst: &mut Vec) {} + + fn matches(_tys: impl Iterator) -> anyhow::Result<()> { + Ok(()) + } + + fn from_abi(vmctx: *mut wasmtime_runtime::VMContext, _abi: ()) -> Self { + unsafe { + match wasmtime_runtime::InstanceHandle::from_vmctx(vmctx).lookup("memory") { + Some(wasmtime_runtime::Export::Memory { + definition, + vmctx: _, + memory: _, + }) => WasiCallerMemory { + base: (*definition).base, + len: (*definition).current_length, + }, + _ => WasiCallerMemory { + base: std::ptr::null_mut(), + len: 0, + }, + } + } + } + + fn into_abi(self) {} +} + +impl WasiCallerMemory { + unsafe fn get(&self) -> Result<&mut [u8], wasi_common::wasi::__wasi_errno_t> { + if self.base.is_null() { + Err(wasi_common::wasi::__WASI_ERRNO_INVAL) + } else { + Ok(std::slice::from_raw_parts_mut(self.base, self.len)) + } + } +} diff --git a/crates/wasi/src/old/mod.rs b/crates/wasi/src/old/mod.rs new file mode 100644 index 0000000000..5d4d33030a --- /dev/null +++ b/crates/wasi/src/old/mod.rs @@ -0,0 +1 @@ +pub mod snapshot_0; diff --git a/crates/wasi/src/old/snapshot_0.rs b/crates/wasi/src/old/snapshot_0.rs new file mode 100644 index 0000000000..23cc8e1b28 --- /dev/null +++ b/crates/wasi/src/old/snapshot_0.rs @@ -0,0 +1,15 @@ +use wasi_common::old::snapshot_0::hostcalls; +use wasi_common::old::snapshot_0::WasiCtx; + +// Defines a `struct Wasi` with member fields and appropriate APIs for dealing +// with all the various WASI exports. +wig::define_wasi_struct!( + "old/snapshot_0" "wasi_unstable" +); + +pub fn is_wasi_module(name: &str) -> bool { + // FIXME: this should be more conservative, but while WASI is in flux and + // we're figuring out how to support multiple revisions, this should do the + // trick. + name.starts_with("wasi") +} diff --git a/crates/wast/Cargo.toml b/crates/wast/Cargo.toml new file mode 100644 index 0000000000..f21d04df87 --- /dev/null +++ b/crates/wast/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "wasmtime-wast" +version = "0.12.0" +authors = ["The Wasmtime Project Developers"] +description = "wast testing support for wasmtime" +license = "Apache-2.0 WITH LLVM-exception" +categories = ["wasm"] +keywords = ["webassembly", "wasm"] +repository = "https://github.com/bytecodealliance/wasmtime" +readme = "README.md" +edition = "2018" + +[dependencies] +anyhow = "1.0.19" +wasmtime = { path = "../api", version = "0.12.0" } +wast = "9.0.0" + +[badges] +maintenance = { status = "actively-developed" } + +[features] +lightbeam = [] diff --git a/crates/wast/LICENSE b/crates/wast/LICENSE new file mode 100644 index 0000000000..f9d81955f4 --- /dev/null +++ b/crates/wast/LICENSE @@ -0,0 +1,220 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + diff --git a/crates/wast/README.md b/crates/wast/README.md new file mode 100644 index 0000000000..ff671b026f --- /dev/null +++ b/crates/wast/README.md @@ -0,0 +1,5 @@ +This is the `wasmtime-wast` crate, which contains an implementation of WebAssembly's +"wast" test scripting language, which is used in the +[WebAssembly spec testsuite], using wasmtime for execution. + +[WebAssembly spec testsuite]: https://github.com/WebAssembly/testsuite diff --git a/crates/wast/src/lib.rs b/crates/wast/src/lib.rs new file mode 100644 index 0000000000..4f8f54f733 --- /dev/null +++ b/crates/wast/src/lib.rs @@ -0,0 +1,32 @@ +//! Implementation of the WAST text format for wasmtime. + +#![deny(missing_docs, trivial_numeric_casts, unused_extern_crates)] +#![warn(unused_import_braces)] +#![deny(unstable_features)] +#![cfg_attr(feature = "clippy", plugin(clippy(conf_file = "../../clippy.toml")))] +#![cfg_attr( + feature = "cargo-clippy", + allow(clippy::new_without_default, clippy::new_without_default_derive) +)] +#![cfg_attr( + feature = "cargo-clippy", + warn( + clippy::float_arithmetic, + clippy::mut_mut, + clippy::nonminimal_bool, + clippy::option_map_unwrap_or, + clippy::option_map_unwrap_or_else, + clippy::print_stdout, + clippy::unicode_not_nfc, + clippy::use_self + ) +)] + +mod spectest; +mod wast; + +pub use crate::spectest::instantiate_spectest; +pub use crate::wast::WastContext; + +/// Version number of this crate. +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/crates/wast/src/spectest.rs b/crates/wast/src/spectest.rs new file mode 100644 index 0000000000..4fcfe190f2 --- /dev/null +++ b/crates/wast/src/spectest.rs @@ -0,0 +1,61 @@ +use std::collections::HashMap; +use wasmtime::*; + +/// Return an instance implementing the "spectest" interface used in the +/// spec testsuite. +pub fn instantiate_spectest(store: &Store) -> HashMap<&'static str, Extern> { + let mut ret = HashMap::new(); + + let func = Func::wrap0(store, || {}); + ret.insert("print", Extern::Func(func)); + + let func = Func::wrap1(store, |val: i32| println!("{}: i32", val)); + ret.insert("print_i32", Extern::Func(func)); + + let func = Func::wrap1(store, |val: i64| println!("{}: i64", val)); + ret.insert("print_i64", Extern::Func(func)); + + let func = Func::wrap1(store, |val: f32| println!("{}: f32", val)); + ret.insert("print_f32", Extern::Func(func)); + + let func = Func::wrap1(store, |val: f64| println!("{}: f64", val)); + ret.insert("print_f64", Extern::Func(func)); + + let func = Func::wrap2(store, |i: i32, f: f32| { + println!("{}: i32", i); + println!("{}: f32", f); + }); + ret.insert("print_i32_f32", Extern::Func(func)); + + let func = Func::wrap2(store, |f1: f64, f2: f64| { + println!("{}: f64", f1); + println!("{}: f64", f2); + }); + ret.insert("print_f64_f64", Extern::Func(func)); + + let ty = GlobalType::new(ValType::I32, Mutability::Const); + let g = Global::new(store, ty, Val::I32(666)).unwrap(); + ret.insert("global_i32", Extern::Global(g)); + + let ty = GlobalType::new(ValType::I64, Mutability::Const); + let g = Global::new(store, ty, Val::I64(666)).unwrap(); + ret.insert("global_i64", Extern::Global(g)); + + let ty = GlobalType::new(ValType::F32, Mutability::Const); + let g = Global::new(store, ty, Val::F32(0x4426_8000)).unwrap(); + ret.insert("global_f32", Extern::Global(g)); + + let ty = GlobalType::new(ValType::F64, Mutability::Const); + let g = Global::new(store, ty, Val::F64(0x4084_d000_0000_0000)).unwrap(); + ret.insert("global_f64", Extern::Global(g)); + + let ty = TableType::new(ValType::FuncRef, Limits::new(10, Some(20))); + let table = Table::new(store, ty, Val::AnyRef(AnyRef::Null)).unwrap(); + ret.insert("table", Extern::Table(table)); + + let ty = MemoryType::new(Limits::new(1, Some(2))); + let memory = Memory::new(store, ty); + ret.insert("memory", Extern::Memory(memory)); + + return ret; +} diff --git a/crates/wast/src/wast.rs b/crates/wast/src/wast.rs new file mode 100644 index 0000000000..4d6ac4f4e5 --- /dev/null +++ b/crates/wast/src/wast.rs @@ -0,0 +1,450 @@ +use crate::spectest::instantiate_spectest; +use anyhow::{anyhow, bail, Context as _, Result}; +use std::collections::HashMap; +use std::path::Path; +use std::str; +use wasmtime::*; + +/// Translate from a `script::Value` to a `RuntimeValue`. +fn runtime_value(v: &wast::Expression<'_>) -> Result { + use wast::Instruction::*; + + if v.instrs.len() != 1 { + bail!("too many instructions in {:?}", v); + } + Ok(match &v.instrs[0] { + I32Const(x) => Val::I32(*x), + I64Const(x) => Val::I64(*x), + F32Const(x) => Val::F32(x.bits), + F64Const(x) => Val::F64(x.bits), + V128Const(x) => Val::V128(u128::from_le_bytes(x.to_le_bytes())), + other => bail!("couldn't convert {:?} to a runtime value", other), + }) +} + +/// The wast test script language allows modules to be defined and actions +/// to be performed on them. +pub struct WastContext { + /// Wast files have a concept of a "current" module, which is the most + /// recently defined. + current: Option, + + instances: HashMap, + store: Store, + spectest: Option>, +} + +enum Outcome> { + Ok(T), + Trap(Trap), +} + +impl Outcome { + fn into_result(self) -> Result { + match self { + Outcome::Ok(t) => Ok(t), + Outcome::Trap(t) => Err(t), + } + } +} + +impl WastContext { + /// Construct a new instance of `WastContext`. + pub fn new(store: Store) -> Self { + Self { + current: None, + store, + spectest: None, + instances: HashMap::new(), + } + } + + fn get_instance(&self, instance_name: Option<&str>) -> Result { + match instance_name { + Some(name) => self + .instances + .get(name) + .cloned() + .ok_or_else(|| anyhow!("failed to find instance named `{}`", name)), + None => self + .current + .clone() + .ok_or_else(|| anyhow!("no previous instance found")), + } + } + + fn instantiate(&self, module: &[u8]) -> Result> { + let module = Module::new(&self.store, module)?; + let mut imports = Vec::new(); + for import in module.imports() { + if import.module() == "spectest" { + let spectest = self + .spectest + .as_ref() + .ok_or_else(|| anyhow!("spectest module isn't instantiated"))?; + let export = spectest + .get(import.name()) + .ok_or_else(|| anyhow!("unknown import `spectest::{}`", import.name()))?; + imports.push(export.clone()); + continue; + } + + let instance = self + .instances + .get(import.module()) + .ok_or_else(|| anyhow!("no module named `{}`", import.module()))?; + let export = instance + .get_export(import.name()) + .ok_or_else(|| anyhow!("unknown import `{}::{}`", import.name(), import.module()))? + .clone(); + imports.push(export); + } + let instance = match Instance::new(&module, &imports) { + Ok(i) => i, + Err(e) => return e.downcast::().map(Outcome::Trap), + }; + Ok(Outcome::Ok(instance)) + } + + /// Register "spectest" which is used by the spec testsuite. + pub fn register_spectest(&mut self) -> Result<()> { + self.spectest = Some(instantiate_spectest(&self.store)); + Ok(()) + } + + /// Perform the action portion of a command. + fn perform_execute(&mut self, exec: wast::WastExecute<'_>) -> Result { + match exec { + wast::WastExecute::Invoke(invoke) => self.perform_invoke(invoke), + wast::WastExecute::Module(mut module) => { + let binary = module.encode()?; + let result = self.instantiate(&binary)?; + Ok(match result { + Outcome::Ok(_) => Outcome::Ok(Vec::new()), + Outcome::Trap(e) => Outcome::Trap(e), + }) + } + wast::WastExecute::Get { module, global } => self.get(module.map(|s| s.name()), global), + } + } + + fn perform_invoke(&mut self, exec: wast::WastInvoke<'_>) -> Result { + let values = exec + .args + .iter() + .map(runtime_value) + .collect::>>()?; + self.invoke(exec.module.map(|i| i.name()), exec.name, &values) + } + + /// Define a module and register it. + fn module(&mut self, instance_name: Option<&str>, module: &[u8]) -> Result<()> { + let instance = match self.instantiate(module)? { + Outcome::Ok(i) => i, + Outcome::Trap(e) => bail!("instantiation failed with: {}", e.message()), + }; + if let Some(name) = instance_name { + self.instances.insert(name.to_string(), instance.clone()); + } + self.current = Some(instance); + Ok(()) + } + + /// Register an instance to make it available for performing actions. + fn register(&mut self, name: Option<&str>, as_name: &str) -> Result<()> { + let instance = self.get_instance(name)?.clone(); + self.instances.insert(as_name.to_string(), instance); + Ok(()) + } + + /// Invoke an exported function from an instance. + fn invoke( + &mut self, + instance_name: Option<&str>, + field: &str, + args: &[Val], + ) -> Result { + let instance = self.get_instance(instance_name.as_ref().map(|x| &**x))?; + let func = instance + .get_export(field) + .and_then(|e| e.func()) + .ok_or_else(|| anyhow!("no function named `{}`", field))?; + Ok(match func.call(args) { + Ok(result) => Outcome::Ok(result.into()), + Err(e) => Outcome::Trap(e), + }) + } + + /// Get the value of an exported global from an instance. + fn get(&mut self, instance_name: Option<&str>, field: &str) -> Result { + let instance = self.get_instance(instance_name.as_ref().map(|x| &**x))?; + let global = instance + .get_export(field) + .and_then(|e| e.global()) + .ok_or_else(|| anyhow!("no global named `{}`", field))?; + Ok(Outcome::Ok(vec![global.get()])) + } + + fn assert_return(&self, result: Outcome, results: &[wast::AssertExpression]) -> Result<()> { + let values = result.into_result()?; + for (v, e) in values.iter().zip(results) { + if val_matches(v, e)? { + continue; + } + bail!("expected {:?}, got {:?}", e, v) + } + Ok(()) + } + + fn assert_trap(&self, result: Outcome, message: &str) -> Result<()> { + let trap = match result { + Outcome::Ok(values) => bail!("expected trap, got {:?}", values), + Outcome::Trap(t) => t, + }; + if trap.message().contains(message) { + return Ok(()); + } + if cfg!(feature = "lightbeam") { + println!("TODO: Check the assert_trap message: {}", message); + return Ok(()); + } + bail!("expected {}, got {}", message, trap.message()) + } + + /// Run a wast script from a byte buffer. + pub fn run_buffer(&mut self, filename: &str, wast: &[u8]) -> Result<()> { + let wast = str::from_utf8(wast)?; + + let adjust_wast = |mut err: wast::Error| { + err.set_path(filename.as_ref()); + err.set_text(wast); + err + }; + + let buf = wast::parser::ParseBuffer::new(wast).map_err(adjust_wast)?; + let ast = wast::parser::parse::(&buf).map_err(adjust_wast)?; + + for directive in ast.directives { + let sp = directive.span(); + self.run_directive(directive).with_context(|| { + let (line, col) = sp.linecol_in(wast); + format!("failed directive on {}:{}:{}", filename, line + 1, col) + })?; + } + Ok(()) + } + + fn run_directive(&mut self, directive: wast::WastDirective) -> Result<()> { + use wast::WastDirective::*; + + match directive { + Module(mut module) => { + let binary = module.encode()?; + self.module(module.id.map(|s| s.name()), &binary)?; + } + Register { + span: _, + name, + module, + } => { + self.register(module.map(|s| s.name()), name)?; + } + Invoke(i) => { + self.perform_invoke(i)?; + } + AssertReturn { + span: _, + exec, + results, + } => { + let result = self.perform_execute(exec)?; + self.assert_return(result, &results)?; + } + AssertTrap { + span: _, + exec, + message, + } => { + let result = self.perform_execute(exec)?; + self.assert_trap(result, message)?; + } + AssertExhaustion { + span: _, + call, + message, + } => { + let result = self.perform_invoke(call)?; + self.assert_trap(result, message)?; + } + AssertInvalid { + span: _, + mut module, + message, + } => { + let bytes = module.encode()?; + let err = match self.module(None, &bytes) { + Ok(()) => bail!("expected module to fail to build"), + Err(e) => e, + }; + let error_message = format!("{:?}", err); + if !is_matching_assert_invalid_error_message(&message, &error_message) { + bail!( + "assert_invalid: expected \"{}\", got \"{}\"", + message, + error_message + ) + } + } + AssertMalformed { + module, + span: _, + message: _, + } => { + let mut module = match module { + wast::QuoteModule::Module(m) => m, + // This is a `*.wat` parser test which we're not + // interested in. + wast::QuoteModule::Quote(_) => return Ok(()), + }; + let bytes = module.encode()?; + if let Ok(_) = self.module(None, &bytes) { + bail!("expected malformed module to fail to instantiate"); + } + } + AssertUnlinkable { + span: _, + mut module, + message, + } => { + let bytes = module.encode()?; + let err = match self.module(None, &bytes) { + Ok(()) => bail!("expected module to fail to link"), + Err(e) => e, + }; + let error_message = format!("{:?}", err); + if !error_message.contains(&message) { + bail!( + "assert_unlinkable: expected {}, got {}", + message, + error_message + ) + } + } + } + + Ok(()) + } + + /// Run a wast script from a file. + pub fn run_file(&mut self, path: &Path) -> Result<()> { + let bytes = + std::fs::read(path).with_context(|| format!("failed to read `{}`", path.display()))?; + self.run_buffer(path.to_str().unwrap(), &bytes) + } +} + +fn is_matching_assert_invalid_error_message(expected: &str, actual: &str) -> bool { + actual.contains(expected) + // Waiting on https://github.com/WebAssembly/bulk-memory-operations/pull/137 + // to propagate to WebAssembly/testsuite. + || (expected.contains("unknown table") && actual.contains("unknown elem")) + // `elem.wast` and `proposals/bulk-memory-operations/elem.wast` disagree + // on the expected error message for the same error. + || (expected.contains("out of bounds") && actual.contains("does not fit")) +} + +fn extract_lane_as_i8(bytes: u128, lane: usize) -> i8 { + (bytes >> (lane * 8)) as i8 +} + +fn extract_lane_as_i16(bytes: u128, lane: usize) -> i16 { + (bytes >> (lane * 16)) as i16 +} + +fn extract_lane_as_i32(bytes: u128, lane: usize) -> i32 { + (bytes >> (lane * 32)) as i32 +} + +fn extract_lane_as_i64(bytes: u128, lane: usize) -> i64 { + (bytes >> (lane * 64)) as i64 +} + +fn is_canonical_f32_nan(bits: u32) -> bool { + (bits & 0x7fff_ffff) == 0x7fc0_0000 +} + +fn is_canonical_f64_nan(bits: u64) -> bool { + (bits & 0x7fff_ffff_ffff_ffff) == 0x7ff8_0000_0000_0000 +} + +fn is_arithmetic_f32_nan(bits: u32) -> bool { + const AF32_NAN: u32 = 0x0040_0000; + (bits & AF32_NAN) == AF32_NAN +} + +fn is_arithmetic_f64_nan(bits: u64) -> bool { + const AF64_NAN: u64 = 0x0008_0000_0000_0000; + (bits & AF64_NAN) == AF64_NAN +} + +fn val_matches(actual: &Val, expected: &wast::AssertExpression) -> Result { + Ok(match (actual, expected) { + (Val::I32(a), wast::AssertExpression::I32(b)) => a == b, + (Val::I64(a), wast::AssertExpression::I64(b)) => a == b, + // Note that these float comparisons are comparing bits, not float + // values, so we're testing for bit-for-bit equivalence + (Val::F32(a), wast::AssertExpression::F32(b)) => f32_matches(*a, b), + (Val::F64(a), wast::AssertExpression::F64(b)) => f64_matches(*a, b), + (Val::V128(a), wast::AssertExpression::V128(b)) => v128_matches(*a, b), + _ => bail!( + "don't know how to compare {:?} and {:?} yet", + actual, + expected + ), + }) +} + +fn f32_matches(actual: u32, expected: &wast::NanPattern) -> bool { + match expected { + wast::NanPattern::CanonicalNan => is_canonical_f32_nan(actual), + wast::NanPattern::ArithmeticNan => is_arithmetic_f32_nan(actual), + wast::NanPattern::Value(expected_value) => actual == expected_value.bits, + } +} + +fn f64_matches(actual: u64, expected: &wast::NanPattern) -> bool { + match expected { + wast::NanPattern::CanonicalNan => is_canonical_f64_nan(actual), + wast::NanPattern::ArithmeticNan => is_arithmetic_f64_nan(actual), + wast::NanPattern::Value(expected_value) => actual == expected_value.bits, + } +} + +fn v128_matches(actual: u128, expected: &wast::V128Pattern) -> bool { + match expected { + wast::V128Pattern::I8x16(b) => b + .iter() + .enumerate() + .all(|(i, b)| *b == extract_lane_as_i8(actual, i)), + wast::V128Pattern::I16x8(b) => b + .iter() + .enumerate() + .all(|(i, b)| *b == extract_lane_as_i16(actual, i)), + wast::V128Pattern::I32x4(b) => b + .iter() + .enumerate() + .all(|(i, b)| *b == extract_lane_as_i32(actual, i)), + wast::V128Pattern::I64x2(b) => b + .iter() + .enumerate() + .all(|(i, b)| *b == extract_lane_as_i64(actual, i)), + wast::V128Pattern::F32x4(b) => b.iter().enumerate().all(|(i, b)| { + let a = extract_lane_as_i32(actual, i) as u32; + f32_matches(a, b) + }), + wast::V128Pattern::F64x2(b) => b.iter().enumerate().all(|(i, b)| { + let a = extract_lane_as_i64(actual, i) as u64; + f64_matches(a, b) + }), + } +} diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md new file mode 100644 index 0000000000..2eb5b2b35b --- /dev/null +++ b/docs/SUMMARY.md @@ -0,0 +1,39 @@ +# Summary + +- [Introduction](./introduction.md) +- [Tutorial](./tutorial.md) + - [Creating `hello-world.wasm`](./tutorial-create-hello-world.md) + - [Running `hello-world.wasm`](./tutorial-run-hello-world.md) +- [Examples](./examples.md) + - [Markdown parser](./examples-markdown.md) +- [Using WebAssembly from your lanugage](./lang.md) + - [Python](./lang-python.md) + - [.NET](./lang-dotnet.md) + - [Rust](./lang-rust.md) + - [Bash](./lang-bash.md) +- [Using the `wasmtime` CLI](./cli.md) + - [Installation](./cli-install.md) + - [CLI Options](./cli-options.md) + - [Cache Configuration](./cli-cache.md) +- [Writing WebAssembly](./wasm.md) + - [Rust](./wasm-rust.md) + - [C/C++](./wasm-c.md) + - [WebAssembly Text Format (`*.wat`)](./wasm-wat.md) + - [Example: Markdown Parser](./wasm-markdown.md) +- [Embedding Wasmtime](embed.md) + - [Rust API](./embed-rust.md) + - [C/C++ API](./embed-c.md) +- [Stability](stability.md) + - [Release Process](./stability-release.md) + - [Platform Support](./stability-platform-support.md) +- [Security](security.md) + - [Disclosure Policy](./security-disclosure.md) + - [Sandboxing](./security-sandboxing.md) +- [Contributing](contributing.md) + - [Building](./contributing-building.md) + - [Testing](./contributing-testing.md) + - [Fuzzing](./contributing-fuzzing.md) + - [CI](./contributing-ci.md) + - [Release Process](./contributing-release-process.md) + - [Governance](./contributing-governance.md) + - [Code of Conduct](./contributing-coc.md) diff --git a/docs/WASI-api.md b/docs/WASI-api.md new file mode 100644 index 0000000000..39bf49e9fa --- /dev/null +++ b/docs/WASI-api.md @@ -0,0 +1,2361 @@ + + +# WASI Core API + +This is the API-level documentation for WASI Core. The function names +are prefixed with "\_\_wasi\_" to reflect how they are spelled in +flat-namespace contexts, however at the wasm module level, they are +unprefixed, because they're inside a module namespace (currently +"wasi\_unstable"). + +Functions that start with `__wasi_fd_` operate on file descriptors, +while functions that start with `__wasi_path_` operate on filesystem +paths, which are relative to directory file descriptors. + +Much inspiration and content here is derived from [CloudABI] and [POSIX], +though there are also several differences from CloudABI and POSIX. For +example, WASI Core has no concept of processes in the traditional Unix +sense. While wasm linear memories have some of the aspects of processes, +and it's possible to *emulate* the full semantics of processes on top of +them, this can sometimes be unnatural and inefficient. The goal for +WASI Core is to be a WebAssembly-native API that exposes APIs that fit +well into the underlying WebAssembly platform, rather than to directly +emulate other platforms. + +This is also a work in progress, and the API here is still evolving. + +[CloudABI]: https://github.com/NuxiNL/cloudabi +[POSIX]: http://pubs.opengroup.org/onlinepubs/9699919799/ + +## System calls + +- [`__wasi_args_get()`](#args_get) +- [`__wasi_args_sizes_get()`](#args_sizes_get) +- [`__wasi_clock_res_get()`](#clock_res_get) +- [`__wasi_clock_time_get()`](#clock_time_get) +- [`__wasi_environ_get()`](#environ_get) +- [`__wasi_environ_sizes_get()`](#environ_sizes_get) +- [`__wasi_fd_advise()`](#fd_advise) +- [`__wasi_fd_allocate()`](#fd_allocate) +- [`__wasi_fd_close()`](#fd_close) +- [`__wasi_fd_datasync()`](#fd_datasync) +- [`__wasi_fd_fdstat_get()`](#fd_fdstat_get) +- [`__wasi_fd_fdstat_set_flags()`](#fd_fdstat_set_flags) +- [`__wasi_fd_fdstat_set_rights()`](#fd_fdstat_set_rights) +- [`__wasi_fd_filestat_get()`](#fd_filestat_get) +- [`__wasi_fd_filestat_set_size()`](#fd_filestat_set_size) +- [`__wasi_fd_filestat_set_times()`](#fd_filestat_set_times) +- [`__wasi_fd_pread()`](#fd_pread) +- [`__wasi_fd_prestat_dir_name()`](#fd_prestat_dir_name) +- [`__wasi_fd_prestat_get()`](#fd_prestat_get) +- [`__wasi_fd_pwrite()`](#fd_pwrite) +- [`__wasi_fd_read()`](#fd_read) +- [`__wasi_fd_readdir()`](#fd_readdir) +- [`__wasi_fd_renumber()`](#fd_renumber) +- [`__wasi_fd_seek()`](#fd_seek) +- [`__wasi_fd_sync()`](#fd_sync) +- [`__wasi_fd_tell()`](#fd_tell) +- [`__wasi_fd_write()`](#fd_write) +- [`__wasi_path_create_directory()`](#path_create_directory) +- [`__wasi_path_filestat_get()`](#path_filestat_get) +- [`__wasi_path_filestat_set_times()`](#path_filestat_set_times) +- [`__wasi_path_link()`](#path_link) +- [`__wasi_path_open()`](#path_open) +- [`__wasi_path_readlink()`](#path_readlink) +- [`__wasi_path_remove_directory()`](#path_remove_directory) +- [`__wasi_path_rename()`](#path_rename) +- [`__wasi_path_symlink()`](#path_symlink) +- [`__wasi_path_unlink_file()`](#path_unlink_file) +- [`__wasi_poll_oneoff()`](#poll_oneoff) +- [`__wasi_proc_exit()`](#proc_exit) +- [`__wasi_proc_raise()`](#proc_raise) +- [`__wasi_random_get()`](#random_get) +- [`__wasi_sched_yield()`](#sched_yield) +- [`__wasi_sock_recv()`](#sock_recv) +- [`__wasi_sock_send()`](#sock_send) +- [`__wasi_sock_shutdown()`](#sock_shutdown) + +### `__wasi_args_get()` + +Read command-line argument data. + +The sizes of the buffers should match that returned by [`__wasi_args_sizes_get()`](#args_sizes_get). + +Inputs: + +- char \*\*argv + + A pointer to a buffer to write the argument pointers. + +- char \*argv\_buf + + A pointer to a buffer to write the argument string data. + +### `__wasi_args_sizes_get()` + +Return command-line argument data sizes. + +Outputs: + +- size\_t argc + + The number of arguments. + +- size\_t argv\_buf\_size + + The size of the argument string data. + +### `__wasi_clock_res_get()` + +Return the resolution of a clock. + +Implementations are required to provide a non-zero value for supported clocks. +For unsupported clocks, return [`__WASI_EINVAL`](#errno.inval). + +Note: This is similar to `clock_getres` in POSIX. + +Inputs: + +- [\_\_wasi\_clockid\_t](#clockid) clock\_id + + The clock for which to return the resolution. + +Outputs: + +- [\_\_wasi\_timestamp\_t](#timestamp) resolution + + The resolution of the clock. + +### `__wasi_clock_time_get()` + +Return the time value of a clock. + +Note: This is similar to `clock_gettime` in POSIX. + +Inputs: + +- [\_\_wasi\_clockid\_t](#clockid) clock\_id + + The clock for which to return the time. + +- [\_\_wasi\_timestamp\_t](#timestamp) precision + + The maximum lag (exclusive) that the returned + time value may have, compared to its actual + value. + +Outputs: + +- [\_\_wasi\_timestamp\_t](#timestamp) time + + The time value of the clock. + +### `__wasi_environ_get()` + +Read environment variable data. + +The sizes of the buffers should match that returned by [`__wasi_environ_sizes_get()`](#environ_sizes_get). + +Inputs: + +- char \*\*environ + + A pointer to a buffer to write the environment variable pointers. + +- char \*environ\_buf + + A pointer to a buffer to write the environment variable string data. + +### `__wasi_environ_sizes_get()` + +Return environment variable data sizes. + +Outputs: + +- size\_t environ\_count + + The number of environment variables. + +- size\_t environ\_buf\_size + + The size of the environment variable string data. + +### `__wasi_fd_advise()` + +Provide file advisory information on a file descriptor. + +Note: This is similar to `posix_fadvise` in POSIX. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + The file descriptor for the file for which to provide file advisory information. + +- [\_\_wasi\_filesize\_t](#filesize) offset + + The offset within the file to which the advisory applies. + +- [\_\_wasi\_filesize\_t](#filesize) len + + The length of the region to which the advisory applies. + +- [\_\_wasi\_advice\_t](#advice) advice + + The advice. + +### `__wasi_fd_allocate()` + +Force the allocation of space in a file. + +Note: This is similar to `posix_fallocate` in POSIX. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + The file descriptor for the file in which to allocate space. + +- [\_\_wasi\_filesize\_t](#filesize) offset + + The offset at which to start the allocation. + +- [\_\_wasi\_filesize\_t](#filesize) len + + The length of the area that is allocated. + +### `__wasi_fd_close()` + +Close a file descriptor. + +Note: This is similar to `close` in POSIX. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + The file descriptor to close. + +### `__wasi_fd_datasync()` + +Synchronize the data of a file to disk. + +Note: This is similar to `fdatasync` in POSIX. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + The file descriptor of the file to synchronize to disk. + +### `__wasi_fd_fdstat_get()` + +Get the attributes of a file descriptor. + +Note: This returns similar flags to `fcntl(fd, F_GETFL)` in POSIX, as well +as additional fields. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + The file descriptor to inspect. + +- [\_\_wasi\_fdstat\_t](#fdstat) \*buf + + The buffer where the file descriptor's attributes are stored. + +### `__wasi_fd_fdstat_set_flags()` + +Adjust the flags associated with a file descriptor. + +Note: This is similar to `fcntl(fd, F_SETFL, flags)` in POSIX. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + The file descriptor to operate on. + +- [\_\_wasi\_fdflags\_t](#fdflags) flags + + The desired values of the file descriptor flags. + +### `__wasi_fd_fdstat_set_rights()` + +Adjust the rights associated with a file descriptor. + +This can only be used to remove rights, and returns +[`__WASI_ENOTCAPABLE`](#errno.notcapable) if called in a way that would attempt +to add rights. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + The file descriptor to operate on. + +- [\_\_wasi\_rights\_t](#rights) fs\_rights\_base + + The desired base rights of the file descriptor. + +- [\_\_wasi\_rights\_t](#rights) fs\_rights\_inheriting + + The desired inheriting rights of the file descriptor. + +### `__wasi_fd_filestat_get()` + +Return the attributes of an open file. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + The file descriptor to inspect. + +- [\_\_wasi\_filestat\_t](#filestat) \*buf + + The buffer where the file's attributes are + stored. + +### `__wasi_fd_filestat_set_size()` + +Adjust the size of an open file. If this increases the file's size, the extra +bytes are filled with zeros. + +Note: This is similar to `ftruncate` in POSIX. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + A file descriptor for the file to adjust. + +- [\_\_wasi\_filesize\_t](#filesize) st\_size + + The desired file size. + +### `__wasi_fd_filestat_set_times()` + +Adjust the timestamps of an open file or directory. + +Note: This is similar to `futimens` in POSIX. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + The file descriptor to operate on. + +- [\_\_wasi\_timestamp\_t](#timestamp) st\_atim + + The desired values of the data access timestamp. + +- [\_\_wasi\_timestamp\_t](#timestamp) st\_mtim + + The desired values of the data modification timestamp. + +- [\_\_wasi\_fstflags\_t](#fstflags) fst\_flags + + A bitmask indicating which timestamps to adjust. + +### `__wasi_fd_pread()` + +Read from a file descriptor, without using and updating the +file descriptor's offset. + +Note: This is similar to `preadv` in POSIX. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + The file descriptor from which to read data. + +- const [\_\_wasi\_iovec\_t](#iovec) \*iovs and size\_t iovs\_len + + List of scatter/gather vectors in which to store data. + +- [\_\_wasi\_filesize\_t](#filesize) offset + + The offset within the file at which to read. + +Outputs: + +- size\_t nread + + The number of bytes read. + +### `__wasi_fd_prestat_dir_name()` + +Return a description of the given preopened file descriptor. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + The file descriptor about which to retrieve information. + +- const char \*path and size\_t path\_len + + A buffer into which to write the preopened directory name. + +### `__wasi_fd_prestat_get()` + +Return a description of the given preopened file descriptor. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + The file descriptor about which to retrieve information. + +- [\_\_wasi\_prestat\_t](#prestat) \*buf + + The buffer where the description is stored. + +### `__wasi_fd_pwrite()` + +Write to a file descriptor, without using and updating the +file descriptor's offset. + +Note: This is similar to `pwritev` in POSIX. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + The file descriptor to which to write data. + +- const [\_\_wasi\_ciovec\_t](#ciovec) \*iovs and size\_t iovs\_len + + List of scatter/gather vectors from which to retrieve data. + +- [\_\_wasi\_filesize\_t](#filesize) offset + + The offset within the file at which to write. + +Outputs: + +- size\_t nwritten + + The number of bytes written. + +### `__wasi_fd_read()` + +Read from a file descriptor. + +Note: This is similar to `readv` in POSIX. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + The file descriptor from which to read data. + +- const [\_\_wasi\_iovec\_t](#iovec) \*iovs and size\_t iovs\_len + + List of scatter/gather vectors to which to store data. + +Outputs: + +- size\_t nread + + The number of bytes read. + +### `__wasi_fd_readdir()` + +Read directory entries from a directory. + +When successful, the contents of the output buffer consist of +a sequence of directory entries. Each directory entry consists +of a [`__wasi_dirent_t`](#dirent) object, followed by [`__wasi_dirent_t::d_namlen`](#dirent.d_namlen) bytes +holding the name of the directory entry. + +This function fills the output buffer as much as possible, +potentially truncating the last directory entry. This allows +the caller to grow its read buffer size in case it's too small +to fit a single large directory entry, or skip the oversized +directory entry. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + The directory from which to read the directory + entries. + +- void \*buf and size\_t buf\_len + + The buffer where directory entries are stored. + +- [\_\_wasi\_dircookie\_t](#dircookie) cookie + + The location within the directory to start + reading. + +Outputs: + +- size\_t bufused + + The number of bytes stored in the read buffer. + If less than the size of the read buffer, the + end of the directory has been reached. + +### `__wasi_fd_renumber()` + +Atomically replace a file descriptor by renumbering another +file descriptor. + +Due to the strong focus on thread safety, this environment +does not provide a mechanism to duplicate or renumber a file +descriptor to an arbitrary number, like dup2(). This would be +prone to race conditions, as an actual file descriptor with the +same number could be allocated by a different thread at the same +time. + +This function provides a way to atomically renumber file +descriptors, which would disappear if dup2() were to be +removed entirely. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) from + + The file descriptor to renumber. + +- [\_\_wasi\_fd\_t](#fd) to + + The file descriptor to overwrite. + +### `__wasi_fd_seek()` + +Move the offset of a file descriptor. + +Note: This is similar to `lseek` in POSIX. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + The file descriptor to operate on. + +- [\_\_wasi\_filedelta\_t](#filedelta) offset + + The number of bytes to move. + +- [\_\_wasi\_whence\_t](#whence) whence + + The base from which the offset is relative. + +Outputs: + +- [\_\_wasi\_filesize\_t](#filesize) newoffset + + The new offset of the file descriptor, + relative to the start of the file. + +### `__wasi_fd_sync()` + +Synchronize the data and metadata of a file to disk. + +Note: This is similar to `fsync` in POSIX. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + The file descriptor of the file containing the data + and metadata to synchronize to disk. + +### `__wasi_fd_tell()` + +Return the current offset of a file descriptor. + +Note: This is similar to `lseek(fd, 0, SEEK_CUR)` in POSIX. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + The file descriptor to inspect. + +Outputs: + +- [\_\_wasi\_filesize\_t](#filesize) offset + + The current offset of the file descriptor, relative to the start of the file. + +### `__wasi_fd_write()` + +Write to a file descriptor. + +Note: This is similar to `writev` in POSIX. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + The file descriptor to which to write data. + +- const [\_\_wasi\_ciovec\_t](#ciovec) \*iovs and size\_t iovs\_len + + List of scatter/gather vectors from which to retrieve data. + +Outputs: + +- size\_t nwritten + + The number of bytes written. + +### `__wasi_path_create_directory()` + +Create a directory. + +Note: This is similar to `mkdirat` in POSIX. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + The working directory at which the resolution of the path starts. + +- const char \*path and size\_t path\_len + + The path at which to create the directory. + +### `__wasi_path_filestat_get()` + +Return the attributes of a file or directory. + +Note: This is similar to `stat` in POSIX. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + The working directory at which the resolution of the path starts. + +- [\_\_wasi\_lookupflags\_t](#lookupflags) flags + + Flags determining the method of how the path is resolved. + +- const char \*path and size\_t path\_len + + The path of the file or directory to inspect. + +- [\_\_wasi\_filestat\_t](#filestat) \*buf + + The buffer where the file's attributes are stored. + +### `__wasi_path_filestat_set_times()` + +Adjust the timestamps of a file or directory. + +Note: This is similar to `utimensat` in POSIX. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + The working directory at which the resolution of the path starts. + +- [\_\_wasi\_lookupflags\_t](#lookupflags) flags + + Flags determining the method of how the path is resolved. + +- const char \*path and size\_t path\_len + + The path of the file or directory to operate on. + +- [\_\_wasi\_timestamp\_t](#timestamp) st\_atim + + The desired values of the data access timestamp. + +- [\_\_wasi\_timestamp\_t](#timestamp) st\_mtim + + The desired values of the data modification timestamp. + +- [\_\_wasi\_fstflags\_t](#fstflags) fst\_flags + + A bitmask indicating which timestamps to adjust. + +### `__wasi_path_link()` + +Create a hard link. + +Note: This is similar to `linkat` in POSIX. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) old\_fd + + The working directory at which the resolution of the old path starts. + +- [\_\_wasi\_lookupflags\_t](#lookupflags) old\_flags + + Flags determining the method of how the path is resolved. + +- const char \*old\_path and size\_t old\_path\_len + + The source path from which to link. + +- [\_\_wasi\_fd\_t](#fd) new\_fd + + The working directory at which the resolution of the new path starts. + +- const char \*new\_path and size\_t new\_path\_len + + The destination path at which to create the hard link. + +### `__wasi_path_open()` + +Open a file or directory. + +The returned file descriptor is not guaranteed to be the lowest-numbered +file descriptor not currently open; it is randomized to prevent +applications from depending on making assumptions about indexes, since +this is error-prone in multi-threaded contexts. The returned file +descriptor is guaranteed to be less than 231. + +Note: This is similar to `openat` in POSIX. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) dirfd + + The working directory at which the resolution of the path starts. + +- [\_\_wasi\_lookupflags\_t](#lookupflags) dirflags + + Flags determining the method of how the path is resolved. + +- const char \*path and size\_t path\_len + + The relative path of the file or directory to open, relative to + the [`dirfd`](#path_open.dirfd) directory. + +- [\_\_wasi\_oflags\_t](#oflags) o_flags + + The method by which to open the file. + +- [\_\_wasi\_rights\_t](#rights) fs\_rights\_base + + The initial base rights of the newly created file descriptor. The + implementation is allowed to return a file descriptor with fewer + rights than specified, if and only if those rights do not apply + to the type of file being opened. + + The *base* rights are rights that will apply to operations using + the file descriptor itself. + +- [\_\_wasi\_rights\_t](#rights) fs\_rights\_inheriting + + The initial inheriting rights of the newly created file descriptor. The + implementation is allowed to return a file descriptor with fewer + rights than specified, if and only if those rights do not apply + to the type of file being opened. + + The *inheriting* rights are rights that will apply to file descriptors derived + from the file descriptor itself. + +- [\_\_wasi\_fdflags\_t](#fdflags) fs\_flags + + The initial flags of the file descriptor. + +Outputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + The file descriptor of the file that has been + opened. + +### `__wasi_path_readlink()` + +Read the contents of a symbolic link. + +Note: This is similar to `readlinkat` in POSIX. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + The working directory at which the resolution of the path starts. + +- const char \*path and size\_t path\_len + + The path of the symbolic link from which to read. + +- char \*buf and size\_t buf\_len + + The buffer to which to write the contents of the symbolic link. + +Outputs: + +- size\_t bufused + + The number of bytes placed in the buffer. + +### `__wasi_path_remove_directory()` + +Remove a directory. + +Return [`__WASI_ENOTEMPTY`](#errno.notempty) if the directory is not empty. + +Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + The working directory at which the resolution of the path starts. + +- const char \*path and size\_t path\_len + + The path to a directory to remove. + +### `__wasi_path_rename()` + +Rename a file or directory. + +Note: This is similar to `renameat` in POSIX. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) old\_fd + + The working directory at which the resolution of the old path starts. + +- const char \*old\_path and size\_t old\_path\_len + + The source path of the file or directory to rename. + +- [\_\_wasi\_fd\_t](#fd) new\_fd + + The working directory at which the resolution of the new path starts. + +- const char \*new\_path and size\_t new\_path\_len + + The destination path to which to rename the file or directory. + +### `__wasi_path_symlink()` + +Create a symbolic link. + +Note: This is similar to `symlinkat` in POSIX. + +Inputs: + +- const char \*old\_path and size\_t old_path\_len + + The contents of the symbolic link. + +- [\_\_wasi\_fd\_t](#fd) fd + + The working directory at which the resolution of the path starts. + +- const char \*new\_path and size\_t new\_path\_len + + The destination path at which to create the symbolic link. + +### `__wasi_path_unlink_file()` + +Unlink a file. + +Return [`__WASI_EISDIR`](#errno.isdir) if the path refers to a directory. + +Note: This is similar to `unlinkat(fd, path, 0)` in POSIX. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) fd + + The working directory at which the resolution of the path starts. + +- const char \*path and size\_t path\_len + + The path to a file to unlink. + +### `__wasi_poll_oneoff()` + +Concurrently poll for the occurrence of a set of events. + +Inputs: + +- const [\_\_wasi\_subscription\_t](#subscription) \*in + + The events to which to subscribe. + +- [\_\_wasi\_event\_t](#event) \*out + + The events that have occurred. + +- size\_t nsubscriptions + + Both the number of subscriptions and events. + +Outputs: + +- size\_t nevents + + The number of events stored. + +### `__wasi_proc_exit()` + +Terminate the process normally. An exit code of 0 indicates successful +termination of the program. The meanings of other values is dependent on +the environment. + +Note: This is similar to `_Exit` in POSIX. + +Inputs: + +- [\_\_wasi\_exitcode\_t](#exitcode) rval + + The exit code returned by the process. + +Does not return. + +### `__wasi_proc_raise()` + +Send a signal to the process of the calling thread. + +Note: This is similar to `raise` in POSIX. + +Inputs: + +- [\_\_wasi\_signal\_t](#signal) sig + + The signal condition to trigger. + +### `__wasi_random_get()` + +Write high-quality random data into a buffer. + +This function blocks when the implementation is unable to immediately +provide sufficient high-quality random data. + +This function may execute slowly, so when large mounts of random +data are required, it's advisable to use this function to seed a +pseudo-random number generator, rather than to provide the +random data directly. + +Inputs: + +- void \*buf and size\_t buf\_len + + The buffer to fill with random data. + +### `__wasi_sched_yield()` + +Temporarily yield execution of the calling thread. + +Note: This is similar to `sched_yield` in POSIX. + +### `__wasi_sock_recv()` + +Receive a message from a socket. + +Note: This is similar to `recv` in POSIX, though it also supports reading +the data into multiple buffers in the manner of `readv`. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) sock + + The socket on which to receive data. + +- const [\_\_wasi\_iovec\_t](#iovec) \*ri\_data and size\_t ri\_data\_len + + List of scatter/gather vectors to which to store data. + +- [\_\_wasi\_riflags\_t](#riflags) ri\_flags + + Message flags. + +Outputs: + +- size\_t ro\_datalen + + Number of bytes stored in [`ri_data`](#sock_recv.ri_data). + +- [\_\_wasi\_roflags\_t](#roflags) ro\_flags + + Message flags. + +### `__wasi_sock_send()` + +Send a message on a socket. + +Note: This is similar to `send` in POSIX, though it also supports writing +the data from multiple buffers in the manner of `writev`. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) sock + + The socket on which to send data. + +- const [\_\_wasi\_ciovec\_t](#ciovec) \*si\_data and size\_t si\_data\_len + + List of scatter/gather vectors to which to retrieve data. + +- [\_\_wasi\_siflags\_t](#siflags) si\_flags + + Message flags. + +Outputs: + +- size\_t so\_datalen + + Number of bytes transmitted. + +### `__wasi_sock_shutdown()` + +Shut down socket send and receive channels. + +Note: This is similar to `shutdown` in POSIX. + +Inputs: + +- [\_\_wasi\_fd\_t](#fd) sock + + The socket on which to shutdown channels. + +- [\_\_wasi\_sdflags\_t](#sdflags) how + + Which channels on the socket to shut down. + +## Types + +### `__wasi_advice_t` (`uint8_t`) + +File or memory access pattern advisory information. + +Used by [`__wasi_fd_advise()`](#fd_advise). + +Possible values: + +- **`__WASI_ADVICE_DONTNEED`** + + The application expects that it will not access the + specified data in the near future. + +- **`__WASI_ADVICE_NOREUSE`** + + The application expects to access the specified data + once and then not reuse it thereafter. + +- **`__WASI_ADVICE_NORMAL`** + + The application has no advice to give on its behavior + with respect to the specified data. + +- **`__WASI_ADVICE_RANDOM`** + + The application expects to access the specified data + in a random order. + +- **`__WASI_ADVICE_SEQUENTIAL`** + + The application expects to access the specified data + sequentially from lower offsets to higher offsets. + +- **`__WASI_ADVICE_WILLNEED`** + + The application expects to access the specified data + in the near future. + +### `__wasi_ciovec_t` (`struct`) + +A region of memory for scatter/gather writes. + +Used by [`__wasi_fd_pwrite()`](#fd_pwrite), [`__wasi_fd_write()`](#fd_write), and [`__wasi_sock_send()`](#sock_send). + +Members: + +- const void \*buf and size\_t buf\_len + + The address and length of the buffer to be written. + +### `__wasi_clockid_t` (`uint32_t`) + +Identifiers for clocks. + +Used by [`__wasi_subscription_t`](#subscription), [`__wasi_clock_res_get()`](#clock_res_get), and [`__wasi_clock_time_get()`](#clock_time_get). + +Possible values: + +- **`__WASI_CLOCK_MONOTONIC`** + + The store-wide monotonic clock, which is defined as a + clock measuring real time, whose value cannot be + adjusted and which cannot have negative clock jumps. + + The epoch of this clock is undefined. The absolute + time value of this clock therefore has no meaning. + +- **`__WASI_CLOCK_PROCESS_CPUTIME_ID`** + + The CPU-time clock associated with the current + process. + +- **`__WASI_CLOCK_REALTIME`** + + The clock measuring real time. Time value + zero corresponds with 1970-01-01T00:00:00Z. + +- **`__WASI_CLOCK_THREAD_CPUTIME_ID`** + + The CPU-time clock associated with the current thread. + +### `__wasi_device_t` (`uint64_t`) + +Identifier for a device containing a file system. Can be used +in combination with [`__wasi_inode_t`](#inode) to uniquely identify a file or +directory in the filesystem. + +Used by [`__wasi_filestat_t`](#filestat). + +### `__wasi_dircookie_t` (`uint64_t`) + +A reference to the offset of a directory entry. + +Used by [`__wasi_dirent_t`](#dirent) and [`__wasi_fd_readdir()`](#fd_readdir). + +Special values: + +- **`__WASI_DIRCOOKIE_START`** + + Permanent reference to the first directory entry + within a directory. + +### `__wasi_dirent_t` (`struct`) + +A directory entry. + +Members: + +- [\_\_wasi\_dircookie\_t](#dircookie) d\_next + + The offset of the next directory entry stored in this + directory. + +- [\_\_wasi\_inode\_t](#inode) d\_ino + + The serial number of the file referred to by this + directory entry. + +- uint32\_t d\_namlen + + The length of the name of the directory entry. + +- [\_\_wasi\_filetype\_t](#filetype) d\_type + + The type of the file referred to by this directory + entry. + +### `__wasi_errno_t` (`uint16_t`) + +Error codes returned by functions. + +Not all of these error codes are returned by the functions +provided by this API; some are used in higher-level library layers, +and others are provided merely for alignment with POSIX. + +Used by [`__wasi_event_t`](#event). + +Possible values: + +- **`__WASI_ESUCCESS`** + + No error occurred. System call completed successfully. + +- **`__WASI_E2BIG`** + + Argument list too long. + +- **`__WASI_EACCES`** + + Permission denied. + +- **`__WASI_EADDRINUSE`** + + Address in use. + +- **`__WASI_EADDRNOTAVAIL`** + + Address not available. + +- **`__WASI_EAFNOSUPPORT`** + + Address family not supported. + +- **`__WASI_EAGAIN`** + + Resource unavailable, or operation would block. + +- **`__WASI_EALREADY`** + + Connection already in progress. + +- **`__WASI_EBADF`** + + Bad file descriptor. + +- **`__WASI_EBADMSG`** + + Bad message. + +- **`__WASI_EBUSY`** + + Device or resource busy. + +- **`__WASI_ECANCELED`** + + Operation canceled. + +- **`__WASI_ECHILD`** + + No child processes. + +- **`__WASI_ECONNABORTED`** + + Connection aborted. + +- **`__WASI_ECONNREFUSED`** + + Connection refused. + +- **`__WASI_ECONNRESET`** + + Connection reset. + +- **`__WASI_EDEADLK`** + + Resource deadlock would occur. + +- **`__WASI_EDESTADDRREQ`** + + Destination address required. + +- **`__WASI_EDOM`** + + Mathematics argument out of domain of function. + +- **`__WASI_EDQUOT`** + + Reserved. + +- **`__WASI_EEXIST`** + + File exists. + +- **`__WASI_EFAULT`** + + Bad address. + +- **`__WASI_EFBIG`** + + File too large. + +- **`__WASI_EHOSTUNREACH`** + + Host is unreachable. + +- **`__WASI_EIDRM`** + + Identifier removed. + +- **`__WASI_EILSEQ`** + + Illegal byte sequence. + +- **`__WASI_EINPROGRESS`** + + Operation in progress. + +- **`__WASI_EINTR`** + + Interrupted function. + +- **`__WASI_EINVAL`** + + Invalid argument. + +- **`__WASI_EIO`** + + I/O error. + +- **`__WASI_EISCONN`** + + Socket is connected. + +- **`__WASI_EISDIR`** + + Is a directory. + +- **`__WASI_ELOOP`** + + Too many levels of symbolic links. + +- **`__WASI_EMFILE`** + + File descriptor value too large. + +- **`__WASI_EMLINK`** + + Too many links. + +- **`__WASI_EMSGSIZE`** + + Message too large. + +- **`__WASI_EMULTIHOP`** + + Reserved. + +- **`__WASI_ENAMETOOLONG`** + + Filename too long. + +- **`__WASI_ENETDOWN`** + + Network is down. + +- **`__WASI_ENETRESET`** + + Connection aborted by network. + +- **`__WASI_ENETUNREACH`** + + Network unreachable. + +- **`__WASI_ENFILE`** + + Too many files open in system. + +- **`__WASI_ENOBUFS`** + + No buffer space available. + +- **`__WASI_ENODEV`** + + No such device. + +- **`__WASI_ENOENT`** + + No such file or directory. + +- **`__WASI_ENOEXEC`** + + Executable file format error. + +- **`__WASI_ENOLCK`** + + No locks available. + +- **`__WASI_ENOLINK`** + + Reserved. + +- **`__WASI_ENOMEM`** + + Not enough space. + +- **`__WASI_ENOMSG`** + + No message of the desired type. + +- **`__WASI_ENOPROTOOPT`** + + Protocol not available. + +- **`__WASI_ENOSPC`** + + No space left on device. + +- **`__WASI_ENOSYS`** + + Function not supported. + +- **`__WASI_ENOTCONN`** + + The socket is not connected. + +- **`__WASI_ENOTDIR`** + + Not a directory or a symbolic link to a directory. + +- **`__WASI_ENOTEMPTY`** + + Directory not empty. + +- **`__WASI_ENOTRECOVERABLE`** + + State not recoverable. + +- **`__WASI_ENOTSOCK`** + + Not a socket. + +- **`__WASI_ENOTSUP`** + + Not supported, or operation not supported on socket. + +- **`__WASI_ENOTTY`** + + Inappropriate I/O control operation. + +- **`__WASI_ENXIO`** + + No such device or address. + +- **`__WASI_EOVERFLOW`** + + Value too large to be stored in data type. + +- **`__WASI_EOWNERDEAD`** + + Previous owner died. + +- **`__WASI_EPERM`** + + Operation not permitted. + +- **`__WASI_EPIPE`** + + Broken pipe. + +- **`__WASI_EPROTO`** + + Protocol error. + +- **`__WASI_EPROTONOSUPPORT`** + + Protocol not supported. + +- **`__WASI_EPROTOTYPE`** + + Protocol wrong type for socket. + +- **`__WASI_ERANGE`** + + Result too large. + +- **`__WASI_EROFS`** + + Read-only file system. + +- **`__WASI_ESPIPE`** + + Invalid seek. + +- **`__WASI_ESRCH`** + + No such process. + +- **`__WASI_ESTALE`** + + Reserved. + +- **`__WASI_ETIMEDOUT`** + + Connection timed out. + +- **`__WASI_ETXTBSY`** + + Text file busy. + +- **`__WASI_EXDEV`** + + Cross-device link. + +- **`__WASI_ENOTCAPABLE`** + + Extension: Capabilities insufficient. + +### `__wasi_event_t` (`struct`) + +An event that occurred. + +Used by [`__wasi_poll_oneoff()`](#poll_oneoff). + +Members: + +- [\_\_wasi\_userdata\_t](#userdata) userdata + + User-provided value that got attached to + [`__wasi_subscription_t::userdata`](#subscription.userdata). + +- [\_\_wasi\_errno\_t](#errno) error + + If non-zero, an error that occurred while processing + the subscription request. + +- [\_\_wasi\_eventtype\_t](#eventtype) type + + The type of the event that occurred. + +- When `type` is [`__WASI_EVENTTYPE_FD_READ`](#eventtype.fd_read) or [`__WASI_EVENTTYPE_FD_WRITE`](#eventtype.fd_write): + + - **`u.fd_readwrite`** + + - [\_\_wasi\_filesize\_t](#filesize) nbytes + + The number of bytes available for reading or writing. + + - [\_\_wasi\_eventrwflags\_t](#eventrwflags) flags + + The state of the file descriptor. + +### `__wasi_eventrwflags_t` (`uint16_t` bitfield) + +The state of the file descriptor subscribed to with +[`__WASI_EVENTTYPE_FD_READ`](#eventtype.fd_read) or [`__WASI_EVENTTYPE_FD_WRITE`](#eventtype.fd_write). + +Used by [`__wasi_event_t`](#event). + +Possible values: + +- **`__WASI_EVENT_FD_READWRITE_HANGUP`** + + The peer of this socket has closed or disconnected. + +### `__wasi_eventtype_t` (`uint8_t`) + +Type of a subscription to an event or its occurrence. + +Used by [`__wasi_event_t`](#event) and [`__wasi_subscription_t`](#subscription). + +Possible values: + +- **`__WASI_EVENTTYPE_CLOCK`** + + The time value of clock [`__wasi_subscription_t::u.clock.clock_id`](#subscription.u.clock.clock_id) + has reached timestamp [`__wasi_subscription_t::u.clock.timeout`](#subscription.u.clock.timeout). + +- **`__WASI_EVENTTYPE_FD_READ`** + + File descriptor [`__wasi_subscription_t::u.fd_readwrite.fd`](#subscription.u.fd_readwrite.fd) has + data available for reading. This event always triggers + for regular files. + +- **`__WASI_EVENTTYPE_FD_WRITE`** + + File descriptor [`__wasi_subscription_t::u.fd_readwrite.fd`](#subscription.u.fd_readwrite.fd) has + capacity available for writing. This event always + triggers for regular files. + +### `__wasi_exitcode_t` (`uint32_t`) + +Exit code generated by a process when exiting. + +Used by [`__wasi_proc_exit()`](#proc_exit). + +### `__wasi_fd_t` (`uint32_t`) + +A file descriptor number. + +Used by many functions in this API. + +As in POSIX, three file descriptor numbers are provided to instances +on startup -- 0, 1, and 2, (a.k.a. `STDIN_FILENO`, `STDOUT_FILENO`, +and `STDERR_FILENO`). Starting at 3 follow a possibly-entry sequence +of preopened file descriptors provided by the host environment or the argument passed to the wasmtime command; +information about these may be obtained through +[`__wasi_fd_prestat_get()`](#fd_prestat_get). + +i.e., if we have called `wasmtime --dir=. ` we can specify `3` that will refer to the `--dir` value. + +Other than these, WASI implementations are not required to allocate +new file descriptors in ascending order. + +### `__wasi_fdflags_t` (`uint16_t` bitfield) + +File descriptor flags. + +Used by [`__wasi_fdstat_t`](#fdstat), [`__wasi_fd_fdstat_set_flags()`](#fd_fdstat_set_flags), and [`__wasi_path_open()`](#path_open). + +Possible values: + +- **`__WASI_FDFLAG_APPEND`** + + Append mode: Data written to the file is always + appended to the file's end. + +- **`__WASI_FDFLAG_DSYNC`** + + Write according to synchronized I/O data integrity + completion. Only the data stored in the file is + synchronized. + +- **`__WASI_FDFLAG_NONBLOCK`** + + Non-blocking mode. + +- **`__WASI_FDFLAG_RSYNC`** + + Synchronized read I/O operations. + +- **`__WASI_FDFLAG_SYNC`** + + Write according to synchronized I/O file integrity completion. + In addition to synchronizing the data stored in the file, the + implementation may also synchronously update the file's metadata. + +### `__wasi_fdstat_t` (`struct`) + +File descriptor attributes. + +Used by [`__wasi_fd_fdstat_get()`](#fd_fdstat_get). + +Members: + +- [\_\_wasi\_filetype\_t](#filetype) fs\_filetype + + File type. + +- [\_\_wasi\_fdflags\_t](#fdflags) fs\_flags + + File descriptor flags. + +- [\_\_wasi\_rights\_t](#rights) fs\_rights\_base + + Rights that apply to this file descriptor. + +- [\_\_wasi\_rights\_t](#rights) fs\_rights\_inheriting + + Maximum set of rights that may be installed on new + file descriptors that are created through this file + descriptor, e.g., through [`__wasi_path_open()`](#path_open). + +### `__wasi_filedelta_t` (`int64_t`) + +Relative offset within a file. + +Used by [`__wasi_fd_seek()`](#fd_seek). + +### `__wasi_filesize_t` (`uint64_t`) + +Non-negative file size or length of a region within a file. + +Used by [`__wasi_event_t`](#event), [`__wasi_filestat_t`](#filestat), [`__wasi_fd_advise()`](#fd_advise), [`__wasi_fd_allocate()`](#fd_allocate), [`__wasi_fd_filestat_set_size()`](#fd_filestat_set_size), [`__wasi_fd_pread()`](#fd_pread), [`__wasi_fd_pwrite()`](#fd_pwrite), [`__wasi_fd_seek()`](#fd_seek), and [`__wasi_fd_tell()`](#fd_tell). + +### `__wasi_filestat_t` (`struct`) + +File attributes. + +Used by [`__wasi_fd_filestat_get()`](#fd_filestat_get) and [`__wasi_path_filestat_get()`](#path_filestat_get). + +Members: + +- [\_\_wasi\_device\_t](#device) st\_dev + + Device ID of device containing the file. + +- [\_\_wasi\_inode\_t](#inode) st\_ino + + File serial number. + +- [\_\_wasi\_filetype\_t](#filetype) st\_filetype + + File type. + +- [\_\_wasi\_linkcount\_t](#linkcount) st\_nlink + + Number of hard links to the file. + +- [\_\_wasi\_filesize\_t](#filesize) st\_size + + For regular files, the file size in bytes. For + symbolic links, the length in bytes of the pathname + contained in the symbolic link. + +- [\_\_wasi\_timestamp\_t](#timestamp) st\_atim + + Last data access timestamp. + +- [\_\_wasi\_timestamp\_t](#timestamp) st\_mtim + + Last data modification timestamp. + +- [\_\_wasi\_timestamp\_t](#timestamp) st\_ctim + + Last file status change timestamp. + +### `__wasi_filetype_t` (`uint8_t`) + +The type of a file descriptor or file. + +Used by [`__wasi_dirent_t`](#dirent), [`__wasi_fdstat_t`](#fdstat), and [`__wasi_filestat_t`](#filestat). + +Possible values: + +- **`__WASI_FILETYPE_UNKNOWN`** + + The type of the file descriptor or file is unknown or + is different from any of the other types specified. + +- **`__WASI_FILETYPE_BLOCK_DEVICE`** + + The file descriptor or file refers to a block device + inode. + +- **`__WASI_FILETYPE_CHARACTER_DEVICE`** + + The file descriptor or file refers to a character + device inode. + +- **`__WASI_FILETYPE_DIRECTORY`** + + The file descriptor or file refers to a directory + inode. + +- **`__WASI_FILETYPE_REGULAR_FILE`** + + The file descriptor or file refers to a regular file + inode. + +- **`__WASI_FILETYPE_SOCKET_DGRAM`** + + The file descriptor or file refers to a datagram + socket. + +- **`__WASI_FILETYPE_SOCKET_STREAM`** + + The file descriptor or file refers to a byte-stream + socket. + +- **`__WASI_FILETYPE_SYMBOLIC_LINK`** + + The file refers to a symbolic link inode. + +### `__wasi_fstflags_t` (`uint16_t` bitfield) + +Which file time attributes to adjust. + +Used by [`__wasi_fd_filestat_set_times()`](#fd_filestat_set_times) and [`__wasi_path_filestat_set_times()`](#path_filestat_set_times). + +Possible values: + +- **`__WASI_FILESTAT_SET_ATIM`** + + Adjust the last data access timestamp to the value + stored in [`__wasi_filestat_t::st_atim`](#filestat.st_atim). + +- **`__WASI_FILESTAT_SET_ATIM_NOW`** + + Adjust the last data access timestamp to the time + of clock [`__WASI_CLOCK_REALTIME`](#clockid.realtime). + +- **`__WASI_FILESTAT_SET_MTIM`** + + Adjust the last data modification timestamp to the + value stored in [`__wasi_filestat_t::st_mtim`](#filestat.st_mtim). + +- **`__WASI_FILESTAT_SET_MTIM_NOW`** + + Adjust the last data modification timestamp to the + time of clock [`__WASI_CLOCK_REALTIME`](#clockid.realtime). + +### `__wasi_inode_t` (`uint64_t`) + +File serial number that is unique within its file system. + +Used by [`__wasi_dirent_t`](#dirent) and [`__wasi_filestat_t`](#filestat). + +### `__wasi_iovec_t` (`struct`) + +A region of memory for scatter/gather reads. + +Used by [`__wasi_fd_pread()`](#fd_pread), [`__wasi_fd_read()`](#fd_read), and [`__wasi_sock_recv()`](#sock_recv). + +Members: + +- void \*buf and size\_t buf\_len + + The address and length of the buffer to be filled. + +### `__wasi_linkcount_t` (`uint32_t`) + +Number of hard links to an inode. + +Used by [`__wasi_filestat_t`](#filestat). + +### `__wasi_lookupflags_t` (`uint32_t` bitfield) + +Flags determining the method of how paths are resolved. + +Used by [`__wasi_path_filestat_get()`](#path_filestat_get), [`__wasi_path_filestat_set_times()`](#path_filestat_set_times), [`__wasi_path_link()`](#path_link), and [`__wasi_path_open()`](#path_open). + +Possible values: + +- **`__WASI_LOOKUP_SYMLINK_FOLLOW`** + + As long as the resolved path corresponds to a symbolic + link, it is expanded. + +### `__wasi_oflags_t` (`uint16_t` bitfield) + +Open flags used by [`__wasi_path_open()`](#path_open). + +Used by [`__wasi_path_open()`](#path_open). + +Possible values: + +- **`__WASI_O_CREAT`** + + Create file if it does not exist. + +- **`__WASI_O_DIRECTORY`** + + Fail if not a directory. + +- **`__WASI_O_EXCL`** + + Fail if file already exists. + +- **`__WASI_O_TRUNC`** + + Truncate file to size 0. + +### `__wasi_preopentype_t` (`uint8_t`) + +Preopened resource type. + +Used by [`__wasi_prestat_t`](#prestat). + +Possible values: + +- **`__WASI_PREOPENTYPE_DIR`** + + Preopened directory. + +### `__wasi_prestat_t` (`struct`) + +Information about a preopened resource. + +Used by [`__wasi_fd_prestat_get()`](#fd_prestat_get). + +Members: + +- [\_\_wasi\_preopentype\_t](#preopentype) pr\_type + + The type of the preopened resource. + +- When `pr_type` is [`__WASI_PREOPENTYPE_DIR`](#preopentype.dir): + + - size\_t u.pr\_name\_len + + The length of the preopened directory name. + +### `__wasi_riflags_t` (`uint16_t` bitfield) + +Flags provided to [`__wasi_sock_recv()`](#sock_recv). + +Used by [`__wasi_sock_recv()`](#sock_recv). + +Possible values: + +- **`__WASI_SOCK_RECV_PEEK`** + + Returns the message without removing it from the + socket's receive queue. + +- **`__WASI_SOCK_RECV_WAITALL`** + + On byte-stream sockets, block until the full amount + of data can be returned. + +### `__wasi_rights_t` (`uint64_t` bitfield) + +File descriptor rights, determining which actions may be +performed. + +Used by [`__wasi_fdstat_t`](#fdstat), [`__wasi_fd_fdstat_set_rights()`](#fd_fdstat_set_rights), and [`__wasi_path_open()`](#path_open). + +Possible values: + +- **`__WASI_RIGHT_FD_DATASYNC`** + + The right to invoke [`__wasi_fd_datasync()`](#fd_datasync). + + If [`__WASI_RIGHT_PATH_OPEN`](#rights.path_open) is set, includes the right to + invoke [`__wasi_path_open()`](#path_open) with [`__WASI_FDFLAG_DSYNC`](#fdflags.dsync). + +- **`__WASI_RIGHT_FD_READ`** + + The right to invoke [`__wasi_fd_read()`](#fd_read) and [`__wasi_sock_recv()`](#sock_recv). + + If [`__WASI_RIGHT_FD_SEEK`](#rights.fd_seek) is set, includes the right to invoke + [`__wasi_fd_pread()`](#fd_pread). + +- **`__WASI_RIGHT_FD_SEEK`** + + The right to invoke [`__wasi_fd_seek()`](#fd_seek). This flag implies + [`__WASI_RIGHT_FD_TELL`](#rights.fd_tell). + +- **`__WASI_RIGHT_FD_FDSTAT_SET_FLAGS`** + + The right to invoke [`__wasi_fd_fdstat_set_flags()`](#fd_fdstat_set_flags). + +- **`__WASI_RIGHT_FD_SYNC`** + + The right to invoke [`__wasi_fd_sync()`](#fd_sync). + + If [`__WASI_RIGHT_PATH_OPEN`](#rights.path_open) is set, includes the right to + invoke [`__wasi_path_open()`](#path_open) with [`__WASI_FDFLAG_RSYNC`](#fdflags.rsync) and + [`__WASI_FDFLAG_DSYNC`](#fdflags.dsync). + +- **`__WASI_RIGHT_FD_TELL`** + + The right to invoke [`__wasi_fd_seek()`](#fd_seek) in such a way that the + file offset remains unaltered (i.e., [`__WASI_WHENCE_CUR`](#whence.cur) with + offset zero), or to invoke [`__wasi_fd_tell()`](#fd_tell). + +- **`__WASI_RIGHT_FD_WRITE`** + + The right to invoke [`__wasi_fd_write()`](#fd_write) and [`__wasi_sock_send()`](#sock_send). + + If [`__WASI_RIGHT_FD_SEEK`](#rights.fd_seek) is set, includes the right to + invoke [`__wasi_fd_pwrite()`](#fd_pwrite). + +- **`__WASI_RIGHT_FD_ADVISE`** + + The right to invoke [`__wasi_fd_advise()`](#fd_advise). + +- **`__WASI_RIGHT_FD_ALLOCATE`** + + The right to invoke [`__wasi_fd_allocate()`](#fd_allocate). + +- **`__WASI_RIGHT_PATH_CREATE_DIRECTORY`** + + The right to invoke [`__wasi_path_create_directory()`](#path_create_directory). + +- **`__WASI_RIGHT_PATH_CREATE_FILE`** + + If [`__WASI_RIGHT_PATH_OPEN`](#rights.path_open) is set, the right to invoke + [`__wasi_path_open()`](#path_open) with [`__WASI_O_CREAT`](#oflags.creat). + +- **`__WASI_RIGHT_PATH_LINK_SOURCE`** + + The right to invoke [`__wasi_path_link()`](#path_link) with the file + descriptor as the source directory. + +- **`__WASI_RIGHT_PATH_LINK_TARGET`** + + The right to invoke [`__wasi_path_link()`](#path_link) with the file + descriptor as the target directory. + +- **`__WASI_RIGHT_PATH_OPEN`** + + The right to invoke [`__wasi_path_open()`](#path_open). + +- **`__WASI_RIGHT_FD_READDIR`** + + The right to invoke [`__wasi_fd_readdir()`](#fd_readdir). + +- **`__WASI_RIGHT_PATH_READLINK`** + + The right to invoke [`__wasi_path_readlink()`](#path_readlink). + +- **`__WASI_RIGHT_PATH_RENAME_SOURCE`** + + The right to invoke [`__wasi_path_rename()`](#path_rename) with the file + descriptor as the source directory. + +- **`__WASI_RIGHT_PATH_RENAME_TARGET`** + + The right to invoke [`__wasi_path_rename()`](#path_rename) with the file + descriptor as the target directory. + +- **`__WASI_RIGHT_PATH_FILESTAT_GET`** + + The right to invoke [`__wasi_path_filestat_get()`](#path_filestat_get). + +- **`__WASI_RIGHT_PATH_FILESTAT_SET_SIZE`** + + The right to change a file's size (there is no `__wasi_path_filestat_set_size()`). + + If [`__WASI_RIGHT_PATH_OPEN`](#rights.path_open) is set, includes the right to + invoke [`__wasi_path_open()`](#path_open) with [`__WASI_O_TRUNC`](#oflags.trunc). + +- **`__WASI_RIGHT_PATH_FILESTAT_SET_TIMES`** + + The right to invoke [`__wasi_path_filestat_set_times()`](#path_filestat_set_times). + +- **`__WASI_RIGHT_FD_FILESTAT_GET`** + + The right to invoke [`__wasi_fd_filestat_get()`](#fd_filestat_get). + +- **`__WASI_RIGHT_FD_FILESTAT_SET_SIZE`** + + The right to invoke [`__wasi_fd_filestat_set_size()`](#fd_filestat_set_size). + +- **`__WASI_RIGHT_FD_FILESTAT_SET_TIMES`** + + The right to invoke [`__wasi_fd_filestat_set_times()`](#fd_filestat_set_times). + +- **`__WASI_RIGHT_PATH_SYMLINK`** + + The right to invoke [`__wasi_path_symlink()`](#path_symlink). + +- **`__WASI_RIGHT_PATH_UNLINK_FILE`** + + The right to invoke [`__wasi_path_unlink_file()`](#path_unlink_file). + +- **`__WASI_RIGHT_PATH_REMOVE_DIRECTORY`** + + The right to invoke [`__wasi_path_remove_directory()`](#path_remove_directory). + +- **`__WASI_RIGHT_POLL_FD_READWRITE`** + + If [`__WASI_RIGHT_FD_READ`](#rights.fd_read) is set, includes the right to + invoke [`__wasi_poll_oneoff()`](#poll_oneoff) to subscribe to [`__WASI_EVENTTYPE_FD_READ`](#eventtype.fd_read). + + If [`__WASI_RIGHT_FD_WRITE`](#rights.fd_write) is set, includes the right to + invoke [`__wasi_poll_oneoff()`](#poll_oneoff) to subscribe to [`__WASI_EVENTTYPE_FD_WRITE`](#eventtype.fd_write). + +- **`__WASI_RIGHT_SOCK_SHUTDOWN`** + + The right to invoke [`__wasi_sock_shutdown()`](#sock_shutdown). + +### `__wasi_roflags_t` (`uint16_t` bitfield) + +Flags returned by [`__wasi_sock_recv()`](#sock_recv). + +Used by [`__wasi_sock_recv()`](#sock_recv). + +Possible values: + +- **`__WASI_SOCK_RECV_DATA_TRUNCATED`** + + Returned by [`__wasi_sock_recv()`](#sock_recv): Message data has been + truncated. + +### `__wasi_sdflags_t` (`uint8_t` bitfield) + +Which channels on a socket to shut down. + +Used by [`__wasi_sock_shutdown()`](#sock_shutdown). + +Possible values: + +- **`__WASI_SHUT_RD`** + + Disables further receive operations. + +- **`__WASI_SHUT_WR`** + + Disables further send operations. + +### `__wasi_siflags_t` (`uint16_t` bitfield) + +Flags provided to [`__wasi_sock_send()`](#sock_send). As there are currently no flags +defined, it must be set to zero. + +Used by [`__wasi_sock_send()`](#sock_send). + +### `__wasi_signal_t` (`uint8_t`) + +Signal condition. + +Used by [`__wasi_proc_raise()`](#proc_raise). + +Possible values: + +- **`__WASI_SIGABRT`** + + Process abort signal. + + Action: Terminates the process. + +- **`__WASI_SIGALRM`** + + Alarm clock. + + Action: Terminates the process. + +- **`__WASI_SIGBUS`** + + Access to an undefined portion of a memory object. + + Action: Terminates the process. + +- **`__WASI_SIGCHLD`** + + Child process terminated, stopped, or continued. + + Action: Ignored. + +- **`__WASI_SIGCONT`** + + Continue executing, if stopped. + + Action: Continues executing, if stopped. + +- **`__WASI_SIGFPE`** + + Erroneous arithmetic operation. + + Action: Terminates the process. + +- **`__WASI_SIGHUP`** + + Hangup. + + Action: Terminates the process. + +- **`__WASI_SIGILL`** + + Illegal instruction. + + Action: Terminates the process. + +- **`__WASI_SIGINT`** + + Terminate interrupt signal. + + Action: Terminates the process. + +- **`__WASI_SIGKILL`** + + Kill. + + Action: Terminates the process. + +- **`__WASI_SIGPIPE`** + + Write on a pipe with no one to read it. + + Action: Ignored. + +- **`__WASI_SIGQUIT`** + + Terminal quit signal. + + Action: Terminates the process. + +- **`__WASI_SIGSEGV`** + + Invalid memory reference. + + Action: Terminates the process. + +- **`__WASI_SIGSTOP`** + + Stop executing. + + Action: Stops executing. + +- **`__WASI_SIGSYS`** + + Bad system call. + + Action: Terminates the process. + +- **`__WASI_SIGTERM`** + + Termination signal. + + Action: Terminates the process. + +- **`__WASI_SIGTRAP`** + + Trace/breakpoint trap. + + Action: Terminates the process. + +- **`__WASI_SIGTSTP`** + + Terminal stop signal. + + Action: Stops executing. + +- **`__WASI_SIGTTIN`** + + Background process attempting read. + + Action: Stops executing. + +- **`__WASI_SIGTTOU`** + + Background process attempting write. + + Action: Stops executing. + +- **`__WASI_SIGURG`** + + High bandwidth data is available at a socket. + + Action: Ignored. + +- **`__WASI_SIGUSR1`** + + User-defined signal 1. + + Action: Terminates the process. + +- **`__WASI_SIGUSR2`** + + User-defined signal 2. + + Action: Terminates the process. + +- **`__WASI_SIGVTALRM`** + + Virtual timer expired. + + Action: Terminates the process. + +- **`__WASI_SIGXCPU`** + + CPU time limit exceeded. + + Action: Terminates the process. + +- **`__WASI_SIGXFSZ`** + + File size limit exceeded. + + Action: Terminates the process. + +### `__wasi_subclockflags_t` (`uint16_t` bitfield) + +Flags determining how to interpret the timestamp provided in +[`__wasi_subscription_t::u.clock.timeout`](#subscription.u.clock.timeout). + +Used by [`__wasi_subscription_t`](#subscription). + +Possible values: + +- **`__WASI_SUBSCRIPTION_CLOCK_ABSTIME`** + + If set, treat the timestamp provided in + [`__wasi_subscription_t::u.clock.timeout`](#subscription.u.clock.timeout) as an absolute timestamp + of clock [`__wasi_subscription_t::u.clock.clock_id`](#subscription.u.clock.clock_id). + + If clear, treat the timestamp provided in + [`__wasi_subscription_t::u.clock.timeout`](#subscription.u.clock.timeout) relative to the current + time value of clock [`__wasi_subscription_t::u.clock.clock_id`](#subscription.u.clock.clock_id). + +### `__wasi_subscription_t` (`struct`) + +Subscription to an event. + +Used by [`__wasi_poll_oneoff()`](#poll_oneoff). + +Members: + +- [\_\_wasi\_userdata\_t](#userdata) userdata + + User-provided value that is attached to the subscription in the + implementation and returned through + [`__wasi_event_t::userdata`](#event.userdata). + +- [\_\_wasi\_eventtype\_t](#eventtype) type + + The type of the event to which to subscribe. + +- When `type` is [`__WASI_EVENTTYPE_CLOCK`](#eventtype.u.clock): + + - **`u.clock`** + + - [\_\_wasi\_userdata\_t](#userdata) identifier + + The user-defined unique identifier of the clock. + + - [\_\_wasi\_clockid\_t](#clockid) clock\_id + + The clock against which to compare the timestamp. + + - [\_\_wasi\_timestamp\_t](#timestamp) timeout + + The absolute or relative timestamp. + + - [\_\_wasi\_timestamp\_t](#timestamp) precision + + The amount of time that the implementation may wait additionally + to coalesce with other events. + + - [\_\_wasi\_subclockflags\_t](#subclockflags) flags + + Flags specifying whether the timeout is absolute or relative. + +- When `type` is [`__WASI_EVENTTYPE_FD_READ`](#eventtype.fd_read) or [`__WASI_EVENTTYPE_FD_WRITE`](#eventtype.fd_write): + + - **`u.fd_readwrite`** + + - [\_\_wasi\_fd\_t](#fd) fd + + The file descriptor on which to wait for it to become ready + for reading or writing. + +### `__wasi_timestamp_t` (`uint64_t`) + +Timestamp in nanoseconds. + +Used by [`__wasi_filestat_t`](#filestat), [`__wasi_subscription_t`](#subscription), [`__wasi_clock_res_get()`](#clock_res_get), [`__wasi_clock_time_get()`](#clock_time_get), [`__wasi_fd_filestat_set_times()`](#fd_filestat_set_times), and [`__wasi_path_filestat_set_times()`](#path_filestat_set_times). + +### `__wasi_userdata_t` (`uint64_t`) + +User-provided value that may be attached to objects that is +retained when extracted from the implementation. + +Used by [`__wasi_event_t`](#event) and [`__wasi_subscription_t`](#subscription). + +### `__wasi_whence_t` (`uint8_t`) + +The position relative to which to set the offset of the file descriptor. + +Used by [`__wasi_fd_seek()`](#fd_seek). + +Possible values: + +- **`__WASI_WHENCE_CUR`** + + Seek relative to current position. + +- **`__WASI_WHENCE_END`** + + Seek relative to end-of-file. + +- **`__WASI_WHENCE_SET`** + + Seek relative to start-of-file. + diff --git a/docs/WASI-background.md b/docs/WASI-background.md new file mode 100644 index 0000000000..9ba1d6a7f4 --- /dev/null +++ b/docs/WASI-background.md @@ -0,0 +1,179 @@ +One of the biggest challenges in WebAssembly is figuring out what it's +supposed to be. + +## A brief tangent on some related history + +The LLVM WebAssembly backend has gone down countless paths that it has +ended up abandoning. One of the early questions was whether we should use +an existing object file format, such as ELF, or design a new format. + +Using an existing format is very appealing. We'd be able to use existing +tools, and be familiar to developers. It would even make porting some +kinds of applications easier. And existing formats carry with them +decades of "lessons learned" from many people in many settings, building, +running, and porting real-world applications. + +The actual WebAssembly format that gets handed to platforms to run is +its own format, but there'd be ways to make things work. To reuse existing +linkers, we could have a post-processing tool which translates from the +linker's existing output format into a runnable WebAssembly module. We +actually made a fair amount of progress toward building this. + +But then, using ELF for example, we'd need to create a custom segment +type (in the `PT_LOPROC`-`PT_HIPROC` range) instead of the standard +`PT_LOAD` for loading code, because WebAssembly functions aren't actually +loaded into the program address space. And same for the `PT_LOAD` for the +data too, because especially once WebAssembly supports threads, memory +initialization will need to +[work differently](https://github.com/WebAssembly/bulk-memory-operations/blob/master/proposals/bulk-memory-operations/Overview.md#design). +And we could omit the `PT_GNU_STACK`, because WebAssembly's stack can't +be executable. And maybe we could omit `PT_PHDR` because unless +we replicate the segment headers in data, they won't actually be +accessible in memory. And so on. + +And while in theory everything can be done within the nominal ELF +standard, in practice we'd have to make major changes to existing ELF +tools to support this way of using ELF, which would defeat many of the +advantages we were hoping to get. And we'd still be stuck with a custom +post-processing step. And it'd be harder to optimize the system to +take advantage of the unique features of WebAssembly, because everything +would have to work within this external set of constraints. + +So while the LLVM WebAssembly backend started out trying to use ELF, we +eventually decided to back out of that and design a +[new format](https://github.com/WebAssembly/tool-conventions/blob/master/Linking.md). + +## Now let's talk APIs + +It's apparent to anyone who's looked under the covers at Emscripten's interface +between WebAssembly and the outside world that the current system is particular +to the way Emscripten currently works, and not well suited for broader adoption. +This is especially true as interest grows in running WebAssembly outside +of browsers and outside of JS VMs. + +It's been obvious since WebAssembly was just getting started that it'd eventually +want some kind of "system call"-like API, which could be standardized, and +implemented in any general-purpose WebAssembly VM. + +And while there are many existing systems we could model this after, [POSIX] +stands out, as being a vendor-neutral standard with considerable momentum. Many +people, including us, have been assuming that WebAssembly would eventually +have some kind of POSIX API. Some people have even started experimenting with +what +[this](https://github.com/WAVM/Wavix/) +[might](https://github.com/jfbastien/musl) +[look](https://github.com/golang/go/blob/e5489cfc12a99f25331831055a79750bfa227943/misc/wasm/wasm_exec.js) +[like](https://github.com/emscripten-core/emscripten/blob/incoming/src/library_syscall.js). + +But while a lot of things map fairly well, some things are less clear. One of +the big questions is how to deal with the concept of a "process". POSIX's IPC +mechanisms are designed around process, and in fact, the term "IPC" itself +has "process" baked into it. The way we even think about what "IPC" means +bakes in in understandings about what processes are and what communication +between them looks like. + +Pipes, Unix-domain sockets, POSIX shared memory, signals, files with `fcntl` +`F_SETLK`/`F_GETLK`-style locking (which is process-associated), are are tied +to processes. But what *is* a process, when we're talking about WebAssembly? + +## Stick a fork in it + +Suppose we say that a WebAssembly instance is a "process", for the purposes +of the POSIX API. This initially seems to work out well, but it leaves us +with several holes to fill. Foremost is `fork`. `fork` is one of the pillars +of Unix, but it's difficult to implement outside of a full Unix-style OS. We +probably *can* make it work in all the places we want to run WebAssembly, but +do we want to? It'd add a bunch of complexity, inefficiency, subtle behavioral +differences, or realistically, a combination of all three. + +Ok, so maybe we can encourage applications to use `posix_spawn` instead. And +some already do, but in doing so we do lose some of the value of POSIX's +momentum. And even with `posix_spawn`, many applications will explicitly do +things like `waitpid` on the resulting PID. We can make this work too, but +we should also take a moment and step back to think about IPC in general. + +In WebAssembly, instances can synchronously call each other, and it can be +very efficient. This is not something that typical processes can do. Arguably, +a lot of what we now think of as "IPC" is just working around the inability +of processes to have calls between each other. And, WebAssembly instances will +be able to import each others' memories and tables, and eventually even pass +around slices to their memories. In WebAssembly circles we don't even tend to +think of these as IPC mechanisms, because the process metaphor just doesn't +fit very well here. We're going to want applications to use these mechanisms, +because they're efficient and take advantage of the platform, rather than +using traditional Unix-style IPC which will often entail emulation and +inefficiencies. + +Of course, there will always be a role for aiding porting of existing +applications. Libraries that emulate various details of Unix semantics are +valuable. But we can consider them tools for solving certain practical +problems, rather than the primary interfaces of the system, because they +miss out on some of the platform's fundamental features. + +## Mm-Mm Mmap + +Some of the fundamental assumptions of `mmap` are that there exists a +relatively large virtual address space, and that unmapped pages don't +occupy actual memory. The former doesn't tend to hold in WebAssembly, +where linear address spaces tend to be only as big as necessary. + +For the latter, would it be possible to make a WebAssembly engine capable +of unmapping pages in the middle of a linear memory region, and releasing +the resources? Sure. Is this a programming technique we want WebAssembly +programs doing in general, requiring all VMs to implement this? +Probably not. + +What's emerging is a sense that what we want is a core set of +APIs that can be implemented very broadly, and then optional API +modules that VMs can opt into supporting if it makes sense for them. +And with this mindset, `mmap` feels like it belongs in one of these +optional sets, rather than in the core. + +(although note that even for the use case of reading files quickly, +`mmap` +[isn't always better than just reading into a buffer](https://blog.burntsushi.net/ripgrep/). + +## A WebAssembly port of Debian? + +This is a thought-experiment. Debian is ported to numerous hardware +architectures. WebAssembly in some settings is presented as a hardware +architecture. Would it make sense to port the Debian userspace to +WebAssembly? What would this look like? What would it be useful for? + +It would be kind of cool to have a WebAssembly-powered Unix shell +environment or even a graphical desktop environment running inside a +browser. But would it be *really* cool? Significantly more cool than, +say, an SSH or VNC session to an instance in the cloud? Because to do +much with it, you'll want a filesystem, a network stack, and so on, +and there's only so much that browsers will let you do. + +To be sure, it certainly would be cool. But there's a tendency in +some circles to think of something like Debian as the natural end goal +in a system API and toolchain for WebAssembly. We feel this tendency +too ourselves. But it's never really been clear how it's supposed to +work. + +The insight here is that we can split the design space, rather than +trying to solve everything at once. We can have a core set of APIs +that will be enough for most applications, but that doesn't try to +support all of Debian userland. This will make implementations more +portable, flexible, testable, and robust than if we tried to make +every implementation support everything, or come up with custom +subsets. + +As mentioned above, there is room for additional optional APIs to be +added beyond the core WASI set. And there's absolutely a place for +tools and libraries that features that aren't in the standard +platform. So people interested in working on a Debian port can still +have a path forward, but we don't need to let this become a focus for +the core WASI design. + +## A picture emerges + +While much of what's written here seems relatively obvious in +retrospect, this clarity is relatively new. We're now seeing many of the +ideas which have been swirling around, some as old as WebAssembly +itself, come together into a cohesive overall plan, which makes this +an exciting time. + +[POSIX]: http://pubs.opengroup.org/onlinepubs/9699919799/ diff --git a/docs/WASI-capabilities.md b/docs/WASI-capabilities.md new file mode 100644 index 0000000000..7ed73a42ff --- /dev/null +++ b/docs/WASI-capabilities.md @@ -0,0 +1,81 @@ +# Additional background on Capabilities + +## Unforgeable references + +One of the key words that describes capabilities is *unforgeable*. + +A pointer in C is forgeable, because untrusted code could cast an integer +to a pointer, thus *forging* access to whatever that pointer value points +to. + +MVP WebAssembly doesn't have unforgeable references, but what we can do instead +is just use integer values which are indices into a table that's held outside +the reach of untrusted code. The indices themselves are forgeable, but +ultimately the table is the thing which holds the actual capabilities, and +its elements are unforgeable. There's no way to gain access to a new resource +by making up a new index. + +When the reference-types proposal lands, references will be unforgeable, and +will likely subsume the current integer-based APIs, at the WASI API layer. + +## Static vs dynamic capabilities + +There are two levels of capabilities that we can describe: static and dynamic. + +The static capabilities of a wasm module are its imports. These essentially +declare the set of "rights" the module itself will be able to request. +An important caveat though is that this doesn't consider capabilities which +may be passed into an instance at runtime. + +The dynamic capabilities of a wasm module are a set of boolean values +associated with a file descriptor, indicating individual "rights". This +includes things like the right to read, or to write, using a given file +descriptor. + +## Filesystem rules + +It happens that integer indices representing capabilities is same thing that +POSIX does, except that POSIX calls these indices *file descriptors*. + +One difference though is that POSIX normally allows processes to request +a file descriptor for any file in the entire filesystem hierarchy, which is +granted based on whatever security policies are in place. This doesn't +violate the capability model, but it doesn't take full advantage of it. + +CloudABI, Fuchsia, and other capability-oriented systems prefer to take +advantage of the hierarchical nature of the filesystem and require untrusted +code to have a capability for a directory in order to access things inside +that directory. + +This way, you can launch untrusted code, and at runtime give it access to +specific directories, without having to set permissions in the filesystem or +in per-application or per-user configuration settings. + +See [this tutorial](WASI-tutorial.md) for an example of how this can look +in practice. + +## Berkeley socket rules + +Sockets aren't naturally hierarchical though, so we'll need to decide what +capabilities look like. This is an area that isn't yet implemented. + +In CloudABI, users launch programs with the sockets they need already +created. That's potentially a starting point, which might be enough for +simple cases. + +We also anticipate an eventual extension to that, where we create a capability +that represents a set of possible sockets that can be created. A set +might be described by ranges of permitted ports, ranges of permitted +addresses, or sets of permitted protocols. In this case the actual socket +wouldn't be created until the application actually requests it. + +## Other info + +CloudABI's intro to capability-based OS security provides additional background info: + +https://github.com/NuxiNL/cloudabi#capability-based-security + + +The Fuchsia project has a blog post on the topic of capability-based OS security: + +https://fuchsia.dev/fuchsia-src/concepts/filesystems/dotdot diff --git a/docs/WASI-documents.md b/docs/WASI-documents.md new file mode 100644 index 0000000000..d1abcb6311 --- /dev/null +++ b/docs/WASI-documents.md @@ -0,0 +1,23 @@ +# WASI Document Guide + +To get started using WASI, see [the intro document](WASI-intro.md) and +[the tutorial](WASI-tutorial.md). + +For more detail on what WASI is, see [the overview](WASI-overview.md). + +For specifics on the API, see the [API documentation](https://github.com/bytecodealliance/wasmtime/blob/master/docs/WASI-api.md). +Additionally, a C header file describing the WASI API is +[here](https://github.com/CraneStation/wasi-libc/blob/master/libc-bottom-half/headers/public/wasi/api.h). + +The WASI libc repository is [wasi-libc](https://github.com/CraneStation/wasi-libc/). + +For some discussion of capability-based design, see the [Capabilities document](WASI-capabilities.md). + +For some discussion of WASI's design inspiration, see the [Background document](WASI-background.md). + +For background on some of the design decisions in WASI, see [the rationale](WASI-rationale.md). + +For some ideas of things that we may want to change about WASI in the +short term, see the [possible changes](WASI-some-possible-changes.md) document. +For longer-term ideas, see the [possible future features](WASI-possible-future-features.md) +document. diff --git a/docs/WASI-intro.md b/docs/WASI-intro.md new file mode 100644 index 0000000000..6f4d485845 --- /dev/null +++ b/docs/WASI-intro.md @@ -0,0 +1,62 @@ +# Welcome to WASI! + +WASI stands for WebAssembly System Interface. It's an API designed by +the [Wasmtime] project that provides access to several operating-system-like +features, including files and filesystems, Berkeley sockets, clocks, and +random numbers, that we'll be proposing for standardization. + +It's designed to be independent of browsers, so it doesn't depend on +Web APIs or JS, and isn't limited by the need to be compatible with JS. +And it has integrated capability-based security, so it extends +WebAssembly's characteristic sandboxing to include I/O. + +See the [WASI Overview](WASI-overview.md) for more detailed background +information, and the [WASI Tutorial](WASI-tutorial.md) for a walkthrough +showing how various pieces fit together. + +Note that everything here is a prototype, and while a lot of stuff works, +there are numerous missing features and some rough edges. For example, +networking support is incomplete. + +## How can I write programs that use WASI? + +The two toolchains that currently work well are the Rust toolchain and +a specially packaged C and C++ toolchain. Of course, we hope other +toolchains will be able to implement WASI as well! + +### Rust + +To install a WASI-enabled Rust toolchain, see the [online section of the +guide](https://bytecodealliance.github.io/wasmtime/wasm-rust.html) + +### C/C++ + +To install a WASI-enabled C/C++ toolchain, see the [online section of the +guide](https://bytecodealliance.github.io/wasmtime/wasm-c.html) + +## How can I run programs that use WASI? + +Currently the options are [Wasmtime] and the [browser polyfill], though we +intend WASI to be implementable in many wasm VMs. + +[Wasmtime]: https://github.com/bytecodealliance/wasmtime +[browser polyfill]: https://wasi.dev/polyfill/ + +### Wasmtime + +[Wasmtime] is a non-Web WebAssembly engine which is part of the +[CraneStation project](https://github.com/CraneStation/). To build +it, download the code and build with `cargo build --release`. It can +run WASI-using wasm programs by simply running `wasmtime foo.wasm`, +or `cargo run --bin wasmtime foo.wasm`. + +### The browser polyfill + +The polyfill is online [here](https://wasi.dev/polyfill/). + +The source is [here](https://github.com/bytecodealliance/wasmtime/tree/master/crates/wasi-c/js-polyfill). + +## Where can I learn more? + +Beyond the [WASI Overview](WASI-overview.md), take a look at the +various [WASI documents](WASI-documents.md). diff --git a/docs/WASI-overview.md b/docs/WASI-overview.md new file mode 100644 index 0000000000..46952f8273 --- /dev/null +++ b/docs/WASI-overview.md @@ -0,0 +1,163 @@ +# WASI: WebAssembly System Interface + +WebAssembly System Interface, or WASI, is a new family of API's being +designed by the [Wasmtime] project to propose as a standard engine-independent +non-Web system-oriented API for WebAssembly. Initially, the focus is on +WASI Core, an API module that covers files, networking, and a few other +things. Additional modules are expected to be added in the future. + +WebAssembly is designed to run well on the Web, however it's +[not limited to the Web](https://github.com/WebAssembly/design/blob/master/NonWeb.md). +The core WebAssembly language is independent of its surrounding +environment, and WebAssembly interacts with the outside world +exclusively through APIs. On the Web, it naturally uses the +existing Web APIs provided by browsers. However outside of +browsers, there's currently no standard set of APIs that +WebAssembly programs can be written to. This makes it difficult to +create truly portable non-Web WebAssembly programs. + +WASI is an initiative to fill this gap, with a clean set of APIs +which can be implemented on multiple platforms by multiple engines, +and which don't depend on browser functionality (although they +still can run in browsers; see below). + +## Capability-Oriented + +The design follows +[CloudABI](https://cloudabi.org/)'s +(and in turn +[Capsicum](https://www.cl.cam.ac.uk/research/security/capsicum/))'s concept of +[capability-based security](https://en.wikipedia.org/wiki/Capability-based_security), +which fits well into WebAssembly's sandbox model. Files, +directories, network sockets, and other resources are identified +by UNIX-like file descriptors, which are indices into external +tables whose elements represent capabilities. Similar to how core +WebAssembly provides no ability to access the outside world without +calling imported functions, WASI APIs provide no ability to access +the outside world without an associated capability. + +For example, instead of a typical +[open](http://pubs.opengroup.org/onlinepubs/009695399/functions/open.html) +system call, WASI provides an +[openat](https://linux.die.net/man/2/openat)-like +system call, requiring the calling process to have a file +descriptor for a directory that contains the file, representing the +capability to open files within that directory. (These ideas are +common in capability-based systems.) + +However, the WASI libc implementation still does provide an +implementation of open, by taking the approach of +[libpreopen](https://github.com/musec/libpreopen). +Programs may be granted capabilities for directories on launch, and +the library maintains a mapping from their filesystem path to the +file descriptor indices representing the associated capabilities. +When a program calls open, they look up the file name in the map, +and automatically supply the appropriate directory capability. It +also means WASI doesn't require the use of CloudABI's `program_main` +construct. This eases porting of existing applications without +compromising the underlying capability model. See the diagram below +for how libpreopen fits into the overall software architecture. + +WASI also automatically provides file descriptors for standard +input and output, and WASI libc provides a normal `printf`. In +general, WASI is aiming to support a fairly full-featured libc +implementation, with the current implementation work being based on +[musl](http://www.musl-libc.org/). + +## Portable System Interface for WebAssembly + +WASI is being designed from the ground up for WebAssembly, with +sandboxing, portability, and API tidiness in mind, making natural +use of WebAssembly features such as i64, import functions with +descriptive names and typed arguments, and aiming to avoid being +tied to a particular implementation. + +We'll often call functions in these APIs "syscalls", because they +serve an analogous purpose to system calls in native executables. +However, they're just functions that are provided by the +surrounding environment that can do I/O on behalf of the program. + +WASI is starting with a basic POSIX-like set of syscall functions, +though adapted to suit the needs of WebAssembly, such as in +excluding functions such as fork and exec which aren't easily +implementable in some of the places people want to run WebAssembly, +and such as in adopting a capabilities-oriented design. + +And, as WebAssembly grows support for +[host bindings](https://github.com/webassembly/host-bindings) +and related features, capabilities can evolve to being represented +as opaque, unforgeable +[reference typed values](https://github.com/WebAssembly/reference-types), +which can allow for finer-grained control over capabilities, and +make the API more accessible beyond the C-like languages that +POSIX-style APIs are typically aimed at. + +## WASI Software Architecture + +To facilitate use of the WASI API, a libc +implementation called WASI libc is being developed, which presents +a relatively normal musl-based libc interface, implemented on top +of a libpreopen-like layer and a system call wrapper layer (derived +from the "bottom half" of +[cloudlibc](https://github.com/NuxiNL/cloudlibc)). +The system call wrapper layer makes calls to the actual WASI +implementation, which may map these calls to whatever the +surrounding environment provides, whether it's native OS resources, +JS runtime resources, or something else entirely. + +[This libc is part of a "sysroot"](https://github.com/WebAssembly/reference-sysroot), +which is a directory containing compiled libraries and C/C++ header +files providing standard library and related facilities laid out in +a standard way to allow compilers to use it directly. + +With the [LLVM 8.0](http://llvm.org/) +release, the WebAssembly backend is now officially stable, but LLVM +itself doesn't provide a libc - a standard C library, which you +need to build anything with clang. This is what the WASI-enabled +sysroot provides, so the combination of clang in LLVM 8.0 and the +new WASI-enabled sysroot provides usable Rust and C compilation +environments that can produce wasm modules that can be run in +[Wasmtime] with WASI support, in browsers with the WASI polyfill, +and in the future other engines as well. + +![WASI software architecture diagram](wasi-software-architecture.png "WASI software architecture diagram") + +## Future Evolution + +The first version of WASI is relatively simple, small, and +POSIX-like in order to make it easy for implementers to prototype +it and port existing code to it, making it a good way to start +building momentum and allow us to start getting feedback based on +experience. + +Future versions will change based on experience +and feedback with the first version, and add features to address +new use cases. They may also see significant architectural +changes. One possibility is that this API could +evolve into something like +[Fuchsia](https://en.wikipedia.org/wiki/Google_Fuchsia)'s +low-level APIs, which are more complex and abstract, though also +more capable. + +We also expect that whatever WASI evolves into in the future, it +should be possible to implement this initial API as a library +on top. + +## Can WASI apps run on the Web? + +Yes! We have a polyfill which implements WASI and runs in browsers. +At the WebAssembly level, WASI is just a set of callable functions that +can be imported by a .wasm module, and these imports can be implemented +in a variety of ways, including by a JavaScript polyfill library running +within browsers. + +And in the future, it's possible that +[builtin modules](https://github.com/tc39/ecma262/issues/395) +could take these ideas even further allowing easier and tighter +integration between .wasm modules importing WASI and the Web. + +## Work in Progress + +WASI is currently experimental. Feedback is welcome! + +[Wasmtime]: https://github.com/bytecodealliance/wasmtime diff --git a/docs/WASI-possible-future-features.md b/docs/WASI-possible-future-features.md new file mode 100644 index 0000000000..77f676b0eb --- /dev/null +++ b/docs/WASI-possible-future-features.md @@ -0,0 +1,49 @@ +# Possible Future Features + +These are some features we're interested in, but don't have yet, and which will +require some amount of design work. + +## File Locking + +POSIX's answer is `fcntl` with `F_SETLK`/`F_GETLK`/etc., which provide advisory +record locking. Unfortunately, these locks are associated with processes, which +means that if two parts of a program independently open a file and try to lock +it, if they're in the same process, they automatically share the lock. + +Other locking APIs exist on various platforms, but none is widely standardized. + +POSIX `F_SETLK`-style locking is used by SQLite. + +## File change monitoring + +POSIX has no performant way to monitor many files or directories for changes. + +Many popular operating systems have system-specific APIs to do this though, so +it'd be desirable to come up with a portable API to provide access to this +functionality. + +## Scalable event-based I/O + +POSIX's `select` and `poll` have the property that each time they're called, +the implementation has to scan through all the file descriptors to report if any +of them has I/O ready, which is inefficient when there are large numbers of +open files or sockets. + +Many popular operating systems have system-specific APIs that provide +alternative ways to monitor large numbers of I/O streams though, so it'd be +desirable to come up with a portable API to provide access to this +functionality. + +## Crash recovery + +POSIX doesn't have clear guidance on what applications can expect their +data will look like if the system crashes or the storage device is otherwise +taken offline abruptly. + +We have `fsync` and `fdatasync`, but even these have been a topic of +[much discussion]. + +[much discussion]: https://wiki.postgresql.org/wiki/Fsync_Errors + +Also, currently WASI's docs don't make any guarantees about things like +`path_rename` being atomic. diff --git a/docs/WASI-proposed-CG-subgroup-charter.md b/docs/WASI-proposed-CG-subgroup-charter.md new file mode 100644 index 0000000000..b732934750 --- /dev/null +++ b/docs/WASI-proposed-CG-subgroup-charter.md @@ -0,0 +1,55 @@ +# WebAssembly System Interface Subgroup Charter + +The System Interface Subgroup is a sub-organization of the +[WebAssembly Community Group](https://www.w3.org/community/webassembly/) of the W3C. +As such, it is intended that its charter align with that of the CG. In particular, +the sections of the [CG charter](https://webassembly.github.io/cg-charter/) relating to +[Community and Business Group Process](https://webassembly.github.io/cg-charter/#process), +[Contribution Mechanics](https://webassembly.github.io/cg-charter/#contrib), +[Transparency](https://webassembly.github.io/cg-charter/#transparency), and +[Decision Process](https://webassembly.github.io/cg-charter/#decision) also apply to the Subgroup. + +## Goals + +The mission of this sugbroup is to provide a forum for pre-standardization +collaboration on a system interface API for WebAssembly programs. + +## Scope + +The Subgroup will consider topics related to system interface APIs, including: + +- APIs for host filesystems, network stacks, and other resources. +- APIs for graphics, audio, input devices +- APIs for encryption, format conversion, and other transformations + (particularly where hardware accelleration may be available on some plaforms) + + +## Deliverables + +### Specifications +The Subgroup may produce several kinds of specification-related work output: +- Creation of new specifications in standards bodies or working +groups (e.g. Wasm WG or TC39) +- Creation of new specifications outside of standards bodies +(e.g. similar to the LLVM object file format documentation in Wasm tool conventions) + +### Non-normative reports +The Subgroup may produce non-normative material such as requirements +documents, recommendations, and use cases. + +### Software +The Subgroup may produce software related to Wasm system interface APIs (either +as standalone libraries, tooling, or integration of interface-related +functionality in existing CG software such as Binaryen or WABT). Capabilities may +include: +- Libraries implementing external standard APIs in terms of WebAssembly + System Interface APIs +- Tools for producing code that uses WebAssembly System Interface APIs +- Tools for implementing WebAssembly APIs +- Tools for debugging programs using WebAssembly System Interface APIs + +## Amendments to this Charter and Chair Selection + +This charter may be amended, and Subgroup Chairs may be selected by vote of the full +WebAssembly Community Group. + diff --git a/docs/WASI-rationale.md b/docs/WASI-rationale.md new file mode 100644 index 0000000000..d1c8e09385 --- /dev/null +++ b/docs/WASI-rationale.md @@ -0,0 +1,160 @@ +## Why not a more traditional set of POSIX-like syscalls? + +In related work, the LLVM wasm backend started out trying to use ELF object +files for wasm, to be as conventional as possible. But wasm doesn't fit into +ELF in some very fundamental ways. Code isn't in the address space, callers +have to know their callee's exact signatures, imports and exports don't have +ELF semantics, function pointers require tables to be populated, index 0 is +valid in some contexts where it isn't in ELF, and so on. It ultimately got +to the point where the work we were considering doing to *emulate* ELF +interfaces to make existing tools happy looked like more than the work that +would be required to just build new tools. + +The analogy isn't perfect, but there are some parallels to what we're now +figuring out about system calls. Many people, including us, had initially +assumed that at least some parts of the wasm ecosystem would eventually +standardize on a basic map of POSIX-like or Linux-like system calls into wasm +imports. However, this turns out to be more complex than it initially seems. + +One of WebAssembly's unique attributes is the ability to run sandboxed +without relying on OS process boundaries. Requiring a 1-to-1 correspondence +between wasm instances and heavyweight OS processes would take away this key +advantage for many use cases. Fork/exec are the obvious example of an API +that's difficult to implement well if you don't have POSIX-style processes, +but a lot of other things in POSIX are tied to processes too. So it isn't +a simple matter to take POSIX, or even a simple subset of it, to WebAssembly. + +We should note that Spectre concerns are relevant here, though for now we'll +just observe that actual security depends on the details of implementations +and use cases, and it's not necessarily a show-stopper. + +Another area where WebAssembly differs from traditional POSIX-like platforms +is in its Capability-oriented approach to security. WebAssembly core has no +ability to address the outside world, except through interacting with +imports/exports. And when reference types are added, they'll be able to +represent very fine-grained and dynamic capabilities. + +A capability-oriented system interface fits naturally into WebAssembly's +existing sandbox model, by extending the simple story that a wasm module +can't do anything until given capabilities. There are ways to sandbox +traditional OS filesystem APIs too, but in a multiple-implementation +ecosystem where the methods for setting up path filtering will likely +differ between implementations, designing the platform around capabilities +will make it easier for people to consistently configure the capabilities +available to wasm modules. + +This is where we see WASI heading. + +## Why not non-blocking? + +This is an open question. We're using blocking APIs for now because that's +*by far* the simpler way to get the overall system to a usable state, on +both the wasm runtime side and the toolchain side. But one can make an +argument that non-blocking APIs would have various advantages, so we +look forward to discussing this topic with the WebAssembly CG subgroup +once it's set up. + +## Why not async? + +We have some ideas about how the current API could be extended to be async. +In particular, we can imagine making a distinction between WebAssembly +programs which are *Commands* and those which we'll call *Reactors*. +Commands have a `main` function which is called once, and when `main` +exits, the program is complete. Reactors have a setup function, but +once that completes, the instance remains live and is called from callbacks. +In a Reactor, there's an event loop which lives outside of the nominal +program. + +With this distinction, we may be able to say things like: + - In a Reactor, WASI APIs are available, but all functions have an + additional argument, which specifies a function to call as a continuation + once the I/O completes. This way, we can use the same conceptual APIs, + but adapt them to run in an callback-based async environment. + - In a Command, WASI APIs don't have callback parameters. Whether or not + they're non-blocking is an open question (see the previous question). + +Reactors might then be able to run in browsers on the main thread, +while Commands in browsers might be limited to running in Workers. + +## Why no mmap and friends? + +True mmap support is something that could be added in the future, +though it is expected to require integration with the core language. +See "Finer-grained control over memory" in WebAssembly's +[Future Features] document for an overview. + +Ignoring the many non-standard mmap extensions out there, +the core mmap behavior is not portable in several respects, even +across POSIX-style systems. See +[LevelDB's decision to stop using mmap], for one example in +practice, and search for the word "unspecified" in the +[POSIX mmap spec] for some others. + +And, some features of mmap can lead to userspace triggering +signals. Accessing memory beyond the end of the file, including in +the case where someone else changes the size of the file, leads to a +`SIGBUS` on POSIX-style systems. Protection modes other than +`PROT_READ|PROT_WRITE` can produce `SIGSEGV`. While some VMs are +prepared to catch such signals transparently, this is a burdensome +requirement for others. + +Another issue is that while WASI is a synchronous I/O API today, +this design may change in the future. `mmap` can create situations +where doing a load can entail blocking I/O, which can make it +harder to characterize all the places where blocking I/O may occur. + +And lastly, WebAssembly linear memory doesn't support the semantics +of mapping and unmapping pages. Most WebAssembly VMs would not +easily be able to support freeing the memory of a page in the middle +of a linear memory region, for example. + +To make things easier for people porting programs that just use +mmap to read and write files in a simple way, WASI libc includes a +minimal userspace emulation of `mmap` and `munmap`. + +[POSIX mmap spec]: http://pubs.opengroup.org/onlinepubs/7908799/xsh/mmap.html +[LevelDB's decision to stop using mmap]: https://groups.google.com/forum/#!topic/leveldb/C5Hh__JfdrQ +[Future Features]: https://webassembly.org/docs/future-features/. + +## Why no UNIX-domain sockets? + +UNIX-domain sockets can communicate three things: + - bytes + - file descriptors + - user credentials + +The concept of "users" doesn't fit within WASI, because many implementations +won't be multi-user in that way. + +It can be useful to pass file descriptor between wasm instances, however in +wasm this can be done by passing them as arguments in plain function calls, +which is much simpler and quicker. And, in WASI implementations where file +descriptors don't correspond to an underlying Unix file descriptor concept, +it's not feasible to do this if the other side of the socket isn't a +cooperating WebAssembly engine. + +We may eventually want to introduce a concept of a WASI-domain socket, for +bidirectional byte-oriented local communication. + +## Why no dup? + +The main use cases for `dup` are setting up the classic Unix dance of setting +up file descriptors in advance of performing a `fork`. Since WASI has no `fork`, +these don't apply. + +And avoiding `dup` for now avoids committing to the POSIX concepts of +descriptors being distinct from file descriptions in subtle ways. + +## Why are `path_remove_directory` and `path_unlink_file` separate syscalls? + +In POSIX, there's a single `unlinkat` function, which has a flag word, +and with the `AT_REMOVEDIR` flag one can specify whether one wishes to +remove a file or a directory. However, there really are two distinct +functions being performed here, and having one system call that can +select between two different behaviors doesn't simplify the actual API +compared to just having two system calls. + +More importantly, in WASI, system call imports represent a static list +of the capabilities requested by a wasm module. Therefore, WASI prefers +each system call to do just one thing, so that it's clear what a wasm +module that imports it might be able to do with it. diff --git a/docs/WASI-some-possible-changes.md b/docs/WASI-some-possible-changes.md new file mode 100644 index 0000000000..84cde2a084 --- /dev/null +++ b/docs/WASI-some-possible-changes.md @@ -0,0 +1,114 @@ +# Possible changes + +The following are a list of relatively straightforward changes +to WASI core that should be considered. + +## Split file/networking/random/clock from args/environ/exit. + +Currently everything is mixed together in one big "core" module. But we can +split them out to allow minimal configurations that don't support this style +of files and networking. + +## Move higher-level and unused errno codes out of the core API. + +The core API currently defines errno codes such as `EDOM` which are +not used for anything. POSIX requires them to be defined, however +that can be done in the higher-level libraries, rather than in the +WASI core API itself. + +## Detecting EOF from read/recv explicitly. + +POSIX's `read` returns 0 if and only if it reaches the end of a file or stream. + +Say you have a read buffer of 1024 bytes, and are reading a file that happens +to be 7 bytes long. The first `read` call will return 7, but unless you happen +to know how big the file is supposed to be, you can't distinguish between +that being all there is, and `read` getting interrupted and returning less +data than you requested. + +Many applications today do an extra `read` when they encounter the end of a +file, to ensure that they get a `read` that returns 0 bytes read, to confirm +that they've reached the end of the file. If `read` instead had a way to +indicate that it had reached the end, this extra call wouldn't be necessary. + +And, `read` on a socket is almost equivalent to `recv` with no flags -- except for +one surprising special case: on a datagram socket, if there's a zero-length +datagram, `read` can't consume it, while `recv` can. This is because `read` can't +indicate that it successfully read 0 bytes, because it has overloaded the meaning +of 0 to indicate eof-of-file. + +So, it would be tidier from multiple perspectives if `read` could indicate +that it had reached the end of a file or stream, independently of how many +bytes it has read. + +## Merging read and recv + +These are very similar, and differ only in subtle ways. It'd make the API +easier to understand if they were unified. + +## Trap instead of returning EFAULT + +POSIX system calls return EFAULT when given invalid pointers, however from an +application perspective, it'd be more natural for them to just segfault. + +## More detailed capability error reporting + +Replace `__WASI_ENOTCAPABLE` with error codes that indicate *which* capabilities +were required but not present. + +## Split `__wasi_path_open` into `__wasi_path_open_file` and `__wasi_path_open_directory`? + +We could also split `__WASI_RIGHT_PATH_OPEN` into file vs directory, +(obviating `__WASI_O_DIRECTORY`). + +## Fix the y2556 bug + +In some places, timestamps are measured in nanoseconds since the UNIX epoch, +so our calculations indicate a 64-bit counter will overflow on +Sunday, July 21, 2554, at 11:34:33 pm UTC. + +These timestamps aren't used in that many places, so it wouldn't cost that +much to widen these timestamps. We can either just extend the current type to +128 bits (two i64's in wasm) or move to a `timespec`-like `tv_sec`/`tv_nsec` +pair. + +## Remove `fd_allocate`? + +Darwin doesn't implement `posix_fallocate` (similar to `fd_allocate`), despite it being +in POSIX since 2001. So we don't currently know any way to implement `fd_allocate` +on Darwin that's safe from race conditions. Should we remove it from the API? + +## Redesign `fstflags_t` + +The relationship between `*_SET_*TIM` and `*_SET_*TIM_NOW` is non-obvious. +We should look at this again. + +## readdir + +Truncating entries that don't fit into a buffer may be error-prone. Should +we redesign how directory reading works? + +## symlinks + +Symlinks are fairly UNIX-specific. Should we remove `__wasi_path_symlink` +and `__wasi_path_readlink`? + +Also, symlink resolution doesn't benefit from libpreopen-style path +translation. Should we move symlink resolution into the libpreopen layer +and do it entirely in "userspace"? + +## Remove the `path_len` argument from `__wasi_fd_prestat_dir_name` + +The buffer should be sized to the length returned from `__wasi_fd_prestat_get`, +so it's not necessary to pass the length back into the runtime. + +## Add a `__wasi_path_filestat_set_size` function? + +Along with libc/libpreopen support, this would enable implementing the +POSIX `truncate` function. + +## errno values returned by `path_open` + +We should specify the errno value returned when `path_open` is told +to open a directory and `__WASI_LOOKUP_SYMLINK_FOLLOW` isn't set, and +the path refers to a symbolic link. diff --git a/docs/WASI-tutorial.md b/docs/WASI-tutorial.md new file mode 100644 index 0000000000..2b6249b242 --- /dev/null +++ b/docs/WASI-tutorial.md @@ -0,0 +1,325 @@ +# WASI tutorial +We'll split the tutorial into two parts: in the first part we'll walk through +compiling C and Rust programs to WASI and executing the compiled WebAssembly module +using `wasmtime` runtime. In the second part we will discuss the compilation of a +simpler WebAssembly program written using the WebAssembly text format, and executing +this using the `wasmtime` runtime. + +- [WASI tutorial](#wasi-tutorial) + - [Running common languages with WASI](#running-common-languages-with-wasi) + - [Compiling to WASI](#compiling-to-wasi) + - [From C](#from-c) + - [From Rust](#from-rust) + - [Executing in `wasmtime` runtime](#executing-in-wasmtime-runtime) + - [Web assembly text example](#web-assembly-text-example) + +## Running common languages with WASI +## Compiling to WASI +#### From C +Let's start with a simple C program which performs a file copy, which will +show to compile and run programs, as well as perform simple sandbox +configuration. The C code here uses standard POSIX APIs, and doesn't have +any knowledge of WASI, WebAssembly, or sandboxing. + +```c +#include +#include +#include +#include +#include +#include + +int main(int argc, char **argv) { + ssize_t n, m; + char buf[BUFSIZ]; + + if (argc != 3) { + fprintf(stderr, "usage: %s \n", argv[0]); + exit(1); + } + + int in = open(argv[1], O_RDONLY); + if (in < 0) { + fprintf(stderr, "error opening input %s: %s\n", argv[1], strerror(errno)); + exit(1); + } + + int out = open(argv[2], O_WRONLY | O_CREAT, 0660); + if (out < 0) { + fprintf(stderr, "error opening output %s: %s\n", argv[2], strerror(errno)); + exit(1); + } + + while ((n = read(in, buf, BUFSIZ)) > 0) { + char *ptr = buf; + while (n > 0) { + m = write(out, ptr, (size_t)n); + if (m < 0) { + fprintf(stderr, "write error: %s\n", strerror(errno)); + exit(1); + } + n -= m; + ptr += m; + } + } + + if (n < 0) { + fprintf(stderr, "read error: %s\n", strerror(errno)); + exit(1); + } + + return EXIT_SUCCESS; +} +``` + +We'll put this source in a file called `demo.c`. + +The [wasi-sdk](https://github.com/CraneStation/wasi-sdk/releases) provides a clang +which is configured to target WASI and use the WASI sysroot by default if you put the extracted tree into `/`, so we can +compile our program like so: + +``` +$ clang demo.c -o demo.wasm +``` + +If you would want to extract it elsewhere, you can specify the sysroot directory like so + +``` +$ clang demo.c --sysroot -o demo.wasm +``` + +If you're using the wasi-sdk, the sysroot directory is located in `opt/wasi-sdk/share/sysroot/` on Linux and mac. + +This is just regular clang, configured to use +a WebAssembly target and sysroot. The output name specified with the "-o" +flag can be anything you want, and *does not* need to contain the `.wasm` extension. +In fact, the output of clang here is a standard WebAssembly module: + +``` +$ file demo.wasm +demo.wasm: WebAssembly (wasm) binary module version 0x1 (MVP) +``` + + +#### From Rust +The same effect can be achieved with Rust. Firstly, go ahead and create a new +binary crate: + +``` +$ cargo new --bin demo +``` + +You can also clone the Rust code with the crate preset for you from +[here](https://github.com/kubkon/rust-wasi-tutorial). + +Now, let's port the C program defined in [From C](#from-c) section to Rust: + +```rust +use std::env; +use std::fs; +use std::io::{Read, Write}; + +fn process(input_fname: &str, output_fname: &str) -> Result<(), String> { + let mut input_file = + fs::File::open(input_fname).map_err(|err| format!("error opening input: {}", err))?; + let mut contents = Vec::new(); + input_file + .read_to_end(&mut contents) + .map_err(|err| format!("read error: {}", err))?; + + let mut output_file = fs::File::create(output_fname) + .map_err(|err| format!("error opening output '{}': {}", output_fname, err))?; + output_file + .write_all(&contents) + .map_err(|err| format!("write error: {}", err)) +} + +fn main() { + let args: Vec = env::args().collect(); + let program = args[0].clone(); + + if args.len() < 3 { + eprintln!("{} ", program); + return; + } + + if let Err(err) = process(&args[1], &args[2]) { + eprintln!("{}", err) + } +} +``` + +Let's put this source in the main file of our crate `src/main.rs`. + +In order to build it, we first need to install a WASI-enabled Rust toolchain: + +``` +$ rustup target add wasm32-wasi +$ cargo build --target wasm32-wasi +``` + +We should now have the WebAssembly module created in `target/wasm32-wasi/debug`: + +``` +$ file target/wasm32-wasi/debug/demo.wasm +demo.wasm: WebAssembly (wasm) binary module version 0x1 (MVP) +``` + +## Executing in `wasmtime` runtime +The resultant WebAssembly module `demo.wasm` compiled either from C or Rust is simply +a single file containing a self-contained wasm module, that doesn't require +any supporting JS code. + +We can execute it with `wasmtime` directly, like so: + +``` +$ wasmtime demo.wasm +usage: demo.wasm +``` + +Ok, this program needs some command-line arguments. So let's give it some: + +``` +$ echo hello world > test.txt +$ wasmtime demo.wasm test.txt /tmp/somewhere.txt +error opening input test.txt: Capabilities insufficient +``` + +Aha, now we're seeing the sandboxing in action. This program is attempting to +access a file by the name of `test.txt`, however it hasn't been given the +capability to do so. + +So let's give it capabilities to access files in the requisite directories: + +``` +$ wasmtime --dir=. --dir=/tmp demo.wasm test.txt /tmp/somewhere.txt +$ cat /tmp/somewhere.txt +hello world +``` + +Now our program runs as expected! + +What's going on under the covers? The `--dir=` option instructs `wasmtime` +to *preopen* a directory, and make it available to the program as a capability +which can be used to open files inside that directory. Now when the program +calls the C/Rust `open` function, passing it either an absolute or relative path, +the WASI libc transparently translates that path into a path that's relative to +one of the given preopened directories, if possible (using a technique based +on [libpreopen](https://github.com/musec/libpreopen)). This way, we can have a +simple capability-oriented model at the system call level, while portable +application code doesn't have to do anything special. + +As a brief aside, note that we used the path `.` above to grant the program +access to the current directory. This is needed because the mapping from +paths to associated capabilities is performed by libc, so it's part of the +WebAssembly program, and we don't expose the actual current working +directory to the WebAssembly program. So providing a full path doesn't work: + +``` +$ wasmtime --dir=$PWD --dir=/tmp demo.wasm test.txt /tmp/somewhere.txt +$ cat /tmp/somewhere.txt +error opening input test.txt: Capabilities insufficient +``` + +So, we always have to use `.` to refer to the current directory. + +Speaking of `.`, what about `..`? Does that give programs a way to break +out of the sandbox? Let's see: + +``` +$ wasmtime --dir=. --dir=/tmp demo.wasm test.txt /tmp/../etc/passwd +error opening output /tmp/../etc/passwd: Capabilities insufficient +``` + +The sandbox says no. And note that this is the capabilities system saying no +here ("Capabilities insufficient"), rather than Unix access controls +("Permission denied"). Even if the user running `wasmtime` had write access to +`/etc/passwd`, WASI programs don't have the capability to access files outside +of the directories they've been granted. This is true when resolving symbolic +links as well. + +`wasmtime` also has the ability to remap directories, with the `--mapdir` +command-line option: + +``` +$ wasmtime --dir=. --mapdir=/tmp::/var/tmp demo.wasm test.txt /tmp/somewhere.txt +$ cat /var/tmp/somewhere.txt +hello world +``` + +This maps the name `/tmp` within the WebAssembly program to `/var/tmp` in the +host filesystem. So the WebAssembly program itself never sees the `/var/tmp` path, +but that's where the output file goes. + +See [here](WASI-capabilities.md) for more information on the capability-based +security model. + +The capability model is very powerful, and what's shown here is just the beginning. +In the future, we'll be exposing much more functionality, including finer-grained +capabilities, capabilities for network ports, and the ability for applications to +explicitly request capabilities. + +## Web assembly text example + +In this example we will look at compiling the WebAssembly text format into wasm, and +running the compiled WebAssembly module using the `wasmtime` runtime. This example +makes use of WASI's `fd_write` implementation to write `hello world` to stdout. + +First, create a new `demo.wat` file: + +```wat +(module + ;; Import the required fd_write WASI function which will write the given io vectors to stdout + ;; The function signature for fd_write is: + ;; (File Descriptor, *iovs, iovs_len, nwritten) -> Returns number of bytes written + (import "wasi_unstable" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32))) + + (memory 1) + (export "memory" (memory 0)) + + ;; Write 'hello world\n' to memory at an offset of 8 bytes + ;; Note the trailing newline which is required for the text to appear + (data (i32.const 8) "hello world\n") + + (func $main (export "_start") + ;; Creating a new io vector within linear memory + (i32.store (i32.const 0) (i32.const 8)) ;; iov.iov_base - This is a pointer to the start of the 'hello world\n' string + (i32.store (i32.const 4) (i32.const 12)) ;; iov.iov_len - The length of the 'hello world\n' string + + (call $fd_write + (i32.const 1) ;; file_descriptor - 1 for stdout + (i32.const 0) ;; *iovs - The pointer to the iov array, which is stored at memory location 0 + (i32.const 1) ;; iovs_len - We're printing 1 string stored in an iov - so one. + (i32.const 20) ;; nwritten - A place in memory to store the number of bytes written + ) + drop ;; Discard the number of bytes written from the top of the stack + ) +) +``` + +`wasmtime` can directly execute `.wat` files: + +``` +$ wasmtime demo.wat +hello world +``` + +Or, you can compile the `.wat` WebAssembly text format into the wasm binary format +yourself using the [wabt] command line tools: + +``` +$ wat2wasm demo.wat +``` + +The created `.wasm` file can now be executed with `wasmtime` directly like so: + +``` +$ wasmtime demo.wasm +hello world +``` + +To run this example within the browser, simply upload the compiled `.wasm` file to +the [WASI browser polyfill]. + +[wabt]: https://github.com/WebAssembly/wabt +[WASI browser polyfill]: https://wasi.dev/polyfill/ diff --git a/docs/book.toml b/docs/book.toml new file mode 100644 index 0000000000..9fa32dfb63 --- /dev/null +++ b/docs/book.toml @@ -0,0 +1,5 @@ +[book] +authors = ["The Wasmtime Project Developers"] +multilingual = false +src = "." +title = "Wasmtime" diff --git a/docs/cli-cache.md b/docs/cli-cache.md new file mode 100644 index 0000000000..f8c6622971 --- /dev/null +++ b/docs/cli-cache.md @@ -0,0 +1,278 @@ +# Cache Configuration of `wasmtime` + +The configuration file uses the [toml] format. +You can create a configuration file at the default location with: +```sh +$ wasmtime config new +``` +It will print the location regardless of the success. +Please refer to the `--help` message for using a custom location. + +All settings, except `enabled`, are **optional**. +If the setting is not specified, the **default** value is used. +***Thus, if you don't know what values to use, don't specify them.*** +The default values might be tuned in the future. + +Wasmtime assumes all the options are in the `cache` section. + +Example config: +```toml +[cache] +enabled = true +directory = "/nfs-share/wasmtime-cache/" +cleanup-interval = "30m" +files-total-size-soft-limit = "1Gi" +``` + +Please refer to the [cache system] section to learn how it works. + +If you think some default value should be tuned, some new settings +should be introduced or some behavior should be changed, you are +welcome to discuss it and contribute to [the Wasmtime repository]. + +[the Wasmtime repository]: https://github.com/bytecodealliance/wasmtime + +Setting `enabled` +----------------- +- **type**: boolean +- **format**: `true | false` +- **default**: `true` + +Specifies whether the cache system is used or not. + +This field is *mandatory*. +The default value is used when configuration file is not specified +and none exists at the default location. + +[`enabled`]: #setting-enabled + +Setting `directory` +----------------- +- **type**: string (path) +- **default**: look up `cache_dir` in [directories] crate + +Specifies where the cache directory is. Must be an absolute path. + +[`directory`]: #setting-directory + +Setting `worker-event-queue-size` +----------------- +- **type**: string (SI prefix) +- **format**: `"{integer}(K | M | G | T | P)?"` +- **default**: `"16"` + +Size of [cache worker] event queue. +If the queue is full, incoming cache usage events will be dropped. + +[`worker-event-queue-size`]: #setting-worker-event-queue-size + +Setting `baseline-compression-level` +------------------ +- **type**: integer +- **default**: `3`, the default zstd compression level + +Compression level used when a new cache file is being written by the [cache system]. +Wasmtime uses [zstd] compression. + +[`baseline-compression-level`]: #setting-baseline-compression-level + +Setting `optimized-compression-level` +------------------ +- **type**: integer +- **default**: `20` + +Compression level used when the [cache worker] decides to recompress a cache file. +Wasmtime uses [zstd] compression. + +[`optimized-compression-level`]: #setting-optimized-compression-level + +Setting `optimized-compression-usage-counter-threshold` +------------------ +- **type**: string (SI prefix) +- **format**: `"{integer}(K | M | G | T | P)?"` +- **default**: `"256"` + +One of the conditions for the [cache worker] to recompress a cache file +is to have usage count of the file exceeding this threshold. + +[`optimized-compression-usage-counter-threshold`]: #setting-optimized-compression-usage-counter-threshold + +Setting `cleanup-interval` +------------------ +- **type**: string (duration) +- **format**: `"{integer}(s | m | h | d)"` +- **default**: `"1h"` + +When the [cache worker] is notified about a cache file being updated by the [cache system] +and this interval has already passed since last cleaning up, +the worker will attempt a new cleanup. + +Please also refer to [`allowed-clock-drift-for-files-from-future`]. + +[`cleanup-interval`]: #setting-cleanup-interval + +Setting `optimizing-compression-task-timeout` +------------------ +- **type**: string (duration) +- **format**: `"{integer}(s | m | h | d)"` +- **default**: `"30m"` + +When the [cache worker] decides to recompress a cache file, it makes sure that +no other worker has started the task for this file within the last +[`optimizing-compression-task-timeout`] interval. +If some worker has started working on it, other workers are skipping this task. + +Please also refer to the [`allowed-clock-drift-for-files-from-future`] section. + +[`optimizing-compression-task-timeout`]: #setting-optimizing-compression-task-timeout + +Setting `allowed-clock-drift-for-files-from-future` +------------------ +- **type**: string (duration) +- **format**: `"{integer}(s | m | h | d)"` +- **default**: `"1d"` + +### Locks +When the [cache worker] attempts acquiring a lock for some task, +it checks if some other worker has already acquired such a lock. +To be fault tolerant and eventually execute every task, +the locks expire after some interval. +However, because of clock drifts and different timezones, +it would happen that some lock was created in the future. +This setting defines a tolerance limit for these locks. +If the time has been changed in the system (i.e. two years backwards), +the [cache system] should still work properly. +Thus, these locks will be treated as expired +(assuming the tolerance is not too big). + +### Cache files +Similarly to the locks, the cache files or their metadata might +have modification time in distant future. +The cache system tries to keep these files as long as possible. +If the limits are not reached, the cache files will not be deleted. +Otherwise, they will be treated as the oldest files, so they might survive. +If the user actually uses the cache file, the modification time will be updated. + +[`allowed-clock-drift-for-files-from-future`]: #setting-allowed-clock-drift-for-files-from-future + +Setting `file-count-soft-limit` +------------------ +- **type**: string (SI prefix) +- **format**: `"{integer}(K | M | G | T | P)?"` +- **default**: `"65536"` + +Soft limit for the file count in the cache directory. + +This doesn't include files with metadata. +To learn more, please refer to the [cache system] section. + +[`file-count-soft-limit`]: #setting-file-count-soft-limit + +Setting `files-total-size-soft-limit` +------------------ +- **type**: string (disk space) +- **format**: `"{integer}(K | Ki | M | Mi | G | Gi | T | Ti | P | Pi)?"` +- **default**: `"512Mi"` + +Soft limit for the total size* of files in the cache directory. + +This doesn't include files with metadata. +To learn more, please refer to the [cache system] section. + +*this is the file size, not the space physically occupied on the disk. + +[`files-total-size-soft-limit`]: #setting-files-total-size-soft-limit + +Setting `file-count-limit-percent-if-deleting` +------------------ +- **type**: string (percent) +- **format**: `"{integer}%"` +- **default**: `"70%"` + +If [`file-count-soft-limit`] is exceeded and the [cache worker] performs the cleanup task, +then the worker will delete some cache files, so after the task, +the file count should not exceed +[`file-count-soft-limit`] * [`file-count-limit-percent-if-deleting`]. + +This doesn't include files with metadata. +To learn more, please refer to the [cache system] section. + +[`file-count-limit-percent-if-deleting`]: #setting-file-count-limit-percent-if-deleting + +Setting `files-total-size-limit-percent-if-deleting` +------------------ +- **type**: string (percent) +- **format**: `"{integer}%"` +- **default**: `"70%"` + +If [`files-total-size-soft-limit`] is exceeded and [cache worker] performs the cleanup task, +then the worker will delete some cache files, so after the task, +the files total size should not exceed +[`files-total-size-soft-limit`] * [`files-total-size-limit-percent-if-deleting`]. + +This doesn't include files with metadata. +To learn more, please refer to the [cache system] section. + +[`files-total-size-limit-percent-if-deleting`]: #setting-files-total-size-limit-percent-if-deleting + +[toml]: https://github.com/toml-lang/toml +[directories]: https://crates.io/crates/directories +[cache system]: #how-does-the-cache-work +[cache worker]: #how-does-the-cache-work +[zstd]: https://facebook.github.io/zstd/ +[Least Recently Used (LRU)]: https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU) + +How does the cache work? +======================== + +**This is an implementation detail and might change in the future.** +Information provided here is meant to help understanding the big picture +and configuring the cache. + +There are two main components - the *cache system* and the *cache worker*. + +Cache system +------------ + +Handles GET and UPDATE cache requests. +- **GET request** - simply loads the cache from disk if it is there. +- **UPDATE request** - compresses received data with [zstd] and [`baseline-compression-level`], then writes the data to the disk. + +In case of successful handling of a request, it notifies the *cache worker* about this +event using the queue. +The queue has a limited size of [`worker-event-queue-size`]. If it is full, it will drop +new events until the *cache worker* pops some event from the queue. + +Cache worker +------------ + +The cache worker runs in a single thread with lower priority and pops events from the queue +in a loop handling them one by one. + +### On GET request +1. Read the statistics file for the cache file, + increase the usage counter and write it back to the disk. +2. Attempt recompressing the cache file if all of the following conditions are met: + - usage counter exceeds [`optimized-compression-usage-counter-threshold`], + - the file is compressed with compression level lower than [`optimized-compression-level`], + - no other worker has started working on this particular task within the last + [`optimizing-compression-task-timeout`] interval. + + When recompressing, [`optimized-compression-level`] is used as a compression level. + +### On UPDATE request +1. Write a fresh statistics file for the cache file. +2. Clean up the cache if no worker has attempted to do this within the last [`cleanup-interval`]. + During this task: + - all unrecognized files and expired task locks in cache directory will be deleted + - if [`file-count-soft-limit`] or [`files-total-size-soft-limit`] is exceeded, + then recognized files will be deleted according to + [`file-count-limit-percent-if-deleting`] and [`files-total-size-limit-percent-if-deleting`]. + Wasmtime uses [Least Recently Used (LRU)] cache replacement policy and requires that + the filesystem maintains proper mtime (modification time) of the files. + Files with future mtimes are treated specially - more details + in [`allowed-clock-drift-for-files-from-future`]. + +### Metadata files +- every cached WebAssembly module has its own statistics file +- every lock is a file diff --git a/docs/cli-install.md b/docs/cli-install.md new file mode 100644 index 0000000000..801d2f5a31 --- /dev/null +++ b/docs/cli-install.md @@ -0,0 +1,67 @@ +# Installing `wasmtime` + +Here we'll show you how to install the `wasmtime` command line tool. Note that +this is distinct from embedding the Wasmtime project into another, for that +you'll want to consult the [embedding documentation](embed.md). + +The easiest way to install the `wasmtime` CLI tool is through our installation +script. Linux and macOS users can execute the following: + +```sh +$ curl https://wasmtime.dev/install.sh -sSf | bash +``` + +This will download a precompiled version of `wasmtime` and place it in +`$HOME/.wasmtime`, and update your shell configuration to place the right +directory in `PATH`. + +Windows users will want to visit our [releases page][releases] and can download +the MSI installer (`wasmtime-dev-x86_64-windows.msi` for example) and use that +to install. + +[releases]: https://github.com/bytecodealliance/wasmtime/releases + +You can confirm your installation works by executing: + +```sh +$ wasmtime -V +wasmtime 0.12.0 +``` + +And now you're off to the races! Be sure to check out the [various CLI +options](cli-options.md) as well. + +## Download Precompiled Binaries + +If you'd prefer to not use an installation script, or you're perhaps +orchestrating something in CI, you can also download one of our precompiled +binaries of `wasmtime`. We have two channels of releases right now for +precompiled binaries: + +1. Each tagged release will have a full set of release artifacts on the [GitHub + releases page][releases]. +2. The [`dev` release] is also continuously updated with the latest build of the + `master` branch. If you want the latest-and-greatest and don't mind a bit of + instability, this is the release for you. + +[`dev` release]: https://github.com/bytecodealliance/wasmtime/releases/tag/dev + +When downloading binaries you'll likely want one of the following archives (for +the `dev` release) + +* Linux users - [`wasmtime-dev-x86_64-linux.tar.xz`] +* macOS users - [`wasmtime-dev-x86_64-macos.tar.xz`] +* Windows users - [`wasmtime-dev-x86_64-windows.zip`] + +Each of these archives has a `wasmtime` binary placed inside which can be +executed normally as the CLI would. + +[wasmtime-dev-x86_64-linux.tar.xz`]: https://github.com/bytecodealliance/wasmtime/releases/download/dev/wasmtime-dev-x86_64-linux.tar.xz +[wasmtime-dev-x86_64-macos.tar.xz`]: https://github.com/bytecodealliance/wasmtime/releases/download/dev/wasmtime-dev-x86_64-macos.tar.xz +[wasmtime-dev-x86_64-windows.zip`]: https://github.com/bytecodealliance/wasmtime/releases/download/dev/wasmtime-dev-x86_64-windows.zip + +## Compiling from Source + +If you'd prefer to compile the `wasmtime` CLI from source, you'll want to +consult the [contributing documentation for building](contributing-building.md). +Be sure to use a `--release` build if you're curious to do benchmarking! diff --git a/docs/cli-options.md b/docs/cli-options.md new file mode 100644 index 0000000000..4f382cc677 --- /dev/null +++ b/docs/cli-options.md @@ -0,0 +1,82 @@ +# CLI Options for `wasmtime` + +The `wasmtime` CLI is organized into a few subcommands. If no subcommand is +provided it'll assume `run`, which is to execute a wasm file. The subcommands +supported by `wasmtime` are: + +## `help` + +This is a general subcommand used to print help information to the terminal. You +can execute any number of the following: + +```sh +$ wasmtime help +$ wasmtime --help +$ wasmtime -h +$ wasmtime help run +$ wasmtime run -h +``` + +When in doubt, try running the `help` command to learn more about functionality! + +## `run` + +This is the `wasmtime` CLI's main subcommand, and it's also the default if no +other subcommand is provided. The `run` command will execute a WebAssembly +module. This means that the module will be compiled to native code, +instantiated, and then optionally have an export executed. + +The `wasmtime` CLI will automatically hook up any WASI-related imported +functionality, but at this time if your module imports anything else it will +fail instantiation. + +The `run` command takes one positional argument which is the name of the module +to run: + +```sh +$ wasmtime run foo.wasm +$ wasmtime foo.wasm +``` + +Note that the `wasmtime` CLI can take both a binary WebAssembly file (`*.wasm`) +as well as the text format for WebAssembly (`*.wat`): + +```sh +$ wasmtime foo.wat +``` + +## `wast` + +The `wast` command executes a `*.wast` file which is the test format for the +official WebAssembly spec test suite. This subcommand will execute the script +file which has a number of directives supported to instantiate modules, link +tests, etc. + +Executing this looks like: + +```sh +$ wasmtime wast foo.wast +``` + +## `config` + +This subcomand is used to control and edit local Wasmtime configuration +settings. The primary purpose of this currently is to configure [how Wasmtime's +code caching works](./cli-cache.md). You can create a new configuration file for +you to edit with: + +```sh +$ wasmtime config new +``` + +And that'll print out the path to the file you can edit. + +## `wasm2obj` + +This is an experimental subcommand to compile a WebAssembly module to native +code. Work for this is still heavily under development, but you can execute this +with: + +```sh +$ wasmtime wasm2obj foo.wasm foo.o +``` diff --git a/docs/cli.md b/docs/cli.md new file mode 100644 index 0000000000..f1ee314aa0 --- /dev/null +++ b/docs/cli.md @@ -0,0 +1,24 @@ +# Using the `wasmtime` CLI + +In addition to the embedding API which allows you to use Wasmtime as a +library, the Wasmtime project also provies a `wasmtime` CLI tool to conveniently +execute WebAssembly modules from the command line. + +This section will provide a guide to the `wasmtime` CLI and major functionality +that it contains. In short, however, you can execute a WebAssembly file +(actually doing work as part of the `start` function) like so: + +```sh +$ wasmtime foo.wasm +``` + +Or similarly if you want to invoke a "start" function, such as with WASI +modules, you can execute + +```sh +$ wasmtime --invoke _start foo.wasm +``` + +For more information be sure to check out [how to install the +CLI](cli-install.md) as well as [the list of options you can +pass](cli-options.md). diff --git a/docs/contributing-building.md b/docs/contributing-building.md new file mode 100644 index 0000000000..676a5e355f --- /dev/null +++ b/docs/contributing-building.md @@ -0,0 +1,61 @@ +# Building + +This section describes everything required to build and run Wasmtime. + +## Prerequisites + +Before we can actually build Wasmtime, we'll need to make sure these things are +installed first. + +### The Rust Toolchain + +[Install the Rust toolchain here.](https://www.rust-lang.org/tools/install) This +includes `rustup`, `cargo`, `rustc`, etc... + +### `libclang` (optional) + +The `wasmtime-fuzzing` crate transitively depends on `bindgen`, which requires +that your system has a `libclang` installed. Therefore, if you want to hack on +Wasmtime's fuzzing infrastructure, you'll need `libclang`. [Details on how to +get `libclang` and make it available for `bindgen` are +here.](https://rust-lang.github.io/rust-bindgen/requirements.html#clang) + +## Building the `wasmtime` CLI + +To make an unoptimized, debug build of the `wasmtime` CLI tool, go to the root +of the repository and run this command: + +```shell +cargo build +``` + +The built executable will be located at `target/debug/wasmtime`. + +To make an optimized build, run this command in the root of the repository: + +```shell +cargo build --release +``` + +The built executable will be located at `target/release/wasmtime`. + +You can also build and run a local `wasmtime` CLI by replacing `cargo build` +with `cargo run`. + +## Building Other Wasmtime Crates + +You can build any of the Wasmtime crates by appending `-p wasmtime-whatever` to +the `cargo build` invocation. For example, to build the `wasmtime-jit` crate, +execute this command: + +```shell +cargo build -p wasmtime-jit +``` + +Alternatively, you can `cd` into the crate's directory, and run `cargo build` +there, without needing to supply the `-p` flag: + +```shell +cd crates/jit/ +cargo build +``` diff --git a/docs/contributing-ci.md b/docs/contributing-ci.md new file mode 100644 index 0000000000..a63df0b73a --- /dev/null +++ b/docs/contributing-ci.md @@ -0,0 +1,3 @@ +# CI + +... more coming soon diff --git a/docs/contributing-coc.md b/docs/contributing-coc.md new file mode 100644 index 0000000000..83cb9cc90e --- /dev/null +++ b/docs/contributing-coc.md @@ -0,0 +1 @@ +{{#include ../CODE_OF_CONDUCT.md }} diff --git a/docs/contributing-fuzzing.md b/docs/contributing-fuzzing.md new file mode 100644 index 0000000000..1a0757e129 --- /dev/null +++ b/docs/contributing-fuzzing.md @@ -0,0 +1,38 @@ +# Fuzzing + +## Test Case Generators and Oracles + +Test case generators and oracles live in the `wasmtime-fuzzing` crate, located +in the `crates/fuzzing` directory. + +A *test case generator* takes raw, unstructured input from a fuzzer and +translates that into a test case. This might involve interpreting the raw input +as "DNA" or pre-determined choices through a decision tree and using it to +generate an in-memory data structure, or it might be a no-op where we interpret +the raw bytes as if they were Wasm. + +An *oracle* takes a test case and determines whether we have a bug. For example, +one of the simplest oracles is to take a Wasm binary as an input test case, +validate and instantiate it, and (implicitly) check that no assertions failed or +segfaults happened. A more complicated oracle might compare the result of +executing a Wasm file with and without optimizations enabled, and make sure that +the two executions are observably identical. + +Our test case generators and oracles strive to be fuzzer-agnostic: they can be +reused with libFuzzer or AFL or any other fuzzing engine or driver. + +## libFuzzer and `cargo fuzz` Fuzz Targets + +We combine a test case generator and one more more oracles into a *fuzz +target*. Because the target needs to pipe the raw input from a fuzzer into the +test case generator, it is specific to a particular fuzzer. This is generally +fine, since they're only a couple of lines of glue code. + +Currently, all of our fuzz targets are written for +[libFuzzer](https://www.llvm.org/docs/LibFuzzer.html) and [`cargo +fuzz`](https://rust-fuzz.github.io/book/cargo-fuzz.html). They are defined in +the `fuzz` subdirectory. + +See +[`fuzz/README.md`](https://github.com/bytecodealliance/wasmtime/blob/master/fuzz/README.md) +for details on how to run these fuzz targets and set up a corpus of seed inputs. diff --git a/docs/contributing-governance.md b/docs/contributing-governance.md new file mode 100644 index 0000000000..ee381b59f5 --- /dev/null +++ b/docs/contributing-governance.md @@ -0,0 +1,3 @@ +# Governance + +... more coming soon diff --git a/docs/contributing-release-process.md b/docs/contributing-release-process.md new file mode 100644 index 0000000000..f412b1e1de --- /dev/null +++ b/docs/contributing-release-process.md @@ -0,0 +1,27 @@ +# Release Process + +This is intended to serve as documentation for wasmtime's release process. It's +largely an internal checklist for those of us performing a wasmtime release, but +others might be curious in this as well! + +To kick off the release process someone decides to do a release. Currently +there's not a schedule for releases or something similar. Once the decision is +made (there's also not really a body governing these decisions, it's more +whimsical currently, or on request from others) then the following steps need to +be executed to make the release: + +1. `git pull` - make sure you've got the latest changes +1. Update the version numbers in `Cargo.toml` for all crates + * Edit `scripts/bump-wasmtime-version.sh`, notable the `version` variable + * Run the script + * Commit the changes +1. Make sure `RELEASES.md` is up-to-date, and fill it out if it doesn't have an + entry yet for the current release. +1. Send this version update as a PR to the wasmtime repository, wait for a merge +1. After merging, tag the merge as `vA.B.C` +1. Push the tag to the repository + * This will trigger the release CI which will create all release artifacts and + publish them to GitHub releases. +1. Run `scripts/publish-all.sh` to publish all crates to crates.io + +And that's it, then you've done a wasmtime release. diff --git a/docs/contributing-testing.md b/docs/contributing-testing.md new file mode 100644 index 0000000000..44c53fda4e --- /dev/null +++ b/docs/contributing-testing.md @@ -0,0 +1,147 @@ +# Testing + +This section describes how to run Wasmtime's tests and add new tests. + +Before continuing, make sure you can [build +Wasmtime](./contributing-building.html) successfully. Can't run the tests if you +can't build it! + +## Running All Tests + +To run all of Wasmtime's tests (excluding WASI integration tests), execute this command: + +```shell +cargo test --all +``` + +To include WASI integration tests, you'll need `wasm32-wasi` target installed, which, +assuming you're using [rustup.rs](https://rustup.rs) to manage your Rust versions, +can be done as follows: + +```shell +rustup target add wasm32-wasi +``` + +Next, to run all tests including the WASI integration tests, execute this command: + +```shell +cargo test --features test_programs --all +``` + +You can also exclude a particular crate from testing with `--exclude`. For +example, if you want to avoid testing the `wastime-fuzzing` crate — which +requires that `libclang` is installed on your system, and for some reason maybe +you don't have it — you can run: + +```shell +cargo test --all --exclude wasmtime-fuzzing +``` + +## Testing a Specific Crate + +You can test a particular Wasmtime crate with `cargo test -p +wasmtime-whatever`. For example, to test the `wasmtime-environ` crate, execute +this command: + +```shell +cargo test -p wasmtime-environ +``` + +Alternatively, you can `cd` into the crate's directory, and run `cargo test` +there, without needing to supply the `-p` flag: + +```shell +cd crates/environ/ +cargo test +``` + +## Running the Wasm Spec Tests + +The spec testsuite itself is in a git submodule, so make sure you've +checked it out and initialized its submodule: + +```shell +git submodule update --init +``` + +When the submodule is checked out, Wasmtime runs the Wasm spec testsuite as part +of testing the `wasmtime-cli` crate: + +```shell +cargo test -p wasmtime-cli +``` + +## Running WASI Integration Tests Only + +WASI integration tests can be run separately from all other tests which +can be useful when working on the `wasi-common` crate. This can be done by +executing this command: + +```shell +cargo test --features test_programs -p test-programs +``` + +## Adding New Tests + +### Adding Rust's `#[test]`-Style Tests + +For very "unit-y" tests, we add `test` modules in the same `.rs` file as the +code that is being tested. These `test` modules are configured to only get +compiled during testing with `#[cfg(test)]`. + +```rust +// some code... + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn some_test_for_that_code() { + // ... + } +} +``` + +If you're writing a unit test and a `test` module doesn't already exist, you can +create one. + +For more "integration-y" tests, we create a `tests` directory within the crate, +and put the tests inside there. For example, there are various code +cache-related tests at `crates/environ/tests/cache_*.rs`. Always feel free to +add a `tests` directory to a crate, if you want to add a new test and there +aren't any existing tests. + +### Adding Specification-Style Wast Tests + +We use the spec testsuite as-is and without custom patches or a forked +version. This probably isn't what you want to modify when adding a new Wasmtime +test! + +When you have a Wasmtime-specific test that you'd like to write in Wast and use +the Wast-style assertions, you can add it to our "misc testsuite". The misc +testsuite uses the same syntax and assertions as the spec testsuite, but lives +in `tests/misc_testsuite`. Feel free to add new tests to existing +`tests/misc_testsuite/*.wast` files or create new ones as needed. These tests +are run as part of the `wasmtime-cli` crate's tests. + +If you have a new test that you think really belongs in the spec testsuite, make +sure it makes sense for every Wasm implementation to run your test (i.e. it +isn't Wasmtime-specific) and send a pull request +[upstream](https://github.com/WebAssembly/testsuite/). Once it is accepted in +the upstream repo, we can update our git submodule and we'll start running the +new tests. + +### Adding WASI Integration Tests + +When you have a WASI-specific test program that you'd like to include as a +test case to run against our WASI implementation, you can add it to our +`test-programs` crate. In particular, you should drop a main-style Rust source +file into `crates/test-programs/wasi-tests/src/bin/some_new_test.rs` with a +name of your choice. And that's it! The build script included in the +`test-programs` crate will automatically generate the necessary boilerplate +code for your test program so that it's run on all supported hosts. + +If you would like to tweak which host to run the test program against however +(for instance, only on Unix but on Windows), you can tweak that in the build +script located under `crates/test-programs/build.rs`. diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000000..72c9a67762 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,31 @@ +# Contributing + +We're excited to work on Wasmtime together with you! This guide should help you +get up and running with Wasmtime development. But first, make sure you've read +the [Code of Conduct](./contributing-coc.html)! + +## Join Our Chat + +We chat about Wasmtime development on Zulip — [join +us!](https://bytecodealliance.zulipchat.com/#narrow/stream/217126-wasmtime) + +If you're having trouble building Wasmtime, aren't sure why a test is failing, +or have any other questions, feel free to ask here. You can also [open an +issue](https://github.com/bytecodealliance/wasmtime/issues/new)! + +## Finding Something to Hack On + +If you're looking for something to do, these are great places to start: + +* [Issues labeled "good first + issue"](https://github.com/bytecodealliance/wasmtime/labels/good%20first%20issue) + — these issues tend to be simple, what needs to be done is well known, + and are good for new contributors to tackle. The goal is to learn Wasmtime's + development workflow and make sure that you can build and test Wasmtime. + +* [Issues labeled "help + wanted"](https://github.com/bytecodealliance/wasmtime/labels/help%20wanted) + — these are issues that we need a little help with! + +If you're unsure if an issue is a good fit for you or not, feel free to ask in a +comment on the issue, or in chat. diff --git a/docs/embed-c.md b/docs/embed-c.md new file mode 100644 index 0000000000..20c5d65b51 --- /dev/null +++ b/docs/embed-c.md @@ -0,0 +1,4 @@ +# Embedding Wasmtime in C + +... more coming soon + diff --git a/docs/embed-rust.md b/docs/embed-rust.md new file mode 100644 index 0000000000..3ec437a518 --- /dev/null +++ b/docs/embed-rust.md @@ -0,0 +1,150 @@ +# Embedding Wasmtime in Rust + +This document shows how to embed Wasmtime using the Rust API, and run a simple +wasm program. + +# Create some wasm + +Let's create a simple WebAssembly file with a single exported function that returns an integer: + +```wat +(;; wat2wasm hello.wat -o $WASM_FILES/hello.wasm ;;) +(module + (func (export "answer") (result i32) + i32.const 42 + ) +) +``` + +# Create rust project + +```sh +$ cargo new --bin wasmtime_hello +$ cd wasmtime_hello +$ cp $WASM_FILES/hello.wasm . +``` + +We will be using the wasmtime engine/API to run the wasm file, so we will add the dependency to `Cargo.toml`: + +```toml +[dependencies] +wasmtime = "" +``` + +where "" is the current version number of the `wasmtime` crate. + +It is time to add code to the `src/main.rs`. First, storage needs to be activated: + +```rust +# extern crate wasmtime; +use wasmtime::*; + +let store = Store::default(); +``` + +The `hello.wasm` can be read from the file system and provided to the `Module` object constructor as `&[u8]`: + +```rust,no_run +# extern crate wasmtime; +# use wasmtime::*; +# fn main() -> Result<(), Box> { +# let store = Store::default(); +use std::fs::read; + +let hello_wasm = read("hello.wasm")?; + +let module = Module::new(&store, &hello_wasm)?; +# Ok(()) +# } +``` + +The module instance can now be created. Normally, you would provide imports, but +in this case, there are none required: + +```rust +# extern crate wasmtime; +# use wasmtime::*; +# fn main() -> Result<(), Box> { +# let store = Store::default(); +# let module = Module::new(&store, "(module)")?; +let instance = Instance::new(&module, &[])?; +# Ok(()) +# } +``` + +Everything is set. If a WebAssembly module has a start function -- it was run. +The instance's exports can be used at this point. wasmtime provides functions +to get an export by name, and ensure that it's a function: + +```rust +# extern crate wasmtime; +# use wasmtime::*; +# fn main() -> Result<(), Box> { +# let store = Store::default(); +# let module = Module::new(&store, r#"(module (func (export "answer")))"#)?; +# let instance = Instance::new(&module, &[])?; +let answer = instance.get_export("answer").expect("answer").func().expect("function"); +# Ok(()) +# } +``` + +The exported function can be called using the `call` method. The exported +"answer" function accepts no parameters and returns a single `i32` value. + +```rust +# extern crate wasmtime; +# use wasmtime::*; +# fn main() -> Result<(), Box> { +# let store = Store::default(); +# let module = Module::new(&store, r#"(module (func (export "answer") (result i32) i32.const 2))"#)?; +# let instance = Instance::new(&module, &[])?; +# let answer = instance.get_export("answer").expect("answer").func().expect("function"); +let result = answer.call(&[])?; +println!("Answer: {:?}", result[0].i32()); +# Ok(()) +# } +``` + +Since we know the signature of the function ahead of time, we can also assert +its signature and call the function directly without doing conversions: + +```rust +# extern crate wasmtime; +# use wasmtime::*; +# fn main() -> Result<(), Box> { +# let store = Store::default(); +# let module = Module::new(&store, r#"(module (func (export "answer") (result i32) i32.const 2))"#)?; +# let instance = Instance::new(&module, &[])?; +# let answer = instance.get_export("answer").expect("answer").func().expect("function"); +let answer = answer.get0::()?; +let result: i32 = answer()?; +println!("Answer: {}", result); +# Ok(()) +# } +``` + +The names of the WebAssembly module's imports and exports can be discovered by +means of module's corresponding methods. + +# src/main.rs + +```rust,no_run +# extern crate wasmtime; +use std::error::Error; +use std::fs::read; +use wasmtime::*; + +fn main() -> Result<(), Box> { + let store = Store::default(); + + let wasm = read("hello.wasm")?; + + let module = Module::new(&store, &wasm)?; + let instance = Instance::new(&module, &[])?; + + let answer = instance.get_export("answer").expect("answer").func().expect("function"); + let result = answer.call(&[])?; + println!("Answer: {:?}", result[0].i32()); + Ok(()) +} +``` diff --git a/docs/embed.md b/docs/embed.md new file mode 100644 index 0000000000..92e6b713af --- /dev/null +++ b/docs/embed.md @@ -0,0 +1,3 @@ +# Embedding Wasmtime + +... more coming soon diff --git a/docs/examples-markdown.md b/docs/examples-markdown.md new file mode 100644 index 0000000000..76c8e56d7f --- /dev/null +++ b/docs/examples-markdown.md @@ -0,0 +1,3 @@ +# Markdown Parser + +... more coming soon diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000000..fbef63c4ad --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,5 @@ +# Examples + +This is an explanation of all examples to come + +... more coming soon diff --git a/docs/introduction.md b/docs/introduction.md new file mode 100644 index 0000000000..5ea7d32ea3 --- /dev/null +++ b/docs/introduction.md @@ -0,0 +1,29 @@ +# Introduction + +[Wasmtime][github] is a [Bytecode Alliance][BA] project that is a standalone +wasm-only optimizing runtime for [WebAssembly] and [WASI]. It runs WebAssembly +code [outside of the Web], and can be used both as a command-line utility or as +a library embedded in a larger application. + +Wasmtime strives to be a highly configurable and embeddable runtime to run on +any scale of application. Many features are still under development so if you +have a question don't hesitate to [file an issue][issue]. + +This guide is intended to server a number of purposes and within you'll find: + +* [How to create simple wasm modules](tutorial-create-hello-world.md) +* [How to use Wasmtime from a number of languages](lang.md) +* [How to use install and use the `wasmtime` CLI](cli.md) +* Information about [stability](stability.md) and [security](security.md) in + Wasmtime. + +... and more! The source for this guide [lives on +GitHub](https://github.com/bytecodealliance/wasmtime/tree/master/docs) and +contributions are welcome! + +[github]: https://github.com/bytecodealliance/wasmtime +[BA]: https://bytecodealliance.org/ +[WebAssembly]: https://webassembly.org/ +[WASI]: https://wasi.dev +[outside of the Web]: https://webassembly.org/docs/non-web/ +[issue]: https://github.com/bytecodealliance/wasmtime/issues/new diff --git a/docs/lang-bash.md b/docs/lang-bash.md new file mode 100644 index 0000000000..4ba858521a --- /dev/null +++ b/docs/lang-bash.md @@ -0,0 +1,3 @@ +# Using WebAssembly from Bash + +... more coming soon diff --git a/docs/lang-dotnet.md b/docs/lang-dotnet.md new file mode 100644 index 0000000000..22f901f40d --- /dev/null +++ b/docs/lang-dotnet.md @@ -0,0 +1,3 @@ +# Using WebAssembly from .NET + +... more coming soon diff --git a/docs/lang-python.md b/docs/lang-python.md new file mode 100644 index 0000000000..4c2c10224d --- /dev/null +++ b/docs/lang-python.md @@ -0,0 +1,3 @@ +# Using WebAssembly from Python + +... more coming soon diff --git a/docs/lang-rust.md b/docs/lang-rust.md new file mode 100644 index 0000000000..2bdd548ccf --- /dev/null +++ b/docs/lang-rust.md @@ -0,0 +1,3 @@ +# Using WebAssembly from Rust + +... more coming soon diff --git a/docs/lang.md b/docs/lang.md new file mode 100644 index 0000000000..ab5adf5f48 --- /dev/null +++ b/docs/lang.md @@ -0,0 +1,3 @@ +# Using WebAssembly from your Language + +... more coming soon diff --git a/docs/security-disclosure.md b/docs/security-disclosure.md new file mode 100644 index 0000000000..b584fe24ba --- /dev/null +++ b/docs/security-disclosure.md @@ -0,0 +1,3 @@ +# Disclosure Policy + +... more coming soon diff --git a/docs/security-sandboxing.md b/docs/security-sandboxing.md new file mode 100644 index 0000000000..5b74628037 --- /dev/null +++ b/docs/security-sandboxing.md @@ -0,0 +1,22 @@ +# Sandboxing + +One of WebAssembly (and Wasmtime's) main goals is to execute untrusted code in +a safe manner inside of a sandbox. WebAssembly is inherently sandboxed by design +(must import all functionality, etc). This document is intended to cover the +various sandboxing implementation strategies that Wasmtime has as they are +developed. + +At this time Wasmtime implements what's necessary for the WebAssembly +specification, for example memory isolation between instances. Additionally the +safe Rust API is intended to mitigate accidental bugs in hosts. + +Different sandboxing implementation techniques will also come with different +tradeoffs in terms of performance and feature limitations, and Wasmtime plans to +offer users choices of which tradeoffs they want to make. + +More will be added here over time! + +## Spectre + +Wasmtime does not yet implement Spectre mitigations, however this is a subject +of ongoing research. diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000000..68616a8545 --- /dev/null +++ b/docs/security.md @@ -0,0 +1,3 @@ +# Security + +... more coming soon diff --git a/docs/stability-platform-support.md b/docs/stability-platform-support.md new file mode 100644 index 0000000000..d2ed7fb8bf --- /dev/null +++ b/docs/stability-platform-support.md @@ -0,0 +1,43 @@ +# Platform Support + +The `wasmtime` project is a configurable and lightweight runtime for WebAssembly +which has a number of ways it can be configured. Not all features are supported +on all platforms, but it is intended that `wasmtime` can run in some capacity on +almost all platforms! The matrix of what's being tested, what works, and what's +supported where is evolving over time, and this document hopes to capture a +snapshot of what the current state of the world looks like. + +All features of `wasmtime` should work on the following platforms: + +* Linux x86\_64 +* macOS x86\_64 +* Windows x86\_64 + +For more detailed information about supported platforms, please check out the +sections below! + +## JIT compiler support + +The JIT compiler, backed by either `lightbeam` or `cranelift` supports only the +x86\_64 architecture at this time. Support for at least ARM, AArch64, and x86 is +planned at this time. + +Usage of the JIT compiler will require a host operating system which supports +creating executable memory pages on-the-fly. In Rust terms this generally means +that `std` needs to be supported on this platform. + +## Interpreter support + +At this time `wasmtime` does not have a mode in which it simply interprets +WebAssembly code. It is planned to add support for an interpreter, however, and +this will have minimal system dependencies. It is planned that the system will +need to support some form of dynamic memory allocation, but other than that not +much else will be needed. + +## What about `#[no_std]`? + +The `wasmtime` project does not currently use `#[no_std]` for its crates, but +this is not because it won't support it! At this time we're still gathering use +cases for for what `#[no_std]` might entail, so if you're interested in this +we'd love to hear about your use case! Feel free to open an issue on the +`wasmtime` repository to discuss this. diff --git a/docs/stability-release.md b/docs/stability-release.md new file mode 100644 index 0000000000..0bc16d92a3 --- /dev/null +++ b/docs/stability-release.md @@ -0,0 +1,3 @@ +# Release Process + +... more coming soon diff --git a/docs/stability.md b/docs/stability.md new file mode 100644 index 0000000000..10100e107b --- /dev/null +++ b/docs/stability.md @@ -0,0 +1,3 @@ +# Stability + +... more coming soon diff --git a/docs/tutorial-create-hello-world.md b/docs/tutorial-create-hello-world.md new file mode 100644 index 0000000000..8e290aae76 --- /dev/null +++ b/docs/tutorial-create-hello-world.md @@ -0,0 +1,3 @@ +# Creating `hello-world.wasm` + +... more coming soon diff --git a/docs/tutorial-run-hello-world.md b/docs/tutorial-run-hello-world.md new file mode 100644 index 0000000000..ffe03c7075 --- /dev/null +++ b/docs/tutorial-run-hello-world.md @@ -0,0 +1,3 @@ +# Running `hello-world.wasm` with Wasmtime + +... more coming soon diff --git a/docs/tutorial.md b/docs/tutorial.md new file mode 100644 index 0000000000..3ba6d0fff0 --- /dev/null +++ b/docs/tutorial.md @@ -0,0 +1,3 @@ +# Tutorial + +... more coming soon diff --git a/docs/wasi-software-architecture.png b/docs/wasi-software-architecture.png new file mode 100644 index 0000000000..6cb8345cc4 Binary files /dev/null and b/docs/wasi-software-architecture.png differ diff --git a/docs/wasm-c.md b/docs/wasm-c.md new file mode 100644 index 0000000000..d9455759b0 --- /dev/null +++ b/docs/wasm-c.md @@ -0,0 +1,15 @@ +# C/C++ + +All the parts needed to support wasm are included in upstream clang, lld, and +compiler-rt, as of the LLVM 8.0 release. However, to use it, you'll need +to build WebAssembly-targeted versions of the library parts, and it can +be tricky to get all the CMake invocations lined up properly. + +To make things easier, we provide +[prebuilt packages](https://github.com/CraneStation/wasi-sdk/releases) +that provide builds of Clang and sysroot libraries. + +WASI doesn't yet support `setjmp`/`longjmp` or C++ exceptions, as it is +waiting for [unwinding support in WebAssembly]. + +[unwinding support in WebAssembly]: https://github.com/WebAssembly/exception-handling/ diff --git a/docs/wasm-markdown.md b/docs/wasm-markdown.md new file mode 100644 index 0000000000..20485f2073 --- /dev/null +++ b/docs/wasm-markdown.md @@ -0,0 +1 @@ +# Example: Markdown Parser diff --git a/docs/wasm-rust.md b/docs/wasm-rust.md new file mode 100644 index 0000000000..b24db17824 --- /dev/null +++ b/docs/wasm-rust.md @@ -0,0 +1,286 @@ +# Rust + +The [Rust Programming Language](https://www.rust-lang.org) supports WebAssembly +as a compilation target. If you're not familiar with Rust it's recommended to +start [with its introductory documentation](https://www.rust-lang.org/learn). +Compiling to WebAssembly will involve specifying the desired target via the +`--target` flag, and to do this there are a number of "target triples" for +WebAssembly compilation in Rust: + +* `wasm32-wasi` - when using `wasmtime` this is likely what you'll be using. The + WASI target is integrated into the standard library and is intended on + producing standalone binaries. +* `wasm32-unknown-unknown` - this target, like the WASI one, is focused on + producing single `*.wasm` binaries. The standard library, however, is largely + stubbed out since the "unknown" part of the target means libstd can't assume + anything. This means that while binaries will likely work in `wasmtime`, + common conveniences like `println!` or `panic!` won't work. +* `wasm32-unknown-emscripten` - this target is intended to work in a web browser + and produces a `*.wasm` file coupled with a `*.js` file, and it is not + compatible with `wasmtime`. + +For the rest of this documentation we'll assume that you're using the +`wasm32-wasi` target for compiling Rust code and executing inside of `wasmtime`. + +## Hello, World! + +Cross-compiling to WebAssembly involves a number of knobs that need +configuration, but you can often gloss over these internal details by using +build tooling intended for the WASI target. For example we can start out writing +a WebAssembly binary with [`cargo +wasi`](https://github.com/bytecodealliance/cargo-wasi). + +First up we'll [install `cargo +wasi`](https://bytecodealliance.github.io/cargo-wasi/install.html): + +```sh +$ cargo install cargo-wasi +``` + +Next we'll make a new Cargo project: + +```sh +$ cargo new hello-world +$ cd hello-world +``` + +Inside of `src/main.rs` you'll see the canonical Rust "Hello, World!" using +`println!`. We'll be executing this for the `wasm32-wasi` target, so you'll want +to make sure you're previously [built `wasmtime` and inserted it into +`PATH`](./cli-install.md); + +```sh +$ cargo wasi run +info: downloading component 'rust-std' for 'wasm32-wasi' +info: installing component 'rust-std' for 'wasm32-wasi' + Compiling hello-world v0.1.0 (/hello-world) + Finished dev [unoptimized + debuginfo] target(s) in 0.16s + Running `/.cargo/bin/cargo-wasi target/wasm32-wasi/debug/hello-world.wasm` + Running `target/wasm32-wasi/debug/hello-world.wasm` +Hello, world! +``` + +And we're already running our first WebAssembly code inside of `wasmtime`! + +While it's automatically happening for you as part of `cargo wasi`, you can also +run `wasmtime` yourself: + +```sh +$ wasmtime target/wasm32-wasi/debug/hello-world.wasm +Hello, world! +``` + +You can check out the [introductory documentation of +`cargo-wasi`](https://bytecodealliance.github.io/cargo-wasi/hello-world.html) as +well for some more information. + +## Writing Libraries + +Previously for "Hello, World!" we created a *binary* project which used +`src/main.rs`. Not all `*.wasm` binaries are intended to be executed like +commands, though. Some are intended to be loaded into applications and called +through various APIs, acting more like libraries. For this use case you'll want +to add this to `Cargo.toml`: + +```toml +# in Cargo.toml ... + +[lib] +crate-type = ['cdylib'] +``` + +and afterwards you'll want to write your code in `src/lib.rs` like so: + +```rust +#[no_mangle] +pub extern "C" fn print_hello() { + println!("Hello, world!"); +} +``` + +When you execute `cargo wasi build` that'll generate a `*.wasm` file which has +one exported function, `print_hello`. We can then run it via the CLI like so: + +```sh +$ cargo wasi build + Compiling hello-world v0.1.0 (/home/alex/code/hello-world) + Finished dev [unoptimized + debuginfo] target(s) in 0.08s +$ wasmtime --invoke print_hello target/wasm32-wasi/debug/hello_world.wasm +Hello, world! +``` + +As a library crate one of your primary consumers may be other languages as well. +You'll want to consult the [section of this book for using `wasmtime` from +Python`](./lang-python.md) and after running through the basics there you can +execute our file in Python: + +```sh +$ cp target/wasm32-wasi/debug/hello_world.wasm . +$ python3 +>>> import wasmtime +>>> import hello_world +>>> hello_world.print_hello() +Hello, world! +() +>>> +``` + +Note that this form of using `#[no_mangle]` Rust functions is pretty primitive. +You're only able to work with primitive datatypes like integers and floats. +While this works for some applications if you need to work with richer types +like strings or structs, then you'll want to use the support in `wasmtime` for +interface types. + +## WebAssembly Interface Types + +Working with WebAssembly modules at the bare-bones level means that you're only +dealing with integers and floats. Many APIs, however, want to work with things +like byte arrays, strings, structures, etc. To facilitate these interactions the +[WebAssembly Interface Types +Proposal](https://github.com/webassembly/interface-types) comes into play. The +`wasmtime` runtime has support for interface types, and the Rust toolchain has +library support in a crate called +[`wasm-bindgen`](https://crates.io/crates/wasm-bindgen). + +> **Note**: WebAssembly Interface Types is still a WebAssembly proposal and is +> under active development. The toolchain may not match the exact specification, +> and during development you'll generally need to make sure tool versions are +> all kept up to date to ensure everything aligns right. This'll all smooth over +> as the proposal stabilizes! + +To get started with WebAssembly interface types let's write a library +module which will generate a greeting for us. The module itself won't do any +printing, we'll simply be working with some strings. + +To get starts let's add this to our `Cargo.toml`: + +```toml +[lib] +crate-type = ['cdylib'] + +[dependencies] +wasm-bindgen = "0.2.54" +``` + +Using this crate, we can then update our `src/lib.rs` with the following: + +```rust,ignore +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub fn greet(name: &str) -> String { + format!("Hello, {}!", name) +} +``` + +Then we can build this with: + +```sh +$ cargo wasi build --release + Updating crates.io index +... + Finished dev [unoptimized + debuginfo] target(s) in 9.57s + Downloading precompiled wasm-bindgen v0.2.54 +``` + +and we have our new wasm binary! + +> **Note**: for now when using `wasm-bindgen` you must use `--release` mode to +> build wasi binaries with interface types. + +We can then test out support for this with the CLI: + +```sh +$ wasmtime --invoke greet ./target/wasm32-wasi/release/hello_world.wasm "wasmtime CLI" +warning: using `--invoke` with a function that takes arguments is experimental and may break in the future +warning: using `--invoke` with a function that returns values is experimental and may break in the future +Hello, wasmtime CLI! +``` + +Here we can see some experimental warnings, but we got our error message printed +out! The first CLI parameter, `"wasmtime CLI"`, was passed as the first argument +of the `greet` function. The resulting string was then printed out to the +console. + +Like before, we can also execute this with Python: + +```sh +$ cp target/wasm32-wasi/release/hello_world.wasm . +$ python3 +>>> import wasmtime +>>> import hello_world +>>> hello_world.greet('python interpreter') +'Hello, python interpreter!' +>>> +``` + +Note that `wasm-bindgen` was originally developed for JS and usage in a browser, +but a subset of its implementation (such as arguments which are strings) are +supported for WebAssembly interface types. You can also check out the [reference +documentation for `wasm-bindgen`](https://rustwasm.github.io/wasm-bindgen/) for +more information about how it works. Note that the `wasm-bindgen` support for +wasm interface type is still in its nascent phase and is likely to be greatly +improved in the future. + +## Exporting Rust functionality + +Currently only Rust functions can be exported from a wasm module. Rust functions +must be `#[no_mangle]` to show up in the final binary, but if you're using +`#[wasm_bindgen]` that will happen automatically for you. + +Memory is by default exported from Rust modules under the name `memory`. This +can be tweaked with the `-Clink-arg` flag to rustc to pass flags to LLD, the +WebAssembly code linker. + +Tables cannot be imported at this time. When using `rustc` directly there is no +support for `anyref` and only one function table is supported. When using +`wasm-bindgen` it may inject an `anyref` table if necessary, but this table is +an internal detail and is not exported. The function table can be exported by +passing the `--export-table` argument to LLD (via `-C link-arg`) or can be +imported with the `--import-table`. + +Rust currently does not have support for exporting or importing custom `global` +values. + +## Importing host functionality + +Only functions can be imported in Rust at this time, and they can be imported +via raw interfaces like: + +```rust +# struct MyStruct; +#[link(wasm_import_module = "the-wasm-import-module")] +extern "C" { + // imports the name `foo` from `the-wasm-import-module` + fn foo(); + + // functions can have integer/float arguments/return values + fn translate(a: i32) -> f32; + + // Note that the ABI of Rust and wasm is somewhat in flux, so while this + // works, it's recommended to rely on raw integer/float values where + // possible. + fn translate_fancy(my_struct: MyStruct) -> u32; + + // you can also explicitly specify the name to import, this imports `bar` + // instead of `baz` from `the-wasm-import-module`. + #[link_name = "bar"] + fn baz(); +} +``` + +When you're using `wasm-bindgen` you would instead use: + +```rust,ignore +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(module = "the-wasm-import-module")] +extern "C" { + fn foo(); + fn baz(); + // ... +} +``` + +Note that unless you're using interface types you likely don't need +`wasm-bindgen`. diff --git a/docs/wasm-wat.md b/docs/wasm-wat.md new file mode 100644 index 0000000000..e1f519b332 --- /dev/null +++ b/docs/wasm-wat.md @@ -0,0 +1,57 @@ +# WebAssembly Text Format (`*.wat`) + +While not necessarily a full-blown language you might be curious how Wasmtime +interacts with [the `*.wat` text format][spec]! The `wasmtime` CLI and Rust +embedding API both support the `*.wat` text format by default. + +"Hello, World!" is pretty nontrivial in the `*.wat` format since it's +assembly-like and not really intended to be a primary programming language. That +being said we can create a simple add function to call it! + +For example if you have a file `add.wat` like so: + +```wat +(module + (func (export "add") (param i32 i32) (result i32) + local.get 0 + local.get 1 + i32.add)) +``` + +Then you can execute this on the CLI with: + +```sh +$ wasmtime add.wat --invoke add 1 2 +warning: ... +warning: ... +3 +``` + +And we can see that we're already adding numbers! + +You can also see how this works in the Rust API like so: + +```rust +# extern crate wasmtime; +# extern crate anyhow; +use wasmtime::*; + +# fn main() -> anyhow::Result<()> { +let store = Store::default(); +let wat = r#" + (module + (func (export "add") (param i32 i32) (result i32) + local.get 0 + local.get 1 + i32.add)) +"#; +let module = Module::new(&store, wat)?; +let instance = Instance::new(&module, &[])?; +let add = instance.get_export("add").and_then(|f| f.func()).unwrap(); +let add = add.get2::()?; +println!("1 + 2 = {}", add(1, 2)?); +# Ok(()) +# } +``` + +[spec]: https://webassembly.github.io/spec/core/text/index.html diff --git a/docs/wasm.md b/docs/wasm.md new file mode 100644 index 0000000000..b524cc811e --- /dev/null +++ b/docs/wasm.md @@ -0,0 +1,12 @@ +# Writing WebAssembly + +Wasmtime is a runtime for *executing* WebAssembly but you also at some point +need to actually produce the WebAssembly module to feed into Wasmtime! This +section of the guide is intended to provide some introductory documentation for +compiling source code to WebAssembly to later run in Wasmtime. There's plenty of +other documentation on the web for doing this, so you'll want to be sure to +check out your language's documentation for WebAssembly as well. + +* [Rust](wasm-rust.md) +* [C/C++](wasm-c.md) +* [WebAssembly Text Format (`*.wat`)](wasm-wat.md) diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000000..ff2738685d --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,2 @@ +artifacts +corpus diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000000..ec70d6c695 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "wasmtime-fuzz" +version = "0.12.0" +authors = ["The Wasmtime Project Developers"] +edition = "2018" +publish = false + +[package.metadata] +cargo-fuzz = true + +[dependencies] +arbitrary = "0.2.0" +wasmtime-fuzzing = { path = "../crates/fuzzing" } +wasmtime = { path = "../crates/api" } +libfuzzer-sys = "0.2.1" + +[[bin]] +name = "compile" +path = "fuzz_targets/compile.rs" +test = false +doc = false + +[[bin]] +name = "instantiate" +path = "fuzz_targets/instantiate.rs" +test = false +doc = false + +[[bin]] +name = "instantiate_translated" +path = "fuzz_targets/instantiate_translated.rs" +test = false +doc = false + +[[bin]] +name = "api_calls" +path = "fuzz_targets/api_calls.rs" +test = false +doc = false + +[[bin]] +name = "differential" +path = "fuzz_targets/differential.rs" +test = false +doc = false diff --git a/fuzz/README.md b/fuzz/README.md new file mode 100644 index 0000000000..993fac6b59 --- /dev/null +++ b/fuzz/README.md @@ -0,0 +1,52 @@ +# `cargo fuzz` Targets for Wasmtime + +This crate defines various [libFuzzer](https://www.llvm.org/docs/LibFuzzer.html) +fuzzing targets for Wasmtime, which can be run via [`cargo +fuzz`](https://rust-fuzz.github.io/book/cargo-fuzz.html). + +These fuzz targets just glue together pre-defined test case generators with +oracles and pass libFuzzer-provided inputs to them. The test case generators and +oracles themselves are independent from the fuzzing engine that is driving the +fuzzing process and are defined in `wasmtime/crates/fuzzing`. + +## Example + +To start fuzzing run the following command, where `$MY_FUZZ_TARGET` is one of +the [available fuzz targets](#available-fuzz-targets): + +```shell +cargo fuzz run $MY_FUZZ_TARGET +``` + +## Available Fuzz Targets + +At the time of writing, we have the following fuzz targets: + +* `compile`: Attempt to compile libFuzzer's raw input bytes with Wasmtime. +* `instantiate`: Attempt to compile and instantiate libFuzzer's raw input bytes + with Wasmtime. +* `instantiate_translated`: Pass libFuzzer's input bytes to `wasm-opt -ttf` to + generate a random, valid Wasm module, and then attempt to instantiate it. + +The canonical list of fuzz targets is the `.rs` files in the `fuzz_targets` +directory: + +```shell +ls wasmtime/fuzz/fuzz_targets/ +``` + +## Corpora + +While you *can* start from scratch, libFuzzer will work better if it is given a +[corpus](https://www.llvm.org/docs/LibFuzzer.html#corpus) of seed inputs to kick +start the fuzzing process. We maintain a corpus for each of these fuzz targets +in [a dedicated repo on +github](https://github.com/bytecodealliance/wasmtime-libfuzzer-corpus). + +You can use our corpora by cloning it and placing it at `wasmtime/fuzz/corpus`: + +```shell +git clone \ + https://github.com/bytecodealliance/wasmtime-libfuzzer-corpus.git \ + wasmtime/fuzz/corpus +``` diff --git a/fuzz/fuzz_targets/api_calls.rs b/fuzz/fuzz_targets/api_calls.rs new file mode 100755 index 0000000000..75f967afc0 --- /dev/null +++ b/fuzz/fuzz_targets/api_calls.rs @@ -0,0 +1,8 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use wasmtime_fuzzing::{generators::api::ApiCalls, oracles}; + +fuzz_target!(|api: ApiCalls| { + oracles::make_api_calls(api); +}); diff --git a/fuzz/fuzz_targets/compile.rs b/fuzz/fuzz_targets/compile.rs new file mode 100755 index 0000000000..d6bbf64f40 --- /dev/null +++ b/fuzz/fuzz_targets/compile.rs @@ -0,0 +1,14 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use wasmtime::Strategy; +use wasmtime_fuzzing::oracles; + +fuzz_target!(|data: &[u8]| { + oracles::compile(data, Strategy::Cranelift); +}); + +#[cfg(feature = "lightbeam")] +fuzz_target!(|data: &[u8]| { + oracles::compile(data, Strategy::Lightbeam); +}); diff --git a/fuzz/fuzz_targets/differential.rs b/fuzz/fuzz_targets/differential.rs new file mode 100755 index 0000000000..5cf14f0523 --- /dev/null +++ b/fuzz/fuzz_targets/differential.rs @@ -0,0 +1,13 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use wasmtime_fuzzing::{generators, oracles}; + +fuzz_target!(|data: ( + generators::DifferentialConfig, + generators::DifferentialConfig, + generators::WasmOptTtf +)| { + let (lhs, rhs, wasm) = data; + oracles::differential_execution(&wasm, &[lhs, rhs]); +}); diff --git a/fuzz/fuzz_targets/instantiate.rs b/fuzz/fuzz_targets/instantiate.rs new file mode 100755 index 0000000000..61b22c1877 --- /dev/null +++ b/fuzz/fuzz_targets/instantiate.rs @@ -0,0 +1,9 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use wasmtime::Strategy; +use wasmtime_fuzzing::oracles; + +fuzz_target!(|data: &[u8]| { + oracles::instantiate(data, Strategy::Auto); +}); diff --git a/fuzz/fuzz_targets/instantiate_translated.rs b/fuzz/fuzz_targets/instantiate_translated.rs new file mode 100755 index 0000000000..4f98c3d2fb --- /dev/null +++ b/fuzz/fuzz_targets/instantiate_translated.rs @@ -0,0 +1,9 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use wasmtime::Strategy; +use wasmtime_fuzzing::{generators, oracles}; + +fuzz_target!(|data: generators::WasmOptTtf| { + oracles::instantiate(&data.wasm, Strategy::Auto); +}); diff --git a/scripts/bump-wasmtime-version.sh b/scripts/bump-wasmtime-version.sh new file mode 100755 index 0000000000..333167b603 --- /dev/null +++ b/scripts/bump-wasmtime-version.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -euo pipefail + +# This is a convenience script for maintainers publishing a new version of +# Wasmtime to crates.io. To use, bump the version number below, run the +# script, and then run the commands that the script prints. + +topdir=$(dirname "$0")/.. +cd "$topdir" + +# All the wasmtime-* crates have the same version number +version="0.12.0" + +# Update the version numbers of the crates to $version. +echo "Updating crate versions to $version" +find -name Cargo.toml \ + -not -path ./crates/wasi-common/wig/WASI/tools/witx/Cargo.toml \ + -exec sed -i.bk -e "s/^version = \"[[:digit:]].*/version = \"$version\"/" {} \; + +# Update the required version numbers of path dependencies. +find -name Cargo.toml \ + -not -path ./crates/wasi-common/wig/WASI/tools/witx/Cargo.toml \ + -exec sed -i.bk \ + -e "/\> *= *{.*\ /dev/null ; then + if ! "$topdir/scripts/format-all.sh" --check ; then + echo "Formatting diffs detected! Run \"cargo fmt --all\" to correct." + exit 1 + fi +else + echo "cargo-fmt not available; formatting not checked!" + echo + echo "If you are using rustup, rustfmt can be installed via" + echo "\"rustup component add --toolchain=stable rustfmt-preview\", or see" + echo "https://github.com/rust-lang-nursery/rustfmt for more information." +fi + +# Make sure the code builds in release mode. +banner "Rust release build" +cargo build --release + +# Make sure the code builds in debug mode. +banner "Rust debug build" +cargo build + +# Run the tests. We run these in debug mode so that assertions are enabled. +banner "Rust unit tests" + +# TODO: lightbeam currently requires rust nightly, so don't try to run the +# tests here. Name all the other packages, rather than using --all. We'll +# run the lightbeam tests below if nightly is available. +#RUST_BACKTRACE=1 cargo test --all +RUST_BACKTRACE=1 cargo test \ + --package wasmtime-cli \ + --package wasmtime \ + --package wasmtime-wasi \ + --package wasmtime-wast \ + --package wasmtime-debug \ + --package wasmtime-environ \ + --package wasmtime-runtime \ + --package wasmtime-jit \ + --package wasmtime-interface-types \ + --package wasmtime-obj \ + +# Test wasmtime-wasi-c, which doesn't support Windows. +if [ "${OS:-Not}" != "Windows_NT" ]; then + RUST_BACKTRACE=1 cargo test \ + --package wasmtime-wasi-c +fi + +# Make sure the documentation builds. +banner "Rust documentation: $topdir/target/doc/wasmtime/index.html" +cargo doc + +# Ensure fuzzer works by running it with a single input +# Note LSAN is disabled due to https://github.com/google/sanitizers/issues/764 +banner "cargo fuzz check" +if rustup toolchain list | grep -q nightly; then + # Temporarily disable fuzz tests until https://github.com/bytecodealliance/cranelift/issues/1216 is resolved + #if cargo install --list | grep -q cargo-fuzz; then + # echo "cargo-fuzz found" + #else + # echo "installing cargo-fuzz" + # cargo +nightly install cargo-fuzz + #fi + + #fuzz_module="1340712d77d3db3c79b4b0c1494df18615485480" + #ASAN_OPTIONS=detect_leaks=0 \ + #cargo +nightly fuzz run compile \ + # "$topdir/fuzz/corpus/compile/$fuzz_module" + + # Nightly is available, so also run lightbeam's tests, which we + # skipped earlier. + cargo +nightly test --features lightbeam --package lightbeam + cargo +nightly test --features lightbeam + + # Also run wasmtime-py and wasmtime-rust's tests. + RUST_BACKTRACE=1 cargo +nightly test \ + --package wasmtime-py \ + --package wasmtime-rust +else + echo "nightly toolchain not found, skipping fuzz target integration test" +fi + +banner "OK" diff --git a/src/bin/wasm2obj.rs b/src/bin/wasm2obj.rs new file mode 100644 index 0000000000..12e02b3e20 --- /dev/null +++ b/src/bin/wasm2obj.rs @@ -0,0 +1,12 @@ +//! The `wasm2obj` command line tool. +//! +//! Translates WebAssembly modules to object files. +//! See `wasm2obj --help` for usage. + +use anyhow::Result; +use structopt::StructOpt; +use wasmtime_cli::commands::WasmToObjCommand; + +fn main() -> Result<()> { + WasmToObjCommand::from_args().execute() +} diff --git a/src/bin/wasmtime.rs b/src/bin/wasmtime.rs new file mode 100644 index 0000000000..89c1078f48 --- /dev/null +++ b/src/bin/wasmtime.rs @@ -0,0 +1,73 @@ +//! The `wasmtime` command line tool. +//! +//! Primarily used to run WebAssembly modules. +//! See `wasmtime --help` for usage. + +use anyhow::Result; +use structopt::{clap::AppSettings, clap::ErrorKind, StructOpt}; +use wasmtime_cli::commands::{ + ConfigCommand, RunCommand, WasmToObjCommand, WastCommand, WASM2OBJ_AFTER_HELP, +}; + +/// Wasmtime WebAssembly Runtime +#[derive(StructOpt)] +#[structopt( + name = "wasmtime", + version = env!("CARGO_PKG_VERSION"), + global_settings = &[ + AppSettings::VersionlessSubcommands, + AppSettings::ColoredHelp + ], + after_help = "If a subcommand is not provided, the `run` subcommand will be used.\n\ + \n\ + Usage examples:\n\ + \n\ + Running a WebAssembly module with a start function:\n\ + \n \ + wasmtime example.wasm + \n\ + Passing command line arguments to a WebAssembly module:\n\ + \n \ + wasmtime example.wasm arg1 arg2 arg3\n\ + \n\ + Invoking a specific function (e.g. `add`) in a WebAssembly module:\n\ + \n \ + wasmtime example.wasm --invoke add 1 2\n" +)] +enum WasmtimeApp { + // !!! IMPORTANT: if subcommands are added or removed, update `parse_module` in `src/commands/run.rs`. !!! + /// Controls Wasmtime configuration settings + Config(ConfigCommand), + /// Runs a WebAssembly module + Run(RunCommand), + /// Translates a WebAssembly module to native object file + #[structopt(name = "wasm2obj", after_help = WASM2OBJ_AFTER_HELP)] + WasmToObj(WasmToObjCommand), + /// Runs a WebAssembly test script file + Wast(WastCommand), +} + +impl WasmtimeApp { + /// Executes the command. + pub fn execute(&self) -> Result<()> { + match self { + Self::Config(c) => c.execute(), + Self::Run(c) => c.execute(), + Self::WasmToObj(c) => c.execute(), + Self::Wast(c) => c.execute(), + } + } +} + +fn main() -> Result<()> { + WasmtimeApp::from_iter_safe(std::env::args()) + .unwrap_or_else(|e| match e.kind { + ErrorKind::HelpDisplayed + | ErrorKind::VersionDisplayed + | ErrorKind::MissingArgumentOrSubcommand => e.exit(), + _ => WasmtimeApp::Run( + RunCommand::from_iter_safe(std::env::args()).unwrap_or_else(|_| e.exit()), + ), + }) + .execute() +} diff --git a/src/bin/wast.rs b/src/bin/wast.rs new file mode 100644 index 0000000000..1407a7eb67 --- /dev/null +++ b/src/bin/wast.rs @@ -0,0 +1,12 @@ +//! The `wast` command line tool. +//! +//! Runs WebAssembly test script files. +//! See `wast --help` for usage. + +use anyhow::Result; +use structopt::StructOpt; +use wasmtime_cli::commands::WastCommand; + +fn main() -> Result<()> { + WastCommand::from_args().execute() +} diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000000..e9891bab99 --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,8 @@ +//! The module for the Wasmtime CLI commands. + +mod config; +mod run; +mod wasm2obj; +mod wast; + +pub use self::{config::*, run::*, wasm2obj::*, wast::*}; diff --git a/src/commands/config.rs b/src/commands/config.rs new file mode 100644 index 0000000000..5507bf502c --- /dev/null +++ b/src/commands/config.rs @@ -0,0 +1,49 @@ +//! The module that implements the `wasmtime config` command. + +use anyhow::{anyhow, Result}; +use structopt::StructOpt; +use wasmtime_environ::cache_create_new_config; + +const CONFIG_NEW_AFTER_HELP: &str = + "If no file path is specified, the system configuration file path will be used."; + +/// Controls Wasmtime configuration settings +#[derive(StructOpt)] +#[structopt(name = "run")] +pub enum ConfigCommand { + /// Creates a new Wasmtime configuration file + #[structopt(after_help = CONFIG_NEW_AFTER_HELP)] + New(ConfigNewCommand), +} + +impl ConfigCommand { + /// Executes the command. + pub fn execute(&self) -> Result<()> { + match self { + Self::New(c) => c.execute(), + } + } +} + +/// Creates a new Wasmtime configuration file +#[derive(StructOpt)] +#[structopt(name = "new", after_help = CONFIG_NEW_AFTER_HELP)] +pub struct ConfigNewCommand { + /// The path of the new configuration file + #[structopt(index = 1, value_name = "FILE_PATH")] + path: Option, +} + +impl ConfigNewCommand { + /// Executes the command. + pub fn execute(&self) -> Result<()> { + let path = cache_create_new_config(self.path.as_ref()).map_err(|e| anyhow!(e))?; + + println!( + "Successfully created a new configuation file at '{}'.", + path.display() + ); + + Ok(()) + } +} diff --git a/src/commands/run.rs b/src/commands/run.rs new file mode 100644 index 0000000000..d5fa7ee0a8 --- /dev/null +++ b/src/commands/run.rs @@ -0,0 +1,332 @@ +//! The module that implements the `wasmtime run` command. + +use crate::{init_file_per_thread_logger, CommonOptions}; +use anyhow::{bail, Context as _, Result}; +use std::{ + ffi::{OsStr, OsString}, + fs::File, + path::{Component, Path, PathBuf}, +}; +use structopt::{clap::AppSettings, StructOpt}; +use wasi_common::preopen_dir; +use wasmtime::{Engine, Instance, Module, Store}; +use wasmtime_interface_types::ModuleData; +use wasmtime_wasi::{old::snapshot_0::Wasi as WasiSnapshot0, Wasi}; + +fn parse_module(s: &OsStr) -> Result { + // Do not accept wasmtime subcommand names as the module name + match s.to_str() { + Some("help") | Some("config") | Some("run") | Some("wasm2obj") | Some("wast") => { + Err("module name cannot be the same as a subcommand".into()) + } + _ => Ok(s.into()), + } +} + +fn parse_env_var(s: &str) -> Result<(String, String)> { + let parts: Vec<_> = s.splitn(2, '=').collect(); + if parts.len() != 2 { + bail!("must be of the form `key=value`"); + } + Ok((parts[0].to_owned(), parts[1].to_owned())) +} + +fn parse_map_dirs(s: &str) -> Result<(String, String)> { + let parts: Vec<&str> = s.split("::").collect(); + if parts.len() != 2 { + bail!("must contain exactly one double colon ('::')"); + } + Ok((parts[0].into(), parts[1].into())) +} + +/// Runs a WebAssembly module +#[derive(StructOpt)] +#[structopt(name = "run", setting = AppSettings::TrailingVarArg)] +pub struct RunCommand { + #[structopt(flatten)] + common: CommonOptions, + + /// Grant access to the given host directory + #[structopt(long = "dir", number_of_values = 1, value_name = "DIRECTORY")] + dirs: Vec, + + /// Pass an environment variable to the program + #[structopt(long = "env", number_of_values = 1, value_name = "NAME=VAL", parse(try_from_str = parse_env_var))] + vars: Vec<(String, String)>, + + /// The name of the function to run + #[structopt(long, value_name = "FUNCTION")] + invoke: Option, + + /// Grant access to a guest directory mapped as a host directory + #[structopt(long = "mapdir", number_of_values = 1, value_name = "GUEST_DIR::HOST_DIR", parse(try_from_str = parse_map_dirs))] + map_dirs: Vec<(String, String)>, + + /// The path of the WebAssembly module to run + #[structopt( + index = 1, + required = true, + value_name = "WASM_MODULE", + parse(try_from_os_str = parse_module), + )] + module: PathBuf, + + /// Load the given WebAssembly module before the main module + #[structopt( + long = "preload", + number_of_values = 1, + value_name = "MODULE_PATH", + parse(from_os_str) + )] + preloads: Vec, + + // NOTE: this must come last for trailing varargs + /// The arguments to pass to the module + #[structopt(value_name = "ARGS")] + module_args: Vec, +} + +impl RunCommand { + /// Executes the command. + pub fn execute(&self) -> Result<()> { + if self.common.debug { + pretty_env_logger::init(); + } else { + let prefix = "wasmtime.dbg."; + init_file_per_thread_logger(prefix); + } + + let config = self.common.config()?; + let engine = Engine::new(&config); + let store = Store::new(&engine); + + // Make wasi available by default. + let preopen_dirs = self.compute_preopen_dirs()?; + let argv = self.compute_argv(); + + let module_registry = ModuleRegistry::new(&store, &preopen_dirs, &argv, &self.vars)?; + + // Load the preload wasm modules. + for preload in self.preloads.iter() { + Self::instantiate_module(&store, &module_registry, preload) + .with_context(|| format!("failed to process preload at `{}`", preload.display()))?; + } + + // Load the main wasm module. + self.handle_module(&store, &module_registry) + .with_context(|| format!("failed to run main module `{}`", self.module.display()))?; + + Ok(()) + } + + fn compute_preopen_dirs(&self) -> Result> { + let mut preopen_dirs = Vec::new(); + + for dir in self.dirs.iter() { + preopen_dirs.push(( + dir.clone(), + preopen_dir(dir).with_context(|| format!("failed to open directory '{}'", dir))?, + )); + } + + for (guest, host) in self.map_dirs.iter() { + preopen_dirs.push(( + guest.clone(), + preopen_dir(host) + .with_context(|| format!("failed to open directory '{}'", host))?, + )); + } + + Ok(preopen_dirs) + } + + fn compute_argv(&self) -> Vec { + let mut result = Vec::new(); + + // Add argv[0], which is the program name. Only include the base name of the + // main wasm module, to avoid leaking path information. + result.push( + self.module + .components() + .next_back() + .map(Component::as_os_str) + .and_then(OsStr::to_str) + .unwrap_or("") + .to_owned(), + ); + + // Add the remaining arguments. + for arg in self.module_args.iter() { + result.push(arg.clone()); + } + + result + } + + fn instantiate_module( + store: &Store, + module_registry: &ModuleRegistry, + path: &Path, + ) -> Result<(Instance, Module, Vec)> { + // Read the wasm module binary either as `*.wat` or a raw binary + let data = wat::parse_file(path)?; + + let module = Module::new(store, &data)?; + + // Resolve import using module_registry. + let imports = module + .imports() + .iter() + .map(|i| { + let export = match i.module() { + "wasi_snapshot_preview1" => { + module_registry.wasi_snapshot_preview1.get_export(i.name()) + } + "wasi_unstable" => module_registry.wasi_unstable.get_export(i.name()), + other => bail!("import module `{}` was not found", other), + }; + match export { + Some(export) => Ok(export.clone().into()), + None => bail!( + "import `{}` was not found in module `{}`", + i.name(), + i.module() + ), + } + }) + .collect::, _>>()?; + + let instance = Instance::new(&module, &imports) + .context(format!("failed to instantiate {:?}", path))?; + + Ok((instance, module, data)) + } + + fn handle_module(&self, store: &Store, module_registry: &ModuleRegistry) -> Result<()> { + let (instance, module, data) = + Self::instantiate_module(store, module_registry, &self.module)?; + + // If a function to invoke was given, invoke it. + if let Some(name) = self.invoke.as_ref() { + let data = ModuleData::new(&data)?; + self.invoke_export(instance, &data, name)?; + } else if module + .exports() + .iter() + .any(|export| export.name().is_empty()) + { + // Launch the default command export. + let data = ModuleData::new(&data)?; + self.invoke_export(instance, &data, "")?; + } else { + // If the module doesn't have a default command export, launch the + // _start function if one is present, as a compatibility measure. + let data = ModuleData::new(&data)?; + self.invoke_export(instance, &data, "_start")?; + } + + Ok(()) + } + + fn invoke_export(&self, instance: Instance, data: &ModuleData, name: &str) -> Result<()> { + use wasm_webidl_bindings::ast; + use wasmtime_interface_types::Value; + + let mut handle = instance.handle().clone(); + + // Use the binding information in `ModuleData` to figure out what arguments + // need to be passed to the function that we're invoking. Currently we take + // the CLI parameters and attempt to parse them into function arguments for + // the function we'll invoke. + let binding = data.binding_for_export(&mut handle, name)?; + if !binding.param_types()?.is_empty() { + eprintln!( + "warning: using `--invoke` with a function that takes arguments \ + is experimental and may break in the future" + ); + } + let mut args = self.module_args.iter(); + let mut values = Vec::new(); + for ty in binding.param_types()? { + let val = match args.next() { + Some(s) => s, + None => bail!("not enough arguments for `{}`", name), + }; + values.push(match ty { + // TODO: integer parsing here should handle hexadecimal notation + // like `0x0...`, but the Rust standard library currently only + // parses base-10 representations. + ast::WebidlScalarType::Long => Value::I32(val.parse()?), + ast::WebidlScalarType::LongLong => Value::I64(val.parse()?), + ast::WebidlScalarType::UnsignedLong => Value::U32(val.parse()?), + ast::WebidlScalarType::UnsignedLongLong => Value::U64(val.parse()?), + + ast::WebidlScalarType::Float | ast::WebidlScalarType::UnrestrictedFloat => { + Value::F32(val.parse()?) + } + ast::WebidlScalarType::Double | ast::WebidlScalarType::UnrestrictedDouble => { + Value::F64(val.parse()?) + } + ast::WebidlScalarType::DomString => Value::String(val.to_string()), + t => bail!("unsupported argument type {:?}", t), + }); + } + + // Invoke the function and then afterwards print all the results that came + // out, if there are any. + let results = data + .invoke_export(&instance, name, &values) + .with_context(|| format!("failed to invoke `{}`", name))?; + if !results.is_empty() { + eprintln!( + "warning: using `--invoke` with a function that returns values \ + is experimental and may break in the future" + ); + } + for result in results { + println!("{}", result); + } + + Ok(()) + } +} + +struct ModuleRegistry { + wasi_snapshot_preview1: Wasi, + wasi_unstable: WasiSnapshot0, +} + +impl ModuleRegistry { + fn new( + store: &Store, + preopen_dirs: &[(String, File)], + argv: &[String], + vars: &[(String, String)], + ) -> Result { + let mut cx1 = wasi_common::WasiCtxBuilder::new(); + + cx1.inherit_stdio().args(argv).envs(vars); + + for (name, file) in preopen_dirs { + cx1.preopened_dir(file.try_clone()?, name); + } + + let cx1 = cx1.build()?; + + let mut cx2 = wasi_common::old::snapshot_0::WasiCtxBuilder::new() + .inherit_stdio() + .args(argv) + .envs(vars); + + for (name, file) in preopen_dirs { + cx2 = cx2.preopened_dir(file.try_clone()?, name); + } + + let cx2 = cx2.build()?; + + Ok(ModuleRegistry { + wasi_snapshot_preview1: Wasi::new(store, cx1), + wasi_unstable: WasiSnapshot0::new(store, cx2), + }) + } +} diff --git a/src/commands/wasm2obj.rs b/src/commands/wasm2obj.rs new file mode 100644 index 0000000000..494857b2a2 --- /dev/null +++ b/src/commands/wasm2obj.rs @@ -0,0 +1,90 @@ +//! The module that implements the `wasmtime wasm2obj` command. + +use crate::obj::compile_to_obj; +use crate::{init_file_per_thread_logger, pick_compilation_strategy, CommonOptions}; +use anyhow::{anyhow, Context as _, Result}; +use std::{ + fs::File, + path::{Path, PathBuf}, + str::FromStr, +}; +use structopt::{clap::AppSettings, StructOpt}; +use target_lexicon::Triple; +use wasmtime_environ::CacheConfig; +#[cfg(feature = "lightbeam")] +use wasmtime_environ::Lightbeam; + +/// The after help text for the `wasm2obj` command. +pub const WASM2OBJ_AFTER_HELP: &str = "The translation is dependent on the environment chosen.\n\ + The default is a dummy environment that produces placeholder values."; + +fn parse_target(s: &str) -> Result { + Triple::from_str(&s).map_err(|e| anyhow!(e)) +} + +/// Translates a WebAssembly module to native object file +#[derive(StructOpt)] +#[structopt( + name = "wasm2obj", + version = env!("CARGO_PKG_VERSION"), + setting = AppSettings::ColoredHelp, + after_help = WASM2OBJ_AFTER_HELP, +)] +pub struct WasmToObjCommand { + #[structopt(flatten)] + common: CommonOptions, + + /// The path of the WebAssembly module to translate + #[structopt(index = 1, value_name = "MODULE_PATH", parse(from_os_str))] + module: PathBuf, + + /// The path of the output object file + #[structopt(index = 2, value_name = "OUTPUT_PATH")] + output: String, + + /// The target triple; default is the host triple + #[structopt(long, value_name = "TARGET", parse(try_from_str = parse_target))] + target: Option, +} + +impl WasmToObjCommand { + /// Executes the command. + pub fn execute(&self) -> Result<()> { + self.handle_module() + } + + fn handle_module(&self) -> Result<()> { + if self.common.debug { + pretty_env_logger::init(); + } else { + let prefix = "wasm2obj.dbg."; + init_file_per_thread_logger(prefix); + } + + let cache_config = if self.common.disable_cache { + CacheConfig::new_cache_disabled() + } else { + CacheConfig::from_file(self.common.config.as_deref())? + }; + let strategy = pick_compilation_strategy(self.common.cranelift, self.common.lightbeam)?; + + let data = wat::parse_file(&self.module).context("failed to parse module")?; + + let obj = compile_to_obj( + &data, + self.target.as_ref(), + strategy, + self.common.enable_simd, + self.common.opt_level(), + self.common.debug_info, + self.output.clone(), + &cache_config, + )?; + + // FIXME: Make the format a parameter. + let file = File::create(Path::new(&self.output)).context("failed to create object file")?; + obj.write(file).context("failed to write object file")?; + + Ok(()) + } +} diff --git a/src/commands/wast.rs b/src/commands/wast.rs new file mode 100644 index 0000000000..4ac459319a --- /dev/null +++ b/src/commands/wast.rs @@ -0,0 +1,52 @@ +//! The module that implements the `wasmtime wast` command. + +use crate::{init_file_per_thread_logger, CommonOptions}; +use anyhow::{Context as _, Result}; +use std::path::PathBuf; +use structopt::{clap::AppSettings, StructOpt}; +use wasmtime::{Engine, Store}; +use wasmtime_wast::WastContext; + +/// Runs a WebAssembly test script file +#[derive(StructOpt)] +#[structopt( + name = "wast", + version = env!("CARGO_PKG_VERSION"), + setting = AppSettings::ColoredHelp, +)] +pub struct WastCommand { + #[structopt(flatten)] + common: CommonOptions, + + /// The path of the WebAssembly test script to run + #[structopt(required = true, value_name = "SCRIPT_FILE", parse(from_os_str))] + scripts: Vec, +} + +impl WastCommand { + /// Executes the command. + pub fn execute(&self) -> Result<()> { + if self.common.debug { + pretty_env_logger::init(); + } else { + let prefix = "wast.dbg."; + init_file_per_thread_logger(prefix); + } + + let config = self.common.config()?; + let store = Store::new(&Engine::new(&config)); + let mut wast_context = WastContext::new(store); + + wast_context + .register_spectest() + .expect("error instantiating \"spectest\""); + + for script in self.scripts.iter() { + wast_context + .run_file(script) + .with_context(|| format!("failed to run script file '{}'", script.display()))? + } + + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000000..dc9b1bfca2 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,195 @@ +//! The Wasmtime command line interface (CLI) crate. +//! +//! This crate implements the Wasmtime command line tools. + +#![deny( + missing_docs, + trivial_numeric_casts, + unused_extern_crates, + unstable_features +)] +#![warn(unused_import_braces)] +#![cfg_attr(feature = "clippy", plugin(clippy(conf_file = "../clippy.toml")))] +#![cfg_attr(feature = "cargo-clippy", allow(clippy::new_without_default))] +#![cfg_attr( + feature = "cargo-clippy", + warn( + clippy::float_arithmetic, + clippy::mut_mut, + clippy::nonminimal_bool, + clippy::option_map_unwrap_or, + clippy::option_map_unwrap_or_else, + clippy::unicode_not_nfc, + clippy::use_self + ) +)] + +pub mod commands; +mod obj; + +use anyhow::{bail, Result}; +use std::path::PathBuf; +use structopt::StructOpt; +use wasmtime::{Config, Strategy}; +use wasmtime_profiling::ProfilingStrategy; + +pub use obj::compile_to_obj; + +fn pick_compilation_strategy(cranelift: bool, lightbeam: bool) -> Result { + Ok(match (lightbeam, cranelift) { + (true, false) => Strategy::Lightbeam, + (false, true) => Strategy::Cranelift, + (false, false) => Strategy::Auto, + (true, true) => bail!("Can't enable --cranelift and --lightbeam at the same time"), + }) +} + +fn pick_profiling_strategy(jitdump: bool) -> Result { + Ok(match jitdump { + true => ProfilingStrategy::JitDumpProfiler, + false => ProfilingStrategy::NullProfiler, + }) +} + +fn init_file_per_thread_logger(prefix: &'static str) { + file_per_thread_logger::initialize(prefix); + + // Extending behavior of default spawner: + // https://docs.rs/rayon/1.1.0/rayon/struct.ThreadPoolBuilder.html#method.spawn_handler + // Source code says DefaultSpawner is implementation detail and + // shouldn't be used directly. + rayon::ThreadPoolBuilder::new() + .spawn_handler(move |thread| { + let mut b = std::thread::Builder::new(); + if let Some(name) = thread.name() { + b = b.name(name.to_owned()); + } + if let Some(stack_size) = thread.stack_size() { + b = b.stack_size(stack_size); + } + b.spawn(move || { + file_per_thread_logger::initialize(prefix); + thread.run() + })?; + Ok(()) + }) + .build_global() + .unwrap(); +} + +/// Common options for commands that translate WebAssembly modules +#[derive(StructOpt)] +struct CommonOptions { + /// Use specified configuration file + #[structopt(long, parse(from_os_str), value_name = "CONFIG_PATH")] + config: Option, + + /// Use Cranelift for all compilation + #[structopt(long, conflicts_with = "lightbeam")] + cranelift: bool, + + /// Enable debug output + #[structopt(short, long)] + debug: bool, + + /// Generate debug information + #[structopt(short = "g")] + debug_info: bool, + + /// Disable cache system + #[structopt(long)] + disable_cache: bool, + + /// Enable support for proposed SIMD instructions + #[structopt(long)] + enable_simd: bool, + + /// Enable support for reference types + #[structopt(long)] + enable_reference_types: bool, + + /// Enable support for multi-value functions + #[structopt(long)] + enable_multi_value: bool, + + /// Enable support for Wasm threads + #[structopt(long)] + enable_threads: bool, + + /// Enable support for bulk memory instructions + #[structopt(long)] + enable_bulk_memory: bool, + + /// Enable all experimental Wasm features + #[structopt(long)] + enable_all: bool, + + /// Use Lightbeam for all compilation + #[structopt(long, conflicts_with = "cranelift")] + lightbeam: bool, + + /// Generate jitdump file (supported on --features=profiling build) + #[structopt(long)] + jitdump: bool, + + /// Run optimization passes on translated functions, on by default + #[structopt(short = "O", long)] + optimize: bool, + + /// Optimization level for generated functions (0 (none), 1, 2 (most), or s + /// (size)) + #[structopt( + long, + parse(try_from_str = parse_opt_level), + default_value = "2", + )] + opt_level: wasmtime::OptLevel, +} + +impl CommonOptions { + fn config(&self) -> Result { + let mut config = Config::new(); + config + .cranelift_debug_verifier(cfg!(debug_assertions)) + .debug_info(self.debug_info) + .wasm_bulk_memory(self.enable_bulk_memory || self.enable_all) + .wasm_simd(self.enable_simd || self.enable_all) + .wasm_reference_types(self.enable_reference_types || self.enable_all) + .wasm_multi_value(self.enable_multi_value || self.enable_all) + .wasm_threads(self.enable_threads || self.enable_all) + .cranelift_opt_level(self.opt_level()) + .strategy(pick_compilation_strategy(self.cranelift, self.lightbeam)?)? + .profiler(pick_profiling_strategy(self.jitdump)?)?; + if !self.disable_cache { + match &self.config { + Some(path) => { + config.cache_config_load(path)?; + } + None => { + config.cache_config_load_default()?; + } + } + } + Ok(config) + } + + fn opt_level(&self) -> wasmtime::OptLevel { + match (self.optimize, self.opt_level.clone()) { + (true, _) => wasmtime::OptLevel::Speed, + (false, other) => other, + } + } +} + +fn parse_opt_level(opt_level: &str) -> Result { + match opt_level { + "s" => Ok(wasmtime::OptLevel::SpeedAndSize), + "0" => Ok(wasmtime::OptLevel::None), + "1" => Ok(wasmtime::OptLevel::Speed), + "2" => Ok(wasmtime::OptLevel::Speed), + other => bail!( + "unknown optimization level `{}`, only 0,1,2,s accepted", + other + ), + } +} diff --git a/src/obj.rs b/src/obj.rs new file mode 100644 index 0000000000..95ba2ce05d --- /dev/null +++ b/src/obj.rs @@ -0,0 +1,151 @@ +use anyhow::{anyhow, bail, Context as _, Result}; +use faerie::Artifact; +use target_lexicon::Triple; +use wasmtime::Strategy; +use wasmtime_debug::{emit_debugsections, read_debuginfo}; +#[cfg(feature = "lightbeam")] +use wasmtime_environ::Lightbeam; +use wasmtime_environ::{ + entity::EntityRef, settings, settings::Configurable, wasm::DefinedMemoryIndex, + wasm::MemoryIndex, CacheConfig, Compiler, Cranelift, ModuleEnvironment, ModuleMemoryOffset, + ModuleVmctxInfo, Tunables, VMOffsets, +}; +use wasmtime_jit::native; +use wasmtime_obj::emit_module; + +/// Creates object file from binary wasm data. +pub fn compile_to_obj( + wasm: &[u8], + target: Option<&Triple>, + strategy: Strategy, + enable_simd: bool, + opt_level: wasmtime::OptLevel, + debug_info: bool, + artifact_name: String, + cache_config: &CacheConfig, +) -> Result { + let isa_builder = match target { + Some(target) => native::lookup(target.clone())?, + None => native::builder(), + }; + let mut flag_builder = settings::builder(); + + // There are two possible traps for division, and this way + // we get the proper one if code traps. + flag_builder.enable("avoid_div_traps").unwrap(); + + if enable_simd { + flag_builder.enable("enable_simd").unwrap(); + } + + match opt_level { + wasmtime::OptLevel::None => {} + wasmtime::OptLevel::Speed => { + flag_builder.set("opt_level", "speed").unwrap(); + } + wasmtime::OptLevel::SpeedAndSize => { + flag_builder.set("opt_level", "speed_and_size").unwrap(); + } + other => bail!("unknown optimization level {:?}", other), + } + + let isa = isa_builder.finish(settings::Flags::new(flag_builder)); + + let mut obj = Artifact::new(isa.triple().clone(), artifact_name); + + // TODO: Expose the tunables as command-line flags. + let tunables = Tunables::default(); + + let ( + module, + module_translation, + lazy_function_body_inputs, + lazy_data_initializers, + target_config, + ) = { + let environ = ModuleEnvironment::new(isa.frontend_config(), tunables); + + let translation = environ + .translate(wasm) + .context("failed to translate module")?; + + ( + translation.module, + translation.module_translation.unwrap(), + translation.function_body_inputs, + translation.data_initializers, + translation.target_config, + ) + }; + + // TODO: use the traps information + let (compilation, relocations, address_transform, value_ranges, stack_slots, _traps) = + match strategy { + Strategy::Auto | Strategy::Cranelift => Cranelift::compile_module( + &module, + &module_translation, + lazy_function_body_inputs, + &*isa, + debug_info, + cache_config, + ), + #[cfg(feature = "lightbeam")] + Strategy::Lightbeam => Lightbeam::compile_module( + &module, + &module_translation, + lazy_function_body_inputs, + &*isa, + debug_info, + cache_config, + ), + #[cfg(not(feature = "lightbeam"))] + Strategy::Lightbeam => bail!("lightbeam support not enabled"), + other => bail!("unsupported compilation strategy {:?}", other), + } + .context("failed to compile module")?; + + if compilation.is_empty() { + bail!("no functions were found/compiled"); + } + + let module_vmctx_info = { + let ofs = VMOffsets::new(target_config.pointer_bytes(), &module.local); + ModuleVmctxInfo { + memory_offset: if ofs.num_imported_memories > 0 { + ModuleMemoryOffset::Imported(ofs.vmctx_vmmemory_import(MemoryIndex::new(0))) + } else if ofs.num_defined_memories > 0 { + ModuleMemoryOffset::Defined( + ofs.vmctx_vmmemory_definition_base(DefinedMemoryIndex::new(0)), + ) + } else { + ModuleMemoryOffset::None + }, + stack_slots, + } + }; + + emit_module( + &mut obj, + &module, + &compilation, + &relocations, + &lazy_data_initializers, + &target_config, + ) + .map_err(|e| anyhow!(e)) + .context("failed to emit module")?; + + if debug_info { + let debug_data = read_debuginfo(wasm); + emit_debugsections( + &mut obj, + &module_vmctx_info, + target_config, + &debug_data, + &address_transform, + &value_ranges, + ) + .context("failed to emit debug sections")?; + } + Ok(obj) +} diff --git a/tests/cli_tests.rs b/tests/cli_tests.rs new file mode 100644 index 0000000000..f2b194a647 --- /dev/null +++ b/tests/cli_tests.rs @@ -0,0 +1,66 @@ +use anyhow::{bail, Result}; +use std::env; +use std::io::Write; +use std::path::Path; +use std::process::{Command, Stdio}; +use tempfile::NamedTempFile; + +fn run_wasmtime(args: &[&str]) -> Result<()> { + let cargo = env::var("CARGO").unwrap_or("cargo".to_string()); + let pkg_dir = env!("CARGO_MANIFEST_DIR"); + let success = Command::new(cargo) + .current_dir(pkg_dir) + .stdout(Stdio::null()) + .args(&["run", "-q", "--"]) + .args(args) + .status()? + .success(); + if !success { + bail!("Failed to execute wasmtime with: {:?}", args); + } + Ok(()) +} + +fn build_wasm(wat_path: impl AsRef) -> Result { + let mut wasm_file = NamedTempFile::new()?; + let wasm = wat::parse_file(wat_path)?; + wasm_file.write(&wasm)?; + Ok(wasm_file) +} + +// Very basic use case: compile binary wasm file and run specific function with arguments. +#[test] +fn run_wasmtime_simple() -> Result<()> { + let wasm = build_wasm("tests/wasm/simple.wat")?; + run_wasmtime(&[ + "run", + wasm.path().to_str().unwrap(), + "--invoke", + "simple", + "4", + ]) +} + +// Wasmtime shakk when not enough arguments were provided. +#[test] +fn run_wasmtime_simple_fail_no_args() -> Result<()> { + let wasm = build_wasm("tests/wasm/simple.wat")?; + assert!( + run_wasmtime(&["run", wasm.path().to_str().unwrap(), "--invoke", "simple"]).is_err(), + "shall fail" + ); + Ok(()) +} + +// Running simple wat +#[test] +fn run_wasmtime_simple_wat() -> Result<()> { + let wasm = build_wasm("tests/wasm/simple.wat")?; + run_wasmtime(&[ + "run", + wasm.path().to_str().unwrap(), + "--invoke", + "simple", + "4", + ]) +} diff --git a/tests/custom_signal_handler.rs b/tests/custom_signal_handler.rs new file mode 100644 index 0000000000..85f9af3651 --- /dev/null +++ b/tests/custom_signal_handler.rs @@ -0,0 +1,280 @@ +#[cfg(not(target_os = "windows"))] +mod tests { + use anyhow::Result; + use std::rc::Rc; + use std::sync::atomic::{AtomicBool, Ordering}; + use wasmtime::unix::InstanceExt; + use wasmtime::*; + + const WAT1: &str = r#" +(module + (func $read (export "read") (result i32) + (i32.load (i32.const 0)) + ) + (func $read_out_of_bounds (export "read_out_of_bounds") (result i32) + (i32.load + (i32.mul + ;; memory size in Wasm pages + (memory.size) + ;; Wasm page size + (i32.const 65536) + ) + ) + ) + (func $start + (i32.store (i32.const 0) (i32.const 123)) + ) + (start $start) + (memory (export "memory") 1 4) +) +"#; + + const WAT2: &str = r#" +(module + (import "other_module" "read" (func $other_module.read (result i32))) + (func $run (export "run") (result i32) + call $other_module.read) +) +"#; + + fn invoke_export(instance: &Instance, func_name: &str) -> Result, Trap> { + let ret = instance + .get_export(func_name) + .unwrap() + .func() + .unwrap() + .call(&[])?; + Ok(ret) + } + + // Locate "memory" export, get base address and size and set memory protection to PROT_NONE + fn set_up_memory(instance: &Instance) -> (*mut u8, usize) { + let mem_export = instance.get_export("memory").unwrap().memory().unwrap(); + let base = mem_export.data_ptr(); + let length = mem_export.data_size(); + + // So we can later trigger SIGSEGV by performing a read + unsafe { + libc::mprotect(base as *mut libc::c_void, length, libc::PROT_NONE); + } + + println!("memory: base={:?}, length={}", base, length); + + (base, length) + } + + fn handle_sigsegv( + base: *mut u8, + length: usize, + signum: libc::c_int, + siginfo: *const libc::siginfo_t, + ) -> bool { + println!("Hello from instance signal handler!"); + // SIGSEGV on Linux, SIGBUS on Mac + if libc::SIGSEGV == signum || libc::SIGBUS == signum { + let si_addr: *mut libc::c_void = unsafe { (*siginfo).si_addr() }; + // Any signal from within module's memory we handle ourselves + let result = (si_addr as u64) < (base as u64) + (length as u64); + // Remove protections so the execution may resume + unsafe { + libc::mprotect( + base as *mut libc::c_void, + length, + libc::PROT_READ | libc::PROT_WRITE, + ); + } + println!("signal handled: {}", result); + result + } else { + // Otherwise, we forward to wasmtime's signal handler. + false + } + } + + #[test] + fn test_custom_signal_handler_single_instance() -> Result<()> { + let engine = Engine::new(&Config::default()); + let store = Store::new(&engine); + let module = Module::new(&store, WAT1)?; + let instance = Instance::new(&module, &[])?; + + let (base, length) = set_up_memory(&instance); + unsafe { + instance.set_signal_handler(move |signum, siginfo, _| { + handle_sigsegv(base, length, signum, siginfo) + }); + } + + let exports = instance.exports(); + assert!(!exports.is_empty()); + + // these invoke wasmtime_call_trampoline from action.rs + { + println!("calling read..."); + let result = invoke_export(&instance, "read").expect("read succeeded"); + assert_eq!(123, result[0].unwrap_i32()); + } + + { + println!("calling read_out_of_bounds..."); + let trap = invoke_export(&instance, "read_out_of_bounds").unwrap_err(); + assert!( + trap.message() + .starts_with("wasm trap: out of bounds memory access"), + "bad trap message: {:?}", + trap.message() + ); + } + + // these invoke wasmtime_call_trampoline from callable.rs + { + let read_func = exports[0] + .func() + .expect("expected a 'read' func in the module"); + println!("calling read..."); + let result = read_func.call(&[]).expect("expected function not to trap"); + assert_eq!(123i32, result[0].clone().unwrap_i32()); + } + + { + let read_out_of_bounds_func = exports[1] + .func() + .expect("expected a 'read_out_of_bounds' func in the module"); + println!("calling read_out_of_bounds..."); + let trap = read_out_of_bounds_func.call(&[]).unwrap_err(); + assert!(trap + .message() + .starts_with("wasm trap: out of bounds memory access")); + } + Ok(()) + } + + #[test] + fn test_custom_signal_handler_multiple_instances() -> Result<()> { + let engine = Engine::new(&Config::default()); + let store = Store::new(&engine); + let module = Module::new(&store, WAT1)?; + + // Set up multiple instances + + let instance1 = Instance::new(&module, &[])?; + let instance1_handler_triggered = Rc::new(AtomicBool::new(false)); + + unsafe { + let (base1, length1) = set_up_memory(&instance1); + + instance1.set_signal_handler({ + let instance1_handler_triggered = instance1_handler_triggered.clone(); + move |_signum, _siginfo, _context| { + // Remove protections so the execution may resume + libc::mprotect( + base1 as *mut libc::c_void, + length1, + libc::PROT_READ | libc::PROT_WRITE, + ); + instance1_handler_triggered.store(true, Ordering::SeqCst); + println!( + "Hello from instance1 signal handler! {}", + instance1_handler_triggered.load(Ordering::SeqCst) + ); + true + } + }); + } + + let instance2 = Instance::new(&module, &[]).expect("failed to instantiate module"); + let instance2_handler_triggered = Rc::new(AtomicBool::new(false)); + + unsafe { + let (base2, length2) = set_up_memory(&instance2); + + instance2.set_signal_handler({ + let instance2_handler_triggered = instance2_handler_triggered.clone(); + move |_signum, _siginfo, _context| { + // Remove protections so the execution may resume + libc::mprotect( + base2 as *mut libc::c_void, + length2, + libc::PROT_READ | libc::PROT_WRITE, + ); + instance2_handler_triggered.store(true, Ordering::SeqCst); + println!( + "Hello from instance2 signal handler! {}", + instance2_handler_triggered.load(Ordering::SeqCst) + ); + true + } + }); + } + + // Invoke both instances and trigger both signal handlers + + // First instance1 + { + let exports1 = instance1.exports(); + assert!(!exports1.is_empty()); + + println!("calling instance1.read..."); + let result = invoke_export(&instance1, "read").expect("read succeeded"); + assert_eq!(123, result[0].unwrap_i32()); + assert_eq!( + instance1_handler_triggered.load(Ordering::SeqCst), + true, + "instance1 signal handler has been triggered" + ); + } + + // And then instance2 + { + let exports2 = instance2.exports(); + assert!(!exports2.is_empty()); + + println!("calling instance2.read..."); + let result = invoke_export(&instance2, "read").expect("read succeeded"); + assert_eq!(123, result[0].unwrap_i32()); + assert_eq!( + instance2_handler_triggered.load(Ordering::SeqCst), + true, + "instance1 signal handler has been triggered" + ); + } + Ok(()) + } + + #[test] + fn test_custom_signal_handler_instance_calling_another_instance() -> Result<()> { + let engine = Engine::new(&Config::default()); + let store = Store::new(&engine); + + // instance1 which defines 'read' + let module1 = Module::new(&store, WAT1)?; + let instance1 = Instance::new(&module1, &[])?; + let (base1, length1) = set_up_memory(&instance1); + unsafe { + instance1.set_signal_handler(move |signum, siginfo, _| { + println!("instance1"); + handle_sigsegv(base1, length1, signum, siginfo) + }); + } + + let instance1_exports = instance1.exports(); + assert!(!instance1_exports.is_empty()); + let instance1_read = instance1_exports[0].clone(); + + // instance2 wich calls 'instance1.read' + let module2 = Module::new(&store, WAT2)?; + let instance2 = Instance::new(&module2, &[instance1_read])?; + // since 'instance2.run' calls 'instance1.read' we need to set up the signal handler to handle + // SIGSEGV originating from within the memory of instance1 + unsafe { + instance2.set_signal_handler(move |signum, siginfo, _| { + handle_sigsegv(base1, length1, signum, siginfo) + }); + } + + println!("calling instance2.run"); + let result = invoke_export(&instance2, "run")?; + assert_eq!(123, result[0].unwrap_i32()); + Ok(()) + } +} diff --git a/tests/debug/dump.rs b/tests/debug/dump.rs new file mode 100644 index 0000000000..a7ea8d2653 --- /dev/null +++ b/tests/debug/dump.rs @@ -0,0 +1,30 @@ +use anyhow::{bail, Result}; +use std::env; +use std::process::Command; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(dead_code)] +pub enum DwarfDumpSection { + DebugInfo, + DebugLine, +} + +pub fn get_dwarfdump(obj: &str, section: DwarfDumpSection) -> Result { + let dwarfdump = env::var("DWARFDUMP").unwrap_or("llvm-dwarfdump".to_string()); + let section_flag = match section { + DwarfDumpSection::DebugInfo => "-debug-info", + DwarfDumpSection::DebugLine => "-debug-line", + }; + let output = Command::new(&dwarfdump) + .args(&[section_flag, obj]) + .output() + .expect("success"); + if !output.status.success() { + bail!( + "failed to execute {}: {}", + dwarfdump, + String::from_utf8_lossy(&output.stderr), + ); + } + Ok(String::from_utf8_lossy(&output.stdout).to_string()) +} diff --git a/tests/debug/main.rs b/tests/debug/main.rs new file mode 100644 index 0000000000..2365f72022 --- /dev/null +++ b/tests/debug/main.rs @@ -0,0 +1,4 @@ +mod dump; +mod obj; +mod simulate; +mod translate; diff --git a/tests/debug/obj.rs b/tests/debug/obj.rs new file mode 100644 index 0000000000..a800373514 --- /dev/null +++ b/tests/debug/obj.rs @@ -0,0 +1,34 @@ +use anyhow::{Context as _, Result}; +use std::fs::File; +use std::path::Path; +use target_lexicon::Triple; +use wasmtime::Strategy; +use wasmtime_cli::compile_to_obj; +use wasmtime_environ::CacheConfig; + +pub fn compile_cranelift( + wasm: &[u8], + target: Option, + output: impl AsRef, +) -> Result<()> { + let obj = compile_to_obj( + wasm, + target.as_ref(), + Strategy::Cranelift, + false, + wasmtime::OptLevel::None, + true, + output + .as_ref() + .file_name() + .unwrap() + .to_string_lossy() + .to_string(), + &CacheConfig::new_cache_disabled(), + )?; + + let file = File::create(output).context("failed to create object file")?; + obj.write(file).context("failed to write object file")?; + + Ok(()) +} diff --git a/tests/debug/simulate.rs b/tests/debug/simulate.rs new file mode 100644 index 0000000000..e5da6a9be5 --- /dev/null +++ b/tests/debug/simulate.rs @@ -0,0 +1,55 @@ +use super::dump::{get_dwarfdump, DwarfDumpSection}; +use super::obj::compile_cranelift; +use anyhow::{format_err, Result}; +use filecheck::{CheckerBuilder, NO_VARIABLES}; +use tempfile::NamedTempFile; +use wat::parse_str; + +#[allow(dead_code)] +fn check_wat(wat: &str) -> Result<()> { + let wasm = parse_str(wat)?; + let obj_file = NamedTempFile::new()?; + let obj_path = obj_file.path().to_str().unwrap(); + compile_cranelift(&wasm, None, obj_path)?; + let dump = get_dwarfdump(obj_path, DwarfDumpSection::DebugInfo)?; + let mut builder = CheckerBuilder::new(); + builder + .text(wat) + .map_err(|e| format_err!("unable to build checker: {:?}", e))?; + let checker = builder.finish(); + let check = checker + .explain(&dump, NO_VARIABLES) + .map_err(|e| format_err!("{:?}", e))?; + assert!(check.0, "didn't pass check {}", check.1); + Ok(()) +} + +#[test] +#[cfg(all( + any(target_os = "linux", target_os = "macos"), + target_pointer_width = "64" +))] +fn test_debug_dwarf_simulate_simple_x86_64() -> Result<()> { + check_wat( + r#" +;; check: DW_TAG_compile_unit +(module +;; check: DW_TAG_subprogram +;; check: DW_AT_name ("wasm-function[0]") +;; check: DW_TAG_formal_parameter +;; check: DW_AT_name ("var0") +;; check: DW_AT_type +;; sameln: "i32" +;; check: DW_TAG_variable +;; check: DW_AT_name ("var1") +;; check: DW_AT_type +;; sameln: "i32" + (func (param i32) (result i32) + (local i32) + local.get 0 + local.set 1 + local.get 1 + ) +)"#, + ) +} diff --git a/tests/debug/testsuite/fib-wasm.c b/tests/debug/testsuite/fib-wasm.c new file mode 100644 index 0000000000..20c06f5efa --- /dev/null +++ b/tests/debug/testsuite/fib-wasm.c @@ -0,0 +1,13 @@ +// Compile with: +// clang --target=wasm32 fib-wasm.c -o fib-wasm.wasm -g \ +// -Wl,--no-entry,--export=fib -nostdlib -fdebug-prefix-map=$PWD=. + +int fib(int n) { + int i, t, a = 0, b = 1; + for (i = 0; i < n; i++) { + t = a; + a = b; + b += t; + } + return b; +} diff --git a/tests/debug/testsuite/fib-wasm.wasm b/tests/debug/testsuite/fib-wasm.wasm new file mode 100755 index 0000000000..0a1ebac429 Binary files /dev/null and b/tests/debug/testsuite/fib-wasm.wasm differ diff --git a/tests/debug/translate.rs b/tests/debug/translate.rs new file mode 100644 index 0000000000..0a16b2d427 --- /dev/null +++ b/tests/debug/translate.rs @@ -0,0 +1,56 @@ +use super::dump::{get_dwarfdump, DwarfDumpSection}; +use super::obj::compile_cranelift; +use anyhow::{format_err, Result}; +use filecheck::{CheckerBuilder, NO_VARIABLES}; +use std::fs::read; +use tempfile::NamedTempFile; + +#[allow(dead_code)] +fn check_wasm(wasm_path: &str, directives: &str) -> Result<()> { + let wasm = read(wasm_path)?; + let obj_file = NamedTempFile::new()?; + let obj_path = obj_file.path().to_str().unwrap(); + compile_cranelift(&wasm, None, obj_path)?; + let dump = get_dwarfdump(obj_path, DwarfDumpSection::DebugInfo)?; + let mut builder = CheckerBuilder::new(); + builder + .text(directives) + .map_err(|e| format_err!("unable to build checker: {:?}", e))?; + let checker = builder.finish(); + let check = checker + .explain(&dump, NO_VARIABLES) + .map_err(|e| format_err!("{:?}", e))?; + assert!(check.0, "didn't pass check {}", check.1); + Ok(()) +} + +#[test] +#[cfg(all( + any(target_os = "linux", target_os = "macos"), + target_pointer_width = "64" +))] +fn test_debug_dwarf_translate() -> Result<()> { + check_wasm( + "tests/debug/testsuite/fib-wasm.wasm", + r##" +check: DW_TAG_compile_unit +# We have "fib" function +check: DW_TAG_subprogram +check: DW_AT_name ("fib") +# Accepts one parameter +check: DW_TAG_formal_parameter +check: DW_AT_name ("n") +check: DW_AT_decl_line (5) +# Has four locals: i, t, a, b +check: DW_TAG_variable +check: DW_AT_name ("i") +check: DW_AT_decl_line (6) +check: DW_TAG_variable +check: DW_AT_name ("t") +check: DW_TAG_variable +check: DW_AT_name ("a") +check: DW_TAG_variable +check: DW_AT_name ("b") + "##, + ) +} diff --git a/tests/instantiate.rs b/tests/instantiate.rs new file mode 100644 index 0000000000..a72a2df878 --- /dev/null +++ b/tests/instantiate.rs @@ -0,0 +1,40 @@ +use more_asserts::assert_gt; +use std::path::PathBuf; +use wasmtime_environ::settings; +use wasmtime_environ::settings::Configurable; +use wasmtime_environ::CacheConfig; +use wasmtime_jit::{instantiate, native, CompilationStrategy, Compiler, NullResolver}; + +const PATH_MODULE_RS2WASM_ADD_FUNC: &str = r"tests/wat/rs2wasm-add-func.wat"; + +/// Simple test reading a wasm-file and translating to binary representation. +#[test] +fn test_environ_translate() { + let path = PathBuf::from(PATH_MODULE_RS2WASM_ADD_FUNC); + let data = wat::parse_file(path).expect("expecting valid wat-file"); + assert_gt!(data.len(), 0); + + let mut flag_builder = settings::builder(); + flag_builder.enable("enable_verifier").unwrap(); + + let isa_builder = native::builder(); + let isa = isa_builder.finish(settings::Flags::new(flag_builder)); + + let mut resolver = NullResolver {}; + let cache_config = CacheConfig::new_cache_disabled(); + let mut compiler = Compiler::new(isa, CompilationStrategy::Auto, cache_config); + unsafe { + let instance = instantiate( + &mut compiler, + &data, + &mut resolver, + // Bulk memory. + false, + // Debug info. + false, + // Profiler. + None, + ); + assert!(instance.is_ok()); + } +} diff --git a/tests/misc_testsuite/bulk-memory-operations/elem-ref-null.wast b/tests/misc_testsuite/bulk-memory-operations/elem-ref-null.wast new file mode 100644 index 0000000000..c904c332dd --- /dev/null +++ b/tests/misc_testsuite/bulk-memory-operations/elem-ref-null.wast @@ -0,0 +1,2 @@ +(module + (elem funcref (ref.null))) diff --git a/tests/misc_testsuite/bulk-memory-operations/elem_drop.wast b/tests/misc_testsuite/bulk-memory-operations/elem_drop.wast new file mode 100644 index 0000000000..a95804b594 --- /dev/null +++ b/tests/misc_testsuite/bulk-memory-operations/elem_drop.wast @@ -0,0 +1,7 @@ +(module + (table 1 1 funcref) + (elem (i32.const 0) funcref (ref.func 0)) + (func (export "elem.drop non-passive element") + (elem.drop 0))) + +(invoke "elem.drop non-passive element") diff --git a/tests/misc_testsuite/bulk-memory-operations/imported-memory-copy.wast b/tests/misc_testsuite/bulk-memory-operations/imported-memory-copy.wast new file mode 100644 index 0000000000..4787173cc7 --- /dev/null +++ b/tests/misc_testsuite/bulk-memory-operations/imported-memory-copy.wast @@ -0,0 +1,129 @@ +(module $foreign + (memory (export "mem") 1 1) + (data 0 (i32.const 1000) "hello") + (data 0 (i32.const 2000) "olleh")) + +(register "foreign" $foreign) + +(module + (memory (import "foreign" "mem") 1 1) + + (func $is_char (param i32 i32) (result i32) + local.get 0 + i32.load8_u + local.get 1 + i32.eq) + + (func (export "is hello?") (param i32) (result i32) + local.get 0 + i32.const 104 ;; 'h' + call $is_char + + local.get 0 + i32.const 1 + i32.add + i32.const 101 ;; 'e' + call $is_char + + local.get 0 + i32.const 2 + i32.add + i32.const 108 ;; 'l' + call $is_char + + local.get 0 + i32.const 3 + i32.add + i32.const 108 ;; 'l' + call $is_char + + local.get 0 + i32.const 4 + i32.add + i32.const 111 ;; 'o' + call $is_char + + i32.and + i32.and + i32.and + i32.and + ) + + (func (export "is olleh?") (param i32) (result i32) + local.get 0 + i32.const 111 ;; 'o' + call $is_char + + local.get 0 + i32.const 1 + i32.add + i32.const 108 ;; 'l' + call $is_char + + local.get 0 + i32.const 2 + i32.add + i32.const 108 ;; 'l' + call $is_char + + local.get 0 + i32.const 3 + i32.add + i32.const 101 ;; 'e' + call $is_char + + local.get 0 + i32.const 4 + i32.add + i32.const 104 ;; 'h' + call $is_char + + i32.and + i32.and + i32.and + i32.and + ) + + (func (export "memory.copy") (param i32 i32 i32) + local.get 0 + local.get 1 + local.get 2 + memory.copy)) + +;; Our memory has our initial data in the right places. +(assert_return + (invoke "is hello?" (i32.const 1000)) + (i32.const 1)) +(assert_return + (invoke "is olleh?" (i32.const 2000)) + (i32.const 1)) + +;; Non-overlapping memory copy with dst < src. +(invoke "memory.copy" (i32.const 500) (i32.const 1000) (i32.const 5)) +(assert_return + (invoke "is hello?" (i32.const 500)) + (i32.const 1)) + +;; Non-overlapping memory copy with dst > src. +(invoke "memory.copy" (i32.const 1500) (i32.const 1000) (i32.const 5)) +(assert_return + (invoke "is hello?" (i32.const 1500)) + (i32.const 1)) + +;; Overlapping memory copy with dst < src. +(invoke "memory.copy" (i32.const 1998) (i32.const 2000) (i32.const 5)) +(assert_return + (invoke "is olleh?" (i32.const 1998)) + (i32.const 1)) + +;; Overlapping memory copy with dst > src. +(invoke "memory.copy" (i32.const 2000) (i32.const 1998) (i32.const 5)) +(assert_return + (invoke "is olleh?" (i32.const 2000)) + (i32.const 1)) + +;; Overlapping memory copy with dst = src. +(invoke "memory.copy" (i32.const 2000) (i32.const 2000) (i32.const 5)) +(assert_return + (invoke "is olleh?" (i32.const 2000)) + (i32.const 1)) diff --git a/tests/misc_testsuite/bulk-memory-operations/memory-copy.wast b/tests/misc_testsuite/bulk-memory-operations/memory-copy.wast new file mode 100644 index 0000000000..d339264423 --- /dev/null +++ b/tests/misc_testsuite/bulk-memory-operations/memory-copy.wast @@ -0,0 +1,124 @@ +(module + (memory 1 1) + (data 0 (i32.const 1000) "hello") + (data 0 (i32.const 2000) "olleh") + + (func $is_char (param i32 i32) (result i32) + local.get 0 + i32.load8_u + local.get 1 + i32.eq) + + (func (export "is hello?") (param i32) (result i32) + local.get 0 + i32.const 104 ;; 'h' + call $is_char + + local.get 0 + i32.const 1 + i32.add + i32.const 101 ;; 'e' + call $is_char + + local.get 0 + i32.const 2 + i32.add + i32.const 108 ;; 'l' + call $is_char + + local.get 0 + i32.const 3 + i32.add + i32.const 108 ;; 'l' + call $is_char + + local.get 0 + i32.const 4 + i32.add + i32.const 111 ;; 'o' + call $is_char + + i32.and + i32.and + i32.and + i32.and + ) + + (func (export "is olleh?") (param i32) (result i32) + local.get 0 + i32.const 111 ;; 'o' + call $is_char + + local.get 0 + i32.const 1 + i32.add + i32.const 108 ;; 'l' + call $is_char + + local.get 0 + i32.const 2 + i32.add + i32.const 108 ;; 'l' + call $is_char + + local.get 0 + i32.const 3 + i32.add + i32.const 101 ;; 'e' + call $is_char + + local.get 0 + i32.const 4 + i32.add + i32.const 104 ;; 'h' + call $is_char + + i32.and + i32.and + i32.and + i32.and + ) + + (func (export "memory.copy") (param i32 i32 i32) + local.get 0 + local.get 1 + local.get 2 + memory.copy)) + +;; Our memory has our initial data in the right places. +(assert_return + (invoke "is hello?" (i32.const 1000)) + (i32.const 1)) +(assert_return + (invoke "is olleh?" (i32.const 2000)) + (i32.const 1)) + +;; Non-overlapping memory copy with dst < src. +(invoke "memory.copy" (i32.const 500) (i32.const 1000) (i32.const 5)) +(assert_return + (invoke "is hello?" (i32.const 500)) + (i32.const 1)) + +;; Non-overlapping memory copy with dst > src. +(invoke "memory.copy" (i32.const 1500) (i32.const 1000) (i32.const 5)) +(assert_return + (invoke "is hello?" (i32.const 1500)) + (i32.const 1)) + +;; Overlapping memory copy with dst < src. +(invoke "memory.copy" (i32.const 1998) (i32.const 2000) (i32.const 5)) +(assert_return + (invoke "is olleh?" (i32.const 1998)) + (i32.const 1)) + +;; Overlapping memory copy with dst > src. +(invoke "memory.copy" (i32.const 2000) (i32.const 1998) (i32.const 5)) +(assert_return + (invoke "is olleh?" (i32.const 2000)) + (i32.const 1)) + +;; Overlapping memory copy with dst = src. +(invoke "memory.copy" (i32.const 2000) (i32.const 2000) (i32.const 5)) +(assert_return + (invoke "is olleh?" (i32.const 2000)) + (i32.const 1)) diff --git a/tests/misc_testsuite/bulk-memory-operations/partial-init-memory-segment.wast b/tests/misc_testsuite/bulk-memory-operations/partial-init-memory-segment.wast new file mode 100644 index 0000000000..471e74a643 --- /dev/null +++ b/tests/misc_testsuite/bulk-memory-operations/partial-init-memory-segment.wast @@ -0,0 +1,35 @@ +(module $m + (memory (export "mem") 1) + + (func (export "load") (param i32) (result i32) + local.get 0 + i32.load8_u)) + +(register "m" $m) + +(assert_trap + (module + (memory (import "m" "mem") 1) + + ;; This is in bounds, and should get written to the memory. + (data (i32.const 0) "abc") + + ;; Partially out of bounds. None of these bytes should get written, and + ;; instantiation should trap. + (data (i32.const 65530) "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz") + ) + "out of bounds" +) + +;; The first data segment got written. +(assert_return (invoke $m "load" (i32.const 0)) (i32.const 97)) +(assert_return (invoke $m "load" (i32.const 1)) (i32.const 98)) +(assert_return (invoke $m "load" (i32.const 2)) (i32.const 99)) + +;; The second did not get partially written. +(assert_return (invoke $m "load" (i32.const 65530)) (i32.const 0)) +(assert_return (invoke $m "load" (i32.const 65531)) (i32.const 0)) +(assert_return (invoke $m "load" (i32.const 65532)) (i32.const 0)) +(assert_return (invoke $m "load" (i32.const 65533)) (i32.const 0)) +(assert_return (invoke $m "load" (i32.const 65534)) (i32.const 0)) +(assert_return (invoke $m "load" (i32.const 65535)) (i32.const 0)) diff --git a/tests/misc_testsuite/bulk-memory-operations/partial-init-table-segment.wast b/tests/misc_testsuite/bulk-memory-operations/partial-init-table-segment.wast new file mode 100644 index 0000000000..0086d4b542 --- /dev/null +++ b/tests/misc_testsuite/bulk-memory-operations/partial-init-table-segment.wast @@ -0,0 +1,35 @@ +(module $m + (table (export "table") funcref (elem $zero $zero $zero $zero $zero $zero $zero $zero $zero $zero)) + + (func $zero (result i32) + (i32.const 0)) + + (func (export "indirect-call") (param i32) (result i32) + local.get 0 + call_indirect (result i32))) + +(register "m" $m) + +(assert_trap + (module + (table (import "m" "table") 10 funcref) + + (func $one (result i32) + (i32.const 1)) + + ;; An in-bounds segment that should get initialized in the table. + (elem (i32.const 7) $one) + + ;; Part of this segment is out of bounds, so none of its elements should be + ;; initialized into the table, and it should trap. + (elem (i32.const 9) $one $one $one) + ) + "out of bounds" +) + +;; The first `$one` segment *was* initialized OK. +(assert_return (invoke "indirect-call" (i32.const 7)) (i32.const 1)) + +;; The second `$one` segment is partially out of bounds, and therefore none of +;; its elements were written into the table. +(assert_return (invoke "indirect-call" (i32.const 9)) (i32.const 0)) diff --git a/tests/misc_testsuite/bulk-memory-operations/table_copy.wast b/tests/misc_testsuite/bulk-memory-operations/table_copy.wast new file mode 100644 index 0000000000..f06258ea24 --- /dev/null +++ b/tests/misc_testsuite/bulk-memory-operations/table_copy.wast @@ -0,0 +1,63 @@ +(module + (func $f (param i32 i32 i32) (result i32) (local.get 0)) + (func $g (param i32 i32 i32) (result i32) (local.get 1)) + (func $h (param i32 i32 i32) (result i32) (local.get 2)) + + ;; Indices: 0 1 2 3 4 5 6 7 8 + (table funcref (elem $f $g $h $f $g $h $f $g $h)) + ;; After table.copy: $g $h $f + + (func (export "copy") (param i32 i32 i32) + local.get 0 + local.get 1 + local.get 2 + table.copy) + + (func (export "call") (param i32 i32 i32 i32) (result i32) + local.get 0 + local.get 1 + local.get 2 + local.get 3 + call_indirect (param i32 i32 i32) (result i32)) +) + +;; Call $f at 0 +(assert_return + (invoke "call" (i32.const 1) (i32.const 0) (i32.const 0) (i32.const 0)) + (i32.const 1)) + +;; Call $g at 1 +(assert_return + (invoke "call" (i32.const 0) (i32.const 1) (i32.const 0) (i32.const 1)) + (i32.const 1)) + +;; Call $h at 2 +(assert_return + (invoke "call" (i32.const 0) (i32.const 0) (i32.const 1) (i32.const 2)) + (i32.const 1)) + +;; Do a `table.copy` to rearrange the elements. Copy from 4..7 to 0..3. +(invoke "copy" (i32.const 0) (i32.const 4) (i32.const 3)) + +;; Call $g at 0 +(assert_return + (invoke "call" (i32.const 0) (i32.const 1) (i32.const 0) (i32.const 0)) + (i32.const 1)) + +;; Call $h at 1 +(assert_return + (invoke "call" (i32.const 0) (i32.const 0) (i32.const 1) (i32.const 1)) + (i32.const 1)) + +;; Call $f at 2 +(assert_return + (invoke "call" (i32.const 1) (i32.const 0) (i32.const 0) (i32.const 2)) + (i32.const 1)) + +;; Copying up to the end does not trap. +(invoke "copy" (i32.const 7) (i32.const 0) (i32.const 2)) + +;; Copying past the end traps. +(assert_trap + (invoke "copy" (i32.const 7) (i32.const 0) (i32.const 3)) + "undefined element") diff --git a/tests/misc_testsuite/call_indirect.wast b/tests/misc_testsuite/call_indirect.wast new file mode 100644 index 0000000000..aa242d6222 --- /dev/null +++ b/tests/misc_testsuite/call_indirect.wast @@ -0,0 +1,187 @@ +;; Test `call_indirect` calls between modules. + +(module + ;; Auxiliary definitions + (type $proc (func)) + (type $out-i32 (func (result i32))) + (type $out-i64 (func (result i64))) + (type $out-f32 (func (result f32))) + (type $out-f64 (func (result f64))) + (type $over-i32 (func (param i32) (result i32))) + (type $over-i64 (func (param i64) (result i64))) + (type $over-f32 (func (param f32) (result f32))) + (type $over-f64 (func (param f64) (result f64))) + (type $f32-i32 (func (param f32 i32) (result i32))) + (type $i32-i64 (func (param i32 i64) (result i64))) + (type $f64-f32 (func (param f64 f32) (result f32))) + (type $i64-f64 (func (param i64 f64) (result f64))) + (type $over-i32-duplicate (func (param i32) (result i32))) + (type $over-i64-duplicate (func (param i64) (result i64))) + (type $over-f32-duplicate (func (param f32) (result f32))) + (type $over-f64-duplicate (func (param f64) (result f64))) + + (func $const-i32 (type $out-i32) (i32.const 0x132)) + (func $const-i64 (type $out-i64) (i64.const 0x164)) + (func $const-f32 (type $out-f32) (f32.const 0xf32)) + (func $const-f64 (type $out-f64) (f64.const 0xf64)) + + (func $id-i32 (type $over-i32) (local.get 0)) + (func $id-i64 (type $over-i64) (local.get 0)) + (func $id-f32 (type $over-f32) (local.get 0)) + (func $id-f64 (type $over-f64) (local.get 0)) + + (func $i32-i64 (type $i32-i64) (local.get 1)) + (func $i64-f64 (type $i64-f64) (local.get 1)) + (func $f32-i32 (type $f32-i32) (local.get 1)) + (func $f64-f32 (type $f64-f32) (local.get 1)) + + (func $over-i32-duplicate (type $over-i32-duplicate) (local.get 0)) + (func $over-i64-duplicate (type $over-i64-duplicate) (local.get 0)) + (func $over-f32-duplicate (type $over-f32-duplicate) (local.get 0)) + (func $over-f64-duplicate (type $over-f64-duplicate) (local.get 0)) + + (table (export "table") funcref + (elem + $const-i32 $const-i64 $const-f32 $const-f64 + $id-i32 $id-i64 $id-f32 $id-f64 + $f32-i32 $i32-i64 $f64-f32 $i64-f64 + $over-i32-duplicate $over-i64-duplicate + $over-f32-duplicate $over-f64-duplicate + ) + ) + + ;; Typing + + (func (export "type-i32") (result i32) + (call_indirect (type $out-i32) (i32.const 0)) + ) + (func (export "type-i64") (result i64) + (call_indirect (type $out-i64) (i32.const 1)) + ) + (func (export "type-f32") (result f32) + (call_indirect (type $out-f32) (i32.const 2)) + ) + (func (export "type-f64") (result f64) + (call_indirect (type $out-f64) (i32.const 3)) + ) + + (func (export "type-index") (result i64) + (call_indirect (type $over-i64) (i64.const 100) (i32.const 5)) + ) + + (func (export "type-first-i32") (result i32) + (call_indirect (type $over-i32) (i32.const 32) (i32.const 4)) + ) + (func (export "type-first-i64") (result i64) + (call_indirect (type $over-i64) (i64.const 64) (i32.const 5)) + ) + (func (export "type-first-f32") (result f32) + (call_indirect (type $over-f32) (f32.const 1.32) (i32.const 6)) + ) + (func (export "type-first-f64") (result f64) + (call_indirect (type $over-f64) (f64.const 1.64) (i32.const 7)) + ) + + (func (export "type-second-i32") (result i32) + (call_indirect (type $f32-i32) (f32.const 32.1) (i32.const 32) (i32.const 8)) + ) + (func (export "type-second-i64") (result i64) + (call_indirect (type $i32-i64) (i32.const 32) (i64.const 64) (i32.const 9)) + ) + (func (export "type-second-f32") (result f32) + (call_indirect (type $f64-f32) (f64.const 64) (f32.const 32) (i32.const 10)) + ) + (func (export "type-second-f64") (result f64) + (call_indirect (type $i64-f64) (i64.const 64) (f64.const 64.1) (i32.const 11)) + ) +) + +(register "test") + +(module + ;; Auxiliary definitions. These are the same types as "test"'s, but in a + ;; different order, since call_indirect types are compared structurally. + (type $out-i32 (func (result i32))) + (type $out-i64 (func (result i64))) + (type $out-f32 (func (result f32))) + (type $out-f64 (func (result f64))) + (type $over-i32 (func (param i32) (result i32))) + (type $over-i64 (func (param i64) (result i64))) + (type $over-f32 (func (param f32) (result f32))) + (type $over-f64 (func (param f64) (result f64))) + (type $f32-i32 (func (param f32 i32) (result i32))) + (type $i32-i64 (func (param i32 i64) (result i64))) + (type $f64-f32 (func (param f64 f32) (result f32))) + (type $i64-f64 (func (param i64 f64) (result f64))) + (type $over-i32-duplicate (func (param i32) (result i32))) + (type $over-i64-duplicate (func (param i64) (result i64))) + (type $over-f32-duplicate (func (param f32) (result f32))) + (type $over-f64-duplicate (func (param f64) (result f64))) + (type $proc (func)) + + ;; Import the table from "test". + + (import "test" "table" (table 16 funcref)) + + ;; Typing + + (func (export "type-i32") (result i32) + (call_indirect (type $out-i32) (i32.const 0)) + ) + (func (export "type-i64") (result i64) + (call_indirect (type $out-i64) (i32.const 1)) + ) + (func (export "type-f32") (result f32) + (call_indirect (type $out-f32) (i32.const 2)) + ) + (func (export "type-f64") (result f64) + (call_indirect (type $out-f64) (i32.const 3)) + ) + + (func (export "type-index") (result i64) + (call_indirect (type $over-i64) (i64.const 100) (i32.const 5)) + ) + + (func (export "type-first-i32") (result i32) + (call_indirect (type $over-i32) (i32.const 32) (i32.const 4)) + ) + (func (export "type-first-i64") (result i64) + (call_indirect (type $over-i64) (i64.const 64) (i32.const 5)) + ) + (func (export "type-first-f32") (result f32) + (call_indirect (type $over-f32) (f32.const 1.32) (i32.const 6)) + ) + (func (export "type-first-f64") (result f64) + (call_indirect (type $over-f64) (f64.const 1.64) (i32.const 7)) + ) + + (func (export "type-second-i32") (result i32) + (call_indirect (type $f32-i32) (f32.const 32.1) (i32.const 32) (i32.const 8)) + ) + (func (export "type-second-i64") (result i64) + (call_indirect (type $i32-i64) (i32.const 32) (i64.const 64) (i32.const 9)) + ) + (func (export "type-second-f32") (result f32) + (call_indirect (type $f64-f32) (f64.const 64) (f32.const 32) (i32.const 10)) + ) + (func (export "type-second-f64") (result f64) + (call_indirect (type $i64-f64) (i64.const 64) (f64.const 64.1) (i32.const 11)) + ) +) + +(assert_return (invoke "type-i32") (i32.const 0x132)) +(assert_return (invoke "type-i64") (i64.const 0x164)) +(assert_return (invoke "type-f32") (f32.const 0xf32)) +(assert_return (invoke "type-f64") (f64.const 0xf64)) + +(assert_return (invoke "type-index") (i64.const 100)) + +(assert_return (invoke "type-first-i32") (i32.const 32)) +(assert_return (invoke "type-first-i64") (i64.const 64)) +(assert_return (invoke "type-first-f32") (f32.const 1.32)) +(assert_return (invoke "type-first-f64") (f64.const 1.64)) + +(assert_return (invoke "type-second-i32") (i32.const 32)) +(assert_return (invoke "type-second-i64") (i64.const 64)) +(assert_return (invoke "type-second-f32") (f32.const 32)) +(assert_return (invoke "type-second-f64") (f64.const 64.1)) diff --git a/tests/misc_testsuite/control-flow.wast b/tests/misc_testsuite/control-flow.wast new file mode 100644 index 0000000000..8535564a13 --- /dev/null +++ b/tests/misc_testsuite/control-flow.wast @@ -0,0 +1,116 @@ +(module (func)) + +(module + (func (export "if-without-result") (param i32) (param i32) (result i32) + (if + (i32.eq + (local.get 0) + (local.get 1) + ) + (then (unreachable)) + ) + + (local.get 0) + ) +) + +(assert_return (invoke "if-without-result" (i32.const 2) (i32.const 3)) (i32.const 2)) + +(module + (func (export "block") (param i32) (param i32) (result i32) + (block (result i32) + local.get 0 + ) + ) +) + +(assert_return (invoke "block" (i32.const 10) (i32.const 20)) (i32.const 10)) + +(module + (func (export "br_block") (param i32) (param i32) (result i32) + local.get 1 + (block (result i32) + local.get 0 + local.get 0 + br 0 + unreachable + ) + i32.add + ) +) + +(assert_return (invoke "br_block" (i32.const 5) (i32.const 7)) (i32.const 12)) + +;; Tests discarding values on the value stack, while +;; carrying over the result using a conditional branch. +(module + (func (export "brif_block") (param i32) (param i32) (result i32) + local.get 1 + (block (result i32) + local.get 0 + local.get 0 + br_if 0 + unreachable + ) + i32.add + ) +) + +(assert_return (invoke "brif_block" (i32.const 5) (i32.const 7)) (i32.const 12)) + +;; Tests that br_if keeps values in the case if the branch +;; hasn't been taken. +(module + (func (export "brif_block_passthru") (param i32) (param i32) (result i32) + (block (result i32) + local.get 1 + local.get 0 + br_if 0 + local.get 1 + i32.add + ) + ) +) + +(assert_return (invoke "brif_block_passthru" (i32.const 0) (i32.const 3)) (i32.const 6)) + +(module + (func (export "i32.div_s") (param i32) (param i32) (result i32) + (i32.div_s (local.get 0) (local.get 1)) + ) +) + +(module + (func (export "br_table") (param $i i32) (result i32) + (return + (block $2 (result i32) + (i32.add (i32.const 10) + (block $1 (result i32) + (i32.add (i32.const 100) + (block $0 (result i32) + (i32.add (i32.const 1000) + (block $default (result i32) + (br_table $0 $1 $2 $default + (i32.mul (i32.const 2) (local.get $i)) + (i32.and (i32.const 3) (local.get $i)) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) +) + +(assert_return (invoke "br_table" (i32.const 0)) (i32.const 110)) +(assert_return (invoke "br_table" (i32.const 1)) (i32.const 12)) +(assert_return (invoke "br_table" (i32.const 2)) (i32.const 4)) +(assert_return (invoke "br_table" (i32.const 3)) (i32.const 1116)) +(assert_return (invoke "br_table" (i32.const 4)) (i32.const 118)) +(assert_return (invoke "br_table" (i32.const 5)) (i32.const 20)) +(assert_return (invoke "br_table" (i32.const 6)) (i32.const 12)) +(assert_return (invoke "br_table" (i32.const 7)) (i32.const 1124)) +(assert_return (invoke "br_table" (i32.const 8)) (i32.const 126)) diff --git a/tests/misc_testsuite/div-rem.wast b/tests/misc_testsuite/div-rem.wast new file mode 100644 index 0000000000..3bcfae85d6 --- /dev/null +++ b/tests/misc_testsuite/div-rem.wast @@ -0,0 +1,31 @@ +(module + (func (export "i32.div_s") (param i32) (param i32) (result i32) + (i32.div_s (local.get 0) (local.get 1)) + ) +) + +(assert_return (invoke "i32.div_s" (i32.const -1) (i32.const -1)) (i32.const 1)) + +(module + (func (export "i32.rem_s") (param i32) (param i32) (result i32) + (i32.rem_s (local.get 0) (local.get 1)) + ) +) + +(assert_return (invoke "i32.rem_s" (i32.const 123121) (i32.const -1)) (i32.const 0)) + +(module + (func (export "i64.div_s") (param i64) (param i64) (result i64) + (i64.div_s (local.get 0) (local.get 1)) + ) +) + +(assert_return (invoke "i64.div_s" (i64.const -1) (i64.const -1)) (i64.const 1)) + +(module + (func (export "i64.rem_s") (param i64) (param i64) (result i64) + (i64.rem_s (local.get 0) (local.get 1)) + ) +) + +(assert_return (invoke "i64.rem_s" (i64.const 123121) (i64.const -1)) (i64.const 0)) diff --git a/tests/misc_testsuite/empty.wast b/tests/misc_testsuite/empty.wast new file mode 100644 index 0000000000..5f26cd22c2 --- /dev/null +++ b/tests/misc_testsuite/empty.wast @@ -0,0 +1,3 @@ +(module (func (export "empty"))) + +(invoke "empty") diff --git a/tests/misc_testsuite/fib.wast b/tests/misc_testsuite/fib.wast new file mode 100644 index 0000000000..0ea19ee3bc --- /dev/null +++ b/tests/misc_testsuite/fib.wast @@ -0,0 +1,97 @@ +(module + (func $fib (export "fib") (param $n i32) (result i32) + (if (result i32) + (i32.eq + (i32.const 0) + (local.get $n) + ) + (then + (i32.const 1) + ) + (else + (if (result i32) + (i32.eq + (i32.const 1) + (local.get $n) + ) + (then + (i32.const 1) + ) + (else + (i32.add + ;; fib(n - 1) + (call $fib + (i32.add + (local.get $n) + (i32.const -1) + ) + ) + ;; fib(n - 2) + (call $fib + (i32.add + (local.get $n) + (i32.const -2) + ) + ) + ) + ) + ) + ) + ) + ) +) + +(assert_return (invoke "fib" (i32.const 0)) (i32.const 1)) +(assert_return (invoke "fib" (i32.const 1)) (i32.const 1)) +(assert_return (invoke "fib" (i32.const 2)) (i32.const 2)) +(assert_return (invoke "fib" (i32.const 3)) (i32.const 3)) +(assert_return (invoke "fib" (i32.const 4)) (i32.const 5)) +(assert_return (invoke "fib" (i32.const 5)) (i32.const 8)) +(assert_return (invoke "fib" (i32.const 6)) (i32.const 13)) +(assert_return (invoke "fib" (i32.const 7)) (i32.const 21)) +(assert_return (invoke "fib" (i32.const 8)) (i32.const 34)) +(assert_return (invoke "fib" (i32.const 9)) (i32.const 55)) +(assert_return (invoke "fib" (i32.const 10)) (i32.const 89)) + +;; Generated by Rust. +(module + (func $fib (export "fib") (param $p0 i32) (result i32) + (local $l1 i32) + (local.set $l1 + (i32.const 1)) + (block $B0 + (br_if $B0 + (i32.lt_u + (local.get $p0) + (i32.const 2))) + (local.set $l1 + (i32.const 1)) + (loop $L1 + (local.set $l1 + (i32.add + (call $fib + (i32.add + (local.get $p0) + (i32.const -1))) + (local.get $l1))) + (br_if $L1 + (i32.gt_u + (local.tee $p0 + (i32.add + (local.get $p0) + (i32.const -2))) + (i32.const 1))))) + (local.get $l1)) +) + +(assert_return (invoke "fib" (i32.const 0)) (i32.const 1)) +(assert_return (invoke "fib" (i32.const 1)) (i32.const 1)) +(assert_return (invoke "fib" (i32.const 2)) (i32.const 2)) +(assert_return (invoke "fib" (i32.const 3)) (i32.const 3)) +(assert_return (invoke "fib" (i32.const 4)) (i32.const 5)) +(assert_return (invoke "fib" (i32.const 5)) (i32.const 8)) +(assert_return (invoke "fib" (i32.const 6)) (i32.const 13)) +(assert_return (invoke "fib" (i32.const 7)) (i32.const 21)) +(assert_return (invoke "fib" (i32.const 8)) (i32.const 34)) +(assert_return (invoke "fib" (i32.const 9)) (i32.const 55)) +(assert_return (invoke "fib" (i32.const 10)) (i32.const 89)) diff --git a/tests/misc_testsuite/misc_traps.wast b/tests/misc_testsuite/misc_traps.wast new file mode 100644 index 0000000000..96acf58bfe --- /dev/null +++ b/tests/misc_testsuite/misc_traps.wast @@ -0,0 +1,67 @@ +(module + (memory 1 1) + (func (export "load_oob") + i32.const 65536 + i32.load + drop + ) +) + +(assert_trap (invoke "load_oob") "out of bounds memory access") +(assert_trap (invoke "load_oob") "out of bounds memory access") + +(module + (memory 1 1) + (func (export "store_oob") + i32.const 65536 + i32.const 65536 + i32.store + ) +) + +(assert_trap (invoke "store_oob") "out of bounds memory access") +(assert_trap (invoke "store_oob") "out of bounds memory access") + +(module + (memory 0 0) + (func (export "load_oob_0") + i32.const 0 + i32.load + drop + ) +) + +(assert_trap (invoke "load_oob_0") "out of bounds memory access") +(assert_trap (invoke "load_oob_0") "out of bounds memory access") + +(module + (memory 0 0) + (func (export "store_oob_0") + i32.const 0 + i32.const 0 + i32.store + ) +) + +(assert_trap (invoke "store_oob_0") "out of bounds memory access") +(assert_trap (invoke "store_oob_0") "out of bounds memory access") + +(module + (func (export "divbyzero") (result i32) + i32.const 1 + i32.const 0 + i32.div_s + ) +) + +(assert_trap (invoke "divbyzero") "integer divide by zero") +(assert_trap (invoke "divbyzero") "integer divide by zero") + +(module + (func (export "unreachable") + (unreachable) + ) +) + +(assert_trap (invoke "unreachable") "unreachable") +(assert_trap (invoke "unreachable") "unreachable") diff --git a/tests/misc_testsuite/reference-types/table_copy_on_imported_tables.wast b/tests/misc_testsuite/reference-types/table_copy_on_imported_tables.wast new file mode 100644 index 0000000000..dae846c2d9 --- /dev/null +++ b/tests/misc_testsuite/reference-types/table_copy_on_imported_tables.wast @@ -0,0 +1,165 @@ +(module $m + (func $f (param i32 i32 i32 i32 i32 i32) (result i32) (local.get 0)) + (func $g (param i32 i32 i32 i32 i32 i32) (result i32) (local.get 1)) + (func $h (param i32 i32 i32 i32 i32 i32) (result i32) (local.get 2)) + + (table $t (export "t") funcref (elem $f $g $h $f $g $h))) + +(register "m" $m) + +(module $n + (table $t (import "m" "t") 6 funcref) + + (func $i (param i32 i32 i32 i32 i32 i32) (result i32) (local.get 3)) + (func $j (param i32 i32 i32 i32 i32 i32) (result i32) (local.get 4)) + (func $k (param i32 i32 i32 i32 i32 i32) (result i32) (local.get 5)) + + (table $u (export "u") funcref (elem $i $j $k $i $j $k)) + + (func (export "copy_into_t_from_u") (param i32 i32 i32) + local.get 0 + local.get 1 + local.get 2 + table.copy $t $u) + + (func (export "copy_into_u_from_t") (param i32 i32 i32) + local.get 0 + local.get 1 + local.get 2 + table.copy $u $t) + + (func (export "call_t") (param i32 i32 i32 i32 i32 i32 i32) (result i32) + local.get 0 + local.get 1 + local.get 2 + local.get 3 + local.get 4 + local.get 5 + local.get 6 + call_indirect $t (param i32 i32 i32 i32 i32 i32) (result i32)) + + (func (export "call_u") (param i32 i32 i32 i32 i32 i32 i32) (result i32) + local.get 0 + local.get 1 + local.get 2 + local.get 3 + local.get 4 + local.get 5 + local.get 6 + call_indirect $u (param i32 i32 i32 i32 i32 i32) (result i32))) + +;; Everything has what we initially expect. +(assert_return + (invoke "call_t" (i32.const 1) (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0) + (i32.const 0)) + (i32.const 1)) +(assert_return + (invoke "call_t" (i32.const 0) (i32.const 1) (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0) + (i32.const 1)) + (i32.const 1)) +(assert_return + (invoke "call_t" (i32.const 0) (i32.const 0) (i32.const 1) (i32.const 0) (i32.const 0) (i32.const 0) + (i32.const 2)) + (i32.const 1)) +(assert_return + (invoke "call_u" (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 1) (i32.const 0) (i32.const 0) + (i32.const 0)) + (i32.const 1)) +(assert_return + (invoke "call_u" (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 1) (i32.const 0) + (i32.const 1)) + (i32.const 1)) +(assert_return + (invoke "call_u" (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 1) + (i32.const 2)) + (i32.const 1)) + +;; Now test copying between a local and an imported table. + +;; Copy $i $j $k into $t at 3..6 from $u at 0..3. +(invoke "copy_into_t_from_u" (i32.const 3) (i32.const 0) (i32.const 3)) + +(assert_return + (invoke "call_t" (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 1) (i32.const 0) (i32.const 0) + (i32.const 3)) + (i32.const 1)) +(assert_return + (invoke "call_t" (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 1) (i32.const 0) + (i32.const 4)) + (i32.const 1)) +(assert_return + (invoke "call_t" (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 1) + (i32.const 5)) + (i32.const 1)) + +;; Copy $f $g $h into $u at 0..3 from $t at 0..3. +(invoke "copy_into_u_from_t" (i32.const 0) (i32.const 0) (i32.const 3)) + +(assert_return + (invoke "call_u" (i32.const 1) (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0) + (i32.const 0)) + (i32.const 1)) +(assert_return + (invoke "call_u" (i32.const 0) (i32.const 1) (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0) + (i32.const 1)) + (i32.const 1)) +(assert_return + (invoke "call_u" (i32.const 0) (i32.const 0) (i32.const 1) (i32.const 0) (i32.const 0) (i32.const 0) + (i32.const 2)) + (i32.const 1)) + +(register "n" $n) + +(module $o + (table $t (import "m" "t") 6 funcref) + (table $u (import "n" "u") 6 funcref) + + (func (export "copy_into_t_from_u_2") (param i32 i32 i32) + local.get 0 + local.get 1 + local.get 2 + table.copy $t $u) + + (func (export "copy_into_u_from_t_2") (param i32 i32 i32) + local.get 0 + local.get 1 + local.get 2 + table.copy $u $t) + + (func (export "call_t_2") (param i32 i32 i32 i32 i32 i32 i32) (result i32) + local.get 0 + local.get 1 + local.get 2 + local.get 3 + local.get 4 + local.get 5 + local.get 6 + call_indirect $t (param i32 i32 i32 i32 i32 i32) (result i32)) + + (func (export "call_u_2") (param i32 i32 i32 i32 i32 i32 i32) (result i32) + local.get 0 + local.get 1 + local.get 2 + local.get 3 + local.get 4 + local.get 5 + local.get 6 + call_indirect $u (param i32 i32 i32 i32 i32 i32) (result i32))) + +;; Now test copying between two imported tables. + +;; Copy $i into $t at 0 from $u at 3. +(invoke "copy_into_t_from_u_2" (i32.const 0) (i32.const 3) (i32.const 1)) + +(assert_return + (invoke "call_t_2" (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 1) (i32.const 0) (i32.const 0) + (i32.const 0)) + (i32.const 1)) + +;; Copy $g into $u at 4 from $t at 1. +(invoke "copy_into_u_from_t_2" (i32.const 4) (i32.const 1) (i32.const 1)) + +(assert_return + (invoke "call_u_2" (i32.const 0) (i32.const 1) (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0) + (i32.const 4)) + (i32.const 1)) diff --git a/tests/misc_testsuite/stack_overflow.wast b/tests/misc_testsuite/stack_overflow.wast new file mode 100644 index 0000000000..baf4c98a7a --- /dev/null +++ b/tests/misc_testsuite/stack_overflow.wast @@ -0,0 +1,26 @@ +(module + (func $foo + (call $foo) + ) + (func (export "stack_overflow") + (call $foo) + ) +) + +(assert_exhaustion (invoke "stack_overflow") "call stack exhausted") +(assert_exhaustion (invoke "stack_overflow") "call stack exhausted") + +(module + (func $foo + (call $bar) + ) + (func $bar + (call $foo) + ) + (func (export "stack_overflow") + (call $foo) + ) +) + +(assert_exhaustion (invoke "stack_overflow") "call stack exhausted") +(assert_exhaustion (invoke "stack_overflow") "call stack exhausted") diff --git a/tests/misc_testsuite/threads.wast b/tests/misc_testsuite/threads.wast new file mode 100644 index 0000000000..4c98ae2c3f --- /dev/null +++ b/tests/misc_testsuite/threads.wast @@ -0,0 +1 @@ +(assert_invalid (module (memory 1 1 shared)) "not supported") diff --git a/tests/spec_testsuite b/tests/spec_testsuite new file mode 160000 index 0000000000..c70c3c8b13 --- /dev/null +++ b/tests/spec_testsuite @@ -0,0 +1 @@ +Subproject commit c70c3c8b136e5e7193135d40ec3960f4ef1cb20a diff --git a/tests/wasm/simple.wat b/tests/wasm/simple.wat new file mode 100644 index 0000000000..7b618ee429 --- /dev/null +++ b/tests/wasm/simple.wat @@ -0,0 +1,5 @@ +(module + (func (export "simple") (param i32) (result i32) + local.get 0 + ) +) \ No newline at end of file diff --git a/tests/wast_testsuites.rs b/tests/wast_testsuites.rs new file mode 100644 index 0000000000..9ff7107205 --- /dev/null +++ b/tests/wast_testsuites.rs @@ -0,0 +1,41 @@ +use std::path::Path; +use wasmtime::{Config, Engine, OptLevel, Store, Strategy}; +use wasmtime_wast::WastContext; + +include!(concat!(env!("OUT_DIR"), "/wast_testsuite_tests.rs")); + +// Each of the tests included from `wast_testsuite_tests` will call this +// function which actually executes the `wast` test suite given the `strategy` +// to compile it. +fn run_wast(wast: &str, strategy: Strategy) -> anyhow::Result<()> { + let wast = Path::new(wast); + + let simd = wast.iter().any(|s| s == "simd"); + + let bulk_mem = wast.iter().any(|s| s == "bulk-memory-operations"); + + // Some simd tests assume support for multiple tables, which are introduced + // by reference types. + let reftypes = simd || wast.iter().any(|s| s == "reference-types"); + + let multi_val = wast.iter().any(|s| s == "multi-value"); + + let mut cfg = Config::new(); + cfg.wasm_simd(simd) + .wasm_bulk_memory(bulk_mem) + .wasm_reference_types(reftypes) + .wasm_multi_value(multi_val) + .strategy(strategy)? + .cranelift_debug_verifier(true); + + // FIXME: https://github.com/bytecodealliance/cranelift/issues/1409 + if simd { + cfg.cranelift_opt_level(OptLevel::None); + } + + let store = Store::new(&Engine::new(&cfg)); + let mut wast_context = WastContext::new(store); + wast_context.register_spectest()?; + wast_context.run_file(wast)?; + Ok(()) +} diff --git a/tests/wat/rs2wasm-add-func.wat b/tests/wat/rs2wasm-add-func.wat new file mode 100644 index 0000000000..21ff7f5d7a --- /dev/null +++ b/tests/wat/rs2wasm-add-func.wat @@ -0,0 +1,20 @@ +(module + (type (;0;) (func)) + (type (;1;) (func (param i32 i32) (result i32))) + (func $add (type 1) (param i32 i32) (result i32) + get_local 1 + get_local 0 + i32.add) + (func $start (type 0)) + (table (;0;) 1 1 anyfunc) + (memory (;0;) 17) + (global (;0;) i32 (i32.const 1049114)) + (global (;1;) i32 (i32.const 1049114)) + (export "memory" (memory 0)) + (export "__indirect_function_table" (table 0)) + (export "__heap_base" (global 0)) + (export "__data_end" (global 1)) + (export "add" (func $add)) + (export "start" (func $start)) + (data (i32.const 1048576) "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00") + (data (i32.const 1049092) "invalid malloc request"))