From 0ac9e7f52ba1b30105fe955be7bd5f5c01a05131 Mon Sep 17 00:00:00 2001 From: Alexander Decurnou Date: Tue, 22 Aug 2023 10:21:26 -0400 Subject: [PATCH] Implement error codes for FFI WASM functions --- packages/fuel-indexer-lib/src/lib.rs | 54 ++++++++++++++++++ packages/fuel-indexer-plugin/src/wasm.rs | 72 ++++++++++++++++++++---- packages/fuel-indexer/src/ffi.rs | 50 ++++++++++------ 3 files changed, 148 insertions(+), 28 deletions(-) diff --git a/packages/fuel-indexer-lib/src/lib.rs b/packages/fuel-indexer-lib/src/lib.rs index 9d757c32e..0acc152e9 100644 --- a/packages/fuel-indexer-lib/src/lib.rs +++ b/packages/fuel-indexer-lib/src/lib.rs @@ -35,6 +35,60 @@ impl ExecutionSource { } } +#[derive(Debug, Clone, Copy)] +pub enum WasmIndexerError { + DeserializationError = 1, + SerializationError, + PutObjectError, + UnableToSaveListType, + UninitializedMemory, + UnableToFetchLogString, + GeneralError, +} + +impl From for WasmIndexerError { + fn from(value: i32) -> Self { + match value { + 0 => unreachable!("WasmIndexerError index starts at 1"), + 1 => Self::DeserializationError, + 2 => Self::SerializationError, + 3 => Self::PutObjectError, + 4 => Self::UnableToSaveListType, + 5 => Self::UninitializedMemory, + 6 => Self::UnableToFetchLogString, + _ => Self::GeneralError, + } + } +} + +impl std::fmt::Display for WasmIndexerError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::SerializationError => { + write!(f, "Failed to serialize") + } + Self::DeserializationError => { + write!(f, "Failed to deserialize") + } + Self::UnableToSaveListType => { + write!(f, "Failed to save list") + } + Self::PutObjectError => { + write!(f, "Failed to save object") + } + Self::UninitializedMemory => { + write!(f, "Failed to create MemoryView for indexer") + } + Self::UnableToFetchLogString => { + write!(f, "Failed to fetch log string") + } + Self::GeneralError => write!(f, "A WASM error occurred"), + } + } +} + +impl std::error::Error for WasmIndexerError {} + /// Return a fully qualified indexer namespace. pub fn fully_qualified_namespace(namespace: &str, identifier: &str) -> String { format!("{}_{}", namespace, identifier) diff --git a/packages/fuel-indexer-plugin/src/wasm.rs b/packages/fuel-indexer-plugin/src/wasm.rs index 38c409a1f..889dc1114 100644 --- a/packages/fuel-indexer-plugin/src/wasm.rs +++ b/packages/fuel-indexer-plugin/src/wasm.rs @@ -4,6 +4,7 @@ use alloc::vec::Vec; use fuel_indexer_lib::{ graphql::MAX_FOREIGN_KEY_LIST_FIELDS, utils::{deserialize, serialize}, + WasmIndexerError, }; use fuel_indexer_schema::{ join::{JoinMetadata, RawQuery}, @@ -17,11 +18,14 @@ pub use sha2::{Digest, Sha256}; pub use std::collections::{HashMap, HashSet}; extern "C" { - // TODO: error codes? or just panic and let the runtime handle it? + // TODO: How do we want to return an error code for + // a function that returns a u32 but actually uses a u8? fn ff_get_object(type_id: i64, ptr: *const u8, len: *mut u8) -> *mut u8; - fn ff_put_object(type_id: i64, ptr: *const u8, len: u32); - fn ff_put_many_to_many_record(ptr: *const u8, len: u32); + // log_data prints information to stdout. fn ff_log_data(ptr: *const u8, len: u32, log_level: u32); + // Put methods have error codes. + fn ff_put_object(type_id: i64, ptr: *const u8, len: u32) -> i32; + fn ff_put_many_to_many_record(ptr: *const u8, len: u32) -> i32; } // TODO: more to do here, hook up to 'impl log::Log for Logger' @@ -49,19 +53,29 @@ impl Logger { } } +/// Trait for a type entity. +/// +/// Any entity type that will be processed through a WASM indexer is required to implement this trait. pub trait Entity<'a>: Sized + PartialEq + Eq + std::fmt::Debug { + /// Unique identifier for a type. const TYPE_ID: i64; + + /// Necessary metadata for saving an entity's list type fields. const JOIN_METADATA: Option<[Option>; MAX_FOREIGN_KEY_LIST_FIELDS]>; + /// Convert database row representation into an instance of an entity. fn from_row(vec: Vec) -> Self; + /// Convert an instance of an entity into row representation for use in a database. fn to_row(&self) -> Vec; + /// Returns an entity's internal type ID. fn type_id(&self) -> i64 { Self::TYPE_ID } - fn save_many_to_many(&self) { + /// Saves a record that contains a list of multiple elements. + fn save_many_to_many(&self) -> Result<(), WasmIndexerError> { if let Some(meta) = Self::JOIN_METADATA { let items = meta.iter().filter_map(|x| x.clone()).collect::>(); let row = self.to_row(); @@ -71,13 +85,34 @@ pub trait Entity<'a>: Sized + PartialEq + Eq + std::fmt::Debug { .filter(|query| !query.is_empty()) .collect::>(); let bytes = serialize(&queries); - unsafe { ff_put_many_to_many_record(bytes.as_ptr(), bytes.len() as u32) } + unsafe { + let res = ff_put_many_to_many_record(bytes.as_ptr(), bytes.len() as u32); + + if res != 0 { + return Err(WasmIndexerError::UnableToSaveListType); + } + + Ok(()) + } + } else { + Ok(()) } } + /// Loads a record given a UID. fn load(id: UID) -> Option { + Self::load_unsafe(id).unwrap() + } + + /// Loads a record through the FFI with the WASM runtime and checks for errors. + fn load_unsafe(id: UID) -> Result, WasmIndexerError> { unsafe { - let buff = bincode::serialize(&id.to_string()).unwrap(); + let buff = if let Ok(bytes) = bincode::serialize(&id.to_string()) { + bytes + } else { + return Err(WasmIndexerError::SerializationError); + }; + let mut bufflen = (buff.len() as u32).to_le_bytes(); let ptr = ff_get_object(Self::TYPE_ID, buff.as_ptr(), bufflen.as_mut_ptr()); @@ -85,26 +120,40 @@ pub trait Entity<'a>: Sized + PartialEq + Eq + std::fmt::Debug { if !ptr.is_null() { let len = u32::from_le_bytes(bufflen) as usize; let bytes = Vec::from_raw_parts(ptr, len, len); - let vec = deserialize(&bytes).expect("Bad serialization."); + let vec = if let Ok(v) = deserialize(&bytes) { + v + } else { + return Err(WasmIndexerError::DeserializationError); + }; - return Some(Self::from_row(vec)); + return Ok(Some(Self::from_row(vec))); } - None + Ok(None) } } + /// Saves a record. fn save(&self) { + self.save_unsafe().unwrap() + } + + /// Saves a record through the FFI with the WASM runtime and checks for errors. + fn save_unsafe(&self) -> Result<(), WasmIndexerError> { unsafe { let buf = serialize(&self.to_row()); - ff_put_object(Self::TYPE_ID, buf.as_ptr(), buf.len() as u32) + let res = ff_put_object(Self::TYPE_ID, buf.as_ptr(), buf.len() as u32); + if res != 0 { + return Err(WasmIndexerError::from(res)); + } } - self.save_many_to_many(); + self.save_many_to_many() } } #[no_mangle] +/// Allocation function to be called by an executor in a WASM runtime. fn alloc_fn(size: u32) -> *const u8 { let vec = Vec::with_capacity(size as usize); let ptr = vec.as_ptr(); @@ -115,6 +164,7 @@ fn alloc_fn(size: u32) -> *const u8 { } #[no_mangle] +/// Deallocation function to be called by an executor in a WASM runtime. fn dealloc_fn(ptr: *mut u8, len: usize) { let _vec = unsafe { Vec::from_raw_parts(ptr, len, len) }; } diff --git a/packages/fuel-indexer/src/ffi.rs b/packages/fuel-indexer/src/ffi.rs index 0beecd840..6dc880840 100644 --- a/packages/fuel-indexer/src/ffi.rs +++ b/packages/fuel-indexer/src/ffi.rs @@ -1,5 +1,5 @@ use async_std::sync::MutexGuard; -use fuel_indexer_lib::defaults; +use fuel_indexer_lib::{defaults, WasmIndexerError}; use fuel_indexer_schema::{join::RawQuery, FtColumn}; use fuel_indexer_types::ffi::{ LOG_LEVEL_DEBUG, LOG_LEVEL_ERROR, LOG_LEVEL_INFO, LOG_LEVEL_TRACE, LOG_LEVEL_WARN, @@ -147,13 +147,19 @@ fn get_object( /// Put the given type at the given pointer into memory. /// /// This function is fallible, and will panic if the type cannot be saved. -fn put_object(mut env: FunctionEnvMut, type_id: i64, ptr: u32, len: u32) { +fn put_object( + mut env: FunctionEnvMut, + type_id: i64, + ptr: u32, + len: u32, +) -> i32 { let (idx_env, store) = env.data_and_store_mut(); - let mem = idx_env - .memory - .as_mut() - .expect("Memory unitialized") - .view(&store); + + let mem = if let Some(memory) = idx_env.memory.as_mut() { + memory.view(&store) + } else { + return WasmIndexerError::UninitializedMemory as i32; + }; let mut bytes = Vec::with_capacity(len as usize); let range = ptr as usize..ptr as usize + len as usize; @@ -166,7 +172,7 @@ fn put_object(mut env: FunctionEnvMut, type_id: i64, ptr: u32, len: u3 Ok(columns) => columns, Err(e) => { error!("Failed to deserialize Vec for put_object: {e:?}",); - return; + return WasmIndexerError::DeserializationError as i32; } }; @@ -179,18 +185,21 @@ fn put_object(mut env: FunctionEnvMut, type_id: i64, ptr: u32, len: u3 .put_object(type_id, columns, bytes) .await }); + + 0 } /// Execute the arbitrary query at the given pointer. /// /// This function is fallible, and will panic if the query cannot be executed. -fn put_many_to_many_record(mut env: FunctionEnvMut, ptr: u32, len: u32) { +fn put_many_to_many_record(mut env: FunctionEnvMut, ptr: u32, len: u32) -> i32 { let (idx_env, store) = env.data_and_store_mut(); - let mem = idx_env - .memory - .as_mut() - .expect("Memory unitialized") - .view(&store); + + let mem = if let Some(memory) = idx_env.memory.as_mut() { + memory.view(&store) + } else { + return WasmIndexerError::UninitializedMemory as i32; + }; let mut bytes = Vec::with_capacity(len as usize); let range = ptr as usize..ptr as usize + len as usize; @@ -199,9 +208,14 @@ fn put_many_to_many_record(mut env: FunctionEnvMut, ptr: u32, len: u32 bytes.extend_from_slice(&mem.data_unchecked()[range]); } - let queries: Vec = - bincode::deserialize(&bytes).expect("Failed to deserialize queries"); - let queries = queries.iter().map(|q| q.to_string()).collect::>(); + let queries: Vec = match bincode::deserialize::>(&bytes) { + Ok(queries) => queries.iter().map(|q| q.to_string()).collect(), + Err(e) => { + error!("Failed to deserialize queries: {e:?}"); + return WasmIndexerError::DeserializationError as i32; + } + }; + let rt = tokio::runtime::Handle::current(); rt.block_on(async { idx_env @@ -211,6 +225,8 @@ fn put_many_to_many_record(mut env: FunctionEnvMut, ptr: u32, len: u32 .put_many_to_many_record(queries) .await }); + + 0 } /// Get the exports for the given store and environment.