Skip to main content

soil_client/executor/polkavm/
mod.rs

1// This file is part of Soil.
2
3// Copyright (C) Soil contributors.
4// Copyright (C) Parity Technologies (UK) Ltd.
5// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
6
7use crate::executor::common::{
8	error::{Error, WasmError},
9	wasm_runtime::{AllocationStats, WasmInstance, WasmModule},
10};
11use polkavm::{CallError, Caller, Reg};
12use subsoil::wasm_interface::{
13	Function, FunctionContext, HostFunctions, Pointer, Value, ValueType, WordSize,
14};
15
16#[repr(transparent)]
17pub struct InstancePre(polkavm::InstancePre<(), String>);
18
19#[repr(transparent)]
20pub struct Instance(polkavm::Instance<(), String>);
21
22impl WasmModule for InstancePre {
23	fn new_instance(&self) -> Result<Box<dyn WasmInstance>, Error> {
24		Ok(Box::new(Instance(self.0.instantiate()?)))
25	}
26}
27
28impl WasmInstance for Instance {
29	fn call_with_allocation_stats(
30		&mut self,
31		name: &str,
32		raw_data: &[u8],
33	) -> (Result<Vec<u8>, Error>, Option<AllocationStats>) {
34		let pc = match self.0.module().exports().find(|e| e.symbol() == name) {
35			Some(export) => export.program_counter(),
36			None => {
37				return (
38					Err(format!("cannot call into the runtime: export not found: '{name}'").into()),
39					None,
40				)
41			},
42		};
43
44		let Ok(raw_data_length) = u32::try_from(raw_data.len()) else {
45			return (
46				Err(format!("cannot call runtime method '{name}': input payload is too big").into()),
47				None,
48			);
49		};
50
51		// TODO: This will leak guest memory; find a better solution.
52
53		// Make sure that the memory is cleared...
54		if let Err(err) = self.0.reset_memory() {
55			return (
56				Err(format!(
57					"call into the runtime method '{name}' failed: reset memory failed: {err}"
58				)
59				.into()),
60				None,
61			);
62		}
63
64		// ... and allocate space for the input payload.
65		if let Err(err) = self.0.sbrk(raw_data_length) {
66			return (
67				Err(format!(
68					"call into the runtime method '{name}' failed: reset memory failed: {err}"
69				)
70				.into()),
71				None,
72			);
73		}
74
75		// Grab the address of where the guest's heap starts; that's where we've just allocated
76		// the memory for the input payload.
77		let data_pointer = self.0.module().memory_map().heap_base();
78
79		if let Err(err) = self.0.write_memory(data_pointer, raw_data) {
80			return (Err(format!("call into the runtime method '{name}': failed to write the input payload into guest memory: {err}").into()), None);
81		}
82
83		match self.0.call_typed(&mut (), pc, (data_pointer, raw_data_length)) {
84			Ok(()) => {},
85			Err(CallError::Trap) => {
86				return (
87					Err(format!("call into the runtime method '{name}' failed: trap").into()),
88					None,
89				)
90			},
91			Err(CallError::Error(err)) => {
92				return (
93					Err(format!("call into the runtime method '{name}' failed: {err}").into()),
94					None,
95				)
96			},
97			Err(CallError::User(err)) => {
98				return (
99					Err(format!("call into the runtime method '{name}' failed: {err}").into()),
100					None,
101				)
102			},
103			Err(CallError::NotEnoughGas) => unreachable!("gas metering is never enabled"),
104			Err(CallError::Step) => unreachable!("stepping is never enabled"),
105		};
106
107		let result_pointer = self.0.reg(Reg::A0);
108		let result_length = self.0.reg(Reg::A1);
109		let output = match self.0.read_memory(result_pointer as u32, result_length as u32) {
110			Ok(output) => output,
111			Err(error) => {
112				return (Err(format!("call into the runtime method '{name}' failed: failed to read the return payload: {error}").into()), None)
113			},
114		};
115
116		(Ok(output), None)
117	}
118}
119
120struct Context<'r, 'a>(&'r mut polkavm::Caller<'a, ()>);
121
122impl<'r, 'a> FunctionContext for Context<'r, 'a> {
123	fn read_memory_into(
124		&self,
125		address: Pointer<u8>,
126		dest: &mut [u8],
127	) -> subsoil::wasm_interface::Result<()> {
128		self.0
129			.instance
130			.read_memory_into(u32::from(address), dest)
131			.map_err(|error| error.to_string())
132			.map(|_| ())
133	}
134
135	fn write_memory(
136		&mut self,
137		address: Pointer<u8>,
138		data: &[u8],
139	) -> subsoil::wasm_interface::Result<()> {
140		self.0
141			.instance
142			.write_memory(u32::from(address), data)
143			.map_err(|error| error.to_string())
144	}
145
146	fn allocate_memory(&mut self, size: WordSize) -> subsoil::wasm_interface::Result<Pointer<u8>> {
147		let pointer = match self.0.instance.sbrk(0) {
148			Ok(pointer) => pointer.expect("fetching the current heap pointer never fails"),
149			Err(err) => return Err(format!("sbrk failed: {err}")),
150		};
151
152		// TODO: This will leak guest memory; find a better solution.
153		match self.0.instance.sbrk(size) {
154			Ok(Some(_)) => (),
155			Ok(None) => return Err(String::from("allocation error")),
156			Err(err) => return Err(format!("sbrk failed: {err}")),
157		}
158
159		Ok(Pointer::new(pointer))
160	}
161
162	fn deallocate_memory(&mut self, _ptr: Pointer<u8>) -> subsoil::wasm_interface::Result<()> {
163		// This is only used by the allocator host function, which is unused under PolkaVM.
164		unimplemented!("'deallocate_memory' is never used when running under PolkaVM");
165	}
166
167	fn register_panic_error_message(&mut self, _message: &str) {
168		unimplemented!("'register_panic_error_message' is never used when running under PolkaVM");
169	}
170}
171
172fn call_host_function(caller: &mut Caller<()>, function: &dyn Function) -> Result<(), String> {
173	let mut args = [Value::I64(0); Reg::ARG_REGS.len()];
174	let mut nth_reg = 0;
175	for (nth_arg, kind) in function.signature().args.iter().enumerate() {
176		match kind {
177			ValueType::I32 => {
178				args[nth_arg] = Value::I32(caller.instance.reg(Reg::ARG_REGS[nth_reg]) as i32);
179				nth_reg += 1;
180			},
181			ValueType::F32 => {
182				args[nth_arg] = Value::F32(caller.instance.reg(Reg::ARG_REGS[nth_reg]) as u32);
183				nth_reg += 1;
184			},
185			ValueType::I64 => {
186				if caller.instance.is_64_bit() {
187					args[nth_arg] = Value::I64(caller.instance.reg(Reg::ARG_REGS[nth_reg]) as i64);
188					nth_reg += 1;
189				} else {
190					let value_lo = caller.instance.reg(Reg::ARG_REGS[nth_reg]);
191					nth_reg += 1;
192
193					let value_hi = caller.instance.reg(Reg::ARG_REGS[nth_reg]);
194					nth_reg += 1;
195
196					args[nth_arg] =
197						Value::I64((u64::from(value_lo) | (u64::from(value_hi) << 32)) as i64);
198				}
199			},
200			ValueType::F64 => {
201				if caller.instance.is_64_bit() {
202					args[nth_arg] = Value::F64(caller.instance.reg(Reg::ARG_REGS[nth_reg]));
203					nth_reg += 1;
204				} else {
205					let value_lo = caller.instance.reg(Reg::ARG_REGS[nth_reg]);
206					nth_reg += 1;
207
208					let value_hi = caller.instance.reg(Reg::ARG_REGS[nth_reg]);
209					nth_reg += 1;
210
211					args[nth_arg] = Value::F64(u64::from(value_lo) | (u64::from(value_hi) << 32));
212				}
213			},
214		}
215	}
216
217	log::trace!(
218		"Calling host function: '{}', args = {:?}",
219		function.name(),
220		&args[..function.signature().args.len()]
221	);
222
223	let value = match function
224		.execute(&mut Context(caller), &mut args.into_iter().take(function.signature().args.len()))
225	{
226		Ok(value) => value,
227		Err(error) => {
228			let name = function.name();
229			return Err(format!("call into the host function '{name}' failed: {error}"));
230		},
231	};
232
233	if let Some(value) = value {
234		match value {
235			Value::I32(value) => {
236				caller.instance.set_reg(Reg::A0, value as u64);
237			},
238			Value::F32(value) => {
239				caller.instance.set_reg(Reg::A0, value as u64);
240			},
241			Value::I64(value) => {
242				if caller.instance.is_64_bit() {
243					caller.instance.set_reg(Reg::A0, value as u64);
244				} else {
245					caller.instance.set_reg(Reg::A0, value as u64);
246					caller.instance.set_reg(Reg::A1, (value >> 32) as u64);
247				}
248			},
249			Value::F64(value) => {
250				if caller.instance.is_64_bit() {
251					caller.instance.set_reg(Reg::A0, value as u64);
252				} else {
253					caller.instance.set_reg(Reg::A0, value as u64);
254					caller.instance.set_reg(Reg::A1, (value >> 32) as u64);
255				}
256			},
257		}
258	}
259
260	Ok(())
261}
262
263pub fn create_runtime<H>(blob: &polkavm::ProgramBlob) -> Result<Box<dyn WasmModule>, WasmError>
264where
265	H: HostFunctions,
266{
267	static ENGINE: std::sync::OnceLock<Result<polkavm::Engine, polkavm::Error>> =
268		std::sync::OnceLock::new();
269
270	let engine = ENGINE.get_or_init(|| {
271		let config = polkavm::Config::from_env()?;
272		polkavm::Engine::new(&config)
273	});
274
275	let engine = match engine {
276		Ok(ref engine) => engine,
277		Err(ref error) => {
278			return Err(WasmError::Other(error.to_string()));
279		},
280	};
281
282	let module =
283		polkavm::Module::from_blob(&engine, &polkavm::ModuleConfig::default(), blob.clone())?;
284
285	let mut linker = polkavm::Linker::new();
286
287	for function in H::host_functions() {
288		linker.define_untyped(function.name(), |mut caller: Caller<()>| {
289			call_host_function(&mut caller, function)
290		})?;
291	}
292	let instance_pre = linker.instantiate_pre(&module)?;
293	Ok(Box::new(InstancePre(instance_pre)))
294}