From a77d4863aeae1c609fb0afe5018559f46f6b69a4 Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Fri, 1 Dec 2023 23:03:41 -0700 Subject: [PATCH 1/7] mini-alloc: a very simple bump allocator --- Cargo.lock | 152 +++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 +- examples/erc20/Cargo.lock | 63 +++------------ examples/erc20/Cargo.toml | 4 +- examples/erc20/src/main.rs | 3 +- mini-alloc/Cargo.toml | 17 +++++ mini-alloc/README.md | 22 ++++++ mini-alloc/src/imp.rs | 67 ++++++++++++++++ mini-alloc/src/lib.rs | 13 ++++ mini-alloc/tests/misc.rs | 33 ++++++++ 10 files changed, 321 insertions(+), 55 deletions(-) create mode 100644 mini-alloc/Cargo.toml create mode 100644 mini-alloc/README.md create mode 100644 mini-alloc/src/imp.rs create mode 100644 mini-alloc/src/lib.rs create mode 100644 mini-alloc/tests/misc.rs diff --git a/Cargo.lock b/Cargo.lock index ef6b96c..6509267 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,6 +74,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + [[package]] name = "byteorder" version = "1.4.3" @@ -92,6 +98,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "const-hex" version = "1.6.1" @@ -224,6 +240,15 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +[[package]] +name = "js-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "keccak" version = "0.1.4" @@ -257,12 +282,26 @@ version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "mini-alloc" +version = "0.4.2" +dependencies = [ + "cfg-if", + "wasm-bindgen-test", +] + [[package]] name = "num-traits" version = "0.2.16" @@ -273,6 +312,12 @@ dependencies = [ "libm", ] +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + [[package]] name = "paste" version = "1.0.14" @@ -410,6 +455,12 @@ dependencies = [ "semver", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "semver" version = "1.0.18" @@ -545,6 +596,107 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wasm-bindgen" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.27", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cf9242c0d27999b831eae4767b2a146feb0b27d332d553e605864acd2afd403" +dependencies = [ + "console_error_panic_hook", + "js-sys", + "scoped-tls", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "794645f5408c9a039fd09f4d113cdfb2e7eba5ff1956b07bcf701cf4b394fe89" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "web-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "zeroize" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index c21d398..b38370b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["stylus-sdk", "stylus-proc"] +members = ["stylus-sdk", "stylus-proc", "mini-alloc"] resolver = "2" [workspace.package] diff --git a/examples/erc20/Cargo.lock b/examples/erc20/Cargo.lock index 578ef6e..7988a62 100644 --- a/examples/erc20/Cargo.lock +++ b/examples/erc20/Cargo.lock @@ -19,7 +19,7 @@ checksum = "e416903084d3392ebd32d94735c395d6709415b76c7728e594d3f996f2b03e65" dependencies = [ "alloy-rlp", "bytes", - "cfg-if 1.0.0", + "cfg-if", "const-hex", "derive_more", "hex-literal", @@ -137,12 +137,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - [[package]] name = "cfg-if" version = "1.0.0" @@ -155,7 +149,7 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08849ed393c907c90016652a01465a12d86361cd38ad2a7de026c56a520cc259" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "cpufeatures", "hex", "serde", @@ -247,8 +241,8 @@ version = "0.1.0" dependencies = [ "alloy-primitives", "alloy-sol-types", + "mini-alloc", "stylus-sdk", - "wee_alloc", ] [[package]] @@ -300,7 +294,7 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "wasi", ] @@ -375,10 +369,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e" [[package]] -name = "memory_units" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" +name = "mini-alloc" +version = "0.4.2" +dependencies = [ + "cfg-if", +] [[package]] name = "num-traits" @@ -628,7 +623,7 @@ version = "0.4.2" dependencies = [ "alloy-primitives", "alloy-sol-types", - "cfg-if 1.0.0", + "cfg-if", "convert_case 0.6.0", "lazy_static", "proc-macro2", @@ -645,7 +640,7 @@ version = "0.4.2" dependencies = [ "alloy-primitives", "alloy-sol-types", - "cfg-if 1.0.0", + "cfg-if", "derivative", "fnv", "hex", @@ -694,7 +689,7 @@ version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "fastrand", "redox_syscall", "rustix", @@ -761,40 +756,6 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "wee_alloc" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" -dependencies = [ - "cfg-if 0.1.10", - "libc", - "memory_units", - "winapi", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-sys" version = "0.48.0" diff --git a/examples/erc20/Cargo.toml b/examples/erc20/Cargo.toml index e02c8f5..b55e876 100644 --- a/examples/erc20/Cargo.toml +++ b/examples/erc20/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" alloy-primitives = "0.3.1" alloy-sol-types = "0.3.1" stylus-sdk = { path = "../../../stylus-sdk-rs/stylus-sdk" } -wee_alloc = "0.4.5" +mini-alloc = { path = "../../mini-alloc" } [features] export-abi = ["stylus-sdk/export-abi"] @@ -19,4 +19,4 @@ lto = true panic = "abort" opt-level = "s" -[workspace] \ No newline at end of file +[workspace] diff --git a/examples/erc20/src/main.rs b/examples/erc20/src/main.rs index 6778a35..de2cb9d 100644 --- a/examples/erc20/src/main.rs +++ b/examples/erc20/src/main.rs @@ -5,8 +5,9 @@ use crate::erc20::{Erc20, Erc20Params}; use alloc::{string::String, vec::Vec}; use stylus_sdk::{alloy_primitives::U256, call, msg, prelude::*}; +#[cfg(target_arch = "wasm32")] #[global_allocator] -static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; +static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc; mod erc20; diff --git a/mini-alloc/Cargo.toml b/mini-alloc/Cargo.toml new file mode 100644 index 0000000..66b77f9 --- /dev/null +++ b/mini-alloc/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "mini-alloc" +keywords = ["wasm", "stylus", "allocator"] +description = "Very simple global allocator" +readme = "README.md" + +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true + +[dev-dependencies] +wasm-bindgen-test = "0.3.0" + +[dependencies] +cfg-if = "1.0.0" diff --git a/mini-alloc/README.md b/mini-alloc/README.md new file mode 100644 index 0000000..3bdc2fe --- /dev/null +++ b/mini-alloc/README.md @@ -0,0 +1,22 @@ +# mini-alloc + +`mini-alloc` is a very small bump allocator for wasm32 intended to be used as +the global Rust allocator. It never deallocates memory -- that is, `dealloc` +does nothing. It's suitable for cases where binary size is at a premium and +it's acceptable to leak all allocations. + +One other major limitation: this crate is not thread safe! `MiniAlloc` +implements `Sync` because that is a required of a global allocator, but this is +not a valid implementation of `Sync`, and it must only be used from a single +thread. + +Also, `core::arch::wasm32::memory_grow` must never be called by any code outside +this crate. + +Use it like this: + +```rust +#[cfg(target_arch = "wasm32")] +#[global_allocator] +static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc; +``` diff --git a/mini-alloc/src/imp.rs b/mini-alloc/src/imp.rs new file mode 100644 index 0000000..f3a5c91 --- /dev/null +++ b/mini-alloc/src/imp.rs @@ -0,0 +1,67 @@ +// Copyright 2023, Offchain Labs, Inc. +// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md + +use core::{ + alloc::{GlobalAlloc, Layout}, + arch::wasm32, + num::NonZeroUsize as NonZero, +}; + +pub struct MiniAlloc; + +/// This is not a valid implementation of [`Sync`] but is ok in single-threaded WASM. +unsafe impl Sync for MiniAlloc {} + +unsafe impl GlobalAlloc for MiniAlloc { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + alloc_impl(layout).unwrap_or(core::ptr::null_mut()) + } + + #[inline] + unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {} +} + +extern "C" { + /// This symbol is created by the LLVM linker. + static __heap_base: u8; +} + +/// The WASM page size, or 2^16 bytes. +const PAGE_SIZE: usize = 1 << 16; + +/// Represents the negation of the allocator's bump offset and boundary +/// +/// We store the negation because we can align the negative offset in fewer +/// instructions than the positive offset. +static mut STATE: Option<(NonZero, usize)> = None; + +fn alloc_impl(layout: Layout) -> Option<*mut u8> { + let (neg_offset, neg_bound) = unsafe { &mut STATE }.get_or_insert_with(|| { + let heap_base = unsafe { &__heap_base } as *const u8 as usize; + let bound = PAGE_SIZE * wasm32::memory_size(0); + ( + unsafe { NonZero::new_unchecked(heap_base.wrapping_neg()) }, + bound.wrapping_neg(), + ) + }); + + let neg_aligned = make_aligned(neg_offset.get(), layout.align()); + let next_neg_offset = neg_aligned.checked_sub(layout.size())?; + let bytes_needed = neg_bound.saturating_sub(next_neg_offset); + if bytes_needed != 0 { + let pages_needed = 1 + (bytes_needed - 1) / PAGE_SIZE; + if wasm32::memory_grow(0, pages_needed) == usize::MAX { + return None; + } + *neg_bound -= PAGE_SIZE * pages_needed; + } + *neg_offset = unsafe { NonZero::new_unchecked(next_neg_offset) }; + Some(neg_aligned.wrapping_neg() as *mut u8) +} + +/// Returns `value` rounded down to the next multiple of `align`. +/// Note: `align` must be a power of two, which is guaranteed by [`Layout::align`]. +#[inline(always)] +fn make_aligned(value: usize, align: usize) -> usize { + value & align.wrapping_neg() +} diff --git a/mini-alloc/src/lib.rs b/mini-alloc/src/lib.rs new file mode 100644 index 0000000..216615c --- /dev/null +++ b/mini-alloc/src/lib.rs @@ -0,0 +1,13 @@ +// Copyright 2023, Offchain Labs, Inc. +// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md + +#![no_std] + +use cfg_if::cfg_if; + +cfg_if! { + if #[cfg(target_arch = "wasm32")] { + mod imp; + pub use imp::MiniAlloc; + } +} diff --git a/mini-alloc/tests/misc.rs b/mini-alloc/tests/misc.rs new file mode 100644 index 0000000..19df015 --- /dev/null +++ b/mini-alloc/tests/misc.rs @@ -0,0 +1,33 @@ +// Copyright 2023, Offchain Labs, Inc. +// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md + +#![no_std] + +extern crate alloc; + +use alloc::vec::Vec; + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen_test::*; + +#[cfg(target_arch = "wasm32")] +#[global_allocator] +static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc; + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen_test] +fn vec_test() { + let p1 = Vec::::with_capacity(700); + let p2 = Vec::::with_capacity(65536); + let p3 = Vec::::with_capacity(700000); + let p4 = Vec::::with_capacity(1); + let p5 = Vec::::with_capacity(1); + let p6 = Vec::::with_capacity(1); + assert_eq!(p1.as_ptr() as usize + 700, p2.as_ptr() as usize); + assert_eq!(p2.as_ptr() as usize + 65536, p3.as_ptr() as usize); + assert!((p4.as_ptr() as usize) < p3.as_ptr() as usize + 700004); + assert!((p4.as_ptr() as usize) >= p3.as_ptr() as usize + 700000); + assert_eq!(p4.as_ptr() as usize & 3, 0); + assert_eq!(p4.as_ptr() as usize + 4, p5.as_ptr() as usize); + assert_eq!(p5.as_ptr() as usize + 2, p6.as_ptr() as usize); +} From 5c82dcbc69239484f10e13904f09db9111bae277 Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Mon, 4 Dec 2023 12:36:09 -0700 Subject: [PATCH 2/7] bring back WeeAlloc; README edit; make the test compile on non-wasm targets; unsafe fn --- Cargo.lock | 63 +++++++++++++++++++++++++++++++++++----- mini-alloc/Cargo.toml | 3 ++ mini-alloc/README.md | 6 ++-- mini-alloc/src/imp.rs | 14 +++++---- mini-alloc/src/lib.rs | 2 ++ mini-alloc/tests/misc.rs | 4 +-- 6 files changed, 75 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6509267..6d6fa83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,7 +18,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66f73f11dcfbf8bb763d88fb1d082fe7cca0a00d3227d9921bdbd52ce5e013e2" dependencies = [ "bytes", - "cfg-if", + "cfg-if 1.0.0", "const-hex", "derive_more", "hex-literal", @@ -92,6 +92,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cfg-if" version = "1.0.0" @@ -104,7 +110,7 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "wasm-bindgen", ] @@ -114,7 +120,7 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "268f52aae268980d03dd9544c1ea591965b2735b038d6998d6e4ab37c8c24445" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "hex", "serde", @@ -294,12 +300,19 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memory_units" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" + [[package]] name = "mini-alloc" version = "0.4.2" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "wasm-bindgen-test", + "wee_alloc", ] [[package]] @@ -489,7 +502,7 @@ version = "0.4.2" dependencies = [ "alloy-primitives", "alloy-sol-types", - "cfg-if", + "cfg-if 1.0.0", "convert_case 0.6.0", "lazy_static", "proc-macro2", @@ -506,7 +519,7 @@ version = "0.4.2" dependencies = [ "alloy-primitives", "alloy-sol-types", - "cfg-if", + "cfg-if 1.0.0", "derivative", "fnv", "hex", @@ -602,7 +615,7 @@ version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "wasm-bindgen-macro", ] @@ -627,7 +640,7 @@ version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "js-sys", "wasm-bindgen", "web-sys", @@ -697,6 +710,40 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "wee_alloc" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "memory_units", + "winapi", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "zeroize" version = "1.6.0" diff --git a/mini-alloc/Cargo.toml b/mini-alloc/Cargo.toml index 66b77f9..9d25951 100644 --- a/mini-alloc/Cargo.toml +++ b/mini-alloc/Cargo.toml @@ -15,3 +15,6 @@ wasm-bindgen-test = "0.3.0" [dependencies] cfg-if = "1.0.0" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +wee_alloc = "0.4.5" diff --git a/mini-alloc/README.md b/mini-alloc/README.md index 3bdc2fe..c90d4ed 100644 --- a/mini-alloc/README.md +++ b/mini-alloc/README.md @@ -13,10 +13,12 @@ thread. Also, `core::arch::wasm32::memory_grow` must never be called by any code outside this crate. +On targets other than wasm32, `MiniAlloc` simply forwards to the allocator from +another crate, `wee_alloc::WeeAlloc`. + Use it like this: ```rust -#[cfg(target_arch = "wasm32")] #[global_allocator] -static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc; +static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc::INIT; ``` diff --git a/mini-alloc/src/imp.rs b/mini-alloc/src/imp.rs index f3a5c91..5524a4b 100644 --- a/mini-alloc/src/imp.rs +++ b/mini-alloc/src/imp.rs @@ -12,6 +12,10 @@ pub struct MiniAlloc; /// This is not a valid implementation of [`Sync`] but is ok in single-threaded WASM. unsafe impl Sync for MiniAlloc {} +impl MiniAlloc { + pub const INIT: Self = MiniAlloc; +} + unsafe impl GlobalAlloc for MiniAlloc { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { alloc_impl(layout).unwrap_or(core::ptr::null_mut()) @@ -35,12 +39,12 @@ const PAGE_SIZE: usize = 1 << 16; /// instructions than the positive offset. static mut STATE: Option<(NonZero, usize)> = None; -fn alloc_impl(layout: Layout) -> Option<*mut u8> { - let (neg_offset, neg_bound) = unsafe { &mut STATE }.get_or_insert_with(|| { - let heap_base = unsafe { &__heap_base } as *const u8 as usize; +unsafe fn alloc_impl(layout: Layout) -> Option<*mut u8> { + let (neg_offset, neg_bound) = STATE.get_or_insert_with(|| { + let heap_base = &__heap_base as *const u8 as usize; let bound = PAGE_SIZE * wasm32::memory_size(0); ( - unsafe { NonZero::new_unchecked(heap_base.wrapping_neg()) }, + NonZero::new_unchecked(heap_base.wrapping_neg()), bound.wrapping_neg(), ) }); @@ -55,7 +59,7 @@ fn alloc_impl(layout: Layout) -> Option<*mut u8> { } *neg_bound -= PAGE_SIZE * pages_needed; } - *neg_offset = unsafe { NonZero::new_unchecked(next_neg_offset) }; + *neg_offset = NonZero::new_unchecked(next_neg_offset); Some(neg_aligned.wrapping_neg() as *mut u8) } diff --git a/mini-alloc/src/lib.rs b/mini-alloc/src/lib.rs index 216615c..69b1aa9 100644 --- a/mini-alloc/src/lib.rs +++ b/mini-alloc/src/lib.rs @@ -9,5 +9,7 @@ cfg_if! { if #[cfg(target_arch = "wasm32")] { mod imp; pub use imp::MiniAlloc; + } else { + pub use wee_alloc::WeeAlloc as MiniAlloc; } } diff --git a/mini-alloc/tests/misc.rs b/mini-alloc/tests/misc.rs index 19df015..58ba2cc 100644 --- a/mini-alloc/tests/misc.rs +++ b/mini-alloc/tests/misc.rs @@ -5,8 +5,6 @@ extern crate alloc; -use alloc::vec::Vec; - #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::*; @@ -17,6 +15,8 @@ static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc; #[cfg(target_arch = "wasm32")] #[wasm_bindgen_test] fn vec_test() { + use alloc::vec::Vec; + let p1 = Vec::::with_capacity(700); let p2 = Vec::::with_capacity(65536); let p3 = Vec::::with_capacity(700000); From 69582361056f6ced756aa08401911232034caf46 Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Tue, 12 Dec 2023 09:58:13 -0800 Subject: [PATCH 3/7] new tests --- mini-alloc/tests/misc.rs | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/mini-alloc/tests/misc.rs b/mini-alloc/tests/misc.rs index 58ba2cc..7a8cfc7 100644 --- a/mini-alloc/tests/misc.rs +++ b/mini-alloc/tests/misc.rs @@ -10,7 +10,7 @@ use wasm_bindgen_test::*; #[cfg(target_arch = "wasm32")] #[global_allocator] -static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc; +static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc::INIT; #[cfg(target_arch = "wasm32")] #[wasm_bindgen_test] @@ -31,3 +31,28 @@ fn vec_test() { assert_eq!(p4.as_ptr() as usize + 4, p5.as_ptr() as usize); assert_eq!(p5.as_ptr() as usize + 2, p6.as_ptr() as usize); } + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen_test] +fn vec_test_loop() { + use alloc::vec::Vec; + + let mut size = 1usize; + let mut p = Vec::::with_capacity(size); + for _ in 0 .. 22 { + let new_size = size*2; + let new_p = Vec::::with_capacity(new_size); + assert_eq!(p.as_ptr() as usize + size, new_p.as_ptr() as usize); + size = new_size; + p = new_p; + } +} + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen_test] +#[should_panic] +fn vec_test_overallocate() { + use alloc::vec::Vec; + + let _ = Vec::::with_capacity(0xFFFFFFFF); +} From 17a326911dc6884499736ec4029e7d10051a1121 Mon Sep 17 00:00:00 2001 From: Rachel Bousfield Date: Wed, 13 Dec 2023 23:14:27 -0700 Subject: [PATCH 4/7] add test and expose constant --- examples/erc20/src/main.rs | 2 +- mini-alloc/.cargo/config.toml | 7 +++ mini-alloc/src/imp.rs | 12 ++--- mini-alloc/tests/misc.rs | 85 ++++++++++++++++++++++++++++++++++- 4 files changed, 97 insertions(+), 9 deletions(-) create mode 100644 mini-alloc/.cargo/config.toml diff --git a/examples/erc20/src/main.rs b/examples/erc20/src/main.rs index de2cb9d..34aaa7a 100644 --- a/examples/erc20/src/main.rs +++ b/examples/erc20/src/main.rs @@ -7,7 +7,7 @@ use stylus_sdk::{alloy_primitives::U256, call, msg, prelude::*}; #[cfg(target_arch = "wasm32")] #[global_allocator] -static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc; +static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc::INIT; mod erc20; diff --git a/mini-alloc/.cargo/config.toml b/mini-alloc/.cargo/config.toml new file mode 100644 index 0000000..bc5bd0b --- /dev/null +++ b/mini-alloc/.cargo/config.toml @@ -0,0 +1,7 @@ +[build] +target = "wasm32-unknown-unknown" + +[target.wasm32-unknown-unknown] +rustflags = [ + "-C", "link-arg=-zstack-size=8192", +] diff --git a/mini-alloc/src/imp.rs b/mini-alloc/src/imp.rs index 5524a4b..2db3c76 100644 --- a/mini-alloc/src/imp.rs +++ b/mini-alloc/src/imp.rs @@ -14,6 +14,9 @@ unsafe impl Sync for MiniAlloc {} impl MiniAlloc { pub const INIT: Self = MiniAlloc; + + /// The WASM page size, or 2^16 bytes. + pub const PAGE_SIZE: usize = 1 << 16; } unsafe impl GlobalAlloc for MiniAlloc { @@ -30,9 +33,6 @@ extern "C" { static __heap_base: u8; } -/// The WASM page size, or 2^16 bytes. -const PAGE_SIZE: usize = 1 << 16; - /// Represents the negation of the allocator's bump offset and boundary /// /// We store the negation because we can align the negative offset in fewer @@ -42,7 +42,7 @@ static mut STATE: Option<(NonZero, usize)> = None; unsafe fn alloc_impl(layout: Layout) -> Option<*mut u8> { let (neg_offset, neg_bound) = STATE.get_or_insert_with(|| { let heap_base = &__heap_base as *const u8 as usize; - let bound = PAGE_SIZE * wasm32::memory_size(0); + let bound = MiniAlloc::PAGE_SIZE * wasm32::memory_size(0); ( NonZero::new_unchecked(heap_base.wrapping_neg()), bound.wrapping_neg(), @@ -53,11 +53,11 @@ unsafe fn alloc_impl(layout: Layout) -> Option<*mut u8> { let next_neg_offset = neg_aligned.checked_sub(layout.size())?; let bytes_needed = neg_bound.saturating_sub(next_neg_offset); if bytes_needed != 0 { - let pages_needed = 1 + (bytes_needed - 1) / PAGE_SIZE; + let pages_needed = 1 + (bytes_needed - 1) / MiniAlloc::PAGE_SIZE; if wasm32::memory_grow(0, pages_needed) == usize::MAX { return None; } - *neg_bound -= PAGE_SIZE * pages_needed; + *neg_bound -= MiniAlloc::PAGE_SIZE * pages_needed; } *neg_offset = NonZero::new_unchecked(next_neg_offset); Some(neg_aligned.wrapping_neg() as *mut u8) diff --git a/mini-alloc/tests/misc.rs b/mini-alloc/tests/misc.rs index 7a8cfc7..fe17956 100644 --- a/mini-alloc/tests/misc.rs +++ b/mini-alloc/tests/misc.rs @@ -39,8 +39,8 @@ fn vec_test_loop() { let mut size = 1usize; let mut p = Vec::::with_capacity(size); - for _ in 0 .. 22 { - let new_size = size*2; + for _ in 0..22 { + let new_size = size * 2; let new_p = Vec::::with_capacity(new_size); assert_eq!(p.as_ptr() as usize + size, new_p.as_ptr() as usize); size = new_size; @@ -56,3 +56,84 @@ fn vec_test_overallocate() { let _ = Vec::::with_capacity(0xFFFFFFFF); } + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen_test] +fn edge_cases() { + use alloc::alloc::{alloc, Layout}; + use core::arch::wasm32; + use mini_alloc::MiniAlloc; + + const PAGE_SIZE: usize = MiniAlloc::PAGE_SIZE; + const PAGE_LIMIT: usize = 65536; + + fn size() -> usize { + wasm32::memory_size(0) as usize + } + + fn size_bytes() -> usize { + size() * PAGE_SIZE + } + + fn next(size: usize) -> usize { + let align = 1; + let layout = Layout::from_size_align(size, align).unwrap(); + unsafe { alloc(layout) as usize } + } + + assert_eq!(size(), 1); + + // check that zero-allocs don't bump + let start = next(0); + assert_eq!(start, next(0)); + assert_eq!(start / PAGE_SIZE, 0); + assert_eq!(size(), 1); + + // fill the rest of the page + let rest = size_bytes() - start; + let end = next(rest); + assert_eq!(end / PAGE_SIZE, 0); + assert_eq!(end, start); + assert_eq!(size(), 1); + + // allocate a second page + let first = next(1); + assert_eq!(first / PAGE_SIZE, 1); + assert_eq!(first, PAGE_SIZE); + assert_eq!(size(), 2); + + // fill the rest of the second page + let rest = size_bytes() - (first + 1); + let end = next(rest); + assert_eq!(end, first + 1); + assert_eq!(size(), 2); + + // jump 4 pages + let jump = next(4 * PAGE_SIZE); + assert_eq!(jump, 2 * PAGE_SIZE); + assert_eq!(size(), 6); + + // allocate many pages + let mut rng: usize = 0; + while size() < PAGE_LIMIT / 2 { + rng = rng.wrapping_mul(1664525).wrapping_add(1013904223); + + let rest = usize::MAX - next(0) + 1; + let bytes = match rng % 4 { + 0 => rng % 1024, + 1 => rng % PAGE_SIZE, + 2 => next(size_bytes() - next(0)), // rest of page + _ => rng % (PAGE_SIZE * 200), + }; + + let offset = next(bytes.min(rest)); + + if offset == size_bytes() { + assert_eq!(bytes, 0); + } else { + assert!(offset < size_bytes()); + } + } + + // TODO: test allocating all 4GB +} From 04dba8978665ee1141424c9d461dfc37cc8536da Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Wed, 13 Dec 2023 22:46:26 -0800 Subject: [PATCH 5/7] overflow fix --- mini-alloc/src/imp.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mini-alloc/src/imp.rs b/mini-alloc/src/imp.rs index 2db3c76..6399e32 100644 --- a/mini-alloc/src/imp.rs +++ b/mini-alloc/src/imp.rs @@ -42,7 +42,7 @@ static mut STATE: Option<(NonZero, usize)> = None; unsafe fn alloc_impl(layout: Layout) -> Option<*mut u8> { let (neg_offset, neg_bound) = STATE.get_or_insert_with(|| { let heap_base = &__heap_base as *const u8 as usize; - let bound = MiniAlloc::PAGE_SIZE * wasm32::memory_size(0); + let bound = MiniAlloc::PAGE_SIZE * wasm32::memory_size(0) - 1; ( NonZero::new_unchecked(heap_base.wrapping_neg()), bound.wrapping_neg(), @@ -51,7 +51,7 @@ unsafe fn alloc_impl(layout: Layout) -> Option<*mut u8> { let neg_aligned = make_aligned(neg_offset.get(), layout.align()); let next_neg_offset = neg_aligned.checked_sub(layout.size())?; - let bytes_needed = neg_bound.saturating_sub(next_neg_offset); + let bytes_needed = neg_bound.saturating_sub(next_neg_offset + 1); if bytes_needed != 0 { let pages_needed = 1 + (bytes_needed - 1) / MiniAlloc::PAGE_SIZE; if wasm32::memory_grow(0, pages_needed) == usize::MAX { From 9c535190bfc0826dd01d232b7db4131e5ba267d0 Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Fri, 22 Dec 2023 11:45:43 -0800 Subject: [PATCH 6/7] alloc_zeroed; ink table in README --- mini-alloc/README.md | 19 ++++++++++++++++++- mini-alloc/src/imp.rs | 5 +++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/mini-alloc/README.md b/mini-alloc/README.md index c90d4ed..a7e8e2a 100644 --- a/mini-alloc/README.md +++ b/mini-alloc/README.md @@ -16,7 +16,24 @@ this crate. On targets other than wasm32, `MiniAlloc` simply forwards to the allocator from another crate, `wee_alloc::WeeAlloc`. -Use it like this: +`mini-alloc` uses less ink on +[Stylus](https://github.com/OffchainLabs/stylus-sdk-rs) compared to other +allocators. When running the `edge_cases` test in this crate on Stylus, here are +ink costs (minus the cost of memory expansion) when using `MiniAlloc` vs +`WeeAlloc` and Rust's default allocator. + + +| | MiniAlloc | WeeAlloc | Default | +| ------------- | ------------ | ------------ | ------------ | +| alloc | 3324474 | 7207816 | 5163328 | +| alloc_zeroed | 3288777 | 954023099920 | 484822031511 | + +`alloc` means `edge_cases` was run as it appears in this crate. `alloc_zeroed` +means the calls to `alloc` were replaced with calls to `alloc_zeroed`. We can +achieve substantial savings in this case because newly expanded memory in WASM +is already zeroed. + +Use `MiniAlloc` like this: ```rust #[global_allocator] diff --git a/mini-alloc/src/imp.rs b/mini-alloc/src/imp.rs index 6399e32..a68eb8d 100644 --- a/mini-alloc/src/imp.rs +++ b/mini-alloc/src/imp.rs @@ -24,6 +24,11 @@ unsafe impl GlobalAlloc for MiniAlloc { alloc_impl(layout).unwrap_or(core::ptr::null_mut()) } + #[inline] + unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { + self.alloc(layout) + } + #[inline] unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {} } From a8145f4834bf66cf3a018a3344e3effd1700795c Mon Sep 17 00:00:00 2001 From: Rachel Bousfield Date: Fri, 22 Dec 2023 13:54:12 -0700 Subject: [PATCH 7/7] update readme --- mini-alloc/README.md | 66 ++++++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/mini-alloc/README.md b/mini-alloc/README.md index a7e8e2a..59e84dd 100644 --- a/mini-alloc/README.md +++ b/mini-alloc/README.md @@ -1,41 +1,47 @@ # mini-alloc -`mini-alloc` is a very small bump allocator for wasm32 intended to be used as -the global Rust allocator. It never deallocates memory -- that is, `dealloc` -does nothing. It's suitable for cases where binary size is at a premium and -it's acceptable to leak all allocations. +`mini-alloc` is a small and performant allocator optimized for `wasm32` targets like [Arbitrum Stylus][Stylus]. You can use it in your program as follows. +```rust +#[global_allocator] +static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc::INIT; +``` -One other major limitation: this crate is not thread safe! `MiniAlloc` -implements `Sync` because that is a required of a global allocator, but this is -not a valid implementation of `Sync`, and it must only be used from a single -thread. +## Benchmarks -Also, `core::arch::wasm32::memory_grow` must never be called by any code outside -this crate. +`mini-alloc` implements a minimal bump allocator strategy. It never deallocates memory -- that is, `dealloc` does nothing. It's suitable for cases where binary size is at a premium and it's acceptable to leak all allocations. The simplicity of this model makes it very efficient, as seen in the following benchmarks. -On targets other than wasm32, `MiniAlloc` simply forwards to the allocator from -another crate, `wee_alloc::WeeAlloc`. +| | `MiniAlloc` | [`WeeAlloc`][WeeAlloc] | Std Library | +|--------------|-------------|------------------------|----------------| +| alloc | 333 gas | 721 gas | 516 gas | +| alloc_zeroed | 329 gas | 95 million gas | 48 million gas | -`mini-alloc` uses less ink on -[Stylus](https://github.com/OffchainLabs/stylus-sdk-rs) compared to other -allocators. When running the `edge_cases` test in this crate on Stylus, here are -ink costs (minus the cost of memory expansion) when using `MiniAlloc` vs -`WeeAlloc` and Rust's default allocator. +The benchmarks compare the performance of this crate's `edge_cases` test in the [Stylus VM][StylusVM]. Normal allocations are over **2x** cheaper than when using [`WeeAlloc`][WeeAlloc], a common WASM alternative that this crate defaults to when built for non-WASM targets. +Replacing each instance of `alloc` in the test with `alloc_zeroed` reveals an over **99%** improvement for zero-filled allocations. Unlike [`WeeAlloc`][WeeAlloc] and the standard library, `MiniAlloc` takes advantage of the fact that WASM pages are zero-filled at initialization, and uses fewer of them due to the layout of Rust's memory. -| | MiniAlloc | WeeAlloc | Default | -| ------------- | ------------ | ------------ | ------------ | -| alloc | 3324474 | 7207816 | 5163328 | -| alloc_zeroed | 3288777 | 954023099920 | 484822031511 | +In the above tests we disable memory expansion costs, which unfairly penelize `WeeAlloc` and the standard library due to their increased resource consumption. -`alloc` means `edge_cases` was run as it appears in this crate. `alloc_zeroed` -means the calls to `alloc` were replaced with calls to `alloc_zeroed`. We can -achieve substantial savings in this case because newly expanded memory in WASM -is already zeroed. +## Notice -Use `MiniAlloc` like this: +`MiniAlloc` should not be used in `wasm32` environments that enable the multithreading proposal. Although `MiniAlloc` implements `Sync` since Rust requires it for the global allocator, this crate should not be used in this way. This should not be a concern in [`Stylus`][Stylus]. -```rust -#[global_allocator] -static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc::INIT; -``` +Also, `core::arch::wasm32::memory_grow` must never be called by any code outside this crate. + +On targets other than wasm32, `MiniAlloc` simply forwards to the allocator from another crate, `wee_alloc::WeeAlloc`. + +## License + +© 2023 Offchain Labs, Inc. + +This project is licensed under either of + +- [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) ([licenses/Apache-2.0](../licenses/Apache-2.0)) +- [MIT license](https://opensource.org/licenses/MIT) ([licenses/MIT](../licenses/MIT)) + +at your option. + +The [SPDX](https://spdx.dev) license identifier for this project is `MIT OR Apache-2.0`. + +[Stylus]: https://github.com/OffchainLabs/stylus-sdk-rs +[StylusVM]: https://github.com/OffchainLabs/stylus +[WeeAlloc]: https://github.com/rustwasm/wee_alloc