diff --git a/Cargo.lock b/Cargo.lock index 053c307c..055b87a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3458,6 +3458,17 @@ dependencies = [ "url", ] +[[package]] +name = "indexer_ffi" +version = "0.1.0" +dependencies = [ + "cbindgen", + "indexer_service", + "log", + "serde_json", + "tokio", +] + [[package]] name = "indexer_service" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index c2853089..e31f9085 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ members = [ "examples/program_deployment/methods/guest", "bedrock_client", "testnet_initial_state", + "indexer_ffi", ] [workspace.dependencies] diff --git a/indexer_ffi/Cargo.toml b/indexer_ffi/Cargo.toml new file mode 100644 index 00000000..4a5a8fd0 --- /dev/null +++ b/indexer_ffi/Cargo.toml @@ -0,0 +1,26 @@ +[package] +edition = "2024" +license = { workspace = true } +name = "indexer_ffi" +version = "0.1.0" + +[dependencies] +indexer_service.workspace = true +log = { workspace = true } +serde_json = { workspace = true } +tokio = { features = ["rt-multi-thread"], workspace = true } + +[build-dependencies] +cbindgen = "0.29" + +[lib] +crate-type = ["cdylib"] +name = "indexer_ffi" + +[lints] +workspace = true + +[package.metadata.cargo-machete] +ignored = [ + "cbindgen", +] # machete does not recognize this for build dep and complains. diff --git a/indexer_ffi/build.rs b/indexer_ffi/build.rs new file mode 100644 index 00000000..92c95407 --- /dev/null +++ b/indexer_ffi/build.rs @@ -0,0 +1,12 @@ +use std::env; + +fn main() { + let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + println!("cargo:rerun-if-changed=src/"); + cbindgen::Builder::new() + .with_crate(crate_dir) + .with_language(cbindgen::Language::C) + .generate() + .expect("Unable to generate bindings") + .write_to_file("indexer_ffi.h"); +} diff --git a/indexer_ffi/cbindgen.toml b/indexer_ffi/cbindgen.toml new file mode 100644 index 00000000..79f622b7 --- /dev/null +++ b/indexer_ffi/cbindgen.toml @@ -0,0 +1,2 @@ +language = "C" # For increased compatibility +no_includes = true diff --git a/indexer_ffi/indexer_ffi.h b/indexer_ffi/indexer_ffi.h new file mode 100644 index 00000000..7c7d9a4d --- /dev/null +++ b/indexer_ffi/indexer_ffi.h @@ -0,0 +1,76 @@ +#include +#include +#include +#include + +typedef enum OperationStatus { + Ok = 0, + NullPointer = 1, + InitializationError = 2, +} OperationStatus; + +typedef struct IndexerServiceFFI { + void *indexer_handle; + void *runtime; +} IndexerServiceFFI; + +/** + * Simple wrapper around a pointer to a value or an error. + * + * Pointer is not guaranteed. You should check the error field before + * dereferencing the pointer. + */ +typedef struct PointerResult_IndexerServiceFFI__OperationStatus { + struct IndexerServiceFFI *value; + enum OperationStatus error; +} PointerResult_IndexerServiceFFI__OperationStatus; + +typedef struct PointerResult_IndexerServiceFFI__OperationStatus InitializedIndexerServiceFFIResult; + +/** + * Creates and starts an indexer based on the provided + * configuration file path. + * + * # Arguments + * + * - `config_path`: A pointer to a string representing the path to the configuration file. + * - `port`: Number representing a port, on which indexers RPC will start. + * + * # Returns + * + * An `InitializedIndexerServiceFFIResult` containing either a pointer to the + * initialized `IndexerServiceFFI` or an error code. + */ +InitializedIndexerServiceFFIResult start_indexer(const char *config_path, uint16_t port); + +/** + * Stops and frees the resources associated with the given indexer service. + * + * # Arguments + * + * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be stopped. + * + * # Returns + * + * An `OperationStatus` indicating success or failure. + * + * # Safety + * + * The caller must ensure that: + * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance + * - The `IndexerServiceFFI` instance was created by this library + * - The pointer will not be used after this function returns + */ +enum OperationStatus stop_indexer(struct IndexerServiceFFI *indexer); + +/** + * # Safety + * It's up to the caller to pass a proper pointer, if somehow from c/c++ side + * this is called with a type which doesn't come from a returned `CString` it + * will cause a segfault. + */ +void free_cstring(char *block); + +bool is_ok(const enum OperationStatus *self); + +bool is_error(const enum OperationStatus *self); diff --git a/indexer_ffi/src/api/lifecycle.rs b/indexer_ffi/src/api/lifecycle.rs new file mode 100644 index 00000000..735efd4d --- /dev/null +++ b/indexer_ffi/src/api/lifecycle.rs @@ -0,0 +1,100 @@ +use std::{ffi::c_char, path::PathBuf}; + +use tokio::runtime::Runtime; + +use crate::{IndexerServiceFFI, api::PointerResult, errors::OperationStatus}; + +pub type InitializedIndexerServiceFFIResult = PointerResult; + +/// Creates and starts an indexer based on the provided +/// configuration file path. +/// +/// # Arguments +/// +/// - `config_path`: A pointer to a string representing the path to the configuration file. +/// - `port`: Number representing a port, on which indexers RPC will start. +/// +/// # Returns +/// +/// An `InitializedIndexerServiceFFIResult` containing either a pointer to the +/// initialized `IndexerServiceFFI` or an error code. +#[unsafe(no_mangle)] +pub extern "C" fn start_indexer( + config_path: *const c_char, + port: u16, +) -> InitializedIndexerServiceFFIResult { + setup_indexer(config_path, port).map_or_else( + InitializedIndexerServiceFFIResult::from_error, + InitializedIndexerServiceFFIResult::from_value, + ) +} + +/// Initializes and starts an indexer based on the provided +/// configuration file path. +/// +/// # Arguments +/// +/// - `config_path`: A pointer to a string representing the path to the configuration file. +/// - `port`: Number representing a port, on which indexers RPC will start. +/// +/// # Returns +/// +/// A `Result` containing either the initialized `IndexerServiceFFI` or an +/// error code. +fn setup_indexer( + config_path: *const c_char, + port: u16, +) -> Result { + let user_config_path = PathBuf::from( + unsafe { std::ffi::CStr::from_ptr(config_path) } + .to_str() + .map_err(|e| { + log::error!("Could not convert the config path to string: {e}"); + OperationStatus::InitializationError + })?, + ); + let config = indexer_service::IndexerConfig::from_path(&user_config_path).map_err(|e| { + log::error!("Failed to read config: {e}"); + OperationStatus::InitializationError + })?; + + let rt = Runtime::new().unwrap(); + + let indexer_handle = rt + .block_on(indexer_service::run_server(config, port)) + .map_err(|e| { + log::error!("Could not start indexer service: {e}"); + OperationStatus::InitializationError + })?; + + Ok(IndexerServiceFFI::new(indexer_handle, rt)) +} + +/// Stops and frees the resources associated with the given indexer service. +/// +/// # Arguments +/// +/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be stopped. +/// +/// # Returns +/// +/// An `OperationStatus` indicating success or failure. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance +/// - The `IndexerServiceFFI` instance was created by this library +/// - The pointer will not be used after this function returns +#[unsafe(no_mangle)] +pub unsafe extern "C" fn stop_indexer(indexer: *mut IndexerServiceFFI) -> OperationStatus { + if indexer.is_null() { + log::error!("Attempted to stop a null indexer pointer. This is a bug. Aborting."); + return OperationStatus::NullPointer; + } + + let indexer = unsafe { Box::from_raw(indexer) }; + drop(indexer); + + OperationStatus::Ok +} diff --git a/indexer_ffi/src/api/memory.rs b/indexer_ffi/src/api/memory.rs new file mode 100644 index 00000000..f266d309 --- /dev/null +++ b/indexer_ffi/src/api/memory.rs @@ -0,0 +1,14 @@ +use std::ffi::{CString, c_char}; + +/// # Safety +/// It's up to the caller to pass a proper pointer, if somehow from c/c++ side +/// this is called with a type which doesn't come from a returned `CString` it +/// will cause a segfault. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn free_cstring(block: *mut c_char) { + if block.is_null() { + log::error!("Trying to free a null pointer. Exiting"); + return; + } + drop(unsafe { CString::from_raw(block) }); +} diff --git a/indexer_ffi/src/api/mod.rs b/indexer_ffi/src/api/mod.rs new file mode 100644 index 00000000..e84a3913 --- /dev/null +++ b/indexer_ffi/src/api/mod.rs @@ -0,0 +1,5 @@ +pub use result::PointerResult; + +pub mod lifecycle; +pub mod memory; +pub mod result; diff --git a/indexer_ffi/src/api/result.rs b/indexer_ffi/src/api/result.rs new file mode 100644 index 00000000..96cbcdd8 --- /dev/null +++ b/indexer_ffi/src/api/result.rs @@ -0,0 +1,29 @@ +/// Simple wrapper around a pointer to a value or an error. +/// +/// Pointer is not guaranteed. You should check the error field before +/// dereferencing the pointer. +#[repr(C)] +pub struct PointerResult { + pub value: *mut Type, + pub error: Error, +} + +impl PointerResult { + pub fn from_pointer(pointer: *mut Type) -> Self { + Self { + value: pointer, + error: Error::default(), + } + } + + pub fn from_value(value: Type) -> Self { + Self::from_pointer(Box::into_raw(Box::new(value))) + } + + pub const fn from_error(error: Error) -> Self { + Self { + value: std::ptr::null_mut(), + error, + } + } +} diff --git a/indexer_ffi/src/errors.rs b/indexer_ffi/src/errors.rs new file mode 100644 index 00000000..92b61e10 --- /dev/null +++ b/indexer_ffi/src/errors.rs @@ -0,0 +1,22 @@ +#[derive(Default, PartialEq, Eq)] +#[repr(C)] +pub enum OperationStatus { + #[default] + Ok = 0x0, + NullPointer = 0x1, + InitializationError = 0x2, +} + +impl OperationStatus { + #[must_use] + #[unsafe(no_mangle)] + pub extern "C" fn is_ok(&self) -> bool { + *self == Self::Ok + } + + #[must_use] + #[unsafe(no_mangle)] + pub extern "C" fn is_error(&self) -> bool { + !self.is_ok() + } +} diff --git a/indexer_ffi/src/indexer.rs b/indexer_ffi/src/indexer.rs new file mode 100644 index 00000000..23f81d2b --- /dev/null +++ b/indexer_ffi/src/indexer.rs @@ -0,0 +1,42 @@ +use std::ffi::c_void; + +use indexer_service::IndexerHandle; +use tokio::runtime::Runtime; + +#[repr(C)] +pub struct IndexerServiceFFI { + indexer_handle: *mut c_void, + runtime: *mut c_void, +} + +impl IndexerServiceFFI { + pub fn new(indexer_handle: indexer_service::IndexerHandle, runtime: Runtime) -> Self { + Self { + // Box the complex types and convert to opaque pointers + indexer_handle: Box::into_raw(Box::new(indexer_handle)).cast::(), + runtime: Box::into_raw(Box::new(runtime)).cast::(), + } + } + + // Helper to safely take ownership back + #[must_use] + pub fn into_parts(self) -> (Box, Box) { + let overwatch = unsafe { Box::from_raw(self.indexer_handle.cast::()) }; + let runtime = unsafe { Box::from_raw(self.runtime.cast::()) }; + (overwatch, runtime) + } +} + +// Implement Drop to prevent memory leaks +impl Drop for IndexerServiceFFI { + fn drop(&mut self) { + if self.indexer_handle.is_null() { + log::error!("Attempted to drop a null indexer pointer. This is a bug"); + } + if self.runtime.is_null() { + log::error!("Attempted to drop a null tokio runtime pointer. This is a bug"); + } + drop(unsafe { Box::from_raw(self.indexer_handle.cast::()) }); + drop(unsafe { Box::from_raw(self.runtime.cast::()) }); + } +} diff --git a/indexer_ffi/src/lib.rs b/indexer_ffi/src/lib.rs new file mode 100644 index 00000000..289def52 --- /dev/null +++ b/indexer_ffi/src/lib.rs @@ -0,0 +1,8 @@ +#![allow(clippy::undocumented_unsafe_blocks, reason = "It is an FFI")] + +pub use errors::OperationStatus; +pub use indexer::IndexerServiceFFI; + +mod api; +mod errors; +mod indexer;