diff --git a/.cargo/config.toml b/.cargo/config.toml index a7dc7ad..83278d5 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,17 @@ [target.wasm32-unknown-unknown] -rustflags = ['--cfg', 'getrandom_backend="wasm_js"'] -runner = 'wasm-bindgen-test-runner' \ No newline at end of file +rustflags = [ + "--cfg", "getrandom_backend=\"wasm_js\"", + "-C", "target-feature=+atomics,+bulk-memory", + "-C", "link-arg=--shared-memory", + "-C", "link-arg=--max-memory=1073741824", + "-C", "link-arg=--import-memory", + "-C", "link-arg=--export=__heap_base", + "-C", "link-arg=--export=__wasm_init_tls", + "-C", "link-arg=--export=__tls_size", + "-C", "link-arg=--export=__tls_align", + "-C", "link-arg=--export=__tls_base" +] +runner = "wasm-bindgen-test-runner" + +[unstable] +build-std = ["std", "panic_abort"] \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8d3473..f0bc5e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,70 +14,57 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v14 + - name: Magic Nix Cache + uses: DeterminateSystems/magic-nix-cache-action@v8 + - name: Rust Cache + uses: Swatinem/rust-cache@v2 - - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-wasip1 - - - uses: Swatinem/rust-cache@v2 - - - name: Build plugin - run: cargo build --target wasm32-wasip1 -p test-plugin --release - - - name: Optimize Wasm - uses: NiklasEi/wasm-opt-action@v2 - with: - file: target/wasm32-wasip1/release/*.wasm - optimize_all: true - - - run: cargo build - - run: cargo test - - run: cargo bench + - run: nix-shell --run "make test-native" + - run: nix-shell --run "make bench-native" test-wasm: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-wasip1, wasm32-unknown-unknown - - - uses: Swatinem/rust-cache@v2 - - - name: Build plugin - run: cargo build --target wasm32-wasip1 -p test-plugin --release - - - name: Optimize Wasm - uses: NiklasEi/wasm-opt-action@v2 - with: - file: target/wasm32-wasip1/release/*.wasm - optimize_all: true - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - uses: jetli/wasm-bindgen-action@v0.2.0 - - - run: cargo build - - run: cargo test - - run: cargo bench + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v14 + - name: Magic Nix Cache + uses: DeterminateSystems/magic-nix-cache-action@v8 + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + + - run: nix-shell --run "make test-wasm" + - run: nix-shell --run "make bench-wasm" clippy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v14 + - name: Magic Nix Cache + uses: DeterminateSystems/magic-nix-cache-action@v8 + - name: Rust Cache + uses: Swatinem/rust-cache@v2 - - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-wasip1 - - - uses: Swatinem/rust-cache@v2 + - run: nix-shell --run "make clippy-native" + - run: nix-shell --run "make clippy-wasm" - - name: Build plugin - run: cargo build --target wasm32-wasip1 -p test-plugin --release - - - name: Run Clippy - run: cargo clippy --workspace --all-targets -- -D warnings \ No newline at end of file + check-artifacts: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v14 + - name: Magic Nix Cache + uses: DeterminateSystems/magic-nix-cache-action@v8 + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + + - name: Rebuild Wasm Artifacts + run: nix-shell --run "make build-all" + + - name: Ensure Artifacts Match Source + run: git diff --exit-code \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4a39dac..461dba6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -/target +target/ .direnv .envrc \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..52e9704 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "rust-analyzer.cargo.target": "wasm32-unknown-unknown", + "rust-analyzer.check.targets": "wasm32-unknown-unknown", + "rust-analyzer.cargo.features": "all", + "rust-analyzer.procMacro.enable": true +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index f4f73c2..2c694b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ - "gimli", + "gimli 0.32.3", ] [[package]] @@ -35,6 +35,12 @@ dependencies = [ "cc", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anes" version = "0.1.6" @@ -43,9 +49,21 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arbitrary" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" [[package]] name = "async-trait" @@ -74,22 +92,95 @@ dependencies = [ "cfg-if", "libc", "miniz_oxide", - "object", + "object 0.37.3", "rustc-demangle", "windows-link", ] +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.11.1", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + [[package]] name = "bitflags" -version = "2.10.0" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "bytecheck" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0caa33a2c0edca0419d15ac723dff03f1956f7978329b1e3b5fdaaaed9d3ca8b" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "rancor", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "bytesize" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" [[package]] name = "cast" @@ -99,14 +190,23 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.49" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -140,20 +240,31 @@ dependencies = [ "half", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" -version = "4.5.53" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstyle", "clap_lex", @@ -161,15 +272,204 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "corosensei" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c54787b605c7df106ceccf798df23da4f2e09918defad66705d1cedf3bb914f" +dependencies = [ + "autocfg", + "cfg-if", + "libc", + "scopeguard", + "windows-sys 0.59.0", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-assembler-x64" +version = "0.129.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b242b4c3675139f52f0b55624fb92571551a344305c5998f55ad20fa527bc55" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.129.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "499715f19799219f32641b14f2a162f91e50bc1b61c2d2184c2be971716f5c56" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.129.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a92d78cc3f087d7e7073828f08d98c7074a3a062b6b29a1b7783ce74305685e" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-bitset" +version = "0.129.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcc73d756f2e0d7eda6144fe64a2bc69c624de893cb1be51f1442aed77881d2" +dependencies = [ + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen" +version = "0.129.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d94c2cd0d73b41369b88da1129589bc3a2d99cf49979af1d14751f35b7a1b" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli 0.33.0", + "hashbrown 0.15.5", + "libm", + "log", + "regalloc2", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.129.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "483b2c94a1b7f6fba0714387ba34ca56d114b2214a80be018acbb2ed40e09a1e" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.129.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4aae718c336a52d90d4ebe9a2d8c3cf0906a4bee78f0e6867e777eebbe554fe" + +[[package]] +name = "cranelift-control" +version = "0.129.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18e94519070dc56cddb71906a08cea6a28a1d7c58ed501b88f273fa6b45fa07" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.129.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59d8e72637246edd2cba337939850caa8b201f6315925ec4c156fdd089999699" +dependencies = [ + "cranelift-bitset", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-frontend" +version = "0.129.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c31db0085c3dfa131e739c3b26f9f9c84d69a9459627aac1ac4ef8355e3411b" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.129.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524d804c1ebd8c542e6f64e71aa36934cec17c5da4a9ae3799796220317f5d23" + +[[package]] +name = "cranelift-srcgen" +version = "0.129.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a001a9dc4557d9e2be324bc932621c0aa9bf33b74dfefa2338f0bf8913329" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] [[package]] name = "criterion" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0dfe5e9e71bdcf4e4954f7d14da74d1cdb92a3a07686452d1509652684b1aab" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" dependencies = [ "alloca", "anes", @@ -177,7 +477,7 @@ dependencies = [ "ciborium", "clap", "criterion-plot", - "itertools", + "itertools 0.13.0", "num-traits", "oorandom", "page_size", @@ -192,12 +492,21 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de36c2bee19fba779808f92bf5d9b0fa5a40095c277aba10c458a12b35d21d6" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" dependencies = [ "cast", - "itertools", + "itertools 0.13.0", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", ] [[package]] @@ -219,6 +528,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -232,26 +550,142 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] -name = "dhat" -version = "0.3.3" +name = "crypto-common" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cd11d84628e233de0ce467de10b8633f4ddaecafadefc86e13b84b8739b827" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ - "backtrace", - "lazy_static", - "mintex", - "parking_lot", - "rustc-hash", - "serde", - "serde_json", - "thousands", + "hybrid-array", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", ] [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "enum-iterator" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "enumset" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "839c4174b41e75c8f7306110b2c51996a293b8d1d850edd529011841d9fede7d" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd536557b58c682b217b8fb199afdff47cd3eff260623f19e77074eb073d63a" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] [[package]] name = "equivalent" @@ -259,11 +693,53 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" @@ -271,11 +747,17 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -288,9 +770,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -298,15 +780,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -315,15 +797,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -332,21 +814,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -356,33 +838,36 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] [[package]] name = "getrandom" -version = "0.2.16" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "r-efi 5.3.0", + "wasip2", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.4" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", + "js-sys", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", + "wasip3", + "wasm-bindgen", ] [[package]] @@ -391,6 +876,24 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "gloo-timers" version = "0.3.0" @@ -414,13 +917,19 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -428,38 +937,93 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] [[package]] -name = "indexmap" -version = "2.12.1" +name = "itertools" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ - "equivalent", - "hashbrown 0.16.1", + "either", ] [[package]] name = "itertools" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -470,6 +1034,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545" + [[package]] name = "leb128fmt" version = "0.1.0" @@ -478,15 +1048,37 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libunwind" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6639b70a7ce854b79c70d7e83f16b5dc0137cc914f3d7d03803b513ecc67ac" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "lock_api" @@ -503,6 +1095,32 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + +[[package]] +name = "macho-unwind-info" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4bdc8b0ce69932332cf76d24af69c3a155242af95c226b2ab6c2e371ed1149" +dependencies = [ + "thiserror", + "zerocopy", + "zerocopy-derive", +] + [[package]] name = "matchers" version = "0.2.0" @@ -514,9 +1132,36 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmap2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d28bba84adfe6646737845bc5ebbfa2c08424eb1c37e94a1fd2a82adb56a872" +dependencies = [ + "libc", +] + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] [[package]] name = "minicov" @@ -528,6 +1173,12 @@ dependencies = [ "walkdir", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -535,13 +1186,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] -name = "mintex" -version = "0.1.4" +name = "more-asserts" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fafa6961cabd9c63bcd77a45d7e3b7f3b552b70417831fb0f56db717e72407e" + +[[package]] +name = "munge" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c" +dependencies = [ + "munge_macro", +] + +[[package]] +name = "munge_macro" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "nom" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c505b3e17ed6b70a7ed2e67fbb2c560ee327353556120d6e72f5232b6880d536" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] [[package]] name = "nu-ansi-term" @@ -549,7 +1231,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -571,11 +1253,25 @@ dependencies = [ "memchr", ] +[[package]] +name = "object" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271638cd5fa9cca89c4c304675ca658efc4e64a66c716b7cfe1afb4b9611dbbc" +dependencies = [ + "crc32fast", + "flate2", + "hashbrown 0.16.1", + "indexmap", + "memchr", + "ruzstd", +] + [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "oorandom" @@ -617,16 +1313,16 @@ dependencies = [ ] [[package]] -name = "pin-project-lite" -version = "0.2.16" +name = "paste" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] -name = "pin-utils" -version = "0.1.0" +name = "pin-project-lite" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "plotters" @@ -665,20 +1361,72 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] +[[package]] +name = "ptr_meta" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -689,11 +1437,26 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rancor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a063ea72381527c2a0561da9c80000ef822bdd7c3241b1cc1b12100e3df081ee" +dependencies = [ + "ptr_meta", +] + [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha", "rand_core", @@ -711,18 +1474,24 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rangemap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" + [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -744,14 +1513,28 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.11.1", +] + +[[package]] +name = "regalloc2" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08effbc1fa53aaebff69521a5c05640523fab037b34a4a2c109506bc938246fa" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.15.5", + "log", + "rustc-hash", + "smallvec", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -761,9 +1544,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -772,21 +1555,94 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "region" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b6ebd13bc009aef9cd476c1310d49ac354d36e240cf1bd753290f3dc7199a7" +dependencies = [ + "bitflags 1.3.2", + "libc", + "mach2 0.4.3", + "windows-sys 0.52.0", +] + +[[package]] +name = "rend" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "rkyv" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73389e0c99e664f919275ab5b5b0471391fe9a8de61e1dff9b1eaf56a90f16e3" +dependencies = [ + "bytecheck", + "bytes", + "hashbrown 0.17.1", + "indexmap", + "munge", + "ptr_meta", + "rancor", + "rend", + "rkyv_derive", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "5d2ed0b54125315fb36bd021e82d314d1c126548f871634b483f46b31d13cac6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "rustc-demangle" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc-hash" -version = "1.1.0" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] [[package]] name = "rustversion" @@ -795,10 +1651,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] -name = "ryu" -version = "1.0.20" +name = "ruzstd" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "a7c1c839d570d835527c9a5e4db7cb2198683a988cb9d7293fc8674e6bd58fc8" +dependencies = [ + "twox-hash", +] [[package]] name = "same-file" @@ -815,6 +1674,18 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.228" @@ -825,6 +1696,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -847,15 +1729,26 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", ] [[package]] @@ -867,17 +1760,39 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shared-buffer" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6c99835bad52957e7aa241d3975ed17c1e5f8c92026377d117a606f36b84b16" +dependencies = [ + "bytes", + "memmap2 0.6.2", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -886,39 +1801,59 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] -name = "spin" -version = "0.9.8" +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tar" +version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] [[package]] -name = "string-interner" -version = "0.19.0" +name = "target-lexicon" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23de088478b31c349c9ba67816fa55d9355232d63c3afea8bf513e31f0f1d2c0" -dependencies = [ - "hashbrown 0.15.5", - "serde", -] +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] -name = "syn" -version = "2.0.111" +name = "tempfile" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", ] [[package]] name = "test-plugin" version = "0.1.0" dependencies = [ + "futures", "rand", "serde_json", - "tokio", "tracing", "tracing-subscriber", "wasmi-plugin-pdk", @@ -926,30 +1861,24 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", "syn", ] -[[package]] -name = "thousands" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" - [[package]] name = "thread_local" version = "1.1.9" @@ -969,11 +1898,26 @@ dependencies = [ "serde_json", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" -version = "1.48.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "pin-project-lite", "tokio-macros", @@ -981,9 +1925,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -992,9 +1936,9 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -1014,9 +1958,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -1035,9 +1979,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -1053,9 +1997,9 @@ dependencies = [ [[package]] name = "tracing-test" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "557b891436fe0d5e0e363427fc7f217abf9ccd510d5136549847bdcbcd011d68" +checksum = "19a4c448db514d4f24c5ddb9f73f2ee71bfb24c526cf0c570ba142d1119e0051" dependencies = [ "tracing-core", "tracing-subscriber", @@ -1064,19 +2008,48 @@ dependencies = [ [[package]] name = "tracing-test-macro" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" +checksum = "ad06847b7afb65c7866a36664b75c40b895e318cea4f71299f013fb22965329d" dependencies = [ "quote", "syn", ] +[[package]] +name = "tracing-wasm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" +dependencies = [ + "tracing", + "tracing-subscriber", + "wasm-bindgen", +] + +[[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-width" @@ -1084,13 +2057,19 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "uuid" -version = "1.19.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.2", "js-sys", "serde_core", "wasm-bindgen", @@ -1113,25 +2092,28 @@ dependencies = [ ] [[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" +name = "wasip2" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] [[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -1142,22 +2124,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1165,9 +2144,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", @@ -1178,18 +2157,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-bindgen-test" -version = "0.3.56" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e90e66d265d3a1efc0e72a54809ab90b9c0c515915c67cdf658689d2c22c6c" +checksum = "6bb55e2540ad1c56eec35fd63e2aea15f83b11ce487fd2de9ad11578dfc047ea" dependencies = [ "async-trait", "cast", @@ -1204,63 +2183,240 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", ] [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.56" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7150335716dce6028bead2b848e72f47b45e7b9422f64cccdc23bedca89affc1" +checksum = "caf0ca1bd612b988616bac1ab34c4e4290ef18f7148a1d8b7f31c150080e9295" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cda5ecc67248c48d3e705d3e03e00af905769b78b9d2a1678b663b8b9d4472" + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser 0.244.0", +] + [[package]] name = "wasm-encoder" -version = "0.243.0" +version = "0.250.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55db9c896d70bd9fa535ce83cd4e1f2ec3726b0edd2142079f594fc3be1cb35" +checksum = "2271adb766023046af314460f1fae02cc34ea16d736d93404d3b65be44270923" dependencies = [ "leb128fmt", - "wasmparser 0.243.0", + "wasmparser 0.250.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.244.0", + "wasmparser 0.244.0", ] [[package]] -name = "wasmi" -version = "1.0.3" +name = "wasmer" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "172f6e29cb11ad08894abcf257cd899a7e559734514ad3b08c16fc3dc1596439" +checksum = "57bf3ce47ae9ef62e35c0b23e89b72250548d6d960d0832a5638b05a701769a8" dependencies = [ - "spin", - "wasmi_collections", - "wasmi_core", - "wasmi_ir", - "wasmparser 0.228.0", + "bindgen", + "bytes", + "cfg-if", + "cmake", + "derive_more", + "indexmap", + "js-sys", + "more-asserts", + "paste", + "rustc-demangle", + "serde", + "serde-wasm-bindgen", + "shared-buffer", + "tar", + "target-lexicon", + "thiserror", + "tracing", + "wasm-bindgen", + "wasmer-compiler", + "wasmer-compiler-cranelift", + "wasmer-derive", + "wasmer-types", + "wasmer-vm", + "wasmparser 0.245.1", "wat", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmer-compiler" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1998787df9de3e84b66616766fb795e9aab954a8ca1e40eb92ef759e325bc782" +dependencies = [ + "backtrace", + "bytes", + "cfg-if", + "crossbeam-channel", + "enum-iterator", + "enumset", + "itertools 0.14.0", + "leb128", + "libc", + "macho-unwind-info", + "memmap2 0.9.10", + "more-asserts", + "object 0.38.1", + "rangemap", + "rayon", + "region", + "rkyv", + "self_cell", + "shared-buffer", + "smallvec", + "target-lexicon", + "tempfile", + "thiserror", + "wasmer-types", + "wasmer-vm", + "wasmparser 0.245.1", + "which", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmer-compiler-cranelift" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757fd205d4e2aac6662fc75f4859e6675ab51f4436b86086d47bfa83439c8931" +dependencies = [ + "cranelift-codegen", + "cranelift-entity", + "cranelift-frontend", + "gimli 0.33.0", + "indexmap", + "itertools 0.14.0", + "leb128", + "more-asserts", + "rayon", + "smallvec", + "target-lexicon", + "tracing", + "wasmer-compiler", + "wasmer-types", +] + +[[package]] +name = "wasmer-derive" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80beffc36bead448bac84e1145d860523729c4e34e441332d56076a8a16b2d64" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasmer-types" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c805281d86063190ad76fbc12d6397aaf33ba60a5c9834e98ee0dcb8f889df7d" +dependencies = [ + "bytecheck", + "enum-iterator", + "enumset", + "getrandom 0.4.2", + "hex", + "indexmap", + "more-asserts", + "rkyv", + "sha2", + "target-lexicon", + "thiserror", +] + +[[package]] +name = "wasmer-vm" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2be14431ae698689440eadaa3d9563e266da4f889adda2749743f317680daa20" +dependencies = [ + "backtrace", + "bytesize", + "cc", + "cfg-if", + "corosensei", + "crossbeam-queue", + "dashmap", + "enum-iterator", + "fnv", + "gimli 0.33.0", + "indexmap", + "itertools 0.14.0", + "libc", + "libunwind", + "mach2 0.6.0", + "memoffset", + "more-asserts", + "parking_lot", + "region", + "rustversion", + "scopeguard", + "thiserror", + "wasmer-types", + "windows-sys 0.61.2", ] [[package]] name = "wasmi-plugin-hdk" version = "0.1.0" dependencies = [ - "async-trait", + "console_error_panic_hook", "criterion", - "dhat", "futures", - "getrandom 0.2.16", + "getrandom 0.3.4", + "gloo-timers", "serde", + "serde-wasm-bindgen", "serde_json", "thiserror", "tokio", "tracing", + "tracing-subscriber", "tracing-test", + "tracing-wasm", "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", "wasm-bindgen-test", - "wasmi", + "wasmer", "wasmi-plugin-pdk", "wasmi-plugin-rt", + "wasmi-plugin-wasi", + "web-sys", "web-time", ] @@ -1268,14 +2424,12 @@ dependencies = [ name = "wasmi-plugin-pdk" version = "0.1.0" dependencies = [ - "async-trait", "futures", "serde", "serde_json", "thiserror", "tokio", "tracing", - "tracing-subscriber", "wasmi-plugin-rt", ] @@ -1285,83 +2439,91 @@ version = "0.1.0" dependencies = [ "gloo-timers", "tokio", + "web-sys", "web-time", ] [[package]] -name = "wasmi_collections" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697e11ec2200705314d6a1437cf6bc04d5362b1b30f15c43b31eb7549a3b568e" +name = "wasmi-plugin-wasi" +version = "0.1.0" dependencies = [ - "string-interner", + "futures", + "getrandom 0.3.4", + "thiserror", + "tracing", + "wasmer", + "wasmi-plugin-rt", + "web-time", ] [[package]] -name = "wasmi_core" -version = "1.0.3" +name = "wasmparser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bb8d179d27c757e1475bf805d02cfc88c998fbc0265155fbf426c0e85dd7fb9" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "libm", + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", ] [[package]] -name = "wasmi_ir" -version = "1.0.3" +name = "wasmparser" +version = "0.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8551984e96c9c53c6a602f2babead4dea1122e8bbdaed456efb0881bf94b82c5" +checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" dependencies = [ - "wasmi_core", + "bitflags 2.11.1", ] [[package]] name = "wasmparser" -version = "0.228.0" +version = "0.250.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4abf1132c1fdf747d56bbc1bb52152400c70f336870f968b85e89ea422198ae3" +checksum = "071d99cdfb8111603ed05500506c3298a940b58d609dd0259d3981785dd33556" dependencies = [ - "bitflags", + "bitflags 2.11.1", "indexmap", + "semver", ] [[package]] -name = "wasmparser" -version = "0.243.0" +name = "wasmtime-internal-core" +version = "42.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d8db401b0528ec316dfbe579e6ab4152d61739cfe076706d2009127970159d" +checksum = "03a4a3f055a804a2f3d86e816a9df78a8fa57762212a8506164959224a40cd48" dependencies = [ - "bitflags", - "indexmap", + "libm", ] [[package]] name = "wast" -version = "243.0.0" +version = "250.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df21d01c2d91e46cb7a221d79e58a2d210ea02020d57c092e79255cc2999ca7f" +checksum = "69e9294a1f0204aeb5c47e95165517f43ef3cc895918c4f3e939380d4c290f4a" dependencies = [ "bumpalo", "leb128fmt", "memchr", "unicode-width", - "wasm-encoder", + "wasm-encoder 0.250.0", ] [[package]] name = "wat" -version = "1.243.0" +version = "1.250.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226a9a91cd80a50449312fef0c75c23478fcecfcc4092bdebe1dc8e760ef521b" +checksum = "0a549ed329a70e444e0f7796391ab2a87d0aef30ddde9f60e16e429224fafd02" dependencies = [ "wast", ] [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -1377,6 +2539,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "which" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459" +dependencies = [ + "libc", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1399,7 +2570,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1414,6 +2585,24 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1423,28 +2612,196 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.244.0", + "wasm-metadata", + "wasmparser 0.244.0", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.244.0", +] + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 826cf22..3c7af10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "crates/wasmi-plugin-hdk", "crates/wasmi-plugin-pdk", "crates/wasmi-plugin-rt", + "crates/wasmi-plugin-wasi", ] resolver = "2" @@ -21,20 +22,32 @@ categories = ["wasm", "virtualization"] exclude = ["tests", "benches"] [workspace.dependencies] -async-trait = "0.1" +chrono = { version = "0.4", features = ["wasmbind"] } +criterion = "0.8" futures = "0.3" -getrandom = { version = "0.2", features = ["js"] } +getrandom = { version = "0.3.4", features = ["wasm_js"] } gloo-timers = { version = "0.3", features = ["futures"] } rand = "0.9" serde = { version = "1", features = ["derive"] } +serde-wasm-bindgen = "0.6" serde_json = "1" thiserror = "2" tokio = { version = "1", features = ["rt", "time", "sync", "macros"] } tracing = "0.1" tracing-subscriber = "0.3" -uuid = { version = "1", features = [ "v4", "serde", "js" ] } +tracing-test = { version = "0.2" } +tracing-wasm = "0.2" +uuid = { version = "1", features = ["v4", "serde", "js"] } +# `wasm-bindgen` 0.2.119 - 0.2.121 introduced a backward-incompatible change that +# conflicts with wasmer 7.1. Once wasmer 7.2+ is released, check and update +# wasm-bindgen to the latest. +wasm-bindgen = "=0.2.118" wasm-bindgen-futures = "0.4" -wasmi = "1.0" +wasm-bindgen-test = "0.3" +wasmer = { version = "7", default-features = false } +wasmi-plugin-hdk = { version = "0.1.0", path = "./crates/wasmi-plugin-hdk" } wasmi-plugin-pdk = { version = "0.1.0", path = "./crates/wasmi-plugin-pdk" } wasmi-plugin-rt = { version = "0.1.0", path = "./crates/wasmi-plugin-rt" } +wasmi-plugin-wasi = { version = "0.1.0", path = "./crates/wasmi-plugin-wasi" } +web-sys = "0.3" web-time = "1" diff --git a/crates/test-plugin/Cargo.toml b/crates/test-plugin/Cargo.toml index b3784b2..921746d 100644 --- a/crates/test-plugin/Cargo.toml +++ b/crates/test-plugin/Cargo.toml @@ -14,13 +14,19 @@ exclude.workspace = true publish = false [dependencies] +futures = { workspace = true } rand = { workspace = true } serde_json = { workspace = true } -tokio = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } wasmi-plugin-pdk = { workspace = true } [profile.release] -lto = "fat" +opt-level = "z" +lto = "thin" codegen-units = 1 +panic = "abort" + +# Keep function names in release builds +strip = "none" +debug = "line-tables-only" diff --git a/crates/test-plugin/src/main.rs b/crates/test-plugin/src/main.rs index 6cda05d..204f1e0 100644 --- a/crates/test-plugin/src/main.rs +++ b/crates/test-plugin/src/main.rs @@ -1,60 +1,81 @@ use rand::Rng; use serde_json::{self, Value}; -use std::{io::stderr, sync::Arc}; -use tracing::{error, info, level_filters::LevelFilter}; +use std::io::stderr; +use tracing::{info, level_filters::LevelFilter}; use tracing_subscriber::fmt; use wasmi_plugin_pdk::{ - rpc_message::RpcError, - server::PluginServer, - transport::{JsonRpcTransport, Transport}, + rpc_message::{RpcError, RpcErrorContext}, + runner::PluginRunner, + transport::{AsyncTransport, SyncManyTransport, SyncTransport, Transport}, }; -async fn ping(_: Arc, _: ()) -> Result { +async fn ping(_: Transport, _: ()) -> Result { Ok(Value::String("pong".to_string())) } -async fn get_random_number(_: Arc, _: ()) -> Result { +async fn get_random_number(_: Transport, _: ()) -> Result { let mut rng = rand::rng(); let random_number: u64 = rng.random(); Ok(Value::Number(random_number.into())) } -async fn get_time(_: Arc, _: ()) -> Result { +async fn get_time(_: Transport, _: ()) -> Result { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .map_err(|e| { - error!("System time before UNIX EPOCH: {}", e); - RpcError::InternalError - })?; + .context("Time before UNIX_EPOCH")?; Ok(Value::Number(now.as_secs().into())) } -async fn sleep(_: Arc, duration_ms: u64) -> Result<(), RpcError> { +async fn sleep(_: Transport, duration_ms: u64) -> Result<(), RpcError> { info!("Sleeping for {} milliseconds", duration_ms); - tokio::time::sleep(std::time::Duration::from_millis(duration_ms)).await; + std::thread::sleep(std::time::Duration::from_millis(duration_ms)); info!("Woke up after sleeping for {} milliseconds", duration_ms); Ok(()) } -async fn many_echo(transport: Arc, limit: u64) -> Result<(), RpcError> { - for i in 0..limit { - let resp = transport.call("echo", Value::Number(i.into())).await?; +async fn call(transport: Transport, _: ()) -> Result { + let resp = transport.call("ping", Value::Null)?; + Ok(resp.result) +} - if resp.id != i { - error!("Incorrect response id: expected {}, got {}", i, resp.id); - return Err(RpcError::InternalError); - } +async fn call_many(transport: Transport, limit: u64) -> Result<(), RpcError> { + let mut calls = Vec::new(); + for _ in 0..limit { + calls.push(("ping", Value::Null)); + } - if resp.result != Value::Number(i.into()) { - error!("Incorrect response result: {:?}", resp.result); - return Err(RpcError::InternalError); - } + transport.call_many(calls)?; + + Ok(()) +} + +async fn call_async(transport: Transport, _: ()) -> Result { + let resp = transport.call_async("ping", Value::Null).await?; + Ok(resp.result) +} + +async fn call_many_async(transport: Transport, limit: u64) -> Result<(), RpcError> { + let mut tasks = vec![]; + for _ in 0..limit { + let transport_clone = transport.clone(); + let task = async move { + transport_clone.call_async("ping", Value::Null).await?; + Ok::<(), RpcError>(()) + }; + tasks.push(task); + } + + let resps: Vec> = + futures::future::join_all(tasks).await.into_iter().collect(); + + for resp in resps { + resp?; } Ok(()) } -async fn prime_sieve(_transport: Arc, limit: u64) -> Result { +async fn prime_sieve(_transport: Transport, limit: u64) -> Result { let limit = limit as usize; let primes = sieve_of_eratosthenes(limit); info!("Generated {} primes up to {}", primes.len(), limit); @@ -92,14 +113,16 @@ fn main() { .with_ansi(false) .without_time() .init(); - info!("Starting plugin..."); - PluginServer::new_with_transport() + PluginRunner::new() .with_method("ping", ping) .with_method("get_random_number", get_random_number) .with_method("get_time", get_time) .with_method("sleep", sleep) - .with_method("many_echo", many_echo) + .with_method("call", call) + .with_method("call_many", call_many) + .with_method("call_async", call_async) + .with_method("call_many_async", call_many_async) .with_method("prime_sieve", prime_sieve) .run(); } diff --git a/crates/wasmi-plugin-hdk/Cargo.toml b/crates/wasmi-plugin-hdk/Cargo.toml index ac9d754..2563165 100644 --- a/crates/wasmi-plugin-hdk/Cargo.toml +++ b/crates/wasmi-plugin-hdk/Cargo.toml @@ -13,28 +13,57 @@ categories.workspace = true exclude.workspace = true [dependencies] -async-trait = { workspace = true } futures = { workspace = true } -getrandom = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } uuid = { workspace = true } -wasmi = { workspace = true } wasmi-plugin-pdk = { workspace = true } wasmi-plugin-rt = { workspace = true } +wasmi-plugin-wasi = { workspace = true } web-time = { workspace = true } -[dev-dependencies] -dhat = "0.3" +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { workspace = true } -tracing-test = { version = "0.2.5", features = ["no-env-filter"] } -wasm-bindgen-test = "0.3" +wasmer = { workspace = true, default-features = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +gloo-timers = { workspace = true } +serde-wasm-bindgen = { workspace = true } +wasm-bindgen = { workspace = true } +wasm-bindgen-futures = { workspace = true } +wasmer = { workspace = true, features = [ + "js-default", + "js-serializable-module", +] } +web-sys = { workspace = true, features = [ + "Worker", + "WorkerOptions", + "DedicatedWorkerGlobalScope", + "WorkerType", + "MessageEvent", + "ErrorEvent", + "Blob", + "BlobPropertyBag", + "Url", + "console", + "TextDecoder", +] } +console_error_panic_hook = "0.1.7" +tracing-wasm = { workspace = true } +getrandom = { workspace = true } + +[dev-dependencies] +tracing-subscriber = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +criterion = { workspace = true } +tracing-test = { workspace = true, features = ["no-env-filter"] } -# Disables criterion for wasm targets since it's not compatible -[target.'cfg(not(target_family = "wasm"))'.dev-dependencies] -criterion = "0.8.0" +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +tracing-wasm = { workspace = true } +wasm-bindgen-test = { workspace = true } [[bench]] name = "benchmark" diff --git a/crates/wasmi-plugin-hdk/benches/benchmark.rs b/crates/wasmi-plugin-hdk/benches/benchmark.rs index af02a12..766a6e6 100644 --- a/crates/wasmi-plugin-hdk/benches/benchmark.rs +++ b/crates/wasmi-plugin-hdk/benches/benchmark.rs @@ -1,115 +1,199 @@ -use criterion::{Criterion, criterion_group, criterion_main}; -use serde_json::Value; -use std::sync::Arc; -use tokio::runtime::Builder; -use wasmi_plugin_hdk::{ - plugin::{Plugin, PluginId}, - server::HostServer, -}; -use wasmi_plugin_pdk::transport::Transport; - -const PLUGIN_WASM: &[u8] = include_bytes!("../../../target/wasm32-wasip1/release/test-plugin.wasm"); - -fn load_plugin_wasm() -> Vec { - PLUGIN_WASM.to_vec() +#[cfg(not(target_arch = "wasm32"))] +mod benchmarks { + use criterion::{Criterion, criterion_group}; + use serde_json::Value; + use std::{ + sync::{Arc, Once}, + time::Duration, + }; + use tokio::runtime::Builder; + use wasmi_plugin_hdk::{plugin::Plugin, plugin_id::PluginId, server::HostServer}; + use wasmi_plugin_pdk::transport::AsyncTransport; + + const PLUGIN_WASM: &[u8] = + include_bytes!("../../../target/wasm32-wasip1/release/test-plugin.wasm"); + + static INIT: Once = Once::new(); + + fn load_plugin_wasm() -> Vec { + PLUGIN_WASM.to_vec() + } + + async fn load_plugin() -> Plugin { + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + let plugin = Plugin::builder("test_plugin", wasm_bytes.clone(), handler) + .with_timeout(Duration::from_secs(10)) + .build() + .await + .unwrap(); + + plugin + } + + fn get_host_server() -> HostServer<(Option, ())> { + HostServer::default() + .with_method( + "ping", + |_, _params: ()| async move { Ok("pong".to_string()) }, + ) + .with_method("echo", |_, params: Value| async move { Ok(params) }) + } + + fn setup_logs() { + INIT.call_once(|| { + // tracing_subscriber::fmt() + // .compact() + // .with_ansi(false) + // .with_max_level(tracing_subscriber::filter::LevelFilter::INFO) + // .with_test_writer() + // .init(); + }); + } + + /// Benchmark the full lifecycle of creating a plugin instance and calling a ping request. + /// Tests the overhead of plugin instantiation. + pub fn bench_lifecycle(c: &mut Criterion) { + setup_logs(); + + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + + c.bench_function("lifecycle", |b| { + b.iter(|| { + let fut = async { + let plugin = + Plugin::builder("test_plugin", wasm_bytes.clone(), handler.clone()) + .with_timeout(Duration::from_secs(10)) + .build() + .await + .unwrap(); + plugin.call_async("ping", Value::Null).await.unwrap(); + }; + + rt.block_on(fut); + }) + }); + } + + /// Benchmark a single ping request to the wasm module. + pub fn bench_ping(c: &mut Criterion) { + setup_logs(); + + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + let plugin = rt.block_on(load_plugin()); + + c.bench_function("ping", |b| { + b.iter(|| { + let fut = async { + plugin.call_async("ping", Value::Null).await.unwrap(); + }; + + rt.block_on(fut); + }) + }); + } + + /// Benchmark the prime sieve function with a small input. Primarily tests the overhead + /// of calling into the wasm module. + pub fn bench_prime_sieve_small(c: &mut Criterion) { + setup_logs(); + + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + let plugin = rt.block_on(load_plugin()); + + c.bench_function("prime_sieve_small", |b| { + b.iter(|| { + let fut = async { + plugin + .call_async("prime_sieve", Value::Number(1.into())) + .await + .unwrap(); + }; + + rt.block_on(fut); + }) + }); + } + + /// Benchmark the prime sieve function with a large input. Tests performance within the wasm module. + pub fn bench_prime_sieve_large(c: &mut Criterion) { + setup_logs(); + + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + let plugin = rt.block_on(load_plugin()); + + c.bench_function("prime_sieve_large", |b| { + b.iter(|| { + let fut = async { + plugin + .call_async("prime_sieve", Value::Number(1_000_000.into())) + .await + .unwrap(); + }; + + rt.block_on(fut); + }) + }); + } + + /// Benchmark sending many echo requests to the host, and receiving responses. + pub fn bench_call_many(c: &mut Criterion) { + setup_logs(); + + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + let plugin = rt.block_on(load_plugin()); + + c.bench_function("call_many", |b| { + b.iter(|| { + let fut = async { + plugin + .call_async("call_many", Value::Number(200.into())) + .await + .unwrap(); + }; + + rt.block_on(fut); + }) + }); + } + + /// Benchmark sending many echo requests to the host, and receiving responses. + pub fn bench_call_many_async(c: &mut Criterion) { + setup_logs(); + + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + let plugin = rt.block_on(load_plugin()); + + c.bench_function("call_many_async", |b| { + b.iter(|| { + let fut = async { + plugin + .call_async("call_many_async", Value::Number(200.into())) + .await + .unwrap(); + }; + + rt.block_on(fut); + }) + }); + } + + criterion_group!( + benches, + bench_ping, + bench_lifecycle, + bench_prime_sieve_small, + bench_prime_sieve_large, + bench_call_many, + bench_call_many_async + ); } -fn get_host_server() -> HostServer<(Option, ())> { - HostServer::default().with_method("echo", |_, params: Value| async move { Ok(params) }) -} - -/// Benchmark a single ping request to the wasm module. -pub fn bench_ping(c: &mut Criterion) { - let rt = Builder::new_current_thread().enable_all().build().unwrap(); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler.clone()).unwrap(); - - c.bench_function("ping", |b| { - b.iter(|| { - let fut = async { - plugin.call("ping", Value::Null).await.unwrap(); - }; - - rt.block_on(fut); - }) - }); -} - -/// Benchmark the prime sieve function with a small input. Primarily tests the overhead -/// of calling into the wasm module. -pub fn bench_prime_sieve_small(c: &mut Criterion) { - let rt = Builder::new_current_thread().enable_all().build().unwrap(); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler).unwrap(); - - c.bench_function("prime_sieve_small", |b| { - b.iter(|| { - let fut = async { - plugin - .call("prime_sieve", Value::Number(1.into())) - .await - .unwrap(); - }; - - rt.block_on(fut); - }) - }); -} - -/// Benchmark the prime sieve function with a large input. Tests both the overhead -/// of calling into the wasm module and performance within the wasm module. -pub fn bench_prime_sieve_large(c: &mut Criterion) { - let rt = Builder::new_current_thread().enable_all().build().unwrap(); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler).unwrap(); - - c.bench_function("prime_sieve_large", |b| { - b.iter(|| { - let fut = async { - plugin - .call("prime_sieve", Value::Number(100_000.into())) - .await - .unwrap(); - }; - - rt.block_on(fut); - }) - }); -} - -/// Benchmark sending many echo requests to the host, and receiving responses. -pub fn bench_echo_many(c: &mut Criterion) { - let rt = Builder::new_current_thread().enable_all().build().unwrap(); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler).unwrap(); - - c.bench_function("many_echo", |b| { - b.iter(|| { - let fut = async { - plugin - .call("many_echo", Value::Number(200.into())) - .await - .unwrap(); - }; - - rt.block_on(fut); - }) - }); -} +#[cfg(not(target_arch = "wasm32"))] +criterion::criterion_main!(benchmarks::benches); -criterion_group!( - benches, - bench_ping, - bench_prime_sieve_small, - bench_prime_sieve_large, - bench_echo_many -); -criterion_main!(benches); +#[cfg(target_arch = "wasm32")] +fn main() {} diff --git a/crates/wasmi-plugin-hdk/benches/wasm_bench.rs b/crates/wasmi-plugin-hdk/benches/wasm_bench.rs new file mode 100644 index 0000000..bb48718 --- /dev/null +++ b/crates/wasmi-plugin-hdk/benches/wasm_bench.rs @@ -0,0 +1,188 @@ +#![cfg(target_arch = "wasm32")] +use serde_json::Value; +use std::{ + sync::{Arc, Once}, + time::Duration, +}; +use tracing::info; +use wasm_bindgen_test::{__rt::criterion::Criterion, wasm_bindgen_bench}; +use wasmi_plugin_hdk::{plugin::Plugin, plugin_id::PluginId, server::HostServer}; +use wasmi_plugin_pdk::transport::AsyncTransport; + +wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + +const PLUGIN_WASM: &[u8] = include_bytes!("../../../target/wasm32-wasip1/release/test-plugin.wasm"); + +static INIT: Once = Once::new(); + +fn load_plugin_wasm() -> Vec { + PLUGIN_WASM.to_vec() +} + +async fn load_plugin() -> Plugin { + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + let plugin = Plugin::builder("test_plugin", wasm_bytes, handler) + .with_timeout(Duration::from_secs(10)) + .build() + .await + .unwrap(); + + plugin +} + +fn get_host_server() -> HostServer<(Option, ())> { + HostServer::default() + .with_method( + "ping", + |_, _params: ()| async move { Ok("pong".to_string()) }, + ) + .with_method("echo", |_, params: Value| async move { Ok(params) }) +} + +fn setup_logs() { + INIT.call_once(|| { + // tracing_wasm::set_as_global_default_with_config( + // tracing_wasm::WASMLayerConfigBuilder::new() + // .set_console_config(tracing_wasm::ConsoleConfig::ReportWithoutConsoleColor) + // .set_max_level(tracing::Level::INFO) + // .build(), + // ); + }); +} + +/// Benchmark the full lifecycle of creating a plugin instance and calling a ping request. +/// Tests the overhead of plugin instantiation. +#[wasm_bindgen_bench] +async fn bench_lifecycle(c: &mut Criterion) { + setup_logs(); + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + + c.bench_async_function("lifecycle", |b| { + let wasm_bytes = wasm_bytes.clone(); + let handler = handler.clone(); + Box::pin(b.iter_future(move || { + let wasm_bytes = wasm_bytes.clone(); + let handler = handler.clone(); + async move { + let plugin = Plugin::builder("test_plugin", wasm_bytes.clone(), handler) + .build() + .await + .unwrap(); + plugin + .call_async("ping", serde_json::Value::Null) + .await + .unwrap(); + } + })) + }) + .await; +} + +#[wasm_bindgen_bench] +async fn bench_ping_wasm(c: &mut Criterion) { + setup_logs(); + let plugin = Arc::new(load_plugin().await); + + c.bench_async_function("ping", |b| { + let plugin = plugin.clone(); + Box::pin(b.iter_future(move || { + let plugin = plugin.clone(); + async move { + plugin + .call_async("ping", serde_json::Value::Null) + .await + .unwrap(); + info!("ping call completed"); + } + })) + }) + .await; +} + +/// Benchmark the prime sieve function with a small input. Primarily tests the overhead +/// of calling into the wasm module. +#[wasm_bindgen_bench] +async fn bench_prime_sieve_small(c: &mut Criterion) { + setup_logs(); + let plugin = Arc::new(load_plugin().await); + + c.bench_async_function("prime_sieve_small", |b| { + let plugin = plugin.clone(); + Box::pin(b.iter_future(move || { + let plugin = plugin.clone(); + async move { + plugin + .call_async("prime_sieve", Value::Number(1.into())) + .await + .unwrap(); + } + })) + }) + .await; +} + +/// Benchmark the prime sieve function with a large input. Tests performance within the wasm module. +#[wasm_bindgen_bench] +async fn bench_prime_sieve_large(c: &mut Criterion) { + setup_logs(); + let plugin = Arc::new(load_plugin().await); + + c.bench_async_function("prime_sieve_large", |b| { + let plugin = plugin.clone(); + Box::pin(b.iter_future(move || { + let plugin = plugin.clone(); + async move { + plugin + .call_async("prime_sieve", Value::Number(1_000_000.into())) + .await + .unwrap(); + } + })) + }) + .await; +} + +/// Benchmark sending many echo requests to the host, and receiving responses. +#[wasm_bindgen_bench] +async fn bench_call_many(c: &mut Criterion) { + setup_logs(); + let plugin = Arc::new(load_plugin().await); + + c.bench_async_function("call_many", |b| { + let plugin = plugin.clone(); + Box::pin(b.iter_future(move || { + let plugin = plugin.clone(); + async move { + info!("Starting call_many benchmark iteration..."); + plugin + .call_async("call_many", Value::Number(200.into())) + .await + .unwrap(); + } + })) + }) + .await; +} + +#[wasm_bindgen_bench] +async fn bench_call_many_async(c: &mut Criterion) { + setup_logs(); + let plugin = Arc::new(load_plugin().await); + + c.bench_async_function("call_many_async", |b| { + let plugin = plugin.clone(); + Box::pin(b.iter_future(move || { + let plugin = plugin.clone(); + async move { + info!("Starting call_many_async benchmark iteration..."); + plugin + .call_async("call_many_async", Value::Number(200.into())) + .await + .unwrap(); + } + })) + }) + .await; +} diff --git a/crates/wasmi-plugin-hdk/src/compile.rs b/crates/wasmi-plugin-hdk/src/compile.rs index e326da2..90fd326 100644 --- a/crates/wasmi-plugin-hdk/src/compile.rs +++ b/crates/wasmi-plugin-hdk/src/compile.rs @@ -1,14 +1,54 @@ -use wasmi::{Config, Engine, Module}; - -pub fn compile_plugin(wasm_bytes: Vec) -> Result<(Engine, Module), wasmi::Error> { - let mut config = Config::default(); - config.consume_fuel(true); - // https://github.com/wasmi-labs/wasmi/issues/1647 - // TODO: Switch to lazy execution, seems faster. - config.compilation_mode(wasmi::CompilationMode::Eager); - config.set_max_cached_stacks(100); - let engine = Engine::new(&config); - let module = Module::new(&engine, wasm_bytes)?; - - Ok((engine, module)) +#[cfg(target_arch = "wasm32")] +use std::sync::Arc; + +#[derive(Clone)] +pub struct Compiled { + pub name: String, + #[cfg(not(target_arch = "wasm32"))] + pub engine: wasmer::Engine, + #[cfg(not(target_arch = "wasm32"))] + pub module: wasmer::Module, + + #[cfg(target_arch = "wasm32")] + pub js_module: web_sys::js_sys::WebAssembly::Module, + #[cfg(target_arch = "wasm32")] + pub wasm_bytes: Arc<[u8]>, +} + +// SAFETY: WebAssembly.Module is one of the few JS objects that is +// "thread-safe" in the browser because it is immutable and +// can be transferred/cloned across workers without issues. +unsafe impl Send for Compiled {} +unsafe impl Sync for Compiled {} + +impl Compiled { + #[cfg(not(target_arch = "wasm32"))] + pub async fn new(name: &str, wasm_bytes: &[u8]) -> Result { + let engine = wasmer::Engine::default(); + let module = wasmer::Module::new(&engine, wasm_bytes)?; + + Ok(Compiled { + name: name.to_string(), + engine, + module, + }) + } + + #[cfg(target_arch = "wasm32")] + pub async fn new(name: &str, wasm_bytes: &[u8]) -> Result { + use wasm_bindgen::JsCast; + use wasm_bindgen_futures::JsFuture; + use web_sys::js_sys::{Uint8Array, WebAssembly}; + + let js_bytes = Uint8Array::from(wasm_bytes); + let promise = WebAssembly::compile(&js_bytes.into()); + let js_module_value = JsFuture::from(promise).await.unwrap(); + let js_module = js_module_value.dyn_into::().unwrap(); + + Ok(Compiled { + name: name.to_string(), + js_module, + wasm_bytes: Arc::from(wasm_bytes), + }) + } } diff --git a/crates/wasmi-plugin-hdk/src/host_handler.rs b/crates/wasmi-plugin-hdk/src/host_handler.rs index ccbb180..46434fd 100644 --- a/crates/wasmi-plugin-hdk/src/host_handler.rs +++ b/crates/wasmi-plugin-hdk/src/host_handler.rs @@ -1,15 +1,15 @@ use serde_json::Value; -use wasmi_plugin_pdk::{rpc_message::RpcError, server::BoxFuture}; +use wasmi_plugin_pdk::{router::BoxFuture, rpc_message::RpcError}; -use crate::plugin::PluginId; +use crate::instance_id::InstanceId; -/// HostHandler is a wrapper around the `wasmi-pdk::RequestHandler` trait that adds -/// the plugin ID as the first arg in the `handle` method. It should be used +/// HostHandler is an extension of `wasmi-pdk::RequestHandler` that adds +/// the instance ID as the first arg in the `handle` method. It should be used /// by hosts that manage multiple plugins to differentiate request sources. pub trait HostHandler: Send + Sync { fn handle<'a>( &'a self, - plugin: PluginId, + instance: InstanceId, method: &'a str, params: Value, ) -> BoxFuture<'a, Result>; diff --git a/crates/wasmi-plugin-hdk/src/instance_id.rs b/crates/wasmi-plugin-hdk/src/instance_id.rs new file mode 100644 index 0000000..5872f1a --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/instance_id.rs @@ -0,0 +1,32 @@ +use std::fmt::Display; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::plugin_id::PluginId; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct InstanceId { + pub plugin: PluginId, + pub id: Uuid, +} + +impl InstanceId { + pub fn new(plugin: PluginId) -> Self { + InstanceId { + plugin, + id: Uuid::new_v4(), + } + } +} + +impl Display for InstanceId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if f.alternate() { + write!(f, "{}:{}", self.plugin, self.id) // full: {:#} + } else { + let uuid_str = self.id.as_simple().to_string(); + write!(f, "{},{}", self.plugin, &uuid_str[uuid_str.len() - 6..]) // short {} + } + } +} diff --git a/crates/wasmi-plugin-hdk/src/lib.rs b/crates/wasmi-plugin-hdk/src/lib.rs index a0ba714..1a2c801 100644 --- a/crates/wasmi-plugin-hdk/src/lib.rs +++ b/crates/wasmi-plugin-hdk/src/lib.rs @@ -1,6 +1,8 @@ mod compile; pub mod host_handler; +pub mod instance_id; pub mod plugin; -mod plugin_instance; +pub mod plugin_id; +pub mod runtime; pub mod server; -mod wasi; +pub mod session; diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index 9f7e0f6..958c59f 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -1,200 +1,198 @@ -use std::{fmt::Display, io::BufReader, sync::Arc}; +use std::{sync::Arc, time::Duration}; -use async_trait::async_trait; -use futures::{AsyncBufReadExt, FutureExt}; -use serde::{Deserialize, Serialize}; +use futures::{AsyncBufReadExt, AsyncRead, FutureExt, io::BufReader}; use serde_json::Value; use thiserror::Error; -use tracing::info; -use uuid::Uuid; -use wasmi::{Engine, Module}; +use tracing::{error, info}; use wasmi_plugin_pdk::{ api::RequestHandler, + router::BoxFuture, rpc_message::{RpcError, RpcResponse}, - server::BoxFuture, - transport::{JsonRpcTransport, Transport}, + transport::AsyncTransport, }; +use wasmi_plugin_rt::sleep; -use crate::{ - compile::compile_plugin, - host_handler::HostHandler, - plugin_instance::{SpawnError, spawn_plugin}, -}; - -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct PluginId(Uuid); +use crate::runtime; +use crate::runtime::Runtime; +use crate::session::{PluginSession, PluginSessionError}; +use crate::{compile::Compiled, plugin_id::PluginId}; +use crate::{host_handler::HostHandler, instance_id::InstanceId}; -impl Default for PluginId { - fn default() -> Self { - PluginId(Uuid::new_v4()) - } +/// Plugin is an async-capable instance of a plugin +#[derive(Clone)] +pub struct Plugin { + inner: Arc, } -impl PluginId { - pub fn new() -> Self { - Self::default() - } +pub struct PluginBuilder { + id: PluginId, + name: String, + handler: Arc, + logger: Arc, + wasm_bytes: Vec, + timeout: Duration, } -impl From for PluginId { - fn from(value: u128) -> Self { - PluginId(Uuid::from_u128(value)) - } +struct InnerPlugin { + id: PluginId, + handler: Arc, + logger: Arc, + compiled: Compiled, + timeout: Duration, } -impl Display for PluginId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if f.alternate() { - write!(f, "{}", self.0) // full: {:#} - } else { - let uuid_str = self.0.as_simple().to_string(); - write!(f, "{}", &uuid_str[uuid_str.len() - 6..]) // short {} - } +pub trait PluginLogger: Send + Sync + 'static { + fn log(&self, name: &str, message: &str) { + info!(target: "plugin", "[plugin] [{}] {}", name, message); } } -type Logger = Box; - -/// Plugin is an async-capable instance of a plugin -pub struct Plugin { - name: String, - id: PluginId, - handler: Arc, - engine: Engine, - module: Module, - logger: Logger, - max_fuel: Option, -} +struct DefaultPluginLogger; +impl PluginLogger for DefaultPluginLogger {} #[derive(Debug, Error)] pub enum PluginError { - #[error("spawn error")] - SpawnError(#[from] SpawnError), - #[error("transport error")] - RpcError(#[from] RpcError), - #[error("plugin died")] - PluginDied, + #[error("Session error: {0}")] + SessionError(#[from] PluginSessionError), + #[error("Runtime Error: {0}")] + RuntimeError(#[from] Box), + #[error("Plugin timeout")] + PluginTimeout, + #[error("Plugin compilation error: {0}")] + CompileError(#[from] wasmer::CompileError), +} + +impl From for RpcError { + fn from(value: PluginError) -> Self { + RpcError::custom(value.to_string()) + } } impl Plugin { - pub fn new( - name: &str, + pub fn builder( + name: impl Into, wasm_bytes: Vec, handler: Arc, - ) -> Result { - let (engine, module) = compile_plugin(wasm_bytes.clone())?; - - Ok(Plugin { - name: name.to_string(), + ) -> PluginBuilder { + PluginBuilder { id: PluginId::new(), + name: name.into(), handler, - engine, - module, - max_fuel: None, - logger: Box::new(default_plugin_logger), + logger: Arc::new(DefaultPluginLogger), + wasm_bytes, + timeout: Duration::from_secs(10), + } + } +} + +impl PluginBuilder { + pub async fn build(self) -> Result { + let compiled = Compiled::new(&self.name, &self.wasm_bytes).await?; + + let inner = InnerPlugin { + id: self.id, + handler: self.handler, + logger: self.logger, + compiled, + timeout: self.timeout, + }; + + Ok(Plugin { + inner: Arc::new(inner), }) } - /// Sets the plugin ID for this instance. If no ID is provided a random - /// UUID is generated. pub fn with_id(mut self, id: PluginId) -> Self { self.id = id; self } - /// Sets a custom logger for the plugin instance. Plugins log messages to - /// stderr, which are captured and passed to this logger function. If - /// no logger is provided a default logger is used. - pub fn with_logger(mut self, logger: Logger) -> Self { - self.logger = Box::new(logger); + pub fn with_logger(mut self, logger: impl PluginLogger) -> Self { + self.logger = Arc::new(logger); self } - /// Sets the maximum fuel for the plugin instance. - /// - /// The fuel limit controls how frequently the plugin is interrupted to - /// check for cancellation and yield to other tasks. Lower fuel limits - /// result in more frequent interruptions, which can improve responsiveness - /// for long-running compute-intensive tasks, but will also incur more overhead. - /// - /// Generally a fuel between 10_000 and 1_000_000 is a good starting point. - /// - /// Leave as None to use the default fuel limit, a sensible default of 100_000. - pub fn with_max_fuel(mut self, max_fuel: u64) -> Self { - self.max_fuel = Some(max_fuel); + pub fn with_timeout(mut self, timeout: Duration) -> Self { + self.timeout = timeout; self } +} +impl Plugin { pub fn name(&self) -> &str { - &self.name + &self.inner.compiled.name } pub fn id(&self) -> PluginId { - self.id + self.inner.id } } -impl PluginError { - pub fn as_rpc_code(&self) -> RpcError { - match self { - PluginError::RpcError(code) => code.clone(), - _ => RpcError::Custom(self.to_string()), - } - } -} - -impl From for RpcError { - fn from(err: PluginError) -> Self { - match err { - PluginError::RpcError(code) => code, - _ => RpcError::Custom(err.to_string()), - } - } -} +impl AsyncTransport for Plugin { + async fn call_async(&self, method: &str, params: Value) -> Result { + let runtime = self.runtime(); + let (id, stdin_writer, stdout_reader, stderr_reader) = runtime + .spawn(self.inner.compiled.clone()) + .await + .map_err(|e| PluginError::RuntimeError(e.into()))?; -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl Transport for Plugin { - async fn call(&self, method: &str, params: Value) -> Result { - let (stdin_writer, stdout_reader, stderr_reader, is_running, instance_task) = - spawn_plugin(&self.engine, &self.module, self.max_fuel)?; - - let name = self.name.clone(); - let stderr_task = async move { - let mut buf_reader = futures::io::BufReader::new(stderr_reader); - let mut line = String::new(); - while buf_reader.read_line(&mut line).await.is_ok_and(|n| n > 0) { - (self.logger)(&name, line.trim_end()); - line.clear(); - } - } - .fuse(); + //? stderr logging + let name = self.inner.compiled.name.clone(); + let logger = self.inner.logger.clone(); + let stderr_task = log_stderr(&name, stderr_reader, logger).fuse(); + //? transport setup let handler = PluginCallback { - handler: self.handler.clone(), - uuid: self.id, + handler: self.inner.handler.clone(), + uuid: InstanceId::new(self.inner.id), }; + let session = PluginSession::new(stdout_reader, stdin_writer, handler); + let session_task = session.call_async(method, params).fuse(); + + //? timeout + let timeout = sleep(self.inner.timeout).fuse(); + + //? Pump futures until (session_task AND stderr_task) OR timeout + futures::pin_mut!(session_task, timeout, stderr_task); + + loop { + futures::select! { + res = session_task => { + return Ok(res?); + } + _ = timeout => { + error!("Plugin {} timed out after {:?}", self.name(), self.inner.timeout); + //? Assume the instance has hung, so terminate its runtime + runtime.terminate(id).await; + return Err(PluginError::PluginTimeout); + } + _ = stderr_task => { + //? Stderr logging finished + } + } + } + } +} - let buf_reader = BufReader::new(stdout_reader); - let transport = JsonRpcTransport::with_handler(buf_reader, stdin_writer, handler); - let rpc_task = transport.call(method, params).fuse(); - - let instance_task = instance_task.fuse(); - futures::pin_mut!(rpc_task, instance_task, stderr_task); - - //? Run the transport, plugin, and stderr logger until one of them completes - let (res, _, _) = futures::join!(rpc_task, instance_task, stderr_task); - let res = res?; - - is_running.store(false, std::sync::atomic::Ordering::SeqCst); +impl Plugin { + #[allow(clippy::type_complexity)] + #[cfg(not(target_arch = "wasm32"))] + fn runtime(&self) -> impl Runtime + Send + Sync + 'static { + runtime::NativeRuntime::new() + } - Ok(res) + #[allow(clippy::type_complexity)] + #[cfg(target_arch = "wasm32")] + fn runtime(&self) -> impl Runtime + Send + Sync + 'static { + runtime::WorkerRuntime::global() } } +/// Helper struct that implements RequestHandler by forwarding it to a HostHandler +/// with the added `uuid` context. struct PluginCallback { handler: Arc, - uuid: PluginId, + uuid: InstanceId, } impl RequestHandler for PluginCallback { @@ -204,6 +202,19 @@ impl RequestHandler for PluginCallback { } } -fn default_plugin_logger(name: &str, msg: &str) { - info!(target: "plugin", "[plugin] [{}] {}", name, msg); +async fn log_stderr( + name: &str, + reader: impl AsyncRead + Unpin + Send + Sync + 'static, + logger: Arc, +) { + let name = name.to_string(); + let mut reader = BufReader::new(reader); + let mut line = String::new(); + while let Ok(n) = reader.read_line(&mut line).await { + if n == 0 { + break; + } + logger.log(&name, line.trim_end()); + line.clear(); + } } diff --git a/crates/wasmi-plugin-hdk/src/plugin_id.rs b/crates/wasmi-plugin-hdk/src/plugin_id.rs new file mode 100644 index 0000000..45bd85a --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/plugin_id.rs @@ -0,0 +1,36 @@ +use std::fmt::Display; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct PluginId(Uuid); + +impl Default for PluginId { + fn default() -> Self { + PluginId(Uuid::new_v4()) + } +} + +impl PluginId { + pub fn new() -> Self { + Self::default() + } +} + +impl From for PluginId { + fn from(value: u128) -> Self { + PluginId(Uuid::from_u128(value)) + } +} + +impl Display for PluginId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if f.alternate() { + write!(f, "{}", self.0) // full: {:#} + } else { + let uuid_str = self.0.as_simple().to_string(); + write!(f, "{}", &uuid_str[uuid_str.len() - 6..]) // short {} + } + } +} diff --git a/crates/wasmi-plugin-hdk/src/plugin_instance.rs b/crates/wasmi-plugin-hdk/src/plugin_instance.rs deleted file mode 100644 index 6b1bba1..0000000 --- a/crates/wasmi-plugin-hdk/src/plugin_instance.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::sync::{Arc, atomic::AtomicBool}; - -use crate::wasi::{ - non_blocking_pipe::{NonBlockingPipeReader, NonBlockingPipeWriter, non_blocking_pipe}, - wasi_ctx::{WasiCtx, add_to_linker}, - wasmi::spawn_wasm, -}; -use thiserror::Error; -use wasmi::{Engine, Linker, Module, Store}; - -#[derive(Error, Debug)] -pub enum SpawnError { - #[error("io error")] - IoError(#[from] std::io::Error), - #[error("start not found")] - StartNotFound, - #[error("wasmi error")] - WasmiError(#[from] wasmi::Error), -} - -/// Creates the plugin pipes and task. -pub fn spawn_plugin( - engine: &Engine, - module: &Module, - max_fuel: Option, -) -> Result< - ( - NonBlockingPipeWriter, - NonBlockingPipeReader, - NonBlockingPipeReader, - Arc, - impl Future, - ), - SpawnError, -> { - let is_running = Arc::new(AtomicBool::new(true)); - - // Setup pipes - let (stdin_reader, stdin_writer) = non_blocking_pipe(); - let (stdout_reader, stdout_writer) = non_blocking_pipe(); - let (stderr_reader, stderr_writer) = non_blocking_pipe(); - - let mut linker = Linker::new(engine); - let wasi = WasiCtx::new() - .set_stdin(stdin_reader) - .set_stdout(stdout_writer) - .set_stderr(stderr_writer); - - let mut store = Store::new(engine, wasi); - add_to_linker(&mut linker)?; - - let instance = linker.instantiate_and_start(&mut store, module)?; - let start_func = instance - .get_func(&store, "_start") - .ok_or(SpawnError::StartNotFound)?; - - let fut = spawn_wasm(store, start_func, is_running.clone(), max_fuel); - - Ok((stdin_writer, stdout_reader, stderr_reader, is_running, fut)) -} diff --git a/crates/wasmi-plugin-hdk/src/runtime/mod.rs b/crates/wasmi-plugin-hdk/src/runtime/mod.rs new file mode 100644 index 0000000..a9abd6d --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/runtime/mod.rs @@ -0,0 +1,39 @@ +#[cfg(not(target_arch = "wasm32"))] +pub use native_runtime::NativeRuntime; +#[cfg(not(target_arch = "wasm32"))] +mod native_runtime; + +#[cfg(target_arch = "wasm32")] +pub use worker_runtime::WorkerRuntime; +#[cfg(target_arch = "wasm32")] +mod wasm_worker_pool; +#[cfg(target_arch = "wasm32")] +mod worker_runtime; + +use crate::compile::Compiled; +use futures::{AsyncRead, AsyncWrite}; +use wasmi_plugin_pdk::router::MaybeSend; + +pub trait Runtime { + type Error: Into>; + + /// Spawn a new plugin instance from the given compiled module. Returns the + /// instance ID and host-side stdio pipes. + fn spawn( + &self, + compiled: Compiled, + ) -> impl std::future::Future< + Output = Result< + ( + u64, + impl AsyncWrite + Unpin + Send + Sync + 'static, + impl AsyncRead + Unpin + Send + Sync + 'static, + impl AsyncRead + Unpin + Send + Sync + 'static, + ), + Self::Error, + >, + > + MaybeSend; + + /// Forcefully terminate a hanging or otherwise failedplugin instance. + fn terminate(&self, instance_id: u64) -> impl std::future::Future + MaybeSend; +} diff --git a/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs new file mode 100644 index 0000000..7fd91a9 --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs @@ -0,0 +1,123 @@ +//! Native wasmer runtime, running the Wasm module in a separate thread. + +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; + +use futures::lock::Mutex; +use futures::{AsyncRead, AsyncWrite}; +use thiserror::Error; +use tokio::task::{JoinHandle, spawn_blocking}; +use tracing::error; +use wasmer::Store; + +use crate::compile::Compiled; +use crate::runtime::Runtime; +use wasmi_plugin_wasi::non_blocking_pipe::{ + NonBlockingPipeReader, NonBlockingPipeWriter, non_blocking_pipe, +}; +use wasmi_plugin_wasi::wasi_ctx::WasiCtx; + +pub struct NativeRuntime { + sessions: Arc>>, + next_id: Arc, +} + +struct NativeSession { + wasm_handle: JoinHandle<()>, +} + +#[derive(Debug, Error)] +pub enum NativeRuntimeError { + #[error("Wasmer Runtime Error: {0}")] + WasmerError(#[from] wasmer::RuntimeError), + #[error("IO error: {0}")] + IOError(#[from] std::io::Error), +} + +impl NativeRuntime { + pub fn new() -> Self { + NativeRuntime { + sessions: Arc::new(Mutex::new(HashMap::default())), + next_id: Arc::new(AtomicU64::new(1)), + } + } +} + +impl Default for NativeRuntime { + fn default() -> Self { + Self::new() + } +} + +impl Runtime for NativeRuntime { + type Error = NativeRuntimeError; + + async fn spawn( + &self, + compiled: Compiled, + ) -> Result< + ( + u64, + impl AsyncWrite + Unpin + Send + Sync + 'static, + impl AsyncRead + Unpin + Send + Sync + 'static, + impl AsyncRead + Unpin + Send + Sync + 'static, + ), + Self::Error, + > { + let id = self.next_id.fetch_add(1, Ordering::Relaxed); + let (stdin_reader, stdin_writer) = non_blocking_pipe(); + let (stdout_reader, stdout_writer) = non_blocking_pipe(); + let (stderr_reader, stderr_writer) = non_blocking_pipe(); + + let wasm_handle = wasm_handle(compiled, stdin_reader, stdout_writer, stderr_writer); + + let session = NativeSession { wasm_handle }; + + self.sessions.lock().await.insert(id, session); + Ok((id, stdin_writer, stdout_reader, stderr_reader)) + } + + async fn terminate(&self, instance_id: u64) -> () { + if let Some(session) = self.sessions.lock().await.remove(&instance_id) { + // TODO: Use gas metering to force terminate the wasm runtime + session.wasm_handle.abort(); + } + } +} + +fn wasm_handle( + compiled: Compiled, + stdin_reader: NonBlockingPipeReader, + stdout_writer: NonBlockingPipeWriter, + stderr_writer: NonBlockingPipeWriter, +) -> JoinHandle<()> { + spawn_blocking(move || { + let result = run_instance(compiled, stdin_reader, stdout_writer, stderr_writer); + if let Err(e) = result { + error!("Plugin execution error: {:?}", e); + } + }) +} + +fn run_instance( + compiled: Compiled, + stdin: NonBlockingPipeReader, + stdout: NonBlockingPipeWriter, + stderr: NonBlockingPipeWriter, +) -> Result<(), Box> { + let engine = compiled.engine; + let module = compiled.module; + + let mut store = Store::new(engine); + + let wasi_ctx = WasiCtx::new() + .set_stdin(stdin) + .set_stdout(stdout) + .set_stderr(stderr); + + let start = wasi_ctx.into_fn(&mut store, &module)?; + start.call(&mut store, &[])?; + + Ok(()) +} diff --git a/crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/mod.rs b/crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/mod.rs new file mode 100644 index 0000000..178a4da --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/mod.rs @@ -0,0 +1,3 @@ +pub mod pool; +mod worker; +mod worker_message; diff --git a/crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/pool.rs b/crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/pool.rs new file mode 100644 index 0000000..8bc41d1 --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/pool.rs @@ -0,0 +1,291 @@ +use std::sync::{Arc, Mutex, OnceLock}; + +use futures::future::LocalBoxFuture; +use thiserror::Error; +use tracing::{error, info}; +use wasm_bindgen::{ + JsCast, JsValue, + prelude::{Closure, wasm_bindgen}, +}; +use web_sys::{ + Blob, Url, Worker, WorkerOptions, WorkerType, + js_sys::{self, Array, Reflect, Uint8Array}, +}; + +use crate::runtime::wasm_worker_pool::worker_message::WorkerMessage; + +/// Pool that manages a set of web workers for executing tasks +/// asynchronously. Workers are reused for multiple tasks to minimize +/// the overhead of spawning new workers. +pub struct WorkerPool { + state: Arc>, +} + +struct PoolState { + workers: Vec>, +} + +enum WorkerState { + Idle, + Busy, + Terminated, +} + +pub struct WorkerHandle { + state: Arc>, + worker: web_sys::Worker, + _on_message: wasm_bindgen::prelude::Closure, + _on_error: wasm_bindgen::prelude::Closure, +} + +#[derive(Debug, Error)] +pub enum SpawnError { + #[error("JavaScript error: {0:?}")] + JsError(JsValue), + #[error("PostMessage error: {0:?}")] + PostMessageError(JsValue), + #[error("Reflect error: {0:?}")] + ReflectError(JsValue), +} + +static GLOBAL_POOL: OnceLock = OnceLock::new(); + +unsafe impl Send for WorkerHandle {} +unsafe impl Sync for WorkerHandle {} + +impl WorkerPool { + pub fn global() -> &'static WorkerPool { + GLOBAL_POOL.get_or_init(|| WorkerPool { + state: Arc::new(Mutex::new(PoolState { workers: vec![] })), + }) + } + + #[allow(dead_code)] + pub fn run(&self, f: F) -> Result, SpawnError> + where + F: FnOnce() -> Fut + Send + 'static, + Fut: Future + 'static, + { + let handle = self.get_or_spawn_worker()?; + + let wrapper = move || -> LocalBoxFuture<'static, ()> { + Box::pin(async move { + f().await; + }) + }; + handle.run(wrapper)?; + Ok(handle) + } + + pub fn run_with( + &self, + extra: &wasm_bindgen::JsValue, + f: F, + ) -> Result, SpawnError> + where + F: FnOnce(wasm_bindgen::JsValue) -> Fut + Send + 'static, + Fut: Future + 'static, + { + let handle = self.get_or_spawn_worker()?; + + let wrapper = move |v: wasm_bindgen::JsValue| -> LocalBoxFuture<'static, ()> { + let v = v.clone(); + Box::pin(async move { + f(v).await; + }) + }; + handle.run_with(extra, wrapper)?; + Ok(handle) + } + + fn get_or_spawn_worker(&self) -> Result, SpawnError> { + // Remove any that are terminated + self.state.lock().unwrap().workers.retain(|w| { + let worker_state = w.state.lock().unwrap(); + !matches!(*worker_state, WorkerState::Terminated) + }); + + // Try to find an idle worker + for worker in self.state.lock().unwrap().workers.iter() { + let mut worker_state = worker.state.lock().unwrap(); + if matches!(*worker_state, WorkerState::Idle) { + *worker_state = WorkerState::Busy; + return Ok(worker.clone()); + } + } + + // Otherwise, spawn a new one + let id = self.state.lock().unwrap().workers.len() as u64; + let handle = WorkerHandle::spawn(id)?; + let handle = Arc::new(handle); + self.state.lock().unwrap().workers.push(handle.clone()); + Ok(handle) + } +} + +impl WorkerHandle { + pub fn spawn(id: u64) -> Result { + let name = format!("worker-{}", id); + + // 2. Create the worker + let options = WorkerOptions::new(); + options.set_name(&name); + options.set_type(WorkerType::Module); + let worker = + Worker::new_with_options(&worker_url(id), &options).map_err(SpawnError::JsError)?; + + // 3. Set up onmessage handler + let state = Arc::new(Mutex::new(WorkerState::Busy)); + let on_message = on_message_handler(state.clone(), name.clone()); + let on_message = Closure::wrap(on_message); + worker.set_onmessage(Some(on_message.as_ref().unchecked_ref())); + + // Set up onerror handler + let on_error = on_error_handler(name.clone()); + let on_error = Closure::wrap(on_error); + worker.set_onerror(Some(on_error.as_ref().unchecked_ref())); + + // 4. Send the "init" message with memory and module + let msg = js_sys::Object::new(); + Reflect::set(&msg, &"type".into(), &"init".into()).map_err(SpawnError::ReflectError)?; + Reflect::set(&msg, &"memory".into(), &wasm_bindgen::memory()) + .map_err(SpawnError::ReflectError)?; + Reflect::set(&msg, &"module".into(), ¤t_module()) + .map_err(SpawnError::ReflectError)?; + Reflect::set(&msg, &"sdkUrl".into(), &sdk_url().into()) + .map_err(SpawnError::ReflectError)?; + worker + .post_message(&msg) + .map_err(SpawnError::PostMessageError)?; + + info!("Spawned worker {}", name); + Ok(WorkerHandle { + state, + worker, + _on_message: on_message, + _on_error: on_error, + }) + } + + #[allow(dead_code)] + fn run(&self, f: F) -> Result<(), SpawnError> + where + F: FnOnce() -> Fut + Send + 'static, + Fut: Future + 'static, + { + // 1. Box the function and get a raw pointer + let box_f: Box LocalBoxFuture<'static, ()> + Send>> = + Box::new(Box::new(move || Box::pin(f()))); + let ptr = Box::into_raw(box_f) as u32; + + // 2. Load my worker + let worker = &self.worker; + + // 3. Send the "run" message with raw ptr + let msg = js_sys::Object::new(); + Reflect::set(&msg, &"type".into(), &"run".into()).unwrap(); + Reflect::set(&msg, &"taskPtr".into(), &ptr.into()).unwrap(); + worker.post_message(&msg).unwrap(); + + Ok(()) + } + + fn run_with(&self, extra: &wasm_bindgen::JsValue, f: F) -> Result<(), SpawnError> + where + F: FnOnce(wasm_bindgen::JsValue) -> Fut + Send + 'static, + Fut: Future + 'static, + { + // 1. Box the function and get a raw pointer + let box_f: Box< + Box LocalBoxFuture<'static, ()> + Send>, + > = Box::new(Box::new(move |v| Box::pin(f(v)))); + let ptr = Box::into_raw(box_f) as u32; + + // 2. Load my worker + let worker = &self.worker; + + // 3. Send the "run" message with raw ptr + let msg = js_sys::Object::new(); + Reflect::set(&msg, &"type".into(), &"run_with".into()).unwrap(); + Reflect::set(&msg, &"taskPtr".into(), &ptr.into()).unwrap(); + Reflect::set(&msg, &"extra".into(), extra).unwrap(); + worker.post_message(&msg).unwrap(); + + Ok(()) + } + + pub fn terminate(&self) { + self.worker.terminate(); + let mut state = self.state.lock().unwrap(); + *state = WorkerState::Terminated; + } +} + +fn worker_url(id: u64) -> String { + let mut script = include_str!("./worker.js").to_string(); + script.push_str(format!("\n//# sourceURL=worker-{}.js", id).as_str()); + + let options = web_sys::BlobPropertyBag::new(); + options.set_type("application/javascript"); + let blob = Blob::new_with_u8_array_sequence_and_options( + Array::from_iter([Uint8Array::from(script.as_bytes())]).as_ref(), + &options, + ) + .unwrap(); + + Url::create_object_url_with_blob(&blob).unwrap() +} + +fn sdk_url() -> String { + #[wasm_bindgen] + extern "C" { + // Adding the thread_local_v2 attribute satisfies the deprecation warning + #[wasm_bindgen(thread_local_v2, js_namespace = ["import", "meta"], js_name = url)] + static IMPORT_META_URL: String; + } + + IMPORT_META_URL.with(|url| url.clone()) +} + +fn current_module() -> JsValue { + wasm_bindgen::module().dyn_into().unwrap() +} + +fn on_message_handler( + state: Arc>, + name: String, +) -> Box { + Box::new(move |e: web_sys::MessageEvent| { + let Ok(msg) = serde_wasm_bindgen::from_value::(e.data()) else { + error!("Received unknown message from worker: {:?}", e.data()); + return; + }; + + match msg { + WorkerMessage::Log { message } => { + log_worker_message(&name, message); + } + WorkerMessage::Idle => { + let mut worker_state = state.lock().unwrap(); + *worker_state = WorkerState::Idle; + } + } + }) +} + +fn log_worker_message(name: &str, msg: String) { + info!(target: "WORKER", "[{}] {}", name, msg); +} + +fn on_error_handler(name: String) -> Box { + Box::new(move |e: web_sys::ErrorEvent| { + error!( + target: "WORKER", + "[{}] Worker error: {} (at {}:{})", + name, + e.message(), + e.filename(), + e.lineno() + ); + }) +} diff --git a/crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/worker.js b/crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/worker.js new file mode 100644 index 0000000..e6aa74c --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/worker.js @@ -0,0 +1,53 @@ +// Forwards JS and WASM logs to the main thread +// Very slow, so disabled unless required for debugging +// ["log", "info", "warn", "error"].forEach(level => { +// globalThis.console[level] = (...args) => { +// globalThis.postMessage({ +// type: "Log", +// level: level, +// message: args.map(String).join(" "), +// ts: globalThis.performance.now() +// }); +// }; +// }); + +let wasmPkg = null; +let processingQueue = Promise.resolve(); + +globalThis.onmessage = async ev => { + processingQueue = processingQueue.then(async () => { + try { + if (ev.data.type === "init") { + const { memory, module, sdkUrl } = ev.data; + + wasmPkg = await import(sdkUrl); + const init = wasmPkg.default || wasmPkg.init; + + if (typeof init !== 'function') { + throw new Error("Wasm init is not a function"); + } + + await init(module, memory); + wasmPkg.init_compute_worker(); + } else if (ev.data.type === "run") { + const { taskPtr } = ev.data; + try { + await wasmPkg.execute_worker_task(taskPtr); + } finally { + globalThis.postMessage({ type: "Idle" }); + } + } else if (ev.data.type === "run_with") { + const { taskPtr, extra } = ev.data; + try { + await wasmPkg.execute_worker_task_with(taskPtr, extra); + } finally { + globalThis.postMessage({ type: "Idle" }); + } + } else { + console.error(`Unknown message type: ${type}`); + } + } catch (e) { + console.error(`Error in worker: ${e.message}`); + } + }); +}; diff --git a/crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/worker.rs b/crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/worker.rs new file mode 100644 index 0000000..75ca6ac --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/worker.rs @@ -0,0 +1,37 @@ +use futures::{FutureExt, future::LocalBoxFuture}; +use tracing::error; +use wasm_bindgen::{JsValue, prelude::wasm_bindgen}; +use web_sys::js_sys; + +#[wasm_bindgen] +pub fn init_compute_worker() { + console_error_panic_hook::set_once(); +} + +#[wasm_bindgen] +pub fn execute_worker_task(ptr: u32) -> js_sys::Promise { + type Task = Box LocalBoxFuture<'static, ()> + Send>; + let task: Box = unsafe { Box::from_raw(ptr as *mut Task) }; + let fut = task(); + + wasm_bindgen_futures::future_to_promise(async move { + if let Err(e) = std::panic::AssertUnwindSafe(fut).catch_unwind().await { + error!("Panic in thread worker: {:?}", e); + } + Ok(JsValue::UNDEFINED) + }) +} + +#[wasm_bindgen] +pub fn execute_worker_task_with(ptr: u32, extra: JsValue) -> js_sys::Promise { + type Task = Box LocalBoxFuture<'static, ()> + Send>; + let task: Box = unsafe { Box::from_raw(ptr as *mut Task) }; + let fut = task(extra); + + wasm_bindgen_futures::future_to_promise(async move { + if let Err(e) = std::panic::AssertUnwindSafe(fut).catch_unwind().await { + error!("Panic in thread worker: {:?}", e); + } + Ok(JsValue::UNDEFINED) + }) +} diff --git a/crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/worker_message.rs b/crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/worker_message.rs new file mode 100644 index 0000000..d9116d2 --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/worker_message.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum WorkerMessage { + Log { message: String }, + Idle, +} diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs new file mode 100644 index 0000000..ad2b619 --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs @@ -0,0 +1,378 @@ +use std::collections::HashMap; +use std::io::Write; +use std::sync::{Arc, Mutex, OnceLock}; + +use futures::{AsyncRead, AsyncWrite}; +use thiserror::Error; +use tracing::{error, warn}; +use wasm_bindgen::JsCast; +use wasmi_plugin_wasi::non_blocking_pipe::non_blocking_pipe; +use wasmi_plugin_wasi::wasi_ctx::{self, WasiReader}; +use web_sys::js_sys::WebAssembly; + +use crate::compile::Compiled; +use crate::runtime::Runtime; +use crate::runtime::wasm_worker_pool::pool::{self, WorkerHandle, WorkerPool}; + +pub struct WorkerRuntime { + inner: Arc>, +} + +pub struct InnerWorkerRuntime { + // coordinator: Worker, + instances: HashMap>, + // ready_tx: Option>, + // shared_ready: Shared>, + // _onmessage_closure: Option>, +} + +// struct PluginInstance { +// stdout_writer: Box, +// stderr_writer: Box, +// run_plugin_resp_tx: Option>>, +// } + +#[derive(Debug, Error)] +pub enum SpawnError { + #[error("Worker Error: {0}")] + WorkerError(String), + #[error("Oneshot cancelled")] + OneshotCancelled, + #[error("Pool error: {0}")] + Pool(#[from] pool::SpawnError), + // #[error("Serde Error: {0}")] + // Serde(#[from] serde_wasm_bindgen::Error), + // #[error("Post Message Error: {0:?}")] + // PostMessage(JsValue), + // #[error("Reflect Error: {0:?}")] + // Reflect(JsValue), +} + +#[derive(Debug, Error)] +enum RunError { + #[error("WASI Error: {0}")] + Wasi(#[from] wasi_ctx::WasiError), + #[error("Compile Error: {0}")] + Compile(#[from] wasmer::CompileError), + #[error("Wasmer Runtime Error: {0}")] + Runtime(#[from] wasmer::RuntimeError), + #[error("Wasmer Trap Error: {0}")] + Trap(String), +} + +static RUNTIME: OnceLock = OnceLock::new(); + +// const WORKER_LOADER_JS: &str = include_str!("../../../wasmi-plugin-coordinator/src/worker.js"); +// const WORKER_JS_GLUE: &str = include_str!("../../../wasmi-plugin-coordinator/assets/worker.js"); +// const WORKER_WASM_BYTES: &[u8] = +// include_bytes!("../../../wasmi-plugin-coordinator/assets/worker.wasm"); + +// unsafe impl Send for InnerWorkerRuntime {} +// unsafe impl Sync for InnerWorkerRuntime {} + +impl WorkerRuntime { + pub fn global() -> &'static WorkerRuntime { + RUNTIME.get_or_init(Self::init) + } + + fn init() -> Self { + // info!("Initializing WorkerRuntime"); + // let options = WorkerOptions::new(); + // options.set_name("coordinator"); + // options.set_type(WorkerType::Module); + // let worker = Worker::new_with_options(&worker_url().unwrap(), &options).unwrap(); + + // let (ready_tx, ready_rx) = futures::channel::oneshot::channel(); + // let shared_ready = ready_rx.map(|_| ()).boxed().shared(); + + let inner = Arc::new(Mutex::new(InnerWorkerRuntime { + // coordinator: worker, + instances: HashMap::new(), + // ready_tx: Some(ready_tx), + // shared_ready, + // _onmessage_closure: None, + })); + + // let inner_clone = inner.clone(); + // let onmessage = Closure::wrap(Box::new(move |e: MessageEvent| { + // on_message(inner_clone.clone(), e); + // }) as Box); + + // inner + // .lock() + // .unwrap() + // .coordinator + // .set_onmessage(Some(onmessage.as_ref().unchecked_ref())); + // inner.lock().unwrap()._onmessage_closure = Some(onmessage); + + Self { inner } + } + + // async fn wait_ready(&self) { + // let shared_ready = { + // let inner = self.inner.lock().unwrap(); + // inner.shared_ready.clone() + // }; + // shared_ready.await; + // } +} + +impl Runtime for &WorkerRuntime { + type Error = SpawnError; + + async fn spawn( + &self, + compiled: Compiled, + ) -> Result< + ( + u64, + impl AsyncWrite + Unpin + Send + Sync + 'static, + impl AsyncRead + Unpin + Send + Sync + 'static, + impl AsyncRead + Unpin + Send + Sync + 'static, + ), + Self::Error, + > { + let instance_id = getrandom::u64().expect("Failed to generate random instance ID"); + let (stdin_reader, stdin_writer) = non_blocking_pipe(); + let (stdout_reader, stdout_writer) = non_blocking_pipe(); + let (stderr_reader, stderr_writer) = non_blocking_pipe(); + + let pool = WorkerPool::global(); + let res = pool.run_with(&compiled.js_module, move |extra| async move { + let res = run_instance(extra, stdin_reader, stdout_writer, stderr_writer); + if let Err(e) = res { + error!("Error running Wasm instance: {}", e); + } + })?; + self.inner + .lock() + .unwrap() + .instances + .insert(instance_id, res); + Ok((instance_id, stdin_writer, stdout_reader, stderr_reader)) + + // let (run_plugin_resp_tx, run_plugin_resp_rx) = futures::channel::oneshot::channel(); + + // let plugin_instance = PluginInstance { + // stdout_writer: Box::new(stdout_writer), + // stderr_writer: Box::new(stderr_writer), + // run_plugin_resp_tx: Some(run_plugin_resp_tx), + // }; + + // self.inner + // .lock() + // .unwrap() + // .instances + // .insert(instance_id, plugin_instance); + + // Start stdin proxy + // stdin_proxy(stdin_reader, instance_id, self.inner.clone()); + + // // Notify the coordinator + // let wasm_module_js = compiled.js_module.clone(); + // let spawn_msg = CoordinatorMessage::RunPlugin { id: instance_id }; + // let msg_value = serde_wasm_bindgen::to_value(&spawn_msg)?; + // Reflect::set(&msg_value, &"wasm_module".into(), &wasm_module_js) + // .map_err(SpawnError::Reflect)?; + + // self.inner + // .lock() + // .unwrap() + // .coordinator + // .post_message(&msg_value) + // .map_err(SpawnError::PostMessage)?; + + // // Await the response + // match run_plugin_resp_rx.await { + // Ok(Ok(())) => {} + // Ok(Err(err_msg)) => return Err(SpawnError::WorkerError(err_msg)), + // Err(_) => return Err(SpawnError::OneshotCancelled), + // } + + // Ok(( + // instance_id.into(), + // stdin_writer, + // stdout_reader, + // stderr_reader, + // )) + } + + async fn terminate(&self, instance_id: u64) { + if let Some(handle) = self.inner.lock().unwrap().instances.remove(&instance_id) { + handle.terminate(); + } else { + warn!("No instance handle found for instance {}", instance_id); + } + } +} + +fn run_instance( + js_module: wasm_bindgen::JsValue, + stdin: impl WasiReader + 'static, + stdout: impl Write + Send + Sync + 'static, + stderr: impl Write + Send + Sync + 'static, +) -> Result<(), RunError> { + let mut store = wasmer::Store::default(); + + let js_module: WebAssembly::Module = js_module.dyn_into().unwrap(); + let module = js_module.into(); + + let wasi_ctx = wasi_ctx::WasiCtx::new() + .set_stdin(stdin) + .set_stdout(stdout) + .set_stderr(stderr); + + let start = wasi_ctx.into_fn(&mut store, &module)?; + match start.call(&mut store, &[]) { + Ok(_) => Ok(()), + Err(e) => { + if let Some(trap) = e.clone().to_trap() { + Err(RunError::Trap(trap.message().to_string())) + } else { + Err(RunError::Runtime(e)) + } + } + } +} + +// fn worker_url() -> Result { +// let options = web_sys::BlobPropertyBag::new(); +// options.set_type("application/wasm"); +// let wasm_blob = web_sys::Blob::new_with_u8_array_sequence_and_options( +// &js_sys::Array::of1(&js_sys::Uint8Array::from(WORKER_WASM_BYTES)), +// &options, +// )?; +// let wasm_url = web_sys::Url::create_object_url_with_blob(&wasm_blob)?; + +// let options = web_sys::BlobPropertyBag::new(); +// options.set_type("text/javascript"); +// let sdk_script = format!("{}\n//# sourceURL=coordinator.js", WORKER_JS_GLUE); +// let sdk_blob = web_sys::Blob::new_with_u8_array_sequence_and_options( +// &js_sys::Array::of1(&js_sys::JsString::from(sdk_script)), +// &options, +// )?; +// let sdk_url = web_sys::Url::create_object_url_with_blob(&sdk_blob)?; + +// let loader_script = WORKER_LOADER_JS +// .replace("{sdk_url}", &sdk_url) +// .replace("{wasm_url}", &wasm_url); + +// let options = web_sys::BlobPropertyBag::new(); +// options.set_type("text/javascript"); +// let blob = web_sys::Blob::new_with_u8_array_sequence_and_options( +// &js_sys::Array::of1(&js_sys::JsString::from(loader_script)), +// &options, +// )?; + +// web_sys::Url::create_object_url_with_blob(&blob) +// } + +// fn on_message(inner: Arc>, e: web_sys::MessageEvent) { +// let data = e.data(); + +// let Ok(msg) = serde_wasm_bindgen::from_value::(data.clone()) else { +// error!("Worker: Failed to deserialize message"); +// return; +// }; + +// match msg { +// CoordinatorMessage::Ready => { +// info!("Coordinator is ready"); +// let mut inner = inner.lock().unwrap(); +// if let Some(tx) = inner.ready_tx.take() { +// let _ = tx.send(()); +// } +// } +// CoordinatorMessage::Stdout { id } => { +// let Ok(data) = Reflect::get(&data, &"data".into()) else { +// error!("Failed to get stdout data from message"); +// return; +// }; +// let data: Vec = data.unchecked_into::().to_vec(); + +// let mut inner = inner.lock().unwrap(); +// if let Some(instance) = inner.instances.get_mut(&id) { +// if let Err(e) = instance.stdout_writer.write_all(&data) { +// error!("Failed to write to stdout pipe: {}", e); +// } +// } else { +// warn!("Received stdout for unknown instance ID {:?}", id); +// } +// } +// CoordinatorMessage::Stderr { id } => { +// let Ok(data) = Reflect::get(&data, &"data".into()) else { +// error!("Failed to get stderr data from message"); +// return; +// }; +// let data: Vec = data.unchecked_into::().to_vec(); + +// let mut inner = inner.lock().unwrap(); +// if let Some(instance) = inner.instances.get_mut(&id) { +// if let Err(e) = instance.stderr_writer.write_all(&data) { +// error!("Failed to write to stderr pipe: {}", e); +// } +// } else { +// warn!( +// "Received stderr for unknown instance ID {:?}: {}", +// id, +// String::from_utf8_lossy(&data) +// ); +// } +// } +// CoordinatorMessage::RunPluginResp { id, status } => { +// let mut inner = inner.lock().unwrap(); +// if let Some(instance) = inner.instances.get_mut(&id) { +// if let Some(tx) = instance.run_plugin_resp_tx.take() { +// let _ = tx.send(status.map_err(|e| e.to_string())); +// } else { +// warn!("No response channel found for instance {:?}", id); +// } +// } else { +// warn!("Received RunPluginResp for unknown instance ID {:?}", id); +// } +// } +// CoordinatorMessage::Log { message } => { +// info!("[Coordinator] {}", message); +// } +// _ => { +// warn!("Unexpected message received from coordinator"); +// } +// } +// } + +// fn stdin_proxy( +// reader: impl AsyncRead + Unpin + Send + 'static, +// id: InstanceId, +// inner: Arc>, +// ) { +// wasm_bindgen_futures::spawn_local(async move { +// let mut reader = reader; +// let mut buffer = [0u8; 1024]; + +// loop { +// match reader.read(&mut buffer).await { +// Ok(0) => break, // EOF +// Ok(n) => { +// let data = js_sys::Uint8Array::from(&buffer[..n]); +// let stdin_msg = CoordinatorMessage::Stdin { id }; +// let msg_value = serde_wasm_bindgen::to_value(&stdin_msg).unwrap(); +// Reflect::set(&msg_value, &"data".into(), &data).unwrap(); +// let transfer_list = js_sys::Array::of1(&data.buffer()); + +// let inner = inner.lock().unwrap(); +// if let Err(e) = inner +// .coordinator +// .post_message_with_transfer(&msg_value, &transfer_list) +// { +// error!("Failed to post stdin message: {:?}", e); +// break; +// } +// } +// Err(e) => { +// error!("Error reading from stdin: {}", e); +// break; +// } +// } +// } +// }); +// } diff --git a/crates/wasmi-plugin-hdk/src/server.rs b/crates/wasmi-plugin-hdk/src/server.rs index b2d4491..0f4490e 100644 --- a/crates/wasmi-plugin-hdk/src/server.rs +++ b/crates/wasmi-plugin-hdk/src/server.rs @@ -1,15 +1,15 @@ use serde::{Serialize, de::DeserializeOwned}; use serde_json::Value; use wasmi_plugin_pdk::{ + router::{BoxFuture, MaybeSend, Router}, rpc_message::RpcError, - server::{BoxFuture, MaybeSend, Router}, }; -use crate::{host_handler::HostHandler, plugin::PluginId}; +use crate::{host_handler::HostHandler, instance_id::InstanceId}; pub struct HostServer { state: S, - router: Router<(PluginId, S)>, + router: Router<(InstanceId, S)>, } impl Default for HostServer { @@ -23,6 +23,10 @@ impl Default for HostServer { impl HostServer { pub fn new(state: S) -> Self { + std::panic::set_hook(Box::new(|panic_info| { + eprintln!("GUEST PANIC: {}", panic_info); + })); + Self { router: Router::new(), state, @@ -33,7 +37,7 @@ impl HostServer { where P: DeserializeOwned + 'static, R: Serialize + 'static, - F: Fn((PluginId, S), P) -> Fut + Send + Sync + 'static, + F: Fn((InstanceId, S), P) -> Fut + Send + Sync + 'static, Fut: Future> + MaybeSend + 'static, { self.router = self.router.with_method(name, func); @@ -44,12 +48,12 @@ impl HostServer { impl HostHandler for HostServer { fn handle<'a>( &'a self, - plugin: PluginId, + instance: InstanceId, method: &'a str, params: Value, ) -> BoxFuture<'a, Result> { Box::pin(async move { - let state = (plugin, self.state.clone()); + let state = (instance, self.state.clone()); self.router.handle_with_state(state, method, params).await }) } diff --git a/crates/wasmi-plugin-hdk/src/session.rs b/crates/wasmi-plugin-hdk/src/session.rs new file mode 100644 index 0000000..ce47308 --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/session.rs @@ -0,0 +1,182 @@ +use std::sync::Arc; + +use futures::{ + AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, FutureExt, SinkExt, StreamExt, + io::BufReader, stream::FuturesUnordered, +}; +use serde_json::Value; +use thiserror::Error; +use tracing::{error, warn}; +use wasmi_plugin_pdk::{ + api::RequestHandler, + router::BoxFuture, + rpc_message::{RpcError, RpcMessage, RpcResponse}, +}; +use wasmi_plugin_rt::yield_now; + +/// A session for communicating with oneshot plugin over an async transport. +pub struct PluginSession { + reader: R, + writer: W, + handler: Arc, +} + +#[derive(Debug, Error)] +pub enum PluginSessionError { + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + #[error("Serde error: {0}, data: {1}")] + Serde(serde_json::Error, String), + #[error("EOF")] + Eof, + #[error(transparent)] + Rpc(#[from] RpcError), + #[error("Channel closed")] + ChannelClosed, +} + +impl> + PluginSession +{ + /// Create a new PluginSession with the given reader, writer, and request handler. + pub fn new(reader: R, writer: W, handler: H) -> Self { + PluginSession { + reader, + writer, + handler: Arc::new(handler), + } + } + + /// Calls a method asynchronously on the plugin and waits for the response. + pub async fn call_async( + self, + method: &str, + params: Value, + ) -> Result { + let (inbound_tx, inbound_rx) = futures::channel::mpsc::unbounded(); + let (outbound_tx, outbound_rx) = futures::channel::mpsc::unbounded(); + + //? Send initial request + let id = 1; + let request = RpcMessage::request(id, method.to_string(), params); + outbound_tx.unbounded_send(request).unwrap(); + + let reader_fut = reader_task(self.reader, inbound_tx).fuse(); + let writer_fut = writer_task(self.writer, outbound_rx).fuse(); + let dispatcher_fut = + dispatcher_task(self.handler.clone(), inbound_rx, outbound_tx, id).fuse(); + + futures::pin_mut!(reader_fut, writer_fut, dispatcher_fut); + + futures::select_biased! { + result = dispatcher_fut => result, + reader_result = reader_fut => { + reader_result?; + //? If reader task ends, it means the pipe has closed. However, + //? the final response might still be in flight in the dispatcher + //? task, so we wait for it to complete. If the pipe did EOF without + //? sending a response, the dispatcher will eventually starve + //? and EOF itself. + dispatcher_fut.await + } + writer_result = writer_fut => { + writer_result?; + Err(PluginSessionError::ChannelClosed) + } + } + } +} + +async fn reader_task( + reader: R, + tx: futures::channel::mpsc::UnboundedSender, +) -> Result<(), PluginSessionError> { + let mut reader = BufReader::new(reader); + let mut line = String::new(); + + loop { + line.clear(); + match reader.read_line(&mut line).await { + Ok(0) => { + //? EOF reached. Returning Ok allows the dispatcher to finish + //? processing any pending messages. + return Ok(()); + } + Ok(_) => {} + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + yield_now().await; + continue; + } + Err(e) => return Err(PluginSessionError::Io(e)), + } + + let msg: RpcMessage = + serde_json::from_str(&line).map_err(|e| PluginSessionError::Serde(e, line.clone()))?; + tx.unbounded_send(msg) + .map_err(|_| PluginSessionError::ChannelClosed)?; + } +} + +async fn writer_task( + mut writer: W, + mut rx: futures::channel::mpsc::UnboundedReceiver, +) -> Result<(), PluginSessionError> { + while let Some(msg) = rx.next().await { + let msg: String = serde_json::to_string(&msg).map_err(|e| { + error!("Serde error while serializing message: {}", e); + PluginSessionError::Serde(e, "".to_string()) + })?; + + let msg = format!("{}\n", msg); + writer.write_all(msg.as_bytes()).await?; + writer.flush().await?; + } + + Ok(()) +} + +async fn dispatcher_task>( + handler: Arc, + mut inbound: futures::channel::mpsc::UnboundedReceiver, + mut outbound: futures::channel::mpsc::UnboundedSender, + awaiting_id: u64, +) -> Result { + let mut pending: FuturesUnordered)>> = + FuturesUnordered::new(); + + loop { + futures::select_biased! { + // Complete handler + (id, result) = pending.select_next_some() => { + let msg = match result { + Ok(value) => RpcMessage::response(id, value), + Err(error) => RpcMessage::error_response(id, error), + }; + outbound.send(msg).await.map_err(|_| PluginSessionError::ChannelClosed)?; + }, + + // Inbound message + msg = inbound.next() => { + let Some(msg) = msg else { + return Err(PluginSessionError::ChannelClosed); + }; + + match msg { + RpcMessage::RpcResponse(resp) if resp.id == awaiting_id => return Ok(resp), + RpcMessage::RpcErrorResponse(err) if err.id == awaiting_id => return Err(err.error.into()), + RpcMessage::RpcRequest(req) => { + let handler = handler.clone(); + let fut = async move { + let result = handler.handle(&req.method, req.params).await; + (req.id, result) + }; + pending.push(Box::pin(fut)); + }, + _ => { + warn!("Unexpected message: {:?}", msg); + } + } + } + } + } +} diff --git a/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs b/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs deleted file mode 100644 index 6358d2a..0000000 --- a/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs +++ /dev/null @@ -1,763 +0,0 @@ -use std::{ - io::{BufRead, Read, Write}, - time::Duration, -}; -use tracing::{error, info, trace, warn}; -use wasmi_plugin_rt::web_time::{Instant, SystemTime}; - -/// A WASI context that can be attached to a wasmi instance. Attaches -/// a subset of WASI syscalls to the instance, allowing it to -/// get args, env vars, read/write to stdin/stdout/stderr, get time, and -/// get random bytes. -/// -/// Intentionally excludes filesystem (except for stdin/stdout/stderr) and network -/// access for improved security and compatibility. -/// -/// https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md -pub struct WasiCtx { - args: Vec, - env: Vec, - stdin_reader: Option>, - stdout_writer: Option>, - stderr_writer: Option>, - - // Time when the host should resume the guest from an out-of-fuel yield. - // If None, the guest should be started immediately. If Some, the guest - // should be resumed after this time is reached. - pub sleep_until: Option, - - /// Whether the Wasi instance is current paused waiting for stdin. If true, - /// the host may choose to wait for stdin to be ready before resuming execution. - pub awaiting_stdin: bool, -} - -#[repr(i32)] -#[derive(Debug, Clone, Copy)] -#[allow(dead_code)] -enum Errno { - Success = 0, - Access = 2, - Again = 6, - Badf = 8, - Fault = 21, - Inval = 28, - Io = 29, -} - -impl Default for WasiCtx { - fn default() -> Self { - Self::new() - } -} - -impl WasiCtx { - pub fn new() -> Self { - Self { - args: vec![], - env: vec![], - stdin_reader: None, - stdout_writer: None, - stderr_writer: None, - sleep_until: None, - awaiting_stdin: false, - } - } - - #[allow(dead_code)] - pub fn add_arg(mut self, arg: &str) -> Self { - self.args.push(arg.to_string()); - self - } - - #[allow(dead_code)] - pub fn add_env(mut self, key: &str, value: &str) -> Self { - self.env.push(format!("{}={}", key, value)); - self - } - - pub fn set_stdin(mut self, reader: R) -> Self { - let reader = std::io::BufReader::new(reader); - self.stdin_reader = Some(Box::new(reader)); - self - } - - pub fn set_stdout(mut self, writer: W) -> Self { - self.stdout_writer = Some(Box::new(writer)); - self - } - - pub fn set_stderr(mut self, writer: W) -> Self { - self.stderr_writer = Some(Box::new(writer)); - self - } - - /// Returns true if there is data available to read from stdin. - pub fn stdin_available(&mut self) -> bool { - let Some(reader) = &mut self.stdin_reader else { - return false; - }; - - match reader.fill_buf() { - Ok(buf) => !buf.is_empty(), - Err(_) => false, - } - } -} - -/// Adds the WASI context to the given wasmi linker. -pub fn add_to_linker(linker: &mut wasmi::Linker) -> Result<(), wasmi::Error> { - linker.func_wrap("wasi_snapshot_preview1", "args_get", args_get)?; - linker.func_wrap("wasi_snapshot_preview1", "args_sizes_get", args_sizes_get)?; - linker.func_wrap("wasi_snapshot_preview1", "environ_get", env_get)?; - linker.func_wrap("wasi_snapshot_preview1", "environ_sizes_get", env_sizes_get)?; - linker.func_wrap("wasi_snapshot_preview1", "fd_read", fd_read)?; - linker.func_wrap("wasi_snapshot_preview1", "fd_fdstat_get", fd_fdstat_get)?; - linker.func_wrap("wasi_snapshot_preview1", "fd_write", fd_write)?; - linker.func_wrap("wasi_snapshot_preview1", "clock_time_get", clock_time_get)?; - linker.func_wrap("wasi_snapshot_preview1", "fd_close", fd_close)?; - linker.func_wrap("wasi_snapshot_preview1", "random_get", random_get)?; - linker.func_wrap("wasi_snapshot_preview1", "sched_yield", sched_yield)?; - linker.func_wrap("wasi_snapshot_preview1", "poll_oneoff", poll_oneoff)?; - linker.func_wrap("wasi_snapshot_preview1", "proc_raise", proc_raise)?; - linker.func_wrap("wasi_snapshot_preview1", "proc_exit", proc_exit)?; - - Ok(()) -} - -/// Read command-line argument data. The size of the array should match that returned by args_sizes_get. Each argument is expected to be \0 terminated. -fn args_get(mut caller: wasmi::Caller<'_, WasiCtx>, argv: u32, argv_buf: u32) -> i32 { - trace!("wasi args_get({}, {})", argv, argv_buf); - - let ctx = caller.data_mut(); - let args = &ctx.args.clone(); - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); - - // Pointer in guest memory to an array of pointers that will be filled with the start of each argument string. - let mut argv_ptr = argv as usize; - // Pointer in guest memory to a buffer that will be filled with the `\0`-terminated argument strings. - let mut buf_ptr = argv_buf as usize; - - for arg in args { - if write_pointer_and_string(&mut caller, &memory, &mut argv_ptr, &mut buf_ptr, arg).is_err() - { - return Errno::Fault as i32; - } - } - - Errno::Success as i32 -} - -/// Returns the number of arguments and the size of the argument string data, or an error. -fn args_sizes_get(mut caller: wasmi::Caller<'_, WasiCtx>, offset0: u32, offset1: u32) -> i32 { - trace!("wasi args_sizes_get({}, {})", offset0, offset1); - - let ctx = caller.data_mut(); - let argc = ctx.args.len() as u32; - let argv_buf_len: u32 = ctx.args.iter().map(|s| s.len() as u32 + 1).sum(); // +1 for null terminator - - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); - - if memory - .write(&mut caller, offset0 as usize, &argc.to_le_bytes()) - .is_err() - { - return Errno::Fault as i32; - } - if memory - .write(&mut caller, offset1 as usize, &argv_buf_len.to_le_bytes()) - .is_err() - { - return Errno::Fault as i32; - } - - Errno::Success as i32 -} - -/// Read environment variable data. The sizes of the buffers should match that -/// returned by environ_sizes_get. Key/value pairs are expected to be joined -/// with =s, and terminated with \0s. -fn env_get(mut caller: wasmi::Caller<'_, WasiCtx>, environ: u32, environ_buf: u32) -> i32 { - trace!("wasi environ_get({}, {})", environ, environ_buf); - - let ctx = caller.data_mut(); - let env = &ctx.env.clone(); - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); - - // Pointer in guest memory to an array of pointers that will be filled with the start of each environment string. - let mut environ_ptr = environ as usize; - // Pointer in guest memory to a buffer that will be filled with the `\0`-terminated environment strings. - let mut buf_ptr = environ_buf as usize; - - for var in env { - if write_pointer_and_string(&mut caller, &memory, &mut environ_ptr, &mut buf_ptr, var) - .is_err() - { - return Errno::Fault as i32; - } - } - - Errno::Success as i32 -} - -/// Returns the number of environment variable arguments and the size of the environment variable data. -fn env_sizes_get(mut caller: wasmi::Caller<'_, WasiCtx>, offset0: u32, offset1: u32) -> i32 { - trace!("wasi environ_sizes_get({}, {})", offset0, offset1); - - let ctx = caller.data_mut(); - let envc = ctx.env.len() as u32; - let env_buf_len: u32 = ctx.env.iter().map(|s| s.len() as u32 + 1).sum(); // +1 for null terminator - - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); - - if memory - .write(&mut caller, offset0 as usize, &envc.to_le_bytes()) - .is_err() - { - return Errno::Fault as i32; - } - if memory - .write(&mut caller, offset1 as usize, &env_buf_len.to_le_bytes()) - .is_err() - { - return Errno::Fault as i32; - } - - Errno::Success as i32 -} - -/// Read from a file descriptor. Note: This is similar to readv in POSIX. -/// Basically that means that instead of reading into a single buffer, we read -/// into multiple buffers described by an array of iovec structures. So we -/// need to read `iov_len` elements from the `iov_ptr` array, each of which -/// describes a buffer we need to fill with data read from this file descriptor. -/// -/// - `fd`: The file descriptor. -/// - `iov_ptr`: Pointer to an array of iovec structures. -/// - `iov_len`: Number of iovec structures in the array -/// - `nread_ptr`: Number of bytes read. -/// -/// For now, we only bother implementing reading from fd 0 (stdin). -fn fd_read( - mut caller: wasmi::Caller<'_, WasiCtx>, - fd: u32, - iov_ptr: i32, - iov_len: i32, - nread_ptr: i32, -) -> i32 { - trace!( - "wasi fd_read({}, {}, {}, {})", - fd, iov_ptr, iov_len, nread_ptr - ); - - if fd != 0 { - return Errno::Badf as i32; - } - - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); - - let mut total_read = 0usize; - - for i in 0..iov_len { - let base = (iov_ptr as usize) + (i as usize * 8); - let mut buf_bytes = [0u8; 4]; - - memory.read(&caller, base, &mut buf_bytes).unwrap(); - let buf_addr = u32::from_le_bytes(buf_bytes) as usize; - - memory.read(&caller, base + 4, &mut buf_bytes).unwrap(); - let buf_len = u32::from_le_bytes(buf_bytes) as usize; - - // Allocate host buffer - let mut host_buf = vec![0u8; buf_len]; - - // Narrow scope: borrow reader only while calling `read` - let n = { - let ctx = caller.data_mut(); - match ctx.stdin_reader.as_mut() { - Some(r) => match r.read(&mut host_buf) { - Ok(n) => n, - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { - trace!("fd_read: WouldBlock, yielding to host"); - ctx.awaiting_stdin = true; - let _ = sched_yield(caller); - return Errno::Again as i32; - } - Err(_) => return Errno::Fault as i32, - }, - None => return Errno::Badf as i32, - } - }; // <- borrow of ctx ends here - - if n == 0 { - trace!("fd_read: EOF"); - break; - } - - total_read += n; - memory.write(&mut caller, buf_addr, &host_buf[..n]).unwrap(); - if n < buf_len { - break; // short read - } - } - - memory - .write( - &mut caller, - nread_ptr as usize, - &(total_read as u32).to_le_bytes(), - ) - .unwrap(); - - trace!("fd_read: total_read={}", total_read); - Errno::Success as i32 -} - -/// Write to a file descriptor. Note: This is similar to writev in POSIX. -/// -/// # Parameters -/// -/// - `fd`: The file descriptor. -/// - `ciov_ptr`: Pointer to an array of iovec structures. -/// - `ciov_len`: Number of iovec structures in the array -/// - `nwrite_ptr`: Number of bytes written. -/// -/// For now we only bother implementing writing to fd 1 (stdout) and fd 2 (stderr). -fn fd_write( - mut caller: wasmi::Caller<'_, WasiCtx>, - fd: i32, - ciov_ptr: i32, - ciov_len: i32, - nwrite_ptr: i32, -) -> i32 { - trace!( - "wasi fd_write({}, {}, {}, {})", - fd, ciov_ptr, ciov_len, nwrite_ptr - ); - - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); - - let mut total_written = 0usize; - - for i in 0..ciov_len { - // Each ciovec is { buf: u32, len: u32 } - let base = (ciov_ptr as usize) + (i as usize * 8); - - let mut buf_bytes = [0u8; 4]; - - // Read buf pointer - memory.read(&caller, base, &mut buf_bytes).unwrap(); - let buf_addr = u32::from_le_bytes(buf_bytes) as usize; - - // Read buf length - memory.read(&caller, base + 4, &mut buf_bytes).unwrap(); - let buf_len = u32::from_le_bytes(buf_bytes) as usize; - - // Copy from guest memory - let mut host_buf = vec![0u8; buf_len]; - if memory.read(&caller, buf_addr, &mut host_buf).is_err() { - return Errno::Fault as i32; - } - - // Borrow writer and write message - //? Narrow scope since we're also mutably borrowing caller elsewhere for memory access - let ctx = caller.data_mut(); - let writer = match fd { - 1 => ctx.stdout_writer.as_mut(), - 2 => ctx.stderr_writer.as_mut(), - _ => return Errno::Badf as i32, - }; - let writer = match writer { - Some(w) => w, - None => return Errno::Badf as i32, - }; - - match writer.write(&host_buf) { - Ok(n) => { - total_written += n; - if n < buf_len { - break; // partial write, stop - } - } - Err(_) => return Errno::Io as i32, - } - } - - // Write total_written into nwrite_ptr - if memory - .write( - &mut caller, - nwrite_ptr as usize, - &(total_written as u32).to_le_bytes(), - ) - .is_err() - { - return Errno::Fault as i32; - } - - Errno::Success as i32 -} - -fn fd_fdstat_get(mut caller: wasmi::Caller<'_, WasiCtx>, fd: i32, buf_ptr: i32) -> i32 { - trace!("wasi fd_fdstat_get({}, {})", fd, buf_ptr); - - // Constants from wasi_snapshot_preview1 - const FILETYPE_CHARACTER_DEVICE: u8 = 2; - - // fdflags (none set) - let fs_flags: u16 = 0; - - // rights (for simplicity, we just allow read or write) - const RIGHTS_FD_READ: u64 = 1 << 1; - const RIGHTS_FD_WRITE: u64 = 1 << 6; - - let (filetype, rights_base) = match fd { - 0 => (FILETYPE_CHARACTER_DEVICE, RIGHTS_FD_READ), - 1 | 2 => (FILETYPE_CHARACTER_DEVICE, RIGHTS_FD_WRITE), - _ => return Errno::Badf as i32, - }; - - let rights_inheriting: u64 = 0; - - let mut buf = [0u8; 24]; - buf[0] = filetype; - buf[1..3].copy_from_slice(&fs_flags.to_le_bytes()); - buf[8..16].copy_from_slice(&rights_base.to_le_bytes()); - buf[16..24].copy_from_slice(&rights_inheriting.to_le_bytes()); - - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); - - if memory.write(&mut caller, buf_ptr as usize, &buf).is_err() { - return Errno::Fault as i32; - } - - Errno::Success as i32 -} - -/// Close a file descriptor. For now we only support closing stdin, stdout, stderr. -fn fd_close(mut caller: wasmi::Caller<'_, WasiCtx>, fd: i32) -> i32 { - trace!("wasi fd_close({})", fd); - - let ctx = caller.data_mut(); - - match fd { - 0 => ctx.stdin_reader = None, - 1 => ctx.stdout_writer = None, - 2 => ctx.stderr_writer = None, - _ => return Errno::Badf as i32, - } - - Errno::Success as i32 -} - -fn clock_time_get( - mut caller: wasmi::Caller<'_, WasiCtx>, - clock_id: i32, - _precision: i64, - result_ptr: i32, -) -> i32 { - trace!("wasi clock_time_get({}, _, {})", clock_id, result_ptr); - - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); - - let now = match clock_id { - // Realtime: nanoseconds since UNIX epoch - 0 | 1 => { - match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { - Ok(dur) => dur.as_nanos() as u64, - Err(_) => return Errno::Inval as i32, // time before epoch shouldn't happen - } - } - _ => { - warn!("unsupported clock_id {}", clock_id); - return Errno::Inval as i32; // unsupported clock - } - }; - - if memory - .write(&mut caller, result_ptr as usize, &now.to_le_bytes()) - .is_err() - { - return Errno::Fault as i32; - } - Errno::Success as i32 -} - -fn random_get(mut caller: wasmi::Caller<'_, WasiCtx>, buf_ptr: i32, buf_len: i32) -> i32 { - trace!("wasi random_get({}, {})", buf_ptr, buf_len); - - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); - - let mut buf = vec![0u8; buf_len as usize]; - if let Err(e) = getrandom::getrandom(&mut buf) { - eprintln!("random_get failed: {:?}", e); - return Errno::Io as i32; - } - - if memory.write(&mut caller, buf_ptr as usize, &buf).is_err() { - return Errno::Fault as i32; - } - - Errno::Success as i32 -} - -fn sched_yield(mut caller: wasmi::Caller<'_, WasiCtx>) -> i32 { - trace!("wasi sched_yield()"); - - // TODO: Would probably better to yield here instead of setting fuel to 0. - // I can't test this, but I expect interrupting & restarting execution isn't very - // effective, expecially for repeated calls like from `fd_read` loops. - caller.set_fuel(0).unwrap(); - - Errno::Success as i32 -} - -const CLOCK_EVENT_TYPE: u8 = 0; - -/// Concurrently poll for the occurrence of a set of events. -/// -/// Essentially allows the guest to wait for multiple events (timers, fd write/read -/// readiness - subscription_u). -/// -/// Based on my understanding & reading other implementations, the function is -/// stateless. So each time it's called it waits until one of the subscriptions -/// is ready, writes the corresponding events into `events_ptr` and returns the -/// number of events written. -/// -/// It is not persistent across calls - so if a guest wants to wait for multiple -/// events, it needs to call this function with the same subscriptions again. -/// -/// For this impl we only care about timers, so we'll ignore the fd subscriptions -/// and return immediately with no event. -fn poll_oneoff( - mut caller: wasmi::Caller<'_, WasiCtx>, - subs_ptr: i32, - events_ptr: i32, - nsubscriptions: i32, - result_ptr: i32, -) -> i32 { - info!( - "wasi poll_oneoff({}, {}, {}, {})", - subs_ptr, events_ptr, nsubscriptions, result_ptr - ); - - if nsubscriptions == 0 { - error!("poll_oneoff called with zero subscriptions"); - return Errno::Inval as i32; - } - - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); - - let mut clock_subscriptions = Vec::new(); - for i in 0..nsubscriptions { - // Each subscription is 48 bytes - let sub_offset = (subs_ptr as usize) + (i as usize * 48); - - // Read the subscription struct - // subscription layout (48 bytes total): - // - userdata: u64 at offset 0 - // - type: u8 at offset 8 (eventtype: 0=clock, 1=fd_read, 2=fd_write) - // - padding: 7 bytes - // - union data: 32 bytes at offset 16 - // - // For clock subscription (subscription_clock at offset 16): - // - id: u32 (clockid) at offset 16 - // - padding: 4 bytes - // - timeout: u64 (timestamp) at offset 24 - // - precision: u64 (timestamp) at offset 32 - // - flags: u16 (subclockflags) at offset 40 - - let mut sub_bytes = [0u8; 48]; - memory.read(&caller, sub_offset, &mut sub_bytes).unwrap(); - - let userdata = u64::from_le_bytes(sub_bytes[0..8].try_into().unwrap()); - let sub_type = sub_bytes[8]; - - if sub_type != CLOCK_EVENT_TYPE { - warn!( - "poll_oneoff: only clock subscriptions supported, got type {}", - sub_type - ); - continue; - } - - let clock_id = u32::from_le_bytes(sub_bytes[16..20].try_into().unwrap()); - let timeout_ns = u64::from_le_bytes(sub_bytes[24..32].try_into().unwrap()); - let precision_ns = u64::from_le_bytes(sub_bytes[32..40].try_into().unwrap()); - let flags = u16::from_le_bytes(sub_bytes[40..42].try_into().unwrap()); - - if clock_id > 1 { - // Only realtime (0) and monotonic (1) - warn!("poll_oneoff: unsupported clock_id {}", clock_id); - continue; - } - - let is_absolute = (flags & 1) != 0; - trace!( - "poll_oneoff: clock_id={}, timeout={}, precision={}, flags={:#x}, is_absolute={}", - clock_id, timeout_ns, precision_ns, flags, is_absolute - ); - - clock_subscriptions.push((userdata, clock_id, timeout_ns, is_absolute)); - } - - info!( - "poll_oneoff: {} clock subscriptions", - clock_subscriptions.len() - ); - - if clock_subscriptions.is_empty() { - warn!("poll_oneoff: no valid clock subscriptions found"); - return Errno::Inval as i32; - } - - // Convert all relative timeouts to absolute deadlines - let now_ns = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { - Ok(dur) => dur.as_nanos() as u64, - Err(_) => return Errno::Inval as i32, // time before epoch shouldn't happen - }; - - clock_subscriptions.iter_mut().for_each(|sub| { - if !sub.3 { - sub.2 = now_ns.saturating_add(sub.2); - } - }); - - // Find the earliest deadline - let (earliest_userdata, _earliest_clock_id, earliest_deadline_ns, _) = - clock_subscriptions.iter().min_by_key(|sub| sub.2).unwrap(); - - // poll_oneoff MUST block from the guest's perspective until 1+ - // events are ready. If we try to return immediately with no events (either) - // via an Errno::Again or Errno::Success with 0 events, the guest will panic. - // - // We can't block here since that this isn't an async block (due to wasmi's API) - // and we don't want to block the entire executor thread since we're in a single- - // threaded-context. SO instead we instantly return a successful result for the - // earliest timer, and if the deadline hasn't been reached yet we set the fuel - // to zero to yield execution back to the host. The host then waits until the - // deadline is reached before resuming execution. - - //? Future deadline trap - if earliest_deadline_ns > &now_ns { - let wait_duration = Duration::from_nanos(earliest_deadline_ns - now_ns); - caller.data_mut().sleep_until = Some(Instant::now() + wait_duration); - info!( - "poll_oneoff: waiting for {:?} before resuming guest", - wait_duration - ); - - //? Need to yield, so set fuel to zero - caller.set_fuel(0).unwrap(); - } - - // Write the event for the earliest deadline - // size: 32 - // - userdata: at offset 0 - // - error: at offset 8 - // - type: at offset 10 - // - fd_readwrite: at offset 16 (empty for clock events) - let mut event_buf = [0u8; 32]; - event_buf[0..8].copy_from_slice(&earliest_userdata.to_le_bytes()); - event_buf[8..10].copy_from_slice(&(Errno::Success as u16).to_le_bytes()); - event_buf[10] = CLOCK_EVENT_TYPE; - // fd_readwrite is empty for clock events - - info!( - "Writing event: userdata={}, error={}, type={}", - earliest_userdata, - Errno::Success as u16, - CLOCK_EVENT_TYPE - ); - info!("Event buffer: {:?}", &event_buf[..16]); - - let event_offset = events_ptr as usize; - memory.write(&mut caller, event_offset, &event_buf).unwrap(); - - // Write number of events (1) into result_ptr - memory - .write(&mut caller, result_ptr as usize, &1u32.to_le_bytes()) - .unwrap(); - - Errno::Success as i32 -} - -fn proc_raise(_caller: wasmi::Caller<'_, WasiCtx>, sig: i32) -> Result<(), wasmi::Error> { - info!("wasi proc_raise({})", sig); - - // https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md#signal - match sig { - 0 | 13 | 16 | 17 | 22 | 27 => { - // Ignored signals - Ok(()) - } - _ => { - // Termination signals - Err(wasmi::Error::i32_exit(128 + sig)) - } - } -} - -fn proc_exit(_caller: wasmi::Caller<'_, WasiCtx>, status: i32) -> Result<(), wasmi::Error> { - info!("wasi proc_exit({})", status); - Err(wasmi::Error::i32_exit(status)) -} - -/// Write a pointer to a string into a table, and the string itself into a buffer, -/// then advance the table and buffer pointers. -/// -/// Utility for writing into guest memory. -fn write_pointer_and_string( - caller: &mut wasmi::Caller<'_, WasiCtx>, - memory: &wasmi::Memory, - table_ptr: &mut usize, - buf_ptr: &mut usize, - s: &str, -) -> Result<(), Errno> { - // 1. Write pointer into table - let ptr_le = (*buf_ptr as u32).to_le_bytes(); - memory - .write(&mut *caller, *table_ptr, &ptr_le) - .map_err(|_| Errno::Fault)?; - *table_ptr += 4; - - // 2. Write string+null into buffer - let mut tmp = Vec::with_capacity(s.len() + 1); - tmp.extend_from_slice(s.as_bytes()); - tmp.push(0); // null terminator - memory - .write(&mut *caller, *buf_ptr, &tmp) - .map_err(|_| Errno::Fault)?; - *buf_ptr += tmp.len(); - - Ok(()) -} diff --git a/crates/wasmi-plugin-hdk/src/wasi/wasmi.rs b/crates/wasmi-plugin-hdk/src/wasi/wasmi.rs deleted file mode 100644 index 3b4bd67..0000000 --- a/crates/wasmi-plugin-hdk/src/wasi/wasmi.rs +++ /dev/null @@ -1,125 +0,0 @@ -use std::sync::{Arc, atomic::AtomicBool}; - -use thiserror::Error; -use tracing::{error, trace}; -use wasmi::{Func, Store}; -use wasmi_plugin_rt::{now, sleep, yield_now}; - -use crate::wasi::wasi_ctx::WasiCtx; - -#[derive(Error, Debug)] -pub enum RunError { - #[error("wasmi error")] - WasmiError(#[from] wasmi::Error), - #[error("resume error")] - ResumeError(wasmi::Error), - #[error("host trap")] - HostTrap(wasmi::ResumableCallHostTrap), -} - -/// Notes on interuptability and performance implications (Robert's desktop - ryzen 5 3600). -/// -/// For a task of running a prime number sieve on 10_000 elements, it: -/// - takes 499 ms when MAX_FUEL is 100_000_000 (no refuels needed) -/// - takes 528 ms when MAX_FUEL is 1_000_000 (8 refuels needed, each one taking 62 ms) -/// - takes ~550 ms when MAX_FUEL is 100_000 (83 refuels needed, each one taking ~6.6 ms) -/// - takes ~575 ms when MAX_FUEL is 10_000 (836 refuels needed, each one taking ~600 us) -/// -/// Significantly lower fuel does not significantly lower performance. -/// For this reason, I figure it's fine to keep MAX_FUEL very low and to build in -/// async yielding into the `run_wasm` function so it works better in single-thread -/// environments (like within wasm when building to target the web). If this later -/// becomes a performance issue we can test it properly. -pub const MAX_FUEL: u64 = 100_000; - -/// Spawns a new async task to run the given wasm start function. `is_running` -/// is an atomic signal that can be used to forcefully exit the plugin early. -pub fn spawn_wasm( - store: Store, - start_func: Func, - is_running: Arc, - max_fuel: Option, -) -> impl Future { - trace!("Spawning plugin task"); - let is_running = is_running.clone(); - async move { - if let Err(e) = run_wasm(store, start_func, is_running.clone(), max_fuel).await { - error!("Plugin error: {:?}", e); - } - is_running.store(false, std::sync::atomic::Ordering::SeqCst); - } -} - -/// run_wasm manages the plugin's lifecycle. Because -/// wasmi doesn't support plugin intercepts or async execution, we -/// need some manual way of interrupting the plugin every so often to check if -/// it's been killed and yield to other cooperative tasks. Here I do that by setting -/// a low fuel limit, catching the out-of-fuel condition and resuming the plugin -/// when it's not been killed. -async fn run_wasm( - mut store: Store, - start_func: Func, - is_running: Arc, - max_fuel: Option, -) -> Result<(), RunError> { - trace!("Plugin task started"); - let max_fuel = max_fuel.unwrap_or(MAX_FUEL); - - //? Starts with zero fuel so we fall into the resumable loop - store.set_fuel(0).unwrap(); - let mut resumable = start_func.call_resumable(&mut store, &[], &mut [])?; - - loop { - match resumable { - wasmi::ResumableCall::Finished => return Ok(()), - wasmi::ResumableCall::HostTrap(trap) => { - return Err(RunError::HostTrap(trap)); - } - wasmi::ResumableCall::OutOfFuel(out_of_fuel) => { - if !is_running.load(std::sync::atomic::Ordering::SeqCst) { - return Ok(()); - } - - let required = out_of_fuel.required_fuel(); - let top_up = required.max(max_fuel); - store.set_fuel(top_up).unwrap(); - - // Handle any sleep requests - if let Some(resume_time) = store.data().sleep_until { - let now = now(); - if resume_time > now { - trace!( - "Plugin requested sleep until {:?}, sleeping...", - resume_time - ); - let dur = resume_time - now; - sleep(dur).await; - } - store.data_mut().sleep_until = None; - } - - // Handle any stdin wait requests, avoids busy waiting - if store.data().awaiting_stdin && !store.data_mut().stdin_available() { - trace!("Plugin is paused waiting for stdin, yielding..."); - yield_now().await; - store.data_mut().awaiting_stdin = false; - } - - // Courtesy yield to other tasks - trace!("Plugin out of fuel, yielding..."); - yield_now().await; - - match out_of_fuel.resume(&mut store, &mut []) { - Ok(next) => { - trace!("Plugin resumed after yielding"); - resumable = next; - } - Err(e) => { - error!("Error resuming plugin: {:?}", e); - return Err(RunError::ResumeError(e)); - } - } - } - } - } -} diff --git a/crates/wasmi-plugin-hdk/tests/integration.rs b/crates/wasmi-plugin-hdk/tests/integration.rs index d1118f5..dcb8883 100644 --- a/crates/wasmi-plugin-hdk/tests/integration.rs +++ b/crates/wasmi-plugin-hdk/tests/integration.rs @@ -1,53 +1,102 @@ use serde_json::Value; -use std::sync::Arc; +use std::{ + sync::{Arc, Once}, + time::Duration, +}; use tracing::info; use wasmi_plugin_hdk::{plugin::Plugin, server::HostServer}; -use wasmi_plugin_pdk::transport::Transport; -use web_time::{Instant, SystemTime}; +use wasmi_plugin_pdk::transport::AsyncTransport; #[cfg(target_family = "wasm")] use wasm_bindgen_test::*; +#[cfg(target_family = "wasm")] +wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + const PLUGIN_WASM: &[u8] = include_bytes!("../../../target/wasm32-wasip1/release/test-plugin.wasm"); +static INIT: Once = Once::new(); + fn load_plugin_wasm() -> Vec { PLUGIN_WASM.to_vec() } +async fn load_plugin() -> Plugin { + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + let plugin = Plugin::builder("test_plugin", wasm_bytes, handler) + .with_timeout(Duration::from_secs(5)) + .build() + .await + .unwrap(); + + plugin +} + fn get_host_server() -> HostServer<()> { HostServer::default() - .with_method("ping", |_, _params: ()| async move { - info!("Received ping request, sending pong response"); - Ok("pong".to_string()) - }) - .with_method("echo", |_, params: Value| async move { - info!("Received echo request, returning response"); - Ok(params) - }) + .with_method( + "ping", + |_, _params: ()| async move { Ok("pong".to_string()) }, + ) + .with_method("echo", |_, params: Value| async move { Ok(params) }) +} + +fn setup_logs() { + #[cfg(not(target_family = "wasm"))] + { + INIT.call_once(|| { + tracing_subscriber::fmt() + .compact() + .with_ansi(false) + .with_max_level(tracing_subscriber::filter::LevelFilter::INFO) + .with_test_writer() + .init(); + }); + } + + #[cfg(target_family = "wasm")] + { + INIT.call_once(|| { + tracing_wasm::set_as_global_default_with_config( + tracing_wasm::WASMLayerConfigBuilder::new() + .set_console_config(tracing_wasm::ConsoleConfig::ReportWithoutConsoleColor) + .build(), + ); + }); + } } #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] #[cfg_attr(not(target_family = "wasm"), tokio::test)] async fn test_plugin() { + setup_logs(); info!("Starting test_plugin..."); let wasm_bytes = load_plugin_wasm(); let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); - plugin.call("ping", Value::Null).await.unwrap(); + let plugin = Plugin::builder("test_plugin", wasm_bytes.clone(), handler) + .with_timeout(Duration::from_secs(2)) + .build() + .await + .unwrap(); + let resp = plugin.call_async("ping", Value::Null).await.unwrap(); + + assert_eq!(resp.result.as_str().unwrap(), "pong"); } #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] #[cfg_attr(not(target_family = "wasm"), tokio::test)] async fn test_get_random_number() { + setup_logs(); info!("Starting get_random_number test..."); - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - - let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); - let response = plugin.call("get_random_number", Value::Null).await.unwrap(); + let plugin = load_plugin().await; + let response = plugin + .call_async("get_random_number", Value::Null) + .await + .unwrap(); info!("Random number response: {:?}", response); response.result.as_u64().unwrap(); @@ -56,18 +105,16 @@ async fn test_get_random_number() { #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] #[cfg_attr(not(target_family = "wasm"), tokio::test)] async fn test_get_time() { + setup_logs(); info!("Starting get_time test..."); - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - - let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); - let response = plugin.call("get_time", Value::Null).await.unwrap(); + let plugin = load_plugin().await; + let response = plugin.call_async("get_time", Value::Null).await.unwrap(); info!("Get time response: {:?}", response); let timestamp = response.result.as_u64().unwrap(); - let now = SystemTime::now() + let now = web_time::SystemTime::now() .duration_since(web_time::UNIX_EPOCH) .unwrap() .as_secs(); @@ -76,20 +123,19 @@ async fn test_get_time() { assert!(diff < 2, "Timestamp difference too large: {}", diff); } -#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +// TODO: Fix this for `wasm-pack test --headless --firefox ./crates/wasmi-plugin-hdk` +// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] #[cfg_attr(not(target_family = "wasm"), tokio::test)] async fn test_sleep() { + setup_logs(); info!("Starting sleep test..."); - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - - let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); + let plugin = load_plugin().await; let sleep_duration = 1500; // milliseconds - let start = Instant::now(); + let start = web_time::Instant::now(); plugin - .call("sleep", Value::Number(sleep_duration.into())) + .call_async("sleep", Value::Number(sleep_duration.into())) .await .unwrap(); let elapsed = start.elapsed().as_millis(); @@ -103,31 +149,62 @@ async fn test_sleep() { #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] #[cfg_attr(not(target_family = "wasm"), tokio::test)] -async fn test_many_echo() { - info!("Starting many echo test..."); +async fn test_call() { + setup_logs(); + info!("Starting call test..."); - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); + let plugin = load_plugin().await; + let resp = plugin.call_async("call", Value::Null).await.unwrap(); - let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); + assert_eq!(resp.result.as_str().unwrap(), "pong"); +} + +#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +#[cfg_attr(not(target_family = "wasm"), tokio::test)] +async fn test_call_many() { + setup_logs(); + info!("Starting call_many test..."); + + let plugin = load_plugin().await; plugin - .call("many_echo", Value::Number(200.into())) + .call_async("call_many", Value::Number(500.into())) .await .unwrap(); } #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] #[cfg_attr(not(target_family = "wasm"), tokio::test)] -async fn test_prime_sieve() { - info!("Starting prime sieve test..."); +async fn test_call_async() { + setup_logs(); + info!("Starting call_async test..."); - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); + let plugin = load_plugin().await; + let resp = plugin.call_async("call_async", Value::Null).await.unwrap(); + assert_eq!(resp.result.as_str().unwrap(), "pong"); +} + +#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +#[cfg_attr(not(target_family = "wasm"), tokio::test)] +async fn test_call_many_async() { + setup_logs(); + info!("Starting call_many_async test..."); - let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); + let plugin = load_plugin().await; + plugin + .call_async("call_many_async", Value::Number(500.into())) + .await + .unwrap(); +} + +#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +#[cfg_attr(not(target_family = "wasm"), tokio::test)] +async fn test_prime_sieve() { + setup_logs(); + info!("Starting prime sieve test..."); + let plugin = load_plugin().await; let response = plugin - .call("prime_sieve", Value::Number(1000.into())) + .call_async("prime_sieve", Value::Number(1000.into())) .await .unwrap(); diff --git a/crates/wasmi-plugin-pdk/Cargo.toml b/crates/wasmi-plugin-pdk/Cargo.toml index 4472ed4..e8814c5 100644 --- a/crates/wasmi-plugin-pdk/Cargo.toml +++ b/crates/wasmi-plugin-pdk/Cargo.toml @@ -13,12 +13,10 @@ categories.workspace = true exclude.workspace = true [dependencies] -async-trait = { workspace = true } futures = { workspace = true } +tokio = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } -tokio = { workspace = true } tracing = { workspace = true } -tracing-subscriber = { workspace = true } wasmi-plugin-rt = { workspace = true } diff --git a/crates/wasmi-plugin-pdk/src/api.rs b/crates/wasmi-plugin-pdk/src/api.rs index e81c8b8..f13b530 100644 --- a/crates/wasmi-plugin-pdk/src/api.rs +++ b/crates/wasmi-plugin-pdk/src/api.rs @@ -1,9 +1,6 @@ use serde_json::Value; -use crate::{rpc_message::RpcError, server::BoxFuture}; - -pub trait ApiError: From + Send + Sync {} -impl ApiError for T where T: From + Send + Sync {} +use crate::router::BoxFuture; /// JSON-RPC request handler. /// diff --git a/crates/wasmi-plugin-pdk/src/lib.rs b/crates/wasmi-plugin-pdk/src/lib.rs index 39ec3c4..16a8eef 100644 --- a/crates/wasmi-plugin-pdk/src/lib.rs +++ b/crates/wasmi-plugin-pdk/src/lib.rs @@ -1,4 +1,6 @@ pub mod api; +mod poll_oneoff; +pub mod router; pub mod rpc_message; -pub mod server; +pub mod runner; pub mod transport; diff --git a/crates/wasmi-plugin-pdk/src/poll_oneoff.rs b/crates/wasmi-plugin-pdk/src/poll_oneoff.rs new file mode 100644 index 0000000..f170b1b --- /dev/null +++ b/crates/wasmi-plugin-pdk/src/poll_oneoff.rs @@ -0,0 +1,22 @@ +#[link(wasm_import_module = "wasi_snapshot_preview1")] +unsafe extern "C" { + fn poll_oneoff(subs: *const u8, events: *mut u8, nsubs: usize, nevents: *mut usize) -> u16; +} + +/// A helper that uses WASI poll_oneoff to sleep the thread until STDIN is ready +pub fn wait_for_stdin() { + let mut sub_bytes = [0u8; 48]; + let mut event_bytes = [0u8; 32]; + let mut nevents: usize = 0; + + sub_bytes[8] = 1; + + unsafe { + poll_oneoff( + sub_bytes.as_ptr(), + event_bytes.as_mut_ptr(), + 1, + &mut nevents, + ); + } +} diff --git a/crates/wasmi-plugin-pdk/src/server.rs b/crates/wasmi-plugin-pdk/src/router.rs similarity index 55% rename from crates/wasmi-plugin-pdk/src/server.rs rename to crates/wasmi-plugin-pdk/src/router.rs index 09a8574..805f0f6 100644 --- a/crates/wasmi-plugin-pdk/src/server.rs +++ b/crates/wasmi-plugin-pdk/src/router.rs @@ -1,11 +1,9 @@ -use std::{collections::HashMap, sync::Arc}; +use std::collections::HashMap; use serde::{Serialize, de::DeserializeOwned}; use serde_json::Value; -use tokio::runtime::Builder; -use tracing::warn; -use crate::{api::RequestHandler, rpc_message::RpcError, transport::JsonRpcTransport}; +use crate::rpc_message::RpcError; #[cfg(target_arch = "wasm32")] pub type BoxFuture<'a, T> = futures::future::LocalBoxFuture<'a, T>; @@ -25,71 +23,11 @@ impl MaybeSend for T {} type HandlerFn = dyn Send + Sync + Fn(S, Value) -> BoxFuture<'static, Result>; -/// Server is a RPC server that can handle requests by dispatching them to registered -/// handler functions. It stores a shared state `S` that is passed into each handler. -pub struct PluginServer { - transport: Arc, - router: Router>, -} - +/// A router for JSON-RPC methods, dispatching calls to registered handler functions pub struct Router { handlers: HashMap>>, } -impl PluginServer { - pub fn new(transport: Arc) -> Self { - Self { - transport, - router: Router::new(), - } - } - - pub fn new_with_transport() -> Self { - let reader = std::io::BufReader::new(std::io::stdin()); - let writer = std::io::stdout(); - let transport = JsonRpcTransport::new(reader, writer); - let transport = Arc::new(transport); - - Self { - transport, - router: Router::new(), - } - } - - pub fn with_method(mut self, name: &str, func: F) -> Self - where - P: DeserializeOwned + 'static, - R: Serialize + 'static, - F: Fn(Arc, P) -> Fut + Send + Sync + 'static, - Fut: Future> + MaybeSend + 'static, - { - self.router = self.router.with_method(name, func); - self - } - - pub fn run(self) { - let transport = self.transport.clone(); - - let rt = Builder::new_current_thread().enable_time().build().unwrap(); - let local = tokio::task::LocalSet::new(); - - rt.block_on(local.run_until(async move { - let _ = transport.process_next_line(Some(&self)).await; - })); - } -} - -impl RequestHandler for PluginServer { - fn handle<'a>( - &'a self, - method: &'a str, - params: Value, - ) -> BoxFuture<'a, Result> { - let state = self.transport.clone(); - Box::pin(async move { self.router.handle_with_state(state, method, params).await }) - } -} - impl Default for Router { fn default() -> Self { Router { @@ -104,7 +42,7 @@ impl Router { } /// Register a new RPC method with the router. The method is identified by the - /// given name, and the handler function should accept the shared state and + /// given name, and the handler function should accept the shared state `S` and /// deserialized params. /// /// Handlers should implement: `async fn handler(state: S, params: P) -> Result` @@ -136,6 +74,9 @@ impl Router { self } + /// Handle a request by dispatching to the appropriate registered handler. + /// + /// If no handler is found for the method, returns `Err(MethodNotFound)`. pub async fn handle_with_state( &self, state: S, @@ -143,7 +84,6 @@ impl Router { params: Value, ) -> Result { let Some(handler) = self.handlers.get(method) else { - warn!("Method not found: {}", method); return Err(RpcError::MethodNotFound); }; handler(state, params).await diff --git a/crates/wasmi-plugin-pdk/src/rpc_message.rs b/crates/wasmi-plugin-pdk/src/rpc_message.rs index 30859d6..ed961af 100644 --- a/crates/wasmi-plugin-pdk/src/rpc_message.rs +++ b/crates/wasmi-plugin-pdk/src/rpc_message.rs @@ -32,76 +32,127 @@ pub struct RpcErrorResponse { pub error: RpcError, } -#[derive(Error, Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +#[derive(Error, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub enum RpcError { #[error("Parse error")] ParseError, - #[error("Invalid request")] - InvalidRequest, #[error("Method not found")] MethodNotFound, #[error("Invalid params")] InvalidParams, - #[error("Internal error")] - InternalError, + /// Custom error with message. Messages are unstable and inconsistent across + /// different plugins and versions. They should NEVER be relied upon for + /// program logic. + /// + /// TODO: Consider poisoning equality checks on this variant to prevent + /// accidental reliance on error message contents. #[error("{0}")] Custom(String), } +pub trait RpcErrorContext { + fn context(self, msg: impl Into) -> Result; + fn with_context(self, f: F) -> Result + where + F: FnOnce() -> S, + S: Into; +} + +pub trait ToRpcResult { + fn rpc_err(self) -> Result; +} + +impl RpcMessage { + pub fn request(id: u64, method: String, params: Value) -> Self { + RpcMessage::RpcRequest(RpcRequest { + jsonrpc: "2.0".to_string(), + id, + method, + params, + }) + } + + pub fn response(id: u64, result: Value) -> Self { + RpcMessage::RpcResponse(RpcResponse { + jsonrpc: "2.0".to_string(), + id, + result, + }) + } + + pub fn error_response(id: u64, error: RpcError) -> Self { + RpcMessage::RpcErrorResponse(RpcErrorResponse { + jsonrpc: "2.0".to_string(), + id, + error, + }) + } +} + impl RpcError { - pub fn code(&self) -> i64 { - match self { - RpcError::ParseError => -32700, - RpcError::InvalidRequest => -32600, - RpcError::MethodNotFound => -32601, - RpcError::InvalidParams => -32602, - RpcError::InternalError => -32603, - RpcError::Custom(_) => -32000, - } + pub fn custom>(msg: S) -> Self { + RpcError::Custom(msg.into()) } +} - pub fn message(&self) -> String { - self.to_string() +impl From for RpcError { + fn from(value: RpcErrorResponse) -> Self { + value.error } } -pub fn to_rpc_err(e: E) -> RpcError { - RpcError::Custom(e.to_string()) +impl From for RpcError { + fn from(s: String) -> Self { + RpcError::Custom(s) + } +} + +impl From<&str> for RpcError { + fn from(s: &str) -> Self { + RpcError::Custom(s.to_string()) + } } -impl Serialize for RpcError { - fn serialize(&self, serializer: S) -> Result +impl From> for RpcError { + fn from(e: Box) -> Self { + RpcError::Custom(e.to_string()) + } +} + +impl RpcErrorContext for Result { + fn context(self, msg: impl Into) -> Result { + self.map_err(|e| RpcError::Custom(format!("{}: {}", msg.into(), e))) + } + + fn with_context(self, f: F) -> Result where - S: serde::Serializer, + F: FnOnce() -> S, + S: Into, { - use serde::ser::SerializeStruct; - let mut state = serializer.serialize_struct("RpcError", 2)?; - state.serialize_field("code", &self.code())?; - state.serialize_field("message", &self.message())?; - state.end() + self.map_err(|e| RpcError::Custom(format!("{}: {}", f().into(), e))) } } -impl<'de> Deserialize<'de> for RpcError { - fn deserialize(deserializer: D) -> Result +impl RpcErrorContext for Option { + fn context(self, msg: impl Into) -> Result { + self.ok_or_else(|| RpcError::Custom(msg.into())) + } + + fn with_context(self, f: F) -> Result where - D: serde::Deserializer<'de>, + F: FnOnce() -> S, + S: Into, { - #[derive(Deserialize)] - struct RpcErrorData { - code: i64, - message: String, - } - - let data = RpcErrorData::deserialize(deserializer)?; - - Ok(match data.code { - -32700 => RpcError::ParseError, - -32600 => RpcError::InvalidRequest, - -32601 => RpcError::MethodNotFound, - -32602 => RpcError::InvalidParams, - -32603 => RpcError::InternalError, - _ => RpcError::Custom(data.message), - }) + self.ok_or_else(|| RpcError::Custom(f().into())) + } +} + +impl ToRpcResult for Result +where + E: std::error::Error + Send + Sync + 'static, +{ + fn rpc_err(self) -> Result { + self.map_err(|e| RpcError::from(Box::new(e) as Box)) } } diff --git a/crates/wasmi-plugin-pdk/src/runner.rs b/crates/wasmi-plugin-pdk/src/runner.rs new file mode 100644 index 0000000..db11d34 --- /dev/null +++ b/crates/wasmi-plugin-pdk/src/runner.rs @@ -0,0 +1,127 @@ +use std::io::{BufRead, BufReader}; + +use serde::{Serialize, de::DeserializeOwned}; +use thiserror::Error; +use tokio::select; +use tracing::error; + +use crate::{ + router::{MaybeSend, Router}, + rpc_message::{RpcError, RpcErrorContext, RpcMessage, RpcRequest}, + transport::Transport, +}; + +/// A one-shot executor for plugins. Reads the host request from stdin, +/// dispatches it to the appropriate handler, and writes the response +/// to stdout. +pub struct PluginRunner { + router: Router, +} + +#[derive(Debug, Error)] +enum PluginServerError { + #[error("Invalid Message: {0:?}")] + InvalidMessage(RpcMessage), + + #[error("serde_json error: {0}")] + Serde(#[from] serde_json::Error), + + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error(transparent)] + Rpc(#[from] RpcError), + + #[error("Transport error: {0}")] + Transport(#[from] crate::transport::TransportError), +} + +#[allow(clippy::new_without_default)] +impl PluginRunner { + pub fn new() -> Self { + let router = Router::new(); + Self { router } + } + + /// Register a RPC method handler with the runner. The handler function + /// will be invoked if the request matches the given method name. + pub fn with_method(mut self, name: &str, func: F) -> Self + where + P: DeserializeOwned + 'static, + R: Serialize + 'static, + F: Fn(Transport, P) -> Fut + Send + Sync + 'static, + Fut: Future> + MaybeSend + 'static, + { + self.router = self.router.with_method(name, func); + self + } + + /// Run the plugin, blocking the current thread until the incoming + /// request has been handled. + pub fn run(self) { + match self.try_run() { + Ok(()) => {} + Err(e) => { + error!("PluginServer encountered an error: {:?}", e); + std::process::exit(1); + } + } + } + + fn try_run(&self) -> Result<(), PluginServerError> { + //? Read request + let request = self.read_request()?; + + let (transport, driver) = Transport::new(std::io::stdin(), std::io::stdout()); + + let rt = tokio::runtime::Builder::new_current_thread() + .enable_time() + .build() + .context("Error creating tokio runtime")?; + + let resp = rt.block_on(async { + let local = tokio::task::LocalSet::new(); + local.run_until(async { + select! { + res = self.router.handle_with_state(transport, &request.method, request.params) => res, + drive_err = driver.run() => { + match drive_err { + Ok(_) => Err(RpcError::custom("Transport driver exited unexpectedly")), + Err(e) => Err(e.into()), + } + } + } + }).await + }); + + //? Send response + let resp = match resp { + Ok(result) => RpcMessage::response(request.id, result), + Err(error) => RpcMessage::error_response(request.id, error), + }; + driver.write_message(&resp)?; + + Ok(()) + } + + fn read_request(&self) -> Result { + let mut reader = BufReader::new(std::io::stdin()); + let mut line = String::new(); + loop { + match reader.read_line(&mut line) { + Ok(_) if !line.is_empty() => break, + Ok(_) => continue, // EOF or empty read + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + continue; + } + Err(e) => return Err(e.into()), + } + } + + let msg: RpcMessage = serde_json::from_str(&line)?; + let RpcMessage::RpcRequest(msg) = msg else { + return Err(PluginServerError::InvalidMessage(msg)); + }; + Ok(msg) + } +} diff --git a/crates/wasmi-plugin-pdk/src/transport.rs b/crates/wasmi-plugin-pdk/src/transport.rs index fde6dce..4a3aa1a 100644 --- a/crates/wasmi-plugin-pdk/src/transport.rs +++ b/crates/wasmi-plugin-pdk/src/transport.rs @@ -1,282 +1,302 @@ use std::{ collections::HashMap, - io::{BufRead, Write}, - sync::atomic::AtomicU64, + io::{BufRead, BufReader, Read, Write}, + sync::{Arc, Mutex, atomic::AtomicU64}, }; -use async_trait::async_trait; -use futures::{ - channel::oneshot::{self, Sender}, - lock::Mutex, -}; +use futures::channel::oneshot; use serde_json::Value; -use tracing::{trace, warn}; +use thiserror::Error; +use tracing::{info, warn}; use wasmi_plugin_rt::yield_now; use crate::{ - api::{ApiError, RequestHandler}, - rpc_message::{RpcError, RpcErrorResponse, RpcMessage, RpcRequest, RpcResponse}, + poll_oneoff::wait_for_stdin, + router::MaybeSend, + rpc_message::{RpcError, RpcErrorResponse, RpcMessage, RpcResponse}, }; -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait Transport { - async fn call(&self, method: &str, params: Value) -> Result; +pub trait SyncTransport: Send + Sync + 'static { + fn call(&self, method: &str, params: Value) -> Result; } -/// Single-threaded, concurrent-safe json-rpc transport layer -pub struct JsonRpcTransport { - id: AtomicU64, - pending: Mutex>>, - reader: Mutex>, - writer: Mutex>, - handler: Option>>, +pub trait SyncManyTransport: Send + Sync + 'static { + fn call_many(&self, calls: Vec<(&str, Value)>) -> Result, E>; } -const JSON_RPC_VERSION: &str = "2.0"; +pub trait AsyncTransport: Send + Sync + 'static { + fn call_async( + &self, + method: &str, + params: Value, + ) -> impl std::future::Future> + MaybeSend; +} -impl JsonRpcTransport { - pub fn new( - reader: impl BufRead + Send + Sync + 'static, - writer: impl Write + Send + Sync + 'static, - ) -> Self { - Self { - id: AtomicU64::new(0), - pending: Mutex::new(HashMap::new()), - reader: Mutex::new(Box::new(reader)), - writer: Mutex::new(Box::new(writer)), - handler: None, - } +#[derive(Debug)] +pub struct Transport { + inner: Arc>, +} + +#[derive(Debug)] +pub struct TransportDriver { + inner: Arc>, +} + +#[derive(Debug)] +struct TransportInner { + reader: Mutex>, + writer: Mutex, + #[allow(clippy::type_complexity)] + pending: Mutex>>>, + next_id: AtomicU64, +} + +#[derive(Debug, Error)] +pub enum TransportError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("serde_json error: {0}")] + Serde(#[from] serde_json::Error), + + #[error("oneshot send error")] + OneshotSend, + + #[error("oneshot Canceled")] + OneshotCanceled(#[from] oneshot::Canceled), + + #[error("EOF")] + Eof, + + #[error(transparent)] + RpcError(#[from] RpcError), +} + +impl From for RpcError { + fn from(err: TransportError) -> Self { + RpcError::Custom(err.to_string()) } +} - pub fn with_handler( - reader: impl BufRead + Send + Sync + 'static, - writer: impl Write + Send + Sync + 'static, - handler: impl RequestHandler + 'static, - ) -> Self { - Self { - id: AtomicU64::new(0), - pending: Mutex::new(HashMap::new()), - reader: Mutex::new(Box::new(reader)), - writer: Mutex::new(Box::new(writer)), - handler: Some(Box::new(handler)), +impl Clone for Transport { + fn clone(&self) -> Self { + Transport { + inner: self.inner.clone(), } } } -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl Transport for JsonRpcTransport { - /// Sends a json-rpc request and waits for the response. `Call` does not - /// pump the reader, so you must call `process_next_line` in another task - /// to read responses / handle incoming requests. - async fn call(&self, method: &str, params: Value) -> Result { - let (tx, mut rx) = oneshot::channel(); - let id = self.id.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - self.pending.lock().await.insert(id, tx); - self.write_request(id, method, params).await?; - - //? Loop handles all incoming messages until we get a response for our call - loop { - match rx.try_recv() { - Ok(Some(msg)) => { - return Ok(msg); - } - Ok(None) => {} - Err(_) => { - warn!("Caller dropped"); - return Err(RpcError::InternalError); - } - } +impl Transport { + pub fn new(reader: R, writer: W) -> (Self, TransportDriver) { + let inner = Arc::new(TransportInner { + reader: Mutex::new(BufReader::new(reader)), + writer: Mutex::new(writer), + pending: Mutex::new(HashMap::new()), + next_id: AtomicU64::new(0), + }); - self.process_next_line(self.handler.as_deref()).await?; - } + ( + Transport { + inner: inner.clone(), + }, + TransportDriver { inner }, + ) } } -impl JsonRpcTransport { - /// Processes a single request from the reader. Can be used to manually - /// pump the reader with a custom handler. - pub async fn process_next_line( - &self, - handler: Option<&dyn RequestHandler>, - ) -> Result<(), RpcError> { - let line: String = self.next_line().await?; - let message = match serde_json::from_str::(line.trim()) { - Ok(msg) => msg, - Err(_) => { - warn!("Failed to parse line: {}", line.trim()); - return Err(RpcError::ParseError); - } - }; +impl SyncTransport for Transport { + fn call(&self, method: &str, params: Value) -> Result { + self.inner.call(method, params) + } +} - trace!("JsonRpcTransport::process_next_line() - {:?}", message); +impl SyncManyTransport for Transport { + fn call_many(&self, calls: Vec<(&str, Value)>) -> Result, TransportError> { + self.inner.call_many(calls) + } +} - self.process_message(message, handler).await +impl AsyncTransport for Transport { + async fn call_async(&self, method: &str, params: Value) -> Result { + self.inner.call_async(method, params).await } +} - async fn write_request(&self, id: u64, method: &str, params: Value) -> Result<(), RpcError> { - let msg = RpcMessage::RpcRequest(RpcRequest { - jsonrpc: JSON_RPC_VERSION.to_string(), - id, - method: method.to_string(), - params, - }); - self.write_message(&msg).await?; - Ok(()) +impl TransportDriver { + pub async fn run(&self) -> Result<(), TransportError> { + self.inner.run().await } - async fn write_message(&self, msg: &RpcMessage) -> Result<(), RpcError> { - let serialized = serde_json::to_string(msg).map_err(|_| { - warn!("Failed to serialize message: {:?}", msg); - RpcError::InternalError - })?; - let msg = format!("{}\n", serialized); - - trace!("JsonRpcTransport::write_message() - {}", msg.trim()); - let mut writer = self.writer.lock().await; - writer.write_all(msg.as_bytes()).map_err(|_| { - warn!("Failed to write message: {}", msg.trim()); - RpcError::InternalError - })?; - writer.flush().map_err(|_| { - warn!("Failed to flush message: {}", msg.trim()); - RpcError::InternalError - })?; - Ok(()) + pub fn write_message(&self, message: &RpcMessage) -> Result<(), TransportError> { + self.inner.write_message(message) } +} - async fn process_message( - &self, - message: RpcMessage, - handler: Option<&dyn RequestHandler>, - ) -> Result<(), RpcError> { - match message.clone() { - RpcMessage::RpcResponse(resp) => { - if let Some(tx) = self.pending.lock().await.remove(&resp.id) { - let _ = tx.send(resp); - } - } - RpcMessage::RpcErrorResponse(err) => { - warn!("Received error response: {:?}", err.error.message()); - return Err(err.error); - } - RpcMessage::RpcRequest(RpcRequest { - jsonrpc: _, - id, - method, - params, - }) => { - let handler = handler.ok_or(RpcError::Custom("Handler not found".into()))?; - match handler.handle(&method, params).await { - Ok(result) => { - let response = RpcMessage::RpcResponse(RpcResponse { - jsonrpc: JSON_RPC_VERSION.to_string(), - id, - result, - }); - self.write_message(&response).await?; - } - Err(error) => { - let response = RpcMessage::RpcErrorResponse(RpcErrorResponse { - jsonrpc: JSON_RPC_VERSION.to_string(), - id, - error, - }); - self.write_message(&response).await?; - } - } - } +impl TransportInner { + async fn run(&self) -> Result<(), TransportError> { + let mut line = String::new(); + + loop { + self.step(&mut self.reader.lock().unwrap(), &mut line)?; + yield_now().await; } + } - Ok(()) + /// Synchronously call a method, blocking until the response is received. + /// + /// This method takes over the current thread to drive IO until the response + /// is received. If responses to other requests (those made asynchronously) arrive + /// in the meantime, they will be processed and queued for delivery. + fn call(&self, method: &str, params: Value) -> Result { + let calls = vec![(method, params)]; + let mut results = self.call_many(calls)?; + Ok(results.remove(0)) } - async fn next_line(&self) -> Result { + fn call_many(&self, calls: Vec<(&str, Value)>) -> Result, TransportError> { + let mut res_rx_list: Vec> = calls + .into_iter() + .map(|(method, params)| { + let id = self.next_id(); + + let (res_tx, res_rx) = oneshot::channel(); + self.pending.lock().unwrap().insert(id, res_tx); + self.write_message(&RpcMessage::request(id, method.to_string(), params))?; + Ok(res_rx) + }) + .collect::>()?; + + //? Lock reader since we'll be driving IO blockingly + let mut results = Vec::new(); + let mut reader = self.reader.lock().unwrap(); let mut line = String::new(); - // Loop until we get a line, awaiting if we would block - //? Uses a loop here instead of an async reader since need this to - //? also work in the plugin wasi environment. There it'll receive the - //? `WouldBlock` error from the host's non-blocking pipe, but since - //? it's just getting that through stdio it can't be async. And using - //? async stdio won't work well because wasi. - loop { - match self.reader.lock().await.read_line(&mut line) { - Ok(0) => { - warn!("EOF reached while reading line"); - return Err(RpcError::InternalError); + // Drive IO until we get our response + while results.len() < res_rx_list.len() + results.len() { + let would_block = self.step(&mut reader, &mut line)?; + + // Poll and remove finished receivers + let mut err = None; + res_rx_list.retain_mut(|rx| match rx.try_recv() { + Ok(Some(res)) => { + results.push(res.map_err(|e| TransportError::RpcError(e.error))); + false // Remove from list } - Ok(_) => return Ok(line), - Err(e) => match e.kind() { - std::io::ErrorKind::WouldBlock => { - yield_now().await; - continue; - } - _ => { - warn!("Failed to read line: {}", e); - return Err(RpcError::InternalError); - } - }, + Ok(None) => true, // Keep in list + Err(e) => { + err = Some(TransportError::OneshotCanceled(e)); + false // Remove from list + } + }); + + if let Some(e) = err { + return Err(e); + } + if would_block && !res_rx_list.is_empty() { + wait_for_stdin(); + } + if res_rx_list.is_empty() { + break; } } + + let mut results: Vec<_> = results.into_iter().collect::>()?; + results.sort_by_key(|res| res.id); + Ok(results) } -} -#[cfg(test)] -mod test { - use super::*; - use futures::executor::block_on; - use std::{ - io::{BufReader, Cursor}, - sync::Arc, - }; - - struct MockWriter { - buffer: Arc>>, + /// Asynchronously call a method, returning a future that resolves + /// to the response. + /// + /// Multiple concurrent async calls can be made, and responses will be + /// matched to requests by ID. + async fn call_async(&self, method: &str, params: Value) -> Result { + let id = self.next_id(); + let request = RpcMessage::request(id, method.to_string(), params); + + let (recv_tx, recv_rx) = oneshot::channel(); + self.pending.lock().unwrap().insert(id, recv_tx); + self.write_message(&request)?; + + // Await the response + let res = recv_rx.await?; + let res = res.map_err(|e| TransportError::RpcError(e.error))?; + Ok(res) } - impl MockWriter { - fn new(buffer: Arc>>) -> Self { - Self { buffer } + /// Perform a single read, processing one incoming message if available. + /// Returns `Ok(true)` if no message was available (WouldBlock) or `Ok(false)` + /// if a message was processed. + fn step( + &self, + reader: &mut std::sync::MutexGuard<'_, BufReader>, + line: &mut String, + ) -> Result { + match reader.read_line(line) { + Ok(0) => return Err(TransportError::Eof), + Ok(_) => {} + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => return Ok(true), + Err(e) => return Err(TransportError::Io(e)), } + + let msg: RpcMessage = serde_json::from_str(line)?; + line.clear(); + + self.handle_message(msg)?; + Ok(false) } - impl Write for MockWriter { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - block_on(async { - self.buffer.lock().await.extend_from_slice(buf); - }); - Ok(buf.len()) + // TODO: Catch WouldBlock + pub fn write_message(&self, message: &RpcMessage) -> Result<(), TransportError> { + let message_str = serde_json::to_string(message)?; + let msg = format!("{}\n", message_str); + { + let mut writer = self.writer.lock().unwrap(); + writer.write_all(msg.as_bytes())?; + writer.flush()?; } - fn flush(&mut self) -> std::io::Result<()> { - Ok(()) + Ok(()) + } + + fn handle_message(&self, message: RpcMessage) -> Result<(), TransportError> { + match message { + RpcMessage::RpcResponse(res) => { + let sender = self.pending.lock().unwrap().remove(&res.id); + let Some(sender) = sender else { + warn!("Ignoring unmatched response ID: {}", res.id); + return Ok(()); + }; + + sender + .send(Ok(res)) + .map_err(|_| TransportError::OneshotSend)?; + } + RpcMessage::RpcErrorResponse(res) => { + let sender = self.pending.lock().unwrap().remove(&res.id); + let Some(sender) = sender else { + warn!("Ignoring unmatched response ID: {}", res.id); + return Ok(()); + }; + + sender + .send(Err(res)) + .map_err(|_| TransportError::OneshotSend)?; + } + RpcMessage::RpcRequest(req) => { + info!("Ignoring request message"); + self.write_message(&RpcMessage::error_response( + req.id, + RpcError::custom("Cannot handle requests"), + ))?; + } } + + Ok(()) } - #[tokio::test] - async fn test_response_received_and_pending_removed() { - let response = RpcResponse { - jsonrpc: "2.0".into(), - id: 42, - result: serde_json::json!({"success": true}), - }; - let response_json = serde_json::to_string(&response).unwrap(); - - let reader = BufReader::new(Cursor::new(format!("{}\n", response_json))); - let output_buffer = Arc::new(Mutex::new(Vec::new())); - let writer = MockWriter::new(output_buffer); - let transport = JsonRpcTransport::new(reader, writer); - - // Inject a fake pending request with the ID and a rx we control - let (tx, mut rx) = oneshot::channel(); - transport.pending.lock().await.insert(42, tx); - - // Process the next line containing our expected response - transport.process_next_line(None).await.unwrap(); - - assert!(!transport.pending.lock().await.contains_key(&42)); - assert_eq!(rx.try_recv().unwrap().unwrap(), response); + fn next_id(&self) -> u64 { + self.next_id + .fetch_add(1, std::sync::atomic::Ordering::Relaxed) } } diff --git a/crates/wasmi-plugin-rt/Cargo.toml b/crates/wasmi-plugin-rt/Cargo.toml index 7b76dd4..0d51dea 100644 --- a/crates/wasmi-plugin-rt/Cargo.toml +++ b/crates/wasmi-plugin-rt/Cargo.toml @@ -17,6 +17,7 @@ web-time = { workspace = true } [target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] gloo-timers = { workspace = true } +web-sys = { workspace = true } [target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] tokio = { workspace = true } diff --git a/crates/wasmi-plugin-rt/src/lib.rs b/crates/wasmi-plugin-rt/src/lib.rs index fd15115..ce4907f 100644 --- a/crates/wasmi-plugin-rt/src/lib.rs +++ b/crates/wasmi-plugin-rt/src/lib.rs @@ -1,11 +1,7 @@ -use std::time::Duration; -pub use web_time; - pub fn yield_now() -> impl std::future::Future { #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] { - use gloo_timers::future::TimeoutFuture; - TimeoutFuture::new(0) + gloo_timers::future::TimeoutFuture::new(0) } #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] @@ -14,11 +10,7 @@ pub fn yield_now() -> impl std::future::Future { } } -pub fn now() -> web_time::Instant { - web_time::Instant::now() -} - -pub async fn sleep(dur: Duration) { +pub async fn sleep(dur: web_time::Duration) { #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] { gloo_timers::future::TimeoutFuture::new(dur.as_millis() as u32).await; @@ -29,3 +21,19 @@ pub async fn sleep(dur: Duration) { tokio::time::sleep(dur).await; } } + +pub fn blocking_sleep(dur: web_time::Duration) { + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + { + let stn = web_sys::js_sys::SharedArrayBuffer::new(4); + let typed_array = web_sys::js_sys::Int32Array::new(&stn); + + let millis = dur.as_millis() as f64; + let _ = web_sys::js_sys::Atomics::wait_with_timeout(&typed_array, 0, 0, millis); + } + + #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] + { + std::thread::sleep(dur); + } +} diff --git a/crates/wasmi-plugin-wasi/Cargo.toml b/crates/wasmi-plugin-wasi/Cargo.toml new file mode 100644 index 0000000..bf8d767 --- /dev/null +++ b/crates/wasmi-plugin-wasi/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "wasmi-plugin-wasi" +version.workspace = true +rust-version.workspace = true +description = "Runtime utilities for wasmi-plugin-framework" +authors.workspace = true +repository.workspace = true +edition.workspace = true +readme.workspace = true +license.workspace = true +keywords.workspace = true +categories.workspace = true +exclude.workspace = true + +[dependencies] +futures = { workspace = true } +getrandom = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } +wasmi-plugin-rt = { workspace = true } +web-time = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +wasmer = { workspace = true, default-features = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasmer = { workspace = true, features = ["js-default", "js-serializable-module"] } diff --git a/crates/wasmi-plugin-hdk/src/wasi/mod.rs b/crates/wasmi-plugin-wasi/src/lib.rs similarity index 75% rename from crates/wasmi-plugin-hdk/src/wasi/mod.rs rename to crates/wasmi-plugin-wasi/src/lib.rs index b981651..aefd07f 100644 --- a/crates/wasmi-plugin-hdk/src/wasi/mod.rs +++ b/crates/wasmi-plugin-wasi/src/lib.rs @@ -1,3 +1,2 @@ pub mod non_blocking_pipe; pub mod wasi_ctx; -pub mod wasmi; diff --git a/crates/wasmi-plugin-hdk/src/wasi/non_blocking_pipe.rs b/crates/wasmi-plugin-wasi/src/non_blocking_pipe.rs similarity index 52% rename from crates/wasmi-plugin-hdk/src/wasi/non_blocking_pipe.rs rename to crates/wasmi-plugin-wasi/src/non_blocking_pipe.rs index 21023e9..50240ec 100644 --- a/crates/wasmi-plugin-hdk/src/wasi/non_blocking_pipe.rs +++ b/crates/wasmi-plugin-wasi/src/non_blocking_pipe.rs @@ -10,8 +10,9 @@ use std::pin::Pin; use std::sync::{Arc, Mutex}; use std::task::{Context, Poll, Waker}; -use futures::AsyncRead; -use tracing::info; +use futures::{AsyncRead, AsyncWrite}; + +use crate::wasi_ctx::WasiReader; struct Inner { buf: VecDeque, @@ -46,57 +47,56 @@ pub fn non_blocking_pipe() -> (NonBlockingPipeReader, NonBlockingPipeWriter) { impl Read for NonBlockingPipeReader { fn read(&mut self, buf: &mut [u8]) -> io::Result { let mut inner = self.inner.lock().unwrap(); - let mut n = 0; - while n < buf.len() { - if let Some(b) = inner.buf.pop_front() { - buf[n] = b; - n += 1; - } else { - break; + + let (front, back) = inner.buf.as_slices(); + let to_read = buf.len().min(front.len() + back.len()); + + if to_read > 0 { + let from_front = to_read.min(front.len()); + buf[..from_front].copy_from_slice(&front[..from_front]); + + if from_front < to_read { + let from_back = to_read - from_front; + buf[from_front..to_read].copy_from_slice(&back[..from_back]); } - } - if n == 0 && inner.closed { + + inner.buf.drain(..to_read); + Ok(to_read) + } else if inner.closed { Ok(0) - } else if n == 0 { - // no data, behave like a nonblocking pipe - Err(io::ErrorKind::WouldBlock.into()) } else { - Ok(n) + Err(io::ErrorKind::WouldBlock.into()) } } } +impl WasiReader for NonBlockingPipeReader { + fn is_ready(&self) -> bool { + let inner = self.inner.lock().unwrap(); + !inner.buf.is_empty() || inner.closed + } + + fn wait_ready(&self, _timeout: Option) { + // TODO: Make native-compatible version of this? + // No-op + } +} + impl AsyncRead for NonBlockingPipeReader { fn poll_read( - self: Pin<&mut Self>, + mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8], ) -> Poll> { - let mut inner = self.inner.lock().unwrap(); - - let mut bytes_read = 0; - - // Read available data - while bytes_read < buf.len() { - if let Some(b) = inner.buf.pop_front() { - buf[bytes_read] = b; - bytes_read += 1; - } else { - break; + let read = self.as_mut().get_mut().read(buf); + match read { + Ok(n) => Poll::Ready(Ok(n)), + Err(e) if e.kind() == io::ErrorKind::WouldBlock => { + let mut inner = self.inner.lock().unwrap(); + inner.waker = Some(cx.waker().clone()); + Poll::Pending } - } - - if bytes_read > 0 { - inner.waker = None; // Clear stale waker - Poll::Ready(Ok(bytes_read)) - } else if inner.closed { - // EOF - inner.waker = None; - Poll::Ready(Ok(0)) - } else { - // No data available, register waker and return pending - inner.waker = Some(cx.waker().clone()); - Poll::Pending + Err(e) => Poll::Ready(Err(e)), } } } @@ -116,12 +116,36 @@ impl Write for NonBlockingPipeWriter { } } +impl AsyncWrite for NonBlockingPipeWriter { + fn poll_write( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + let mut inner = self.inner.lock().unwrap(); + if inner.closed { + return Poll::Ready(Err(io::ErrorKind::BrokenPipe.into())); + } + inner.buf.extend(buf); + if let Some(waker) = inner.waker.take() { + waker.wake(); + } + Poll::Ready(Ok(buf.len())) + } + + fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} + impl Drop for NonBlockingPipeWriter { fn drop(&mut self) { let mut inner = self.inner.lock().unwrap(); - info!("Pipe writer dropped, closing pipe"); inner.closed = true; - inner.waker = None; if let Some(waker) = inner.waker.take() { waker.wake(); } diff --git a/crates/wasmi-plugin-wasi/src/wasi_ctx.rs b/crates/wasmi-plugin-wasi/src/wasi_ctx.rs new file mode 100644 index 0000000..d1ffafc --- /dev/null +++ b/crates/wasmi-plugin-wasi/src/wasi_ctx.rs @@ -0,0 +1,792 @@ +use std::io::{BufReader, Read, Write}; +use thiserror::Error; +use tracing::{error, info, trace, warn}; +use wasmer::{FunctionEnvMut, RuntimeError}; +use wasmi_plugin_rt::blocking_sleep; +use web_time::{Duration, SystemTime}; + +pub trait WasiReader: Read + Send + Sync { + /// Blocks the current thread until the reader is ready to read, or the timeout is reached. + fn wait_ready(&self, timeout: Option); + + /// Returns true if the reader is ready to read without blocking. + fn is_ready(&self) -> bool; +} + +/// A WASI context that can be attached to a wasmi instance. Attaches +/// a subset of WASI syscalls to the instance, allowing it to +/// get args, env vars, read/write to stdin/stdout/stderr, get time, and +/// get random bytes. +/// +/// Intentionally excludes filesystem (except for stdin/stdout/stderr) and network +/// access for improved security and compatibility. +/// +/// https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md +pub struct WasiCtx { + args: Vec, + env: Vec, + stdin_reader: Option>>, + stdout_writer: Option>, + stderr_writer: Option>, + + memory: Option, +} + +#[derive(Debug, Error)] +pub enum WasiError { + #[error("Instantiation Error: {0}")] + InstantiationError(#[from] wasmer::InstantiationError), + #[error("Export Error: {0}")] + ExportError(#[from] wasmer::ExportError), +} + +impl Default for WasiCtx { + fn default() -> Self { + Self::new() + } +} + +impl WasiCtx { + pub fn new() -> Self { + Self { + args: vec![], + env: vec![], + stdin_reader: None, + stdout_writer: None, + stderr_writer: None, + memory: None, + } + } + + #[allow(dead_code)] + pub fn add_arg(mut self, arg: &str) -> Self { + self.args.push(arg.to_string()); + self + } + + #[allow(dead_code)] + pub fn add_env(mut self, key: &str, value: &str) -> Self { + self.env.push(format!("{}={}", key, value)); + self + } + + pub fn set_stdin(mut self, reader: R) -> Self { + let reader = std::io::BufReader::new(reader); + self.stdin_reader = Some(Box::new(reader)); + self + } + + pub fn set_stdout(mut self, writer: W) -> Self { + self.stdout_writer = Some(Box::new(writer)); + self + } + + pub fn set_stderr(mut self, writer: W) -> Self { + self.stderr_writer = Some(Box::new(writer)); + self + } + + #[allow(clippy::result_large_err)] + pub fn into_fn( + self, + mut store: &mut wasmer::Store, + module: &wasmer::Module, + ) -> Result { + let (ctx, imports) = self.get_imports(store); + + let instance = wasmer::Instance::new(&mut store, module, &imports)?; + let memory = instance.exports.get_memory("memory")?; + ctx.as_mut(&mut store).set_memory(memory.clone()); + + let start_fn = instance.exports.get_function("_start")?; + + Ok(start_fn.clone()) + } + + pub fn get_imports( + self, + mut store: &mut wasmer::Store, + ) -> (wasmer::FunctionEnv, wasmer::Imports) { + let ctx = wasmer::FunctionEnv::new(&mut store, self); + let imports = wasmer::imports! { + "wasi_snapshot_preview1" => { + "args_get" => wasmer::Function::new_typed_with_env(&mut store, &ctx, args_get), + "args_sizes_get" => wasmer::Function::new_typed_with_env(&mut store, &ctx, args_sizes_get), + "environ_get" => wasmer::Function::new_typed_with_env(&mut store, &ctx, env_get), + "environ_sizes_get" => wasmer::Function::new_typed_with_env(&mut store, &ctx, env_sizes_get), + "fd_read" => wasmer::Function::new_typed_with_env(&mut store, &ctx, fd_read), + "fd_write" => wasmer::Function::new_typed_with_env(&mut store, &ctx, fd_write), + "fd_fdstat_get" => wasmer::Function::new_typed_with_env(&mut store, &ctx, fd_fdstat_get), + "fd_close" => wasmer::Function::new_typed_with_env(&mut store, &ctx, fd_close), + "clock_time_get" => wasmer::Function::new_typed_with_env(&mut store, &ctx, clock_time_get), + "random_get" => wasmer::Function::new_typed_with_env(&mut store, &ctx, random_get), + "poll_oneoff" => wasmer::Function::new_typed_with_env(&mut store, &ctx, poll_oneoff), + "sched_yield" => wasmer::Function::new_typed_with_env(&mut store, &ctx, sched_yield), + "proc_raise" => wasmer::Function::new_typed_with_env(&mut store, &ctx, proc_raise), + "proc_exit" => wasmer::Function::new_typed_with_env(&mut store, &ctx, proc_exit), + } + }; + + (ctx, imports) + } + + pub fn set_memory(&mut self, memory: wasmer::Memory) { + self.memory = Some(memory); + } +} + +#[repr(i32)] +#[derive(Debug, Clone, Copy)] +#[allow(dead_code)] +enum Errno { + Success = 0, + Access = 2, + Again = 6, + Badf = 8, + Fault = 21, + Inval = 28, + Io = 29, +} + +pub fn args_get(mut env: FunctionEnvMut, argv: i32, argv_buf: i32) -> i32 { + trace!("wasi args_get({}, {})", argv, argv_buf); + + let (ctx, store) = env.data_and_store_mut(); + let memory = ctx.memory.as_ref().expect("Memory not initialized"); + let view = memory.view(&store); + + // Pointer in guest memory to an array of pointers that will be filled with the start of each argument string. + let mut argv_ptr = argv as u64; + // Pointer in guest memory to a buffer that will be filled with the `\0`-terminated argument strings. + let mut buf_ptr = argv_buf as u64; + + for arg in &ctx.args { + if write_pointer_and_string(&view, &mut argv_ptr, &mut buf_ptr, arg).is_err() { + return Errno::Fault as i32; + } + } + + Errno::Success as i32 +} + +pub fn args_sizes_get(mut env: FunctionEnvMut, offset0: i32, offset1: i32) -> i32 { + trace!("wasi args_sizes_get({}, {})", offset0, offset1); + + let (ctx, store) = env.data_and_store_mut(); + let memory = ctx.memory.as_ref().expect("Memory not initialized"); + let view = memory.view(&store); + + let argc = ctx.args.len() as u64; + let argv_buf_len: u64 = ctx.args.iter().map(|s| s.len() as u64 + 1).sum(); // +1 for null terminator + + if view.write(offset0 as u64, &argc.to_le_bytes()).is_err() { + return Errno::Fault as i32; + } + if view + .write(offset1 as u64, &argv_buf_len.to_le_bytes()) + .is_err() + { + return Errno::Fault as i32; + } + + Errno::Success as i32 +} + +/// Read environment variable data. The sizes of the buffers should match that +/// returned by environ_sizes_get. Key/value pairs are expected to be joined +/// with =s, and terminated with \0s. +pub fn env_get(mut env: FunctionEnvMut, environ: i32, environ_buf: i32) -> i32 { + trace!("wasi environ_get({}, {})", environ, environ_buf); + + let (ctx, store) = env.data_and_store_mut(); + let memory = ctx.memory.as_ref().expect("Memory not initialized"); + let view = memory.view(&store); + + // Pointer in guest memory to an array of pointers that will be filled with the start of each environment string. + let mut environ_ptr = environ as u64; + // Pointer in guest memory to a buffer that will be filled with the `\0`-terminated environment strings. + let mut buf_ptr = environ_buf as u64; + + for var in &ctx.env { + if write_pointer_and_string(&view, &mut environ_ptr, &mut buf_ptr, var).is_err() { + return Errno::Fault as i32; + } + } + + Errno::Success as i32 +} + +/// Returns the number of environment variable arguments and the size of the environment variable data. +pub fn env_sizes_get(mut env: FunctionEnvMut, offset0: i32, offset1: i32) -> i32 { + trace!("wasi environ_sizes_get({}, {})", offset0, offset1); + + let (ctx, store) = env.data_and_store_mut(); + let memory = ctx.memory.as_ref().expect("Memory not initialized"); + let view = memory.view(&store); + + let envc = ctx.env.len() as u64; + let env_buf_len: u64 = ctx.env.iter().map(|s| s.len() as u64 + 1).sum(); // +1 for null terminator + + if view.write(offset0 as u64, &envc.to_le_bytes()).is_err() { + return Errno::Fault as i32; + } + if view + .write(offset1 as u64, &env_buf_len.to_le_bytes()) + .is_err() + { + return Errno::Fault as i32; + } + + Errno::Success as i32 +} + +pub fn fd_read( + mut env: FunctionEnvMut, + fd: u32, + iov_ptr: i32, + iov_len: i32, + nread_ptr: i32, +) -> i32 { + trace!( + "wasi fd_read({}, {}, {}, {})", + fd, iov_ptr, iov_len, nread_ptr + ); + + if fd != 0 { + return Errno::Badf as i32; + } + + let (ctx, store) = env.data_and_store_mut(); + let memory = ctx.memory.as_ref().expect("Memory not initialized"); + let view = memory.view(&store); + + let mut total_read = 0u64; + + for i in 0..iov_len { + let base = (iov_ptr as u64) + (i as u64 * 8); + let mut buf_bytes = [0u8; 4]; + + view.read(base, &mut buf_bytes).unwrap(); + let buf_addr = u32::from_le_bytes(buf_bytes) as u64; + + view.read(base + 4, &mut buf_bytes).unwrap(); + let buf_len = u32::from_le_bytes(buf_bytes) as usize; + + // Allocate host buffer + let mut host_buf = vec![0u8; buf_len]; + + // Narrow scope: borrow reader only while calling `read` + let n = { + match ctx.stdin_reader.as_mut() { + Some(r) => match r.read(&mut host_buf) { + Ok(n) => n, + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + return Errno::Again as i32; + } + Err(_) => return Errno::Fault as i32, + }, + None => return Errno::Badf as i32, + } + }; // <- borrow of ctx ends here + + if n == 0 { + trace!("fd_read: EOF"); + break; + } + + total_read += n as u64; + view.write(buf_addr, &host_buf[..n]).unwrap(); + if n < buf_len { + break; // short read + } + } + + view.write(nread_ptr as u64, &(total_read).to_le_bytes()) + .unwrap(); + + trace!("fd_read: total_read={}", total_read); + Errno::Success as i32 +} + +pub fn fd_write( + mut env: FunctionEnvMut, + fd: i32, + ciov_ptr: i32, + ciov_len: i32, + nwrite_ptr: i32, +) -> i32 { + trace!( + "wasi fd_write({}, {}, {}, {})", + fd, ciov_ptr, ciov_len, nwrite_ptr + ); + + let (ctx, store) = env.data_and_store_mut(); + let memory = ctx.memory.as_ref().expect("Memory not initialized"); + let view = memory.view(&store); + + let mut total_written = 0u32; + + let writer = match fd { + 1 => ctx.stdout_writer.as_mut(), + 2 => ctx.stderr_writer.as_mut(), + _ => return Errno::Badf as i32, + }; + + let Some(writer) = writer else { + return Errno::Badf as i32; + }; + + let mut scratch = [0u8; 8192]; + for i in 0..ciov_len { + let base = (ciov_ptr as u64) + (i as u64 * 8); + + // Read the ciovec (ptr: u32, len: u32) + let mut cio_buf = [0u8; 8]; + if view.read(base, &mut cio_buf).is_err() { + return Errno::Fault as i32; + } + + let buf_addr = u32::from_le_bytes(cio_buf[0..4].try_into().unwrap()) as u64; + let buf_len = u32::from_le_bytes(cio_buf[4..8].try_into().unwrap()) as usize; + + if buf_len == 0 { + continue; + } + + // Process iovecs in chunks with the scratch buffer + let mut written = 0; + while written < buf_len { + let to_copy = (buf_len - written).min(scratch.len()); + if view + .read(buf_addr + written as u64, &mut scratch[..to_copy]) + .is_err() + { + return Errno::Fault as i32; + } + + // Print the data to be copied as a string + match writer.write(&scratch[..to_copy]) { + Ok(0) => { + break; + } + Ok(n) => { + written += n; + total_written += n as u32; + if n < to_copy { + break; // partial write, stop + } + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + // Spin until writable + continue; + } + Err(_) => { + return Errno::Io as i32; + } + } + } + + //? If we couldn't write the full iovec, stop processing further iovecs + if written < buf_len { + break; + } + } + + // Write total_written into nwrite_ptr + if view + .write(nwrite_ptr as u64, &total_written.to_le_bytes()) + .is_err() + { + return Errno::Fault as i32; + } + + Errno::Success as i32 +} + +pub fn fd_fdstat_get(mut env: FunctionEnvMut, fd: i32, buf_ptr: i32) -> i32 { + trace!("wasi fd_fdstat_get({}, {})", fd, buf_ptr); + + let (ctx, store) = env.data_and_store_mut(); + let memory = ctx.memory.as_ref().expect("Memory not initialized"); + let view = memory.view(&store); + + // Constants from wasi_snapshot_preview1 + const FILETYPE_CHARACTER_DEVICE: u8 = 2; + + // fdflags (none set) + let fs_flags: u16 = 0; + + // rights (for simplicity, we just allow read or write) + const RIGHTS_FD_READ: u64 = 1 << 1; + const RIGHTS_FD_WRITE: u64 = 1 << 6; + + let (filetype, rights_base) = match fd { + 0 => (FILETYPE_CHARACTER_DEVICE, RIGHTS_FD_READ), + 1 | 2 => (FILETYPE_CHARACTER_DEVICE, RIGHTS_FD_WRITE), + _ => return Errno::Badf as i32, + }; + + let rights_inheriting: u64 = 0; + + let mut buf = [0u8; 24]; + buf[0] = filetype; + buf[1..3].copy_from_slice(&fs_flags.to_le_bytes()); + buf[8..16].copy_from_slice(&rights_base.to_le_bytes()); + buf[16..24].copy_from_slice(&rights_inheriting.to_le_bytes()); + + if view.write(buf_ptr as u64, &buf).is_err() { + return Errno::Fault as i32; + } + + Errno::Success as i32 +} + +/// Close a file descriptor. For now we only support closing stdin, stdout, stderr. +pub fn fd_close(mut env: FunctionEnvMut, fd: i32) -> i32 { + trace!("wasi fd_close({})", fd); + + let ctx = env.data_mut(); + + match fd { + 0 => ctx.stdin_reader = None, + 1 => ctx.stdout_writer = None, + 2 => ctx.stderr_writer = None, + _ => return Errno::Badf as i32, + } + + Errno::Success as i32 +} + +pub fn clock_time_get( + mut env: FunctionEnvMut, + clock_id: i32, + _precision: i64, + result_ptr: i32, +) -> i32 { + trace!("wasi clock_time_get({}, _, {})", clock_id, result_ptr); + + let (ctx, store) = env.data_and_store_mut(); + let memory = ctx.memory.as_ref().expect("Memory not initialized"); + let view = memory.view(&store); + + let now = match clock_id { + // Realtime: nanoseconds since UNIX epoch + 0 | 1 => { + match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { + Ok(dur) => dur.as_nanos() as u64, + Err(_) => return Errno::Inval as i32, // time before epoch shouldn't happen + } + } + _ => { + warn!("unsupported clock_id {}", clock_id); + return Errno::Inval as i32; // unsupported clock + } + }; + + if view.write(result_ptr as u64, &now.to_le_bytes()).is_err() { + return Errno::Fault as i32; + } + Errno::Success as i32 +} + +pub fn random_get(mut env: FunctionEnvMut, buf_ptr: i32, buf_len: i32) -> i32 { + trace!("wasi random_get({}, {})", buf_ptr, buf_len); + + let (ctx, store) = env.data_and_store_mut(); + let memory = ctx.memory.as_ref().expect("Memory not initialized"); + let view = memory.view(&store); + + let mut buf = vec![0u8; buf_len as usize]; + if let Err(e) = getrandom::fill(&mut buf) { + eprintln!("random_get failed: {:?}", e); + return Errno::Io as i32; + } + + if view.write(buf_ptr as u64, &buf).is_err() { + return Errno::Fault as i32; + } + + Errno::Success as i32 +} + +const CLOCK_EVENT_TYPE: u8 = 0; +const FD_READ_EVENT_TYPE: u8 = 1; +#[allow(dead_code)] +const FD_WRITE_EVENT_TYPE: u8 = 2; + +#[repr(C, packed)] +#[derive(Copy, Clone)] +struct WasiSubscription { + userdata: u64, + type_: u8, + _padding: [u8; 7], + union: WasiSubscriptionUnion, +} + +#[repr(C, packed)] +#[derive(Copy, Clone)] +union WasiSubscriptionUnion { + clock: WasiSubscriptionClock, + fd_readwrite: WasiSubscriptionFdReadWrite, +} + +#[repr(C, packed)] +#[derive(Copy, Clone)] +struct WasiSubscriptionClock { + id: u32, + _padding: [u8; 4], + timeout: u64, + precision: u64, + flags: u16, +} + +#[repr(C, packed)] +#[derive(Copy, Clone)] +struct WasiSubscriptionFdReadWrite { + fd: u32, +} + +#[repr(C, packed)] +struct WasiEvent { + userdata: u64, + error: u16, + type_: u8, + _padding: [u8; 5], + fd_readwrite: WasiEventFdReadWrite, +} + +#[repr(C, packed)] +struct WasiEventFdReadWrite { + nbytes: u64, + flags: u16, + _padding: [u8; 6], +} + +pub fn poll_oneoff( + mut env: FunctionEnvMut, + subs_ptr: i32, + events_ptr: i32, + nsubscriptions: i32, + result_ptr: i32, +) -> i32 { + trace!( + "wasi poll_oneoff({}, {}, {}, {})", + subs_ptr, events_ptr, nsubscriptions, result_ptr + ); + + if nsubscriptions == 0 { + error!("poll_oneoff called with zero subscriptions"); + return Errno::Inval as i32; + } + + let (ctx, store) = env.data_and_store_mut(); + let memory = ctx.memory.as_ref().expect("Memory not initialized"); + let view = memory.view(&store); + + //? Currently only supports a single clock / stdin subscription, and doesn't + //? support stdout. Takes the earliest clock and the last stdin subscription. + + // TODO: Add stdout support (via similar wait_ready mechanism as stdin) + // TODO: Support multiple file descriptors concurrently. Perhaps change + // the wait_* mechanism to async and select across multiple futures? + + let mut stdin_sub: Option = None; + let mut stdin_userdata: u64 = 0; + + // Time at which earliest clock sub will resolve + let mut earliest_clock_sub_time: Option = None; + let mut earliest_clock_sub: Option = None; + let mut clock_userdata: u64 = 0; + for i in 0..nsubscriptions { + // Each subscription is 48 bytes + let sub_offset = (subs_ptr as usize) + (i as usize * 48); + let mut sub_bytes = [0u8; 48]; + view.read(sub_offset as u64, &mut sub_bytes).unwrap(); + + // Cast bytes to struct + let sub = unsafe { &*(sub_bytes.as_ptr() as *const WasiSubscription) }; + + match sub.type_ { + CLOCK_EVENT_TYPE => { + let clock = unsafe { sub.union.clock }; + let is_absolute = (clock.flags & 1) != 0; + + //? Convert to absolute time if required + let timeout_ns = if is_absolute { + clock.timeout + } else { + match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { + Ok(dur) => dur.as_nanos() as u64 + clock.timeout, + Err(_) => return Errno::Inval as i32, // time before epoch shouldn't happen + } + }; + + //? Keep the earliest clock subscription + if earliest_clock_sub_time.is_none() + || timeout_ns < earliest_clock_sub_time.unwrap() + { + earliest_clock_sub_time = Some(timeout_ns); + earliest_clock_sub = Some(clock); + clock_userdata = sub.userdata; + } + } + FD_READ_EVENT_TYPE => { + let fd_read = unsafe { sub.union.fd_readwrite }; + + // Only support stdin (fd 0) for now + if fd_read.fd != 0 { + warn!("poll_oneoff: only fd_read on stdin (fd 0) is supported"); + continue; + } + stdin_sub = Some(fd_read); + stdin_userdata = sub.userdata; + } + _ => { + warn!("poll_oneoff: unsupported subscription type {}", sub.type_); + continue; + } + } + } + + //? If no subscriptions were found, return error + if stdin_sub.is_none() && earliest_clock_sub.is_none() { + warn!("poll_oneoff: no valid subscriptions found"); + return Errno::Inval as i32; + } + + let clock_duration = earliest_clock_sub_time.map(|deadline| { + let now_ns = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_nanos() as u64; + if deadline > now_ns { + Duration::from_nanos(deadline - now_ns) + } else { + Duration::from_nanos(0) + } + }); + + //? If there's a stdin subscription, wait on that with the clock as timeout. Otherwise just wait on the clock. + if stdin_sub.is_some() + && let Some(reader) = &ctx.stdin_reader + { + trace!("poll_oneoff: waiting for stdin or clock timeout..."); + let reader = reader.get_ref(); + reader.wait_ready(clock_duration); + } else if let Some(duration) = clock_duration { + trace!("poll_oneoff: waiting for clock timeout..."); + blocking_sleep(duration); + } + + let mut events_written = 0; + + // 1. Check if Stdin is ready (Data available or Pipe Closed) + if let Some(_sub) = stdin_sub + && let Some(reader) = &ctx.stdin_reader + && reader.get_ref().is_ready() + { + write_event( + &view, + events_ptr, + events_written, + stdin_userdata, + FD_READ_EVENT_TYPE, + ); + events_written += 1; + } + + // 2. Check if Clock has expired + if let Some(deadline_ns) = earliest_clock_sub_time { + let now_ns = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_nanos() as u64; + + if now_ns >= deadline_ns { + write_event( + &view, + events_ptr, + events_written, + clock_userdata, + CLOCK_EVENT_TYPE, + ); + events_written += 1; + } + } + + // Write number of events triggered to result_ptr + view.write(result_ptr as u64, &events_written.to_le_bytes()) + .unwrap(); + + Errno::Success as i32 +} + +pub fn sched_yield(mut _env: FunctionEnvMut) -> i32 { + trace!("wasi sched_yield()"); + Errno::Success as i32 +} + +pub fn proc_raise(_: FunctionEnvMut, sig: i32) -> Result<(), RuntimeError> { + info!("wasi proc_raise({})", sig); + + // https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md#signal + match sig { + 0 | 13 | 16 | 17 | 22 | 27 => { + // Ignored signals + Ok(()) + } + _ => { + // Termination signals + Err(RuntimeError::new(format!("exit: {}", sig + 128))) + } + } +} + +pub fn proc_exit(_: FunctionEnvMut, status: i32) -> Result<(), RuntimeError> { + info!("wasi proc_exit({})", status); + Err(RuntimeError::new(format!("exit: {}", status))) +} + +fn write_event(view: &wasmer::MemoryView, events_ptr: i32, index: u32, userdata: u64, type_: u8) { + let event = WasiEvent { + userdata, + error: 0, // Errno::Success + type_, + _padding: [0; 5], + fd_readwrite: WasiEventFdReadWrite { + nbytes: 0, + flags: 0, + _padding: [0; 6], + }, + }; + + let offset = (events_ptr as u64) + (index as u64 * 32); + let bytes: [u8; 32] = unsafe { std::mem::transmute(event) }; + view.write(offset, &bytes).unwrap(); + + trace!( + "poll_oneoff: Wrote event type {} for userdata {}", + type_, userdata + ); +} + +fn write_pointer_and_string( + view: &wasmer::MemoryView, + table_ptr: &mut u64, + buf_ptr: &mut u64, + s: &str, +) -> Result<(), Errno> { + // 1. Write pointer into table + let ptr_le = (*buf_ptr as u32).to_le_bytes(); + view.write(*table_ptr, &ptr_le).map_err(|_| Errno::Fault)?; + *table_ptr += 4; + + // 2. Write string+null into buffer + let mut tmp = Vec::with_capacity(s.len() + 1); + tmp.extend_from_slice(s.as_bytes()); + tmp.push(0); // null terminator + view.write(*buf_ptr, &tmp).map_err(|_| Errno::Fault)?; + *buf_ptr += tmp.len() as u64; + + Ok(()) +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..67e77eb --- /dev/null +++ b/flake.lock @@ -0,0 +1,99 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1779102034, + "narHash": "sha256-vZJZjLo513IeI8hjzHFc6TDezUd4uCE2Eq4SNO3DNNg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "687f05a9184cad4eaf905c48b63649e3a86f5433", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay", + "unstable": "unstable" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1779333539, + "narHash": "sha256-lpmN2lrBDZDPjov2cbD3bOOJsI0fkKolKXasYPCqSys=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "672fa5fc5608d5cd82286a6f69aaf84a40b4fe41", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "unstable": { + "locked": { + "lastModified": 1778869304, + "narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d233902339c02a9c334e7e593de68855ad26c4cb", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..288d72f --- /dev/null +++ b/flake.nix @@ -0,0 +1,70 @@ +{ + description = "Tlock dev shell"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; + unstable.url = "github:NixOS/nixpkgs/nixos-unstable"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { + self, + nixpkgs, + unstable, + rust-overlay, + flake-utils, + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ rust-overlay.overlays.default ]; + }; + + unstablePkgs = import unstable { + inherit system; + }; + + rustToolchain = pkgs.rust-bin.nightly.latest.default.override { + extensions = [ + "rust-src" + "llvm-tools" + "rust-analyzer" + ]; + targets = [ + "wasm32-wasip1" + "wasm32-unknown-unknown" + ]; + }; + + rustfmtNightly = pkgs.rust-bin.nightly.latest.rustfmt; + in + { + devShells = { + default = pkgs.mkShell { + packages = [ + rustToolchain + pkgs.cargo-sort + pkgs.cargo-machete + pkgs.cargo-udeps + pkgs.samply + pkgs.binaryen + pkgs.wasm-pack + pkgs.geckodriver + unstablePkgs.wasm-bindgen-cli_0_2_118 + pkgs.wasm-tools + pkgs.wabt + ]; + + env.WASM_BINDGEN = "${unstablePkgs.wasm-bindgen-cli_0_2_121}/bin/wasm-bindgen"; + }; + }; + } + ); +} diff --git a/makefile b/makefile new file mode 100644 index 0000000..2a5b882 --- /dev/null +++ b/makefile @@ -0,0 +1,44 @@ +# === Configuration === +BROWSER := --firefox + +.PHONY: help build-plugin build-all bench-wasm + +build-plugin: + @echo "--- Building Plugin WASM ---" + cargo build --target wasm32-wasip1 -p test-plugin --release + wasm-opt -O3 --debuginfo \ + target/wasm32-wasip1/release/test-plugin.wasm \ + -o target/wasm32-wasip1/release/test-plugin.wasm + +build-all: build-plugin + +clippy-native: build-all + @echo "--- Running Clippy Lints ---" + cargo clippy -- -D warnings + +clippy-wasm: build-all + @echo "--- Running Clippy Lints for Wasm ---" + cargo clippy \ + -p wasmi-plugin-hdk \ + --target wasm32-unknown-unknown \ + -- -D warnings + +test-native: build-plugin + @echo "--- Running Plugin Tests ---" + cargo test + +bench-native: build-plugin + @echo "--- Running Plugin Benchmarks ---" + cargo bench + +test-wasm: build-all + @echo "--- Running Wasm Tests ---" + cargo test \ + -p wasmi-plugin-hdk \ + --target wasm32-unknown-unknown + +bench-wasm: build-all + @echo "--- Running Wasm Benchmarks ---" + cargo bench \ + -p wasmi-plugin-hdk \ + --target wasm32-unknown-unknown \ No newline at end of file diff --git a/shell.nix b/shell.nix deleted file mode 100644 index 74d32b0..0000000 --- a/shell.nix +++ /dev/null @@ -1,28 +0,0 @@ -let - pkgs = import { - overlays = [ - (import (builtins.fetchTarball "https://github.com/oxalica/rust-overlay/archive/master.tar.gz")) - ]; - }; - unstable = import { }; - wasm-bindgen-cli_0_2_106 = pkgs.callPackage ./flakes/wasm-bindgen-cli.nix { }; -in -pkgs.mkShell { - packages = with pkgs; [ - # Rust toolchain with WASI target - (rust-bin.stable.latest.default.override { - extensions = [ "rust-src" ]; - targets = [ - "wasm32-wasip1" - "wasm32-unknown-unknown" - ]; - }) - - cargo - cargo-sort - cargo-machete - samply - rustfmt - rust-bin.stable.latest.rust-analyzer - ]; -}