Skip to main content

linera_execution/wasm/
mod.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Support for user applications compiled as WebAssembly (Wasm) modules.
5//!
6//! Requires a WebAssembly runtime to be selected and enabled using one of the following features:
7//!
8//! - `wasmer` enables the [Wasmer](https://wasmer.io/) runtime
9//! - `wasmtime` enables the [Wasmtime](https://wasmtime.dev/) runtime
10
11#![cfg(with_wasm_runtime)]
12
13mod entrypoints;
14mod module_cache;
15#[macro_use]
16mod runtime_api;
17#[cfg(with_wasmer)]
18mod wasmer;
19#[cfg(with_wasmtime)]
20mod wasmtime;
21
22#[cfg(with_fs)]
23use std::path::Path;
24
25use linera_base::data_types::Bytecode;
26#[cfg(with_metrics)]
27use linera_base::prometheus_util::MeasureLatency as _;
28use thiserror::Error;
29#[cfg(with_wasmer)]
30use wasmer::{WasmerContractInstance, WasmerServiceInstance};
31#[cfg(with_wasmtime)]
32use wasmtime::{WasmtimeContractInstance, WasmtimeServiceInstance};
33
34pub use self::{
35    entrypoints::{ContractEntrypoints, ServiceEntrypoints},
36    runtime_api::{BaseRuntimeApi, ContractRuntimeApi, RuntimeApiData, ServiceRuntimeApi},
37};
38use crate::{
39    ContractSyncRuntimeHandle, ExecutionError, ServiceSyncRuntimeHandle, UserContractInstance,
40    UserContractModule, UserServiceInstance, UserServiceModule, WasmRuntime,
41};
42
43#[cfg(with_metrics)]
44pub(crate) mod metrics {
45    use linera_base::prometheus_util::{
46        exponential_bucket_interval, exponential_bucket_latencies, register_histogram_vec,
47    };
48    use prometheus::HistogramVec;
49
50    linera_base::declare_metrics! {
51        pub static CONTRACT_INSTANTIATION_LATENCY: HistogramVec =
52            register_histogram_vec(
53                "wasm_contract_instantiation_latency",
54                "Wasm contract instantiation latency",
55                &[],
56                exponential_bucket_latencies(100.0),
57            );
58
59        pub static SERVICE_INSTANTIATION_LATENCY: HistogramVec =
60            register_histogram_vec(
61                "wasm_service_instantiation_latency",
62                "Wasm service instantiation latency",
63                &[],
64                exponential_bucket_latencies(100.0),
65            );
66
67        pub static WASM_BYTECODE_SIZE_BYTES: HistogramVec =
68            register_histogram_vec(
69                "wasm_bytecode_size_bytes",
70                "Size in bytes of WASM bytecodes being loaded",
71                &["type"],
72                exponential_bucket_interval(10_000.0, 100_000_000.0),
73            );
74    }
75}
76
77/// A user contract in a compiled WebAssembly module.
78#[derive(Clone)]
79#[allow(missing_docs)]
80pub enum WasmContractModule {
81    #[cfg(with_wasmer)]
82    Wasmer {
83        engine: ::wasmer::Engine,
84        module: ::wasmer::Module,
85    },
86    #[cfg(with_wasmtime)]
87    Wasmtime { module: ::wasmtime::Module },
88}
89
90impl WasmContractModule {
91    /// Creates a new [`WasmContractModule`] using the WebAssembly module with the provided bytecode.
92    pub async fn new(
93        contract_bytecode: Bytecode,
94        runtime: WasmRuntime,
95    ) -> Result<Self, WasmExecutionError> {
96        match runtime {
97            #[cfg(with_wasmer)]
98            WasmRuntime::Wasmer => Self::from_wasmer(contract_bytecode).await,
99            #[cfg(with_wasmtime)]
100            WasmRuntime::Wasmtime => Self::from_wasmtime(contract_bytecode).await,
101        }
102    }
103
104    /// Creates a new [`WasmContractModule`] using the WebAssembly module in `contract_bytecode_file`.
105    #[cfg(with_fs)]
106    pub async fn from_file(
107        contract_bytecode_file: impl AsRef<Path>,
108        runtime: WasmRuntime,
109    ) -> Result<Self, WasmExecutionError> {
110        Self::new(
111            Bytecode::load_from_file(contract_bytecode_file)
112                .await
113                .map_err(anyhow::Error::from)
114                .map_err(WasmExecutionError::LoadContractModule)?,
115            runtime,
116        )
117        .await
118    }
119}
120
121impl UserContractModule for WasmContractModule {
122    fn instantiate(
123        &self,
124        runtime: ContractSyncRuntimeHandle,
125    ) -> Result<UserContractInstance, ExecutionError> {
126        #[cfg(with_metrics)]
127        let _instantiation_latency = metrics::CONTRACT_INSTANTIATION_LATENCY.measure_latency();
128
129        let instance: UserContractInstance = match self {
130            #[cfg(with_wasmtime)]
131            WasmContractModule::Wasmtime { module } => {
132                Box::new(WasmtimeContractInstance::prepare(module, runtime)?)
133            }
134            #[cfg(with_wasmer)]
135            WasmContractModule::Wasmer { engine, module } => Box::new(
136                WasmerContractInstance::prepare(engine.clone(), module, runtime)?,
137            ),
138        };
139
140        Ok(instance)
141    }
142}
143
144/// A user service in a compiled WebAssembly module.
145#[derive(Clone)]
146#[allow(missing_docs)]
147pub enum WasmServiceModule {
148    #[cfg(with_wasmer)]
149    Wasmer { module: ::wasmer::Module },
150    #[cfg(with_wasmtime)]
151    Wasmtime { module: ::wasmtime::Module },
152}
153
154impl WasmServiceModule {
155    /// Creates a new [`WasmServiceModule`] using the WebAssembly module with the provided bytecode.
156    pub async fn new(
157        service_bytecode: Bytecode,
158        runtime: WasmRuntime,
159    ) -> Result<Self, WasmExecutionError> {
160        match runtime {
161            #[cfg(with_wasmer)]
162            WasmRuntime::Wasmer => Self::from_wasmer(service_bytecode).await,
163            #[cfg(with_wasmtime)]
164            WasmRuntime::Wasmtime => Self::from_wasmtime(service_bytecode).await,
165        }
166    }
167
168    /// Creates a new [`WasmServiceModule`] using the WebAssembly module in `service_bytecode_file`.
169    #[cfg(with_fs)]
170    pub async fn from_file(
171        service_bytecode_file: impl AsRef<Path>,
172        runtime: WasmRuntime,
173    ) -> Result<Self, WasmExecutionError> {
174        Self::new(
175            Bytecode::load_from_file(service_bytecode_file)
176                .await
177                .map_err(anyhow::Error::from)
178                .map_err(WasmExecutionError::LoadServiceModule)?,
179            runtime,
180        )
181        .await
182    }
183}
184
185impl UserServiceModule for WasmServiceModule {
186    fn instantiate(
187        &self,
188        runtime: ServiceSyncRuntimeHandle,
189    ) -> Result<UserServiceInstance, ExecutionError> {
190        #[cfg(with_metrics)]
191        let _instantiation_latency = metrics::SERVICE_INSTANTIATION_LATENCY.measure_latency();
192
193        let instance: UserServiceInstance = match self {
194            #[cfg(with_wasmtime)]
195            WasmServiceModule::Wasmtime { module } => {
196                Box::new(WasmtimeServiceInstance::prepare(module, runtime)?)
197            }
198            #[cfg(with_wasmer)]
199            WasmServiceModule::Wasmer { module } => {
200                Box::new(WasmerServiceInstance::prepare(module, runtime)?)
201            }
202        };
203
204        Ok(instance)
205    }
206}
207
208/// Instrument the [`Bytecode`] to add fuel metering.
209pub fn add_metering(bytecode: &Bytecode) -> Result<Bytecode, WasmExecutionError> {
210    pub struct Costs;
211    impl walrus_meter::Costs for Costs {
212        fn instruction(&self, instruction: &walrus::ir::Instr) -> i32 {
213            use walrus::ir::Instr::*;
214            match instruction {
215                Drop(_) | Block(_) | Loop(_) | Unreachable(_) => 0,
216                _ => 1,
217            }
218        }
219    }
220
221    let instrumented_module = walrus_meter::instrument(
222        &bytecode.bytes,
223        Costs,
224        ("linera:app/contract-runtime-api", "consume-fuel"),
225    )
226    .map_err(|_| WasmExecutionError::InstrumentModule)?;
227
228    Ok(Bytecode::new(instrumented_module))
229}
230
231#[cfg(web)]
232const _: () = {
233    use js_sys::wasm_bindgen::JsValue;
234    use web_thread_select as web_thread;
235
236    impl web_thread::AsJs for WasmServiceModule {
237        fn to_js(&self) -> Result<JsValue, JsValue> {
238            match self {
239                #[cfg(with_wasmer)]
240                Self::Wasmer { module } => Ok(::wasmer::Module::clone(module).into()),
241            }
242        }
243
244        fn from_js(value: JsValue) -> Result<Self, JsValue> {
245            // TODO(#2775): be generic over possible implementations
246
247            cfg_if::cfg_if! {
248                if #[cfg(with_wasmer)] {
249                    Ok(Self::Wasmer {
250                        module: value.try_into()?,
251                    })
252                } else {
253                    Err(value)
254                }
255            }
256        }
257    }
258
259    impl web_thread::Post for WasmServiceModule {}
260
261    impl web_thread::AsJs for WasmContractModule {
262        fn to_js(&self) -> Result<JsValue, JsValue> {
263            match self {
264                #[cfg(with_wasmer)]
265                Self::Wasmer { module, engine: _ } => Ok(::wasmer::Module::clone(module).into()),
266            }
267        }
268
269        fn from_js(value: JsValue) -> Result<Self, JsValue> {
270            // TODO(#2775): be generic over possible implementations
271
272            cfg_if::cfg_if! {
273                if #[cfg(with_wasmer)] {
274                    Ok(Self::Wasmer {
275                        module: value.try_into()?,
276                        engine: Default::default(),
277                    })
278                } else {
279                    Err(value)
280                }
281            }
282        }
283    }
284
285    impl web_thread::Post for WasmContractModule {}
286};
287
288/// Errors that can occur when executing a user application in a WebAssembly module.
289#[derive(Debug, Error)]
290#[allow(missing_docs)]
291pub enum WasmExecutionError {
292    #[error("Failed to load contract Wasm module: {_0}")]
293    LoadContractModule(#[source] anyhow::Error),
294    #[error("Failed to load service Wasm module: {_0}")]
295    LoadServiceModule(#[source] anyhow::Error),
296    #[error("Failed to instrument Wasm module to add fuel metering")]
297    InstrumentModule,
298    #[cfg(with_wasmer)]
299    #[error("Failed to instantiate Wasm module: {_0}")]
300    InstantiateModuleWithWasmer(#[from] Box<::wasmer::InstantiationError>),
301    #[cfg(with_wasmtime)]
302    #[error("Failed to create and configure Wasmtime runtime: {_0}")]
303    CreateWasmtimeEngine(#[source] anyhow::Error),
304    #[cfg(with_wasmer)]
305    #[error(
306        "Failed to execute Wasm module in Wasmer. This may be caused by panics or insufficient fuel. {0}"
307    )]
308    ExecuteModuleInWasmer(#[from] ::wasmer::RuntimeError),
309    #[cfg(with_wasmtime)]
310    #[error("Failed to execute Wasm module in Wasmtime: {0}")]
311    ExecuteModuleInWasmtime(#[from] ::wasmtime::Trap),
312    #[error("Failed to execute Wasm module: {0}")]
313    ExecuteModule(#[from] linera_witty::RuntimeError),
314    #[error("Attempt to wait for an unknown promise")]
315    UnknownPromise,
316    #[error("Attempt to call incorrect `wait` function for a promise")]
317    IncorrectPromise,
318}
319
320#[cfg(with_wasmer)]
321impl From<::wasmer::InstantiationError> for WasmExecutionError {
322    fn from(instantiation_error: ::wasmer::InstantiationError) -> Self {
323        WasmExecutionError::InstantiateModuleWithWasmer(Box::new(instantiation_error))
324    }
325}
326
327/// This assumes that the current directory is one of the crates.
328#[cfg(with_testing)]
329pub mod test {
330    use std::{path::Path, sync::LazyLock};
331
332    fn build_applications_in_directory(dir: &str) -> Result<(), std::io::Error> {
333        let output = std::process::Command::new("cargo")
334            .current_dir(dir)
335            .args(["build", "--release", "--target", "wasm32-unknown-unknown"])
336            .output()?;
337        if !output.status.success() {
338            panic!(
339                "Failed to build applications in directory {dir}.\n\n\
340                 stdout:\n-------\n{}\n\n\
341                 stderr:\n-------\n{}",
342                String::from_utf8_lossy(&output.stdout),
343                String::from_utf8_lossy(&output.stderr),
344            );
345        }
346        Ok(())
347    }
348
349    fn build_applications() -> Result<(), std::io::Error> {
350        for dir in ["../examples", "../linera-sdk/tests/fixtures"] {
351            build_applications_in_directory(dir)?;
352        }
353        Ok(())
354    }
355
356    /// Returns the contract and service bytecode file paths for the named example application.
357    pub fn get_example_bytecode_paths(name: &str) -> Result<(String, String), std::io::Error> {
358        let name = name.replace('-', "_");
359        static INSTANCE: LazyLock<()> = LazyLock::new(|| build_applications().unwrap());
360        LazyLock::force(&INSTANCE);
361        for dir in ["../examples", "../linera-sdk/tests/fixtures"] {
362            let prefix = format!("{dir}/target/wasm32-unknown-unknown/release");
363            let file_contract = format!("{prefix}/{name}_contract.wasm");
364            let file_service = format!("{prefix}/{name}_service.wasm");
365            if Path::new(&file_contract).exists() && Path::new(&file_service).exists() {
366                return Ok((file_contract, file_service));
367            }
368        }
369        Err(std::io::Error::last_os_error())
370    }
371}