1#![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#[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 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 #[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#[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 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 #[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
208pub 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 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 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#[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#[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> {
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}