Skip to main content

gear_core/
pages.rs

1// Copyright (C) Gear Technologies Inc.
2// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
3
4//! Module for memory pages.
5
6use alloc::format;
7use core::cmp::Ordering;
8use num_traits::bounds::{LowerBounded, UpperBounded};
9use numerated::{Bound, Numerated, interval::Interval, iterators::IntervalIterator};
10use scale_decode::DecodeAsType;
11use scale_encode::EncodeAsType;
12use scale_info::{
13    TypeInfo,
14    scale::{Decode, Encode},
15};
16
17pub use numerated::{self, num_traits};
18
19/// A WebAssembly page has a constant size of 64KiB.
20const WASM_PAGE_SIZE: u32 = 64 * 1024;
21
22/// A size of memory pages in program data storage.
23/// If program changes some memory page during execution, then page of this size will be uploaded to the storage.
24/// If during execution program accesses some data in memory, then data of this size will be downloaded from the storage.
25/// Currently equal to 16KiB to be bigger than most common host page sizes.
26const GEAR_PAGE_SIZE: u32 = 16 * 1024;
27
28const _: () = assert!(WASM_PAGE_SIZE.is_multiple_of(GEAR_PAGE_SIZE));
29
30/// Struct represents memory pages amount with some constant size `SIZE` in bytes.
31/// - `SIZE` type is u32, so page size < 4GiB (wasm32 memory size limit).
32/// - `SIZE` must be power of two and must not be equal to one or zero bytes.
33#[derive(
34    Clone,
35    Copy,
36    Debug,
37    Decode,
38    DecodeAsType,
39    Encode,
40    EncodeAsType,
41    PartialEq,
42    Eq,
43    PartialOrd,
44    Ord,
45    Hash,
46    TypeInfo,
47    Default,
48    derive_more::Into,
49)]
50pub struct PagesAmount<const SIZE: u32>(u32);
51
52impl<const SIZE: u32> PagesAmount<SIZE> {
53    /// Page size. May be any number power of two in interval [2, u32::MAX].
54    ///
55    /// NOTE: In case SIZE == 0 or 1 or any not power of two number, then you would receive compilation error.
56    pub const SIZE: u32 = SIZE;
57
58    /// Number of max pages amount. Equal to max page number + 1.
59    ///
60    /// NOTE: const computation contains checking in order to prevent incorrect SIZE.
61    pub const UPPER: Self = Self(u32::MAX / SIZE + 1 / if SIZE.is_power_of_two() { 1 } else { 0 });
62
63    /// Pages amount addition. Returns None if overflow.
64    #[cfg(test)]
65    pub fn add(&self, other: Self) -> Option<Self> {
66        self.0
67            .checked_add(other.0)
68            .and_then(|r| (r <= Self::UPPER.0).then_some(Self(r)))
69    }
70
71    /// Get page number, which bounds this pages amount.
72    /// If pages amount == 4GB size, then returns None, because such page number does not exist.
73    pub fn to_page_number(&self) -> Option<Page<SIZE>> {
74        self.unbound()
75    }
76
77    /// Returns corresponding amount of pages with another size `S`.
78    pub fn to_pages_amount<const S: u32>(&self) -> PagesAmount<S> {
79        let raw = if Self::SIZE > S {
80            (Self::SIZE / S) * self.0
81        } else {
82            self.0 / (S / Self::SIZE)
83        };
84        PagesAmount(raw)
85    }
86
87    /// Returns amount in bytes.
88    /// Can be also considered as offset of a page with corresponding number.
89    /// In 32-bits address space it can be up to u32::MAX + 1,
90    /// so we returns u64 to prevent overflow.
91    pub fn offset(&self) -> u64 {
92        self.0 as u64 * SIZE as u64
93    }
94}
95
96impl<const SIZE: u32> From<Page<SIZE>> for PagesAmount<SIZE> {
97    fn from(value: Page<SIZE>) -> Self {
98        Self(value.0)
99    }
100}
101
102impl<const SIZE: u32> From<Option<Page<SIZE>>> for PagesAmount<SIZE> {
103    fn from(value: Option<Page<SIZE>>) -> Self {
104        value.map(|page| page.into()).unwrap_or(Self::UPPER)
105    }
106}
107
108impl<const SIZE: u32> Bound<Page<SIZE>> for PagesAmount<SIZE> {
109    fn unbound(self) -> Option<Page<SIZE>> {
110        match self.cmp(&Self::UPPER) {
111            Ordering::Greater => {
112                // This panic is impossible because of `PagesAmount` constructors implementation.
113                let err_msg = format!(
114                    "PagesAmount::unbound: PageBound must be always less or equal than UPPER. \
115                    Page bound - {:?}, UPPER - {:?}",
116                    self,
117                    Self::UPPER
118                );
119
120                log::error!("{err_msg}");
121                unreachable!("{err_msg}")
122            }
123            Ordering::Equal => None,
124            Ordering::Less => Some(Page(self.0)),
125        }
126    }
127}
128
129/// Try from u32 error for [PagesAmount].
130#[derive(Debug, Clone, derive_more::Display)]
131#[display("Tries to make pages amount from {_0}, which must be less or equal to {_1}")]
132pub struct PagesAmountError(u32, u32);
133
134impl<const SIZE: u32> TryFrom<u32> for PagesAmount<SIZE> {
135    type Error = PagesAmountError;
136
137    fn try_from(raw: u32) -> Result<Self, Self::Error> {
138        if raw > Self::UPPER.0 {
139            Err(PagesAmountError(raw, Self::UPPER.0))
140        } else {
141            Ok(Self(raw))
142        }
143    }
144}
145
146impl<const SIZE: u32> PartialEq<Page<SIZE>> for PagesAmount<SIZE> {
147    fn eq(&self, other: &Page<SIZE>) -> bool {
148        self.0 == other.0
149    }
150}
151
152impl<const SIZE: u32> PartialOrd<Page<SIZE>> for PagesAmount<SIZE> {
153    fn partial_cmp(&self, other: &Page<SIZE>) -> Option<Ordering> {
154        self.0.partial_cmp(&other.0)
155    }
156}
157
158impl<const SIZE: u32> PartialEq<PagesAmount<SIZE>> for Page<SIZE> {
159    fn eq(&self, other: &PagesAmount<SIZE>) -> bool {
160        self.0 == other.0
161    }
162}
163
164impl<const SIZE: u32> PartialOrd<PagesAmount<SIZE>> for Page<SIZE> {
165    fn partial_cmp(&self, other: &PagesAmount<SIZE>) -> Option<Ordering> {
166        self.0.partial_cmp(&other.0)
167    }
168}
169
170/// Struct represents memory page number with some constant size `SIZE` in bytes.
171/// - `SIZE` type is u32, so page size < 4GiB (wasm32 memory size limit).
172/// - `SIZE` must be power of two and must not be equal to zero bytes.
173/// - `SIZE == 1` is possible, but then you cannot use [PagesAmount] for these pages.
174#[derive(
175    Clone,
176    Copy,
177    Debug,
178    Decode,
179    DecodeAsType,
180    Encode,
181    EncodeAsType,
182    PartialEq,
183    Eq,
184    PartialOrd,
185    Ord,
186    Hash,
187    TypeInfo,
188    Default,
189    derive_more::Into,
190    derive_more::Display,
191)]
192#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
193pub struct Page<const SIZE: u32>(u32);
194
195impl<const SIZE: u32> Page<SIZE> {
196    /// Page size. May be any number power of two in interval [1, u32::MAX].
197    pub const SIZE: u32 = SIZE;
198
199    /// Max possible page number in 4GB memory.
200    ///
201    /// Note: const computation contains checking in order to prevent incorrect SIZE.
202    #[allow(clippy::erasing_op)]
203    pub const UPPER: Self = Self(u32::MAX / SIZE + 0 / if SIZE.is_power_of_two() { 1 } else { 0 });
204
205    /// Increment page number. Returns `PagesAmount<SIZE>`, because this allows to avoid overflows.
206    pub fn inc(&self) -> PagesAmount<SIZE> {
207        PagesAmount(self.0 + 1)
208    }
209
210    /// Constructs new page from byte offset: returns page which contains this byte.
211    pub fn from_offset(offset: u32) -> Self {
212        Self(offset / Self::SIZE)
213    }
214
215    /// Returns page zero byte offset.
216    pub fn offset(&self) -> u32 {
217        self.0 * Self::SIZE
218    }
219
220    /// Returns page last byte offset.
221    pub fn end_offset(&self) -> u32 {
222        self.0 * Self::SIZE + (Self::SIZE - 1)
223    }
224
225    /// Returns new page, which contains `self` zero byte.
226    pub fn to_page<const S1: u32>(self) -> Page<S1> {
227        Page::from_offset(self.offset())
228    }
229
230    /// Returns an iterator that iterates over the range of pages from `self` to the end page,
231    /// inclusive. Each iteration yields a page of type [`Page<S1>`].
232    ///
233    /// # Example
234    ///
235    /// ```
236    /// use gear_core::pages::{GearPage, WasmPage};
237    ///
238    /// let x: Vec<GearPage> = WasmPage::from(5).to_iter().collect();
239    /// println!("{x:?}");
240    /// ```
241    /// For this example must be printed: `[GearPage(20), GearPage(21), GearPage(22), GearPage(23)]`
242    pub fn to_iter<const S1: u32>(self) -> IntervalIterator<Page<S1>> {
243        let start = Page::<S1>::from_offset(self.offset());
244        let end = Page::<S1>::from_offset(self.end_offset());
245        // Safe, cause end byte offset is always greater or equal to offset, so `start <= end`.
246        unsafe { Interval::new_unchecked(start, end).iter() }
247    }
248}
249
250/// Try from u32 error for [Page].
251#[derive(Debug, Clone, derive_more::Display, derive_more::Error)]
252#[display("Tries to make page from {_0}, which must be less or equal to {_1}")]
253pub struct PageError(u32, u32);
254
255impl<const SIZE: u32> TryFrom<u32> for Page<SIZE> {
256    type Error = PageError;
257
258    fn try_from(raw: u32) -> Result<Self, Self::Error> {
259        if raw > Self::UPPER.0 {
260            Err(PageError(raw, Self::UPPER.0))
261        } else {
262            Ok(Self(raw))
263        }
264    }
265}
266
267impl<const SIZE: u32> Numerated for Page<SIZE> {
268    type Distance = u32;
269    type Bound = PagesAmount<SIZE>;
270
271    fn add_if_enclosed_by(self, num: Self::Distance, other: Self) -> Option<Self> {
272        self.0
273            .checked_add(num)
274            .and_then(|sum| sum.enclosed_by(&self.0, &other.0).then_some(Self(sum)))
275    }
276
277    fn sub_if_enclosed_by(self, num: Self::Distance, other: Self) -> Option<Self> {
278        self.0
279            .checked_sub(num)
280            .and_then(|sub| sub.enclosed_by(&self.0, &other.0).then_some(Self(sub)))
281    }
282
283    fn distance(self, other: Self) -> Self::Distance {
284        self.0.abs_diff(other.0)
285    }
286}
287
288impl<const SIZE: u32> LowerBounded for Page<SIZE> {
289    fn min_value() -> Self {
290        Self(0)
291    }
292}
293
294impl<const SIZE: u32> UpperBounded for Page<SIZE> {
295    fn max_value() -> Self {
296        Self::UPPER
297    }
298}
299
300/// Page of wasm page size - 64 kiB.
301pub type WasmPage = Page<WASM_PAGE_SIZE>;
302/// Page of gear page size - 16 kiB.
303pub type GearPage = Page<GEAR_PAGE_SIZE>;
304/// Pages amount for wasm page size - 64 kiB.
305pub type WasmPagesAmount = PagesAmount<WASM_PAGE_SIZE>;
306/// Pages amount for gear page size - 16 kiB.
307pub type GearPagesAmount = PagesAmount<GEAR_PAGE_SIZE>;
308
309impl From<u16> for WasmPagesAmount {
310    fn from(value: u16) -> Self {
311        const { assert!(WASM_PAGE_SIZE <= 0x10_000) };
312        Self(value as u32)
313    }
314}
315
316impl From<u16> for WasmPage {
317    fn from(value: u16) -> Self {
318        const { assert!(WASM_PAGE_SIZE <= 0x10_000) };
319        Self(value as u32)
320    }
321}
322
323impl From<u16> for GearPagesAmount {
324    fn from(value: u16) -> Self {
325        const { assert!(GEAR_PAGE_SIZE <= 0x10_000) };
326        Self(value as u32)
327    }
328}
329
330impl From<u16> for GearPage {
331    fn from(value: u16) -> Self {
332        const { assert!(GEAR_PAGE_SIZE <= 0x10_000) };
333        Self(value as u32)
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340    use alloc::{vec, vec::Vec};
341
342    #[test]
343    fn test_page_inc() {
344        assert_eq!(WasmPage::from(10).inc(), WasmPagesAmount::from(11));
345        assert_eq!(WasmPage::UPPER.inc(), WasmPagesAmount::UPPER);
346    }
347
348    #[test]
349    fn test_page_from_offset() {
350        assert_eq!(WasmPage::from_offset(WASM_PAGE_SIZE - 1), WasmPage::from(0));
351        assert_eq!(WasmPage::from_offset(WASM_PAGE_SIZE), WasmPage::from(1));
352        assert_eq!(WasmPage::from_offset(WASM_PAGE_SIZE + 1), WasmPage::from(1));
353    }
354
355    #[test]
356    fn test_page_offset() {
357        assert_eq!(WasmPage::from(80).offset(), 80 * WASM_PAGE_SIZE);
358    }
359
360    #[test]
361    fn test_page_end_offset() {
362        assert_eq!(
363            WasmPage::from(80).end_offset(),
364            80 * WASM_PAGE_SIZE + (WASM_PAGE_SIZE - 1)
365        );
366    }
367
368    #[test]
369    fn test_page_to_page() {
370        assert_eq!(
371            WasmPage::from(80).to_page::<GEAR_PAGE_SIZE>(),
372            GearPage::from(80 * 4)
373        );
374    }
375
376    #[test]
377    fn test_page_to_iter() {
378        assert_eq!(
379            WasmPage::from(5).to_iter().collect::<Vec<GearPage>>(),
380            vec![
381                GearPage::from(20),
382                GearPage::from(21),
383                GearPage::from(22),
384                GearPage::from(23)
385            ]
386        );
387    }
388
389    #[test]
390    fn test_pages_amount_add() {
391        let a = WasmPagesAmount::from(10);
392        let b = WasmPagesAmount::from(20);
393        assert_eq!(a.add(b), Some(WasmPagesAmount::from(30)));
394        assert_eq!(a.add(WasmPagesAmount::UPPER), None);
395    }
396
397    #[test]
398    fn test_pages_amount_to_page_number() {
399        assert_eq!(
400            WasmPagesAmount::from(10).to_page_number(),
401            Some(WasmPage::from(10))
402        );
403        assert_eq!(WasmPagesAmount::UPPER.to_page_number(), None);
404    }
405
406    #[test]
407    fn test_pages_amount_to_pages_amount() {
408        assert_eq!(
409            WasmPagesAmount::from(10).to_pages_amount::<GEAR_PAGE_SIZE>(),
410            GearPagesAmount::from(40)
411        );
412        assert_eq!(
413            GearPagesAmount::from(40).to_pages_amount::<WASM_PAGE_SIZE>(),
414            WasmPagesAmount::from(10)
415        );
416    }
417
418    #[test]
419    fn test_pages_amount_offset() {
420        assert_eq!(
421            WasmPagesAmount::from(10).offset(),
422            10 * WASM_PAGE_SIZE as u64
423        );
424        assert_eq!(WasmPagesAmount::UPPER.offset(), u32::MAX as u64 + 1);
425    }
426}
427
428#[cfg(test)]
429mod property_tests {
430    use super::*;
431    use numerated::mock::{self, IntervalAction};
432    use proptest::{
433        prelude::{Arbitrary, any},
434        proptest,
435        strategy::{BoxedStrategy, Strategy},
436        test_runner::Config as ProptestConfig,
437    };
438
439    impl<const S: u32> Arbitrary for Page<S> {
440        type Parameters = ();
441        type Strategy = BoxedStrategy<Page<S>>;
442
443        fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
444            (0..=Page::<S>::UPPER.0).prop_map(Page).boxed()
445        }
446    }
447
448    proptest! {
449        #![proptest_config(ProptestConfig::with_cases(1024))]
450
451        #[test]
452        fn gear_page_numerated(x in any::<GearPage>(), y in any::<GearPage>()) {
453            mock::test_numerated(x, y);
454        }
455
456        #[test]
457        fn gear_page_interval(action in any::<IntervalAction<GearPage>>()) {
458            mock::test_interval(action);
459        }
460
461        #[test]
462        fn wasm_page_numerated(x in any::<WasmPage>(), y in any::<WasmPage>()) {
463            mock::test_numerated(x, y);
464        }
465
466        #[test]
467        fn wasm_page_interval(action in any::<IntervalAction<WasmPage>>()) {
468            mock::test_interval(action);
469        }
470    }
471
472    proptest! {
473        #![proptest_config(ProptestConfig::with_cases(64))]
474
475        #[test]
476        fn gear_page_tree((initial, actions) in mock::tree_actions::<GearPage>(0..128, 2..8)) {
477            mock::test_tree(initial, actions);
478        }
479
480        #[test]
481        fn wasm_page_tree((initial, actions) in mock::tree_actions::<WasmPage>(0..128, 10..20)) {
482            mock::test_tree(initial, actions);
483        }
484    }
485}