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)]
82pub enum WasmContractModule {
83    #[cfg(with_wasmer)]
84    Wasmer {
85        engine: ::wasmer::Engine,
86        module: ::wasmer::Module,
87    },
88    #[cfg(with_wasmtime)]
89    Wasmtime { module: ::wasmtime::Module },
90}
91
92impl WasmContractModule {
93    /// Creates a new [`WasmContractModule`] using the WebAssembly module with the provided bytecode.
94    pub async fn new(
95        contract_bytecode: Bytecode,
96        runtime: WasmRuntime,
97    ) -> Result<Self, WasmExecutionError> {
98        match runtime {
99            #[cfg(with_wasmer)]
100            WasmRuntime::Wasmer => Self::from_wasmer(contract_bytecode).await,
101            #[cfg(with_wasmtime)]
102            WasmRuntime::Wasmtime => Self::from_wasmtime(contract_bytecode).await,
103        }
104    }
105
106    /// Creates a new [`WasmContractModule`] using the WebAssembly module in `contract_bytecode_file`.
107    #[cfg(with_fs)]
108    pub async fn from_file(
109        contract_bytecode_file: impl AsRef<Path>,
110        runtime: WasmRuntime,
111    ) -> Result<Self, WasmExecutionError> {
112        Self::new(
113            Bytecode::load_from_file(contract_bytecode_file)
114                .await
115                .map_err(anyhow::Error::from)
116                .map_err(WasmExecutionError::LoadContractModule)?,
117            runtime,
118        )
119        .await
120    }
121}
122
123impl UserContractModule for WasmContractModule {
124    fn instantiate(
125        &self,
126        runtime: ContractSyncRuntimeHandle,
127    ) -> Result<UserContractInstance, ExecutionError> {
128        #[cfg(with_metrics)]
129        let _instantiation_latency = metrics::CONTRACT_INSTANTIATION_LATENCY.measure_latency();
130
131        let instance: UserContractInstance = match self {
132            #[cfg(with_wasmtime)]
133            WasmContractModule::Wasmtime { module } => {
134                Box::new(WasmtimeContractInstance::prepare(module, runtime)?)
135            }
136            #[cfg(with_wasmer)]
137            WasmContractModule::Wasmer { engine, module } => Box::new(
138                WasmerContractInstance::prepare(engine.clone(), module, runtime)?,
139            ),
140        };
141
142        Ok(instance)
143    }
144}
145
146/// A user service in a compiled WebAssembly module.
147#[derive(Clone)]
148pub enum WasmServiceModule {
149    #[cfg(with_wasmer)]
150    Wasmer { module: ::wasmer::Module },
151    #[cfg(with_wasmtime)]
152    Wasmtime { module: ::wasmtime::Module },
153}
154
155impl WasmServiceModule {
156    /// Creates a new [`WasmServiceModule`] using the WebAssembly module with the provided bytecode.
157    pub async fn new(
158        service_bytecode: Bytecode,
159        runtime: WasmRuntime,
160    ) -> Result<Self, WasmExecutionError> {
161        match runtime {
162            #[cfg(with_wasmer)]
163            WasmRuntime::Wasmer => Self::from_wasmer(service_bytecode).await,
164            #[cfg(with_wasmtime)]
165            WasmRuntime::Wasmtime => Self::from_wasmtime(service_bytecode).await,
166        }
167    }
168
169    /// Creates a new [`WasmServiceModule`] using the WebAssembly module in `service_bytecode_file`.
170    #[cfg(with_fs)]
171    pub async fn from_file(
172        service_bytecode_file: impl AsRef<Path>,
173        runtime: WasmRuntime,
174    ) -> Result<Self, WasmExecutionError> {
175        Self::new(
176            Bytecode::load_from_file(service_bytecode_file)
177                .await
178                .map_err(anyhow::Error::from)
179                .map_err(WasmExecutionError::LoadServiceModule)?,
180            runtime,
181        )
182        .await
183    }
184}
185
186impl UserServiceModule for WasmServiceModule {
187    fn instantiate(
188        &self,
189        runtime: ServiceSyncRuntimeHandle,
190    ) -> Result<UserServiceInstance, ExecutionError> {
191        #[cfg(with_metrics)]
192        let _instantiation_latency = metrics::SERVICE_INSTANTIATION_LATENCY.measure_latency();
193
194        let instance: UserServiceInstance = match self {
195            #[cfg(with_wasmtime)]
196            WasmServiceModule::Wasmtime { module } => {
197                Box::new(WasmtimeServiceInstance::prepare(module, runtime)?)
198            }
199            #[cfg(with_wasmer)]
200            WasmServiceModule::Wasmer { module } => {
201                Box::new(WasmerServiceInstance::prepare(module, runtime)?)
202            }
203        };
204
205        Ok(instance)
206    }
207}
208
209/// Instrument the [`Bytecode`] to add fuel metering.
210pub fn add_metering(bytecode: &Bytecode) -> Result<Bytecode, WasmExecutionError> {
211    pub struct Costs;
212    impl walrus_meter::Costs for Costs {
213        fn instruction(&self, instruction: &walrus::ir::Instr) -> i32 {
214            use walrus::ir::Instr::*;
215            match instruction {
216                Drop(_) | Block(_) | Loop(_) | Unreachable(_) => 0,
217                _ => 1,
218            }
219        }
220    }
221
222    let instrumented_module = walrus_meter::instrument(
223        &bytecode.bytes,
224        Costs,
225        ("linera:app/contract-runtime-api", "consume-fuel"),
226    )
227    .map_err(|_| WasmExecutionError::InstrumentModule)?;
228
229    Ok(Bytecode::new(instrumented_module))
230}
231
232#[cfg(web)]
233const _: () = {
234    use js_sys::wasm_bindgen::JsValue;
235    use web_thread_select as web_thread;
236
237    impl web_thread::AsJs for WasmServiceModule {
238        fn to_js(&self) -> Result<JsValue, JsValue> {
239            match self {
240                #[cfg(with_wasmer)]
241                Self::Wasmer { module } => Ok(::wasmer::Module::clone(module).into()),
242            }
243        }
244
245        fn from_js(value: JsValue) -> Result<Self, JsValue> {
246            // TODO(#2775): be generic over possible implementations
247
248            cfg_if::cfg_if! {
249                if #[cfg(with_wasmer)] {
250                    Ok(Self::Wasmer {
251                        module: value.try_into()?,
252                    })
253                } else {
254                    Err(value)
255                }
256            }
257        }
258    }
259
260    impl web_thread::Post for WasmServiceModule {}
261
262    impl web_thread::AsJs for WasmContractModule {
263        fn to_js(&self) -> Result<JsValue, JsValue> {
264            match self {
265                #[cfg(with_wasmer)]
266                Self::Wasmer { module, engine: _ } => Ok(::wasmer::Module::clone(module).into()),
267            }
268        }
269
270        fn from_js(value: JsValue) -> Result<Self, JsValue> {
271            // TODO(#2775): be generic over possible implementations
272
273            cfg_if::cfg_if! {
274                if #[cfg(with_wasmer)] {
275                    Ok(Self::Wasmer {
276                        module: value.try_into()?,
277                        engine: Default::default(),
278                    })
279                } else {
280                    Err(value)
281                }
282            }
283        }
284    }
285
286    impl web_thread::Post for WasmContractModule {}
287};
288
289/// Errors that can occur when executing a user application in a WebAssembly module.
290#[derive(Debug, Error)]
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    pub fn get_example_bytecode_paths(name: &str) -> Result<(String, String), std::io::Error> {
357        let name = name.replace('-', "_");
358        static INSTANCE: LazyLock<()> = LazyLock::new(|| build_applications().unwrap());
359        LazyLock::force(&INSTANCE);
360        for dir in ["../examples", "../linera-sdk/tests/fixtures"] {
361            let prefix = format!("{dir}/target/wasm32-unknown-unknown/release");
362            let file_contract = format!("{prefix}/{name}_contract.wasm");
363            let file_service = format!("{prefix}/{name}_service.wasm");
364            if Path::new(&file_contract).exists() && Path::new(&file_service).exists() {
365                return Ok((file_contract, file_service));
366            }
367        }
368        Err(std::io::Error::last_os_error())
369    }
370}