diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c4ac924..813e6471 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,9 +4,11 @@ on: pull_request env: CARGO_TERM_COLOR: always + RUSTFLAGS: -Dwarnings jobs: build: + name: Build and test runs-on: ${{ matrix.os }} strategy: matrix: @@ -31,15 +33,20 @@ jobs: os: ubuntu-latest target: s390x-unknown-linux-gnu steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + with: + submodules: true + - run: rustup update stable --no-self-update + - run: rustup target add wasm32-wasip2 - name: Build - run: cargo build --verbose + run: cargo build --verbose --workspace --exclude wizer-fuzz - name: Run tests - run: cargo test --verbose + run: cargo test --verbose --workspace --exclude wizer-fuzz - name: Checking benches run : cargo check --benches check_fuzz: + name: Checking fuzz runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -47,6 +54,7 @@ jobs: - run: cargo fuzz build --dev -s none rustfmt: + name: Checking format and docs runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -54,3 +62,4 @@ jobs: - run: rustup default stable - run: rustup component add rustfmt - run: cargo fmt --all -- --check + - run: cargo doc diff --git a/.gitignore b/.gitignore index 196c957b..a3dea82d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ npm/wizer-darwin-x64/ npm/wizer-linux-x64/ npm/wizer-win32-x64/ npm/wizer-linux-s390x/ -npm/wizer-linux-arm64/ \ No newline at end of file +npm/wizer-linux-arm64/ diff --git a/Cargo.lock b/Cargo.lock index 5414c76b..84f5d3fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.3" @@ -136,9 +142,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.79" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", @@ -156,6 +162,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "auditable-serde" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5" +dependencies = [ + "semver", + "serde", + "serde_json", + "topological-sort", +] + [[package]] name = "autocfg" version = "1.2.0" @@ -172,7 +190,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.4", "object 0.32.2", "rustc-demangle", ] @@ -225,6 +243,15 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +[[package]] +name = "camino" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0b03af37dad7a14518b7691d81acb0f8222604ad3d1b02f6b4bed5188c0cd5" +dependencies = [ + "serde", +] + [[package]] name = "cap-fs-ext" version = "3.4.4" @@ -303,6 +330,29 @@ dependencies = [ "winx", ] +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 1.0.58", +] + [[package]] name = "cast" version = "0.3.0" @@ -361,7 +411,7 @@ dependencies = [ "ansi_term", "atty", "bitflags 1.3.2", - "strsim", + "strsim 0.8.0", "textwrap", "unicode-width 0.1.11", "vec_map", @@ -374,6 +424,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -382,8 +433,22 @@ version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ + "anstream", "anstyle", "clap_lex", + "strsim 0.11.1", +] + +[[package]] +name = "clap_derive" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.98", ] [[package]] @@ -404,6 +469,58 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "component-init" +version = "0.2.0" +dependencies = [ + "component-init-macro", + "wit-bindgen", +] + +[[package]] +name = "component-init-cli" +version = "0.2.0" +dependencies = [ + "anyhow", + "clap 4.5.4", + "component-init-wasmtime", + "tokio", +] + +[[package]] +name = "component-init-macro" +version = "0.2.0" +dependencies = [ + "quote", + "syn 2.0.98", +] + +[[package]] +name = "component-init-transform" +version = "0.2.0" +dependencies = [ + "anyhow", + "async-trait", + "futures", + "wasm-encoder 0.238.1", + "wasm-metadata 0.238.1", + "wasmparser 0.238.1", +] + +[[package]] +name = "component-init-wasmtime" +version = "0.2.0" +dependencies = [ + "anyhow", + "async-trait", + "component-init-transform", + "test-programs-artifacts", + "tokio", + "wasmtime", + "wasmtime-wasi", + "wat", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -786,6 +903,16 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdeb3aa5e95cf9aabc17f060cfa0ced7b83f042390760ca53bf09df9968acaa1" +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide 0.8.9", +] + [[package]] name = "foldhash" version = "0.1.4" @@ -814,12 +941,13 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -828,9 +956,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -838,39 +966,66 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -1226,6 +1381,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.27" @@ -1271,6 +1436,15 @@ dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "1.0.2" @@ -1325,6 +1499,29 @@ version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1412,6 +1609,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" +dependencies = [ + "proc-macro2", + "syn 2.0.98", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1527,6 +1734,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags 2.5.0", +] + [[package]] name = "redox_users" version = "0.4.5" @@ -1630,7 +1846,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1658,6 +1874,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "semver" version = "1.0.22" @@ -1731,6 +1953,15 @@ dependencies = [ "digest", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + [[package]] name = "slab" version = "0.4.11" @@ -1739,9 +1970,9 @@ checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" dependencies = [ "serde", ] @@ -1756,6 +1987,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spdx" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" +dependencies = [ + "smallvec", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -1768,6 +2008,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "structopt" version = "0.3.26" @@ -1845,6 +2091,22 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "test-programs" +version = "0.0.0" +dependencies = [ + "component-init", + "wit-bindgen", +] + +[[package]] +name = "test-programs-artifacts" +version = "0.0.0" +dependencies = [ + "cargo_metadata", + "heck 0.5.0", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -1930,12 +2192,26 @@ dependencies = [ "io-uring", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "slab", "socket2", + "tokio-macros", "windows-sys 0.52.0", ] +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "toml" version = "0.8.12" @@ -1970,6 +2246,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "topological-sort" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" + [[package]] name = "tracing" version = "0.1.40" @@ -2193,12 +2475,53 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.238.0" +version = "0.238.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50143b010bdc3adbd16275710f9085cc80d9c12cb869309a51a98ce2ff96558e" +checksum = "d50d48c31c615f77679b61c607b8151378a5d03159616bf3d17e8e2005afdaf5" dependencies = [ "leb128fmt", - "wasmparser 0.238.0", + "wasmparser 0.238.1", +] + +[[package]] +name = "wasm-encoder" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c" +dependencies = [ + "leb128fmt", + "wasmparser 0.239.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.238.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00094573b000c92134f2ef0f8afa4f6f892de37e78442988c946243a8c44364e" +dependencies = [ + "anyhow", + "auditable-serde", + "flate2", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "spdx", + "url", + "wasm-encoder 0.238.1", + "wasmparser 0.238.1", +] + +[[package]] +name = "wasm-metadata" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20b3ec880a9ac69ccd92fbdbcf46ee833071cf09f82bb005b2327c7ae6025ae2" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.239.0", + "wasmparser 0.239.0", ] [[package]] @@ -2230,9 +2553,9 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.238.0" +version = "0.238.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ad4ca2ecb86b79ea410cd970985665de1d05774b7107b214bc5852b1bcbad7" +checksum = "3fa99c8328024423875ae4a55345cfde8f0371327fb2d0f33b0f52a06fc44408" dependencies = [ "bitflags 2.5.0", "hashbrown", @@ -2241,6 +2564,18 @@ dependencies = [ "serde", ] +[[package]] +name = "wasmparser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0" +dependencies = [ + "bitflags 2.5.0", + "hashbrown", + "indexmap", + "semver", +] + [[package]] name = "wasmprinter" version = "0.236.1" @@ -2254,13 +2589,13 @@ dependencies = [ [[package]] name = "wasmprinter" -version = "0.238.0" +version = "0.238.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fec8a560f7288effd1a61fe8d7bfe9fc3efdc2173949d7a5ee38ea9e8eaa336" +checksum = "cd2d53749ac5922bdc60ef3288adc7b45990fb079331d977b25dd7e83c75c810" dependencies = [ "anyhow", "termcolor", - "wasmparser 0.238.0", + "wasmparser 0.238.1", ] [[package]] @@ -2386,7 +2721,7 @@ dependencies = [ "syn 2.0.98", "wasmtime-internal-component-util", "wasmtime-internal-wit-bindgen", - "wit-parser", + "wit-parser 0.236.1", ] [[package]] @@ -2528,7 +2863,7 @@ dependencies = [ "bitflags 2.5.0", "heck 0.5.0", "indexmap", - "wit-parser", + "wit-parser 0.236.1", ] [[package]] @@ -2586,24 +2921,24 @@ dependencies = [ [[package]] name = "wast" -version = "238.0.0" +version = "239.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c671ea796336ebaa49b963adb14cf13cb98de4e64d69ed4a16ace8c7b4db87b" +checksum = "9139176fe8a2590e0fb174cdcaf373b224cb93c3dde08e4297c1361d2ba1ea5d" dependencies = [ "bumpalo", "leb128fmt", "memchr", "unicode-width 0.2.0", - "wasm-encoder 0.238.0", + "wasm-encoder 0.239.0", ] [[package]] name = "wat" -version = "1.238.0" +version = "1.239.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de04a6a9c93aaae4de7bec6323bf11f810457b479f9f877e80d212fd77ffdbc" +checksum = "3e1c941927d34709f255558166f8901a2005f8ab4a9650432e9281b7cc6f3b75" dependencies = [ - "wast 238.0.0", + "wast 239.0.0", ] [[package]] @@ -2898,6 +3233,79 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +dependencies = [ + "bitflags 2.5.0", + "futures", + "once_cell", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabd629f94da277abc739c71353397046401518efb2c707669f805205f0b9890" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser 0.239.0", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a4232e841089fa5f3c4fc732a92e1c74e1a3958db3b12f1de5934da2027f1f4" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn 2.0.98", + "wasm-metadata 0.239.0", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d4698c2913d8d9c2b220d116409c3f51a7aa8d7765151b886918367179ee9" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.98", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a866b19dba2c94d706ec58c92a4c62ab63e482b4c935d2a085ac94caecb136" +dependencies = [ + "anyhow", + "bitflags 2.5.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.239.0", + "wasm-metadata 0.239.0", + "wasmparser 0.239.0", + "wit-parser 0.239.0", +] + [[package]] name = "wit-parser" version = "0.236.1" @@ -2916,6 +3324,24 @@ dependencies = [ "wasmparser 0.236.1", ] +[[package]] +name = "wit-parser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55c92c939d667b7bf0c6bf2d1f67196529758f99a2a45a3355cc56964fd5315d" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.239.0", +] + [[package]] name = "witx" version = "0.9.1" @@ -2939,9 +3365,9 @@ dependencies = [ "log", "rayon", "structopt", - "wasm-encoder 0.238.0", - "wasmparser 0.238.0", - "wasmprinter 0.238.0", + "wasm-encoder 0.238.1", + "wasmparser 0.238.1", + "wasmprinter 0.238.1", "wasmtime", "wasmtime-wasi", "wat", @@ -2955,7 +3381,7 @@ dependencies = [ "libfuzzer-sys", "log", "wasm-smith", - "wasmprinter 0.238.0", + "wasmprinter 0.238.1", "wasmtime", "wizer", ] diff --git a/Cargo.toml b/Cargo.toml index 2d0836b7..58ee04c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,10 +6,10 @@ documentation = "https://docs.rs/wizer" edition = "2018" exclude = ["**.wasm"] homepage = "https://github.com/bytecodealliance/wizer" -license = "Apache-2.0 WITH LLVM-exception" +license = { workspace = true } name = "wizer" readme = "./README.md" -repository = "https://github.com/bytecodealliance/wizer" +repository = { workspace = true } version = "10.0.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -27,14 +27,14 @@ name = "uap" harness = false [dependencies] -anyhow = "1.0.97" +anyhow = { workspace = true } cap-std = "3.4.3" env_logger = { version = "0.11.8", optional = true } log = "0.4.27" rayon = "1.10.0" structopt = { version = "0.3.26", optional = true } -wasm-encoder = "0.238.0" -wasmparser = "0.238.0" +wasm-encoder = { workspace = true } +wasmparser = { workspace = true } wasmtime = { workspace = true } wasmtime-wasi = { workspace = true, features = ["preview1"] } @@ -44,25 +44,57 @@ wasmtime-wasi = { workspace = true, features = ["preview1"] } workspace = true optional = true -[workspace.dependencies] -wasmprinter = "0.238.0" -wasmtime = "36" -wasmtime-wasi = "36" - [dev-dependencies] criterion = "0.5.1" env_logger = "0.11.8" wasmprinter = { workspace = true } -wat = "1.238.0" +wat = { workspace = true } + [workspace] members = [ "benches/regex-bench", "benches/uap-bench", + "component-init/cli", + "component-init/guest-rust", + "component-init/guest-rust/macro", + "component-init/test-programs", + "component-init/test-programs/artifacts", + "component-init/transform", + "component-init/wasmtime", "fuzz", "tests/regex-test", ] +[workspace.dependencies] +anyhow = "1.0.97" +async-trait = "0.1.83" +clap = { version = "4", features = ["derive"] } +component-init = { version = "0.2.0", path = "./component-init/guest-rust" } +component-init-macro = { version = "0.2.0", path = "./component-init/guest-rust/macro" } +component-init-transform = { version = "0.2.0", path = "./component-init/transform" } +component-init-wasmtime = { version = "0.2.0", path = "./component-init/wasmtime" } +futures = "0.3.31" +quote = "1.0" +syn = "2.0" +test-programs = { path = "./component-init/test-programs" } +test-programs-artifacts = { path = "./component-init/test-programs/artifacts" } +tokio = { version = "1", features = ["full"] } +wasm-encoder = { version = "0.238.1", features = ["wasmparser"] } +wasmparser = "0.238.1" +wasmprinter = "0.238.1" +wasmtime = "36" +wasmtime-wasi = "36" +wasmtime-wasi-http = "36" +wat = "1.238.1" +wit-bindgen = "0.46.0" +wasm-metadata = "0.238.1" + +[workspace.package] +edition = "2024" +license = "Apache-2.0 WITH LLVM-exception" +repository = "https://github.com/bytecodealliance/wizer" + [profile.bench] debug = true diff --git a/benches/regex-bench/src/lib.rs b/benches/regex-bench/src/lib.rs index cef08b3b..9dd37502 100644 --- a/benches/regex-bench/src/lib.rs +++ b/benches/regex-bench/src/lib.rs @@ -19,6 +19,7 @@ pub extern "C" fn run(ptr: *mut u8, len: usize) -> i32 { let slice = std::slice::from_raw_parts(ptr, len); std::str::from_utf8(slice).unwrap() }; + #[expect(static_mut_refs, reason = "single threaded")] let regex = unsafe { REGEX.as_ref().unwrap() }; regex.is_match(&s) as u8 as i32 } diff --git a/benches/uap-bench/src/lib.rs b/benches/uap-bench/src/lib.rs index 00bed5d3..94ceb04f 100644 --- a/benches/uap-bench/src/lib.rs +++ b/benches/uap-bench/src/lib.rs @@ -33,6 +33,7 @@ pub extern "C" fn init() { .map(|e| e.regex.replace("\\/", "/").replace("\\!", "!")), ) .unwrap(); + #[expect(static_mut_refs, reason = "single threaded")] unsafe { assert!(UA_REGEX_SET.is_none()); UA_REGEX_SET = Some(regex_set); @@ -48,6 +49,7 @@ pub extern "C" fn run(ptr: *mut u8, len: usize) -> i32 { let slice = std::slice::from_raw_parts(ptr, len); std::str::from_utf8(slice).unwrap() }; + #[expect(static_mut_refs, reason = "single threaded")] let regex_set = unsafe { UA_REGEX_SET.as_ref().unwrap() }; regex_set.is_match(&s) as u8 as i32 } diff --git a/component-init/README.md b/component-init/README.md new file mode 100644 index 00000000..743fc2e6 --- /dev/null +++ b/component-init/README.md @@ -0,0 +1,58 @@ +# Pre-initialize WebAssembly Components + +[Wizer][] demonstrated that WebAssembly modules can be made to start up +faster by pre-initializing them. `component-init` does the same thing, +but for the WebAssembly Component Model. + +[Wizer]: https://github.com/bytecodealliance/wizer/ + +`component-init` is a Rust library that can be used in other projects. +(One prominent example is [componentize-py][].) To use it in your +project, you additionally need a WebAssembly runtime that supports the +Component Model; [Wasmtime][] is known to work. + +[componentize-py]: https://github.com/bytecodealliance/componentize-py +[Wasmtime]: https://wasmtime.dev/ + +# Design rationale + +To pre-initialize a WebAssembly program, you need to do three things: + +1. Run its initialization function. +2. Snapshot any resulting changes to its internal mutable state. +3. Emit a new WebAssembly program with the updated mutable state. + +Step 1 is easy and any runtime will do. Step 3 is a little tedious but +it just involves copying the original program mostly unchanged except +for updating data sections and such, so it's not too hard. + +The challenge is in step 2: How do you refer to the internal state from +outside the program? Some state is explicitly exported from a module or +component, so that's easy to access via any runtime, but what about the +rest? + +To solve this, both Wizer and component-init insert an extra step at the +beginning: Generate a new WebAssembly program that exports every piece +of mutable state (e.g. memories and globals) using new names, and +remember those names to look them up again during snapshotting. The +initialization function is then run in that "instrumented" version of +the original program. + +Unlike core WebAssembly modules, WebAssembly components can't directly +export globals or memories, so component-init adds an exported accessor +function for each instead. The effect is essentially the same as Wizer's +use of exports though. + +But component-init adds an additional wrinkle: Although Wizer relies on +Wasmtime as its runtime, component-init requires that whoever uses it +provide a suitable component runtime. This could be Wasmtime but doesn't +have to be. + +So the caller needs to provide a callback function which will be given a +byte array containing a component. It then should run the initialization +function of that component, and return an object that component-init can +use to access various types of named exports from the component. + +Given that callback, component-init can take care of the rest of the +process: instrumenting the component beforehand, and extracting the +state and writing a new component afterwards. diff --git a/component-init/cli/Cargo.lock b/component-init/cli/Cargo.lock new file mode 100644 index 00000000..65803ca8 --- /dev/null +++ b/component-init/cli/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "component-init-cli" +version = "0.1.0" diff --git a/component-init/cli/Cargo.toml b/component-init/cli/Cargo.toml new file mode 100644 index 00000000..73d757a5 --- /dev/null +++ b/component-init/cli/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "component-init-cli" +description = "Tool for pre-initializing WebAssembly components: CLI" +license.workspace = true +edition.workspace = true +version = "0.2.0" + +[[bin]] +name = "component-init" +path = "src/main.rs" + +[dependencies] +anyhow.workspace = true +clap.workspace = true +component-init-wasmtime.workspace = true +tokio.workspace = true diff --git a/component-init/cli/src/main.rs b/component-init/cli/src/main.rs new file mode 100644 index 00000000..2d523dc2 --- /dev/null +++ b/component-init/cli/src/main.rs @@ -0,0 +1,30 @@ +use anyhow::{Context, Result}; +use clap::Parser; +use std::path::PathBuf; + +/// Initialize a component. Saves the internal state of the component +/// after its `component-init` export function has been called. +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// Input component. + #[arg(required = true)] + input: PathBuf, + + /// Output component. If not provided, output will overwrite input. + #[arg(short, long)] + output: Option, +} + +#[tokio::main] +async fn main() -> Result<()> { + let args = Args::parse(); + + let infile = &args.input; + let input = std::fs::read(infile).with_context(|| format!("reading input from {infile:?}"))?; + let output = component_init_wasmtime::initialize(&input).await?; + + let outfile = args.output.as_ref().unwrap_or(infile); + std::fs::write(outfile, output).with_context(|| format!("writing output to {outfile:?}"))?; + Ok(()) +} diff --git a/component-init/guest-rust/Cargo.toml b/component-init/guest-rust/Cargo.toml new file mode 100644 index 00000000..95ad7f1f --- /dev/null +++ b/component-init/guest-rust/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "component-init" +edition.workspace = true +license.workspace = true +version = "0.2.0" + +[dependencies] +component-init-macro.workspace = true +wit-bindgen.workspace = true diff --git a/component-init/guest-rust/macro/Cargo.toml b/component-init/guest-rust/macro/Cargo.toml new file mode 100644 index 00000000..47b49a03 --- /dev/null +++ b/component-init/guest-rust/macro/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "component-init-macro" +license.workspace = true +edition.workspace = true +version = "0.2.0" + +[lib] +proc-macro = true + +[dependencies] +syn = { workspace = true, features = ["full"] } +quote.workspace = true diff --git a/component-init/guest-rust/macro/src/lib.rs b/component-init/guest-rust/macro/src/lib.rs new file mode 100644 index 00000000..9eadd306 --- /dev/null +++ b/component-init/guest-rust/macro/src/lib.rs @@ -0,0 +1,45 @@ +use proc_macro::TokenStream; +use quote::{quote, quote_spanned}; +use syn::{ItemFn, parse_macro_input, spanned::Spanned}; + +#[proc_macro_attribute] +pub fn init(_attr: TokenStream, item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as ItemFn); + if input.sig.asyncness.is_some() { + return quote_spanned! { input.sig.fn_token.span()=> + compile_error!("fn cannot be async"); + } + .into(); + } + if !input.sig.inputs.is_empty() { + return quote_spanned! { input.sig.inputs.span()=> + compile_error!("function cannot have arguments"); + } + .into(); + } + let callee = &input.sig.ident; + + quote! { + #input + + mod __component_init { + component_init::__bindgen::generate!({ + inline: r" + package this:wit; + world w { + export component-init: func(); + } + ", + runtime_path: "component_init::__bindgen::rt", + }); + struct __S; + impl Guest for __S { + fn component_init() { + super::#callee() + } + } + export!(__S); + } + } + .into() +} diff --git a/component-init/guest-rust/src/lib.rs b/component-init/guest-rust/src/lib.rs new file mode 100644 index 00000000..2bf8fc7d --- /dev/null +++ b/component-init/guest-rust/src/lib.rs @@ -0,0 +1,6 @@ +pub use component_init_macro::init; + +#[doc(hidden)] +pub mod __bindgen { + pub use wit_bindgen::*; +} diff --git a/component-init/test-programs/Cargo.toml b/component-init/test-programs/Cargo.toml new file mode 100644 index 00000000..cc9b0419 --- /dev/null +++ b/component-init/test-programs/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "test-programs" +version = "0.0.0" +edition.workspace = true +license.workspace = true +publish = false + +[dependencies] +component-init.workspace = true +wit-bindgen.workspace = true diff --git a/component-init/test-programs/artifacts/Cargo.toml b/component-init/test-programs/artifacts/Cargo.toml new file mode 100644 index 00000000..7843ced1 --- /dev/null +++ b/component-init/test-programs/artifacts/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "test-programs-artifacts" +version = "0.0.0" +edition.workspace = true +license.workspace = true +publish = false + +[dependencies] + +[build-dependencies] +cargo_metadata = "0.18" +heck = "0.5" diff --git a/component-init/test-programs/artifacts/build.rs b/component-init/test-programs/artifacts/build.rs new file mode 100644 index 00000000..ee4b29cc --- /dev/null +++ b/component-init/test-programs/artifacts/build.rs @@ -0,0 +1,62 @@ +use heck::ToShoutySnakeCase; +use std::env::var_os; +use std::path::PathBuf; +use std::process::Command; + +fn main() { + let out_dir = PathBuf::from(var_os("OUT_DIR").expect("OUT_DIR env var exists")); + + let meta = cargo_metadata::MetadataCommand::new() + .exec() + .expect("cargo metadata"); + let test_programs_meta = meta + .packages + .iter() + .find(|p| p.name == "test-programs") + .expect("test-programs is in cargo metadata"); + let test_programs_root = test_programs_meta.manifest_path.parent().unwrap(); + println!( + "cargo:rerun-if-changed={}", + test_programs_root.as_os_str().to_str().unwrap() + ); + + let status = Command::new("cargo") + .arg("build") + .arg("--target=wasm32-wasip2") + .arg("--package=test-programs") + .env("CARGO_TARGET_DIR", &out_dir) + .env("CARGO_PROFILE_DEV_DEBUG", "2") + .env("RUSTFLAGS", rustflags()) + .env_remove("CARGO_ENCODED_RUSTFLAGS") + .status() + .expect("cargo build test programs"); + assert!(status.success()); + + let mut generated_code = "// THIS FILE IS GENERATED CODE\n".to_string(); + + for binary in test_programs_meta + .targets + .iter() + .filter(|t| t.kind == ["bin"]) + { + let component_path = out_dir + .join("wasm32-wasip2") + .join("debug") + .join(format!("{}.wasm", binary.name)); + + let const_name = binary.name.to_shouty_snake_case(); + generated_code += &format!( + "pub const {const_name}: &str = {:?};\n", + component_path.as_os_str().to_str().expect("path is str") + ); + } + + std::fs::write(out_dir.join("gen.rs"), generated_code).unwrap(); +} + +fn rustflags() -> &'static str { + match option_env!("RUSTFLAGS") { + Some(s) if s.contains("-D warnings") => "-D warnings", + _ => "", + } +} diff --git a/component-init/test-programs/artifacts/src/lib.rs b/component-init/test-programs/artifacts/src/lib.rs new file mode 100644 index 00000000..26a930a6 --- /dev/null +++ b/component-init/test-programs/artifacts/src/lib.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/gen.rs")); diff --git a/component-init/test-programs/src/bin/raw.rs b/component-init/test-programs/src/bin/raw.rs new file mode 100644 index 00000000..7dd3b79d --- /dev/null +++ b/component-init/test-programs/src/bin/raw.rs @@ -0,0 +1,26 @@ +use std::sync::atomic::{AtomicBool, Ordering}; + +wit_bindgen::generate!({ + inline: r" + package this:wit; + world w { + export component-init: func(); + }" +}); + +static IS_INITIALIZED: AtomicBool = AtomicBool::new(false); + +struct S; +impl Guest for S { + fn component_init() { + let before = IS_INITIALIZED.swap(true, Ordering::Relaxed); + assert!(!before, "component should only be initialized once"); + } +} + +export!(S); + +fn main() { + let initialized = IS_INITIALIZED.load(Ordering::Relaxed); + assert!(initialized, "component was not initialized") +} diff --git a/component-init/test-programs/src/bin/using_macro.rs b/component-init/test-programs/src/bin/using_macro.rs new file mode 100644 index 00000000..880e53c9 --- /dev/null +++ b/component-init/test-programs/src/bin/using_macro.rs @@ -0,0 +1,14 @@ +use std::sync::atomic::{AtomicBool, Ordering}; + +static IS_INITIALIZED: AtomicBool = AtomicBool::new(false); + +#[component_init::init] +fn init() { + let before = IS_INITIALIZED.swap(true, Ordering::Relaxed); + assert!(!before, "component should only be initialized once"); +} + +fn main() { + let initialized = IS_INITIALIZED.load(Ordering::Relaxed); + assert!(initialized, "component was not initialized") +} diff --git a/component-init/transform/Cargo.toml b/component-init/transform/Cargo.toml new file mode 100644 index 00000000..bb8029e9 --- /dev/null +++ b/component-init/transform/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "component-init-transform" +description = "Tool for pre-initializing WebAssembly components" +license.workspace = true +edition.workspace = true +version = "0.2.0" + +[dependencies] +anyhow.workspace = true +async-trait.workspace = true +futures.workspace = true +wasm-encoder.workspace = true +wasmparser.workspace = true +wasm-metadata.workspace = true diff --git a/component-init/transform/src/lib.rs b/component-init/transform/src/lib.rs new file mode 100644 index 00000000..7f705512 --- /dev/null +++ b/component-init/transform/src/lib.rs @@ -0,0 +1,947 @@ +#![deny(warnings)] + +use { + anyhow::{Context, Result, bail}, + async_trait::async_trait, + futures::future::BoxFuture, + std::{ + collections::{HashMap, hash_map::Entry}, + convert, iter, + ops::Range, + }, + wasm_encoder::{ + Alias, CanonicalFunctionSection, CanonicalOption, CodeSection, Component, + ComponentAliasSection, ComponentExportKind, ComponentExportSection, ComponentTypeSection, + ComponentValType, ConstExpr, DataCountSection, DataSection, ExportKind, ExportSection, + Function, FunctionSection, GlobalSection, GlobalType, ImportSection, InstanceSection, + Instruction as Ins, MemArg, MemorySection, Module, ModuleArg, ModuleSection, + NestedComponentSection, PrimitiveValType, RawSection, TypeSection, ValType, + reencode::{Reencode, RoundtripReencoder as Encode}, + }, + wasmparser::{ + CanonicalFunction, ComponentAlias, ComponentExternalKind, ComponentTypeRef, ExternalKind, + Instance, Operator, Parser, Payload, TypeRef, Validator, + }, +}; + +const PAGE_SIZE_BYTES: i32 = 64 * 1024; + +// TODO: this should ideally be 8 in order to minimize binary size, but that can result in larger numbers of data +// segments than some tools and runtimes will tolerate. We should probably start at 8 and increase as necessary if +// the segment count is too high for a given component. +const MAX_CONSECUTIVE_ZEROS: usize = 64; + +#[async_trait] +pub trait Invoker { + async fn call_s32(&mut self, function: &str) -> Result; + async fn call_s64(&mut self, function: &str) -> Result; + async fn call_f32(&mut self, function: &str) -> Result; + async fn call_f64(&mut self, function: &str) -> Result; + async fn call_list_u8(&mut self, function: &str) -> Result>; +} + +pub async fn initialize( + component: &[u8], + initialize: impl FnOnce(Vec) -> BoxFuture<'static, Result>>, +) -> Result> { + initialize_staged(component, None, initialize).await +} + +pub type Stage2Map<'a> = Option<(&'a [u8], &'a dyn Fn(u32) -> u32)>; + +#[allow(clippy::type_complexity)] +pub async fn initialize_staged( + component_stage1: &[u8], + component_stage2_and_map_module_index: Stage2Map<'_>, + initialize: impl FnOnce(Vec) -> BoxFuture<'static, Result>>, +) -> Result> { + // First, instrument the input component, validating that it conforms to certain rules and exposing the memory + // and all mutable globals via synthesized function exports. + // + // Note that we currently only support a certain style of component, but plan to eventually generalize this + // tool to support arbitrary component graphs. + // + // Current rules: + // - Flat structure (i.e. no subcomponents) + // - Single memory + // - No runtime table operations + // - No reference type globals + // - Each module instantiated at most once + // + // `instrumentation` keeps track of all of the state which will be gathered from the instrumented + // component. + let (instrumented_component, instrumentation) = instrument(component_stage1)?; + + Validator::new().validate_all(&instrumented_component)?; + + // A component runtime will instantiate the component and run its component init function. + let mut invoker = initialize(instrumented_component).await?; + + // The Invoker interface is used to extract the values instrumentation provided into a + // measurement. + let measurement = instrumentation.measure(&mut invoker).await?; + + // Finally, create a new component by applying the measurement (contents of all globals and memory) to the component. + // The resulting component will identical to the original except with all mutable globals initialized to + // the snapshoted values, with all data sections and start functions removed, and with a single active data + // section added containing the memory snapshot. + apply( + measurement, + component_stage1, + component_stage2_and_map_module_index, + ) +} + +struct MemoryInfo { + module_index: u32, + export_name: String, + ty: wasmparser::MemoryType, +} +type GlobalMap = HashMap>; +#[derive(Debug)] +enum GlobalExport { + Existing { + module_index: u32, + global_index: u32, + export_name: String, + }, + Synthesized { + module_index: u32, + global_index: u32, + }, +} +impl GlobalExport { + fn module_export(&self) -> String { + match self { + Self::Existing { export_name, .. } => export_name.clone(), + Self::Synthesized { global_index, .. } => format!("component-init:{global_index}"), + } + } + fn component_export(&self) -> String { + match self { + Self::Existing { + module_index, + global_index, + .. + } => format!("component-init-get-module{module_index}-global{global_index}"), + Self::Synthesized { + module_index, + global_index, + } => format!("component-init-get-module{module_index}-global{global_index}"), + } + } +} + +#[derive(Default)] +struct Instrumentation { + memory: Option, + globals: GlobalMap<(GlobalExport, wasmparser::ValType)>, +} +impl Instrumentation { + fn register_memory( + &mut self, + module_index: u32, + name: impl AsRef, + ty: wasmparser::MemoryType, + ) -> Result<()> { + if self.memory.is_some() { + bail!("only one memory allowed per component"); + } + self.memory = Some(MemoryInfo { + module_index, + export_name: name.as_ref().to_string(), + ty, + }); + Ok(()) + } + fn register_global(&mut self, module_index: u32, global_index: u32, ty: wasmparser::ValType) { + self.globals.entry(module_index).or_default().insert( + global_index, + ( + GlobalExport::Synthesized { + module_index, + global_index, + }, + ty, + ), + ); + } + fn register_global_export( + &mut self, + module_index: u32, + global_index: u32, + export_name: impl AsRef, + ) { + if let Some((name, _)) = self + .globals + .get_mut(&module_index) + .and_then(|map| map.get_mut(&global_index)) + { + let export_name = export_name.as_ref().to_string(); + *name = GlobalExport::Existing { + module_index, + global_index, + export_name, + }; + } + } + fn amend_module_exports(&self, module_index: u32, exports: &mut ExportSection) { + if let Some(g_map) = self.globals.get(&module_index) { + for (export, _ty) in g_map.values() { + if let GlobalExport::Synthesized { global_index, .. } = export { + exports.export(&export.module_export(), ExportKind::Global, *global_index); + } + } + } + } + async fn measure(&self, invoker: &mut Box) -> Result { + let mut globals = HashMap::new(); + + for (module_index, globals_to_export) in &self.globals { + let mut my_global_values = HashMap::new(); + for (global_index, (global_export, ty)) in globals_to_export { + my_global_values.insert( + *global_index, + match ty { + wasmparser::ValType::I32 => ConstExpr::i32_const( + invoker + .call_s32(&global_export.component_export()) + .await + .with_context(|| { + format!("retrieving global value {global_export:?}") + })?, + ), + wasmparser::ValType::I64 => ConstExpr::i64_const( + invoker + .call_s64(&global_export.component_export()) + .await + .with_context(|| { + format!("retrieving global value {global_export:?}") + })?, + ), + wasmparser::ValType::F32 => ConstExpr::f32_const( + invoker + .call_f32(&global_export.component_export()) + .await + .with_context(|| { + format!("retrieving global value {global_export:?}") + })? + .into(), + ), + wasmparser::ValType::F64 => ConstExpr::f64_const( + invoker + .call_f64(&global_export.component_export()) + .await + .with_context(|| { + format!("retrieving global value {global_export:?}") + })? + .into(), + ), + wasmparser::ValType::V128 => bail!("V128 not yet supported"), + wasmparser::ValType::Ref(_) => bail!("reference types not supported"), + }, + ); + } + globals.insert(*module_index, my_global_values); + } + + let memory = if let Some(info) = &self.memory { + let name = "component-init-get-memory"; + Some(( + info.module_index, + invoker + .call_list_u8(name) + .await + .with_context(|| format!("retrieving memory with {name}"))?, + )) + } else { + None + }; + Ok(Measurement { memory, globals }) + } +} + +struct Measurement { + memory: Option<(u32, Vec)>, + globals: GlobalMap, +} + +impl Measurement { + fn data_section(&self, module_index: u32) -> (Option, u32) { + if let Some((m_ix, value)) = &self.memory + && *m_ix == module_index + { + let mut data = DataSection::new(); + let mut data_segment_count = 0; + for (start, len) in Segments::new(value) { + data_segment_count += 1; + data.active( + 0, + &ConstExpr::i32_const(start.try_into().unwrap()), + value[start..][..len].iter().copied(), + ); + } + (Some(data), data_segment_count) + } else { + (None, 0) + } + } + + fn memory_initial(&self, module_index: u32) -> Option { + if let Some((m_ix, value)) = &self.memory + && *m_ix == module_index + { + Some( + u64::try_from((value.len() / usize::try_from(PAGE_SIZE_BYTES).unwrap()) + 1) + .unwrap(), + ) + } else { + None + } + } + + fn global_init(&self, module_index: u32, global_index: u32) -> Option { + self.globals + .get(&module_index) + .and_then(|m| m.get(&global_index).cloned()) + } +} + +fn instrument(component_stage1: &[u8]) -> Result<(Vec, Instrumentation)> { + let mut module_count = 0; + let mut instance_count = 0; + let mut core_function_count = 0; + let mut function_count = 0; + let mut type_count = 0; + let mut instrumentation = Instrumentation::default(); + let mut instantiations = HashMap::new(); + let mut instrumented_component = Component::new(); + let mut parser = Parser::new(0).parse_all(component_stage1); + #[allow(clippy::while_let_on_iterator)] + while let Some(payload) = parser.next() { + let payload = payload?; + let section = payload.as_section(); + match payload { + Payload::ComponentSection { + unchecked_range, .. + } => { + let mut subcomponent = Component::new(); + while let Some(payload) = parser.next() { + let payload = payload?; + let section = payload.as_section(); + let my_range = section.as_ref().map(|(_, range)| range.clone()); + copy_component_section(section, component_stage1, &mut subcomponent); + + if let Some(my_range) = my_range + && my_range.end >= unchecked_range.end + { + break; + } + } + instrumented_component.section(&NestedComponentSection(&subcomponent)); + } + + Payload::ModuleSection { + unchecked_range, .. + } => { + let module_index = get_and_increment(&mut module_count); + let mut global_types = Vec::new(); + let mut instrumented_module = Module::new(); + let mut global_count = 0; + while let Some(payload) = parser.next() { + let payload = payload?; + let section = payload.as_section(); + let my_range = section.as_ref().map(|(_, range)| range.clone()); + match payload { + Payload::ImportSection(reader) => { + for import in reader { + if let TypeRef::Global(_) = import?.ty { + global_count += 1; + } + } + copy_module_section( + section, + component_stage1, + &mut instrumented_module, + ); + } + + Payload::MemorySection(reader) => { + for memory in reader { + instrumentation.register_memory(module_index, "memory", memory?)?; + } + copy_module_section( + section, + component_stage1, + &mut instrumented_module, + ); + } + + Payload::GlobalSection(reader) => { + for global in reader { + let global = global?; + let ty = global.ty; + global_types.push(ty); + let global_index = get_and_increment(&mut global_count); + if global.ty.mutable { + instrumentation.register_global( + module_index, + global_index, + ty.content_type, + ) + } + } + copy_module_section( + section, + component_stage1, + &mut instrumented_module, + ); + } + + Payload::ExportSection(reader) => { + let mut exports = ExportSection::new(); + for export in reader { + let export = export?; + if let ExternalKind::Global = export.kind { + instrumentation.register_global_export( + module_index, + export.index, + export.name, + ) + } + exports.export( + export.name, + Encode.export_kind(export.kind)?, + export.index, + ); + } + + instrumentation.amend_module_exports(module_index, &mut exports); + + instrumented_module.section(&exports); + } + + Payload::CodeSectionEntry(body) => { + for operator in body.get_operators_reader()? { + match operator? { + Operator::TableCopy { .. } + | Operator::TableFill { .. } + | Operator::TableGrow { .. } + | Operator::TableInit { .. } + | Operator::TableSet { .. } => { + bail!("table operations not allowed"); + } + + _ => (), + } + } + copy_module_section( + section, + component_stage1, + &mut instrumented_module, + ); + } + + _ => { + copy_module_section(section, component_stage1, &mut instrumented_module) + } + } + + if let Some(my_range) = my_range + && my_range.end >= unchecked_range.end + { + break; + } + } + instrumented_component.section(&ModuleSection(&instrumented_module)); + } + + Payload::InstanceSection(reader) => { + for instance in reader { + let instance_index = get_and_increment(&mut instance_count); + + if let Instance::Instantiate { module_index, .. } = instance? { + match instantiations.entry(module_index) { + Entry::Vacant(entry) => { + entry.insert(instance_index); + } + Entry::Occupied(_) => bail!("modules may be instantiated at most once"), + } + } + } + copy_component_section(section, component_stage1, &mut instrumented_component); + } + + Payload::ComponentAliasSection(reader) => { + for alias in reader { + match alias? { + ComponentAlias::CoreInstanceExport { + kind: ExternalKind::Func, + .. + } => { + core_function_count += 1; + } + ComponentAlias::InstanceExport { + kind: ComponentExternalKind::Type, + .. + } => { + type_count += 1; + } + ComponentAlias::InstanceExport { + kind: ComponentExternalKind::Func, + .. + } => { + function_count += 1; + } + _ => (), + } + } + copy_component_section(section, component_stage1, &mut instrumented_component); + } + + Payload::ComponentCanonicalSection(reader) => { + for function in reader { + match function? { + CanonicalFunction::Lower { .. } + | CanonicalFunction::ResourceNew { .. } + | CanonicalFunction::ResourceDrop { .. } + | CanonicalFunction::ResourceRep { .. } => { + core_function_count += 1; + } + CanonicalFunction::Lift { .. } => { + function_count += 1; + } + // Unused for now + _ => {} + } + } + copy_component_section(section, component_stage1, &mut instrumented_component); + } + + Payload::ComponentImportSection(reader) => { + for import in reader { + match import?.ty { + ComponentTypeRef::Func(_) => { + function_count += 1; + } + ComponentTypeRef::Type(_) => { + type_count += 1; + } + _ => (), + } + } + copy_component_section(section, component_stage1, &mut instrumented_component); + } + + Payload::ComponentExportSection(reader) => { + for export in reader { + match export?.kind { + ComponentExternalKind::Func => { + function_count += 1; + } + ComponentExternalKind::Type => { + type_count += 1; + } + _ => (), + } + } + copy_component_section(section, component_stage1, &mut instrumented_component); + } + + Payload::ComponentTypeSection(reader) => { + for _ in reader { + type_count += 1; + } + copy_component_section(section, component_stage1, &mut instrumented_component); + } + + _ => copy_component_section(section, component_stage1, &mut instrumented_component), + } + } + + let mut types = TypeSection::new(); + let mut imports = ImportSection::new(); + let mut functions = FunctionSection::new(); + let mut exports = ExportSection::new(); + let mut code = CodeSection::new(); + let mut aliases = ComponentAliasSection::new(); + let mut lifts = CanonicalFunctionSection::new(); + let mut component_types = ComponentTypeSection::new(); + let mut component_exports = ComponentExportSection::new(); + + for (module_index, module_globals) in &instrumentation.globals { + for (global_export, ty) in module_globals.values() { + let ty = Encode.val_type(*ty)?; + let offset = types.len(); + types.ty().function([], [ty]); + imports.import( + &module_index.to_string(), + &global_export.module_export(), + GlobalType { + val_type: ty, + mutable: true, + shared: false, + }, + ); + functions.function(offset); + let mut function = Function::new([]); + function.instruction(&Ins::GlobalGet(offset)); + function.instruction(&Ins::End); + code.function(&function); + let export_name = global_export.component_export(); + exports.export(&export_name, ExportKind::Func, offset); + aliases.alias(Alias::CoreInstanceExport { + instance: instance_count, + kind: ExportKind::Func, + name: &export_name, + }); + component_types + .function() + .params(iter::empty::<(_, ComponentValType)>()) + .result(Some(ComponentValType::Primitive(match ty { + ValType::I32 => PrimitiveValType::S32, + ValType::I64 => PrimitiveValType::S64, + ValType::F32 => PrimitiveValType::F32, + ValType::F64 => PrimitiveValType::F64, + ValType::V128 => bail!("V128 not yet supported"), + ValType::Ref(_) => bail!("reference types not supported"), + }))); + lifts.lift( + core_function_count + offset, + type_count + component_types.len() - 1, + [CanonicalOption::UTF8], + ); + component_exports.export( + &export_name, + ComponentExportKind::Func, + function_count + offset, + None, + ); + } + } + + if let Some(memory_info) = &instrumentation.memory { + let offset = types.len(); + types.ty().function([], [wasm_encoder::ValType::I32]); + imports.import( + &memory_info.module_index.to_string(), + &memory_info.export_name, + Encode.entity_type(TypeRef::Memory(memory_info.ty))?, + ); + functions.function(offset); + + let mut function = Function::new([(1, wasm_encoder::ValType::I32)]); + function.instruction(&Ins::MemorySize(0)); + // stack[0] = current memory, in pages + + function.instruction(&Ins::I32Const(PAGE_SIZE_BYTES)); + function.instruction(&Ins::I32Mul); + function.instruction(&Ins::LocalTee(0)); + // stack[0] = local[0] = current memory, in bytes + + function.instruction(&Ins::I32Const(1)); + function.instruction(&Ins::MemoryGrow(0)); + // stack[1] = old memory, in bytes + // stack[0] = grown memory, in pages, or -1 if failed + function.instruction(&Ins::I32Const(0)); + function.instruction(&Ins::I32LtS); + function.instruction(&Ins::If(wasm_encoder::BlockType::Empty)); + // Trap if memory grow failed + function.instruction(&Ins::Unreachable); + function.instruction(&Ins::Else); + function.instruction(&Ins::End); + + // stack[0] = old memory, in bytes + function.instruction(&Ins::I32Const(0)); + // stack[1] = old memory in bytes + // stack[0] = 0 (start of memory) + function.instruction(&Ins::I32Store(mem_arg(0, 1))); + // 0 stored at end of old memory + function.instruction(&Ins::LocalGet(0)); + function.instruction(&Ins::LocalGet(0)); + // stack[1] = old memory in bytes + // stack[0] = old memory in bytes + function.instruction(&Ins::I32Store(mem_arg(4, 1))); + // old memory size, stored at old memory + 4 + + function.instruction(&Ins::LocalGet(0)); + // stack[0] = old memory in bytes + function.instruction(&Ins::End); + code.function(&function); + + let export_name = "component-init-get-memory".to_owned(); + exports.export(&export_name, ExportKind::Func, offset); + aliases.alias(Alias::CoreInstanceExport { + instance: instance_count, + kind: ExportKind::Func, + name: &export_name, + }); + let list_type = type_count + component_types.len(); + component_types.defined_type().list(PrimitiveValType::U8); + component_types + .function() + .params(iter::empty::<(_, ComponentValType)>()) + .result(Some(ComponentValType::Type(list_type))); + lifts.lift( + core_function_count + offset, + type_count + component_types.len() - 1, + [CanonicalOption::UTF8, CanonicalOption::Memory(0)], + ); + component_exports.export( + &export_name, + ComponentExportKind::Func, + function_count + offset, + None, + ); + } + + let mut instances = InstanceSection::new(); + instances.instantiate( + module_count, + instantiations + .into_iter() + .map(|(module_index, instance_index)| { + ( + module_index.to_string(), + ModuleArg::Instance(instance_index), + ) + }), + ); + + let mut module = Module::new(); + module.section(&types); + module.section(&imports); + module.section(&functions); + module.section(&exports); + module.section(&code); + + instrumented_component.section(&ModuleSection(&module)); + instrumented_component.section(&instances); + instrumented_component.section(&component_types); + instrumented_component.section(&aliases); + instrumented_component.section(&lifts); + instrumented_component.section(&component_exports); + + // Next, invoke the provided `initialize` function, which will return a trait object through which we can + // invoke the functions we added above to capture the state of the initialized instance. + + let instrumented_component = instrumented_component.finish(); + Ok((instrumented_component, instrumentation)) +} + +fn apply( + measurement: Measurement, + component_stage1: &[u8], + component_stage2_and_map_module_index: Stage2Map<'_>, +) -> Result> { + let (component_stage2, map_module_index) = + component_stage2_and_map_module_index.unwrap_or((component_stage1, &convert::identity)); + let mut initialized_component = Component::new(); + let mut parser = Parser::new(0).parse_all(component_stage2); + let mut module_count = 0; + #[allow(clippy::while_let_on_iterator)] + while let Some(payload) = parser.next() { + let payload = payload?; + let section = payload.as_section(); + match payload { + Payload::ComponentSection { + unchecked_range, .. + } => { + let mut subcomponent = Component::new(); + while let Some(payload) = parser.next() { + let payload = payload?; + let section = payload.as_section(); + let my_range = section.as_ref().map(|(_, range)| range.clone()); + copy_component_section(section, component_stage2, &mut subcomponent); + + if let Some(my_range) = my_range + && my_range.end >= unchecked_range.end + { + break; + } + } + initialized_component.section(&NestedComponentSection(&subcomponent)); + } + + Payload::ModuleSection { + unchecked_range, .. + } => { + let module_index = map_module_index(get_and_increment(&mut module_count)); + let mut initialized_module = Module::new(); + let mut global_count = 0; + let (data_section, data_segment_count) = measurement.data_section(module_index); + while let Some(payload) = parser.next() { + let payload = payload?; + let section = payload.as_section(); + let my_range = section.as_ref().map(|(_, range)| range.clone()); + match payload { + Payload::MemorySection(reader) => { + let mut memories = MemorySection::new(); + for memory in reader { + let mut memory = memory?; + + memory.initial = measurement + .memory_initial(module_index) + .expect("measurement for module's memory"); + + memories.memory(Encode.memory_type(memory)?); + } + initialized_module.section(&memories); + } + + Payload::ImportSection(reader) => { + for import in reader { + if let TypeRef::Global(_) = import?.ty { + global_count += 1; + } + } + copy_module_section(section, component_stage2, &mut initialized_module); + } + + Payload::GlobalSection(reader) => { + let mut globals = GlobalSection::new(); + for global in reader { + let global = global?; + let global_index = get_and_increment(&mut global_count); + globals.global( + Encode.global_type(global.ty)?, + &if global.ty.mutable { + measurement + .global_init(module_index, global_index) + .expect("measurement for global") + } else { + Encode.const_expr(global.init_expr)? + }, + ); + } + initialized_module.section(&globals); + } + + Payload::DataSection(_) | Payload::StartSection { .. } => (), + + Payload::DataCountSection { .. } => { + initialized_module.section(&DataCountSection { + count: data_segment_count, + }); + } + + _ => { + copy_module_section(section, component_stage2, &mut initialized_module) + } + } + + if let Some(my_range) = my_range + && my_range.end >= unchecked_range.end + { + break; + } + } + if let Some(data_section) = data_section { + initialized_module.section(&data_section); + } + + initialized_component.section(&ModuleSection(&initialized_module)); + } + + _ => copy_component_section(section, component_stage2, &mut initialized_component), + } + } + + let initialized_component = initialized_component.finish(); + + let mut add = wasm_metadata::AddMetadata::default(); + add.processed_by = vec![( + "component-init-transform".to_owned(), + env!("CARGO_PKG_VERSION").to_owned(), + )]; + + let initialized_component = add.to_wasm(&initialized_component)?; + + Validator::new().validate_all(&initialized_component)?; + + Ok(initialized_component) +} + +struct Segments<'a> { + bytes: &'a [u8], + offset: usize, +} + +impl<'a> Segments<'a> { + fn new(bytes: &'a [u8]) -> Self { + Self { bytes, offset: 0 } + } +} + +impl<'a> Iterator for Segments<'a> { + type Item = (usize, usize); + + fn next(&mut self) -> Option { + let mut zero_count = 0; + let mut start = 0; + let mut length = 0; + for (index, value) in self.bytes[self.offset..].iter().enumerate() { + if *value == 0 { + zero_count += 1; + } else { + if zero_count > MAX_CONSECUTIVE_ZEROS { + if length > 0 { + start += self.offset; + self.offset += index; + return Some((start, length)); + } else { + start = index; + length = 1; + } + } else { + length += zero_count + 1; + } + zero_count = 0; + } + } + if length > 0 { + start += self.offset; + self.offset = self.bytes.len(); + Some((start, length)) + } else { + self.offset = self.bytes.len(); + None + } + } +} + +fn get_and_increment(n: &mut u32) -> u32 { + let v = *n; + *n += 1; + v +} + +fn mem_arg(offset: u64, align: u32) -> MemArg { + MemArg { + offset, + align, + memory_index: 0, + } +} + +fn copy_component_section( + section: Option<(u8, Range)>, + component: &[u8], + result: &mut Component, +) { + if let Some((id, range)) = section { + result.section(&RawSection { + id, + data: &component[range], + }); + } +} + +fn copy_module_section(section: Option<(u8, Range)>, module: &[u8], result: &mut Module) { + if let Some((id, range)) = section { + result.section(&RawSection { + id, + data: &module[range], + }); + } +} diff --git a/component-init/wasmtime/Cargo.toml b/component-init/wasmtime/Cargo.toml new file mode 100644 index 00000000..69c56694 --- /dev/null +++ b/component-init/wasmtime/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "component-init-wasmtime" +description = "Tool for pre-initializing WebAssembly components: Wasmtime integration" +license.workspace = true +edition.workspace = true +version = "0.2.0" + +[dependencies] +anyhow.workspace = true +async-trait.workspace = true +component-init-transform.workspace = true +wasmtime.workspace = true + +[dev-dependencies] +tokio.workspace = true +test-programs-artifacts.workspace = true +wat.workspace = true +wasmtime-wasi.workspace = true diff --git a/component-init/wasmtime/src/lib.rs b/component-init/wasmtime/src/lib.rs new file mode 100644 index 00000000..02168327 --- /dev/null +++ b/component-init/wasmtime/src/lib.rs @@ -0,0 +1,94 @@ +use anyhow::{Context, Result, anyhow}; +use component_init_transform::Invoker; +use wasmtime::{ + Config, Engine, Store, + component::{Component, ComponentNamedList, Instance, Lift, Linker}, +}; + +pub async fn initialize(component: &[u8]) -> Result> { + component_init_transform::initialize(component, |instrumented| { + Box::pin(async move { + let i = invoker(instrumented) + .await + .context("running instrumented component")?; + let i: Box = Box::new(i); + Ok(i) + }) + }) + .await +} + +async fn invoker(component: Vec) -> Result { + let mut config = Config::new(); + config.async_support(true); + let engine = Engine::new(&config).context("creating engine")?; + let component = + Component::new(&engine, &component).context("compiling instrumented component")?; + let mut linker = Linker::new(&engine); + linker + .define_unknown_imports_as_traps(&component) + .context("link unknown imports as traps")?; + let mut store = Store::new(&engine, Ctx); + let instance = linker + .instantiate_async(&mut store, &component) + .await + .context("instantiate")?; + let mut this = Impl { instance, store }; + this.call::<()>("component-init") + .await + .context("running the component-init export func")?; + Ok(this) +} + +pub struct Ctx; + +struct Impl { + instance: Instance, + store: Store, +} + +impl Impl { + async fn call( + &mut self, + name: &str, + ) -> Result { + let export = self + .instance + .get_export_index(&mut self.store, None, name) + .ok_or_else(|| anyhow!("{name} is not exported"))?; + let func = self + .instance + .get_func(&mut self.store, export) + .ok_or_else(|| anyhow!("{name} export is not a func"))?; + let func = func + .typed::<(), T>(&mut self.store) + .with_context(|| format!("type of {name} func"))?; + let r = func + .call_async(&mut self.store, ()) + .await + .with_context(|| format!("executing {name}"))?; + func.post_return_async(&mut self.store) + .await + .with_context(|| format!("post-return {name}"))?; + Ok(r) + } +} + +#[async_trait::async_trait] +impl Invoker for Impl { + async fn call_s32(&mut self, name: &str) -> Result { + Ok(self.call::<(i32,)>(name).await?.0) + } + async fn call_s64(&mut self, name: &str) -> Result { + Ok(self.call::<(i64,)>(name).await?.0) + } + async fn call_f32(&mut self, name: &str) -> Result { + Ok(self.call::<(f32,)>(name).await?.0) + } + async fn call_f64(&mut self, name: &str) -> Result { + Ok(self.call::<(f64,)>(name).await?.0) + } + async fn call_list_u8(&mut self, name: &str) -> Result> { + Ok(self.call::<(Vec,)>(name).await?.0) + } +} diff --git a/component-init/wasmtime/tests/rust.rs b/component-init/wasmtime/tests/rust.rs new file mode 100644 index 00000000..c01e6bdd --- /dev/null +++ b/component-init/wasmtime/tests/rust.rs @@ -0,0 +1,100 @@ +use anyhow::{Context, Result, anyhow}; +use test_programs_artifacts::{RAW, USING_MACRO}; + +async fn execute(component: &[u8]) -> Result<()> { + use wasmtime::{ + Config, Engine, Store, + component::{Component, Linker, ResourceTable}, + }; + use wasmtime_wasi::{WasiCtx, WasiCtxView}; + + let mut config = Config::new(); + config.async_support(true); + let engine = Engine::new(&config).context("create engine")?; + let component = Component::new(&engine, component).context("load component")?; + + struct Ctx { + table: ResourceTable, + wasi: WasiCtx, + } + impl wasmtime_wasi::WasiView for Ctx { + fn ctx(&mut self) -> WasiCtxView<'_> { + WasiCtxView { + ctx: &mut self.wasi, + table: &mut self.table, + } + } + } + + let mut linker = Linker::new(&engine); + wasmtime_wasi::p2::add_to_linker_async(&mut linker).context("add wasi to linker")?; + let mut store = Store::new( + &engine, + Ctx { + table: ResourceTable::new(), + wasi: WasiCtx::builder().inherit_stdout().inherit_stderr().build(), + }, + ); + + let instance = linker + .instantiate_async(&mut store, &component) + .await + .context("instantiate")?; + + let wasi_cli_run = instance + .get_export_index(&mut store, None, "wasi:cli/run@0.2.3") + .ok_or_else(|| anyhow!("`wasi:cli/run` is not exported"))?; + let export = instance + .get_export_index(&mut store, Some(&wasi_cli_run), "run") + .ok_or_else(|| anyhow!("`wasi:cli/run.run` is not exported"))?; + let func = instance + .get_func(&mut store, export) + .ok_or_else(|| anyhow!("`wasi:cli/run.run` export is not a func"))?; + let func = func + .typed::<(), (Result<(), ()>,)>(&mut store) + .context("type of run func")?; + func.call_async(&mut store, ()) + .await + .context("executing run")? + .0 + .map_err(|()| anyhow!("run returned error"))?; + func.post_return_async(&mut store) + .await + .context("post-return run")?; + Ok(()) +} + +async fn inits_properly(component: &[u8]) -> Result<()> { + // Without initialization, run will trap. + let err = execute(component) + .await + .err() + .context("uninitialized run should trap")?; + assert!( + format!("{err:?}").contains("unreachable"), + "should die with an unreachable trap, got: {err:?}" + ); + + let initialized_component = component_init_wasmtime::initialize(component).await?; + + // After initialization, will not trap. + execute(&initialized_component) + .await + .context("execute initialized component")?; + + Ok(()) +} + +#[tokio::test] +async fn raw() -> Result<()> { + println!("test component: {RAW:?}"); + let component = std::fs::read(RAW)?; + inits_properly(&component).await +} + +#[tokio::test] +async fn using_macro() -> Result<()> { + println!("test component: {USING_MACRO:?}"); + let component = std::fs::read(USING_MACRO)?; + inits_properly(&component).await +} diff --git a/component-init/wasmtime/tests/wat.rs b/component-init/wasmtime/tests/wat.rs new file mode 100644 index 00000000..cbc6e18e --- /dev/null +++ b/component-init/wasmtime/tests/wat.rs @@ -0,0 +1,138 @@ +use anyhow::{Context, Result, anyhow}; + +#[tokio::test] +async fn init_memory() -> Result<()> { + // This component exports a `component-init` which initializes a core memory, + // and a `run` which traps unless initialization has occured. + let wat_component = r#" + (component + (core module $m + (memory (export "memory") 1) + ;; assert mem location 0 is 0, set it to 1 + (func (export "component-init") + (i32.load (i32.const 0)) + if (unreachable) else end + (i32.store (i32.const 0) (i32.const 1)) + ) + ;; assert mem location 0 is 1 + (func (export "run") + (i32.eq (i32.const 0) (i32.load (i32.const 0))) + if (unreachable) else end + ) + ) + (core instance $i (instantiate $m)) + (func (export "component-init") + (canon lift (core func $i "component-init"))) + (func (export "run") + (canon lift (core func $i "run"))) + + ;; component_init needs the memory aliased in the component at index 0 + (alias core export $i "memory" (core memory (;0;))) + ) + "#; + test(wat_component).await +} + +#[tokio::test] +async fn init_global() -> Result<()> { + // This component exports a `component-init` which initializes a global, + // and a `run` which traps unless initialization has occured. + let wat_component = r#" + (component + (core module $m + (global $g (mut i32) (i32.const 0)) + ;; assert $g is 0, set it to 1 + (func (export "component-init") + (global.get $g) + if (unreachable) else end + (global.set $g (i32.const 1)) + ) + ;; assert $g is 1 + (func (export "run") + (i32.eq (i32.const 0) (global.get $g)) + if (unreachable) else end + ) + ) + (core instance $i (instantiate $m)) + (func (export "component-init") + (canon lift (core func $i "component-init"))) + (func (export "run") + (canon lift (core func $i "run"))) + ) + "#; + test(wat_component).await +} + +async fn test(wat_component: &str) -> Result<()> { + // component-init only supports component binaries, not wat form + let component = wat::parse_str(wat_component)?; + + // Without initialization, run will trap. + let err = execute(&component, "run") + .await + .err() + .context("uninitialized run should trap")?; + assert!( + format!("{err:?}").contains("unreachable"), + "should die with an unreachable trap, got: {err:?}" + ); + + // Initialize the component. This will execute component-init on the base component. + let initialized_component = component_init_wasmtime::initialize(&component).await?; + + // After initialization, run will not trap. + execute(&initialized_component, "run") + .await + .context("initialized component should not trap in `run`")?; + + // After initialization, component-init will trap. + let err = execute(&initialized_component, "component-init") + .await + .err() + .context("calling component-init again should trap")?; + assert!( + format!("{err:?}").contains("unreachable"), + "should die with an unreachable trap, got: {err:?}" + ); + + Ok(()) +} + +async fn execute(component: &[u8], name: &str) -> Result<()> { + use wasmtime::{ + Config, Engine, Store, + component::{Component, Linker}, + }; + + let mut config = Config::new(); + config.async_support(true); + let engine = Engine::new(&config).context("create engine")?; + let component = Component::new(&engine, component).context("load component")?; + let mut linker = Linker::new(&engine); + linker + .define_unknown_imports_as_traps(&component) + .context("unknown imports as traps")?; + let mut store = Store::new(&engine, ()); + + let instance = linker + .instantiate_async(&mut store, &component) + .await + .context("instantiate")?; + + let export = instance + .get_export_index(&mut store, None, name) + .ok_or_else(|| anyhow!("{name} is not exported"))?; + let func = instance + .get_func(&mut store, export) + .ok_or_else(|| anyhow!("{name} export is not a func"))?; + let func = func + .typed::<(), ()>(&mut store) + .with_context(|| format!("type of {name} func"))?; + func.call_async(&mut store, ()) + .await + .with_context(|| format!("executing {name}"))?; + func.post_return_async(&mut store) + .await + .with_context(|| format!("post-return {name}"))?; + Ok(()) +} diff --git a/tests/regex-test/src/lib.rs b/tests/regex-test/src/lib.rs index 1d71433f..29324d50 100644 --- a/tests/regex-test/src/lib.rs +++ b/tests/regex-test/src/lib.rs @@ -13,6 +13,7 @@ pub fn init() { #[no_mangle] pub fn run(n: i32) -> i32 { let s = format!("{}", n); + #[expect(static_mut_refs, reason = "single threaded")] let regex = unsafe { REGEX.as_ref().unwrap() }; if regex.is_match(&s) { 42