I’m trying to use a SHashMap with [u8, 136] as a key however I’m hitting all sorts of problems. The main issue now seems to be with Candid serialization/ deserialization… which is a bit beyond my rust skills.
Has anyone got a working example of SHashMap with a [u8, N] type key?
For info folks - Managed to get it to compile… I didn’t realise [u8; Size] was considered a generic as the type was known. I swapped [u8, Size] for Vec and the errors went away.
#[derive(CandidType, Deserialize, StableType, Hash, Eq, PartialEq, Clone)]
pub struct IDKey(pub Vec<u8>);
impl AsFixedSizeBytes for IDKey {
const SIZE: usize = 135;
type Buf = Vec<u8>; // use for generics
fn as_fixed_size_bytes(&self, buf: &mut [u8]) {
let key_bytes = self.0.as_slice();
buf[0] = key_bytes.len() as u8;
buf[1..(1 + key_bytes.len())].copy_from_slice(key_bytes);
}
fn from_fixed_size_bytes(buf: &[u8]) -> Self {
let key_len = buf[0] as usize;
let key: &[u8] = &buf[1..(1 + key_len)];
return IDKey(key.try_into().unwrap());
}
}
#[derive(StableType, AsFixedSizeBytes)]
pub struct Directory {
pub id_to_ref: SHashMap<IDKey, u32>,
pub ref_to_id: SHashMap<u32, IDKey>,
pub next_ref: u32,
}
The serialized is fine, but the deserialized will lose some of the performance due to an allocation.
But this is better in terms of performance, than to use Vec<u8> as your key, since most of the time it will work exclusively on stack and only allocate when deserialized (when received as an argument to a canister function).
I’ve got the stable memory working well however I’m interested in how the stable memory works along with runtime state and if it’s possible to persist runtime state over upgrades without overwriting or damaging memory allocated for stable structures.
Code for reference:
#[derive(StableType, AsFixedSizeBytes, Debug, Default)]
pub struct Main {
pub canister_data: CanisterSettings,
pub processed_data: u64,
pub directory_data: Directory,
}
#[derive(CandidType, Default, Clone)]
pub struct RuntimeState{
pub latest_txs: BlockHolder,
pub canister_logs: Vec<LogEntry>,
pub temp_vec_ptx: Vec<ProcessedTX>,
pub temp_vec_stx: Vec<SmallTX>
}
thread_local! {
pub static RUNTIME_STATE: RefCell<RuntimeState> = RefCell::default();
pub static STABLE_STATE: RefCell<Option<Main>> = RefCell::default();
}
pub fn state_init(){
stable_memory_init();
// init stable state
let mut stable_data = Main::default();
let default_admin = string_to_idkey(&"2vxsx-fae".to_string()).unwrap();
let default_canister_name = string_to_idkey(&"Name Me Please!".to_string()).unwrap();
stable_data.canister_data.authorised.push(default_admin).expect("Out of memory");
stable_data.canister_data.canister_name = default_canister_name;
STABLE_STATE.with(|state| {
*state.borrow_mut() = Some(stable_data);
});
// init runtime state
let mut runtime_state = RuntimeState::default();
runtime_state.latest_txs.init();
RUNTIME_STATE.with(|state| {
*state.borrow_mut() = runtime_state;
});
log("Canister Initialised");
}
pub fn state_pre_upgrade(){
let state: Main = STABLE_STATE.with(|s| s.borrow_mut().take().unwrap());
let boxed_state = SBox::new(state).expect("Out of memory");
store_custom_data(0, boxed_state);
stable_memory_pre_upgrade().expect("Out of memory");
}
pub fn state_post_upgrade(){
stable_memory_post_upgrade();
let state: Main = retrieve_custom_data::<Main>(0).unwrap().into_inner();
STABLE_STATE.with(|s| {
*s.borrow_mut() = Some(state);
});
}
The RUNTIME_STATE structs have a lot of Strings/ Vecs and other dynamically sized stuff which I’d like to keep on the heap rather than in stable memory. Is this possible?
You can store your RUNTIME_STATE the same way you’re storing your STABLE_STATE.
Just put it inside an SBox and use store_custom_data() function with another id.
Since you don’t implement StableType and AsDynSizeBytes for RuntimeState, I assume, you can’t do that for some reason. So, in this situation, the way you can resolve it is to simply encode/decode it manually, using candid encoding functions: encode_one() and decode_one(). Then you can put the resulting byte array into the SBox.
// I didn't check the following code, before writing it here
// if it doesn't work, please let me know
pub fn state_pre_upgrade(){
let state: Main = STABLE_STATE.with(|s| s.borrow_mut().take().unwrap());
let boxed_state = SBox::new(state).expect("Out of memory");
store_custom_data(0, boxed_state);
// RefCell supports .take() just like Option does
let rstate = RUNTIME_STATE.take();
let bytes = encode_one(rstate).expect("Unable to candid encode");
let boxed_bytes = SBox::new(bytes).expect("Out of memory");
store_custom_data(1, boxed_bytes);
stable_memory_pre_upgrade().expect("Out of memory");
}
pub fn state_post_upgrade(){
stable_memory_post_upgrade();
let state: Main = retrieve_custom_data::<Main>(0).unwrap().into_inner();
STABLE_STATE.with(|s| {
*s.borrow_mut() = Some(state);
});
let bytes: Vec<u8> = retrieve_custom_data(1).unwrap().into_inner();
let rstate: RuntimeState = decode_one(&bytes).expect("Unable to candid decode");
RUNTIME_STATE.replace(rstate);
}
I’ve had a go with this but still hitting a bit of an issue. Got compile errors for take() and replace() being unstable but got around those by using the following code
pub fn state_pre_upgrade(){
// Stable Storage
let state: Main = STABLE_STATE.with(|s| s.borrow_mut().take().unwrap());
let boxed_state = SBox::new(state).expect("Out of memory");
store_custom_data(0, boxed_state);
// Runtime Storage
let rstate = RUNTIME_STATE.with(|s|{s.borrow_mut().to_owned()});
let bytes = encode_one(rstate).expect("Unable to candid encode");
let boxed_bytes = SBox::new(bytes).expect("Out of memory");
store_custom_data(1, boxed_bytes);
stable_memory_pre_upgrade().expect("Out of memory");
}
pub fn state_post_upgrade(){
stable_memory_post_upgrade();
let state: Main = retrieve_custom_data::<Main>(0).unwrap().into_inner();
STABLE_STATE.with(|s| {
*s.borrow_mut() = Some(state);
});
// Runtime Storage
let bytes: Vec<u8> = retrieve_custom_data::<RuntimeState>(1).unwrap().into_inner();
let rstate: RuntimeState = decode_one(&bytes).expect("Unable to candid decode");
RUNTIME_STATE.with(|s| {
*s.borrow_mut() = rstate;
});
}
However this then throws an error for .into_inner() for trait bounds not satisfied - AsDynSizeBytes, and StableType.
So I had a bash at deriving both for RuntimeState. AsDynSizeBytes wasn’t a problem as I can use SBoxes to wrap the dynamic stuff, however I hit a bit of a dead end with StableType.
RuntimeState has a VecDeque which I don’t think is supported at all. Removing this from the struct still gives issues with any Vec which contains a struct. I think that StableType only compiles if the type is rust standard type (string etc) or principal type?
It’s not a massive issue if I can’t save RUNTIME_STATE during upgrades in this canister… but would be a nice-to-have.