├── .clang-format ├── .github └── workflows │ ├── ci.yml │ └── semgrep.yml ├── .gitignore ├── .gitmodules ├── .prettierrc.yaml ├── Dockerfile ├── LICENSE.md ├── Makefile ├── README.md ├── make_release.sh ├── package-lock.json ├── package.json ├── src ├── config.h ├── global.d.ts ├── helpers.ts ├── index.ts ├── memfs.cc ├── memfs.ts ├── snapshot_preview1.ts ├── streams.ts ├── util.cc └── util.h ├── test ├── Makefile ├── benchmark-build.mjs ├── benchmark.test.ts ├── driver │ ├── common.ts │ ├── standalone.ts │ ├── worker.ts │ └── wrangler.toml ├── generate-wasm-table.mjs ├── package-lock.json ├── package.json ├── subjects │ ├── read.c │ └── write.c ├── transform.js ├── tsconfig.json ├── utils.ts ├── wasi-test-suite.test.ts └── wasmtime-wasi-tests.test.ts └── tsconfig.json /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | submodules: recursive 16 | - name: Set up Docker Buildx 17 | id: buildx 18 | uses: docker/setup-buildx-action@v1 19 | - name: Set up build env 20 | run: cat ./Dockerfile | docker build -t workers-wasi-build - 21 | - name: Build 22 | run: docker run --rm -v $(pwd):/workers-wasi workers-wasi-build make -j ci 23 | - name: Test Report 24 | uses: dorny/test-reporter@v1 25 | if: (success() || failure()) && github.event.pull_request.head.repo.full_name == github.repository 26 | with: 27 | name: Test Report 28 | path: ./build/test/junit.xml 29 | reporter: jest-junit 30 | - uses: actions/upload-artifact@v2 31 | if: github.event.pull_request.head.repo.full_name == github.repository 32 | with: 33 | name: cloudflare-workers-wasi.tgz 34 | path: ./build/cloudflare-workers-wasi-*.tgz 35 | 36 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | 2 | on: 3 | pull_request: {} 4 | workflow_dispatch: {} 5 | push: 6 | branches: 7 | - main 8 | - master 9 | schedule: 10 | - cron: '0 0 * * *' 11 | name: Semgrep config 12 | jobs: 13 | semgrep: 14 | name: semgrep/ci 15 | runs-on: ubuntu-20.04 16 | env: 17 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 18 | SEMGREP_URL: https://cloudflare.semgrep.dev 19 | SEMGREP_APP_URL: https://cloudflare.semgrep.dev 20 | SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version 21 | container: 22 | image: returntocorp/semgrep 23 | steps: 24 | - uses: actions/checkout@v3 25 | - run: semgrep ci 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | node_modules 4 | deps/wasi-sdk-13.0 -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/rapidjson"] 2 | path = deps/rapidjson 3 | url = https://github.com/Tencent/rapidjson 4 | [submodule "deps/wasmtime"] 5 | path = deps/wasmtime 6 | url = https://github.com/bytecodealliance/wasmtime.git 7 | [submodule "deps/littlefs"] 8 | path = deps/littlefs 9 | url = https://github.com/littlefs-project/littlefs 10 | [submodule "deps/wasi-test-suite"] 11 | path = deps/wasi-test-suite 12 | url = https://github.com/caspervonb/wasi-test-suite 13 | [submodule "deps/asyncify"] 14 | path = deps/asyncify 15 | url = https://github.com/GoogleChromeLabs/asyncify 16 | ignore = dirty 17 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | trailingComma: "es5" 2 | useTabs: false 3 | lineWidth: 100 4 | tabWidth: 2 5 | semi: false 6 | singleQuote: true 7 | 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:slim-buster 2 | RUN rustup target add wasm32-wasi 3 | 4 | ENV DEBIAN_FRONTEND=noninteractive 5 | RUN apt-get update -qq && \ 6 | apt-get install -qq --no-install-recommends -y make curl git && \ 7 | rm -rf /var/lib/apt/lists/* 8 | 9 | # install nodejs 10 | RUN curl -sL https://deb.nodesource.com/setup_17.x | bash - 11 | RUN apt-get -qq -y install nodejs 12 | 13 | WORKDIR /workers-wasi 14 | 15 | CMD ["make", "-j", "test"] 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Cloudflare, Inc. and contributors. 2 | Copyright (c) 2021 Cloudflare, Inc. 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | 9 | 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. 10 | 11 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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. 14 | 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # disable built-in rules 2 | .SUFFIXES: 3 | 4 | all: dist/index.mjs 5 | 6 | ci: test 7 | npm pack --pack-destination ./build --quiet 8 | @git diff --exit-code HEAD # error on unexpected changes, eg. out of date package-lock.json 9 | 10 | test: all 11 | cd ./test && $(MAKE) 12 | 13 | clean: 14 | rm -rf ./dist/ 15 | rm -rf ./build/ 16 | rm -rf ./node_modules/ 17 | rm -rf ./test/node_modules/ 18 | 19 | WASI_SDK_PATH := ./deps/wasi-sdk-13.0 20 | WASI_SYSROOT := $(abspath ${WASI_SDK_PATH}/share/wasi-sysroot) 21 | 22 | export CC := $(abspath ${WASI_SDK_PATH}/bin/clang) -target wasm32-wasi --sysroot=${WASI_SYSROOT} 23 | export CFLAGS := -Oz -flto -I ./deps/rapidjson/include -I./deps/littlefs -fno-exceptions -include ./src/config.h 24 | export LDFLAGS := -lstdc++ -flto -Wl,--allow-undefined 25 | export CXXFLAGS := -std=c++20 26 | 27 | WASM_OBJ := \ 28 | ./build/obj/deps/littlefs/lfs.o \ 29 | ./build/obj/deps/littlefs/lfs_util.o \ 30 | ./build/obj/deps/littlefs/bd/lfs_rambd.o \ 31 | ./build/obj/src/memfs.o \ 32 | ./build/obj/src/util.o 33 | 34 | HEADERS := $(wildcard ./src/*.h) 35 | build/obj/%.o: %.c $(HEADERS) $(WASI_SDK_PATH) 36 | mkdir -p $(@D) 37 | $(CC) -c $(CFLAGS) $< -o $@ 38 | 39 | build/obj/%.o: %.cc $(HEADERS) $(WASI_SDK_PATH) 40 | mkdir -p $(@D) 41 | $(CC) -c $(CFLAGS) $(CXXFLAGS) $< -o $@ 42 | 43 | dist/memfs.wasm: $(WASM_OBJ) 44 | mkdir -p $(@D) 45 | $(CC) $(CFLAGS) $(LDFLAGS) $(WASM_OBJ) -o $@ 46 | 47 | node_modules: ./package.json ./package-lock.json 48 | npm install --no-audit --no-optional --no-fund --no-progress --quiet 49 | touch $@ 50 | 51 | dist/index.mjs: $(wildcard ./src/**) node_modules dist/memfs.wasm 52 | sed -i 's/^class Asyncify/export class Asyncify/g' ./deps/asyncify/asyncify.mjs 53 | $(shell npm bin)/tsc -p ./tsconfig.json 54 | $(shell npm bin)/esbuild --bundle ./src/index.ts --outfile=$@ --format=esm --log-level=warning --external:*.wasm 55 | 56 | $(WASI_SDK_PATH): 57 | mkdir -p $(@D) 58 | curl -sLo wasi-sdk.tar.gz https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-13/wasi-sdk-13.0-linux.tar.gz 59 | echo 'aea04267dd864a2f41e21f6cc43591b73dd8901e1ad4e87decf8c4b5905c73cf wasi-sdk.tar.gz' | sha256sum -c 60 | tar zxf wasi-sdk.tar.gz --touch -C deps 61 | rm wasi-sdk.tar.gz 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Workers WASI 2 | 3 | ### *Work in progress* 4 | 5 | An experimental implementation of the WebAssembly System Interface designed to run on [Cloudflare Workers](https://workers.cloudflare.com) 6 | 7 | ## Usage 8 | 9 | ```typescript 10 | import { WASI } from '@cloudflare/workers-wasi'; 11 | import mywasm from './mywasm.wasm'; 12 | 13 | const wasi = new WASI(); 14 | const instance = new WebAssembly.Instance(mywasm, { 15 | wasi_snapshot_preview1: wasi.wasiImport 16 | }); 17 | 18 | await wasi.start(instance); 19 | ``` 20 | 21 | ## Development 22 | Install [Rust](https://www.rust-lang.org/tools/install) and [nvm](https://github.com/nvm-sh/nvm) then run 23 | ``` 24 | nvm use --lts 25 | ``` 26 | Build and test 27 | 28 | ``` 29 | git clone --recursive git@github.com:cloudflare/workers-wasi.git 30 | cd ./workers-wasi 31 | make -j test 32 | ``` 33 | 34 | ## Build with Docker 35 | 36 | ``` 37 | git clone --recursive git@github.com:cloudflare/workers-wasi.git 38 | cd ./workers-wasi 39 | cat ./Dockerfile | docker build -t workers-wasi-build - 40 | docker run --rm -it -v $(pwd):/workers-wasi workers-wasi-build 41 | ``` 42 | 43 | ## Testing 44 | 45 | We aim to be interchangeable with other WASI implementations. Integration tests are run locally using [Miniflare](https://github.com/cloudflare/miniflare) against the following test suites: 46 | - [x] `(52/52)` https://github.com/caspervonb/wasi-test-suite 47 | - [ ] `(28/42)` https://github.com/bytecodealliance/wasmtime/tree/main/crates/test-programs/wasi-tests 48 | 49 | ## Notes 50 | 51 | An ephemeral filesystem implementation built on [littlefs](https://github.com/littlefs-project/littlefs) is included. 52 | Both soft and hard links are not yet supported. 53 | 54 | The following syscalls are not yet supported and return `ENOSYS` 55 | - `fd_readdir` 56 | - `path_link` 57 | - `path_readlink` 58 | - `path_symlink` 59 | - `poll_oneoff` 60 | - `sock_recv` 61 | - `sock_send` 62 | - `sock_shutdown` 63 | 64 | Timestamps are captured using `Date.now()` which has [unique behavior](https://developers.cloudflare.com/workers/runtime-apis/web-standards#javascript-standards) on the Workers platform for [security reasons](https://blog.cloudflare.com/mitigating-spectre-and-other-security-threats-the-cloudflare-workers-security-model/). This affects the implementation of 65 | - `clock_res_get` 66 | - `clock_time_get` 67 | - `fd_filestat_set_times` 68 | - `path_filestat_set_times` 69 | 70 | ## TODO (remove) 71 | Misc TODO: 72 | - [ ] path_rename (update old path for existing open fds) 73 | - [ ] fix preopens interface (use object), and update options docs 74 | - [ ] document difference between nodejs options and ours (streams/fs) 75 | - [ ] fd_close (stdio) 76 | - [ ] fd_renumber (stdio) 77 | - [ ] fd_read/fd_write does not work with renumbering stdio 78 | - [ ] update file timestamps appropriately 79 | -------------------------------------------------------------------------------- /make_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | 4 | : ${1?' Usage: $0 '} 5 | 6 | # git checkout -B release-$1 origin/main 7 | 8 | npm --no-git-tag-version version $1 9 | 10 | # make sure to update test ./package-lock.json 11 | make -j test 12 | 13 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudflare/workers-wasi", 3 | "version": "0.0.5", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@cloudflare/workers-wasi", 9 | "version": "0.0.5", 10 | "license": "BSD-3-Clause", 11 | "devDependencies": { 12 | "@cloudflare/workers-types": "^3.1.1", 13 | "esbuild": "^0.13.4", 14 | "typescript": "^4.4.4" 15 | } 16 | }, 17 | "node_modules/@cloudflare/workers-types": { 18 | "version": "3.1.1", 19 | "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-3.1.1.tgz", 20 | "integrity": "sha512-7NnSrdUJ1EdmJyMImKemlcOyjRCc9GYc939Ih5Lab9Mg4T5KvTMdHQDSUXFZJObPnS0YuYMcyfLJTmxWJj+1Sw==", 21 | "dev": true 22 | }, 23 | "node_modules/esbuild": { 24 | "version": "0.13.13", 25 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.13.tgz", 26 | "integrity": "sha512-Z17A/R6D0b4s3MousytQ/5i7mTCbaF+Ua/yPfoe71vdTv4KBvVAvQ/6ytMngM2DwGJosl8WxaD75NOQl2QF26Q==", 27 | "dev": true, 28 | "hasInstallScript": true, 29 | "bin": { 30 | "esbuild": "bin/esbuild" 31 | }, 32 | "optionalDependencies": { 33 | "esbuild-android-arm64": "0.13.13", 34 | "esbuild-darwin-64": "0.13.13", 35 | "esbuild-darwin-arm64": "0.13.13", 36 | "esbuild-freebsd-64": "0.13.13", 37 | "esbuild-freebsd-arm64": "0.13.13", 38 | "esbuild-linux-32": "0.13.13", 39 | "esbuild-linux-64": "0.13.13", 40 | "esbuild-linux-arm": "0.13.13", 41 | "esbuild-linux-arm64": "0.13.13", 42 | "esbuild-linux-mips64le": "0.13.13", 43 | "esbuild-linux-ppc64le": "0.13.13", 44 | "esbuild-netbsd-64": "0.13.13", 45 | "esbuild-openbsd-64": "0.13.13", 46 | "esbuild-sunos-64": "0.13.13", 47 | "esbuild-windows-32": "0.13.13", 48 | "esbuild-windows-64": "0.13.13", 49 | "esbuild-windows-arm64": "0.13.13" 50 | } 51 | }, 52 | "node_modules/esbuild-android-arm64": { 53 | "version": "0.13.13", 54 | "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.13.tgz", 55 | "integrity": "sha512-T02aneWWguJrF082jZworjU6vm8f4UQ+IH2K3HREtlqoY9voiJUwHLRL6khRlsNLzVglqgqb7a3HfGx7hAADCQ==", 56 | "cpu": [ 57 | "arm64" 58 | ], 59 | "dev": true, 60 | "optional": true, 61 | "os": [ 62 | "android" 63 | ] 64 | }, 65 | "node_modules/esbuild-darwin-64": { 66 | "version": "0.13.13", 67 | "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.13.tgz", 68 | "integrity": "sha512-wkaiGAsN/09X9kDlkxFfbbIgR78SNjMOfUhoel3CqKBDsi9uZhw7HBNHNxTzYUK8X8LAKFpbODgcRB3b/I8gHA==", 69 | "cpu": [ 70 | "x64" 71 | ], 72 | "dev": true, 73 | "optional": true, 74 | "os": [ 75 | "darwin" 76 | ] 77 | }, 78 | "node_modules/esbuild-darwin-arm64": { 79 | "version": "0.13.13", 80 | "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.13.tgz", 81 | "integrity": "sha512-b02/nNKGSV85Gw9pUCI5B48AYjk0vFggDeom0S6QMP/cEDtjSh1WVfoIFNAaLA0MHWfue8KBwoGVsN7rBshs4g==", 82 | "cpu": [ 83 | "arm64" 84 | ], 85 | "dev": true, 86 | "optional": true, 87 | "os": [ 88 | "darwin" 89 | ] 90 | }, 91 | "node_modules/esbuild-freebsd-64": { 92 | "version": "0.13.13", 93 | "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.13.tgz", 94 | "integrity": "sha512-ALgXYNYDzk9YPVk80A+G4vz2D22Gv4j4y25exDBGgqTcwrVQP8rf/rjwUjHoh9apP76oLbUZTmUmvCMuTI1V9A==", 95 | "cpu": [ 96 | "x64" 97 | ], 98 | "dev": true, 99 | "optional": true, 100 | "os": [ 101 | "freebsd" 102 | ] 103 | }, 104 | "node_modules/esbuild-freebsd-arm64": { 105 | "version": "0.13.13", 106 | "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.13.tgz", 107 | "integrity": "sha512-uFvkCpsZ1yqWQuonw5T1WZ4j59xP/PCvtu6I4pbLejhNo4nwjW6YalqnBvBSORq5/Ifo9S/wsIlVHzkzEwdtlw==", 108 | "cpu": [ 109 | "arm64" 110 | ], 111 | "dev": true, 112 | "optional": true, 113 | "os": [ 114 | "freebsd" 115 | ] 116 | }, 117 | "node_modules/esbuild-linux-32": { 118 | "version": "0.13.13", 119 | "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.13.tgz", 120 | "integrity": "sha512-yxR9BBwEPs9acVEwTrEE2JJNHYVuPQC9YGjRfbNqtyfK/vVBQYuw8JaeRFAvFs3pVJdQD0C2BNP4q9d62SCP4w==", 121 | "cpu": [ 122 | "ia32" 123 | ], 124 | "dev": true, 125 | "optional": true, 126 | "os": [ 127 | "linux" 128 | ] 129 | }, 130 | "node_modules/esbuild-linux-64": { 131 | "version": "0.13.13", 132 | "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.13.tgz", 133 | "integrity": "sha512-kzhjlrlJ+6ESRB/n12WTGll94+y+HFeyoWsOrLo/Si0s0f+Vip4b8vlnG0GSiS6JTsWYAtGHReGczFOaETlKIw==", 134 | "cpu": [ 135 | "x64" 136 | ], 137 | "dev": true, 138 | "optional": true, 139 | "os": [ 140 | "linux" 141 | ] 142 | }, 143 | "node_modules/esbuild-linux-arm": { 144 | "version": "0.13.13", 145 | "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.13.tgz", 146 | "integrity": "sha512-hXub4pcEds+U1TfvLp1maJ+GHRw7oizvzbGRdUvVDwtITtjq8qpHV5Q5hWNNn6Q+b3b2UxF03JcgnpzCw96nUQ==", 147 | "cpu": [ 148 | "arm" 149 | ], 150 | "dev": true, 151 | "optional": true, 152 | "os": [ 153 | "linux" 154 | ] 155 | }, 156 | "node_modules/esbuild-linux-arm64": { 157 | "version": "0.13.13", 158 | "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.13.tgz", 159 | "integrity": "sha512-KMrEfnVbmmJxT3vfTnPv/AiXpBFbbyExH13BsUGy1HZRPFMi5Gev5gk8kJIZCQSRfNR17aqq8sO5Crm2KpZkng==", 160 | "cpu": [ 161 | "arm64" 162 | ], 163 | "dev": true, 164 | "optional": true, 165 | "os": [ 166 | "linux" 167 | ] 168 | }, 169 | "node_modules/esbuild-linux-mips64le": { 170 | "version": "0.13.13", 171 | "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.13.tgz", 172 | "integrity": "sha512-cJT9O1LYljqnnqlHaS0hdG73t7hHzF3zcN0BPsjvBq+5Ad47VJun+/IG4inPhk8ta0aEDK6LdP+F9299xa483w==", 173 | "cpu": [ 174 | "mips64el" 175 | ], 176 | "dev": true, 177 | "optional": true, 178 | "os": [ 179 | "linux" 180 | ] 181 | }, 182 | "node_modules/esbuild-linux-ppc64le": { 183 | "version": "0.13.13", 184 | "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.13.tgz", 185 | "integrity": "sha512-+rghW8st6/7O6QJqAjVK3eXzKkZqYAw6LgHv7yTMiJ6ASnNvghSeOcIvXFep3W2oaJc35SgSPf21Ugh0o777qQ==", 186 | "cpu": [ 187 | "ppc64" 188 | ], 189 | "dev": true, 190 | "optional": true, 191 | "os": [ 192 | "linux" 193 | ] 194 | }, 195 | "node_modules/esbuild-netbsd-64": { 196 | "version": "0.13.13", 197 | "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.13.tgz", 198 | "integrity": "sha512-A/B7rwmzPdzF8c3mht5TukbnNwY5qMJqes09ou0RSzA5/jm7Jwl/8z853ofujTFOLhkNHUf002EAgokzSgEMpQ==", 199 | "cpu": [ 200 | "x64" 201 | ], 202 | "dev": true, 203 | "optional": true, 204 | "os": [ 205 | "netbsd" 206 | ] 207 | }, 208 | "node_modules/esbuild-openbsd-64": { 209 | "version": "0.13.13", 210 | "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.13.tgz", 211 | "integrity": "sha512-szwtuRA4rXKT3BbwoGpsff6G7nGxdKgUbW9LQo6nm0TVCCjDNDC/LXxT994duIW8Tyq04xZzzZSW7x7ttDiw1w==", 212 | "cpu": [ 213 | "x64" 214 | ], 215 | "dev": true, 216 | "optional": true, 217 | "os": [ 218 | "openbsd" 219 | ] 220 | }, 221 | "node_modules/esbuild-sunos-64": { 222 | "version": "0.13.13", 223 | "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.13.tgz", 224 | "integrity": "sha512-ihyds9O48tVOYF48iaHYUK/boU5zRaLOXFS+OOL3ceD39AyHo46HVmsJLc7A2ez0AxNZCxuhu+P9OxfPfycTYQ==", 225 | "cpu": [ 226 | "x64" 227 | ], 228 | "dev": true, 229 | "optional": true, 230 | "os": [ 231 | "sunos" 232 | ] 233 | }, 234 | "node_modules/esbuild-windows-32": { 235 | "version": "0.13.13", 236 | "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.13.tgz", 237 | "integrity": "sha512-h2RTYwpG4ldGVJlbmORObmilzL8EECy8BFiF8trWE1ZPHLpECE9//J3Bi+W3eDUuv/TqUbiNpGrq4t/odbayUw==", 238 | "cpu": [ 239 | "ia32" 240 | ], 241 | "dev": true, 242 | "optional": true, 243 | "os": [ 244 | "win32" 245 | ] 246 | }, 247 | "node_modules/esbuild-windows-64": { 248 | "version": "0.13.13", 249 | "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.13.tgz", 250 | "integrity": "sha512-oMrgjP4CjONvDHe7IZXHrMk3wX5Lof/IwFEIbwbhgbXGBaN2dke9PkViTiXC3zGJSGpMvATXVplEhlInJ0drHA==", 251 | "cpu": [ 252 | "x64" 253 | ], 254 | "dev": true, 255 | "optional": true, 256 | "os": [ 257 | "win32" 258 | ] 259 | }, 260 | "node_modules/esbuild-windows-arm64": { 261 | "version": "0.13.13", 262 | "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.13.tgz", 263 | "integrity": "sha512-6fsDfTuTvltYB5k+QPah/x7LrI2+OLAJLE3bWLDiZI6E8wXMQU+wLqtEO/U/RvJgVY1loPs5eMpUBpVajczh1A==", 264 | "cpu": [ 265 | "arm64" 266 | ], 267 | "dev": true, 268 | "optional": true, 269 | "os": [ 270 | "win32" 271 | ] 272 | }, 273 | "node_modules/typescript": { 274 | "version": "4.4.4", 275 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", 276 | "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", 277 | "dev": true, 278 | "bin": { 279 | "tsc": "bin/tsc", 280 | "tsserver": "bin/tsserver" 281 | }, 282 | "engines": { 283 | "node": ">=4.2.0" 284 | } 285 | } 286 | }, 287 | "dependencies": { 288 | "@cloudflare/workers-types": { 289 | "version": "3.1.1", 290 | "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-3.1.1.tgz", 291 | "integrity": "sha512-7NnSrdUJ1EdmJyMImKemlcOyjRCc9GYc939Ih5Lab9Mg4T5KvTMdHQDSUXFZJObPnS0YuYMcyfLJTmxWJj+1Sw==", 292 | "dev": true 293 | }, 294 | "esbuild": { 295 | "version": "0.13.13", 296 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.13.tgz", 297 | "integrity": "sha512-Z17A/R6D0b4s3MousytQ/5i7mTCbaF+Ua/yPfoe71vdTv4KBvVAvQ/6ytMngM2DwGJosl8WxaD75NOQl2QF26Q==", 298 | "dev": true, 299 | "requires": { 300 | "esbuild-android-arm64": "0.13.13", 301 | "esbuild-darwin-64": "0.13.13", 302 | "esbuild-darwin-arm64": "0.13.13", 303 | "esbuild-freebsd-64": "0.13.13", 304 | "esbuild-freebsd-arm64": "0.13.13", 305 | "esbuild-linux-32": "0.13.13", 306 | "esbuild-linux-64": "0.13.13", 307 | "esbuild-linux-arm": "0.13.13", 308 | "esbuild-linux-arm64": "0.13.13", 309 | "esbuild-linux-mips64le": "0.13.13", 310 | "esbuild-linux-ppc64le": "0.13.13", 311 | "esbuild-netbsd-64": "0.13.13", 312 | "esbuild-openbsd-64": "0.13.13", 313 | "esbuild-sunos-64": "0.13.13", 314 | "esbuild-windows-32": "0.13.13", 315 | "esbuild-windows-64": "0.13.13", 316 | "esbuild-windows-arm64": "0.13.13" 317 | } 318 | }, 319 | "esbuild-android-arm64": { 320 | "version": "0.13.13", 321 | "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.13.tgz", 322 | "integrity": "sha512-T02aneWWguJrF082jZworjU6vm8f4UQ+IH2K3HREtlqoY9voiJUwHLRL6khRlsNLzVglqgqb7a3HfGx7hAADCQ==", 323 | "dev": true, 324 | "optional": true 325 | }, 326 | "esbuild-darwin-64": { 327 | "version": "0.13.13", 328 | "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.13.tgz", 329 | "integrity": "sha512-wkaiGAsN/09X9kDlkxFfbbIgR78SNjMOfUhoel3CqKBDsi9uZhw7HBNHNxTzYUK8X8LAKFpbODgcRB3b/I8gHA==", 330 | "dev": true, 331 | "optional": true 332 | }, 333 | "esbuild-darwin-arm64": { 334 | "version": "0.13.13", 335 | "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.13.tgz", 336 | "integrity": "sha512-b02/nNKGSV85Gw9pUCI5B48AYjk0vFggDeom0S6QMP/cEDtjSh1WVfoIFNAaLA0MHWfue8KBwoGVsN7rBshs4g==", 337 | "dev": true, 338 | "optional": true 339 | }, 340 | "esbuild-freebsd-64": { 341 | "version": "0.13.13", 342 | "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.13.tgz", 343 | "integrity": "sha512-ALgXYNYDzk9YPVk80A+G4vz2D22Gv4j4y25exDBGgqTcwrVQP8rf/rjwUjHoh9apP76oLbUZTmUmvCMuTI1V9A==", 344 | "dev": true, 345 | "optional": true 346 | }, 347 | "esbuild-freebsd-arm64": { 348 | "version": "0.13.13", 349 | "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.13.tgz", 350 | "integrity": "sha512-uFvkCpsZ1yqWQuonw5T1WZ4j59xP/PCvtu6I4pbLejhNo4nwjW6YalqnBvBSORq5/Ifo9S/wsIlVHzkzEwdtlw==", 351 | "dev": true, 352 | "optional": true 353 | }, 354 | "esbuild-linux-32": { 355 | "version": "0.13.13", 356 | "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.13.tgz", 357 | "integrity": "sha512-yxR9BBwEPs9acVEwTrEE2JJNHYVuPQC9YGjRfbNqtyfK/vVBQYuw8JaeRFAvFs3pVJdQD0C2BNP4q9d62SCP4w==", 358 | "dev": true, 359 | "optional": true 360 | }, 361 | "esbuild-linux-64": { 362 | "version": "0.13.13", 363 | "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.13.tgz", 364 | "integrity": "sha512-kzhjlrlJ+6ESRB/n12WTGll94+y+HFeyoWsOrLo/Si0s0f+Vip4b8vlnG0GSiS6JTsWYAtGHReGczFOaETlKIw==", 365 | "dev": true, 366 | "optional": true 367 | }, 368 | "esbuild-linux-arm": { 369 | "version": "0.13.13", 370 | "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.13.tgz", 371 | "integrity": "sha512-hXub4pcEds+U1TfvLp1maJ+GHRw7oizvzbGRdUvVDwtITtjq8qpHV5Q5hWNNn6Q+b3b2UxF03JcgnpzCw96nUQ==", 372 | "dev": true, 373 | "optional": true 374 | }, 375 | "esbuild-linux-arm64": { 376 | "version": "0.13.13", 377 | "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.13.tgz", 378 | "integrity": "sha512-KMrEfnVbmmJxT3vfTnPv/AiXpBFbbyExH13BsUGy1HZRPFMi5Gev5gk8kJIZCQSRfNR17aqq8sO5Crm2KpZkng==", 379 | "dev": true, 380 | "optional": true 381 | }, 382 | "esbuild-linux-mips64le": { 383 | "version": "0.13.13", 384 | "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.13.tgz", 385 | "integrity": "sha512-cJT9O1LYljqnnqlHaS0hdG73t7hHzF3zcN0BPsjvBq+5Ad47VJun+/IG4inPhk8ta0aEDK6LdP+F9299xa483w==", 386 | "dev": true, 387 | "optional": true 388 | }, 389 | "esbuild-linux-ppc64le": { 390 | "version": "0.13.13", 391 | "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.13.tgz", 392 | "integrity": "sha512-+rghW8st6/7O6QJqAjVK3eXzKkZqYAw6LgHv7yTMiJ6ASnNvghSeOcIvXFep3W2oaJc35SgSPf21Ugh0o777qQ==", 393 | "dev": true, 394 | "optional": true 395 | }, 396 | "esbuild-netbsd-64": { 397 | "version": "0.13.13", 398 | "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.13.tgz", 399 | "integrity": "sha512-A/B7rwmzPdzF8c3mht5TukbnNwY5qMJqes09ou0RSzA5/jm7Jwl/8z853ofujTFOLhkNHUf002EAgokzSgEMpQ==", 400 | "dev": true, 401 | "optional": true 402 | }, 403 | "esbuild-openbsd-64": { 404 | "version": "0.13.13", 405 | "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.13.tgz", 406 | "integrity": "sha512-szwtuRA4rXKT3BbwoGpsff6G7nGxdKgUbW9LQo6nm0TVCCjDNDC/LXxT994duIW8Tyq04xZzzZSW7x7ttDiw1w==", 407 | "dev": true, 408 | "optional": true 409 | }, 410 | "esbuild-sunos-64": { 411 | "version": "0.13.13", 412 | "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.13.tgz", 413 | "integrity": "sha512-ihyds9O48tVOYF48iaHYUK/boU5zRaLOXFS+OOL3ceD39AyHo46HVmsJLc7A2ez0AxNZCxuhu+P9OxfPfycTYQ==", 414 | "dev": true, 415 | "optional": true 416 | }, 417 | "esbuild-windows-32": { 418 | "version": "0.13.13", 419 | "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.13.tgz", 420 | "integrity": "sha512-h2RTYwpG4ldGVJlbmORObmilzL8EECy8BFiF8trWE1ZPHLpECE9//J3Bi+W3eDUuv/TqUbiNpGrq4t/odbayUw==", 421 | "dev": true, 422 | "optional": true 423 | }, 424 | "esbuild-windows-64": { 425 | "version": "0.13.13", 426 | "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.13.tgz", 427 | "integrity": "sha512-oMrgjP4CjONvDHe7IZXHrMk3wX5Lof/IwFEIbwbhgbXGBaN2dke9PkViTiXC3zGJSGpMvATXVplEhlInJ0drHA==", 428 | "dev": true, 429 | "optional": true 430 | }, 431 | "esbuild-windows-arm64": { 432 | "version": "0.13.13", 433 | "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.13.tgz", 434 | "integrity": "sha512-6fsDfTuTvltYB5k+QPah/x7LrI2+OLAJLE3bWLDiZI6E8wXMQU+wLqtEO/U/RvJgVY1loPs5eMpUBpVajczh1A==", 435 | "dev": true, 436 | "optional": true 437 | }, 438 | "typescript": { 439 | "version": "4.4.4", 440 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", 441 | "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", 442 | "dev": true 443 | } 444 | } 445 | } 446 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudflare/workers-wasi", 3 | "version": "0.0.5", 4 | "main": "dist/index.mjs", 5 | "types": "dist/index.d.ts", 6 | "repository": "github:cloudflare/workers-wasi", 7 | "files": [ 8 | "dist", 9 | "src" 10 | ], 11 | "license": "BSD-3-Clause", 12 | "devDependencies": { 13 | "@cloudflare/workers-types": "^3.1.1", 14 | "esbuild": "^0.13.4", 15 | "typescript": "^4.4.4" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifdef __cplusplus 4 | extern "C" { 5 | #endif 6 | void wasi_trace(int error, const char *fmt, ...); 7 | #ifdef __cplusplus 8 | } 9 | #endif 10 | 11 | // prevent includes and configure below 12 | #define LFS_NO_DEBUG 13 | #define LFS_NO_WARN 14 | #define LFS_NO_ERROR 15 | 16 | // #define LFS_TRACE(...) wasi_trace(0, __VA_ARGS__) 17 | // #define LFS_DEBUG(...) wasi_trace(0, __VA_ARGS__) 18 | #define LFS_WARN(...) wasi_trace(1, __VA_ARGS__) 19 | #define LFS_ERROR(...) wasi_trace(1, __VA_ARGS__) 20 | 21 | #define LFS_ASSERT(x) REQUIRE(x) 22 | #define RAPIDJSON_ASSERT(x) (void)(x); 23 | 24 | #define unlikely(x) __builtin_expect(!!(x), 0) 25 | 26 | #define REQUIRE(x) \ 27 | do { \ 28 | if (unlikely(!(x))) { \ 29 | wasi_trace(1, "REQUIRE(%s)", #x); \ 30 | } \ 31 | } while (0) 32 | 33 | #define LFS_REQUIRE(x) REQUIRE((x) >= 0) 34 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace WebAssembly { 2 | interface Memory { 3 | readonly buffer: ArrayBuffer 4 | grow(delta: number): number 5 | } 6 | 7 | interface Instance { 8 | readonly exports: Exports 9 | } 10 | 11 | interface Module {} 12 | 13 | var Instance: { 14 | prototype: Instance 15 | new (module: Module, importObject?: Imports): Instance 16 | } 17 | 18 | type ImportValue = ExportValue | number 19 | type ModuleImports = Record 20 | type Imports = Record 21 | type ExportValue = Function | Memory 22 | type Exports = Record 23 | } 24 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | const wrapFunc = (name: string, f: Function, log: (data: string) => void) => { 2 | return function (...args: any[]) { 3 | try { 4 | const result = f.apply(undefined, args) 5 | log(`${name}(${args.join(', ')}) = ${result}`) 6 | return result 7 | } catch (e) { 8 | log(`${name}(${args.join(', ')}) = Error(${e})`) 9 | throw e 10 | } 11 | } 12 | } 13 | 14 | /** 15 | * @internal 16 | */ 17 | export const traceImportsToConsole = ( 18 | imports: Record 19 | ): Record => { 20 | for (const key in imports) { 21 | imports[key] = wrapFunc(key, imports[key], console.log) 22 | } 23 | return imports 24 | } 25 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { traceImportsToConsole } from './helpers' 2 | import * as wasi from './snapshot_preview1' 3 | import { MemFS, _FS } from './memfs' 4 | // @ts-ignore 5 | import { Asyncify } from '../deps/asyncify/asyncify.mjs' 6 | import { 7 | FileDescriptor, 8 | fromReadableStream, 9 | fromWritableStream, 10 | } from './streams' 11 | 12 | export type Environment = { [key: string]: string } 13 | 14 | /** 15 | * ProcessExit is thrown when `proc_exit` is called 16 | * @public 17 | */ 18 | export class ProcessExit extends Error { 19 | /** 20 | * The exit code passed to `proc_exit` 21 | */ 22 | code: number 23 | 24 | constructor(code: number) { 25 | super(`proc_exit=${code}`) 26 | this.code = code 27 | 28 | // https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work 29 | Object.setPrototypeOf(this, ProcessExit.prototype) 30 | } 31 | } 32 | 33 | /** 34 | * Options to configure the {@link WASI} interface 35 | * 36 | * @public 37 | */ 38 | export interface WASIOptions { 39 | /** 40 | * Command-line arguments 41 | * 42 | * @defaultValue `[]` 43 | * 44 | */ 45 | args?: string[] 46 | /** 47 | * Environment variables 48 | * 49 | * @defaultValue `{}` 50 | * 51 | */ 52 | env?: Environment 53 | 54 | /** 55 | * By default WASI applications that call `proc_exit` will throw a {@link ProcessExit} exception, setting this option to true will cause {@link WASI.start} to return the the exit code instead. 56 | * 57 | * @defaultValue `false` 58 | * 59 | */ 60 | returnOnExit?: boolean 61 | 62 | /** 63 | * A list of directories that will be accessible in the WebAssembly application's sandbox. 64 | * 65 | * @defaultValue `[]` 66 | * 67 | */ 68 | preopens?: string[] 69 | 70 | /** 71 | * Input stream that the application will be able to read from via stdin 72 | */ 73 | stdin?: ReadableStream 74 | 75 | /** 76 | * Output stream that the application will be able to write to via stdin 77 | */ 78 | stdout?: WritableStream 79 | 80 | /** 81 | * Output stream that the application will be able to write to via stderr 82 | */ 83 | stderr?: WritableStream 84 | 85 | /** 86 | * Enable async IO for stdio streams, requires the application is built with {@link asyncify|https://web.dev/asyncify/} 87 | * 88 | * @experimental 89 | * @defaultValue `false` 90 | * 91 | */ 92 | streamStdio?: boolean 93 | 94 | /** 95 | * Initial filesystem contents, currently used for testing with 96 | * existing WASI test suites 97 | * @internal 98 | * 99 | */ 100 | fs?: _FS 101 | } 102 | 103 | /** 104 | * @public 105 | */ 106 | export class WASI { 107 | #args: Array 108 | #env: Array 109 | #memory?: WebAssembly.Memory 110 | #preopens: Array 111 | #returnOnExit: boolean 112 | #streams: Array 113 | 114 | #memfs: MemFS 115 | #state: any = new Asyncify() 116 | #asyncify: boolean 117 | 118 | constructor(options?: WASIOptions) { 119 | this.#args = options?.args ?? [] 120 | const env = options?.env ?? {} 121 | this.#env = Object.keys(env).map((key) => { 122 | return `${key}=${env[key]}` 123 | }) 124 | 125 | this.#returnOnExit = options?.returnOnExit ?? false 126 | this.#preopens = options?.preopens ?? [] 127 | 128 | this.#asyncify = options?.streamStdio ?? false 129 | this.#streams = [ 130 | fromReadableStream(options?.stdin, this.#asyncify), 131 | fromWritableStream(options?.stdout, this.#asyncify), 132 | fromWritableStream(options?.stderr, this.#asyncify), 133 | ] 134 | this.#memfs = new MemFS(this.#preopens, options?.fs ?? {}) 135 | } 136 | 137 | /** 138 | * See {@link https://nodejs.org/api/wasi.html} 139 | * 140 | * @throws {@link ProcessExit} 141 | * 142 | * This exception is thrown if {@link WASIOptions.returnOnExit} is set to `false` 143 | * and `proc_exit` is called 144 | * 145 | */ 146 | async start(instance: WebAssembly.Instance): Promise { 147 | this.#memory = instance.exports.memory as WebAssembly.Memory 148 | this.#memfs.initialize(this.#memory) 149 | 150 | try { 151 | if (this.#asyncify) { 152 | if (!instance.exports.asyncify_get_state) { 153 | throw new Error( 154 | "streamStdio is requested but the module is missing 'Asyncify' exports, see https://github.com/GoogleChromeLabs/asyncify" 155 | ) 156 | } 157 | 158 | this.#state.init(instance) 159 | } 160 | 161 | await Promise.all(this.#streams.map((s) => s.preRun())) 162 | if (this.#asyncify) { 163 | await this.#state.exports._start() 164 | } else { 165 | const entrypoint = instance.exports._start as Function 166 | entrypoint() 167 | } 168 | } catch (e) { 169 | if (!this.#returnOnExit) { 170 | throw e 171 | } 172 | 173 | if ((e as Error).message === 'unreachable') { 174 | return 134 175 | } else if (e instanceof ProcessExit) { 176 | return e.code 177 | } else { 178 | throw e 179 | } 180 | } finally { 181 | // We must call close to avoid early termination due to hanging promise 182 | await Promise.all(this.#streams.map((s) => s.close())) 183 | await Promise.all(this.#streams.map((s) => s.postRun())) 184 | } 185 | return undefined 186 | } 187 | 188 | get wasiImport(): Record { 189 | const wrap = (f: any, self: any = this) => { 190 | const bound = f.bind(self) 191 | if (this.#asyncify) { 192 | return this.#state.wrapImportFn(bound) 193 | } 194 | return bound 195 | } 196 | 197 | return { 198 | args_get: wrap(this.#args_get), 199 | args_sizes_get: wrap(this.#args_sizes_get), 200 | clock_res_get: wrap(this.#clock_res_get), 201 | clock_time_get: wrap(this.#clock_time_get), 202 | environ_get: wrap(this.#environ_get), 203 | environ_sizes_get: wrap(this.#environ_sizes_get), 204 | fd_advise: wrap(this.#memfs.exports.fd_advise), 205 | fd_allocate: wrap(this.#memfs.exports.fd_allocate), 206 | fd_close: wrap(this.#memfs.exports.fd_close), 207 | fd_datasync: wrap(this.#memfs.exports.fd_datasync), 208 | fd_fdstat_get: wrap(this.#memfs.exports.fd_fdstat_get), 209 | fd_fdstat_set_flags: wrap(this.#memfs.exports.fd_fdstat_set_flags), 210 | fd_fdstat_set_rights: wrap(this.#memfs.exports.fd_fdstat_set_rights), 211 | fd_filestat_get: wrap(this.#memfs.exports.fd_filestat_get), 212 | fd_filestat_set_size: wrap(this.#memfs.exports.fd_filestat_set_size), 213 | fd_filestat_set_times: wrap(this.#memfs.exports.fd_filestat_set_times), 214 | fd_pread: wrap(this.#memfs.exports.fd_pread), 215 | fd_prestat_dir_name: wrap(this.#memfs.exports.fd_prestat_dir_name), 216 | fd_prestat_get: wrap(this.#memfs.exports.fd_prestat_get), 217 | fd_pwrite: wrap(this.#memfs.exports.fd_pwrite), 218 | fd_read: wrap(this.#fd_read), 219 | fd_readdir: wrap(this.#memfs.exports.fd_readdir), 220 | fd_renumber: wrap(this.#memfs.exports.fd_renumber), 221 | fd_seek: wrap(this.#memfs.exports.fd_seek), 222 | fd_sync: wrap(this.#memfs.exports.fd_sync), 223 | fd_tell: wrap(this.#memfs.exports.fd_tell), 224 | fd_write: wrap(this.#fd_write), 225 | path_create_directory: wrap(this.#memfs.exports.path_create_directory), 226 | path_filestat_get: wrap(this.#memfs.exports.path_filestat_get), 227 | path_filestat_set_times: wrap( 228 | this.#memfs.exports.path_filestat_set_times 229 | ), 230 | path_link: wrap(this.#memfs.exports.path_link), 231 | path_open: wrap(this.#memfs.exports.path_open), 232 | path_readlink: wrap(this.#memfs.exports.path_readlink), 233 | path_remove_directory: wrap(this.#memfs.exports.path_remove_directory), 234 | path_rename: wrap(this.#memfs.exports.path_rename), 235 | path_symlink: wrap(this.#memfs.exports.path_symlink), 236 | path_unlink_file: wrap(this.#memfs.exports.path_unlink_file), 237 | poll_oneoff: wrap(this.#poll_oneoff), 238 | proc_exit: wrap(this.#proc_exit), 239 | proc_raise: wrap(this.#proc_raise), 240 | random_get: wrap(this.#random_get), 241 | sched_yield: wrap(this.#sched_yield), 242 | sock_recv: wrap(this.#sock_recv), 243 | sock_send: wrap(this.#sock_send), 244 | sock_shutdown: wrap(this.#sock_shutdown), 245 | } 246 | } 247 | 248 | #view(): DataView { 249 | if (!this.#memory) { 250 | throw new Error('this.memory not set') 251 | } 252 | return new DataView(this.#memory.buffer) 253 | } 254 | 255 | #fillValues( 256 | values: Array, 257 | iter_ptr_ptr: number, 258 | buf_ptr: number 259 | ): number { 260 | const encoder = new TextEncoder() 261 | const buffer = new Uint8Array(this.#memory!.buffer) 262 | 263 | const view = this.#view() 264 | for (const value of values) { 265 | view.setUint32(iter_ptr_ptr, buf_ptr, true) 266 | iter_ptr_ptr += 4 267 | 268 | const data = encoder.encode(`${value}\0`) 269 | buffer.set(data, buf_ptr) 270 | buf_ptr += data.length 271 | } 272 | 273 | return wasi.Result.SUCCESS 274 | } 275 | 276 | #fillSizes( 277 | values: Array, 278 | count_ptr: number, 279 | buffer_size_ptr: number 280 | ): number { 281 | const view = this.#view() 282 | const encoder = new TextEncoder() 283 | const len = values.reduce((acc, value) => { 284 | return acc + encoder.encode(`${value}\0`).length 285 | }, 0) 286 | 287 | view.setUint32(count_ptr, values.length, true) 288 | view.setUint32(buffer_size_ptr, len, true) 289 | return wasi.Result.SUCCESS 290 | } 291 | 292 | #args_get(argv_ptr_ptr: number, argv_buf_ptr: number): number { 293 | return this.#fillValues(this.#args, argv_ptr_ptr, argv_buf_ptr) 294 | } 295 | 296 | #args_sizes_get(argc_ptr: number, argv_buf_size_ptr: number): number { 297 | return this.#fillSizes(this.#args, argc_ptr, argv_buf_size_ptr) 298 | } 299 | 300 | #clock_res_get(id: number, retptr0: number): number { 301 | switch (id) { 302 | case wasi.Clock.REALTIME: 303 | case wasi.Clock.MONOTONIC: 304 | case wasi.Clock.PROCESS_CPUTIME_ID: 305 | case wasi.Clock.THREAD_CPUTIME_ID: { 306 | const view = this.#view() 307 | view.setBigUint64(retptr0, BigInt(1e6), true) 308 | return wasi.Result.SUCCESS 309 | } 310 | } 311 | return wasi.Result.EINVAL 312 | } 313 | 314 | #clock_time_get(id: number, precision: bigint, retptr0: number): number { 315 | switch (id) { 316 | case wasi.Clock.REALTIME: 317 | case wasi.Clock.MONOTONIC: 318 | case wasi.Clock.PROCESS_CPUTIME_ID: 319 | case wasi.Clock.THREAD_CPUTIME_ID: { 320 | const view = this.#view() 321 | view.setBigUint64(retptr0, BigInt(Date.now()) * BigInt(1e6), true) 322 | return wasi.Result.SUCCESS 323 | } 324 | } 325 | return wasi.Result.EINVAL 326 | } 327 | 328 | #environ_get(env_ptr_ptr: number, env_buf_ptr: number): number { 329 | return this.#fillValues(this.#env, env_ptr_ptr, env_buf_ptr) 330 | } 331 | 332 | #environ_sizes_get(env_ptr: number, env_buf_size_ptr: number): number { 333 | return this.#fillSizes(this.#env, env_ptr, env_buf_size_ptr) 334 | } 335 | 336 | #fd_read( 337 | fd: number, 338 | iovs_ptr: number, 339 | iovs_len: number, 340 | retptr0: number 341 | ): Promise | number { 342 | if (fd < 3) { 343 | const desc = this.#streams[fd] 344 | const view = this.#view() 345 | const iovs = wasi.iovViews(view, iovs_ptr, iovs_len) 346 | const result = desc.readv(iovs) 347 | if (typeof result === 'number') { 348 | view.setUint32(retptr0, result, true) 349 | return wasi.Result.SUCCESS 350 | } 351 | const promise = result as Promise 352 | return promise.then((read: number) => { 353 | view.setUint32(retptr0, read, true) 354 | return wasi.Result.SUCCESS 355 | }) 356 | } 357 | return this.#memfs.exports.fd_read(fd, iovs_ptr, iovs_len, retptr0) 358 | } 359 | 360 | #fd_write( 361 | fd: number, 362 | ciovs_ptr: number, 363 | ciovs_len: number, 364 | retptr0: number 365 | ): Promise | number { 366 | if (fd < 3) { 367 | const desc = this.#streams[fd] 368 | const view = this.#view() 369 | const iovs = wasi.iovViews(view, ciovs_ptr, ciovs_len) 370 | const result = desc.writev(iovs) 371 | if (typeof result === 'number') { 372 | view.setUint32(retptr0, result, true) 373 | return wasi.Result.SUCCESS 374 | } 375 | let promise = result as Promise 376 | return promise.then((written: number) => { 377 | view.setUint32(retptr0, written, true) 378 | return wasi.Result.SUCCESS 379 | }) 380 | } 381 | return this.#memfs.exports.fd_write(fd, ciovs_ptr, ciovs_len, retptr0) 382 | } 383 | 384 | #poll_oneoff( 385 | in_ptr: number, 386 | out_ptr: number, 387 | nsubscriptions: number, 388 | retptr0: number 389 | ): number { 390 | return wasi.Result.ENOSYS 391 | } 392 | 393 | #proc_exit(code: number) { 394 | throw new ProcessExit(code) 395 | } 396 | 397 | #proc_raise(signal: number): number { 398 | return wasi.Result.ENOSYS 399 | } 400 | 401 | #random_get(buffer_ptr: number, buffer_len: number): number { 402 | const buffer = new Uint8Array(this.#memory!.buffer, buffer_ptr, buffer_len) 403 | crypto.getRandomValues(buffer) 404 | return wasi.Result.SUCCESS 405 | } 406 | 407 | #sched_yield(): number { 408 | return wasi.Result.SUCCESS 409 | } 410 | 411 | #sock_recv( 412 | fd: number, 413 | ri_data_ptr: number, 414 | ri_data_len: number, 415 | ri_flags: number, 416 | retptr0: number, 417 | retptr1: number 418 | ): number { 419 | return wasi.Result.ENOSYS 420 | } 421 | 422 | #sock_send( 423 | fd: number, 424 | si_data_ptr: number, 425 | si_data_len: number, 426 | si_flags: number, 427 | retptr0: number 428 | ): number { 429 | return wasi.Result.ENOSYS 430 | } 431 | 432 | #sock_shutdown(fd: number, how: number): number { 433 | return wasi.Result.ENOSYS 434 | } 435 | } 436 | 437 | export type { _FS } 438 | -------------------------------------------------------------------------------- /src/memfs.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include "bd/lfs_rambd.h" 11 | #include "config.h" 12 | #include "lfs.h" 13 | #include "util.h" 14 | 15 | void wasi_trace(int error, const char* fmt, ...) { 16 | char* line; 17 | va_list ap; 18 | va_start(ap, fmt); 19 | vasprintf(&line, fmt, ap); 20 | va_end(ap); 21 | trace(error, reinterpret_cast(line), strlen(line)); 22 | free(line); 23 | } 24 | 25 | __wasi_filetype_t from_lfs_type(int type) { 26 | switch (type) { 27 | case LFS_TYPE_DIR: 28 | return __WASI_FILETYPE_DIRECTORY; 29 | case LFS_TYPE_REG: 30 | break; 31 | } 32 | return __WASI_FILETYPE_REGULAR_FILE; 33 | } 34 | 35 | int to_lfs_open_flags(const __wasi_oflags_t flags, 36 | const __wasi_rights_t rights) { 37 | int result = 0; 38 | 39 | if (rights & __WASI_RIGHTS_FD_READ) { 40 | result |= LFS_O_RDONLY; 41 | } 42 | 43 | if (rights & __WASI_RIGHTS_FD_WRITE) { 44 | result |= LFS_O_WRONLY; 45 | } 46 | 47 | if (flags & __WASI_OFLAGS_CREAT) { 48 | result |= LFS_O_CREAT; 49 | } 50 | 51 | if (flags & __WASI_OFLAGS_EXCL) { 52 | result |= LFS_O_EXCL; 53 | } 54 | 55 | if (flags & __WASI_OFLAGS_TRUNC) { 56 | result |= LFS_O_TRUNC; 57 | } 58 | return result; 59 | } 60 | 61 | __wasi_errno_t from_lfs_error(int error) { 62 | switch (error) { 63 | case LFS_ERR_NOENT: 64 | return __WASI_ERRNO_NOENT; 65 | case LFS_ERR_EXIST: 66 | return __WASI_ERRNO_EXIST; 67 | case LFS_ERR_ISDIR: 68 | return __WASI_ERRNO_ISDIR; 69 | case LFS_ERR_NOTEMPTY: 70 | return __WASI_ERRNO_NOTEMPTY; 71 | case LFS_ERR_NOTDIR: 72 | return __WASI_ERRNO_NOTDIR; 73 | case LFS_ERR_INVAL: 74 | return __WASI_ERRNO_INVAL; 75 | } 76 | REQUIRE(false); 77 | return __WASI_ERRNO_SUCCESS; 78 | } 79 | 80 | struct FileMetadata { 81 | // 100 required for wastime tests 82 | __wasi_timestamp_t mtim = 100; 83 | __wasi_timestamp_t atim = 100; 84 | }; 85 | 86 | #define RETURN_IF_LFS_ERR(x) \ 87 | ({ \ 88 | const auto __rc = (x); \ 89 | if (__rc < 0) { \ 90 | return from_lfs_error(__rc); \ 91 | } \ 92 | __rc; \ 93 | }) 94 | 95 | #define RETURN_IF_WASI_ERR(x) \ 96 | ({ \ 97 | const auto __rc = (x); \ 98 | if (__rc != __WASI_ERRNO_SUCCESS) { \ 99 | return __rc; \ 100 | } \ 101 | __rc; \ 102 | }) 103 | 104 | #define REQUIRE_TYPED_FD(__fd, __type, __rights, __allow_stream) \ 105 | (*({ \ 106 | FileDescriptor* __desc; \ 107 | RETURN_IF_WASI_ERR( \ 108 | lookup_fd(__fd, __type, __rights, __allow_stream, &__desc)); \ 109 | __desc; \ 110 | })) 111 | 112 | #define REQUIRE_FD(fd, rights) REQUIRE_TYPED_FD(fd, 0, rights, false) 113 | #define REQUIRE_FD_OR_STREAM(fd, rights) REQUIRE_TYPED_FD(fd, 0, rights, true) 114 | 115 | struct FileDescriptor { 116 | std::string path; 117 | __wasi_rights_t rights_base = 0; 118 | __wasi_rights_t rights_inheriting = 0; 119 | __wasi_fdflags_t fd_flags = 0; 120 | lfs_type type{}; 121 | bool stream = false; 122 | 123 | union State { 124 | lfs_file_t file; 125 | lfs_dir_t dir; 126 | } state; 127 | 128 | lfs_file_t& file() { 129 | REQUIRE(type == LFS_TYPE_REG); 130 | REQUIRE(!stream); 131 | return state.file; 132 | } 133 | lfs_dir_t& dir() { 134 | REQUIRE(type == LFS_TYPE_DIR); 135 | REQUIRE(!stream); 136 | return state.dir; 137 | } 138 | }; 139 | 140 | // clang-format off 141 | constexpr const __wasi_rights_t WASI_PATH_RIGHTS = 142 | __WASI_RIGHTS_PATH_CREATE_DIRECTORY | 143 | __WASI_RIGHTS_PATH_CREATE_FILE | 144 | __WASI_RIGHTS_PATH_LINK_SOURCE | 145 | __WASI_RIGHTS_PATH_LINK_TARGET | 146 | __WASI_RIGHTS_PATH_OPEN | 147 | __WASI_RIGHTS_PATH_RENAME_SOURCE | 148 | __WASI_RIGHTS_PATH_RENAME_TARGET | 149 | __WASI_RIGHTS_PATH_FILESTAT_GET | 150 | __WASI_RIGHTS_PATH_FILESTAT_SET_SIZE | 151 | __WASI_RIGHTS_PATH_FILESTAT_SET_TIMES | 152 | __WASI_RIGHTS_PATH_SYMLINK | 153 | __WASI_RIGHTS_PATH_REMOVE_DIRECTORY | 154 | __WASI_RIGHTS_PATH_UNLINK_FILE; 155 | // clang-format on 156 | 157 | // clang-format off 158 | constexpr const __wasi_rights_t WASI_FD_RIGHTS = 159 | __WASI_RIGHTS_FD_DATASYNC | 160 | __WASI_RIGHTS_FD_READ | 161 | __WASI_RIGHTS_FD_SEEK | 162 | __WASI_RIGHTS_FD_FDSTAT_SET_FLAGS | 163 | __WASI_RIGHTS_FD_SYNC | 164 | __WASI_RIGHTS_FD_TELL | 165 | __WASI_RIGHTS_FD_WRITE | 166 | __WASI_RIGHTS_FD_ADVISE | 167 | __WASI_RIGHTS_FD_ALLOCATE | 168 | __WASI_RIGHTS_FD_READDIR | 169 | __WASI_RIGHTS_FD_FILESTAT_GET | 170 | __WASI_RIGHTS_FD_FILESTAT_SET_SIZE | 171 | __WASI_RIGHTS_FD_FILESTAT_SET_TIMES; 172 | // clang-format on 173 | 174 | struct Context { 175 | lfs_t lfs; 176 | int next_fd = 2147483647; 177 | std::vector preopens; 178 | std::unordered_map<__wasi_fd_t, std::unique_ptr> fds; 179 | 180 | struct lfs_rambd rambd {}; 181 | 182 | const struct lfs_config cfg = { 183 | .context = &rambd, 184 | .read = lfs_rambd_read, 185 | .prog = lfs_rambd_prog, 186 | .erase = lfs_rambd_erase, 187 | .sync = lfs_rambd_sync, 188 | .read_size = 16, 189 | .prog_size = 16, 190 | .block_size = 4096, 191 | .block_count = 128, 192 | .block_cycles = 500, 193 | .cache_size = 16, 194 | .lookahead_size = 16, 195 | }; 196 | 197 | __wasi_fd_t allocate_fd() { 198 | for (;;) { 199 | const auto fd = next_fd--; 200 | if (next_fd < preopens.size()) { 201 | next_fd = std::numeric_limits::max(); 202 | } 203 | 204 | if (fds.find(fd) != fds.end()) { 205 | continue; 206 | } 207 | 208 | return fd; 209 | } 210 | } 211 | 212 | __wasi_errno_t filestat_get(const char* path, __wasi_filestat_t* result) { 213 | lfs_info info{}; 214 | RETURN_IF_LFS_ERR(lfs_stat(&lfs, path, &info)); 215 | 216 | const auto m = get_metadata(path); 217 | *result = {.dev = 0, 218 | .ino = 0, 219 | .filetype = from_lfs_type(info.type), 220 | .nlink = 1, 221 | .size = info.size, 222 | .atim = m.atim, 223 | .mtim = m.mtim}; 224 | return __WASI_ERRNO_SUCCESS; 225 | } 226 | 227 | FileMetadata get_metadata(const char* path) { 228 | FileMetadata m = {}; 229 | if (lfs_getattr(&lfs, path, 1, (void*)&m, sizeof(m)) > 0) { 230 | return m; 231 | } 232 | return {}; 233 | } 234 | 235 | void set_metadata(const char* path, const FileMetadata& m) { 236 | lfs_setattr(&lfs, path, 1, (const void*)&m, sizeof(m)); 237 | } 238 | 239 | __wasi_errno_t lookup_fd(const __wasi_fd_t fd, const int type, 240 | const __wasi_rights_t rights, 241 | const bool allow_streams, FileDescriptor** result) { 242 | auto iter = fds.find(fd); 243 | if (iter == fds.end()) { 244 | return __WASI_ERRNO_BADF; 245 | } 246 | 247 | auto& desc = *iter->second; 248 | if (desc.stream && !allow_streams) { 249 | return __WASI_ERRNO_NOTSUP; 250 | } 251 | 252 | if (type == LFS_TYPE_REG && desc.type != type) { 253 | return __WASI_ERRNO_BADF; 254 | } else if (type == LFS_TYPE_DIR && desc.type != type) { 255 | return __WASI_ERRNO_NOTDIR; 256 | } 257 | 258 | if ((rights & desc.rights_base) != rights) { 259 | return __WASI_ERRNO_NOTCAPABLE; 260 | } 261 | 262 | *result = &desc; 263 | return __WASI_ERRNO_SUCCESS; 264 | } 265 | 266 | __wasi_errno_t require_not_preopen(__wasi_fd_t fd) { 267 | if (fd < 3) { 268 | return __WASI_ERRNO_SUCCESS; 269 | } 270 | const auto index = fd - 3; 271 | if (index < preopens.size()) { 272 | return __WASI_ERRNO_NOTSUP; 273 | } 274 | return __WASI_ERRNO_SUCCESS; 275 | } 276 | 277 | __wasi_errno_t fd_advise(__wasi_fd_t fd, __wasi_filesize_t, __wasi_filesize_t, 278 | __wasi_advice_t) { 279 | REQUIRE_FD(fd, __WASI_RIGHTS_FD_ADVISE); 280 | return __WASI_ERRNO_SUCCESS; 281 | } 282 | 283 | __wasi_errno_t fd_allocate(__wasi_fd_t fd, __wasi_filesize_t offset, 284 | __wasi_filesize_t len) { 285 | auto& desc = REQUIRE_FD(fd, __WASI_RIGHTS_FD_ALLOCATE); 286 | const auto required_size = offset + len; 287 | 288 | const auto current_size = lfs_file_size(&lfs, &desc.file()); 289 | if (current_size < required_size) { 290 | RETURN_IF_LFS_ERR(lfs_file_truncate(&lfs, &desc.file(), required_size)); 291 | RETURN_IF_LFS_ERR(lfs_file_sync(&lfs, &desc.file())); 292 | } 293 | 294 | return __WASI_ERRNO_SUCCESS; 295 | } 296 | 297 | __wasi_errno_t fd_close(__wasi_fd_t fd) { 298 | RETURN_IF_WASI_ERR(require_not_preopen(fd)); 299 | 300 | // TODO: We should flush here vs after execution ends and error on multiple close 301 | if (fd < 3) { 302 | return __WASI_ERRNO_SUCCESS; 303 | } 304 | 305 | auto& desc = REQUIRE_FD(fd, __wasi_rights_t{0}); 306 | 307 | if (desc.type == LFS_TYPE_DIR) { 308 | RETURN_IF_LFS_ERR(lfs_dir_close(&lfs, &desc.dir())); 309 | } else { 310 | RETURN_IF_LFS_ERR(lfs_file_close(&lfs, &desc.file())); 311 | } 312 | fds.erase(fd); 313 | return __WASI_ERRNO_SUCCESS; 314 | } 315 | 316 | __wasi_errno_t fd_datasync(__wasi_fd_t fd) { 317 | REQUIRE_FD(fd, __WASI_RIGHTS_FD_DATASYNC); 318 | // we currenty flush on all writes so this is a noop 319 | return __WASI_ERRNO_SUCCESS; 320 | } 321 | 322 | __wasi_errno_t fd_fdstat_get(const __wasi_fd_t fd, __wasi_fdstat_t* retptr0) { 323 | auto& desc = REQUIRE_FD_OR_STREAM(fd, __wasi_rights_t{0}); 324 | *retptr0 = {.fs_filetype = from_lfs_type(desc.type), 325 | .fs_flags = desc.fd_flags, 326 | .fs_rights_base = desc.rights_base, 327 | .fs_rights_inheriting = desc.rights_inheriting}; 328 | return __WASI_ERRNO_SUCCESS; 329 | } 330 | 331 | __wasi_errno_t fd_fdstat_set_flags(__wasi_fd_t fd, __wasi_fdflags_t flags) { 332 | auto& desc = REQUIRE_FD_OR_STREAM(fd, __WASI_RIGHTS_FD_FDSTAT_SET_FLAGS); 333 | desc.fd_flags = flags; 334 | return __WASI_ERRNO_SUCCESS; 335 | } 336 | 337 | __wasi_errno_t fd_fdstat_set_rights(__wasi_fd_t fd, 338 | __wasi_rights_t fs_rights_base, 339 | __wasi_rights_t fs_rights_inheriting) { 340 | auto& desc = REQUIRE_FD_OR_STREAM(fd, __wasi_rights_t{0}); 341 | 342 | const auto new_rights_base = desc.rights_base & fs_rights_base; 343 | if (new_rights_base != fs_rights_base) { 344 | return __WASI_ERRNO_NOTCAPABLE; 345 | } 346 | const auto new_rights_inheriting = 347 | desc.rights_inheriting & fs_rights_inheriting; 348 | if (new_rights_inheriting != fs_rights_inheriting) { 349 | return __WASI_ERRNO_NOTCAPABLE; 350 | } 351 | 352 | desc.rights_base = new_rights_base; 353 | desc.rights_inheriting = new_rights_inheriting; 354 | 355 | return __WASI_ERRNO_SUCCESS; 356 | } 357 | 358 | __wasi_errno_t fd_filestat_get(__wasi_fd_t fd, __wasi_filestat_t* retptr0) { 359 | auto& desc = REQUIRE_FD_OR_STREAM(fd, __WASI_RIGHTS_FD_FILESTAT_GET); 360 | if (desc.stream) { 361 | *retptr0 = {.dev = 0, 362 | .ino = 0, 363 | .filetype = __WASI_FILETYPE_SOCKET_STREAM, 364 | .nlink = 1}; 365 | return __WASI_ERRNO_SUCCESS; 366 | } 367 | RETURN_IF_WASI_ERR(filestat_get(desc.path.c_str(), retptr0)); 368 | return __WASI_ERRNO_SUCCESS; 369 | } 370 | 371 | __wasi_errno_t fd_filestat_set_size(__wasi_fd_t fd, __wasi_filesize_t size) { 372 | auto& desc = REQUIRE_FD(fd, __WASI_RIGHTS_FD_FILESTAT_SET_SIZE); 373 | RETURN_IF_LFS_ERR(lfs_file_truncate(&lfs, &desc.file(), size)); 374 | RETURN_IF_LFS_ERR(lfs_file_sync(&lfs, &desc.file())); 375 | return __WASI_ERRNO_SUCCESS; 376 | } 377 | 378 | __wasi_errno_t fd_filestat_set_times(__wasi_fd_t fd, __wasi_timestamp_t atim, 379 | __wasi_timestamp_t mtim, 380 | __wasi_fstflags_t fst_flags) { 381 | auto& desc = REQUIRE_FD(fd, __WASI_RIGHTS_FD_FILESTAT_SET_TIMES); 382 | return set_file_times(desc.path.c_str(), atim, mtim, fst_flags); 383 | } 384 | 385 | __wasi_errno_t fd_pread(__wasi_fd_t fd, const __wasi_iovec_t* iovs, 386 | size_t iovs_len, __wasi_filesize_t offset, 387 | __wasi_size_t* retptr0) { 388 | auto& desc = REQUIRE_FD(fd, __WASI_RIGHTS_FD_READ); 389 | 390 | const auto previous_offset = desc.file().pos; 391 | RETURN_IF_LFS_ERR(lfs_file_seek(&lfs, &desc.file(), offset, LFS_SEEK_SET)); 392 | RETURN_IF_LFS_ERR(lfs_file_sync(&lfs, &desc.file())); 393 | 394 | lfs_ssize_t read = 0; 395 | for (size_t i = 0; i < iovs_len; ++i) { 396 | read += RETURN_IF_LFS_ERR( 397 | lfs_file_read(&lfs, &desc.file(), iovs[i].buf, iovs[i].buf_len)); 398 | } 399 | 400 | RETURN_IF_LFS_ERR( 401 | lfs_file_seek(&lfs, &desc.file(), previous_offset, LFS_SEEK_SET)); 402 | RETURN_IF_LFS_ERR(lfs_file_sync(&lfs, &desc.file())); 403 | 404 | *retptr0 = read; 405 | return __WASI_ERRNO_SUCCESS; 406 | } 407 | 408 | __wasi_errno_t fd_prestat_dir_name(__wasi_fd_t fd, 409 | const std::span& result) { 410 | const auto table_offset = 3; 411 | if (fd < table_offset) { 412 | return __WASI_ERRNO_NOTSUP; 413 | } 414 | const auto index = fd - table_offset; 415 | if (index >= preopens.size()) { 416 | return __WASI_ERRNO_BADF; 417 | } 418 | 419 | auto& path = preopens[index]; 420 | REQUIRE(path.size() == result.size()); 421 | std::copy(path.begin(), path.end(), result.begin()); 422 | 423 | return __WASI_ERRNO_SUCCESS; 424 | } 425 | 426 | __wasi_errno_t fd_prestat_get(__wasi_fd_t fd, __wasi_prestat_t* retptr0) { 427 | const auto table_offset = 3; 428 | if (fd < table_offset) { 429 | return __WASI_ERRNO_NOTSUP; 430 | } 431 | const auto index = fd - table_offset; 432 | if (index >= preopens.size()) { 433 | return __WASI_ERRNO_BADF; 434 | } 435 | *retptr0 = __wasi_prestat_t{ 436 | .tag = __WASI_PREOPENTYPE_DIR, 437 | .u = __wasi_prestat_dir_t{.pr_name_len = preopens[index].size()}}; 438 | return __WASI_ERRNO_SUCCESS; 439 | } 440 | 441 | __wasi_errno_t fd_pwrite(__wasi_fd_t fd, const __wasi_ciovec_t* iovs, 442 | size_t iovs_len, __wasi_filesize_t offset, 443 | __wasi_size_t* retptr0) { 444 | auto& desc = REQUIRE_FD(fd, __WASI_RIGHTS_FD_WRITE); 445 | 446 | const auto previous_offset = desc.file().pos; 447 | RETURN_IF_LFS_ERR(lfs_file_seek(&lfs, &desc.file(), offset, LFS_SEEK_SET)); 448 | 449 | lfs_ssize_t written = 0; 450 | for (size_t i = 0; i < iovs_len; ++i) { 451 | written += RETURN_IF_LFS_ERR( 452 | lfs_file_write(&lfs, &desc.file(), iovs[i].buf, iovs[i].buf_len)); 453 | } 454 | 455 | RETURN_IF_LFS_ERR( 456 | lfs_file_seek(&lfs, &desc.file(), previous_offset, LFS_SEEK_SET)); 457 | RETURN_IF_LFS_ERR(lfs_file_sync(&lfs, &desc.file())); 458 | 459 | *retptr0 = written; 460 | return __WASI_ERRNO_SUCCESS; 461 | } 462 | 463 | __wasi_errno_t fd_read(__wasi_fd_t fd, const __wasi_iovec_t* iovs, 464 | size_t iovs_len, __wasi_size_t* retptr0) { 465 | auto& desc = REQUIRE_FD(fd, __WASI_RIGHTS_FD_READ); 466 | 467 | lfs_ssize_t read = 0; 468 | for (size_t i = 0; i < iovs_len; ++i) { 469 | read += RETURN_IF_LFS_ERR( 470 | lfs_file_read(&lfs, &desc.file(), iovs[i].buf, iovs[i].buf_len)); 471 | } 472 | RETURN_IF_LFS_ERR(lfs_file_sync(&lfs, &desc.file())); 473 | 474 | *retptr0 = read; 475 | return __WASI_ERRNO_SUCCESS; 476 | } 477 | 478 | __wasi_errno_t fd_readdir(__wasi_fd_t fd, MutableView& buffer, 479 | __wasi_dircookie_t cookie, __wasi_size_t* retptr0) { 480 | return __WASI_ERRNO_NOSYS; 481 | } 482 | 483 | __wasi_errno_t fd_renumber(__wasi_fd_t fd, __wasi_fd_t to) { 484 | RETURN_IF_WASI_ERR(require_not_preopen(fd)); 485 | const auto iter = fds.find(fd); 486 | if (iter == fds.end()) { 487 | return __WASI_ERRNO_BADF; 488 | } 489 | if (fds.find(to) != fds.end()) { 490 | RETURN_IF_WASI_ERR(fd_close(to)); 491 | } 492 | 493 | REQUIRE(fds.emplace(to, std::move(iter->second)).second); 494 | fds.erase(fd); 495 | return __WASI_ERRNO_SUCCESS; 496 | } 497 | 498 | __wasi_errno_t fd_seek(__wasi_fd_t fd, __wasi_filedelta_t offset, 499 | __wasi_whence_t whence, __wasi_filesize_t* retptr0) { 500 | const auto is_read_only = whence == __WASI_WHENCE_CUR && offset == 0; 501 | const auto required_rights = 502 | is_read_only ? (__WASI_RIGHTS_FD_SEEK | __WASI_RIGHTS_FD_TELL) 503 | : __WASI_RIGHTS_FD_SEEK; 504 | auto& desc = REQUIRE_TYPED_FD(fd, LFS_TYPE_REG, required_rights, true); 505 | if (desc.stream) { 506 | return __WASI_ERRNO_SPIPE; 507 | } 508 | 509 | auto& file = desc.file(); 510 | switch (whence) { 511 | case __WASI_WHENCE_SET: 512 | *retptr0 = 513 | RETURN_IF_LFS_ERR(lfs_file_seek(&lfs, &file, offset, LFS_SEEK_SET)); 514 | break; 515 | case __WASI_WHENCE_CUR: 516 | *retptr0 = 517 | RETURN_IF_LFS_ERR(lfs_file_seek(&lfs, &file, offset, LFS_SEEK_CUR)); 518 | break; 519 | case __WASI_WHENCE_END: 520 | *retptr0 = 521 | RETURN_IF_LFS_ERR(lfs_file_seek(&lfs, &file, offset, LFS_SEEK_END)); 522 | break; 523 | default: 524 | return __WASI_ERRNO_INVAL; 525 | } 526 | 527 | return __WASI_ERRNO_SUCCESS; 528 | } 529 | 530 | __wasi_errno_t fd_sync(__wasi_fd_t fd) { 531 | REQUIRE_FD(fd, __WASI_RIGHTS_FD_SYNC); 532 | // we currenty flush on all writes so this is a noop 533 | return __WASI_ERRNO_SUCCESS; 534 | } 535 | 536 | __wasi_errno_t fd_tell(__wasi_fd_t fd, __wasi_filesize_t* retptr0) { 537 | return fd_seek(fd, 0, __WASI_WHENCE_CUR, retptr0); 538 | } 539 | 540 | __wasi_errno_t fd_write(__wasi_fd_t fd, const __wasi_ciovec_t* iovs, 541 | size_t iovs_len, __wasi_size_t* retptr0) { 542 | auto& desc = REQUIRE_FD(fd, __WASI_RIGHTS_FD_WRITE); 543 | 544 | lfs_ssize_t written = 0; 545 | 546 | auto& file = desc.file(); 547 | RETURN_IF_LFS_ERR(lfs_file_sync(&lfs, &file)); 548 | 549 | const auto previous_offset = file.pos; 550 | const bool append = desc.fd_flags & __WASI_FDFLAGS_APPEND; 551 | if (append) { 552 | file.flags |= LFS_O_APPEND; 553 | } 554 | 555 | for (size_t i = 0; i < iovs_len; ++i) { 556 | written += RETURN_IF_LFS_ERR( 557 | lfs_file_write(&lfs, &file, iovs[i].buf, iovs[i].buf_len)); 558 | } 559 | 560 | if (append) { 561 | // reset file position 562 | file.flags &= ~LFS_O_APPEND; 563 | RETURN_IF_LFS_ERR( 564 | lfs_file_seek(&lfs, &file, previous_offset, LFS_SEEK_SET)); 565 | } 566 | 567 | RETURN_IF_LFS_ERR(lfs_file_sync(&lfs, &file)); 568 | 569 | *retptr0 = written; 570 | return __WASI_ERRNO_SUCCESS; 571 | } 572 | 573 | __wasi_errno_t path_create_directory( 574 | CallFrame& frame, __wasi_fd_t fd, 575 | const std::string_view& unresolved_path) { 576 | const char* path; 577 | RETURN_IF_WASI_ERR(resolve_path(frame, fd, unresolved_path, 578 | __WASI_RIGHTS_PATH_CREATE_DIRECTORY, 579 | &path)); 580 | RETURN_IF_LFS_ERR(lfs_mkdir(&lfs, path)); 581 | return __WASI_ERRNO_SUCCESS; 582 | } 583 | 584 | __wasi_errno_t path_filestat_get(CallFrame& frame, __wasi_fd_t fd, 585 | __wasi_lookupflags_t flags, 586 | const std::string_view& unresolved_path, 587 | __wasi_filestat_t* retptr0) { 588 | const char* path; 589 | RETURN_IF_WASI_ERR(resolve_path(frame, fd, unresolved_path, 590 | __WASI_RIGHTS_PATH_FILESTAT_GET, &path)); 591 | 592 | RETURN_IF_WASI_ERR(filestat_get(path, retptr0)); 593 | 594 | return __WASI_ERRNO_SUCCESS; 595 | } 596 | 597 | __wasi_errno_t path_filestat_set_times( 598 | CallFrame& frame, __wasi_fd_t fd, __wasi_lookupflags_t flags, 599 | const std::string_view& unresolved_path, __wasi_timestamp_t atim, 600 | __wasi_timestamp_t mtim, __wasi_fstflags_t fst_flags) { 601 | const char* path; 602 | RETURN_IF_WASI_ERR(resolve_path(frame, fd, unresolved_path, 603 | __WASI_RIGHTS_PATH_FILESTAT_SET_TIMES, 604 | &path)); 605 | return set_file_times(path, atim, mtim, fst_flags); 606 | } 607 | 608 | __wasi_errno_t path_link(__wasi_fd_t old_fd, __wasi_lookupflags_t old_flags, 609 | const std::string_view& old_path, __wasi_fd_t new_fd, 610 | const std::string_view& new_path) { 611 | return __WASI_ERRNO_NOSYS; 612 | } 613 | 614 | __wasi_errno_t path_open(CallFrame& frame, const __wasi_fd_t fd, 615 | const __wasi_lookupflags_t dirflags, 616 | const std::string_view& unresolved_path, 617 | const __wasi_oflags_t oflags, 618 | const __wasi_rights_t fs_rights_base, 619 | const __wasi_rights_t fs_rights_inheriting, 620 | const __wasi_fdflags_t fd_flags, 621 | __wasi_fd_t* retptr0) { 622 | __wasi_rights_t required_rights = __WASI_RIGHTS_PATH_OPEN; 623 | if (oflags & __WASI_OFLAGS_CREAT) { 624 | required_rights |= __WASI_RIGHTS_PATH_CREATE_FILE; 625 | } 626 | if (oflags & __WASI_OFLAGS_TRUNC) { 627 | required_rights |= __WASI_RIGHTS_PATH_FILESTAT_SET_SIZE; 628 | } 629 | 630 | const auto& dir = 631 | REQUIRE_TYPED_FD(fd, LFS_TYPE_DIR, required_rights, false); 632 | 633 | const char* path; 634 | RETURN_IF_WASI_ERR(resolve_path(frame, dir.path, unresolved_path, &path)); 635 | 636 | auto desc = std::make_unique(); 637 | desc->path = path; 638 | desc->rights_inheriting = fs_rights_inheriting; 639 | desc->fd_flags = fd_flags; 640 | desc->rights_base = fs_rights_base & dir.rights_inheriting; 641 | if (oflags & __WASI_OFLAGS_DIRECTORY) { 642 | desc->type = LFS_TYPE_DIR; 643 | desc->rights_base &= ~WASI_FD_RIGHTS; 644 | RETURN_IF_LFS_ERR(lfs_dir_open(&lfs, &desc->dir(), path)); 645 | } else { 646 | desc->type = LFS_TYPE_REG; 647 | desc->rights_base &= ~WASI_PATH_RIGHTS; 648 | RETURN_IF_LFS_ERR( 649 | lfs_file_open(&lfs, &desc->file(), path, 650 | to_lfs_open_flags(oflags, desc->rights_base))); 651 | } 652 | 653 | const auto new_fd = allocate_fd(); 654 | REQUIRE(fds.emplace(new_fd, std::move(desc)).second); 655 | 656 | auto m = get_metadata(path); 657 | set_metadata(path, m); 658 | 659 | *retptr0 = new_fd; 660 | return __WASI_ERRNO_SUCCESS; 661 | } 662 | 663 | __wasi_errno_t path_readlink(__wasi_fd_t fd, 664 | const std::string_view& unresolved_path, 665 | std::span result, 666 | __wasi_size_t* retptr0) { 667 | return __WASI_ERRNO_NOSYS; 668 | } 669 | 670 | __wasi_errno_t path_remove_directory( 671 | CallFrame& frame, __wasi_fd_t fd, 672 | const std::string_view& unresolved_path) { 673 | const char* path; 674 | RETURN_IF_WASI_ERR(resolve_path(frame, fd, unresolved_path, 675 | __WASI_RIGHTS_PATH_REMOVE_DIRECTORY, 676 | &path)); 677 | 678 | lfs_info info{}; 679 | const auto rc = lfs_stat(&lfs, path, &info); 680 | if (rc == LFS_ERR_OK && info.type != LFS_TYPE_DIR) { 681 | return __WASI_ERRNO_NOTDIR; 682 | } 683 | 684 | RETURN_IF_LFS_ERR(lfs_remove(&lfs, path)); 685 | return __WASI_ERRNO_SUCCESS; 686 | } 687 | 688 | __wasi_errno_t path_rename(CallFrame& frame, __wasi_fd_t old_fd, 689 | const std::string_view& old_unresolved_path, 690 | __wasi_fd_t new_fd, 691 | const std::string_view& new_unresolved_path) { 692 | // TODO: update state for open FDS 693 | 694 | const char* old_path; 695 | RETURN_IF_WASI_ERR(resolve_path(frame, old_fd, old_unresolved_path, 696 | __WASI_RIGHTS_PATH_RENAME_SOURCE, 697 | &old_path)); 698 | const auto is_old_file = is_regular_file(old_path); 699 | if (is_old_file) { 700 | RETURN_IF_WASI_ERR(verify_is_valid_file_path(old_path)); 701 | } 702 | 703 | const char* new_path; 704 | RETURN_IF_WASI_ERR(resolve_path(frame, new_fd, new_unresolved_path, 705 | __WASI_RIGHTS_PATH_RENAME_TARGET, 706 | &new_path)); 707 | if (is_old_file) { 708 | RETURN_IF_WASI_ERR(verify_is_valid_file_path(new_path)); 709 | } else { 710 | // trailing '/' is valid for directory but not for lfs destination path 711 | const auto len = strlen(new_path); 712 | if (new_path[len - 1] == '/') { 713 | ((char*)new_path)[len - 1] = 0; 714 | } 715 | } 716 | 717 | const auto result = lfs_rename(&lfs, old_path, new_path); 718 | if (result == LFS_ERR_ISDIR) { 719 | // for type mismatches use error code based on destination file type 720 | const auto is_new_file = is_regular_file(new_path); 721 | return is_new_file ? __WASI_ERRNO_NOTDIR : __WASI_ERRNO_ISDIR; 722 | } 723 | RETURN_IF_LFS_ERR(result); 724 | 725 | return __WASI_ERRNO_SUCCESS; 726 | } 727 | 728 | __wasi_errno_t path_symlink(const std::string_view& old_unresolved_path, 729 | __wasi_fd_t fd, 730 | const std::string_view& new_unresolved_path) { 731 | return __WASI_ERRNO_NOSYS; 732 | } 733 | 734 | __wasi_errno_t path_unlink_file(CallFrame& frame, __wasi_fd_t fd, 735 | const std::string_view& unresolved_path) { 736 | const char* path; 737 | RETURN_IF_WASI_ERR(resolve_path(frame, fd, unresolved_path, 738 | __WASI_RIGHTS_PATH_UNLINK_FILE, &path)); 739 | 740 | lfs_info info{}; 741 | const auto rc = lfs_stat(&lfs, path, &info); 742 | if (rc == LFS_ERR_OK && info.type == LFS_TYPE_DIR) { 743 | return __WASI_ERRNO_ISDIR; 744 | } 745 | 746 | const auto len = strlen(path); 747 | if (path[len - 1] == '/') { 748 | return __WASI_ERRNO_NOTDIR; 749 | } 750 | 751 | RETURN_IF_LFS_ERR(lfs_remove(&lfs, path)); 752 | return __WASI_ERRNO_SUCCESS; 753 | } 754 | 755 | private: 756 | __wasi_errno_t set_file_times(const char* path, const __wasi_timestamp_t atim, 757 | const __wasi_timestamp_t mtim, 758 | const __wasi_fstflags_t fst_flags) { 759 | auto m = get_metadata(path); 760 | if ((fst_flags & __WASI_FSTFLAGS_ATIM) && 761 | (fst_flags & __WASI_FSTFLAGS_ATIM_NOW)) { 762 | return __WASI_ERRNO_INVAL; 763 | } 764 | 765 | if ((fst_flags & __WASI_FSTFLAGS_MTIM) && 766 | (fst_flags & __WASI_FSTFLAGS_MTIM_NOW)) { 767 | return __WASI_ERRNO_INVAL; 768 | } 769 | 770 | if (fst_flags & __WASI_FSTFLAGS_ATIM) { 771 | m.atim = atim; 772 | } 773 | if (fst_flags & __WASI_FSTFLAGS_MTIM) { 774 | m.mtim = mtim; 775 | } 776 | 777 | if (fst_flags & __WASI_FSTFLAGS_ATIM_NOW) { 778 | m.atim = now_ms() * 10000000; 779 | } 780 | if (fst_flags & __WASI_FSTFLAGS_MTIM_NOW) { 781 | m.mtim = now_ms() * 10000000; 782 | } 783 | 784 | set_metadata(path, m); 785 | return __WASI_ERRNO_SUCCESS; 786 | } 787 | 788 | bool is_regular_file(const char* path) { 789 | lfs_info info{}; 790 | return lfs_stat(&lfs, path, &info) == LFS_ERR_OK && 791 | info.type == LFS_TYPE_REG; 792 | } 793 | 794 | __wasi_errno_t verify_is_valid_file_path(const char* path) { 795 | const auto len = strlen(path); 796 | if (path[len - 1] == '/') { 797 | return __WASI_ERRNO_NOTDIR; 798 | } 799 | return __WASI_ERRNO_SUCCESS; 800 | } 801 | 802 | __wasi_errno_t resolve_path(CallFrame& frame, const std::string_view& dir, 803 | const std::string_view& unresolved_path, 804 | const char** result) { 805 | // TODO: proper canonicalize 806 | const auto new_path_size = dir.size() + unresolved_path.size() + 2; 807 | auto resolved_path = frame.alloc_uninitialized(new_path_size); 808 | 809 | char* iter = resolved_path.data(); 810 | memcpy(iter, dir.begin(), dir.size()); 811 | iter += dir.size(); 812 | 813 | if (unresolved_path == ".") { 814 | *iter = 0; 815 | } else { 816 | *iter = '/'; 817 | ++iter; 818 | memcpy(iter, &unresolved_path[0], unresolved_path.size()); 819 | iter += unresolved_path.size(); 820 | *iter = 0; 821 | } 822 | 823 | *result = resolved_path.data(); 824 | 825 | return __WASI_ERRNO_SUCCESS; 826 | } 827 | 828 | __wasi_errno_t resolve_path(CallFrame& frame, __wasi_fd_t fd, 829 | const std::string_view& unresolved_path, 830 | __wasi_rights_t rights, const char** result) { 831 | return resolve_path(frame, 832 | REQUIRE_TYPED_FD(fd, LFS_TYPE_DIR, rights, false).path, 833 | unresolved_path, result); 834 | } 835 | } state; 836 | 837 | template 838 | auto with_external_ciovs(CallFrame& frame, int32_t iovs_ptr, int32_t iovs_len, 839 | T&& callback) { 840 | auto iovs = frame.ref_array<__wasi_ciovec_t>(iovs_ptr, iovs_len); 841 | for (auto& iov : iovs) { 842 | iov.buf = frame.ref_array(iov.buf, iov.buf_len).data(); 843 | } 844 | return callback(iovs.data()); 845 | } 846 | 847 | template 848 | auto with_external_iovs(CallFrame& frame, int32_t iovs_ptr, int32_t iovs_len, 849 | T&& callback) { 850 | auto iovs = frame.ref_array<__wasi_iovec_t>(iovs_ptr, iovs_len); 851 | 852 | std::vector> rw_buffers; 853 | rw_buffers.reserve(iovs_len); 854 | 855 | for (auto& iov : iovs) { 856 | rw_buffers.emplace_back(frame, reinterpret_cast(iov.buf), 857 | iov.buf_len); 858 | iov.buf = rw_buffers.back().value.data(); 859 | } 860 | return callback(iovs.data()); 861 | } 862 | 863 | #define EXPORT(x) __attribute__((__export_name__(#x))) x 864 | 865 | int32_t EXPORT(fd_advise)(int32_t arg0, int64_t arg1, int64_t arg2, 866 | int32_t arg3) { 867 | return state.fd_advise(arg0, arg1, arg2, arg3); 868 | } 869 | 870 | int32_t EXPORT(fd_allocate)(int32_t arg0, int64_t arg1, int64_t arg2) { 871 | return state.fd_allocate(arg0, arg1, arg2); 872 | } 873 | 874 | int32_t EXPORT(fd_close)(int32_t arg0) { return state.fd_close(arg0); } 875 | 876 | int32_t EXPORT(fd_datasync)(int32_t arg0) { return state.fd_datasync(arg0); } 877 | 878 | int32_t EXPORT(fd_fdstat_get)(int32_t arg0, int32_t arg1) { 879 | CallFrame frame; 880 | MutableView<__wasi_fdstat_t> out(frame, arg1); 881 | return state.fd_fdstat_get(arg0, &out.get()); 882 | } 883 | 884 | int32_t EXPORT(fd_fdstat_set_flags)(int32_t arg0, int32_t arg1) { 885 | return state.fd_fdstat_set_flags(arg0, arg1); 886 | } 887 | 888 | int32_t EXPORT(fd_fdstat_set_rights)(int32_t arg0, int64_t arg1, int64_t arg2) { 889 | return state.fd_fdstat_set_rights(arg0, arg1, arg2); 890 | } 891 | 892 | int32_t EXPORT(fd_filestat_get)(int32_t arg0, int32_t arg1) { 893 | CallFrame frame; 894 | MutableView<__wasi_filestat_t> out(frame, arg1); 895 | return state.fd_filestat_get(arg0, &out.get()); 896 | } 897 | 898 | int32_t EXPORT(fd_filestat_set_size)(int32_t arg0, int64_t arg1) { 899 | return state.fd_filestat_set_size(arg0, arg1); 900 | } 901 | 902 | int32_t EXPORT(fd_filestat_set_times)(int32_t arg0, int64_t arg1, int64_t arg2, 903 | int32_t arg3) { 904 | return state.fd_filestat_set_times(arg0, arg1, arg2, arg3); 905 | } 906 | 907 | int32_t EXPORT(fd_pread)(int32_t arg0, int32_t arg1, int32_t arg2, int64_t arg3, 908 | int32_t arg4) { 909 | CallFrame frame; 910 | MutableView<__wasi_size_t> out(frame, arg4); 911 | return with_external_iovs(frame, arg1, arg2, [&](__wasi_iovec_t* iovs) { 912 | return state.fd_pread(arg0, iovs, arg2, arg3, &out.get()); 913 | }); 914 | } 915 | 916 | int32_t EXPORT(fd_prestat_get)(int32_t arg0, int32_t arg1) { 917 | CallFrame frame; 918 | MutableView<__wasi_prestat_t> out(frame, arg1); 919 | return state.fd_prestat_get(arg0, &out.get()); 920 | } 921 | 922 | int32_t EXPORT(fd_prestat_dir_name)(int32_t arg0, int32_t arg1, int32_t arg2) { 923 | CallFrame frame; 924 | MutableView out(frame, arg1, arg2); 925 | return state.fd_prestat_dir_name(arg0, out.value); 926 | } 927 | 928 | int32_t EXPORT(fd_pwrite)(int32_t arg0, int32_t arg1, int32_t arg2, 929 | int64_t arg3, int32_t arg4) { 930 | CallFrame frame; 931 | MutableView<__wasi_size_t> out(frame, arg4); 932 | 933 | return with_external_ciovs(frame, arg1, arg2, [&](__wasi_ciovec_t* iovs) { 934 | return state.fd_pwrite(arg0, iovs, arg2, arg3, &out.get()); 935 | }); 936 | } 937 | 938 | int32_t EXPORT(fd_read)(int32_t arg0, int32_t arg1, int32_t arg2, 939 | int32_t arg3) { 940 | CallFrame frame; 941 | MutableView<__wasi_size_t> out(frame, arg3); 942 | return with_external_iovs(frame, arg1, arg2, [&](__wasi_iovec_t* iovs) { 943 | return state.fd_read(arg0, iovs, arg2, &out.get()); 944 | }); 945 | } 946 | 947 | int32_t EXPORT(fd_readdir)(int32_t arg0, int32_t arg1, int32_t arg2, 948 | int64_t arg3, int32_t arg4) { 949 | CallFrame frame; 950 | MutableView out1(frame, arg1, arg2); 951 | MutableView<__wasi_size_t> out2(frame, arg4); 952 | return state.fd_readdir(arg0, out1, arg3, &out2.get()); 953 | } 954 | 955 | int32_t EXPORT(fd_renumber)(int32_t arg0, int32_t arg1) { 956 | return state.fd_renumber(arg0, arg1); 957 | } 958 | 959 | int32_t EXPORT(fd_seek)(int32_t arg0, int64_t arg1, int32_t arg2, 960 | int32_t arg3) { 961 | CallFrame frame; 962 | MutableView<__wasi_filesize_t> out(frame, arg3); 963 | return state.fd_seek(arg0, arg1, arg2, &out.get()); 964 | } 965 | 966 | int32_t EXPORT(fd_sync)(int32_t arg0) { return state.fd_sync(arg0); } 967 | 968 | int32_t EXPORT(fd_tell)(int32_t arg0, int32_t arg1) { 969 | CallFrame frame; 970 | MutableView<__wasi_filesize_t> out(frame, arg1); 971 | return state.fd_tell(arg0, &out.get()); 972 | } 973 | 974 | int32_t EXPORT(fd_write)(int32_t arg0, int32_t arg1, int32_t arg2, 975 | int32_t arg3) { 976 | CallFrame frame; 977 | MutableView<__wasi_size_t> out(frame, arg3); 978 | return with_external_ciovs(frame, arg1, arg2, [&](__wasi_ciovec_t* iovs) { 979 | return state.fd_write(arg0, iovs, arg2, &out.get()); 980 | }); 981 | } 982 | 983 | int32_t EXPORT(path_create_directory)(int32_t arg0, int32_t arg1, 984 | int32_t arg2) { 985 | CallFrame frame; 986 | return state.path_create_directory(frame, arg0, frame.ref_string(arg1, arg2)); 987 | } 988 | 989 | int32_t EXPORT(path_filestat_get)(int32_t arg0, int32_t arg1, int32_t arg2, 990 | int32_t arg3, int32_t arg4) { 991 | CallFrame frame; 992 | MutableView<__wasi_filestat_t> out(frame, arg4); 993 | return state.path_filestat_get(frame, arg0, arg1, 994 | frame.ref_string(arg2, arg3), &out.get()); 995 | } 996 | 997 | int32_t EXPORT(path_filestat_set_times)(int32_t arg0, int32_t arg1, 998 | int32_t arg2, int32_t arg3, 999 | int64_t arg4, int64_t arg5, 1000 | int32_t arg6) { 1001 | CallFrame frame; 1002 | return state.path_filestat_set_times( 1003 | frame, arg0, arg1, frame.ref_string(arg2, arg3), arg4, arg5, arg6); 1004 | } 1005 | 1006 | int32_t EXPORT(path_link)(int32_t arg0, int32_t arg1, int32_t arg2, 1007 | int32_t arg3, int32_t arg4, int32_t arg5, 1008 | int32_t arg6) { 1009 | CallFrame frame; 1010 | return state.path_link(arg0, arg1, frame.ref_string(arg2, arg3), arg4, 1011 | frame.ref_string(arg5, arg6)); 1012 | } 1013 | 1014 | int32_t EXPORT(path_open)(int32_t arg0, int32_t arg1, int32_t arg2, 1015 | int32_t arg3, int32_t arg4, int64_t arg5, 1016 | int64_t arg6, int32_t arg7, int32_t arg8) { 1017 | CallFrame frame; 1018 | MutableView<__wasi_fd_t> out(frame, arg8); 1019 | return state.path_open(frame, arg0, arg1, frame.ref_string(arg2, arg3), arg4, 1020 | arg5, arg6, arg7, &out.get()); 1021 | } 1022 | 1023 | int32_t EXPORT(path_readlink)(int32_t arg0, int32_t arg1, int32_t arg2, 1024 | int32_t arg3, int32_t arg4, int32_t arg5) { 1025 | CallFrame frame; 1026 | MutableView out1(frame, arg3, arg4); 1027 | MutableView<__wasi_size_t> out2(frame, arg5); 1028 | return state.path_readlink(arg0, frame.ref_string(arg1, arg2), out1.value, 1029 | &out2.get()); 1030 | } 1031 | 1032 | int32_t EXPORT(path_remove_directory)(int32_t arg0, int32_t arg1, 1033 | int32_t arg2) { 1034 | CallFrame frame; 1035 | return state.path_remove_directory(frame, arg0, frame.ref_string(arg1, arg2)); 1036 | } 1037 | 1038 | int32_t EXPORT(path_rename)(int32_t arg0, int32_t arg1, int32_t arg2, 1039 | int32_t arg3, int32_t arg4, int32_t arg5) { 1040 | CallFrame frame; 1041 | return state.path_rename(frame, arg0, frame.ref_string(arg1, arg2), arg3, 1042 | frame.ref_string(arg4, arg5)); 1043 | } 1044 | 1045 | int32_t EXPORT(path_symlink)(int32_t arg0, int32_t arg1, int32_t arg2, 1046 | int32_t arg3, int32_t arg4) { 1047 | CallFrame frame; 1048 | return state.path_symlink(frame.ref_string(arg0, arg1), arg2, 1049 | frame.ref_string(arg3, arg4)); 1050 | } 1051 | 1052 | int32_t EXPORT(path_unlink_file)(int32_t arg0, int32_t arg1, int32_t arg2) { 1053 | CallFrame frame; 1054 | return state.path_unlink_file(frame, arg0, frame.ref_string(arg1, arg2)); 1055 | } 1056 | 1057 | namespace { 1058 | std::unique_ptr make_preopen_fd(const std::string_view& path) { 1059 | auto desc = std::make_unique(); 1060 | desc->path = path; 1061 | desc->type = LFS_TYPE_DIR; 1062 | desc->rights_base = WASI_PATH_RIGHTS; 1063 | desc->rights_inheriting = ~(__wasi_rights_t{}); 1064 | return desc; 1065 | } 1066 | 1067 | std::unique_ptr make_stream_fd(const __wasi_rights_t rights) { 1068 | auto desc = std::make_unique(); 1069 | desc->type = LFS_TYPE_REG; 1070 | desc->rights_base = __WASI_RIGHTS_POLL_FD_READWRITE | rights; 1071 | desc->rights_inheriting = ~(__wasi_rights_t{}); 1072 | desc->stream = true; 1073 | return desc; 1074 | } 1075 | 1076 | void mkdirp(const char* path) { 1077 | auto* copy = strdup(path); 1078 | const char* parent = dirname(copy); 1079 | if (!strcmp(parent, path)) { 1080 | lfs_mkdir(&state.lfs, parent); 1081 | return; 1082 | } 1083 | 1084 | mkdirp(parent); 1085 | lfs_mkdir(&state.lfs, parent); 1086 | free(copy); 1087 | } 1088 | 1089 | std::string_view to_string_view(int32_t ptr, int32_t len) { 1090 | return std::string_view{reinterpret_cast(ptr), 1091 | static_cast(len)}; 1092 | } 1093 | 1094 | } // namespace 1095 | 1096 | int32_t EXPORT(allocate)(int32_t size) { 1097 | return reinterpret_cast(::malloc(size)); 1098 | } 1099 | 1100 | int32_t EXPORT(initialize_internal)(int32_t arg0, int32_t arg1) { 1101 | const auto json = to_string_view(arg0, arg1); 1102 | 1103 | rapidjson::Document d; 1104 | d.Parse(json.data(), json.size()); 1105 | 1106 | REQUIRE(d.HasMember("preopens")); 1107 | for (const auto& preopen : d["preopens"].GetArray()) { 1108 | const auto new_fd = state.preopens.size() + 3; 1109 | state.preopens.emplace_back(preopen.GetString()); 1110 | state.fds.emplace(new_fd, make_preopen_fd(state.preopens.back())); 1111 | } 1112 | 1113 | REQUIRE(d.HasMember("fs")); 1114 | for (const auto& m : d["fs"].GetObject()) { 1115 | const auto* path = m.name.GetString(); 1116 | mkdirp(path); 1117 | 1118 | lfs_file_t file; 1119 | LFS_REQUIRE(lfs_file_open(&state.lfs, &file, path, 1120 | LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL)); 1121 | LFS_REQUIRE(lfs_file_write(&state.lfs, &file, m.value.GetString(), 1122 | m.value.GetStringLength())); 1123 | LFS_REQUIRE(lfs_file_close(&state.lfs, &file)); 1124 | } 1125 | 1126 | REQUIRE(state.fds.emplace(0, make_stream_fd(__WASI_RIGHTS_FD_READ)).second); 1127 | REQUIRE(state.fds.emplace(1, make_stream_fd(__WASI_RIGHTS_FD_WRITE)).second); 1128 | REQUIRE(state.fds.emplace(2, make_stream_fd(__WASI_RIGHTS_FD_WRITE)).second); 1129 | 1130 | return __WASI_ERRNO_SUCCESS; 1131 | } 1132 | 1133 | int main() { 1134 | LFS_REQUIRE(lfs_rambd_create(&state.cfg)); 1135 | LFS_REQUIRE(lfs_format(&state.lfs, &state.cfg)); 1136 | LFS_REQUIRE(lfs_mount(&state.lfs, &state.cfg)); 1137 | return 0; 1138 | } 1139 | -------------------------------------------------------------------------------- /src/memfs.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import wasm from './memfs.wasm' 3 | import * as wasi from './snapshot_preview1' 4 | 5 | /** 6 | * Used to initialize filesystem contents, currently used for testing with 7 | * existing WASI test suites 8 | * @internal 9 | * 10 | */ 11 | export interface _FS { 12 | [filename: string]: string 13 | } 14 | 15 | export class MemFS { 16 | exports: wasi.SnapshotPreview1 17 | 18 | #instance: WebAssembly.Instance 19 | #hostMemory?: WebAssembly.Memory 20 | 21 | constructor(preopens: Array, fs: _FS) { 22 | this.#instance = new WebAssembly.Instance(wasm, { 23 | internal: { 24 | now_ms: () => Date.now(), 25 | trace: (isError: number, addr: number, size: number) => { 26 | const view = new Uint8Array( 27 | this.#getInternalView().buffer, 28 | addr, 29 | size 30 | ) 31 | const s = new TextDecoder().decode(view) 32 | if (isError) { 33 | throw new Error(s) 34 | } else { 35 | console.info(s) 36 | } 37 | }, 38 | copy_out: (srcAddr: number, dstAddr: number, size: number) => { 39 | const dst = new Uint8Array(this.#hostMemory!.buffer, dstAddr, size) 40 | const src = new Uint8Array( 41 | this.#getInternalView().buffer, 42 | srcAddr, 43 | size 44 | ) 45 | dst.set(src) 46 | }, 47 | copy_in: (srcAddr: number, dstAddr: number, size: number) => { 48 | const src = new Uint8Array(this.#hostMemory!.buffer, srcAddr, size) 49 | const dst = new Uint8Array( 50 | this.#getInternalView().buffer, 51 | dstAddr, 52 | size 53 | ) 54 | dst.set(src) 55 | }, 56 | }, 57 | wasi_snapshot_preview1: { 58 | proc_exit: (_: number) => {}, 59 | fd_seek: (): number => wasi.Result.ENOSYS, 60 | fd_write: (): number => wasi.Result.ENOSYS, 61 | fd_close: (): number => wasi.Result.ENOSYS, 62 | }, 63 | }) 64 | this.exports = this.#instance.exports as unknown as wasi.SnapshotPreview1 65 | 66 | const start = this.#instance.exports._start as Function 67 | start() 68 | 69 | const data = new TextEncoder().encode(JSON.stringify({ preopens, fs })) 70 | 71 | const initialize_internal = this.#instance.exports 72 | .initialize_internal as Function 73 | initialize_internal(this.#copyFrom(data), data.byteLength) 74 | } 75 | 76 | initialize(hostMemory: WebAssembly.Memory) { 77 | this.#hostMemory = hostMemory 78 | } 79 | 80 | #getInternalView(): DataView { 81 | const memory = this.#instance.exports.memory as WebAssembly.Memory 82 | return new DataView(memory.buffer) 83 | } 84 | 85 | #copyFrom(src: Uint8Array): number { 86 | const dstAddr = (this.#instance.exports.allocate as Function)( 87 | src.byteLength 88 | ) 89 | new Uint8Array(this.#getInternalView().buffer, dstAddr, src.byteLength).set( 90 | src 91 | ) 92 | return dstAddr 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/snapshot_preview1.ts: -------------------------------------------------------------------------------- 1 | // See: https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/witx/wasi_snapshot_preview1.witx 2 | export interface SnapshotPreview1 { 3 | args_get(argv_ptr: number, argv_buf_ptr: number): number 4 | 5 | args_sizes_get(argc_ptr: number, argv_buf_size_ptr: number): number 6 | 7 | clock_res_get(id: number, retptr0: number): number 8 | 9 | clock_time_get(id: number, precision: bigint, retptr0: number): number 10 | 11 | environ_get(env_ptr_ptr: number, env_buf_ptr: number): number 12 | 13 | environ_sizes_get(env_ptr: number, env_buf_size_ptr: number): number 14 | 15 | fd_advise(fd: number, offset: bigint, length: bigint, advice: number): number 16 | 17 | fd_allocate(fd: number, offset: bigint, length: bigint): number 18 | 19 | fd_close(fd: number): number 20 | 21 | fd_datasync(fd: number): number 22 | 23 | fd_fdstat_get(fd: number, retptr0: number): number 24 | 25 | fd_fdstat_set_flags(fd: number, flags: number): number 26 | 27 | fd_fdstat_set_rights( 28 | fd: number, 29 | fs_rights_base: bigint, 30 | fs_rights_inheriting: bigint 31 | ): number 32 | 33 | fd_filestat_get(fd: number, retptr0: number): number 34 | 35 | fd_filestat_set_size(fd: number, size: bigint): number 36 | 37 | fd_filestat_set_times( 38 | fd: number, 39 | atim: bigint, 40 | mtim: bigint, 41 | fst_flags: number 42 | ): number 43 | 44 | fd_pread( 45 | fd: number, 46 | iovs_ptr: number, 47 | iovs_len: number, 48 | offset: bigint, 49 | retptr0: number 50 | ): number 51 | 52 | fd_prestat_dir_name(fd: number, path_ptr: number, path_len: number): number 53 | 54 | fd_prestat_get(fd: number, retptr0: number): number 55 | 56 | fd_pwrite( 57 | fd: number, 58 | ciovs_ptr: number, 59 | ciovs_len: number, 60 | offset: bigint, 61 | retptr0: number 62 | ): number 63 | 64 | fd_read( 65 | fd: number, 66 | iovs_ptr: number, 67 | iovs_len: number, 68 | retptr0: number 69 | ): number 70 | 71 | fd_readdir( 72 | fd: number, 73 | buf: number, 74 | buf_len: number, 75 | cookie: bigint, 76 | retptr0: number 77 | ): number 78 | 79 | fd_renumber(old_fd: number, new_fd: number): number 80 | 81 | fd_seek(fd: number, offset: bigint, whence: number, retptr0: number): number 82 | 83 | fd_sync(fd: number): number 84 | 85 | fd_tell(fd: number, retptr0: number): number 86 | 87 | fd_write( 88 | fd: number, 89 | ciovs_ptr: number, 90 | ciovs_len: number, 91 | retptr0: number 92 | ): number 93 | 94 | path_create_directory(fd: number, path_ptr: number, path_len: number): number 95 | 96 | path_filestat_get( 97 | fd: number, 98 | flags: number, 99 | path_ptr: number, 100 | path_len: number, 101 | retptr0: number 102 | ): number 103 | 104 | path_filestat_set_times( 105 | fd: number, 106 | flags: number, 107 | path_ptr: number, 108 | path_len: number, 109 | atim: bigint, 110 | mtime: bigint, 111 | fstFlags: number 112 | ): number 113 | 114 | path_link( 115 | old_fd: number, 116 | old_flags: number, 117 | old_path_ptr: number, 118 | old_path_len: number, 119 | new_fd: number, 120 | new_path_ptr: number, 121 | new_path_len: number 122 | ): number 123 | 124 | path_open( 125 | fd: number, 126 | dirFlags: number, 127 | pathOffset: number, 128 | pathLen: number, 129 | oflags: number, 130 | rightsBase: bigint, 131 | rightsInheriting: bigint, 132 | fdflags: number, 133 | retptr0: number 134 | ): number 135 | 136 | path_readlink( 137 | fd: number, 138 | path_ptr: number, 139 | path_len: number, 140 | buf_ptr: number, 141 | buf_len: number, 142 | retptr0: number 143 | ): number 144 | 145 | path_remove_directory(fd: number, path_ptr: number, path_len: number): number 146 | 147 | path_rename( 148 | old_fd: number, 149 | old_path_ptr: number, 150 | old_path_len: number, 151 | new_fd: number, 152 | new_path_ptr: number, 153 | new_path_len: number 154 | ): number 155 | 156 | path_symlink( 157 | old_path_ptr: number, 158 | old_path_len: number, 159 | fd: number, 160 | new_path_ptr: number, 161 | new_path_len: number 162 | ): number 163 | 164 | path_unlink_file(fd: number, path_ptr: number, path_len: number): number 165 | 166 | poll_oneoff( 167 | in_ptr: number, 168 | out_ptr: number, 169 | nsubscriptions: number, 170 | retptr0: number 171 | ): number 172 | 173 | proc_exit(code: number): void 174 | 175 | proc_raise(signal: number): number 176 | 177 | random_get(buffer_ptr: number, buffer_len: number): number 178 | 179 | sched_yield(): number 180 | 181 | sock_recv( 182 | fd: number, 183 | ri_data_ptr: number, 184 | ri_data_len: number, 185 | ri_flags: number, 186 | retptr0: number, 187 | retptr1: number 188 | ): number 189 | 190 | sock_send( 191 | fd: number, 192 | si_data_ptr: number, 193 | si_data_len: number, 194 | si_flags: number, 195 | retptr0: number 196 | ): number 197 | 198 | sock_shutdown(fd: number, how: number): number 199 | } 200 | 201 | export enum Result { 202 | SUCCESS = 0, 203 | EBADF = 8, 204 | EINVAL = 28, 205 | ENOENT = 44, 206 | ENOSYS = 52, 207 | ENOTSUP = 58, 208 | } 209 | 210 | export enum Clock { 211 | REALTIME = 0, 212 | MONOTONIC = 1, 213 | PROCESS_CPUTIME_ID = 2, 214 | THREAD_CPUTIME_ID = 3, 215 | } 216 | 217 | export const iovViews = ( 218 | view: DataView, 219 | iovs_ptr: number, 220 | iovs_len: number 221 | ): Array => { 222 | let result = Array(iovs_len) 223 | 224 | for (let i = 0; i < iovs_len; i++) { 225 | const bufferPtr = view.getUint32(iovs_ptr, true) 226 | iovs_ptr += 4 227 | 228 | const bufferLen = view.getUint32(iovs_ptr, true) 229 | iovs_ptr += 4 230 | 231 | result[i] = new Uint8Array(view.buffer, bufferPtr, bufferLen) 232 | } 233 | return result 234 | } 235 | -------------------------------------------------------------------------------- /src/streams.ts: -------------------------------------------------------------------------------- 1 | import * as wasi from './snapshot_preview1' 2 | 3 | export interface FileDescriptor { 4 | writev(iovs: Array): Promise | number 5 | readv(iovs: Array): Promise | number 6 | close(): Promise | void 7 | 8 | preRun(): Promise 9 | postRun(): Promise 10 | } 11 | 12 | class DevNull implements FileDescriptor { 13 | writev(iovs: Array): number { 14 | return iovs.map((iov) => iov.byteLength).reduce((prev, curr) => prev + curr) 15 | } 16 | 17 | readv(iovs: Array): number { 18 | return 0 19 | } 20 | 21 | close(): void {} 22 | 23 | async preRun(): Promise {} 24 | async postRun(): Promise {} 25 | } 26 | 27 | class ReadableStreamBase { 28 | writev(iovs: Array): number { 29 | throw new Error('Attempting to call write on a readable stream') 30 | } 31 | 32 | close(): void {} 33 | 34 | async preRun(): Promise {} 35 | async postRun(): Promise {} 36 | } 37 | 38 | class AsyncReadableStreamAdapter 39 | extends ReadableStreamBase 40 | implements FileDescriptor 41 | { 42 | #pending = new Uint8Array() 43 | #reader: ReadableStreamDefaultReader 44 | 45 | constructor(reader: ReadableStreamDefaultReader) { 46 | super() 47 | this.#reader = reader 48 | } 49 | 50 | async readv(iovs: Array): Promise { 51 | let read = 0 52 | for (let iov of iovs) { 53 | while (iov.byteLength > 0) { 54 | // pull only if pending queue is empty 55 | if (this.#pending.byteLength === 0) { 56 | const result = await this.#reader.read() 57 | if (result.done) { 58 | return read 59 | } 60 | this.#pending = result.value 61 | } 62 | const bytes = Math.min(iov.byteLength, this.#pending.byteLength) 63 | iov.set(this.#pending!.subarray(0, bytes)) 64 | this.#pending = this.#pending!.subarray(bytes) 65 | read += bytes 66 | 67 | iov = iov.subarray(bytes) 68 | } 69 | } 70 | return read 71 | } 72 | } 73 | 74 | class WritableStreamBase { 75 | readv(iovs: Array): number { 76 | throw new Error('Attempting to call read on a writable stream') 77 | } 78 | 79 | close(): void {} 80 | 81 | async preRun(): Promise {} 82 | async postRun(): Promise {} 83 | } 84 | 85 | class AsyncWritableStreamAdapter 86 | extends WritableStreamBase 87 | implements FileDescriptor 88 | { 89 | #writer: WritableStreamDefaultWriter 90 | 91 | constructor(writer: WritableStreamDefaultWriter) { 92 | super() 93 | this.#writer = writer 94 | } 95 | 96 | async writev(iovs: Array): Promise { 97 | let written = 0 98 | for (const iov of iovs) { 99 | if (iov.byteLength === 0) { 100 | continue 101 | } 102 | await this.#writer.write(iov) 103 | written += iov.byteLength 104 | } 105 | return written 106 | } 107 | 108 | async close(): Promise { 109 | await this.#writer.close() 110 | } 111 | } 112 | 113 | class SyncWritableStreamAdapter 114 | extends WritableStreamBase 115 | implements FileDescriptor 116 | { 117 | #writer: WritableStreamDefaultWriter 118 | #buffer: Uint8Array = new Uint8Array(4096) 119 | #bytesWritten: number = 0 120 | 121 | constructor(writer: WritableStreamDefaultWriter) { 122 | super() 123 | this.#writer = writer 124 | } 125 | 126 | writev(iovs: Array): number { 127 | let written = 0 128 | for (const iov of iovs) { 129 | if (iov.byteLength === 0) { 130 | continue 131 | } 132 | 133 | // Check if we're about to overflow the buffer and resize if need be. 134 | const requiredCapacity = this.#bytesWritten + iov.byteLength 135 | if (requiredCapacity > this.#buffer.byteLength) { 136 | let desiredCapacity = this.#buffer.byteLength 137 | while (desiredCapacity < requiredCapacity) { 138 | desiredCapacity *= 1.5; 139 | } 140 | 141 | const oldBuffer = this.#buffer 142 | this.#buffer = new Uint8Array(desiredCapacity) 143 | this.#buffer.set(oldBuffer) 144 | } 145 | 146 | this.#buffer.set(iov, this.#bytesWritten) 147 | written += iov.byteLength 148 | this.#bytesWritten += iov.byteLength 149 | } 150 | return written 151 | } 152 | 153 | async postRun(): Promise { 154 | const slice = this.#buffer.subarray(0, this.#bytesWritten) 155 | await this.#writer.write(slice) 156 | await this.#writer.close() 157 | } 158 | } 159 | 160 | class SyncReadableStreamAdapter 161 | extends ReadableStreamBase 162 | implements FileDescriptor 163 | { 164 | #buffer?: Uint8Array 165 | #reader: ReadableStreamDefaultReader 166 | 167 | constructor(reader: ReadableStreamDefaultReader) { 168 | super() 169 | this.#reader = reader 170 | } 171 | 172 | readv(iovs: Array): number { 173 | let read = 0 174 | for (const iov of iovs) { 175 | const bytes = Math.min(iov.byteLength, this.#buffer!.byteLength) 176 | if (bytes <= 0) { 177 | break; 178 | } 179 | iov.set(this.#buffer!.subarray(0, bytes)) 180 | this.#buffer = this.#buffer!.subarray(bytes) 181 | read += bytes 182 | } 183 | return read 184 | } 185 | 186 | async preRun(): Promise { 187 | const pending: Array = [] 188 | let length = 0 189 | 190 | for (;;) { 191 | const result = await this.#reader.read() 192 | if (result.done) { 193 | break 194 | } 195 | 196 | const data = result.value 197 | pending.push(data) 198 | length += data.length 199 | } 200 | 201 | let result = new Uint8Array(length) 202 | let offset = 0 203 | 204 | pending.forEach((item) => { 205 | result.set(item, offset) 206 | offset += item.length 207 | }) 208 | 209 | this.#buffer = result 210 | } 211 | } 212 | 213 | export const fromReadableStream = ( 214 | stream: ReadableStream | undefined, 215 | supportsAsync: boolean 216 | ): FileDescriptor => { 217 | if (!stream) { 218 | return new DevNull() 219 | } 220 | 221 | if (supportsAsync) { 222 | return new AsyncReadableStreamAdapter(stream.getReader()) 223 | } 224 | 225 | return new SyncReadableStreamAdapter(stream.getReader()) 226 | } 227 | 228 | export const fromWritableStream = ( 229 | stream: WritableStream | undefined, 230 | supportsAsync: boolean 231 | ): FileDescriptor => { 232 | if (!stream) { 233 | return new DevNull() 234 | } 235 | 236 | if (supportsAsync) { 237 | return new AsyncWritableStreamAdapter(stream.getWriter()) 238 | } 239 | 240 | return new SyncWritableStreamAdapter(stream.getWriter()) 241 | } 242 | -------------------------------------------------------------------------------- /src/util.cc: -------------------------------------------------------------------------------- 1 | #include "util.h" 2 | 3 | #include "config.h" 4 | 5 | char* CallFrame::alloc(const std::size_t size) { 6 | auto* result = tmp_buffer + tmp_offset; 7 | tmp_offset += size; 8 | REQUIRE(tmp_offset <= sizeof(tmp_buffer)); 9 | return result; 10 | } 11 | 12 | std::string_view CallFrame::ref_string(int32_t addr, const int32_t len) { 13 | const auto span = ref_array(addr, len); 14 | return {span.data(), span.size()}; 15 | } 16 | -------------------------------------------------------------------------------- /src/util.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | class CallFrame { 7 | public: 8 | template 9 | std::span alloc_uninitialized(const std::size_t count); 10 | 11 | template 12 | std::span ref_array(const U addr, std::size_t const count); 13 | 14 | std::string_view ref_string(const int32_t addr, const int32_t size); 15 | 16 | private: 17 | char* alloc(const std::size_t size); 18 | 19 | char tmp_buffer[4096 * 10]; 20 | int tmp_offset = 0; 21 | }; 22 | 23 | #define IMPORT(x) \ 24 | __attribute__((__import_module__("internal"), __import_name__(#x))) x 25 | int32_t IMPORT(copy_out)(int32_t src_addr, int32_t dst_addr, int32_t size); 26 | int32_t IMPORT(copy_in)(int32_t src_addr, int32_t dst_addr, int32_t size); 27 | int32_t IMPORT(trace)(int32_t is_error, int32_t addr, int32_t size); 28 | int32_t IMPORT(now_ms)(); 29 | #undef IMPORT 30 | 31 | template 32 | class MutableView { 33 | public: 34 | explicit MutableView(CallFrame& frame, const int32_t addr, 35 | const int32_t count = 1) 36 | : value(frame.ref_array(addr, count)), addr(addr) {} 37 | 38 | T& get() { return value[0]; } 39 | 40 | ~MutableView() { 41 | copy_out(reinterpret_cast(value.data()), addr, value.size_bytes()); 42 | } 43 | 44 | const std::span value; 45 | 46 | private: 47 | const int32_t addr; 48 | }; 49 | 50 | template 51 | [[nodiscard]] std::span CallFrame::ref_array(const U addr, 52 | std::size_t const count) { 53 | auto data = alloc_uninitialized(count); 54 | copy_in(reinterpret_cast(addr), 55 | reinterpret_cast(data.data()), data.size_bytes()); 56 | return data; 57 | } 58 | 59 | template 60 | [[nodiscard]] std::span CallFrame::alloc_uninitialized( 61 | const std::size_t count) { 62 | static_assert(std::is_trivial_v); 63 | const auto byte_length = count * sizeof(T); 64 | return {reinterpret_cast(alloc(byte_length)), count}; 65 | } 66 | -------------------------------------------------------------------------------- /test/Makefile: -------------------------------------------------------------------------------- 1 | default: run-tests 2 | 3 | WASM_OPT := ../build/binaryen/bin/wasm-opt 4 | WASI_SDK_PATH := ../deps/wasi-sdk-13.0 5 | WASI_SYSROOT := $(abspath ${WASI_SDK_PATH}/share/wasi-sysroot) 6 | OUTPUT_DIR := ../build/test 7 | 8 | # https://github.com/caspervonb/wasi-test-suite 9 | WASI_TEST_SUITE_SRC := ../deps/wasi-test-suite 10 | WASI_TEST_SUITE_DST := $(OUTPUT_DIR)/wasi-test-suite 11 | WASI_TEST_SUITE_SRC_TESTS := $(shell ls $(WASI_TEST_SUITE_SRC)/**/*.wasm) 12 | WASI_TEST_SUITE_DST_TESTS := $(WASI_TEST_SUITE_SRC_TESTS:$(WASI_TEST_SUITE_SRC)/%.wasm=$(WASI_TEST_SUITE_DST)/%.wasm) 13 | 14 | $(WASI_TEST_SUITE_DST)/%.wasm: $(WASI_TEST_SUITE_SRC)/%.wasm $(WASM_OPT) 15 | mkdir -p $(@D) 16 | $(WASM_OPT) -g -O --asyncify $< -o $@ 17 | 18 | # https://github.com/bytecodealliance/wasmtime/tree/main/crates/test-programs/wasi-tests 19 | WASI_TESTS_CRATE_PATH := ../deps/wasmtime/crates/test-programs/wasi-tests 20 | WASMTIME_BIN := $(WASI_TESTS_CRATE_PATH)/target/wasm32-wasi/debug 21 | WASMTIME_SRC := $(WASI_TESTS_CRATE_PATH)/src/bin 22 | WASMTIME_DST := $(OUTPUT_DIR)/wasmtime 23 | WASMTIME_SRC_TESTS := $(shell ls $(WASI_TESTS_CRATE_PATH)/src/bin/*.rs) 24 | WASMTIME_DST_TESTS := $(WASMTIME_SRC_TESTS:$(WASMTIME_SRC)/%.rs=$(WASMTIME_DST)/%.wasm) 25 | 26 | BENCHMARK_SRC := ./subjects 27 | BENCHMARK_DST := $(OUTPUT_DIR)/benchmark 28 | BENCHMARK_SRC_TESTS := $(shell ls ./subjects/*.c) 29 | BENCHMARK_DST_TESTS := $(BENCHMARK_SRC_TESTS:$(BENCHMARK_SRC)/%.c=$(BENCHMARK_DST)/%.wasm) 30 | 31 | $(WASMTIME_BIN)/%.wasm: $(wildcard $(WASI_TESTS_CRATE_PATH)/src/**) 32 | cargo build --bin $* --target wasm32-wasi --manifest-path $(WASI_TESTS_CRATE_PATH)/Cargo.toml 33 | 34 | $(WASMTIME_DST)/%.wasm: $(WASMTIME_BIN)/%.wasm $(WASM_OPT) 35 | mkdir -p $(@D) 36 | $(WASM_OPT) -g -O --asyncify $< -o $@ 37 | 38 | BUNDLE := $(OUTPUT_DIR)/index.mjs 39 | MEMFS_SRC := ../dist/memfs.wasm 40 | MEMFS_DST := $(OUTPUT_DIR)/memfs.wasm 41 | 42 | node_modules: ./package.json ./package-lock.json ../package.json 43 | npm install --no-audit --no-fund --no-progress --quiet 44 | touch $@ 45 | 46 | $(OUTPUT_DIR)/standalone.mjs: $(BENCHMARK_DST_TESTS) node_modules ./driver/*.ts 47 | node benchmark-build.mjs 48 | 49 | run-tests: $(BUNDLE) $(OUTPUT_DIR)/wasm-table.ts node_modules $(OUTPUT_DIR)/standalone.mjs 50 | $(shell npm bin)/tsc -p ./tsconfig.json 51 | JEST_JUNIT_OUTPUT_DIR=$(OUTPUT_DIR) OUTPUT_DIR=$(OUTPUT_DIR) NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 \ 52 | $(shell npm bin)/jest --detectOpenHandles -i 53 | 54 | $(OUTPUT_DIR)/wasm-table.ts: $(WASI_TEST_SUITE_DST_TESTS) $(WASMTIME_DST_TESTS) 55 | mkdir -p $(@D) 56 | node ./generate-wasm-table.mjs $(OUTPUT_DIR) > $@ 57 | 58 | $(MEMFS_DST): $(MEMFS_SRC) 59 | mkdir -p $(@D) 60 | cp $< $@ 61 | 62 | $(BUNDLE): $(wildcard ../dist/**) $(wildcard ./driver/**) $(MEMFS_DST) $(OUTPUT_DIR)/wasm-table.ts node_modules 63 | mkdir -p $(@D) 64 | $(shell npm bin)/esbuild --bundle ./driver/worker.ts --outfile=$@ --format=esm --log-level=warning --external:*.wasm 65 | 66 | $(WASM_OPT): 67 | @$(call color,"downloading binaryen") 68 | mkdir -p $(@D) 69 | curl -Lo binaryen.tar.gz https://github.com/WebAssembly/binaryen/releases/download/version_100/binaryen-version_100-x86_64-linux.tar.gz 70 | echo '9057c8f3f0bbfec47a95985c8f0faad8cc2aa3932e94a7d6b705e245ed140e19 binaryen.tar.gz' | sha256sum -c 71 | tar zxvf binaryen.tar.gz --strip-components=1 --touch -C ../build/binaryen 72 | rm binaryen.tar.gz 73 | 74 | export WASI_CC := $(abspath ${WASI_SDK_PATH}/bin/clang) -target wasm32-wasi --sysroot=${WASI_SYSROOT} 75 | export WASI_CFLAGS := -Oz -flto 76 | export WASI_LDFLAGS := -flto -Wl,--allow-undefined 77 | 78 | $(BENCHMARK_DST)/%.wasm: $(WASI_SDK_PATH) $(WASM_OPT) $(BENCHMARK_SRC)/%.c 79 | mkdir -p $(BENCHMARK_DST) 80 | $(WASI_CC) $(WASI_CFLAGS) $(WASI_LDFLAGS) subjects/$*.c -o $(BENCHMARK_DST)/$*.wasm 81 | $(WASM_OPT) -g -O --asyncify $(BENCHMARK_DST)/$*.wasm -o $(BENCHMARK_DST)/$*.asyncify.wasm 82 | -------------------------------------------------------------------------------- /test/benchmark-build.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import * as esbuild from 'esbuild' 3 | import * as path from 'path' 4 | 5 | const OUT_DIR = '../build/test/' 6 | 7 | /** 8 | * Node currently can't import wasm files in the way that we do with Workers, it either throws an 9 | * error if you try to import a wasm file or imports an instantiated wasm instance. Whereas in 10 | * Workers we get a WebAssembly.Module as the default export if we import a wasm file, so this 11 | * plugin is to replicate that behavior in the bundle. 12 | * @type {import('esbuild').Plugin} 13 | */ 14 | const wasmLoaderPlugin = { 15 | name: 'wasm-module-loader', 16 | setup: (build) => { 17 | build.onResolve({ filter: /\.wasm$/ }, (args) => ({ 18 | path: args.path, 19 | namespace: 'wasm-module', 20 | })) 21 | 22 | build.onLoad({ filter: /.*/, namespace: 'wasm-module' }, (args) => ({ 23 | contents: ` 24 | import * as fs from 'fs'; 25 | import * as path from 'path'; 26 | import * as url from 'url'; 27 | 28 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); 29 | export default new WebAssembly.Module(fs.readFileSync(path.resolve(__dirname, '${args.path}'))); 30 | `, 31 | })) 32 | }, 33 | } 34 | 35 | esbuild.build({ 36 | bundle: true, 37 | outfile: path.join(OUT_DIR, 'standalone.mjs'), 38 | format: 'esm', 39 | logLevel: 'warning', 40 | entryPoints: ['./driver/standalone.ts'], 41 | plugins: [wasmLoaderPlugin], 42 | platform: 'node', 43 | }) 44 | -------------------------------------------------------------------------------- /test/benchmark.test.ts: -------------------------------------------------------------------------------- 1 | import * as child from 'node:child_process' 2 | import * as fs from 'node:fs' 3 | import { cwd } from 'node:process' 4 | import path from 'path/posix' 5 | import type { ExecOptions } from './driver/common' 6 | 7 | const { OUTPUT_DIR } = process.env 8 | const moduleNames = fs 9 | .readdirSync(`${OUTPUT_DIR}/benchmark`) 10 | .map((dirent) => `benchmark/${dirent}`) 11 | 12 | for (const modulePath of moduleNames) { 13 | const prettyName = modulePath.split('/').pop() 14 | if (!prettyName) throw new Error('unreachable') 15 | 16 | test(`${prettyName}`, async () => { 17 | const execOptions: ExecOptions = { 18 | moduleName: prettyName, 19 | asyncify: prettyName.endsWith('.asyncify.wasm'), 20 | fs: {}, 21 | preopens: [], 22 | returnOnExit: false, 23 | } 24 | 25 | // Spawns a child process that runs the wasm so we can isolate the profiling to just that 26 | // specific test case. 27 | const proc = child.execFile( 28 | `node`, 29 | [ 30 | '--experimental-vm-modules', 31 | '--cpu-prof', 32 | '--cpu-prof-dir=./prof', 33 | `--cpu-prof-name=${prettyName}.${Date.now()}.cpuprofile`, 34 | 'standalone.mjs', 35 | modulePath, 36 | JSON.stringify(execOptions), 37 | ], 38 | { 39 | encoding: 'utf8', 40 | cwd: OUTPUT_DIR, 41 | } 42 | ) 43 | 44 | let stderr = '' 45 | proc.stderr?.on('data', (data) => (stderr += data)) 46 | 47 | const exitCode = await new Promise((resolve) => proc.once('exit', resolve)) 48 | 49 | if (exitCode !== 0) { 50 | console.error(`Child process exited with code ${exitCode}:\n${stderr}`) 51 | } 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /test/driver/common.ts: -------------------------------------------------------------------------------- 1 | import { Environment, WASI, _FS } from '@cloudflare/workers-wasi' 2 | 3 | export interface ExecOptions { 4 | args?: string[] 5 | asyncify: boolean 6 | env?: Environment 7 | fs: _FS 8 | moduleName: string 9 | preopens: string[] 10 | returnOnExit: boolean 11 | stdin?: string 12 | } 13 | 14 | export interface ExecResult { 15 | stdout: string 16 | stderr: string 17 | status?: number 18 | } 19 | 20 | export const exec = async ( 21 | options: ExecOptions, 22 | wasm: WebAssembly.Module, 23 | body?: ReadableStream 24 | ): Promise => { 25 | let TransformStream = global.TransformStream 26 | 27 | if (TransformStream === undefined) { 28 | const streams = await import('node:stream/web').catch(() => { 29 | throw new Error('unreachable') 30 | }) 31 | 32 | TransformStream = streams.TransformStream as typeof TransformStream 33 | } 34 | 35 | const stdout = new TransformStream() 36 | const stderr = new TransformStream() 37 | 38 | const wasi = new WASI({ 39 | args: options.args, 40 | env: options.env, 41 | fs: options.fs, 42 | preopens: options.preopens, 43 | returnOnExit: options.returnOnExit, 44 | stderr: stderr.writable, 45 | stdin: body, 46 | stdout: stdout.writable, 47 | streamStdio: options.asyncify, 48 | }) 49 | const instance = new WebAssembly.Instance(wasm, { 50 | wasi_snapshot_preview1: wasi.wasiImport, 51 | }) 52 | const promise = wasi.start(instance) 53 | 54 | const streams = await Promise.all([ 55 | collectStream(stdout.readable), 56 | collectStream(stderr.readable), 57 | ]) 58 | 59 | try { 60 | const result = { 61 | stdout: streams[0], 62 | stderr: streams[1], 63 | status: await promise, 64 | } 65 | return result 66 | } catch (e: any) { 67 | e.message = `${e}\n\nstdout:\n${streams[0]}\n\nstderr:\n${streams[1]}\n\n` 68 | throw e 69 | } 70 | } 71 | 72 | const collectStream = async (stream: ReadableStream): Promise => { 73 | const chunks: Uint8Array[] = [] 74 | 75 | // @ts-ignore 76 | for await (const chunk of stream) { 77 | chunks.push(chunk) 78 | } 79 | 80 | const size = chunks.reduce((acc, chunk) => acc + chunk.byteLength, 0) 81 | const buffer = new Uint8Array(size) 82 | let offset = 0 83 | 84 | chunks.forEach((chunk) => { 85 | buffer.set(chunk, offset) 86 | offset += chunk.byteLength 87 | }) 88 | 89 | return new TextDecoder().decode(buffer) 90 | } 91 | -------------------------------------------------------------------------------- /test/driver/standalone.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs/promises' 2 | import { ReadableStream } from 'node:stream/web' 3 | import { exec } from './common' 4 | 5 | const [modulePath, rawOptions] = process.argv.slice(2) 6 | const options = JSON.parse(rawOptions) 7 | 8 | const nulls = new Uint8Array(4096).fill(0) 9 | let written = 0 10 | 11 | const stdinStream = new ReadableStream({ 12 | pull: (controller) => { 13 | if (written > 1_000_000) { 14 | controller.close() 15 | } else { 16 | controller.enqueue(nulls) 17 | written += nulls.byteLength 18 | } 19 | }, 20 | }) 21 | 22 | fs.readFile(modulePath) 23 | .then((wasmBytes) => new WebAssembly.Module(wasmBytes)) 24 | .then((wasmModule) => exec(options, wasmModule, stdinStream as any)) 25 | .then((result) => { 26 | console.log(result.stdout) 27 | console.error(result.stderr) 28 | process.exit(result.status) 29 | }) 30 | -------------------------------------------------------------------------------- /test/driver/worker.ts: -------------------------------------------------------------------------------- 1 | import { ModuleTable } from '../../build/test/wasm-table' 2 | import { ExecOptions, exec } from './common' 3 | 4 | export default { 5 | async fetch(request: Request) { 6 | const options: ExecOptions = JSON.parse( 7 | atob(request.headers.get('EXEC_OPTIONS')!) 8 | ) 9 | 10 | const result = await exec( 11 | options, 12 | ModuleTable[options.moduleName], 13 | request.body ?? undefined 14 | ) 15 | return new Response(JSON.stringify(result)) 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /test/driver/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "worker" 2 | type = "javascript" 3 | route = '' 4 | zone_id = '' 5 | usage_model = '' 6 | compatibility_flags = [] 7 | workers_dev = true 8 | compatibility_date = "2021-10-05" 9 | 10 | [build] 11 | 12 | [build.upload] 13 | format = "modules" 14 | main = "./index.mjs" 15 | dir = "../../build/test/" 16 | 17 | [[build.upload.rules]] 18 | type = "CompiledWasm" 19 | globs = ["**/*.wasm"] 20 | -------------------------------------------------------------------------------- /test/generate-wasm-table.mjs: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as fs from 'fs/promises' 3 | 4 | const recurseFiles = async (dir) => { 5 | const entries = await fs.readdir(dir, { withFileTypes: true }) 6 | return ( 7 | await Promise.all( 8 | entries.map(async (dirent) => { 9 | if (dirent.isDirectory()) { 10 | return recurseFiles(path.join(dir, dirent.name)) 11 | } 12 | if (!dirent.name.endsWith('.wasm')) { 13 | return [] 14 | } 15 | 16 | return [path.resolve(dir, dirent.name)] 17 | }) 18 | ) 19 | ).flat() 20 | } 21 | 22 | const dir = path.resolve(process.argv[2]) 23 | const files = (await recurseFiles(dir)).map((f) => 24 | path.join('./', f.substr(dir.length)) 25 | ) 26 | 27 | for (const file of files) { 28 | console.log('// @ts-ignore') 29 | const identifier = file.replace(/[-\/\.]/g, '_') 30 | console.log(`import ${identifier} from '${file}'`) 31 | } 32 | console.log() 33 | 34 | console.log( 35 | 'export const ModuleTable: { [key: string]: WebAssembly.Module } = {' 36 | ) 37 | for (const file of files) { 38 | const identifier = file.replace(/[-\/\.]/g, '_') 39 | console.log(` '${file}': ${identifier},`) 40 | } 41 | 42 | console.log('}') 43 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "private": true, 4 | "devDependencies": { 5 | "@cloudflare/workers-types": "^3.2.0", 6 | "@types/jest": "^27.0.2", 7 | "@types/node": "^17.0.16", 8 | "esbuild": "^0.13.13", 9 | "jest": "^27.3.1", 10 | "jest-junit": "^13.0.0", 11 | "miniflare": "^1.4.1" 12 | }, 13 | "dependencies": { 14 | "@cloudflare/workers-wasi": "file:.." 15 | }, 16 | "jest-junit": { 17 | "usePathForSuiteName": "true" 18 | }, 19 | "jest": { 20 | "extensionsToTreatAsEsm": [ 21 | ".ts" 22 | ], 23 | "reporters": [ 24 | "default", 25 | "jest-junit" 26 | ], 27 | "verbose": true, 28 | "testRegex": "/.*\\.test\\.ts$", 29 | "transform": { 30 | "^.+\\.tsx?$": "./transform.js" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/subjects/read.c: -------------------------------------------------------------------------------- 1 | #include "assert.h" 2 | #include "stdint.h" 3 | #include "stdio.h" 4 | #include "string.h" 5 | 6 | #define CHUNK_SIZE 4096 7 | #define ITERATIONS 1024 8 | 9 | int main() { 10 | const char *chunk_buf[CHUNK_SIZE] = {0}; 11 | 12 | for (int iterations = 0; iterations < ITERATIONS; iterations++) { 13 | fread(chunk_buf, 1, CHUNK_SIZE, stdin); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/subjects/write.c: -------------------------------------------------------------------------------- 1 | #include "assert.h" 2 | #include "stdint.h" 3 | #include "stdio.h" 4 | #include "string.h" 5 | 6 | #define CHUNK_SIZE 4096 7 | #define ITERATIONS 1024 8 | 9 | int main() { 10 | const char *chunk_buf[CHUNK_SIZE] = {0}; 11 | 12 | for (int iterations = 0; iterations < ITERATIONS; iterations++) { 13 | fwrite(chunk_buf, 1, CHUNK_SIZE, stdout); 14 | } 15 | 16 | fflush(stdout); 17 | } 18 | -------------------------------------------------------------------------------- /test/transform.js: -------------------------------------------------------------------------------- 1 | const { transformSync } = require('esbuild') 2 | 3 | exports.process = (code, file) => { 4 | const options = { 5 | target: 'esnext', 6 | format: 'esm', 7 | loader: 'ts', 8 | sourcemap: 'inline', 9 | sourcefile: file, 10 | } 11 | return transformSync(code, options).code 12 | } 13 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "emitDeclarationOnly": false, 6 | "skipLibCheck": true, 7 | "lib": ["ESNext", "DOM"], 8 | "types": ["@cloudflare/workers-types", "@types/node", "@types/jest"] 9 | }, 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | import { Miniflare } from 'miniflare' 2 | import { Buffer } from 'buffer' 3 | import { ExecOptions, ExecResult } from './driver/common' 4 | import fs from 'fs/promises' 5 | import path from 'path' 6 | import type { _FS } from '@cloudflare/workers-wasi' 7 | 8 | export class TestEnv { 9 | #mf: Miniflare 10 | 11 | constructor(mf: Miniflare) { 12 | this.#mf = mf 13 | } 14 | 15 | async exec(config: ExecOptions): Promise { 16 | const response = await this.#mf.dispatchFetch('http://localhost:8787', { 17 | body: config.stdin, 18 | method: 'POST', 19 | headers: { 20 | EXEC_OPTIONS: Buffer.from(JSON.stringify(config)).toString('base64'), 21 | }, 22 | }) 23 | const body = await response.text() 24 | return JSON.parse(body) 25 | } 26 | } 27 | 28 | export const withEnv = async (callback: (env: TestEnv) => Promise) => { 29 | const mf = new Miniflare({ 30 | wranglerConfigPath: './driver/wrangler.toml', 31 | }) 32 | 33 | await callback(new TestEnv(mf)) 34 | await mf.dispose() 35 | } 36 | 37 | export const filesWithExt = async ( 38 | dir: string, 39 | ext: string 40 | ): Promise => { 41 | const files = await fs.readdir(dir) 42 | return files.filter((path) => path.endsWith(ext)) 43 | } 44 | 45 | async function mapENOENT(f: () => Promise): Promise { 46 | try { 47 | return await f() 48 | } catch (e) { 49 | if ((e as any).code === 'ENOENT') { 50 | return Promise.resolve(undefined) 51 | } 52 | throw e 53 | } 54 | } 55 | 56 | export const maybeReadContents = async ( 57 | file: string 58 | ): Promise => { 59 | return mapENOENT(async () => { 60 | return new TextDecoder().decode(await fs.readFile(file)) 61 | }) 62 | } 63 | 64 | export const withExtension = (file: string, ext: string) => { 65 | const basename = path.basename(file, path.extname(file)) 66 | return path.join(path.dirname(file), basename + ext) 67 | } 68 | 69 | const recurseFiles = async (dir: string): Promise => { 70 | const entries = await fs.readdir(dir, { withFileTypes: true }) 71 | return ( 72 | await Promise.all( 73 | entries.map(async (dirent): Promise => { 74 | if (dirent.isDirectory()) { 75 | return recurseFiles(path.join(dir, dirent.name)) 76 | } 77 | 78 | return [path.join(dir, dirent.name)] 79 | }) 80 | ) 81 | ).flat() 82 | } 83 | 84 | export const readfs = async (dir: string): Promise<_FS> => { 85 | const prefix = path.dirname(dir) 86 | const files = await mapENOENT(async () => await recurseFiles(dir)) 87 | if (files === undefined) { 88 | return {} 89 | } 90 | 91 | const result = Object.fromEntries( 92 | await Promise.all( 93 | files.map(async (file) => { 94 | const contents = await fs.readFile(file) 95 | const resolved = file.substr(prefix.length) 96 | return [resolved, contents.toString('utf8')] 97 | }) 98 | ) 99 | ) 100 | return result 101 | } 102 | -------------------------------------------------------------------------------- /test/wasi-test-suite.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | import path from 'path' 3 | import * as utils from './utils' 4 | import type { Environment } from '@cloudflare/workers-wasi' 5 | 6 | interface Config { 7 | env?: Environment 8 | status?: number 9 | stdin?: string 10 | stdout?: string 11 | args?: string[] 12 | } 13 | 14 | const parseEnvironment = (x: string): Environment => { 15 | return Object.fromEntries( 16 | x 17 | .trim() 18 | .split('\n') 19 | .map((line) => { 20 | return line.split('=') 21 | }) 22 | ) 23 | } 24 | 25 | const parseStatus = (x: string): number => { 26 | return parseInt(x) 27 | } 28 | 29 | const parseArgs = (x: string): string[] => { 30 | const s = x.trim() 31 | return s ? s.split('\n') : [] 32 | } 33 | 34 | const readConfig = async (file: string): Promise => { 35 | const mapContents = async ( 36 | ext: string, 37 | parse: (x: string) => T = (x: any) => x 38 | ): Promise => { 39 | const x = await utils.maybeReadContents(utils.withExtension(file, ext)) 40 | if (x !== undefined) { 41 | return parse(x) 42 | } 43 | return undefined 44 | } 45 | 46 | return { 47 | env: await mapContents('.env', parseEnvironment), 48 | stdin: await mapContents('.stdin'), 49 | stdout: await mapContents('.stdout'), 50 | status: await mapContents('.status', parseStatus), 51 | args: await mapContents('.arg', parseArgs), 52 | } 53 | } 54 | 55 | const generateTestCases = async ( 56 | fixture: utils.TestEnv, 57 | asyncify: boolean, 58 | dir: string 59 | ) => { 60 | const wasmFiles = await utils.filesWithExt(dir, '.wasm') 61 | for (const file of wasmFiles) { 62 | const absFile = path.resolve(path.join(dir, file)) 63 | const config = await readConfig(absFile) 64 | const args = config.args 65 | if (args != undefined) { 66 | args.unshift(file) 67 | } 68 | 69 | const moduleName = path.join('wasi-test-suite', path.basename(dir), file) 70 | 71 | const preopensDir = path.basename(utils.withExtension(file, '.dir')) 72 | const fs = await utils.readfs(utils.withExtension(absFile, '.dir')) 73 | test(moduleName, async () => { 74 | const result = await fixture.exec({ 75 | preopens: ['/' + preopensDir], 76 | fs, 77 | asyncify, 78 | args, 79 | env: config.env, 80 | moduleName, 81 | stdin: config.stdin, 82 | returnOnExit: config.status !== undefined, 83 | }) 84 | if (config.status) { 85 | expect(result.status).toBe(config.status) 86 | } 87 | if (config.stdout) { 88 | expect(result.stdout).toBe(config.stdout) 89 | } 90 | }) 91 | } 92 | } 93 | 94 | await utils.withEnv(async (fixture: utils.TestEnv) => { 95 | await generateTestCases(fixture, true, '../deps/wasi-test-suite/libc/') 96 | await generateTestCases(fixture, true, '../deps/wasi-test-suite/libstd/') 97 | 98 | // assemblyscript tests not compatible with default asyncify memory layout 99 | await generateTestCases(fixture, false, '../deps/wasi-test-suite/core/') 100 | }) 101 | -------------------------------------------------------------------------------- /test/wasmtime-wasi-tests.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { withEnv, TestEnv, filesWithExt } from './utils' 3 | 4 | const todos = new Set([ 5 | 'wasmtime/dangling_symlink.wasm', 6 | 'wasmtime/fd_readdir.wasm', 7 | 'wasmtime/file_unbuffered_write.wasm', 8 | 'wasmtime/interesting_paths.wasm', 9 | 'wasmtime/nofollow_errors.wasm', 10 | 'wasmtime/path_exists.wasm', 11 | 'wasmtime/path_link.wasm', 12 | 'wasmtime/path_symlink_trailing_slashes.wasm', 13 | 'wasmtime/poll_oneoff_files.wasm', 14 | 'wasmtime/poll_oneoff_stdio.wasm', 15 | 'wasmtime/readlink.wasm', 16 | 'wasmtime/symlink_create.wasm', 17 | 'wasmtime/symlink_filestat.wasm', 18 | 'wasmtime/symlink_loop.wasm', 19 | ]) 20 | 21 | await withEnv(async (fixture: TestEnv) => { 22 | const dir = '../build/test/wasmtime/' 23 | const files = await filesWithExt(dir, '.wasm') 24 | for (const file of files) { 25 | const name = path.join(path.basename(dir), file) 26 | const fn = todos.has(name) ? test.skip : test 27 | fn(name, async () => { 28 | await fixture.exec({ 29 | preopens: ['/tmp'], 30 | fs: { 31 | '/tmp/.gitkeep': '', 32 | }, 33 | asyncify: false, 34 | args: [file, '/tmp'], 35 | moduleName: name, 36 | returnOnExit: false, 37 | }) 38 | }) 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "strict": true, 5 | "target": "es2020", 6 | "lib": ["ESNext"], 7 | "typeRoots": [], 8 | "declaration": true, 9 | "isolatedModules": true, 10 | "emitDeclarationOnly": true, 11 | "declarationDir": "dist", 12 | "module": "esnext", 13 | "esModuleInterop": true, 14 | "types": [ 15 | "@cloudflare/workers-types" 16 | ] 17 | }, 18 | "include": ["src/**/*"] 19 | } 20 | 21 | --------------------------------------------------------------------------------