diff --git a/keetanetwork-bindings/src/lib.rs b/keetanetwork-bindings/src/lib.rs index aca41e1..bb56136 100644 --- a/keetanetwork-bindings/src/lib.rs +++ b/keetanetwork-bindings/src/lib.rs @@ -11,6 +11,7 @@ pub mod account; pub mod error; pub mod parse; pub mod permissions; +pub mod registry; pub mod time; pub mod x509; diff --git a/keetanetwork-bindings/src/registry.rs b/keetanetwork-bindings/src/registry.rs new file mode 100644 index 0000000..5033356 --- /dev/null +++ b/keetanetwork-bindings/src/registry.rs @@ -0,0 +1,170 @@ +//! Opaque-handle registry shared by the flat-ABI binding surfaces. +//! +//! A raw FFI surface (e.g. a WASI P1 core module) cannot pass Rust objects +//! across the boundary, so it exposes them as opaque integer handles backed by +//! an internal table. + +use alloc::collections::BTreeMap; +use alloc::format; + +use crate::error::CodedError; + +/// Code for a request referencing an unknown or freed handle. +pub const INVALID_HANDLE: &str = "INVALID_HANDLE"; + +/// An opaque-handle table for `T`. +pub struct HandleRegistry { + resource: &'static str, + next: i32, + entries: BTreeMap, +} + +impl HandleRegistry { + /// An empty registry whose errors name `resource`. + pub const fn new(resource: &'static str) -> Self { + Self { resource, next: 0, entries: BTreeMap::new() } + } + + /// Store `value` under a fresh non-zero handle and return the handle. + /// + /// Skips zero and any handle still live, so a wrapped counter never + /// reassigns an existing entry. + pub fn store(&mut self, value: T) -> i32 { + loop { + self.next = self.next.wrapping_add(1).max(1); + if !self.entries.contains_key(&self.next) { + break; + } + } + + self.entries.insert(self.next, value); + + self.next + } + + /// Run `body` against the value at `handle`. + /// + /// # Errors + /// + /// Returns [`INVALID_HANDLE`] when `handle` is unknown. + pub fn with(&self, handle: i32, body: impl FnOnce(&T) -> R) -> Result { + self.entries + .get(&handle) + .map(body) + .ok_or_else(|| self.unknown()) + } + + /// Run `body` against the mutable value at `handle`. + /// + /// # Errors + /// + /// Returns [`INVALID_HANDLE`] when `handle` is unknown. + pub fn with_mut(&mut self, handle: i32, body: impl FnOnce(&mut T) -> R) -> Result { + self.entries + .get_mut(&handle) + .map(body) + .ok_or_else(|| self.unknown()) + } + + /// Remove and return the value at `handle`. + /// + /// # Errors + /// + /// Returns [`INVALID_HANDLE`] when `handle` is unknown. + pub fn take(&mut self, handle: i32) -> Result { + self.entries.remove(&handle).ok_or_else(|| self.unknown()) + } + + /// Release `handle`, ignoring an unknown one. + pub fn remove(&mut self, handle: i32) { + self.entries.remove(&handle); + } + + /// The coded error for a request referencing an unknown handle. + fn unknown(&self) -> CodedError { + CodedError::new(INVALID_HANDLE, format!("unknown {} handle", self.resource)) + } +} + +#[cfg(test)] +mod tests { + use alloc::string::ToString; + + use super::*; + + #[test] + fn store_returns_distinct_non_zero_handles() { + let mut registry = HandleRegistry::new("thing"); + let first = registry.store(1u8); + let second = registry.store(2u8); + assert_ne!(first, 0); + assert_ne!(second, 0); + assert_ne!(first, second); + } + + #[test] + fn with_reads_a_stored_value() { + let mut registry = HandleRegistry::new("thing"); + let handle = registry.store(7u8); + assert!(matches!(registry.with(handle, |value| *value), Ok(7))); + } + + #[test] + fn with_mut_mutates_in_place() { + let mut registry = HandleRegistry::new("thing"); + let handle = registry.store(1u8); + assert!(matches!(registry.with_mut(handle, |value| *value += 8), Ok(()))); + assert!(matches!(registry.with(handle, |value| *value), Ok(9))); + } + + #[test] + fn take_removes_and_returns_the_value() { + let mut registry = HandleRegistry::new("thing"); + let handle = registry.store(5u8); + assert!(matches!(registry.take(handle), Ok(5))); + assert!(registry.with(handle, |value| *value).is_err()); + } + + #[test] + fn an_unknown_handle_is_rejected_with_a_stable_code() { + let registry: HandleRegistry = HandleRegistry::new("thing"); + let code = registry + .with(1, |value| *value) + .err() + .map(|error| error.code); + assert_eq!(code, Some(INVALID_HANDLE.to_string())); + } + + #[test] + fn the_error_message_names_the_resource() { + let registry: HandleRegistry = HandleRegistry::new("thing"); + let message = registry + .with(1, |value| *value) + .err() + .map(|error| error.message); + assert_eq!(message, Some("unknown thing handle".to_string())); + } + + #[test] + fn remove_frees_the_handle() { + let mut registry = HandleRegistry::new("thing"); + let handle = registry.store(1u8); + + registry.remove(handle); + + assert!(registry.with(handle, |value| *value).is_err()); + } + + #[test] + fn a_wrapped_counter_skips_zero_and_live_handles() { + let mut registry = HandleRegistry::new("thing"); + let first = registry.store(1u8); + + registry.next = i32::MAX; + + let second = registry.store(2u8); + assert_eq!(first, 1); + assert_eq!(second, 2); + assert!(matches!(registry.with(first, |value| *value), Ok(1))); + } +} diff --git a/keetanetwork-client-wasi/src/p1/mod.rs b/keetanetwork-client-wasi/src/p1/mod.rs index 7d86686..d14bb5c 100644 --- a/keetanetwork-client-wasi/src/p1/mod.rs +++ b/keetanetwork-client-wasi/src/p1/mod.rs @@ -1,12 +1,12 @@ //! WASI Preview 1 core-module ABI: a flat, handle-based C ABI over the shared //! [`crate::pure`] surface, modeled on the JNI binding. -use std::collections::HashMap; use std::sync::{Mutex, MutexGuard, OnceLock}; use keetanetwork_account::KeyPairType; use keetanetwork_bindings::error::CodedError; use keetanetwork_bindings::parse::adjust_method; +use keetanetwork_bindings::registry::HandleRegistry; use keetanetwork_block::{AccountRef, Block, BlockBuilder, Operation, Permissions, UnsignedBlock}; use keetanetwork_vote::Vote; use keetanetwork_x509::certificates::Certificate; @@ -14,25 +14,33 @@ use keetanetwork_x509::certificates::Certificate; use crate::pure; /// Registry of live handles plus the last error, behind a single lock. -#[derive(Default)] struct State { - next: i32, - accounts: HashMap, - blocks: HashMap, - bytes: HashMap>, - permissions: HashMap, - operations: HashMap, - builders: HashMap, - unsigned: HashMap, - votes: HashMap, - certificates: HashMap, + accounts: HandleRegistry, + blocks: HandleRegistry, + bytes: HandleRegistry>, + permissions: HandleRegistry, + operations: HandleRegistry, + builders: HandleRegistry, + unsigned: HandleRegistry, + votes: HandleRegistry, + certificates: HandleRegistry, last_error: Option, } -impl State { - fn allocate(&mut self) -> i32 { - self.next += 1; - self.next +impl Default for State { + fn default() -> Self { + Self { + accounts: HandleRegistry::new("account"), + blocks: HandleRegistry::new("block"), + bytes: HandleRegistry::new("bytes"), + permissions: HandleRegistry::new("permissions"), + operations: HandleRegistry::new("operation"), + builders: HandleRegistry::new("builder"), + unsigned: HandleRegistry::new("unsigned-block"), + votes: HandleRegistry::new("vote"), + certificates: HandleRegistry::new("certificate"), + last_error: None, + } } } @@ -52,126 +60,99 @@ pub fn fail(error: CodedError) -> i32 { /// Store `bytes` as a result and return its handle. pub fn store_bytes(bytes: Vec) -> i32 { - let mut guard = state(); - let handle = guard.allocate(); - - guard.bytes.insert(handle, bytes); - handle + state().bytes.store(bytes) } /// A value kind tracked in the handle registry. trait Registered: Clone + Sized { - /// Handle-kind label used in `INVALID_HANDLE` messages. - const KIND: &'static str; - /// The registry table holding values of this kind. - fn table(state: &mut State) -> &mut HashMap; + fn table(state: &mut State) -> &mut HandleRegistry; } impl Registered for AccountRef { - const KIND: &'static str = "account"; - - fn table(state: &mut State) -> &mut HashMap { + fn table(state: &mut State) -> &mut HandleRegistry { &mut state.accounts } } impl Registered for Block { - const KIND: &'static str = "block"; - - fn table(state: &mut State) -> &mut HashMap { + fn table(state: &mut State) -> &mut HandleRegistry { &mut state.blocks } } impl Registered for Permissions { - const KIND: &'static str = "permissions"; - - fn table(state: &mut State) -> &mut HashMap { + fn table(state: &mut State) -> &mut HandleRegistry { &mut state.permissions } } impl Registered for Operation { - const KIND: &'static str = "operation"; - - fn table(state: &mut State) -> &mut HashMap { + fn table(state: &mut State) -> &mut HandleRegistry { &mut state.operations } } impl Registered for BlockBuilder { - const KIND: &'static str = "builder"; - - fn table(state: &mut State) -> &mut HashMap { + fn table(state: &mut State) -> &mut HandleRegistry { &mut state.builders } } impl Registered for UnsignedBlock { - const KIND: &'static str = "unsigned-block"; - - fn table(state: &mut State) -> &mut HashMap { + fn table(state: &mut State) -> &mut HandleRegistry { &mut state.unsigned } } impl Registered for Vote { - const KIND: &'static str = "vote"; - - fn table(state: &mut State) -> &mut HashMap { + fn table(state: &mut State) -> &mut HandleRegistry { &mut state.votes } } impl Registered for Certificate { - const KIND: &'static str = "certificate"; - - fn table(state: &mut State) -> &mut HashMap { + fn table(state: &mut State) -> &mut HandleRegistry { &mut state.certificates } } -/// The `INVALID_HANDLE` error for a missing handle of kind `T`. -fn unknown_handle() -> CodedError { - CodedError::new("INVALID_HANDLE", format!("unknown {} handle", T::KIND)) +/// Reduce a registry lookup to its value, recording the coded error and +/// yielding `None` for an unknown handle. Callers drop the state lock before +/// invoking this so the miss never re-enters [`state`]. +fn ok_or_fail(result: Result) -> Option { + match result { + Ok(value) => Some(value), + Err(error) => { + fail(error); + None + } + } } /// Store `value` under a fresh handle and return it. fn store(value: T) -> i32 { - let mut guard = state(); - let handle = guard.allocate(); - - T::table(&mut guard).insert(handle, value); - handle + T::table(&mut state()).store(value) } /// Resolve a handle to a clone, recording an error and returning `None` when /// the handle is unknown. The lock is released before recording the error so a /// miss never re-enters [`state`]. fn resolve(handle: i32) -> Option { - let value = T::table(&mut state()).get(&handle).cloned(); - if value.is_none() { - fail(unknown_handle::()); - } - - value + let result = T::table(&mut state()).with(handle, Clone::clone); + ok_or_fail(result) } /// Remove and return a handle's value, recording an error and returning `None` /// when the handle is unknown. fn take(handle: i32) -> Option { - let value = T::table(&mut state()).remove(&handle); - if value.is_none() { - fail(unknown_handle::()); - } - - value + let result = T::table(&mut state()).take(handle); + ok_or_fail(result) } /// Release a handle, ignoring an unknown one. fn release(handle: i32) { - T::table(&mut state()).remove(&handle); + T::table(&mut state()).remove(handle); } /// Store an account and return its handle. @@ -347,8 +328,8 @@ pub unsafe extern "C" fn keeta_dealloc(ptr: i32, len: i32) { pub extern "C" fn keeta_bytes_ptr(handle: i32) -> i32 { state() .bytes - .get(&handle) - .map_or(0, |bytes| bytes.as_ptr() as i32) + .with(handle, |bytes| bytes.as_ptr() as i32) + .unwrap_or(0) } /// Length of a bytes handle's data, or `0` for an unknown handle. @@ -356,14 +337,14 @@ pub extern "C" fn keeta_bytes_ptr(handle: i32) -> i32 { pub extern "C" fn keeta_bytes_len(handle: i32) -> i32 { state() .bytes - .get(&handle) - .map_or(0, |bytes| bytes.len() as i32) + .with(handle, |bytes| bytes.len() as i32) + .unwrap_or(0) } /// Release a bytes handle. #[no_mangle] pub extern "C" fn keeta_bytes_free(handle: i32) { - state().bytes.remove(&handle); + state().bytes.remove(handle); } /// The last error code as a bytes handle (`0` when no error is pending).