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