diff --git a/.github/workflows/CreateRelease.yml b/.github/workflows/CreateRelease.yml index c346da4..b9439fa 100644 --- a/.github/workflows/CreateRelease.yml +++ b/.github/workflows/CreateRelease.yml @@ -56,6 +56,7 @@ jobs: - name: Publish hyperlight-js run: | + cargo publish -p hyperlight-js-common cargo publish -p hyperlight-js-runtime cargo publish -p hyperlight-js env: diff --git a/Cargo.lock b/Cargo.lock index 016270c..83b22fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,7 +11,7 @@ dependencies = [ "cfg-if", "once_cell", "version_check", - "zerocopy 0.8.31", + "zerocopy 0.8.40", ] [[package]] @@ -166,9 +166,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.15.1" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b5ce75405893cd713f9ab8e297d8e438f624dde7d706108285f7e17a25a180f" +checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" dependencies = [ "aws-lc-sys", "zeroize", @@ -176,9 +176,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.34.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "179c3777a8b5e70e90ea426114ffc565b2c1a9f82f6c4a0c5a34aa6ef5e781b6" +checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" dependencies = [ "cc", "cmake", @@ -268,9 +268,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" @@ -372,7 +372,7 @@ dependencies = [ "js-sys", "num-traits", "wasm-bindgen", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -455,9 +455,9 @@ checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "cmake" -version = "0.1.56" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b042e5d8a74ae91bb0961acd039822472ec99f8ab0948cbf6d1369588f8be586" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" dependencies = [ "cc", ] @@ -493,9 +493,9 @@ dependencies = [ [[package]] name = "console" -version = "0.16.1" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4" +checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" dependencies = [ "encode_unicode", "libc", @@ -748,18 +748,18 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ "convert_case 0.10.0", "proc-macro2", @@ -951,9 +951,9 @@ dependencies = [ [[package]] name = "fast-glob" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d26eec0ae9682c457cb0f85de67ad417b716ae852736a5d94c2ad6e92a997c9" +checksum = "3b9e81515b0279bf618200fd15d132e7195d2048fb46eed6f0f3c10cbc068266" dependencies = [ "arrayvec", ] @@ -1030,9 +1030,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[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", @@ -1045,9 +1045,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", @@ -1055,15 +1055,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", @@ -1072,15 +1072,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", @@ -1089,21 +1089,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", @@ -1113,7 +1113,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1143,16 +1142,17 @@ dependencies = [ [[package]] name = "generator" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2" +checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" dependencies = [ "cc", "cfg-if", "libc", "log", "rustversion", - "windows 0.61.3", + "windows-link", + "windows-result", ] [[package]] @@ -1167,9 +1167,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", @@ -1184,19 +1184,19 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core 0.10.0", "wasip2", "wasip3", @@ -1223,9 +1223,9 @@ dependencies = [ [[package]] name = "goblin" -version = "0.10.4" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4db6758c546e6f81f265638c980e5e84dfbda80cfd8e89e02f83454c8e8124bd" +checksum = "983a6aafb3b12d4c41ea78d39e189af4298ce747353945ff5105b54a056e5cd9" dependencies = [ "log", "plain", @@ -1234,9 +1234,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -1259,7 +1259,7 @@ checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", - "zerocopy 0.8.31", + "zerocopy 0.8.40", ] [[package]] @@ -1420,14 +1420,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -1551,8 +1550,8 @@ dependencies = [ "tracing-opentelemetry", "uuid", "vmm-sys-util", - "windows 0.62.2", - "windows-result 0.4.1", + "windows", + "windows-result", "windows-sys 0.61.2", "windows-version", ] @@ -1574,6 +1573,7 @@ dependencies = [ "fn-traits", "hyperlight-common", "hyperlight-host", + "hyperlight-js-common", "hyperlight-js-runtime", "lazy_static", "libc", @@ -1601,6 +1601,10 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "hyperlight-js-common" +version = "0.1.1" + [[package]] name = "hyperlight-js-runtime" version = "0.1.1" @@ -1618,6 +1622,7 @@ dependencies = [ "hyperlight-common", "hyperlight-guest", "hyperlight-guest-bin", + "hyperlight-js-common", "rquickjs", "serde", "serde_json", @@ -1629,9 +1634,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1639,7 +1644,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core", ] [[package]] @@ -1767,9 +1772,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", @@ -1779,15 +1784,15 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -1819,15 +1824,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" -version = "0.2.16" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" dependencies = [ "jiff-static", "log", @@ -1838,9 +1843,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.16" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" dependencies = [ "proc-macro2", "quote", @@ -1862,6 +1867,7 @@ name = "js-host-api" version = "0.1.1" dependencies = [ "hyperlight-js", + "hyperlight-js-common", "napi", "napi-build", "napi-derive", @@ -1871,9 +1877,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -1934,7 +1940,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -1944,16 +1950,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" dependencies = [ "cfg-if", - "windows-link 0.2.1", + "windows-link", ] [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags 2.11.0", "libc", ] @@ -2040,9 +2045,9 @@ 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 = "metrics" @@ -2116,21 +2121,21 @@ dependencies = [ [[package]] name = "mshv-bindings" -version = "0.6.5" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66f415da68542aca92b33f55ac3e93031dc30a2941952b99679258f7e0527353" +checksum = "3cbfd4f32d185152003679339751839da77c17e18fa8882a11051a236f841426" dependencies = [ "libc", "num_enum", "vmm-sys-util", - "zerocopy 0.8.31", + "zerocopy 0.8.40", ] [[package]] name = "mshv-ioctls" -version = "0.6.5" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e52a2a02c4107e08f46ba9dfc4e0f4461dffd44fbeca3e5631b4a047d15376c9" +checksum = "f035616abe1e4cbc026a1a8094ff8d3900f5063fe6608309098bc745926fdfd8" dependencies = [ "libc", "mshv-bindings", @@ -2299,9 +2304,9 @@ checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "openssl-probe" -version = "0.1.6" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "opentelemetry" @@ -2432,7 +2437,7 @@ dependencies = [ "thiserror 2.0.18", "tracing", "url", - "windows 0.62.2", + "windows", ] [[package]] @@ -2475,7 +2480,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -2535,18 +2540,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", @@ -2555,9 +2560,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -2601,15 +2606,15 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" dependencies = [ "portable-atomic", ] @@ -2629,7 +2634,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.8.31", + "zerocopy 0.8.40", ] [[package]] @@ -2644,27 +2649,27 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ "toml_edit", ] [[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 = "prost" -version = "0.14.1" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" dependencies = [ "bytes", "prost-derive", @@ -2672,9 +2677,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.14.1" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", "itertools 0.14.0", @@ -2700,9 +2705,9 @@ dependencies = [ [[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", ] @@ -2713,6 +2718,12 @@ 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 = "radix_trie" version = "0.2.1" @@ -2730,7 +2741,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -2740,7 +2751,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ "chacha20", - "getrandom 0.4.1", + "getrandom 0.4.2", "rand_core 0.10.0", ] @@ -2751,14 +2762,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[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", ] @@ -2775,7 +2786,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" dependencies = [ - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -2822,7 +2833,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", "thiserror 2.0.18", ] @@ -2849,9 +2860,9 @@ dependencies = [ [[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", @@ -2861,9 +2872,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", @@ -2872,9 +2883,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "relative-path" @@ -2887,9 +2898,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.26" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b4c14b2d9afca6a60277086b0cc6a6ae0b568f6f7916c943a8cdc79f8be240f" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", @@ -2927,7 +2938,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -2995,9 +3006,9 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.9.0" +version = "8.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fa2c8c9e8711e10f9c4fd2d64317ef13feaab820a4c51541f1a8c8e2e851ab2" +checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" dependencies = [ "proc-macro2", "quote", @@ -3009,9 +3020,9 @@ dependencies = [ [[package]] name = "rust-embed-utils" -version = "8.9.0" +version = "8.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475" +checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" dependencies = [ "globset", "sha2", @@ -3048,9 +3059,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "aws-lc-rs", "once_cell", @@ -3062,9 +3073,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -3074,18 +3085,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "aws-lc-rs", "ring", @@ -3101,9 +3112,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -3157,9 +3168,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.5.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags 2.11.0", "core-foundation", @@ -3170,9 +3181,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -3190,9 +3201,9 @@ dependencies = [ [[package]] name = "self_cell" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" [[package]] name = "semver" @@ -3295,9 +3306,9 @@ dependencies = [ [[package]] name = "shellexpand" -version = "3.1.1" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb" +checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8" dependencies = [ "dirs", ] @@ -3331,10 +3342,11 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -3358,21 +3370,21 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "siphasher" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "sketches-ddsketch" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a" +checksum = "0c6f73aeb92d671e0cc4dca167e59b2deb6387c375391bc99ee743f326994a2b" [[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" @@ -3382,9 +3394,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" dependencies = [ "libc", "windows-sys 0.60.2", @@ -3425,9 +3437,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.111" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -3461,7 +3473,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", - "getrandom 0.4.1", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -3562,9 +3574,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -3583,9 +3595,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -3594,9 +3606,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -3607,18 +3619,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "1.0.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.23.10+spec-1.0.0" +version = "0.25.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" dependencies = [ "indexmap", "toml_datetime", @@ -3628,18 +3640,18 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.8+spec-1.1.0" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0742ff5ff03ea7e67c8ae6c93cac239e0d9784833362da3f9a9c1da8dfefcbdc" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ "winnow", ] [[package]] name = "tonic" -version = "0.14.2" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" +checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" dependencies = [ "async-trait", "base64", @@ -3663,9 +3675,9 @@ dependencies = [ [[package]] name = "tonic-prost" -version = "0.14.2" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67" +checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" dependencies = [ "bytes", "prost", @@ -3674,9 +3686,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -3829,9 +3841,9 @@ dependencies = [ [[package]] name = "tracy-client" -version = "0.18.3" +version = "0.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d722a05fe49b31fef971c4732a7d4aa6a18283d9ba46abddab35f484872947" +checksum = "a4f6fc3baeac5d86ab90c772e9e30620fc653bf1864295029921a15ef478e6a5" dependencies = [ "loom", "once_cell", @@ -3840,9 +3852,9 @@ dependencies = [ [[package]] name = "tracy-client-sys" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fb391ac70462b3097a755618fbf9c8f95ecc1eb379a414f7b46f202ed10db1f" +checksum = "c5f7c95348f20c1c913d72157b3c6dee6ea3e30b3d19502c5a7f6d3f160dacbf" dependencies = [ "cc", "windows-targets 0.52.6", @@ -3862,9 +3874,9 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" @@ -3892,9 +3904,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -3916,11 +3928,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.21.0" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ - "getrandom 0.4.1", + "getrandom 0.4.2", "js-sys", "serde_core", "wasm-bindgen", @@ -3987,11 +3999,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "wit-bindgen 0.46.0", + "wit-bindgen", ] [[package]] @@ -4000,14 +4012,14 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -4018,11 +4030,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -4031,9 +4044,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4041,9 +4054,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -4054,9 +4067,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -4097,9 +4110,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -4117,9 +4130,9 @@ dependencies = [ [[package]] name = "which" -version = "8.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" +checksum = "3a824aeba0fbb27264f815ada4cff43d65b1741b7a4ed7629ff9089148c4a4e0" dependencies = [ "env_home", "regex", @@ -4158,38 +4171,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections 0.2.0", - "windows-core 0.61.2", - "windows-future 0.2.1", - "windows-link 0.1.3", - "windows-numerics 0.2.0", -] - [[package]] name = "windows" version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ - "windows-collections 0.3.2", - "windows-core 0.62.2", - "windows-future 0.3.2", - "windows-numerics 0.3.1", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core 0.61.2", + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", ] [[package]] @@ -4198,20 +4189,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ - "windows-core 0.62.2", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", + "windows-core", ] [[package]] @@ -4222,20 +4200,9 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", - "windows-threading 0.1.0", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] @@ -4244,9 +4211,9 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ - "windows-core 0.62.2", - "windows-link 0.2.1", - "windows-threading 0.2.1", + "windows-core", + "windows-link", + "windows-threading", ] [[package]] @@ -4271,45 +4238,20 @@ dependencies = [ "syn", ] -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", -] - [[package]] name = "windows-numerics" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ - "windows-core 0.62.2", - "windows-link 0.2.1", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", + "windows-core", + "windows-link", ] [[package]] @@ -4318,16 +4260,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -4336,7 +4269,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -4363,7 +4296,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -4388,7 +4321,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link 0.2.1", + "windows-link", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", @@ -4399,22 +4332,13 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - [[package]] name = "windows-threading" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -4423,7 +4347,7 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -4524,9 +4448,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] @@ -4537,12 +4461,6 @@ version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - [[package]] name = "wit-bindgen" version = "0.51.0" @@ -4672,11 +4590,11 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" dependencies = [ - "zerocopy-derive 0.8.31", + "zerocopy-derive 0.8.40", ] [[package]] @@ -4692,9 +4610,9 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", @@ -4763,6 +4681,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.2" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4a4e8e9dc5c62d159f04fcdbe07f4c3fb710415aab4754bf11505501e3251d" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 55b0388..a3864d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["src/hyperlight-js", "src/js-host-api", "src/hyperlight-js-runtime"] +members = ["src/hyperlight-js", "src/js-host-api", "src/hyperlight-js-runtime", "src/hyperlight-js-common"] [workspace.package] version = "0.1.1" @@ -16,6 +16,7 @@ hyperlight-guest-bin = { git = "https://github.com/hyperlight-dev/hyperlight", r hyperlight-guest = { git = "https://github.com/hyperlight-dev/hyperlight", rev = "620339aa95d508e8cbd1d38b4374f09090aade7b" } hyperlight-host = { git = "https://github.com/hyperlight-dev/hyperlight", rev = "620339aa95d508e8cbd1d38b4374f09090aade7b", default-features = false, features = ["executable_heap", "init-paging"] } hyperlight-js = { version = "0.1.1", path = "src/hyperlight-js" } +hyperlight-js-common = { version = "0.1.1", path = "src/hyperlight-js-common" } hyperlight-js-runtime = { version = "0.1.1", path = "src/hyperlight-js-runtime" } [profile.dev] diff --git a/docs/release.md b/docs/release.md index bd2b575..794ed3e 100644 --- a/docs/release.md +++ b/docs/release.md @@ -51,7 +51,7 @@ When this job is done, a new [GitHub release](https://github.com/hyperlight-dev/ This release contains the benchmark results and the source code for the release along with automatically generated release notes. -In addition the hyperlight-js crates will be published to crates.io. You can verify this by going to the [hyperlight-js page on crates.io](https://crates.io/crates/hyperlight-js) and checking that the new version is listed. +In addition the hyperlight-js crates will be published to crates.io in dependency order (`hyperlight-js-common` → `hyperlight-js-runtime` → `hyperlight-js`). You can verify this by going to the [hyperlight-js page on crates.io](https://crates.io/crates/hyperlight-js) and checking that the new version is listed. ## Patching a release diff --git a/src/hyperlight-js-common/Cargo.toml b/src/hyperlight-js-common/Cargo.toml new file mode 100644 index 0000000..decbc97 --- /dev/null +++ b/src/hyperlight-js-common/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "hyperlight-js-common" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +readme.workspace = true +description = """ +Shared constants and binary framing utilities for hyperlight-js. + +This crate is `no_std`-compatible (with `alloc`) so it can be used by both +the host-side `hyperlight-js` crate and the guest-side `hyperlight-js-runtime` +crate (which compiles for `x86_64-hyperlight-none`). +""" + +[dependencies] +# no_std + alloc only — no std, no serde, no anyhow diff --git a/src/hyperlight-js-common/src/lib.rs b/src/hyperlight-js-common/src/lib.rs new file mode 100644 index 0000000..09aad97 --- /dev/null +++ b/src/hyperlight-js-common/src/lib.rs @@ -0,0 +1,368 @@ +/* +Copyright 2026 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +//! Shared constants and binary framing utilities for hyperlight-js. +//! +//! This crate is the **single source of truth** for the wire-format used to +//! pass binary data (`Uint8Array` / `Buffer`) between guest JavaScript and +//! host functions. Both `hyperlight-js` (host) and `hyperlight-js-runtime` +//! (guest, `no_std`) depend on this crate instead of duplicating the logic. +//! +//! # Wire Format — Binary Sidecar +//! +//! Binary blobs are packed into a length-prefixed sidecar: +//! +//! ```text +//! [count: u32-le] [len0: u32-le] [bytes0...] [len1: u32-le] [bytes1...] ... +//! ``` +//! +//! # Wire Format — Tagged Returns +//! +//! Host function returns use a single-byte tag prefix: +//! - `0x00` + payload → JSON string follows +//! - `0x01` + payload → raw binary follows (single buffer return) + +#![no_std] +extern crate alloc; + +use alloc::fmt; +use alloc::string::String; +use alloc::vec::Vec; + +// ── Constants ──────────────────────────────────────────────────────── + +/// Tag byte indicating the return payload is JSON. +pub const TAG_JSON: u8 = 0x00; + +/// Tag byte indicating the return payload is raw binary. +pub const TAG_BINARY: u8 = 0x01; + +/// JSON key used as a placeholder in serialised arguments to mark the +/// position of a binary blob that has been moved to the sidecar channel. +/// The value is the zero-based index into the sidecar blob array. +/// +/// Example: `{"__bin__": 0}` means "insert sidecar blob 0 here". +pub const PLACEHOLDER_BIN: &str = "__bin__"; + +/// JSON key used as a base64-encoded binary marker in the NAPI ↔ JS +/// bridge. The value is a base64 string representation of the bytes. +/// +/// Example: `{"__buffer__": "SGVsbG8="}` +pub const MARKER_BUFFER: &str = "__buffer__"; + +// ── Error type ─────────────────────────────────────────────────────── + +/// Lightweight decoding error — `no_std`-compatible (no `anyhow`, no `std`). +/// +/// Both the host (`hyperlight-js`) and guest (`hyperlight-js-runtime`) +/// convert this into their own error types via `From` impls. +#[derive(Debug, Clone)] +pub struct DecodeError(String); + +impl DecodeError { + /// Create a new decode error with the given message. + pub fn new(msg: impl Into) -> Self { + Self(msg.into()) + } +} + +impl fmt::Display for DecodeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +// ── Encoding ───────────────────────────────────────────────────────── + +/// Encodes multiple binary blobs into the sidecar format. +/// +/// Format: `[count: u32-le] [len0: u32-le] [bytes0...] [len1: u32-le] [bytes1...] ...` +/// +/// Accepts any slice of items that implement `AsRef<[u8]>` — e.g. +/// `&[Vec]`, `&[&[u8]]`, `&[Box<[u8]>]` — so callers don't need to +/// build an intermediate `Vec<&[u8]>` just to satisfy the signature. +pub fn encode_binaries>(blobs: &[B]) -> Vec { + // Validate that count and blob lengths fit in u32 — the wire format + // uses u32-le for these fields. Overflow would create a corrupt sidecar + // that the decoder would reject, but we fail early with a clear message. + assert!( + blobs.len() <= u32::MAX as usize, + "encode_binaries: blob count ({}) exceeds u32::MAX", + blobs.len() + ); + + // Calculate total size: 4 bytes for count + (4 bytes length + data) per blob + let total_size = 4 + blobs.iter().map(|b| 4 + b.as_ref().len()).sum::(); + let mut buf = Vec::with_capacity(total_size); + + // Write count + buf.extend_from_slice(&(blobs.len() as u32).to_le_bytes()); + + // Write each blob with length prefix + for blob in blobs { + let bytes = blob.as_ref(); + assert!( + bytes.len() <= u32::MAX as usize, + "encode_binaries: blob length ({}) exceeds u32::MAX", + bytes.len() + ); + buf.extend_from_slice(&(bytes.len() as u32).to_le_bytes()); + buf.extend_from_slice(bytes); + } + + buf +} + +/// Encodes a JSON return value with the appropriate tag. +pub fn encode_json_return(json: &str) -> Vec { + let mut buf = Vec::with_capacity(1 + json.len()); + buf.push(TAG_JSON); + buf.extend_from_slice(json.as_bytes()); + buf +} + +/// Encodes a binary return value with the appropriate tag. +pub fn encode_binary_return(data: &[u8]) -> Vec { + let mut buf = Vec::with_capacity(1 + data.len()); + buf.push(TAG_BINARY); + buf.extend_from_slice(data); + buf +} + +// ── Decoding ───────────────────────────────────────────────────────── + +/// Decodes the sidecar format into individual binary blobs. +/// +/// Returns a [`DecodeError`] if the buffer is malformed (truncated, +/// invalid lengths, or suspiciously large blob counts). +pub fn decode_binaries(data: &[u8]) -> Result>, DecodeError> { + if data.len() < 4 { + return Err(DecodeError::new( + "Binary sidecar too short for count header", + )); + } + + let count = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize; + + // Sanity check: each blob needs at least 4 bytes for length header. + // This prevents allocation of a huge Vec when count is maliciously large. + let max_possible_blobs = (data.len().saturating_sub(4)) / 4; + if count > max_possible_blobs { + return Err(DecodeError::new(alloc::format!( + "Binary sidecar count ({count}) exceeds maximum possible ({max_possible_blobs})" + ))); + } + + let mut offset = 4; + let mut blobs = Vec::with_capacity(count); + + for i in 0..count { + if offset + 4 > data.len() { + return Err(DecodeError::new(alloc::format!( + "Binary sidecar truncated at blob {i} length header" + ))); + } + + let len = u32::from_le_bytes([ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + ]) as usize; + offset += 4; + + if offset + len > data.len() { + return Err(DecodeError::new(alloc::format!( + "Binary sidecar truncated at blob {i} data (need {len} bytes, have {})", + data.len() - offset + ))); + } + + blobs.push(data[offset..offset + len].to_vec()); + offset += len; + } + + // Reject trailing data — the sidecar should be fully consumed. + // Trailing bytes could indicate a version mismatch or corruption. + if offset != data.len() { + return Err(DecodeError::new(alloc::format!( + "Binary sidecar has {} trailing bytes after all {count} blobs", + data.len() - offset + ))); + } + + Ok(blobs) +} + +/// Maximum recursion depth for JSON tree traversal. +/// Shared across host and NAPI layers to limit stack usage. +pub const MAX_JSON_DEPTH: usize = 64; + +/// Result of decoding a tagged return value. +#[derive(Debug, Clone)] +pub enum FnReturn { + /// JSON string payload. + Json(String), + /// Raw binary payload. + Binary(Vec), +} + +/// Decodes a tagged return value from the host. +/// +/// The first byte is a tag (see [`TAG_JSON`] / [`TAG_BINARY`]), +/// the rest is the payload. +pub fn decode_return(data: &[u8]) -> Result { + if data.is_empty() { + return Err(DecodeError::new("Empty return payload")); + } + + match data[0] { + TAG_JSON => { + let json = core::str::from_utf8(&data[1..]).map_err(|e| { + DecodeError::new(alloc::format!("Invalid UTF-8 in JSON return: {e}")) + })?; + Ok(FnReturn::Json(json.into())) + } + TAG_BINARY => Ok(FnReturn::Binary(data[1..].to_vec())), + tag => Err(DecodeError::new(alloc::format!( + "Unknown return tag: 0x{tag:02x}" + ))), + } +} + +// ── Tests ──────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + extern crate alloc; + use alloc::string::ToString; + use alloc::vec; + use alloc::vec::Vec; + + use super::*; + + #[test] + fn test_encode_decode_empty() { + let encoded = encode_binaries::<&[u8]>(&[]); + assert_eq!(encoded, vec![0, 0, 0, 0]); // count = 0 + + let decoded = decode_binaries(&encoded).unwrap(); + assert!(decoded.is_empty()); + } + + #[test] + fn test_encode_decode_single() { + let blob = b"hello"; + let encoded = encode_binaries(&[blob]); + + // count=1, len=5, "hello" + let expected: Vec = vec![1, 0, 0, 0, 5, 0, 0, 0, b'h', b'e', b'l', b'l', b'o']; + assert_eq!(encoded, expected); + + let decoded = decode_binaries(&encoded).unwrap(); + assert_eq!(decoded, vec![b"hello".to_vec()]); + } + + #[test] + fn test_encode_decode_multiple() { + let blobs: &[&[u8]] = &[b"abc", b"", b"xy"]; + let encoded = encode_binaries(blobs); + + let decoded = decode_binaries(&encoded).unwrap(); + assert_eq!(decoded, vec![b"abc".to_vec(), b"".to_vec(), b"xy".to_vec()]); + } + + #[test] + fn test_encode_decode_vec_of_vecs() { + let blobs: Vec> = vec![b"ABC".to_vec(), b"XY".to_vec()]; + let encoded = encode_binaries(&blobs); + + let decoded = decode_binaries(&encoded).unwrap(); + assert_eq!(decoded, blobs); + } + + #[test] + fn test_decode_truncated_count() { + let result = decode_binaries(&[1, 2, 3]); + assert!(result.is_err()); + } + + #[test] + fn test_decode_truncated_length() { + // count=1 but no length header + let result = decode_binaries(&[1, 0, 0, 0]); + assert!(result.is_err()); + } + + #[test] + fn test_decode_truncated_data() { + // count=1, len=10 but only 3 bytes of data + let result = decode_binaries(&[1, 0, 0, 0, 10, 0, 0, 0, 1, 2, 3]); + assert!(result.is_err()); + } + + #[test] + fn test_decode_trailing_data() { + // Valid sidecar with one blob "abc" followed by trailing garbage + let mut data = encode_binaries(&[b"abc" as &[u8]]); + data.push(0xFF); // trailing byte + let result = decode_binaries(&data); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("trailing")); + } + + #[test] + fn test_return_json() { + let json = r#"{"result":42}"#; + let encoded = encode_json_return(json); + assert_eq!(encoded[0], TAG_JSON); + + match decode_return(&encoded).unwrap() { + FnReturn::Json(s) => assert_eq!(s, json), + _ => panic!("Expected JSON return"), + } + } + + #[test] + fn test_return_binary() { + let data = b"\x00\x01\x02\xff"; + let encoded = encode_binary_return(data); + assert_eq!(encoded[0], TAG_BINARY); + + match decode_return(&encoded).unwrap() { + FnReturn::Binary(b) => assert_eq!(b, data), + _ => panic!("Expected binary return"), + } + } + + #[test] + fn test_return_empty() { + let result = decode_return(&[]); + assert!(result.is_err()); + } + + #[test] + fn test_return_unknown_tag() { + let result = decode_return(&[0x99, 1, 2, 3]); + assert!(result.is_err()); + } + + #[test] + fn test_decode_error_display() { + let err = DecodeError::new("something went wrong"); + assert_eq!(err.to_string(), "something went wrong"); + } +} diff --git a/src/hyperlight-js-runtime/Cargo.toml b/src/hyperlight-js-runtime/Cargo.toml index 70f4f8e..232f3da 100644 --- a/src/hyperlight-js-runtime/Cargo.toml +++ b/src/hyperlight-js-runtime/Cargo.toml @@ -16,6 +16,7 @@ harness = false test = false [dependencies] +hyperlight-js-common = { workspace = true } anyhow = { version = "1.0", default-features = false } base64 = {version = "0.22", default-features = false, features = ["alloc"] } fn-traits = "0.2.0" diff --git a/src/hyperlight-js-runtime/src/host_fn.rs b/src/hyperlight-js-runtime/src/host_fn.rs index 531b97d..81581d8 100644 --- a/src/hyperlight-js-runtime/src/host_fn.rs +++ b/src/hyperlight-js-runtime/src/host_fn.rs @@ -17,17 +17,21 @@ use alloc::format; use alloc::rc::Rc; use alloc::string::{String, ToString as _}; use alloc::sync::Arc; +use alloc::vec::Vec; use core::cell::{Ref, RefCell, RefMut}; use core::ptr::NonNull; use anyhow::{bail, ensure, Context as _}; +use base64::Engine as _; use hashbrown::HashMap; +use hyperlight_js_common::{FnReturn, MARKER_BUFFER, PLACEHOLDER_BIN}; use rquickjs::loader::{Loader, Resolver}; use rquickjs::module::{Declarations, Exports, ModuleDef}; use rquickjs::prelude::Rest; -use rquickjs::{Ctx, Exception, Function, JsLifetime, Module, Value}; +use rquickjs::{Array, Ctx, Exception, Function, JsLifetime, Module, TypedArray, Value}; use serde::de::DeserializeOwned; use serde::Serialize; +use serde_json::json; /// A clone of rquickjs::Module so that we can access the ctx from it by transmuting. struct NakedModule<'js> { @@ -87,6 +91,188 @@ where f } +/// Checks if a JS value is a Uint8Array and extracts its bytes. +fn try_extract_uint8array(value: &Value<'_>) -> Option> { + let obj = value.as_object()?; + let typed_array = obj.as_typed_array::()?; + typed_array.as_bytes().map(|b| b.to_vec()) +} + +/// Maximum recursion depth for JSON tree traversal in the guest runtime. +/// Matches the host-side limit in `hyperlight-js-common::MAX_JSON_DEPTH`. +const MAX_GUEST_JSON_DEPTH: usize = 64; + +/// Recursively processes a JS value, extracting binary data and replacing with placeholders. +/// Returns a serde_json::Value with placeholders and collects binary blobs. +fn value_to_json_with_binaries<'js>( + ctx: &Ctx<'js>, + value: Value<'js>, + binaries: &mut Vec>, + depth: usize, +) -> anyhow::Result { + if depth > MAX_GUEST_JSON_DEPTH { + anyhow::bail!("JSON nesting depth exceeds maximum ({MAX_GUEST_JSON_DEPTH})"); + } + + // Check for Uint8Array first + if let Some(bytes) = try_extract_uint8array(&value) { + let index = binaries.len(); + binaries.push(bytes); + return Ok(json!({PLACEHOLDER_BIN: index})); + } + + // Handle null/undefined + if value.is_null() || value.is_undefined() { + return Ok(serde_json::Value::Null); + } + + // Handle booleans + if let Some(b) = value.as_bool() { + return Ok(serde_json::Value::Bool(b)); + } + + // Handle numbers + // QuickJS stores numbers as doubles internally but optimises small + // integers into SMIs. We check as_int() first for integer fidelity, + // falling back to as_float() for all other numeric values. + // For floats that represent whole numbers (e.g. 42.0 from JSON.parse), + // we emit them as integers to match JSON.stringify behaviour and + // preserve serde integer deserialization on the host side. + if let Some(n) = value.as_int() { + return Ok(serde_json::Value::Number(n.into())); + } + if let Some(n) = value.as_float() { + // Handle NaN and Infinity as null (like JSON.stringify) + if n.is_finite() { + // If the float is a whole number that fits in i64, emit as integer + // to match JSON.stringify behaviour (42.0 → 42, not 42.0) + if n == (n as i64) as f64 && n >= i64::MIN as f64 && n <= i64::MAX as f64 { + return Ok(serde_json::Value::Number((n as i64).into())); + } + if let Some(num) = serde_json::Number::from_f64(n) { + return Ok(serde_json::Value::Number(num)); + } + } + return Ok(serde_json::Value::Null); + } + + // Handle strings + if let Some(s) = value.as_string() { + let s = s.to_string()?; + return Ok(serde_json::Value::String(s)); + } + + // Handle arrays + if let Some(array) = value.as_array() { + let mut json_array = Vec::with_capacity(array.len()); + for item in array.iter::() { + let item = item?; + json_array.push(value_to_json_with_binaries(ctx, item, binaries, depth + 1)?); + } + return Ok(serde_json::Value::Array(json_array)); + } + + // Handle objects + if let Some(obj) = value.as_object() { + let mut json_obj = serde_json::Map::new(); + for entry in obj.props::() { + let (key, val) = entry?; + json_obj.insert( + key, + value_to_json_with_binaries(ctx, val, binaries, depth + 1)?, + ); + } + return Ok(serde_json::Value::Object(json_obj)); + } + + // Fallback: use JSON.stringify for anything else + let json_str = ctx + .json_stringify(value)? + .map(|s| s.to_string()) + .transpose()? + .unwrap_or_else(|| "null".into()); + let parsed: serde_json::Value = serde_json::from_str(&json_str)?; + Ok(parsed) +} + +/// Extracts binary data from JS arguments, replacing with placeholders. +/// Returns the JSON string with placeholders and the collected binary blobs. +fn extract_binaries<'js>( + ctx: &Ctx<'js>, + args: Vec>, +) -> anyhow::Result<(String, Vec>)> { + let mut binaries = Vec::new(); + let mut json_args = Vec::with_capacity(args.len()); + + for arg in args { + json_args.push(value_to_json_with_binaries(ctx, arg, &mut binaries, 0)?); + } + + let json = serde_json::to_string(&json_args)?; + Ok((json, binaries)) +} + +/// Converts a serde_json Value to a rquickjs Value, converting `__buffer__` markers to Uint8Array. +fn json_to_value_with_buffers<'js>( + ctx: &Ctx<'js>, + value: serde_json::Value, + depth: usize, +) -> anyhow::Result> { + if depth > MAX_GUEST_JSON_DEPTH { + anyhow::bail!("JSON nesting depth exceeds maximum ({MAX_GUEST_JSON_DEPTH})"); + } + + match value { + serde_json::Value::Null => Ok(Value::new_null(ctx.clone())), + serde_json::Value::Bool(b) => Ok(Value::new_bool(ctx.clone(), b)), + serde_json::Value::Number(n) => { + if let Some(i) = n.as_i64() + && let Ok(i32_val) = i32::try_from(i) + { + Ok(Value::new_int(ctx.clone(), i32_val)) + } else if let Some(f) = n.as_f64() { + Ok(Value::new_float(ctx.clone(), f)) + } else { + Ok(Value::new_null(ctx.clone())) + } + } + serde_json::Value::String(s) => { + let js_str = rquickjs::String::from_str(ctx.clone(), &s)?; + Ok(js_str.into_value()) + } + serde_json::Value::Array(arr) => { + let js_array = Array::new(ctx.clone())?; + for (i, item) in arr.into_iter().enumerate() { + let js_item = json_to_value_with_buffers(ctx, item, depth + 1)?; + js_array.set(i, js_item)?; + } + Ok(js_array.into_value()) + } + serde_json::Value::Object(obj) => { + // Check for __buffer__ marker + if obj.len() == 1 + && let Some(serde_json::Value::String(b64)) = obj.get(MARKER_BUFFER) + { + // Decode base64 and create Uint8Array + let bytes = base64::engine::general_purpose::STANDARD + .decode(b64) + .map_err(|e| { + anyhow::anyhow!("Invalid base64 in {} marker: {e}", MARKER_BUFFER) + })?; + let array = TypedArray::::new(ctx.clone(), bytes)?; + return Ok(array.into_value()); + } + // Regular object + let js_obj = rquickjs::Object::new(ctx.clone())?; + for (key, val) in obj { + let js_val = json_to_value_with_buffers(ctx, val, depth + 1)?; + js_obj.set(&key, js_val)?; + } + Ok(js_obj.into_value()) + } + } +} + /// A `ModuleDef` implementation that can be used to declare and evaluate host modules. /// This module will look up the module name in the ctx userdata and declare/evaluate /// the functions in the module accordingly. @@ -129,7 +315,7 @@ impl ModuleDef for HostModuleDef { let module: &Module = unsafe { core::mem::transmute(exports) }; let module_name: String = module.name()?; - // We don't have access to self in this function, so we can pass rich data to this function. + // We don't have access to self in this function, so we can't pass rich data to this function. // Instead, we use a userdata in the context to get the list of functions to export. let Some(loader) = ctx.userdata::() else { return Err(Exception::throw_internal(ctx, "HostModuleLoader not found")); @@ -176,7 +362,10 @@ impl HostFunction { func(ctx, args).map_err(|e| match e.downcast::() { Ok(e) => e, Err(e) => { - Exception::throw_internal(ctx, &format!("Host function error: {e:#?}")) + // Use Display chain ({e:#}) instead of Debug struct + // ({e:#?}) to keep the message compact and avoid + // truncation at the hyperlight guest↔host boundary. + Exception::throw_internal(ctx, &format!("Host function error: {e:#}")) } }) }, @@ -188,6 +377,10 @@ impl HostFunction { /// /// This is useful for hyperlight, where we use JSON as the serialization format for communication /// with the host. + /// + /// **Note:** This variant does not support `Uint8Array`/`Buffer` arguments — + /// they will be serialized as empty objects by QuickJS's `JSON.stringify`. + /// Use [`new_bin`](Self::new_bin) for functions that handle binary data. pub fn new_json(func: impl Fn(String) -> anyhow::Result + 'static) -> Self { Self::new( move |ctx: &Ctx, args: Rest| -> anyhow::Result { @@ -202,6 +395,52 @@ impl HostFunction { ) } + /// Create a new `HostFunction` from a closure that supports binary data. + /// + /// This variant detects `Uint8Array`/`ArrayBuffer` arguments and passes them + /// through a sidecar binary channel instead of JSON-encoding them. The JSON + /// contains `{"__bin__": N}` placeholders that reference the sidecar blobs. + /// + /// The closure receives: + /// - `args_json`: JSON string with placeholders for binary arguments + /// - `binaries`: Packed binary sidecar (length-prefixed format) + /// + /// The closure returns a tagged result: + /// - `0x00` + JSON = JSON return value + /// - `0x01` + bytes = raw binary return (becomes `Uint8Array` on JS side) + pub fn new_bin(func: impl Fn(String, Vec) -> anyhow::Result> + 'static) -> Self { + Self::new( + move |ctx: &Ctx, args: Rest| -> anyhow::Result { + // Extract binary blobs and replace with placeholders + let (json_args, binaries) = extract_binaries(ctx, args.into_inner())?; + + // Encode binaries into sidecar format — encode_binaries + // accepts &[Vec] directly, no intermediate Vec<&[u8]> needed + let packed = hyperlight_js_common::encode_binaries(&binaries); + + // Call the host function + let result = func(json_args, packed).context("Calling binary host function")?; + + // Decode the tagged return value + match hyperlight_js_common::decode_return(&result) + .map_err(|e| anyhow::anyhow!("{e}"))? + { + FnReturn::Json(json) => { + // Parse JSON and convert __buffer__ markers to Uint8Array + let json_value: serde_json::Value = + serde_json::from_str(&json).context("Parsing JSON return from host")?; + json_to_value_with_buffers(ctx, json_value, 0) + } + FnReturn::Binary(data) => { + // Create a Uint8Array from the binary data + let array = TypedArray::::new(ctx.clone(), data)?; + Ok(array.into_value()) + } + } + }, + ) + } + /// Create a new `HostFunction` from a closure that takes and returns any type that can be /// serialized by serde. /// diff --git a/src/hyperlight-js-runtime/src/lib.rs b/src/hyperlight-js-runtime/src/lib.rs index 4d89e2e..f2f6efe 100644 --- a/src/hyperlight-js-runtime/src/lib.rs +++ b/src/hyperlight-js-runtime/src/lib.rs @@ -27,6 +27,7 @@ pub(crate) mod utils; use alloc::format; use alloc::rc::Rc; use alloc::string::{String, ToString}; +use alloc::vec::Vec; use anyhow::{anyhow, Context as _}; use hashbrown::HashMap; @@ -100,44 +101,54 @@ impl JsRuntime { } /// Register a host function in the specified module. - /// The function takes and returns a JSON string, which is deserialized and serialized by the runtime. - /// The arguments are serialized as a JSON array containing all the arguments passed to the function. - pub fn register_json_host_function( + /// The function takes and returns any type that can be (de)serialized by `serde`. + pub fn register_host_function( &mut self, module_name: impl Into, function_name: impl Into, - function: impl Fn(String) -> anyhow::Result + 'static, - ) -> anyhow::Result<()> { + function: impl fn_traits::Fn> + 'static, + ) -> anyhow::Result<()> + where + Args: DeserializeOwned, + Output: Serialize, + { self.context.with(|ctx| { ctx.userdata::() .context("HostModuleLoader not found in context")? .borrow_mut() .entry(module_name.into()) .or_default() - .add_function(function_name.into(), HostFunction::new_json(function)); + .add_function(function_name.into(), HostFunction::new_serde(function)); Ok(()) }) } - /// Register a host function in the specified module. - /// The function takes and returns any type that can be (de)serialized by `serde`. - pub fn register_host_function( + /// Register a binary-capable host function in the specified module. + /// + /// This variant supports `Uint8Array`/`ArrayBuffer` arguments and returns. + /// Binary data is passed via a sidecar channel instead of JSON encoding, + /// avoiding base64 overhead. + /// + /// The function receives: + /// - `args_json`: JSON string with `{"__bin__": N}` placeholders for binary args + /// - `binaries`: Packed binary sidecar (length-prefixed format) + /// + /// The function returns a tagged result: + /// - `0x00` + JSON = JSON return value + /// - `0x01` + bytes = raw binary return (becomes `Uint8Array` on JS side) + pub fn register_binary_host_function( &mut self, module_name: impl Into, function_name: impl Into, - function: impl fn_traits::Fn> + 'static, - ) -> anyhow::Result<()> - where - Args: DeserializeOwned, - Output: Serialize, - { + function: impl Fn(String, Vec) -> anyhow::Result> + 'static, + ) -> anyhow::Result<()> { self.context.with(|ctx| { ctx.userdata::() .context("HostModuleLoader not found in context")? .borrow_mut() .entry(module_name.into()) .or_default() - .add_function(function_name.into(), HostFunction::new_serde(function)); + .add_function(function_name.into(), HostFunction::new_bin(function)); Ok(()) }) } diff --git a/src/hyperlight-js-runtime/src/main/hyperlight.rs b/src/hyperlight-js-runtime/src/main/hyperlight.rs index 350ab50..6678012 100644 --- a/src/hyperlight-js-runtime/src/main/hyperlight.rs +++ b/src/hyperlight-js-runtime/src/main/hyperlight.rs @@ -93,7 +93,12 @@ fn register_handler( } #[host_function("CallHostJsFunction")] -fn call_host_js_function(module_name: String, func_name: String, args: String) -> Result; +fn call_host_js_function( + module_name: String, + func_name: String, + args_json: String, + binaries: Vec, +) -> Result>; #[guest_function("RegisterHostModules")] fn register_host_modules(host_modules_json: String) -> Result<()> { @@ -112,12 +117,28 @@ fn register_host_modules(host_modules_json: String) -> Result<()> { for (module_name, functions) in host_modules { for function_name in functions { let module_name = module_name.clone(); - runtime.register_json_host_function( + // Register binary-capable host function that can handle Uint8Array/Buffer + runtime.register_binary_host_function( module_name.clone(), function_name.clone(), - move |args: String| -> anyhow::Result { - call_host_js_function(module_name.clone(), function_name.clone(), args) - .map_err(|e| anyhow!("Calling host function {module_name:?} {function_name:?} failed: {e:#?}")) + move |args_json: String, binaries: Vec| -> anyhow::Result> { + call_host_js_function( + module_name.clone(), + function_name.clone(), + args_json, + binaries, + ) + .map_err(|e| { + // Use e.message directly — {e:#?} would expand into a + // huge Debug struct that exceeds the hyperlight + // guest↔host error buffer and gets truncated. + // Include the error kind for diagnostics. + anyhow!( + "Calling host function {module_name:?} {function_name:?} failed ({:?}): {}", + e.kind, + e.message + ) + }) }, )?; } diff --git a/src/hyperlight-js/Cargo.toml b/src/hyperlight-js/Cargo.toml index f473cbc..94e2bb0 100644 --- a/src/hyperlight-js/Cargo.toml +++ b/src/hyperlight-js/Cargo.toml @@ -14,6 +14,7 @@ It is built on top of Hyperlight. # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +hyperlight-js-common = { workspace = true } anyhow = "1.0.102" fn-traits = "0.2.0" hyperlight-host = { workspace = true } diff --git a/src/hyperlight-js/src/lib.rs b/src/hyperlight-js/src/lib.rs index 916faba..2932e2a 100644 --- a/src/hyperlight-js/src/lib.rs +++ b/src/hyperlight-js/src/lib.rs @@ -27,6 +27,9 @@ mod script; pub mod sandbox; use hyperlight_host::func::HostFunction; +// Re-export FnReturn for the NAPI bridge (used in register_js signature). +#[doc(hidden)] +pub use sandbox::host_fn::FnReturn; /// A Hyperlight Sandbox with a JavaScript run time loaded but no guest code. pub use sandbox::js_sandbox::JSSandbox; /// A Hyperlight Sandbox with a JavaScript run time loaded and guest code loaded. diff --git a/src/hyperlight-js/src/sandbox/host_fn.rs b/src/hyperlight-js/src/sandbox/host_fn.rs index 46a1e97..ddab933 100644 --- a/src/hyperlight-js/src/sandbox/host_fn.rs +++ b/src/hyperlight-js/src/sandbox/host_fn.rs @@ -18,6 +18,7 @@ use std::collections::HashMap; use serde::de::DeserializeOwned; use serde::ser::SerializeSeq; use serde::Serialize; +use serde_json::Value as JsonValue; // Unlike hyperlight-host's Function, this Function trait uses `serde`'s Serialize and DeserializeOwned traits for input and output types. @@ -48,22 +49,57 @@ where } } -type BoxFunction = Box crate::Result + Send + Sync>; +type JsonFn = std::sync::Arc crate::Result + Send + Sync>; + +/// Re-export the unified return type from the common crate. +pub use hyperlight_js_common::FnReturn; + +/// The closure type for JS bridge host functions. +/// +/// Receives the parsed JSON arguments (with `{"__bin__": N}` placeholders +/// still in place) and the decoded individual binary blobs. This avoids a +/// redundant stringify→parse round-trip that would occur if we passed a +/// pre-processed JSON string. +type BinaryFn = + std::sync::Arc>) -> crate::Result + Send + Sync>; + +/// A registered host function — either typed (serde) or JS bridge. +/// +/// This enum allows a single `HashMap` to store both variants, eliminating +/// the need for parallel maps and cross-removal bookkeeping. +#[derive(Clone)] +enum HostFn { + /// Typed: receives a JSON args string, deserializes via serde, + /// returns a JSON result string. Does not support binary args. + Typed(JsonFn), + /// JS bridge: receives parsed JSON args + binary blobs, returns a + /// tagged result (JSON or binary). + JsBridge(BinaryFn), +} fn type_erased( func: impl Function + Send + Sync + 'static, -) -> BoxFunction { - Box::new(move |args: String| { +) -> JsonFn { + std::sync::Arc::new(move |args: String| { let args: Args = serde_json::from_str(&args)?; let output: Output = func.call(args); Ok(serde_json::to_string(&output)?) }) } +/// Decodes the sidecar binary format into individual blobs. +/// +/// Thin wrapper around [`hyperlight_js_common::decode_binaries`] that maps +/// the common crate's `DecodeError` into the host's `HyperlightError`. +pub(crate) fn decode_binaries(data: &[u8]) -> crate::Result>> { + hyperlight_js_common::decode_binaries(data) + .map_err(|e| crate::HyperlightError::Error(e.to_string())) +} + /// A module containing host functions that can be called from the guest JavaScript code. -#[derive(Default)] +#[derive(Default, Clone)] pub struct HostModule { - functions: HashMap, + functions: HashMap, } // The serialization of this struct has to match the deserialization in @@ -79,7 +115,18 @@ impl Serialize for HostModule { } impl HostModule { - /// Register a host function that can be called from the guest JavaScript code. + /// Register a typed host function that can be called from the guest + /// JavaScript code. + /// + /// Arguments are deserialized from JSON via serde and the return value + /// is serialized back to JSON automatically. + /// + /// This variant does **not** support `Uint8Array`/`Buffer` arguments. + /// For binary data support, use the JS bridge API instead. + /// + /// ```text + /// module.register("add", |a: i32, b: i32| a + b); + /// ``` /// /// Registering a function with the same `name` as an existing function /// overwrites the previous registration. @@ -88,32 +135,122 @@ impl HostModule { name: impl Into, func: impl Function + Send + Sync + 'static, ) -> &mut Self { - self.functions.insert(name.into(), type_erased(func)); + self.functions + .insert(name.into(), HostFn::Typed(type_erased(func))); self } - /// Register a raw host function that operates on JSON strings directly. + /// Register a host function for the JavaScript bridge (NAPI layer). /// - /// Unlike [`register`](Self::register), which handles serde serialization / - /// deserialization automatically via the [`Function`] trait, this method - /// passes the raw JSON string argument from the guest to the closure and - /// expects a JSON string result. + /// This is an internal API used by the `js-host-api` NAPI bridge. + /// Rust users should use [`register`](Self::register) instead, which + /// handles binary data transparently via serde. /// - /// This is primarily intended for dynamic / bridge scenarios (e.g. NAPI - /// bindings) where argument types are not known at compile time. - /// - /// Registering a function with the same `name` as an existing function - /// overwrites the previous registration. - pub fn register_raw( + /// The closure receives parsed `JsonValue` args and decoded binary + /// blobs directly. Return [`FnReturn::Json`] or [`FnReturn::Binary`]. + #[doc(hidden)] + pub fn register_js( &mut self, name: impl Into, - func: impl Fn(String) -> crate::Result + Send + Sync + 'static, + func: impl Fn(JsonValue, Vec>) -> crate::Result + Send + Sync + 'static, ) -> &mut Self { - self.functions.insert(name.into(), Box::new(func)); + self.functions + .insert(name.into(), HostFn::JsBridge(std::sync::Arc::new(func))); self } - pub(crate) fn get(&self, name: &str) -> Option<&BoxFunction> { - self.functions.get(name) + /// Dispatch a guest→host function call. + /// + /// Decodes the binary sidecar (if present) and routes to the + /// appropriate handler variant. + /// + /// For `Typed` functions, binary blobs in the sidecar are rejected — + /// use `register_js` for functions that need binary data. + /// + /// Always returns a tagged result: + /// - `TAG_JSON (0x00)` + JSON bytes for JSON returns + /// - `TAG_BINARY (0x01)` + raw bytes for binary returns + pub(crate) fn call( + &self, + name: &str, + args_json: String, + binaries: Option>, + ) -> crate::Result> { + let blobs = if let Some(bin_data) = binaries { + decode_binaries(&bin_data)? + } else { + Vec::new() + }; + + match self.functions.get(name) { + Some(HostFn::JsBridge(func)) => { + // JS bridge path: parse JSON and pass blobs directly. + let json_value: JsonValue = serde_json::from_str(&args_json)?; + match func(json_value, blobs)? { + FnReturn::Json(json) => Ok(hyperlight_js_common::encode_json_return(&json)), + FnReturn::Binary(bytes) => { + Ok(hyperlight_js_common::encode_binary_return(&bytes)) + } + } + } + Some(HostFn::Typed(func)) => { + // Typed path: serde deserializes args from JSON. Binary + // data is not supported — reject if blobs are present. + if !blobs.is_empty() { + return Err(crate::HyperlightError::Error(format!( + "Function '{name}' received {} binary argument(s) but was registered \ + with `register` (typed JSON-only). Use `register_js` for functions \ + that accept Uint8Array/Buffer arguments.", + blobs.len() + ))); + } + let result = func(args_json)?; + Ok(hyperlight_js_common::encode_json_return(&result)) + } + None => Err(crate::HyperlightError::Error(format!( + "Function '{}' not found", + name + ))), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn call_typed_no_binaries() { + let mut module = HostModule::default(); + module.register("add", |a: i32, b: i32| a + b); + + // count=0 sidecar + let sidecar = vec![0u8, 0, 0, 0]; + let result = module + .call("add", "[3,4]".to_string(), Some(sidecar)) + .unwrap(); + assert_eq!(result[0], hyperlight_js_common::TAG_JSON); + assert_eq!(&result[1..], b"7"); + } + + #[test] + fn call_typed_rejects_binary_args() { + let mut module = HostModule::default(); + module.register("add", |a: i32, b: i32| a + b); + + // Sidecar with one blob — typed functions should reject this + let sidecar = hyperlight_js_common::encode_binaries(&[b"ABC" as &[u8]]); + let err = module + .call("add", "[1,2]".to_string(), Some(sidecar)) + .unwrap_err(); + assert!(err.to_string().contains("binary argument")); + assert!(err.to_string().contains("register_js")); + } + + #[test] + fn call_not_found() { + let module = HostModule::default(); + let err = module.call("nope", "[]".to_string(), None).unwrap_err(); + assert!(err.to_string().contains("not found")); } } diff --git a/src/hyperlight-js/src/sandbox/proto_js_sandbox.rs b/src/hyperlight-js/src/sandbox/proto_js_sandbox.rs index 987f2db..79afa50 100644 --- a/src/hyperlight-js/src/sandbox/proto_js_sandbox.rs +++ b/src/hyperlight-js/src/sandbox/proto_js_sandbox.rs @@ -136,20 +136,20 @@ impl ProtoJSSandbox { let host_modules_json = serde_json::to_string(&host_modules)?; + // Register the host function that the guest calls for all host + // function invocations. Binary data (if any) is carried in a + // length-prefixed sidecar alongside the JSON args. self.inner.register( "CallHostJsFunction", - move |module_name: String, func_name: String, args: String| -> Result { + move |module_name: String, + func_name: String, + args_json: String, + binaries: Vec| + -> Result> { let module = host_modules .get(&module_name) .ok_or_else(|| new_error!("Host module '{}' not found", module_name))?; - let func = module.get(&func_name).ok_or_else(|| { - new_error!( - "Host function '{}' not found in module '{}'", - func_name, - module_name - ) - })?; - func(args) + module.call(&func_name, args_json, Some(binaries)) }, )?; @@ -212,27 +212,6 @@ impl ProtoJSSandbox { self.host_module(module).register(name, func); Ok(()) } - - /// Register a raw host function that operates on JSON strings directly. - /// - /// This is equivalent to calling `sbox.host_module(module).register_raw(name, func)`. - /// - /// Unlike [`register`](Self::register), which handles serde serialization / - /// deserialization automatically, this method passes the raw JSON string - /// from the guest to the callback and expects a JSON string result. - /// - /// Primarily intended for dynamic / bridge scenarios (e.g. NAPI bindings) - /// where argument types are not known at compile time. - #[instrument(err(Debug), skip(self, func), level=Level::INFO)] - pub fn register_raw( - &mut self, - module: impl Into + Debug, - name: impl Into + Debug, - func: impl Fn(String) -> Result + Send + Sync + 'static, - ) -> Result<()> { - self.host_module(module).register_raw(name, func); - Ok(()) - } } impl std::fmt::Debug for ProtoJSSandbox { diff --git a/src/hyperlight-js/tests/host_functions.rs b/src/hyperlight-js/tests/host_functions.rs index 76274f0..ddbcedc 100644 --- a/src/hyperlight-js/tests/host_functions.rs +++ b/src/hyperlight-js/tests/host_functions.rs @@ -17,7 +17,7 @@ limitations under the License. #![allow(clippy::disallowed_macros)] -use hyperlight_js::{new_error, SandboxBuilder, Script}; +use hyperlight_js::{SandboxBuilder, Script}; #[test] fn can_call_host_functions() { @@ -214,13 +214,21 @@ fn host_fn_with_unusual_names() { assert!(res == "42"); } +// ── Binary data (register_js) tests ────────────────────────────────── +// +// These test the binary sidecar round-trip through the hypervisor using +// register_js directly. register_js is #[doc(hidden)] but still pub — +// it's the foundation of the NAPI bridge and needs integration coverage. + #[test] -fn register_raw_basic() { +fn register_js_binary_arg_round_trip() { + // Guest sends Uint8Array → host receives blobs → returns length as JSON let handler = Script::from_content( r#" - import * as math from "math"; + import * as host from "host"; function handler(event) { - return { result: math.add(10, 32) }; + const data = new Uint8Array([72, 101, 108, 108, 111]); + return { len: host.byte_length(data) }; } "#, ); @@ -229,16 +237,58 @@ fn register_raw_basic() { let mut proto_js_sandbox = SandboxBuilder::new().build().unwrap(); - // register_raw receives the guest args as a JSON string "[10,32]" - // and must return a JSON string result. - proto_js_sandbox - .register_raw("math", "add", |args: String| { - let parsed: Vec = serde_json::from_str(&args)?; - let sum: i64 = parsed.iter().sum(); - Ok(serde_json::to_string(&sum)?) - }) + proto_js_sandbox.host_module("host").register_js( + "byte_length", + |_args: serde_json::Value, blobs: Vec>| { + // The first arg should be a placeholder {"__bin__": 0} + // and blobs should contain the Uint8Array bytes + let len = if let Some(blob) = blobs.first() { + blob.len() + } else { + // Fallback: try to read from the JSON args + 0 + }; + let result = serde_json::to_string(&len) + .map_err(|e| hyperlight_js::HyperlightError::Error(format!("JSON error: {e}")))?; + Ok(hyperlight_js::FnReturn::Json(result)) + }, + ); + + let mut sandbox = proto_js_sandbox.load_runtime().unwrap(); + sandbox.add_handler("handler", handler).unwrap(); + let mut loaded_sandbox = sandbox.get_loaded_sandbox().unwrap(); + + let res = loaded_sandbox + .handle_event("handler", event.to_string(), None) .unwrap(); + assert_eq!(res, r#"{"len":5}"#); +} + +#[test] +fn register_js_binary_return() { + // Host returns FnReturn::Binary → guest sees Uint8Array + let handler = Script::from_content( + r#" + import * as host from "host"; + function handler(event) { + const data = host.get_bytes(); + return { len: data.length, first: data[0], last: data[4] }; + } + "#, + ); + + let event = r#"{}"#; + + let mut proto_js_sandbox = SandboxBuilder::new().build().unwrap(); + + proto_js_sandbox.host_module("host").register_js( + "get_bytes", + |_args: serde_json::Value, _blobs: Vec>| { + Ok(hyperlight_js::FnReturn::Binary(vec![10, 20, 30, 40, 50])) + }, + ); + let mut sandbox = proto_js_sandbox.load_runtime().unwrap(); sandbox.add_handler("handler", handler).unwrap(); let mut loaded_sandbox = sandbox.get_loaded_sandbox().unwrap(); @@ -247,18 +297,18 @@ fn register_raw_basic() { .handle_event("handler", event.to_string(), None) .unwrap(); - assert_eq!(res, r#"{"result":42}"#); + assert_eq!(res, r#"{"len":5,"first":10,"last":50}"#); } #[test] -fn register_raw_mixed_with_typed() { +fn register_js_mixed_args() { + // Guest sends string + Uint8Array + number → host receives all correctly let handler = Script::from_content( r#" - import * as math from "math"; + import * as host from "host"; function handler(event) { - let sum = math.add(10, 32); - let doubled = math.double(sum); - return { result: doubled }; + const data = new Uint8Array([1, 2, 3]); + return { result: host.describe("pfx", data, 42) }; } "#, ); @@ -267,18 +317,64 @@ fn register_raw_mixed_with_typed() { let mut proto_js_sandbox = SandboxBuilder::new().build().unwrap(); - // Typed registration via the Function trait - proto_js_sandbox - .register("math", "add", |a: i32, b: i32| a + b) + proto_js_sandbox.host_module("host").register_js( + "describe", + |args: serde_json::Value, blobs: Vec>| { + // args is [{"__bin__": 0}, "pfx", 42] or ["pfx", {"__bin__": 0}, 42] + // depending on arg order. Extract what we need. + let arr = args.as_array().unwrap(); + let mut prefix = String::new(); + let mut num = 0i64; + let blob_len = blobs.first().map(|b| b.len()).unwrap_or(0); + + for val in arr { + if let Some(s) = val.as_str() { + prefix = s.to_string(); + } else if let Some(n) = val.as_i64() { + num = n; + } + } + + let result = format!("{prefix}-{blob_len}-{num}"); + let json = serde_json::to_string(&result) + .map_err(|e| hyperlight_js::HyperlightError::Error(format!("JSON error: {e}")))?; + Ok(hyperlight_js::FnReturn::Json(json)) + }, + ); + + let mut sandbox = proto_js_sandbox.load_runtime().unwrap(); + sandbox.add_handler("handler", handler).unwrap(); + let mut loaded_sandbox = sandbox.get_loaded_sandbox().unwrap(); + + let res = loaded_sandbox + .handle_event("handler", event.to_string(), None) .unwrap(); - // Raw registration alongside typed — both in the same module + assert_eq!(res, r#"{"result":"pfx-3-42"}"#); +} + +#[test] +fn register_typed_rejects_binary_args_e2e() { + // Guest sends Uint8Array to a typed register() function — should error + let handler = Script::from_content( + r#" + import * as host from "host"; + function handler(event) { + try { + host.add(new Uint8Array([1, 2]), 3); + return { error: "should have thrown" }; + } catch (e) { + return { caught: true }; + } + } + "#, + ); + + let event = r#"{}"#; + + let mut proto_js_sandbox = SandboxBuilder::new().build().unwrap(); proto_js_sandbox - .register_raw("math", "double", |args: String| { - let parsed: Vec = serde_json::from_str(&args)?; - let val = parsed.first().copied().unwrap_or(0); - Ok(serde_json::to_string(&(val * 2))?) - }) + .register("host", "add", |a: i32, b: i32| a + b) .unwrap(); let mut sandbox = proto_js_sandbox.load_runtime().unwrap(); @@ -289,16 +385,19 @@ fn register_raw_mixed_with_typed() { .handle_event("handler", event.to_string(), None) .unwrap(); - assert_eq!(res, r#"{"result":84}"#); + // The guest should catch the error from the typed function rejecting binary + assert_eq!(res, r#"{"caught":true}"#); } #[test] -fn register_raw_error_propagation() { +fn register_js_empty_uint8array() { + // Guest sends empty Uint8Array — should work, blobs[0] is empty vec let handler = Script::from_content( r#" import * as host from "host"; function handler(event) { - return host.fail(); + const data = new Uint8Array(0); + return { len: host.byte_length(data) }; } "#, ); @@ -307,31 +406,79 @@ fn register_raw_error_propagation() { let mut proto_js_sandbox = SandboxBuilder::new().build().unwrap(); - proto_js_sandbox - .register_raw("host", "fail", |_args: String| { - Err(new_error!("intentional failure from raw host fn")) - }) + proto_js_sandbox.host_module("host").register_js( + "byte_length", + |_args: serde_json::Value, blobs: Vec>| { + let len = blobs.first().map(|b| b.len()).unwrap_or(0); + let result = serde_json::to_string(&len) + .map_err(|e| hyperlight_js::HyperlightError::Error(format!("{e}")))?; + Ok(hyperlight_js::FnReturn::Json(result)) + }, + ); + + let mut sandbox = proto_js_sandbox.load_runtime().unwrap(); + sandbox.add_handler("handler", handler).unwrap(); + let mut loaded_sandbox = sandbox.get_loaded_sandbox().unwrap(); + + let res = loaded_sandbox + .handle_event("handler", event.to_string(), None) .unwrap(); + assert_eq!(res, r#"{"len":0}"#); +} + +#[test] +fn register_js_multiple_binary_args() { + // Guest sends two separate Uint8Arrays as args + let handler = Script::from_content( + r#" + import * as host from "host"; + function handler(event) { + const a = new Uint8Array([1, 2, 3]); + const b = new Uint8Array([4, 5]); + return { total: host.total_length(a, b) }; + } + "#, + ); + + let event = r#"{}"#; + + let mut proto_js_sandbox = SandboxBuilder::new().build().unwrap(); + + proto_js_sandbox.host_module("host").register_js( + "total_length", + |_args: serde_json::Value, blobs: Vec>| { + let total: usize = blobs.iter().map(|b| b.len()).sum(); + let result = serde_json::to_string(&total) + .map_err(|e| hyperlight_js::HyperlightError::Error(format!("{e}")))?; + Ok(hyperlight_js::FnReturn::Json(result)) + }, + ); + let mut sandbox = proto_js_sandbox.load_runtime().unwrap(); sandbox.add_handler("handler", handler).unwrap(); let mut loaded_sandbox = sandbox.get_loaded_sandbox().unwrap(); - let err = loaded_sandbox + let res = loaded_sandbox .handle_event("handler", event.to_string(), None) - .unwrap_err(); + .unwrap(); - assert!(err.to_string().contains("intentional failure")); + assert_eq!(res, r#"{"total":5}"#); } #[test] -fn register_raw_via_host_module() { +fn register_js_binary_in_nested_object() { + // Guest sends an object containing a Uint8Array as a property let handler = Script::from_content( r#" - import * as utils from "utils"; + import * as host from "host"; function handler(event) { - let greeting = utils.greet("World"); - return { greeting }; + const payload = { + name: "test", + data: new Uint8Array([10, 20, 30]), + count: 3, + }; + return { result: host.process(payload) }; } "#, ); @@ -340,14 +487,21 @@ fn register_raw_via_host_module() { let mut proto_js_sandbox = SandboxBuilder::new().build().unwrap(); - // Use host_module() accessor + register_raw() directly on HostModule - proto_js_sandbox - .host_module("utils") - .register_raw("greet", |args: String| { - let parsed: Vec = serde_json::from_str(&args)?; - let name = parsed.first().cloned().unwrap_or_default(); - Ok(serde_json::to_string(&format!("Hello, {}!", name))?) - }); + proto_js_sandbox.host_module("host").register_js( + "process", + |args: serde_json::Value, blobs: Vec>| { + // The object should have {"name": "test", "data": {"__bin__": 0}, "count": 3} + // with blobs containing [10, 20, 30] + let arr = args.as_array().unwrap(); + let obj = arr[0].as_object().unwrap(); + let name = obj.get("name").unwrap().as_str().unwrap(); + let blob_len = blobs.first().map(|b| b.len()).unwrap_or(0); + let result = format!("{name}-{blob_len}"); + let json = serde_json::to_string(&result) + .map_err(|e| hyperlight_js::HyperlightError::Error(format!("{e}")))?; + Ok(hyperlight_js::FnReturn::Json(json)) + }, + ); let mut sandbox = proto_js_sandbox.load_runtime().unwrap(); sandbox.add_handler("handler", handler).unwrap(); @@ -357,5 +511,197 @@ fn register_raw_via_host_module() { .handle_event("handler", event.to_string(), None) .unwrap(); - assert_eq!(res, r#"{"greeting":"Hello, World!"}"#); + assert_eq!(res, r#"{"result":"test-3"}"#); +} + +// ── Numeric type tests ─────────────────────────────────────────────── +// QuickJS stores JSON-parsed numbers as doubles internally. The binary +// host function path (extract_binaries → value_to_json_with_binaries) +// must serialize whole-number floats as integers to preserve serde +// deserialization on the host side. + +#[test] +fn host_fn_with_i32_arg_from_event_data() { + // event.x is parsed from JSON → stored as f64 in QuickJS → must + // arrive at the host as an integer, not 42.0 + let handler = Script::from_content( + r#" + import * as math from "math"; + function handler(event) { + return { result: math.double(event.x) }; + } + "#, + ); + + let mut proto = SandboxBuilder::new().build().unwrap(); + proto.register("math", "double", |x: i32| x * 2).unwrap(); + + let mut sandbox = proto.load_runtime().unwrap(); + sandbox.add_handler("handler", handler).unwrap(); + let mut loaded = sandbox.get_loaded_sandbox().unwrap(); + + let res = loaded + .handle_event("handler", r#"{"x": 42}"#.to_string(), None) + .unwrap(); + assert_eq!(res, r#"{"result":84}"#); +} + +#[test] +fn host_fn_with_i64_arg_from_event_data() { + let handler = Script::from_content( + r#" + import * as math from "math"; + function handler(event) { + return { result: math.negate(event.x) }; + } + "#, + ); + + let mut proto = SandboxBuilder::new().build().unwrap(); + proto.register("math", "negate", |x: i64| -x).unwrap(); + + let mut sandbox = proto.load_runtime().unwrap(); + sandbox.add_handler("handler", handler).unwrap(); + let mut loaded = sandbox.get_loaded_sandbox().unwrap(); + + let res = loaded + .handle_event("handler", r#"{"x": 100}"#.to_string(), None) + .unwrap(); + assert_eq!(res, r#"{"result":-100}"#); +} + +#[test] +fn host_fn_with_f64_arg_preserves_fractional() { + // Actual floats (3.14) must remain as floats, not be truncated + let handler = Script::from_content( + r#" + import * as math from "math"; + function handler(event) { + return { result: math.half(event.x) }; + } + "#, + ); + + let mut proto = SandboxBuilder::new().build().unwrap(); + proto.register("math", "half", |x: f64| x / 2.0).unwrap(); + + let mut sandbox = proto.load_runtime().unwrap(); + sandbox.add_handler("handler", handler).unwrap(); + let mut loaded = sandbox.get_loaded_sandbox().unwrap(); + + let res = loaded + .handle_event("handler", r#"{"x": 3.14}"#.to_string(), None) + .unwrap(); + + let json: serde_json::Value = serde_json::from_str(&res).unwrap(); + let result = json["result"].as_f64().unwrap(); + assert!( + (result - 1.57).abs() < 0.001, + "Expected ~1.57, got {result}" + ); +} + +#[test] +fn host_fn_with_bool_arg() { + let handler = Script::from_content( + r#" + import * as logic from "logic"; + function handler(event) { + return { result: logic.flip(event.flag) }; + } + "#, + ); + + let mut proto = SandboxBuilder::new().build().unwrap(); + proto.register("logic", "flip", |b: bool| !b).unwrap(); + + let mut sandbox = proto.load_runtime().unwrap(); + sandbox.add_handler("handler", handler).unwrap(); + let mut loaded = sandbox.get_loaded_sandbox().unwrap(); + + let res = loaded + .handle_event("handler", r#"{"flag": true}"#.to_string(), None) + .unwrap(); + assert_eq!(res, r#"{"result":false}"#); +} + +#[test] +fn host_fn_with_mixed_numeric_types() { + // i32 + f64 mix in the same call + let handler = Script::from_content( + r#" + import * as math from "math"; + function handler(event) { + return { result: math.weighted_add(event.a, event.b, event.weight) }; + } + "#, + ); + + let mut proto = SandboxBuilder::new().build().unwrap(); + proto + .register("math", "weighted_add", |a: i32, b: i32, w: f64| { + (a as f64 * w + b as f64 * (1.0 - w)) as i32 + }) + .unwrap(); + + let mut sandbox = proto.load_runtime().unwrap(); + sandbox.add_handler("handler", handler).unwrap(); + let mut loaded = sandbox.get_loaded_sandbox().unwrap(); + + let res = loaded + .handle_event( + "handler", + r#"{"a": 100, "b": 200, "weight": 0.75}"#.to_string(), + None, + ) + .unwrap(); + assert_eq!(res, r#"{"result":125}"#); +} + +#[test] +fn host_fn_with_negative_integer() { + let handler = Script::from_content( + r#" + import * as math from "math"; + function handler(event) { + return { result: math.abs(event.x) }; + } + "#, + ); + + let mut proto = SandboxBuilder::new().build().unwrap(); + proto.register("math", "abs", |x: i32| x.abs()).unwrap(); + + let mut sandbox = proto.load_runtime().unwrap(); + sandbox.add_handler("handler", handler).unwrap(); + let mut loaded = sandbox.get_loaded_sandbox().unwrap(); + + let res = loaded + .handle_event("handler", r#"{"x": -42}"#.to_string(), None) + .unwrap(); + assert_eq!(res, r#"{"result":42}"#); +} + +#[test] +fn host_fn_with_zero() { + let handler = Script::from_content( + r#" + import * as math from "math"; + function handler(event) { + return { result: math.inc(event.x) }; + } + "#, + ); + + let mut proto = SandboxBuilder::new().build().unwrap(); + proto.register("math", "inc", |x: i32| x + 1).unwrap(); + + let mut sandbox = proto.load_runtime().unwrap(); + sandbox.add_handler("handler", handler).unwrap(); + let mut loaded = sandbox.get_loaded_sandbox().unwrap(); + + let res = loaded + .handle_event("handler", r#"{"x": 0}"#.to_string(), None) + .unwrap(); + assert_eq!(res, r#"{"result":1}"#); } diff --git a/src/js-host-api/Cargo.toml b/src/js-host-api/Cargo.toml index 9232a04..754e7ed 100644 --- a/src/js-host-api/Cargo.toml +++ b/src/js-host-api/Cargo.toml @@ -13,6 +13,7 @@ crate-type = ["cdylib"] [dependencies] hyperlight-js = { workspace = true, features = ["monitor-wall-clock", "monitor-cpu-time"] } +hyperlight-js-common = { workspace = true } napi = { version = "3.8", features = ["tokio_rt", "serde-json"] } napi-derive = "3.5" serde_json = "1" diff --git a/src/js-host-api/README.md b/src/js-host-api/README.md index d3c2079..7f04fb8 100644 --- a/src/js-host-api/README.md +++ b/src/js-host-api/README.md @@ -285,15 +285,15 @@ sequenceDiagram Note over Guest,Host: Registration (before loadRuntime) Host->>Bridge: proto.hostModule('math').register('add', callback) - Bridge->>HL: register_raw('math', 'add', closure) + Bridge->>HL: Stores closure in HostModule Note over Guest,Host: Invocation (during callHandler) Guest->>HL: math.add(1, 2) - HL->>Bridge: closure("[1,2]") - Bridge->>Host: callback("[1,2]") + HL->>Bridge: Dispatch args + optional binary sidecar + Bridge->>Host: callback(1, 2) Note right of Host: sync: return immediately
async: await Promise - Host-->>Bridge: "3" - Bridge-->>HL: Ok("3") + Host-->>Bridge: 3 + Bridge-->>HL: Tagged result HL-->>Guest: 3 ``` @@ -327,12 +327,15 @@ const result = await loaded.callHandler('handler', { a: 10, b: 32 }); console.log(result); // { result: 42 } ``` -### The JSON Wire Protocol +### The Wire Protocol -All arguments and return values cross the sandbox boundary as **JSON strings**. +Arguments and return values cross the sandbox boundary as **JSON strings**, +except binary data (`Uint8Array`/`Buffer`) which are carried separately in a +raw **binary sidecar**. With `register()`, this is handled automatically — your callback receives individual arguments (parsed from the JSON array) and the return value is -automatically `JSON.stringify`'d. +automatically `JSON.stringify`'d. Top-level `Buffer` arguments and returns +are passed as raw bytes via the sidecar channel. ```javascript // Guest calls: math.add(1, 2) @@ -340,6 +343,11 @@ automatically `JSON.stringify`'d. // Your return value: 3 — automatically stringified to '3' math.register('add', (a, b) => a + b); +// Guest calls: crypto.hash(new Uint8Array([1,2,3])) +// Your callback receives: (Buffer) — a native Node.js Buffer +// Return a Buffer → becomes Uint8Array on guest side +crypto.register('hash', (data) => createHash('sha256').update(data).digest()); + // Guest calls: db.query("users") // Your callback receives: ("users") — the single string arg // Your return value is automatically stringified @@ -367,9 +375,9 @@ const math = proto.hostModule('math'); Throws `ERR_INVALID_ARG` if name is empty. -#### `builder.register(name, callback)` → `HostModule` +#### `builder.register(name, callback)` → `void` -Registers a function within the module. Returns the builder for chaining. +Registers a function within the module. Arguments are auto-parsed from the guest's JSON array and spread into your callback. The return value is automatically `JSON.stringify`'d. @@ -422,6 +430,65 @@ Node.js code. > Node.js main thread and waits for the result via a oneshot channel. This > allows both sync and async JS callbacks to work transparently. +### Binary Data (Buffers) + +Host functions natively support `Uint8Array`/`Buffer` arguments and returns. +Binary data travels through a dedicated sidecar channel, keeping overhead +minimal. Top-level `Buffer` arguments and returns are passed as raw bytes +with no encoding. Nested Buffers inside returned objects/arrays are +serialized by napi-rs's default conversion. + +```javascript +const { SandboxBuilder } = require('@hyperlight/js-host-api'); +const { createHash } = require('crypto'); +const zlib = require('zlib'); + +const proto = await new SandboxBuilder().build(); + +// Buffer arguments: guest Uint8Array → host Buffer +proto.hostModule('crypto').register('sha256', (data) => { + // data is a Node.js Buffer + return createHash('sha256').update(data).digest(); // returns Buffer +}); + +// Mixed args: regular values and Buffers together +proto.hostModule('io').register('compress', (algorithm, data) => { + // algorithm is a string, data is a Buffer + if (algorithm === 'gzip') return zlib.gzipSync(data); + return data; +}); + +const sandbox = await proto.loadRuntime(); +sandbox.addHandler('handler', ` + import * as crypto from "host:crypto"; + import * as io from "host:io"; + function handler(event) { + const data = new Uint8Array([72, 101, 108, 108, 111]); + const hash = crypto.sha256(data); // Uint8Array + const compressed = io.compress('gzip', data); // Uint8Array + return { hashLen: hash.length, compLen: compressed.length }; + } +`); + +const loaded = await sandbox.getLoadedSandbox(); +const result = await loaded.callHandler('handler', {}); +``` + +**How it works under the hood:** + +1. Guest `Uint8Array` args are extracted from the QuickJS VM and packed + into a length-prefixed binary sidecar alongside JSON placeholders +2. The sidecar crosses the hypervisor boundary as raw bytes (no encoding) +3. On the host side, placeholders are replaced with native Node.js `Buffer` + objects via the NAPI API — your callback receives real Buffers +4. `Buffer` returns are detected natively and sent back through the + sidecar channel, arriving as `Uint8Array` on the guest side + +> **Note:** If your callback returns an object/array containing nested +> Buffers (e.g. `{ data: Buffer.from([1,2,3]) }`), the nested Buffers +> will be serialized by napi-rs's default conversion (not as raw bytes). +> For best performance, return Buffers as top-level values. + ### Error Handling If your callback throws (sync) or rejects (async), the error propagates @@ -523,8 +590,10 @@ spawn_blocking thread Node.js main thread 7. block_on(receiver) gets the Promise 8. await Promise -9. JSON stringify result -10. Return to guest +9. Return result to guest + (Buffer → binary tag, + other → JSON stringify) +10. Guest receives value ``` This design: diff --git a/src/js-host-api/lib.js b/src/js-host-api/lib.js index 6a82443..fc096e5 100644 --- a/src/js-host-api/lib.js +++ b/src/js-host-api/lib.js @@ -180,19 +180,81 @@ ProtoJSSandbox.prototype.hostModule = wrapSync(ProtoJSSandbox.prototype.hostModu }); } -// HostModule — register() +// HostModule — register() with Buffer support { const origRegister = HostModule.prototype.register; if (!origRegister) throw new Error('Cannot wrap missing method: HostModule.register'); HostModule.prototype.register = wrapSync(function (name, callback) { - // the rust code expects the host function to return a Promise, so we wrap the callback result in Promise.resolve().then(..) to allow sync functions as well - // note that Promise.resolve(callback(...args)) would not work because if callback throws that would not return a rejected promise, it would just throw before returning the promise. + // Wrap the callback to handle Buffer returns. + // Args: Rust now creates native Buffer objects directly via the + // NAPI C API — no conversion needed on the JS side. + // Returns: Top-level Buffer/Uint8Array returns are passed through + // to Rust where napi_is_buffer detects them natively. + // Nested Buffers in objects/arrays must still be converted + // to __buffer__ markers for JSON transport. return origRegister.call(this, name, (...args) => - Promise.resolve().then(() => callback(...args)) + Promise.resolve() + .then(() => callback(...args)) + .then((result) => { + // Top-level Buffer/Uint8Array: ensure it's a Buffer + // so Rust's napi_is_buffer detects it (plain Uint8Array + // is not detected by napi_is_buffer). + if (Buffer.isBuffer(result) || result instanceof Uint8Array) { + return Buffer.from(result); + } + // Non-Buffer: convert any nested Buffers to markers + return convertResultBuffers(result); + }) ); }); } +/** + * Recursively converts Buffer objects to `{__buffer__: "base64..."}` markers. + * + * Only used for **nested** Buffers inside objects/arrays returned by host + * callbacks. Top-level Buffer returns are detected natively by Rust via + * `napi_is_buffer` and never hit this path. + * + * Limitations: + * - Circular references will cause a stack overflow (host callbacks should + * not return circular structures — JSON.stringify would fail anyway). + * - Non-plain objects (Date, RegExp, etc.) are iterated as key/value pairs, + * which may produce unexpected results. Return plain objects for best results. + * + * @param {any} value - The value to convert + * @returns {any} The converted value with markers + */ +const MAX_RESULT_DEPTH = 64; + +function convertResultBuffers(value, depth = 0) { + if (depth > MAX_RESULT_DEPTH) { + throw new Error(`Nested result depth exceeds maximum (${MAX_RESULT_DEPTH})`); + } + if (value === null || value === undefined) { + return value; + } + // Check for Buffer (or Uint8Array) + if (Buffer.isBuffer(value) || value instanceof Uint8Array) { + return { __buffer__: Buffer.from(value).toString('base64') }; + } + if (typeof value === 'object') { + // Recursively process arrays + if (Array.isArray(value)) { + return value.map((v) => convertResultBuffers(v, depth + 1)); + } + // Use null-prototype object to prevent prototype pollution — + // the result is immediately JSON-serialized so the lack of + // hasOwnProperty/toString is not an issue. + const result = Object.create(null); + for (const [key, val] of Object.entries(value)) { + result[key] = convertResultBuffers(val, depth + 1); + } + return result; + } + return value; +} + // SandboxBuilder — async build + sync setters SandboxBuilder.prototype.build = wrapAsync(SandboxBuilder.prototype.build); diff --git a/src/js-host-api/src/lib.rs b/src/js-host-api/src/lib.rs index 8c4caa9..1ec164b 100644 --- a/src/js-host-api/src/lib.rs +++ b/src/js-host-api/src/lib.rs @@ -21,7 +21,7 @@ use hyperlight_js::{ CpuTimeMonitor, HyperlightError, InterruptHandle, JSSandbox, LoadedJSSandbox, ProtoJSSandbox, SandboxBuilder, Script, Snapshot, WallClockMonitor, }; -use napi::bindgen_prelude::{JsValuesTupleIntoVec, Promise, ToNapiValue}; +use napi::bindgen_prelude::{FromNapiValue, JsValuesTupleIntoVec, Promise, ToNapiValue}; use napi::sys::{napi_env, napi_value}; use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; use napi::{tokio, Status}; @@ -511,9 +511,9 @@ impl ProtoJSSandboxWrapper { module_name: String, function_name: String, func: ThreadsafeFunction< - Rest>, - Promise>, - Rest>, + Rest>, + Promise>, + Rest>, Status, false, true, @@ -537,6 +537,242 @@ impl JsValuesTupleIntoVec for Rest { } } +// ── Native Buffer marshalling ──────────────────────────────────────── +// +// These types eliminate the base64 round-trip that previously occurred +// when passing binary data between Rust and JS callbacks. Instead of +// encoding blobs as `{"__buffer__": "base64..."}` markers in +// `serde_json::Value`, we now create/detect native Node.js `Buffer` +// objects directly via the NAPI C API. + +/// A JS argument value that can contain native Buffers in place of +/// `{"__bin__": N}` placeholder objects. +/// +/// When napi-rs calls `ToNapiValue` to convert this into a JS value, +/// the recursive converter walks the JSON tree and creates real Buffer +/// objects at placeholder positions — no base64 encoding needed. +pub struct JsArg { + /// The JSON value tree, potentially containing `{"__bin__": N}` placeholders. + value: serde_json::Value, + /// Shared reference to the decoded binary blobs. Placeholders index + /// into this Vec. + blobs: Arc>>, +} + +impl ToNapiValue for JsArg { + /// # Safety + /// + /// Must be called on the JS thread with a valid `napi_env`. + unsafe fn to_napi_value(env: napi_env, val: Self) -> napi::Result { + if val.blobs.is_empty() { + // Fast path: no binary data — delegate entirely to napi-rs's + // built-in serde_json conversion (avoids the recursive walk). + // SAFETY: env is valid, val.value is a valid serde_json::Value. + return unsafe { serde_json::Value::to_napi_value(env, val.value) }; + } + // SAFETY: env is valid, blobs contains valid byte slices. + unsafe { json_to_napi_with_buffers(env, val.value, &val.blobs, 0) } + } +} + +/// A JS return value that may be a native Buffer. +/// +/// When napi-rs converts the JS callback's return value, we check for +/// Buffer first (via `napi_is_buffer`) before falling back to the +/// standard `serde_json::Value` conversion. +pub enum JsReturn { + /// Regular JSON-serializable value. + Value(serde_json::Value), + /// Native Buffer — extracted directly, no base64 decoding. + Buffer(Vec), +} + +impl FromNapiValue for JsReturn { + /// # Safety + /// + /// Must be called on the JS thread with a valid `napi_env` and `napi_value`. + unsafe fn from_napi_value(env: napi_env, val: napi_value) -> napi::Result { + // Check for Buffer first — this is a fast C-level type check. + let mut is_buffer = false; + // SAFETY: env and val are valid napi handles. + let status = unsafe { napi::sys::napi_is_buffer(env, val, &mut is_buffer) }; + if status != napi::sys::Status::napi_ok { + return Err(napi::Error::new( + napi::Status::from(status), + "Failed to check buffer type", + )); + } + if is_buffer { + let mut data = std::ptr::null_mut(); + let mut len = 0; + // SAFETY: env and val are valid, val is confirmed to be a buffer. + let status = unsafe { napi::sys::napi_get_buffer_info(env, val, &mut data, &mut len) }; + if status != napi::sys::Status::napi_ok { + return Err(napi::Error::new( + napi::Status::from(status), + "Failed to get buffer info", + )); + } + // Handle empty buffers: napi_get_buffer_info returns data=null, len=0 + // for empty buffers. slice::from_raw_parts requires non-null pointer + // even for zero-length slices, so we handle this case specially. + if len == 0 { + return Ok(JsReturn::Buffer(Vec::new())); + } + // Non-empty buffer: data must be valid and non-null. + // If it's null with len > 0, the buffer's backing store was likely + // garbage collected (e.g., a Buffer.subarray view whose parent died). + if data.is_null() { + return Err(napi::Error::from_reason( + "Buffer has null data pointer with non-zero length - backing store may have been garbage collected" + )); + } + // SAFETY: data points to len bytes of valid buffer memory. + let bytes = unsafe { std::slice::from_raw_parts(data as *const u8, len) }.to_vec(); + return Ok(JsReturn::Buffer(bytes)); + } + + // Not a buffer — fall through to standard JSON conversion. + // SAFETY: env and val are valid napi handles. + let value = unsafe { serde_json::Value::from_napi_value(env, val)? }; + Ok(JsReturn::Value(value)) + } +} + +/// Recursively converts a `serde_json::Value` into a `napi_value`, +/// replacing `{"__bin__": N}` placeholders with native Node.js Buffers. +/// +/// Non-container values (strings, numbers, booleans, null) are delegated +/// to napi-rs's built-in `serde_json::Value` → JS conversion. +/// +/// # Safety +/// +/// Caller must ensure `env` is a valid napi environment. +unsafe fn json_to_napi_with_buffers( + env: napi_env, + value: serde_json::Value, + blobs: &[Vec], + depth: usize, +) -> napi::Result { + use hyperlight_js_common::PLACEHOLDER_BIN; + + if depth > hyperlight_js_common::MAX_JSON_DEPTH { + return Err(napi::Error::from_reason(format!( + "JSON nesting depth exceeds maximum ({})", + hyperlight_js_common::MAX_JSON_DEPTH + ))); + } + + match value { + serde_json::Value::Object(obj) => { + // Check for __bin__ placeholder: {"__bin__": N} + if obj.len() == 1 + && let Some(serde_json::Value::Number(n)) = obj.get(PLACEHOLDER_BIN) + && let Some(idx) = n.as_u64() + { + let idx = idx as usize; + if idx < blobs.len() { + // SAFETY: env is valid, blobs[idx] is a valid byte slice. + return unsafe { create_napi_buffer(env, &blobs[idx]) }; + } + return Err(napi::Error::from_reason(format!( + "Binary placeholder index {idx} out of bounds (have {} blobs)", + blobs.len() + ))); + } + + // Regular object — recursively convert properties + let mut js_obj: napi_value = std::ptr::null_mut(); + // SAFETY: env is valid. + let status = unsafe { napi::sys::napi_create_object(env, &mut js_obj) }; + if status != napi::sys::Status::napi_ok { + return Err(napi::Error::new( + napi::Status::from(status), + "Failed to create JS object", + )); + } + + for (key, val) in obj { + // SAFETY: env is valid, recursive call maintains invariants. + let js_val = unsafe { json_to_napi_with_buffers(env, val, blobs, depth + 1)? }; + let c_key = std::ffi::CString::new(key) + .map_err(|_| napi::Error::from_reason("Object key contains null byte"))?; + // SAFETY: env, js_obj, c_key, js_val are all valid. + let status = unsafe { + napi::sys::napi_set_named_property(env, js_obj, c_key.as_ptr(), js_val) + }; + if status != napi::sys::Status::napi_ok { + return Err(napi::Error::new( + napi::Status::from(status), + "Failed to set object property", + )); + } + } + Ok(js_obj) + } + serde_json::Value::Array(arr) => { + let len = arr.len(); + if len > u32::MAX as usize { + return Err(napi::Error::from_reason(format!( + "Array length {len} exceeds u32::MAX" + ))); + } + let mut js_arr: napi_value = std::ptr::null_mut(); + // SAFETY: env is valid. + let status = unsafe { napi::sys::napi_create_array_with_length(env, len, &mut js_arr) }; + if status != napi::sys::Status::napi_ok { + return Err(napi::Error::new( + napi::Status::from(status), + "Failed to create JS array", + )); + } + + for (i, val) in arr.into_iter().enumerate() { + // SAFETY: env is valid, recursive call maintains invariants. + let js_val = unsafe { json_to_napi_with_buffers(env, val, blobs, depth + 1)? }; + // SAFETY: env, js_arr, js_val are valid; i is in bounds. + let status = unsafe { napi::sys::napi_set_element(env, js_arr, i as u32, js_val) }; + if status != napi::sys::Status::napi_ok { + return Err(napi::Error::new( + napi::Status::from(status), + "Failed to set array element", + )); + } + } + Ok(js_arr) + } + // Non-container values can't contain placeholders — delegate to napi-rs + // SAFETY: env is valid, other is a valid serde_json::Value. + other => unsafe { serde_json::Value::to_napi_value(env, other) }, + } +} + +/// Creates a native Node.js Buffer by copying raw bytes into V8's heap. +/// +/// # Safety +/// +/// Caller must ensure `env` is a valid napi environment. +unsafe fn create_napi_buffer(env: napi_env, data: &[u8]) -> napi::Result { + let mut buf: napi_value = std::ptr::null_mut(); + // SAFETY: env is valid, data is a valid byte slice. + let status = unsafe { + napi::sys::napi_create_buffer_copy( + env, + data.len(), + data.as_ptr().cast(), + std::ptr::null_mut(), // we don't need the result_data pointer + &mut buf, + ) + }; + if status != napi::sys::Status::napi_ok { + return Err(napi::Error::new( + napi::Status::from(status), + "Failed to create Buffer", + )); + } + Ok(buf) +} + // ── HostModule ─────────────────────────────────────────────────────── /// A builder for registering host functions in a named module. @@ -583,6 +819,11 @@ impl HostModuleWrapper { /// Both sync and async callbacks are supported — if the callback /// returns a `Promise`, the bridge awaits it automatically. /// + /// **Binary data support**: `Uint8Array`/`Buffer` arguments from guest + /// code are automatically converted to Node.js `Buffer` objects before + /// being passed to your callback. If your callback returns a `Buffer`, + /// it will be converted back to a `Uint8Array` on the guest side. + /// /// Registering a function with the same name as an existing one in /// this module overwrites the previous registration. /// @@ -597,6 +838,12 @@ impl HostModuleWrapper { /// const res = await fetch(url); /// return res.json(); /// }); + /// + /// // Binary data — Buffer args/returns work natively + /// math.register('compress', (data) => { + /// // data is a Buffer if guest passed Uint8Array + /// return zlib.gzipSync(data); // Return Buffer → Uint8Array on guest + /// }); /// ``` /// /// @param name - Function name within the module (must be non-empty) @@ -608,9 +855,9 @@ impl HostModuleWrapper { &self, name: String, func: ThreadsafeFunction< - Rest>, - Promise>, - Rest>, + Rest>, + Promise>, + Rest>, Status, false, true, @@ -619,14 +866,46 @@ impl HostModuleWrapper { if name.is_empty() { return Err(invalid_arg_error("Function name must not be empty")); } - let wrapper = move |args: String| -> hyperlight_js::Result { + + // Use binary-capable registration to support Buffer arguments. + // The closure receives parsed JsonValue args (with {"__bin__": N} + // placeholders) and decoded binary blobs. JsArg's ToNapiValue + // impl converts placeholders directly to native Node.js Buffers + // via the NAPI API — no base64 encoding needed. + let wrapper = move |args: serde_json::Value, + blobs: Vec>| + -> hyperlight_js::Result { + use hyperlight_js::FnReturn; use ThreadsafeFunctionCallMode::NonBlocking; - let args: Vec> = serde_json::from_str(&args)?; + + let blobs = Arc::new(blobs); + + // Spread the JSON array into individual JsArg values. + // Each JsArg carries a reference to the blobs so its + // ToNapiValue impl can resolve __bin__ placeholders at + // any nesting depth. + let js_args: Vec> = match args { + JsonValue::Array(arr) => arr + .into_iter() + .map(|v| { + Some(JsArg { + value: v, + blobs: blobs.clone(), + }) + }) + .collect(), + other => vec![Some(JsArg { + value: other, + blobs: blobs.clone(), + })], + }; + let (tx, rx) = oneshot::channel(); - let status = func.call_with_return_value(Rest(args), NonBlocking, move |result, _| { - let _ = tx.send(result); - Ok(()) - }); + let status = + func.call_with_return_value(Rest(js_args), NonBlocking, move |result, _| { + let _ = tx.send(result); + Ok(()) + }); if status != Status::Ok { return Err(HyperlightError::Error(format!( "Host function call failed: {status:?}" @@ -642,14 +921,22 @@ impl HostModuleWrapper { .await .map_err(|err| HyperlightError::Error(format!("{err}")))?; - let value = serde_json::to_string(&value)?; - Ok(value) + // JsReturn discriminates Buffer from JSON natively via + // napi_is_buffer — no base64 markers to hunt for. + match value { + Some(JsReturn::Buffer(bytes)) => Ok(FnReturn::Binary(bytes)), + Some(JsReturn::Value(v)) => { + let json = serde_json::to_string(&v)?; + Ok(FnReturn::Json(json)) + } + None => Ok(FnReturn::Json("null".into())), + } }) }; self.sandbox.with_inner_mut(|sandbox| { sandbox .host_module(&self.module_name) - .register_raw(name, wrapper); + .register_js(name, wrapper); Ok(()) })?; Ok(()) diff --git a/src/js-host-api/tests/host-functions.test.js b/src/js-host-api/tests/host-functions.test.js index 9626610..31dfe35 100644 --- a/src/js-host-api/tests/host-functions.test.js +++ b/src/js-host-api/tests/host-functions.test.js @@ -543,3 +543,137 @@ describe('Multi-sandbox isolation', () => { expect(resultB).toEqual({ sum: 21, product: 10 }); }); }); + +// ── Binary data (Buffer/Uint8Array) ────────────────────────────────── + +describe('Binary data support', () => { + it('should pass Buffer args from guest Uint8Array to host', async () => { + const loaded = await buildLoadedSandbox( + (proto) => { + proto.hostModule('host').register('byte_length', (data) => { + expect(Buffer.isBuffer(data)).toBe(true); + return data.length; + }); + }, + ` + import * as host from "host:host"; + function handler() { + const data = new Uint8Array([72, 101, 108, 108, 111]); + return { len: host.byte_length(data) }; + } + ` + ); + const result = await loaded.callHandler('handler', {}); + expect(result).toEqual({ len: 5 }); + }); + + it('should return Buffer from host as Uint8Array on guest', async () => { + const loaded = await buildLoadedSandbox( + (proto) => { + proto.hostModule('host').register('get_bytes', () => { + return Buffer.from([1, 2, 3, 4, 5]); + }); + }, + ` + import * as host from "host:host"; + function handler() { + const data = host.get_bytes(); + return { len: data.length, first: data[0], last: data[4] }; + } + ` + ); + const result = await loaded.callHandler('handler', {}); + expect(result).toEqual({ len: 5, first: 1, last: 5 }); + }); + + it('should handle mixed Buffer and JSON args', async () => { + const loaded = await buildLoadedSandbox( + (proto) => { + proto.hostModule('host').register('describe', (prefix, data, num) => { + expect(typeof prefix).toBe('string'); + expect(Buffer.isBuffer(data)).toBe(true); + expect(typeof num).toBe('number'); + return `${prefix}-${data.length}-${num}`; + }); + }, + ` + import * as host from "host:host"; + function handler() { + const data = new Uint8Array([10, 20, 30]); + return { result: host.describe("pfx", data, 42) }; + } + ` + ); + const result = await loaded.callHandler('handler', {}); + expect(result).toEqual({ result: 'pfx-3-42' }); + }); + + it('should handle empty Uint8Array', async () => { + const loaded = await buildLoadedSandbox( + (proto) => { + proto.hostModule('host').register('check_empty', (data) => { + expect(Buffer.isBuffer(data)).toBe(true); + return data.length; + }); + }, + ` + import * as host from "host:host"; + function handler() { + return { len: host.check_empty(new Uint8Array(0)) }; + } + ` + ); + const result = await loaded.callHandler('handler', {}); + expect(result).toEqual({ len: 0 }); + }); + + it('should handle host returning empty Buffer', async () => { + // Regression: napi_get_buffer_info returns data=null, len=0 for + // empty buffers. JsReturn::from_napi_value must not panic on the + // null pointer — it should return an empty Vec instead. + const loaded = await buildLoadedSandbox( + (proto) => { + proto.hostModule('host').register('empty_response', () => { + return Buffer.alloc(0); + }); + }, + ` + import * as host from "host:host"; + function handler() { + const data = host.empty_response(); + return { len: data.length, isUint8: data instanceof Uint8Array }; + } + ` + ); + const result = await loaded.callHandler('handler', {}); + expect(result).toEqual({ len: 0, isUint8: true }); + }); + + it('should round-trip binary data (send and receive)', async () => { + const loaded = await buildLoadedSandbox( + (proto) => { + proto.hostModule('host').register('echo_bytes', (data) => { + // Return the same Buffer back + return data; + }); + }, + ` + import * as host from "host:host"; + function handler() { + const input = new Uint8Array([0, 127, 128, 255]); + const output = host.echo_bytes(input); + // Verify round-trip preserves all byte values + return { + len: output.length, + b0: output[0], + b1: output[1], + b2: output[2], + b3: output[3], + }; + } + ` + ); + const result = await loaded.callHandler('handler', {}); + expect(result).toEqual({ len: 4, b0: 0, b1: 127, b2: 128, b3: 255 }); + }); +});