From 88743d76ba20f099d1a993c4a8e2383389d70f45 Mon Sep 17 00:00:00 2001 From: James Kay Date: Tue, 5 Nov 2024 00:55:24 +0000 Subject: [PATCH] `JsValue` module conversions (#2771) * Update `linera-wasmer` to `4.4.0-linera.6` This version includes `JsValue` conversions for `Module` that include type hints. * `linera-execution`: add `JsValue` conversions for `wasm::Module` * `linera-execution`: preserve JsValue conversions in module traits * `linera-execution`: document `DynInto` * `linera-base`: adopt `dyn_convert` * `linera_base::task`: adopt `linera_execution::Post` * `linera_execution::wasm`: fix up comments * `linera-execution`: remove redundant TODO We actually can't remove this `Send` and `Sync` until we can send the `ExecutionStateRequest`s via `postMessage`, i.e. issue #2552. That issue is far enough divorced from this code that I don't want to link it here, so let's just remove the TODO. --- Cargo.lock | 26 +++--- Cargo.toml | 5 +- examples/Cargo.lock | 26 +++--- linera-base/src/dyn_convert.rs | 18 +++++ linera-base/src/lib.rs | 1 + linera-base/src/task.rs | 17 ++++ linera-execution/Cargo.toml | 4 +- linera-execution/src/lib.rs | 79 +++++++++++++++++-- .../src/test_utils/mock_application.rs | 4 + linera-execution/src/wasm/mod.rs | 74 +++++++++++++++-- 10 files changed, 216 insertions(+), 38 deletions(-) create mode 100644 linera-base/src/dyn_convert.rs diff --git a/Cargo.lock b/Cargo.lock index 617ecc1df24..87b06f654ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4238,6 +4238,7 @@ dependencies = [ "derive_more 1.0.0", "dyn-clone", "futures", + "js-sys", "linera-base", "linera-execution", "linera-views", @@ -4692,9 +4693,9 @@ dependencies = [ [[package]] name = "linera-wasmer" -version = "4.4.0-linera.5" +version = "4.4.0-linera.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69638eb27ddb5517313d539603f09153d62eff05b5bb02c3e7b7e01d6892d3f6" +checksum = "a0878cce565a3c8e3b7c786a33c6f682f35fea56c816aef2014d67537a9faad6" dependencies = [ "bytes", "cfg-if", @@ -4723,9 +4724,9 @@ dependencies = [ [[package]] name = "linera-wasmer-compiler" -version = "4.4.0-linera.5" +version = "4.4.0-linera.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44e8668359b8c051635c0040687faf0b6005af4611543a6bb6d3bef5957de6c3" +checksum = "149ce5d54f0353047349ccf1ac68b6571e8fda10c2082720fba4c83fc3277cdb" dependencies = [ "backtrace", "bytes", @@ -4741,6 +4742,8 @@ dependencies = [ "region", "rkyv", "self_cell", + "serde", + "serde_bytes", "shared-buffer", "smallvec", "thiserror", @@ -4752,9 +4755,9 @@ dependencies = [ [[package]] name = "linera-wasmer-compiler-cranelift" -version = "4.4.0-linera.5" +version = "4.4.0-linera.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6904c07f0399879c18c3c548d59fff8e7a5c46e6b6e3aec2511ec37beda0c93b" +checksum = "693854eb826bb380d8ef1fe168998bdbba66c707c4990ebd27e95d99821c617f" dependencies = [ "cranelift-codegen 0.91.1", "cranelift-entity 0.91.1", @@ -4771,9 +4774,9 @@ dependencies = [ [[package]] name = "linera-wasmer-compiler-singlepass" -version = "4.4.0-linera.5" +version = "4.4.0-linera.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b740b428562504bc5c306240d0859657a2b266996818de7e24058453a057f31" +checksum = "eeea457da007afc2adbb106ea6025d2563cef774f6cd26e9e2f1cd348c423f1a" dependencies = [ "byteorder", "dynasm", @@ -4790,9 +4793,9 @@ dependencies = [ [[package]] name = "linera-wasmer-vm" -version = "4.4.0-linera.5" +version = "4.4.0-linera.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77de5db5a977bcca136e2824c7eb97022d5d5177c25ce41d180b26921f0a268f" +checksum = "4c74dd9994ec47851c5166543ab4bb93457670024663192cb5753e01548acc15" dependencies = [ "backtrace", "cc", @@ -4811,6 +4814,7 @@ dependencies = [ "more-asserts", "region", "scopeguard", + "serde", "thiserror", "wasmer-types", "windows-sys 0.59.0", @@ -8309,6 +8313,8 @@ dependencies = [ "indexmap 1.9.3", "more-asserts", "rkyv", + "serde", + "serde_bytes", "sha2", "target-lexicon", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 0e90234319e..11a0d592acd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -161,13 +161,14 @@ wasm-bindgen-test = "0.3.42" wasm-encoder = "0.24.1" wasm-instrument = "0.4.0" wasm_thread = "0.3.0" -wasmer = { package = "linera-wasmer", version = "4.4.0-linera.5", default-features = false } -wasmer-compiler-singlepass = { package = "linera-wasmer-compiler-singlepass", version = "4.4.0-linera.5", default-features = false, features = ["std", "unwind", "avx"] } +wasmer = { package = "linera-wasmer", version = "4.4.0-linera.6", default-features = false } +wasmer-compiler-singlepass = { package = "linera-wasmer-compiler-singlepass", version = "4.4.0-linera.6", default-features = false, features = ["std", "unwind", "avx"] } wasmparser = "0.101.1" wasmtime = { version = "25.0.0", default-features = false, features = ["cranelift", "runtime", "std"] } wasmtimer = "0.2.0" webassembly-test = "0.1.0" web-sys = "0.3.69" +js-sys = "0.3.70" web-time = "1.1.0" wit-bindgen = "0.24.0" zstd = "0.13.2" diff --git a/examples/Cargo.lock b/examples/Cargo.lock index b0c9b30112f..42df494a619 100644 --- a/examples/Cargo.lock +++ b/examples/Cargo.lock @@ -2634,6 +2634,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -3505,9 +3506,9 @@ dependencies = [ [[package]] name = "linera-wasmer" -version = "4.4.0-linera.5" +version = "4.4.0-linera.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69638eb27ddb5517313d539603f09153d62eff05b5bb02c3e7b7e01d6892d3f6" +checksum = "a0878cce565a3c8e3b7c786a33c6f682f35fea56c816aef2014d67537a9faad6" dependencies = [ "bytes", "cfg-if", @@ -3535,9 +3536,9 @@ dependencies = [ [[package]] name = "linera-wasmer-compiler" -version = "4.4.0-linera.5" +version = "4.4.0-linera.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44e8668359b8c051635c0040687faf0b6005af4611543a6bb6d3bef5957de6c3" +checksum = "149ce5d54f0353047349ccf1ac68b6571e8fda10c2082720fba4c83fc3277cdb" dependencies = [ "backtrace", "bytes", @@ -3553,6 +3554,8 @@ dependencies = [ "region", "rkyv", "self_cell", + "serde", + "serde_bytes", "shared-buffer", "smallvec", "thiserror", @@ -3564,9 +3567,9 @@ dependencies = [ [[package]] name = "linera-wasmer-compiler-cranelift" -version = "4.4.0-linera.5" +version = "4.4.0-linera.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6904c07f0399879c18c3c548d59fff8e7a5c46e6b6e3aec2511ec37beda0c93b" +checksum = "693854eb826bb380d8ef1fe168998bdbba66c707c4990ebd27e95d99821c617f" dependencies = [ "cranelift-codegen 0.91.1", "cranelift-entity 0.91.1", @@ -3583,9 +3586,9 @@ dependencies = [ [[package]] name = "linera-wasmer-compiler-singlepass" -version = "4.4.0-linera.5" +version = "4.4.0-linera.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b740b428562504bc5c306240d0859657a2b266996818de7e24058453a057f31" +checksum = "eeea457da007afc2adbb106ea6025d2563cef774f6cd26e9e2f1cd348c423f1a" dependencies = [ "byteorder", "dynasm", @@ -3602,9 +3605,9 @@ dependencies = [ [[package]] name = "linera-wasmer-vm" -version = "4.4.0-linera.5" +version = "4.4.0-linera.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77de5db5a977bcca136e2824c7eb97022d5d5177c25ce41d180b26921f0a268f" +checksum = "4c74dd9994ec47851c5166543ab4bb93457670024663192cb5753e01548acc15" dependencies = [ "backtrace", "cc", @@ -3623,6 +3626,7 @@ dependencies = [ "more-asserts", "region", "scopeguard", + "serde", "thiserror", "wasmer-types", "windows-sys 0.59.0", @@ -6311,6 +6315,8 @@ dependencies = [ "indexmap 1.9.3", "more-asserts", "rkyv", + "serde", + "serde_bytes", "sha2", "target-lexicon", "thiserror", diff --git a/linera-base/src/dyn_convert.rs b/linera-base/src/dyn_convert.rs new file mode 100644 index 00000000000..92451a5c6c6 --- /dev/null +++ b/linera-base/src/dyn_convert.rs @@ -0,0 +1,18 @@ +// Copyright (c) Zefchain Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/*! +Object-safe conversion traits. +*/ + +/// An object-safe version of `std::convert::Into`. +pub trait DynInto { + /// Converts a boxed object into the target type. + fn into_box(self: Box) -> To; +} + +impl> DynInto for From { + fn into_box(self: Box) -> To { + (*self).into() + } +} diff --git a/linera-base/src/lib.rs b/linera-base/src/lib.rs index b4d402e31a9..b220bcdbda0 100644 --- a/linera-base/src/lib.rs +++ b/linera-base/src/lib.rs @@ -16,6 +16,7 @@ pub mod abi; pub mod command; pub mod crypto; pub mod data_types; +pub mod dyn_convert; mod graphql; pub mod identifiers; mod limited_writer; diff --git a/linera-base/src/task.rs b/linera-base/src/task.rs index b7d05d7f4cc..17e9dd79beb 100644 --- a/linera-base/src/task.rs +++ b/linera-base/src/task.rs @@ -11,6 +11,13 @@ use std::future::Future; mod implementation { use super::*; + /// Types that can be _explicitly_ sent to a new thread. + /// This differs from `Send` in that we can provide an explicit post step + /// (e.g. `postMessage` on the Web). + pub trait Post: Send + Sync {} + + impl Post for T {} + /// The type of errors that can result from awaiting a task to completion. pub type Error = tokio::task::JoinError; /// The type of a future awaiting another task. @@ -36,8 +43,18 @@ mod implementation { #[cfg(web)] mod implementation { use futures::channel::oneshot; + use wasm_bindgen_futures::wasm_bindgen::JsValue; use super::*; + use crate::dyn_convert; + + /// Types that can be _explicitly_ sent to a new thread. + /// This differs from `Send` in that we can provide an explicit post step + /// (e.g. `postMessage` on the Web). + // TODO(#2809): this trait is overly liberal. + pub trait Post: dyn_convert::DynInto {} + + impl> Post for T {} /// The type of errors that can result from awaiting a task to completion. pub type Error = oneshot::Canceled; diff --git a/linera-execution/Cargo.toml b/linera-execution/Cargo.toml index b30fbf176da..5ba85434462 100644 --- a/linera-execution/Cargo.toml +++ b/linera-execution/Cargo.toml @@ -18,6 +18,7 @@ metrics = ["prometheus", "linera-views/metrics"] unstable-oracles = [] wasmer = [ "dep:wasmer", + "wasmer/enable-serde", "linera-witty/wasmer", "wasm-encoder", "wasm-instrument", @@ -29,7 +30,7 @@ wasmtime = [ "wasm-encoder", "wasmparser", ] -web = ["linera-base/web", "linera-views/web"] +web = ["linera-base/web", "linera-views/web", "js-sys"] [dependencies] anyhow.workspace = true @@ -43,6 +44,7 @@ dashmap.workspace = true derive_more = { workspace = true, features = ["display"] } dyn-clone.workspace = true futures.workspace = true +js-sys = { workspace = true, optional = true } linera-base.workspace = true linera-views.workspace = true linera-views-derive.workspace = true diff --git a/linera-execution/src/lib.rs b/linera-execution/src/lib.rs index 501e6899984..a1c419c1b0d 100644 --- a/linera-execution/src/lib.rs +++ b/linera-execution/src/lib.rs @@ -4,6 +4,7 @@ //! This module manages the execution of the system application and the user applications in a //! Linera chain. +#![cfg_attr(web, feature(trait_upcasting))] #![deny(clippy::large_futures)] mod applications; @@ -21,7 +22,7 @@ mod transaction_tracker; mod util; mod wasm; -use std::{fmt, str::FromStr, sync::Arc}; +use std::{any::Any, fmt, str::FromStr, sync::Arc}; use async_graphql::SimpleObject; use async_trait::async_trait; @@ -29,6 +30,8 @@ use committee::Epoch; use custom_debug_derive::Debug; use dashmap::DashMap; use derive_more::Display; +#[cfg(web)] +use js_sys::wasm_bindgen::JsValue; use linera_base::{ abi::Abi, crypto::CryptoHash, @@ -42,6 +45,7 @@ use linera_base::{ GenericApplicationId, MessageId, Owner, StreamName, UserApplicationId, }, ownership::ChainOwnership, + task, }; use linera_views::{batch::Batch, views::ViewError}; use serde::{Deserialize, Serialize}; @@ -81,10 +85,12 @@ const MAX_EVENT_KEY_LEN: usize = 64; const MAX_STREAM_NAME_LEN: usize = 64; /// An implementation of [`UserContractModule`]. -pub type UserContractCode = Box; +#[derive(Clone)] +pub struct UserContractCode(Box); /// An implementation of [`UserServiceModule`]. -pub type UserServiceCode = Box; +#[derive(Clone)] +pub struct UserServiceCode(Box); /// An implementation of [`UserContract`]. pub type UserContractInstance = Box; @@ -93,7 +99,7 @@ pub type UserContractInstance = Box; pub type UserServiceInstance = Box; /// A factory trait to obtain a [`UserContract`] from a [`UserContractModule`] -pub trait UserContractModule: dyn_clone::DynClone { +pub trait UserContractModule: dyn_clone::DynClone + Any + task::Post + Send + Sync { fn instantiate( &self, runtime: ContractSyncRuntimeHandle, @@ -102,14 +108,14 @@ pub trait UserContractModule: dyn_clone::DynClone { impl From for UserContractCode { fn from(module: T) -> Self { - Box::new(module) + Self(Box::new(module)) } } dyn_clone::clone_trait_object!(UserContractModule); /// A factory trait to obtain a [`UserService`] from a [`UserServiceModule`] -pub trait UserServiceModule: dyn_clone::DynClone { +pub trait UserServiceModule: dyn_clone::DynClone + Any + task::Post + Send + Sync { fn instantiate( &self, runtime: ServiceSyncRuntimeHandle, @@ -118,12 +124,68 @@ pub trait UserServiceModule: dyn_clone::DynClone { impl From for UserServiceCode { fn from(module: T) -> Self { - Box::new(module) + Self(Box::new(module)) } } dyn_clone::clone_trait_object!(UserServiceModule); +impl UserServiceCode { + fn instantiate( + &self, + runtime: ServiceSyncRuntimeHandle, + ) -> Result { + self.0.instantiate(runtime) + } +} + +impl UserContractCode { + fn instantiate( + &self, + runtime: ContractSyncRuntimeHandle, + ) -> Result { + self.0.instantiate(runtime) + } +} + +#[cfg(web)] +const _: () = { + // TODO(#2775): add a vtable pointer into the JsValue rather than assuming the + // implementor + + impl From for JsValue { + fn from(code: UserContractCode) -> JsValue { + let module: WasmContractModule = *(code.0 as Box) + .downcast() + .expect("we only support Wasm modules on the Web for now"); + module.into() + } + } + + impl From for JsValue { + fn from(code: UserServiceCode) -> JsValue { + let module: WasmServiceModule = *(code.0 as Box) + .downcast() + .expect("we only support Wasm modules on the Web for now"); + module.into() + } + } + + impl TryFrom for UserContractCode { + type Error = JsValue; + fn try_from(value: JsValue) -> Result { + WasmContractModule::try_from(value).map(Into::into) + } + } + + impl TryFrom for UserServiceCode { + type Error = JsValue; + fn try_from(value: JsValue) -> Result { + WasmServiceModule::try_from(value).map(Into::into) + } + } +}; + /// A type for errors happening during execution. #[derive(Error, Debug)] pub enum ExecutionError { @@ -909,7 +971,8 @@ impl TestExecutionRuntimeContext { } #[cfg(with_testing)] -#[async_trait] +#[cfg_attr(not(web), async_trait)] +#[cfg_attr(web, async_trait(?Send))] impl ExecutionRuntimeContext for TestExecutionRuntimeContext { fn chain_id(&self) -> ChainId { self.chain_id diff --git a/linera-execution/src/test_utils/mock_application.rs b/linera-execution/src/test_utils/mock_application.rs index 834ec8f85d1..ccfab4a8f87 100644 --- a/linera-execution/src/test_utils/mock_application.rs +++ b/linera-execution/src/test_utils/mock_application.rs @@ -15,6 +15,9 @@ use std::{ }, }; +#[cfg(web)] +use js_sys::wasm_bindgen; + use crate::{ ContractSyncRuntimeHandle, ExecutionError, FinalizeContext, MessageContext, OperationContext, QueryContext, ServiceSyncRuntimeHandle, UserContract, UserContractModule, UserService, @@ -25,6 +28,7 @@ use crate::{ /// /// Should be configured with any expected calls, and can then be used to create a /// [`MockApplicationInstance`] that implements [`UserContract`] and [`UserService`]. +#[cfg_attr(web, wasm_bindgen::prelude::wasm_bindgen)] #[derive(Clone, Default)] pub struct MockApplication { expected_calls: Arc>>, diff --git a/linera-execution/src/wasm/mod.rs b/linera-execution/src/wasm/mod.rs index 7fd7a3fee13..c1fe5c54d4e 100644 --- a/linera-execution/src/wasm/mod.rs +++ b/linera-execution/src/wasm/mod.rs @@ -20,19 +20,18 @@ mod wasmer; #[cfg(with_wasmtime)] mod wasmtime; -#[cfg(with_metrics)] -use std::sync::LazyLock; - use linera_base::data_types::Bytecode; -#[cfg(with_metrics)] -use linera_base::prometheus_util::{self, MeasureLatency}; -#[cfg(with_metrics)] -use prometheus::HistogramVec; use thiserror::Error; #[cfg(with_wasmer)] use wasmer::{WasmerContractInstance, WasmerServiceInstance}; #[cfg(with_wasmtime)] use wasmtime::{WasmtimeContractInstance, WasmtimeServiceInstance}; +#[cfg(with_metrics)] +use { + linera_base::prometheus_util::{self, MeasureLatency}, + prometheus::HistogramVec, + std::sync::LazyLock, +}; use self::sanitizer::sanitize; pub use self::{ @@ -214,6 +213,67 @@ impl UserServiceModule for WasmServiceModule { } } +#[cfg(web)] +const _: () = { + use js_sys::wasm_bindgen::JsValue; + + impl TryFrom for WasmServiceModule { + type Error = JsValue; + + fn try_from(value: JsValue) -> Result { + // TODO(#2775): be generic over possible implementations + + cfg_if::cfg_if! { + if #[cfg(with_wasmer)] { + Ok(Self::Wasmer { + module: value.try_into()?, + }) + } else { + Err(value) + } + } + } + } + + impl From for JsValue { + fn from(module: WasmServiceModule) -> JsValue { + match module { + #[cfg(with_wasmer)] + WasmServiceModule::Wasmer { module } => ::wasmer::Module::clone(&module).into(), + } + } + } + + impl TryFrom for WasmContractModule { + type Error = JsValue; + + fn try_from(value: JsValue) -> Result { + // TODO(#2775): be generic over possible implementations + cfg_if::cfg_if! { + if #[cfg(with_wasmer)] { + Ok(Self::Wasmer { + module: value.try_into()?, + engine: Default::default(), + }) + } else { + Err(value) + } + } + } + } + + impl From for JsValue { + fn from(module: WasmContractModule) -> JsValue { + match module { + #[cfg(with_wasmer)] + WasmContractModule::Wasmer { module, engine: _ } => { + ::wasmer::Module::clone(&module).into() + } + } + } + } +}; + /// Errors that can occur when executing a user application in a WebAssembly module. #[cfg(any(with_wasmer, with_wasmtime))] #[derive(Debug, Error)]