Skip to content

Commit 90aad92

Browse files
HCastanocmichi
andauthored
Allow option to opt-out of provided memory allocator (#1661)
* Add feature to disable global memory allocator * Add example of how to use a different allocator * Add concrete downsides to warning * Update example to use a `Vec` * Appease Clippy * Add missing period Co-authored-by: Michael Müller <[email protected]> * Move example to `integration-tests` folder --------- Co-authored-by: Michael Müller <[email protected]>
1 parent b8862a1 commit 90aad92

File tree

8 files changed

+232
-2
lines changed

8 files changed

+232
-2
lines changed

crates/allocator/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ quickcheck_macros = "1"
2525
default = ["std"]
2626
std = []
2727
ink-fuzz-tests = ["std"]
28+
no-allocator = []

crates/allocator/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
#![cfg_attr(not(feature = "std"), no_std)]
2626
#![cfg_attr(not(feature = "std"), feature(alloc_error_handler))]
2727

28-
#[cfg(not(feature = "std"))]
28+
#[cfg(not(any(feature = "std", feature = "no-allocator")))]
2929
#[global_allocator]
3030
static mut ALLOC: bump::BumpAllocator = bump::BumpAllocator {};
3131

crates/env/Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,9 @@ std = [
7070
"sha3",
7171
"blake2",
7272
]
73+
7374
# Enable contract debug messages via `debug_print!` and `debug_println!`.
7475
ink-debug = []
76+
77+
# Disable the ink! provided global memory allocator.
78+
no-allocator = ["ink_allocator/no-allocator"]

crates/env/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ fn panic(info: &core::panic::PanicInfo) -> ! {
6565

6666
// This extern crate definition is required since otherwise rustc
6767
// is not recognizing its allocator and panic handler definitions.
68-
#[cfg(not(feature = "std"))]
68+
#[cfg(not(any(feature = "std", feature = "no-allocator")))]
6969
extern crate ink_allocator;
7070

7171
mod api;

crates/ink/Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,6 @@ ink-debug = [
5050
"ink_env/ink-debug",
5151
]
5252
show-codegen-docs = []
53+
54+
# Disable the ink! provided global memory allocator.
55+
no-allocator = ["ink_env/no-allocator"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Ignore build artifacts from the local tests sub-crate.
2+
/target/
3+
4+
# Ignore backup files creates by cargo fmt.
5+
**/*.rs.bk
6+
7+
# Remove Cargo.lock when creating an executable, leave it for libraries
8+
# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock
9+
Cargo.lock
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[package]
2+
name = "custom_allocator"
3+
version = "4.0.0"
4+
authors = ["Parity Technologies <[email protected]>"]
5+
edition = "2021"
6+
publish = false
7+
8+
[dependencies]
9+
# We're going to use a different allocator than the one provided by ink!. To do that we
10+
# first need to disable the included memory allocator.
11+
ink = { path = "../../crates/ink", default-features = false, features = ["no-allocator"] }
12+
13+
# This is going to be our new global memory allocator.
14+
dlmalloc = {version = "0.2", default-features = false, features = ["global"] }
15+
16+
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
17+
scale-info = { version = "2.3", default-features = false, features = ["derive"], optional = true }
18+
19+
[dev-dependencies]
20+
ink_e2e = { path = "../../crates/e2e" }
21+
22+
[lib]
23+
path = "lib.rs"
24+
25+
[features]
26+
default = ["std"]
27+
std = [
28+
"ink/std",
29+
"scale/std",
30+
"scale-info/std",
31+
]
32+
ink-as-dependency = []
33+
e2e-tests = []
+180
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
//! # Custom Allocator
2+
//!
3+
//! This example demonstrates how to opt-out of the ink! provided global memory allocator.
4+
//!
5+
//! We will use [`dlmalloc`](https://github.com/alexcrichton/dlmalloc-rs) instead.
6+
//!
7+
//! ## Warning!
8+
//!
9+
//! We **do not** recommend you opt-out of the provided allocator for production contract
10+
//! deployments!
11+
//!
12+
//! If you don't handle allocations correctly you can introduce security vulnerabilities to your
13+
//! contracts.
14+
//!
15+
//! You may also introduce performance issues. This is because the code of your allocator will
16+
//! be included in the final contract binary, potentially increasing gas usage significantly.
17+
//!
18+
//! ## Why Change the Allocator?
19+
//!
20+
//! The default memory allocator was designed to have a tiny size footprint, and made some
21+
//! compromises to achieve that, e.g it does not free/deallocate memory.
22+
//!
23+
//! You may have a use case where you want to deallocate memory, or allocate it using a different
24+
//! strategy.
25+
//!
26+
//! Providing your own allocator lets you choose the right tradeoffs for your use case.
27+
28+
#![cfg_attr(not(feature = "std"), no_std)]
29+
// Since we opted out of the default allocator we must also bring our own out-of-memory (OOM)
30+
// handler. The Rust compiler doesn't let us do this unless we add this unstable/nightly feature.
31+
#![cfg_attr(not(feature = "std"), feature(alloc_error_handler))]
32+
33+
// Here we set `dlmalloc` to be the global memory allocator.
34+
//
35+
// The [`GlobalAlloc`](https://doc.rust-lang.org/std/alloc/trait.GlobalAlloc.html) trait is
36+
// important to understand if you're swapping our your allocator.
37+
#[cfg(not(feature = "std"))]
38+
#[global_allocator]
39+
static ALLOC: dlmalloc::GlobalDlmalloc = dlmalloc::GlobalDlmalloc;
40+
41+
// As mentioned earlier, we need to provide our own OOM handler.
42+
//
43+
// We don't try and handle this and opt to abort contract execution instead.
44+
#[cfg(not(feature = "std"))]
45+
#[alloc_error_handler]
46+
fn oom(_: core::alloc::Layout) -> ! {
47+
core::arch::wasm32::unreachable()
48+
}
49+
50+
#[ink::contract]
51+
mod custom_allocator {
52+
use ink::prelude::{
53+
vec,
54+
vec::Vec,
55+
};
56+
57+
#[ink(storage)]
58+
pub struct CustomAllocator {
59+
/// Stores a single `bool` value on the storage.
60+
///
61+
/// # Note
62+
///
63+
/// We're using a `Vec` here as it allocates its elements onto the heap, as opposed to the
64+
/// stack. This allows us to demonstrate that our new allocator actually works.
65+
value: Vec<bool>,
66+
}
67+
68+
impl CustomAllocator {
69+
/// Constructor that initializes the `bool` value to the given `init_value`.
70+
#[ink(constructor)]
71+
pub fn new(init_value: bool) -> Self {
72+
Self {
73+
value: vec![init_value],
74+
}
75+
}
76+
77+
/// Creates a new flipper smart contract initialized to `false`.
78+
#[ink(constructor)]
79+
pub fn default() -> Self {
80+
Self::new(Default::default())
81+
}
82+
83+
/// A message that can be called on instantiated contracts.
84+
/// This one flips the value of the stored `bool` from `true`
85+
/// to `false` and vice versa.
86+
#[ink(message)]
87+
pub fn flip(&mut self) {
88+
self.value[0] = !self.value[0];
89+
}
90+
91+
/// Simply returns the current value of our `bool`.
92+
#[ink(message)]
93+
pub fn get(&self) -> bool {
94+
self.value[0]
95+
}
96+
}
97+
98+
#[cfg(test)]
99+
mod tests {
100+
use super::*;
101+
102+
#[ink::test]
103+
fn default_works() {
104+
let custom_allocator = CustomAllocator::default();
105+
assert!(!custom_allocator.get());
106+
}
107+
108+
#[ink::test]
109+
fn it_works() {
110+
let mut custom_allocator = CustomAllocator::new(false);
111+
assert!(!custom_allocator.get());
112+
custom_allocator.flip();
113+
assert!(custom_allocator.get());
114+
}
115+
}
116+
117+
#[cfg(all(test, feature = "e2e-tests"))]
118+
mod e2e_tests {
119+
use super::*;
120+
121+
use ink_e2e::build_message;
122+
123+
type E2EResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;
124+
125+
/// We test that we can upload and instantiate the contract using its default constructor.
126+
#[ink_e2e::test]
127+
async fn default_works(mut client: ink_e2e::Client<C, E>) -> E2EResult<()> {
128+
// Given
129+
let constructor = CustomAllocatorRef::default();
130+
131+
// When
132+
let contract_account_id = client
133+
.instantiate("custom_allocator", &ink_e2e::alice(), constructor, 0, None)
134+
.await
135+
.expect("instantiate failed")
136+
.account_id;
137+
138+
// Then
139+
let get = build_message::<CustomAllocatorRef>(contract_account_id.clone())
140+
.call(|custom_allocator| custom_allocator.get());
141+
let get_result = client.call_dry_run(&ink_e2e::alice(), &get, 0, None).await;
142+
assert!(matches!(get_result.return_value(), false));
143+
144+
Ok(())
145+
}
146+
147+
/// We test that we can read and write a value from the on-chain contract contract.
148+
#[ink_e2e::test]
149+
async fn it_works(mut client: ink_e2e::Client<C, E>) -> E2EResult<()> {
150+
// Given
151+
let constructor = CustomAllocatorRef::new(false);
152+
let contract_account_id = client
153+
.instantiate("custom_allocator", &ink_e2e::bob(), constructor, 0, None)
154+
.await
155+
.expect("instantiate failed")
156+
.account_id;
157+
158+
let get = build_message::<CustomAllocatorRef>(contract_account_id.clone())
159+
.call(|custom_allocator| custom_allocator.get());
160+
let get_result = client.call_dry_run(&ink_e2e::bob(), &get, 0, None).await;
161+
assert!(matches!(get_result.return_value(), false));
162+
163+
// When
164+
let flip = build_message::<CustomAllocatorRef>(contract_account_id.clone())
165+
.call(|custom_allocator| custom_allocator.flip());
166+
let _flip_result = client
167+
.call(&ink_e2e::bob(), flip, 0, None)
168+
.await
169+
.expect("flip failed");
170+
171+
// Then
172+
let get = build_message::<CustomAllocatorRef>(contract_account_id.clone())
173+
.call(|custom_allocator| custom_allocator.get());
174+
let get_result = client.call_dry_run(&ink_e2e::bob(), &get, 0, None).await;
175+
assert!(matches!(get_result.return_value(), true));
176+
177+
Ok(())
178+
}
179+
}
180+
}

0 commit comments

Comments
 (0)