linera_witty/runtime/
memory.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Abstraction over how different runtimes manipulate the guest WebAssembly module's memory.
5
6use std::borrow::Cow;
7
8use frunk::{hlist, hlist_pat, HList};
9
10use super::{
11    traits::{CabiFreeAlias, CabiReallocAlias},
12    InstanceWithFunction, Runtime, RuntimeError,
13};
14use crate::{Layout, WitType};
15
16#[cfg(test)]
17#[path = "unit_tests/memory.rs"]
18mod tests;
19
20/// An address for a location in a guest WebAssembly module's memory.
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub struct GuestPointer(pub(crate) u32);
23
24impl GuestPointer {
25    /// Returns a new address that's the current address advanced to add padding to ensure it's
26    /// aligned to the `alignment` byte boundary.
27    pub const fn aligned_at(&self, alignment: u32) -> Self {
28        // The following computation is equivalent to:
29        // `(alignment - (self.0 % alignment)) % alignment`.
30        // Source: https://en.wikipedia.org/wiki/Data_structure_alignment#Computing_padding
31        let padding = (-(self.0 as i32) & (alignment as i32 - 1)) as u32;
32
33        GuestPointer(self.0 + padding)
34    }
35
36    /// Returns a new address that's the current address advanced to after the size of `T`.
37    pub const fn after<T: WitType>(&self) -> Self {
38        GuestPointer(self.0 + T::SIZE)
39    }
40
41    /// Returns a new address that's the current address advanced to add padding to ensure it's
42    /// aligned properly for `T`.
43    pub const fn after_padding_for<T: WitType>(&self) -> Self {
44        self.aligned_at(<T::Layout as Layout>::ALIGNMENT)
45    }
46
47    /// Returns the address of an element in a contiguous list of properly aligned `T` types.
48    pub const fn index<T: WitType>(&self, index: u32) -> Self {
49        let element_size = GuestPointer(T::SIZE).after_padding_for::<T>();
50
51        GuestPointer(self.0 + index * element_size.0)
52    }
53}
54
55/// Interface for accessing a runtime specific memory.
56pub trait RuntimeMemory<Instance> {
57    /// Reads `length` bytes from memory from the provided `location`.
58    fn read<'instance>(
59        &self,
60        instance: &'instance Instance,
61        location: GuestPointer,
62        length: u32,
63    ) -> Result<Cow<'instance, [u8]>, RuntimeError>;
64
65    /// Writes the `bytes` to memory at the provided `location`.
66    fn write(
67        &mut self,
68        instance: &mut Instance,
69        location: GuestPointer,
70        bytes: &[u8],
71    ) -> Result<(), RuntimeError>;
72}
73
74/// A handle to interface with a guest Wasm module instance's memory.
75#[expect(clippy::type_complexity)]
76pub struct Memory<'runtime, Instance>
77where
78    Instance: CabiReallocAlias + CabiFreeAlias,
79{
80    instance: &'runtime mut Instance,
81    memory: <Instance::Runtime as Runtime>::Memory,
82    cabi_realloc: Option<
83        <Instance as InstanceWithFunction<HList![i32, i32, i32, i32], HList![i32]>>::Function,
84    >,
85    cabi_free: Option<<Instance as InstanceWithFunction<HList![i32], HList![]>>::Function>,
86}
87
88impl<'runtime, Instance> Memory<'runtime, Instance>
89where
90    Instance: CabiReallocAlias + CabiFreeAlias,
91{
92    /// Creates a new [`Memory`] instance using a Wasm module `instance` and its `memory` export.
93    pub(super) fn new(
94        instance: &'runtime mut Instance,
95        memory: <Instance::Runtime as Runtime>::Memory,
96    ) -> Self {
97        Memory {
98            instance,
99            memory,
100            cabi_realloc: None,
101            cabi_free: None,
102        }
103    }
104}
105
106impl<Instance> Memory<'_, Instance>
107where
108    Instance: CabiReallocAlias + CabiFreeAlias,
109    <Instance::Runtime as Runtime>::Memory: RuntimeMemory<Instance>,
110{
111    /// Reads `length` bytes from `location`.
112    ///
113    /// The underlying runtime may return either a memory slice or an owned buffer.
114    pub fn read(&self, location: GuestPointer, length: u32) -> Result<Cow<'_, [u8]>, RuntimeError> {
115        self.memory.read(&*self.instance, location, length)
116    }
117
118    /// Writes `bytes` to `location`.
119    pub fn write(&mut self, location: GuestPointer, bytes: &[u8]) -> Result<(), RuntimeError> {
120        self.memory.write(&mut *self.instance, location, bytes)
121    }
122
123    /// Returns a newly allocated buffer of `size` bytes in the guest module's memory.
124    ///
125    /// Calls the guest module to allocate the memory, so the resulting allocation is managed by
126    /// the guest.
127    pub fn allocate(&mut self, size: u32) -> Result<GuestPointer, RuntimeError> {
128        if self.cabi_realloc.is_none() {
129            self.cabi_realloc = Some(<Instance as InstanceWithFunction<
130                HList![i32, i32, i32, i32],
131                HList![i32],
132            >>::load_function(self.instance, "cabi_realloc")?);
133        }
134
135        let size = i32::try_from(size).map_err(|_| RuntimeError::AllocationTooLarge)?;
136
137        let cabi_realloc = self
138            .cabi_realloc
139            .as_ref()
140            .expect("`cabi_realloc` function was not loaded before it was called");
141
142        let hlist_pat![allocation_address] =
143            self.instance.call(cabi_realloc, hlist![0, 0, 1, size])?;
144
145        Ok(GuestPointer(
146            allocation_address
147                .try_into()
148                .map_err(|_| RuntimeError::AllocationFailed)?,
149        ))
150    }
151
152    /// Deallocates the `allocation` managed by the guest.
153    pub fn deallocate(&mut self, allocation: GuestPointer) -> Result<(), RuntimeError> {
154        if self.cabi_free.is_none() {
155            self.cabi_free = Some(
156                <Instance as InstanceWithFunction<HList![i32], HList![]>>::load_function(
157                    self.instance,
158                    "cabi_free",
159                )?,
160            );
161        }
162
163        let address = allocation
164            .0
165            .try_into()
166            .map_err(|_| RuntimeError::DeallocateInvalidAddress)?;
167
168        let cabi_free = self
169            .cabi_free
170            .as_ref()
171            .expect("`cabi_free` function was not loaded before it was called");
172
173        self.instance.call(cabi_free, hlist![address])?;
174
175        Ok(())
176    }
177}