Unverified Commit b0681a04 authored by Hero Bird's avatar Hero Bird Committed by GitHub
Browse files

Add method to finalize the dynamic storage allocator (#418)

* [core] implement alloc::finalize for the dynamic storage allocator

* [core] adjust tests for the changes

* [core] apply rustfmt

* [core] fix clippy warning

* [core] add SAFETY comments to dynamic storage allocator for Wasm

* [core] add a Note to the docs of alloc::{initialize, finalize}
parent 718b9f5e
Pipeline #93822 failed with stages
in 6 minutes and 2 seconds
...@@ -13,8 +13,15 @@ ...@@ -13,8 +13,15 @@
// limitations under the License. // limitations under the License.
use super::DynamicAllocator; use super::DynamicAllocator;
use crate::storage2::traits::pull_spread_root; use crate::storage2::traits::{
pull_spread_root,
push_spread_root,
};
use cfg_if::cfg_if; use cfg_if::cfg_if;
use core::{
mem,
mem::ManuallyDrop,
};
use ink_primitives::Key; use ink_primitives::Key;
/// The default dynamic allocator key offset. /// The default dynamic allocator key offset.
...@@ -53,8 +60,10 @@ enum DynamicAllocatorState { ...@@ -53,8 +60,10 @@ enum DynamicAllocatorState {
/// with the assumption that a former contract deployment has already /// with the assumption that a former contract deployment has already
/// taken place in the past. /// taken place in the past.
UninitCall, UninitCall,
/// The global instance has already been initialized successfully. /// The global instance has been initialized successfully and can be used.
Initialized(DynamicAllocator), Initialized(DynamicAllocator),
/// The global instance has been finalized and can no longer be used.
Finalized,
} }
impl From<ContractPhase> for DynamicAllocatorState { impl From<ContractPhase> for DynamicAllocatorState {
...@@ -66,6 +75,104 @@ impl From<ContractPhase> for DynamicAllocatorState { ...@@ -66,6 +75,104 @@ impl From<ContractPhase> for DynamicAllocatorState {
} }
} }
impl DynamicAllocatorState {
/// Initializes the global dynamic storage allocator instance.
///
/// The `phase` parameter describes for which execution phase the dynamic
/// storage allocator needs to be initialized since this is different
/// in contract instantiations and calls.
pub fn initialize(&mut self, phase: ContractPhase) {
match self {
DynamicAllocatorState::Initialized(_)
// We only perform this check on Wasm compilation to avoid
// some overly constrained check for the off-chain testing.
if cfg!(all(not(feature = "std"), target_arch = "wasm32")) =>
{
panic!(
"cannot initialize the dynamic storage \
allocator instance twice in Wasm",
)
}
DynamicAllocatorState::Finalized => {
panic!(
"cannot initialize the dynamic storage \
allocator after it has been finalized",
)
}
state => {
*state = phase.into();
}
}
}
/// Finalizes the global instance for the dynamic storage allocator.
///
/// The global dynamic storage allocator must not be used after this!
pub fn finalize(&mut self) {
match self {
DynamicAllocatorState::Initialized(allocator) => {
// Push all state of the global dynamic storage allocator
// instance back onto the contract storage.
push_spread_root::<DynamicAllocator>(
&allocator,
&Key(DYNAMIC_ALLOCATOR_KEY_OFFSET),
);
// Prevent calling `drop` on the dynamic storage allocator
// instance since this would clear all contract storage
// again.
let _ = ManuallyDrop::new(mem::take(allocator));
*self = DynamicAllocatorState::Finalized;
}
DynamicAllocatorState::Finalized => {
panic!(
"cannot finalize the dynamic storage allocator \
after it has already been finalized"
)
}
DynamicAllocatorState::UninitCall | DynamicAllocatorState::UninitDeploy => {
// Nothing to do in these states.
}
}
}
/// Runs the closure on the global instance for the dynamic storage allocator.
///
/// Will automatically initialize the global allocator instance if it has not
/// yet been initialized.
///
/// # Panics
///
/// If the global dynamic storage allocator instance has already been finalized.
pub fn on_instance<F, R>(&mut self, f: F) -> R
where
F: FnOnce(&mut DynamicAllocator) -> R,
{
match self {
DynamicAllocatorState::UninitDeploy => {
let mut allocator = DynamicAllocator::default();
let result = f(&mut allocator);
*self = DynamicAllocatorState::Initialized(allocator);
result
}
DynamicAllocatorState::UninitCall => {
let mut allocator = pull_spread_root::<DynamicAllocator>(&Key(
DYNAMIC_ALLOCATOR_KEY_OFFSET,
));
let result = f(&mut allocator);
*self = DynamicAllocatorState::Initialized(allocator);
result
}
DynamicAllocatorState::Initialized(ref mut allocator) => f(allocator),
DynamicAllocatorState::Finalized => {
panic!(
"cannot operate on the dynamic storage \
allocator after it has been finalized"
);
}
}
}
}
cfg_if! { cfg_if! {
if #[cfg(all(not(feature = "std"), target_arch = "wasm32"))] { if #[cfg(all(not(feature = "std"), target_arch = "wasm32"))] {
// Procedures for the Wasm compilation: // Procedures for the Wasm compilation:
...@@ -73,40 +180,36 @@ cfg_if! { ...@@ -73,40 +180,36 @@ cfg_if! {
/// The global instance for the dynamic storage allocator. /// The global instance for the dynamic storage allocator.
static mut GLOBAL_INSTANCE: DynamicAllocatorState = DynamicAllocatorState::UninitDeploy; static mut GLOBAL_INSTANCE: DynamicAllocatorState = DynamicAllocatorState::UninitDeploy;
/// Commands the (re-)initialization of the global instance for the dynamic /// Forwards to the `initialize` of the global dynamic storage allocator instance.
/// storage allocator. pub fn initialize(phase: ContractPhase) {
pub fn initialize_for(phase: ContractPhase) { // SAFETY: Accessing the global allocator in Wasm mode is single
let instance = unsafe { &mut GLOBAL_INSTANCE }; // threaded and will not return back a reference to its
// We do not allow reinitialization for Wasm targets for performance reasons. // internal state. Also the `initialize` method won't
if let DynamicAllocatorState::Initialized(_) = instance { // re-enter the dynamic storage in any possible way.
panic!("cannot reinitialize dynamic storage allocator instance in Wasm"); unsafe { &mut GLOBAL_INSTANCE }.initialize(phase);
} }
*instance = phase.into();
/// Forwards to the `finalize` of the global dynamic storage allocator instance.
pub fn finalize() {
// SAFETY: Accessing the global allocator in Wasm mode is single
// threaded and will not return back a reference to its
// internal state. Also the `finalize` method won't
// re-enter the dynamic storage in any possible way.
unsafe { &mut GLOBAL_INSTANCE }.finalize();
} }
/// Runs the given closure on the global instance for the dynamic storage allocator. /// Forwards to the `on_instance` of the global dynamic storage allocator instance.
pub fn on_call<F, R>(f: F) -> R pub fn on_instance<F, R>(f: F) -> R
where where
F: FnOnce(&mut DynamicAllocator) -> R, F: FnOnce(&mut DynamicAllocator) -> R,
{ {
let instance = unsafe { &mut GLOBAL_INSTANCE }; // SAFETY: Accessing the global allocator in Wasm mode is single
match instance { // threaded and will not return back a reference to its
DynamicAllocatorState::UninitDeploy => { // internal state. Also this is an internal API only called
let mut allocator = DynamicAllocator::default(); // through `alloc` and `free` both of which do not return
let result = f(&mut allocator); // anything that could allow to re-enter the dynamic storage
*instance = DynamicAllocatorState::Initialized(allocator); // allocator instance.
result unsafe { &mut GLOBAL_INSTANCE }.on_instance(f)
}
DynamicAllocatorState::UninitCall => {
let mut allocator = pull_spread_root::<DynamicAllocator>(&Key(DYNAMIC_ALLOCATOR_KEY_OFFSET));
let result = f(&mut allocator);
*instance = DynamicAllocatorState::Initialized(allocator);
result
}
DynamicAllocatorState::Initialized(ref mut allocator) => {
f(allocator)
}
}
} }
} else if #[cfg(feature = "std")] { } else if #[cfg(feature = "std")] {
...@@ -115,40 +218,31 @@ cfg_if! { ...@@ -115,40 +218,31 @@ cfg_if! {
use ::core::cell::RefCell; use ::core::cell::RefCell;
thread_local!( thread_local!(
/// The global instance for the dynamic storage allocator. /// The global instance for the dynamic storage allocator.
static GLOBAL_INSTANCE: RefCell<DynamicAllocatorState> = RefCell::new(DynamicAllocatorState::UninitDeploy); static GLOBAL_INSTANCE: RefCell<DynamicAllocatorState> = RefCell::new(
DynamicAllocatorState::UninitDeploy
);
); );
/// Forwards to the `initialize` of the global dynamic storage allocator instance.
pub fn initialize(phase: ContractPhase) {
GLOBAL_INSTANCE.with(|instance| {
instance.borrow_mut().initialize(phase)
});
}
/// Commands the (re-)initialization of the global instance for the dynamic /// Forwards to the `finalize` of the global dynamic storage allocator instance.
/// storage allocator. pub fn finalize() {
pub fn initialize_for(phase: ContractPhase) {
GLOBAL_INSTANCE.with(|instance| { GLOBAL_INSTANCE.with(|instance| {
instance.replace_with(|_| phase.into()) instance.borrow_mut().finalize()
}); });
} }
/// Runs the given closure on the global instance for the dynamic storage allocator. /// Forwards to the `on_instance` of the global dynamic storage allocator instance.
pub fn on_call<F, R>(f: F) -> R pub fn on_instance<F, R>(f: F) -> R
where where
F: FnOnce(&mut DynamicAllocator) -> R, F: FnOnce(&mut DynamicAllocator) -> R,
{ {
GLOBAL_INSTANCE.with(|instance| { GLOBAL_INSTANCE.with(|instance| {
match &mut *instance.borrow_mut() { instance.borrow_mut().on_instance(f)
instance @ DynamicAllocatorState::UninitDeploy => {
let mut allocator = DynamicAllocator::default();
let result = f(&mut allocator);
*instance = DynamicAllocatorState::Initialized(allocator);
result
}
instance @ DynamicAllocatorState::UninitCall => {
let mut allocator = pull_spread_root::<DynamicAllocator>(&Key(DYNAMIC_ALLOCATOR_KEY_OFFSET));
let result = f(&mut allocator);
*instance = DynamicAllocatorState::Initialized(allocator);
result
}
DynamicAllocatorState::Initialized(instance) => {
f(instance)
}
}
}) })
} }
......
...@@ -75,21 +75,15 @@ mod init; ...@@ -75,21 +75,15 @@ mod init;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
use self::allocator::DynamicAllocator;
pub use self::{ pub use self::{
allocation::DynamicAllocation, allocation::DynamicAllocation,
init::{ init::ContractPhase,
initialize_for,
ContractPhase,
},
};
use self::{
allocator::DynamicAllocator,
init::on_call,
}; };
/// Returns a new dynamic storage allocation. /// Returns a new dynamic storage allocation.
pub fn alloc() -> DynamicAllocation { pub fn alloc() -> DynamicAllocation {
on_call(DynamicAllocator::alloc) init::on_instance(DynamicAllocator::alloc)
} }
/// Frees the given dynamic storage allocation. /// Frees the given dynamic storage allocation.
...@@ -97,5 +91,43 @@ pub fn alloc() -> DynamicAllocation { ...@@ -97,5 +91,43 @@ pub fn alloc() -> DynamicAllocation {
/// This makes the given dynamic storage allocation available again /// This makes the given dynamic storage allocation available again
/// for new dynamic storage allocations. /// for new dynamic storage allocations.
pub fn free(allocation: DynamicAllocation) { pub fn free(allocation: DynamicAllocation) {
on_call(|allocator| allocator.free(allocation)) init::on_instance(|allocator| allocator.free(allocation))
}
/// Tells the global dynamic storage allocator instance how it shall initialize.
///
/// # Note
///
/// Normally users of ink! do not have to call this function directly as it is
/// automatically being use in the correct order and way by the generated code.
///
/// - The `phase` parameter describes for which execution phase the dynamic
/// storage allocator needs to be initialized since this is different
/// in contract instantiations and calls.
/// - This has to be issued before the first interaction with the global allocator.
/// - The actual instantiation will happen only upon the first interaction with
/// the global allocator, e.g. using its `alloc` or `free` calls. Until then
/// it remains uninitialized.
///
/// If this function is not called before the first global allocator interaction
/// then the default initialization scheme is for contract instantiation.
/// However, this behavior might change and must not be relied upon.
pub fn initialize(phase: ContractPhase) {
init::initialize(phase);
}
/// Finalizes the global dynamic storage allocator instance.
///
/// This pushes all the accumulated state from this contract execution back to
/// the contract storage to be used in the next contract execution for the same
/// contract instance.
///
/// The global dynamic storage allocator must not be used after this!
///
/// # Note
///
/// Normally users of ink! do not have to call this function directly as it is
/// automatically being use in the correct order and way by the generated code.
pub fn finalize() {
init::finalize()
} }
...@@ -15,7 +15,6 @@ ...@@ -15,7 +15,6 @@
use super::{ use super::{
alloc, alloc,
free, free,
initialize_for,
ContractPhase, ContractPhase,
DynamicAllocation, DynamicAllocation,
DynamicAllocator, DynamicAllocator,
...@@ -25,9 +24,12 @@ use crate::{ ...@@ -25,9 +24,12 @@ use crate::{
test, test,
DefaultEnvTypes, DefaultEnvTypes,
}, },
storage2::traits::{ storage2::{
KeyPtr, alloc,
SpreadLayout, traits::{
KeyPtr,
SpreadLayout,
},
}, },
}; };
use ink_primitives::Key; use ink_primitives::Key;
...@@ -36,7 +38,7 @@ fn run_default_test<F>(f: F) ...@@ -36,7 +38,7 @@ fn run_default_test<F>(f: F)
where where
F: FnOnce(), F: FnOnce(),
{ {
initialize_for(ContractPhase::Deploy); alloc::initialize(ContractPhase::Deploy);
test::run_test::<DefaultEnvTypes, _>(|_| { test::run_test::<DefaultEnvTypes, _>(|_| {
f(); f();
Ok(()) Ok(())
...@@ -195,7 +197,7 @@ fn test_call_setup_works() { ...@@ -195,7 +197,7 @@ fn test_call_setup_works() {
assert_eq!(allocator.alloc(), DynamicAllocation(1)); assert_eq!(allocator.alloc(), DynamicAllocation(1));
let root_key = Key([0xFE; 32]); let root_key = Key([0xFE; 32]);
SpreadLayout::push_spread(&allocator, &mut KeyPtr::from(root_key)); SpreadLayout::push_spread(&allocator, &mut KeyPtr::from(root_key));
initialize_for(ContractPhase::Call); alloc::initialize(ContractPhase::Call);
assert_eq!(alloc(), DynamicAllocation(2)); assert_eq!(alloc(), DynamicAllocation(2));
assert_eq!(alloc(), DynamicAllocation(3)); assert_eq!(alloc(), DynamicAllocation(3));
free(DynamicAllocation(0)); free(DynamicAllocation(0));
......
...@@ -17,10 +17,8 @@ use crate::{ ...@@ -17,10 +17,8 @@ use crate::{
env, env,
env::test::DefaultAccounts, env::test::DefaultAccounts,
storage2::{ storage2::{
alloc::{ alloc,
initialize_for, alloc::ContractPhase,
ContractPhase,
},
traits::{ traits::{
KeyPtr, KeyPtr,
SpreadLayout, SpreadLayout,
...@@ -50,7 +48,7 @@ where ...@@ -50,7 +48,7 @@ where
F: FnOnce(DefaultAccounts<env::DefaultEnvTypes>), F: FnOnce(DefaultAccounts<env::DefaultEnvTypes>),
{ {
env::test::run_test::<env::DefaultEnvTypes, _>(|default_accounts| { env::test::run_test::<env::DefaultEnvTypes, _>(|default_accounts| {
initialize_for(ContractPhase::Deploy); alloc::initialize(ContractPhase::Deploy);
f(default_accounts); f(default_accounts);
Ok(()) Ok(())
}) })
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment