fix!(indexer_ffi): fix 3 existing issues, refactor the runtime handling, rm unused cbindgen.toml

This commit is contained in:
erhant 2026-06-19 12:29:19 +03:00
parent 70d33f5a16
commit 956dc6278a
20 changed files with 456 additions and 427 deletions

8
.gitignore vendored
View File

@ -1,17 +1,25 @@
.gitconfig
res/
target/
deps/
data/
.idea/
.vscode/
rocksdb
sequencer/service/data/
storage.json
result
wallet-ffi/wallet_ffi.h
bedrock_signing_key
integration_tests/configs/debug/
venv/
keycard_wallet/python/__pycache__/
keycard_wallet/python/keycard-py/
.DS_Store

8
Cargo.lock generated
View File

@ -3823,16 +3823,14 @@ dependencies = [
name = "indexer_ffi"
version = "0.1.0"
dependencies = [
"anyhow",
"cbindgen",
"indexer_service",
"env_logger",
"futures",
"indexer_core",
"indexer_service_protocol",
"indexer_service_rpc",
"jsonrpsee",
"lee",
"log",
"tokio",
"url",
]
[[package]]

View File

@ -5,7 +5,7 @@
)]
use anyhow::Result;
use indexer_ffi::{Runtime, api::types::FfiOption};
use indexer_ffi::api::types::FfiOption;
use integration_tests::L2_TO_L1_TIMEOUT;
use log::info;
@ -14,21 +14,21 @@ mod indexer_ffi_helpers;
#[test]
fn indexer_ffi_block_batching() -> Result<()> {
let (ctx, indexer_ffi, _indexer_dir) = indexer_ffi_helpers::setup()?;
// `_ctx` keeps the bedrock/sequencer harness (and its runtime) alive for the
// duration of the test; the indexer was started on that runtime.
let (_ctx, indexer_ffi, _indexer_dir) = indexer_ffi_helpers::setup()?;
// WAIT
info!("Waiting for indexer to parse blocks");
std::thread::sleep(L2_TO_L1_TIMEOUT);
// Safety: ctx runtime is valid for the lifetime of the returned Runtime
let runtime = unsafe { Runtime::from_borrowed(ctx.runtime()) };
let last_block_indexer_ffi_res = unsafe {
indexer_ffi_helpers::query_last_block(&raw const runtime, &raw const indexer_ffi)
};
let last_block_indexer_ffi_res =
unsafe { indexer_ffi_helpers::query_last_block(&raw const indexer_ffi) };
assert!(last_block_indexer_ffi_res.error.is_ok());
assert!(last_block_indexer_ffi_res.is_some);
let last_block_indexer = unsafe { *last_block_indexer_ffi_res.value };
let last_block_indexer = last_block_indexer_ffi_res.block_id;
info!("Last block on indexer FFI now is {last_block_indexer}");
@ -37,14 +37,8 @@ fn indexer_ffi_block_batching() -> Result<()> {
let before_ffi = FfiOption::<u64>::from_none();
let limit = 100;
let block_batch_ffi_res = unsafe {
indexer_ffi_helpers::query_block_vec(
&raw const runtime,
&raw const indexer_ffi,
before_ffi,
limit,
)
};
let block_batch_ffi_res =
unsafe { indexer_ffi_helpers::query_block_vec(&raw const indexer_ffi, before_ffi, limit) };
assert!(block_batch_ffi_res.error.is_ok());

View File

@ -13,6 +13,7 @@ use indexer_ffi::{
api::{
PointerResult,
lifecycle::InitializedIndexerServiceFFIResult,
query::LastBlockIdResult,
types::{FfiAccountId, FfiOption, FfiVec, account::FfiAccount, block::FfiBlock},
},
};
@ -20,20 +21,15 @@ use integration_tests::{BlockingTestContext, TestContext};
use tempfile::TempDir;
unsafe extern "C" {
pub unsafe fn query_last_block(
runtime: *const Runtime,
indexer: *const IndexerServiceFFI,
) -> PointerResult<u64, OperationStatus>;
pub unsafe fn query_last_block(indexer: *const IndexerServiceFFI) -> LastBlockIdResult;
pub unsafe fn query_block_vec(
runtime: *const Runtime,
indexer: *const IndexerServiceFFI,
before: FfiOption<u64>,
limit: u64,
) -> PointerResult<FfiVec<FfiBlock>, OperationStatus>;
pub unsafe fn query_account(
runtime: *const Runtime,
indexer: *const IndexerServiceFFI,
account_id: FfiAccountId,
) -> PointerResult<FfiAccount, OperationStatus>;
@ -41,7 +37,6 @@ unsafe extern "C" {
pub unsafe fn start_indexer(
runtime: *const Runtime,
config_path: *const c_char,
port: u16,
) -> InitializedIndexerServiceFFIResult;
}
@ -69,7 +64,7 @@ pub fn setup_indexer_ffi(
let res =
// SAFETY: lib function ensures validity of value.
unsafe { start_indexer(std::ptr::from_ref(runtime), CString::new(config_path.to_str().unwrap())?.as_ptr(), 0) };
unsafe { start_indexer(std::ptr::from_ref(runtime), CString::new(config_path.to_str().unwrap())?.as_ptr()) };
if res.error.is_error() {
anyhow::bail!("Indexer FFI error {:?}", res.error);

View File

@ -5,7 +5,6 @@
)]
use anyhow::Result;
use indexer_ffi::Runtime;
use integration_tests::L2_TO_L1_TIMEOUT;
use log::info;
@ -14,20 +13,20 @@ mod indexer_ffi_helpers;
#[test]
fn indexer_test_run_ffi() -> Result<()> {
let (ctx, indexer_ffi, _indexer_dir) = indexer_ffi_helpers::setup()?;
// `_ctx` keeps the bedrock/sequencer harness (and its runtime) alive for the
// duration of the test; the indexer was started on that runtime.
let (_ctx, indexer_ffi, _indexer_dir) = indexer_ffi_helpers::setup()?;
// RUN OBSERVATION
std::thread::sleep(L2_TO_L1_TIMEOUT);
// Safety: ctx runtime is valid for the lifetime of the returned Runtime
let runtime = unsafe { Runtime::from_borrowed(ctx.runtime()) };
let last_block_indexer_ffi_res = unsafe {
indexer_ffi_helpers::query_last_block(&raw const runtime, &raw const indexer_ffi)
};
let last_block_indexer_ffi_res =
unsafe { indexer_ffi_helpers::query_last_block(&raw const indexer_ffi) };
assert!(last_block_indexer_ffi_res.error.is_ok());
assert!(last_block_indexer_ffi_res.is_some);
let last_block_indexer_ffi = unsafe { *last_block_indexer_ffi_res.value };
let last_block_indexer_ffi = last_block_indexer_ffi_res.block_id;
info!("Last block on indexer FFI now is {last_block_indexer_ffi}");

View File

@ -6,15 +6,15 @@ version = "0.1.0"
[dependencies]
lee.workspace = true
indexer_service.workspace = true
indexer_service_rpc = { workspace = true, features = ["client"] }
indexer_service_protocol.workspace = true
indexer_core.workspace = true
# `convert` brings in the LEE <-> protocol `From`/`TryFrom` impls the query
# marshalling relies on (previously pulled in transitively via `indexer_service`).
indexer_service_protocol = { workspace = true, features = ["convert"] }
url.workspace = true
env_logger.workspace = true
log = { workspace = true }
tokio = { features = ["rt-multi-thread"], workspace = true }
jsonrpsee.workspace = true
anyhow.workspace = true
futures.workspace = true
[build-dependencies]
cbindgen = "0.29"

View File

@ -6,6 +6,7 @@ fn main() {
cbindgen::Builder::new()
.with_crate(crate_dir)
.with_language(cbindgen::Language::C)
.with_pragma_once(true)
.generate()
.expect("Unable to generate bindings")
.write_to_file("indexer_ffi.h");

View File

@ -1,2 +0,0 @@
language = "C" # For increased compatibility
no_includes = true

View File

@ -1,3 +1,5 @@
#pragma once
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
@ -22,11 +24,19 @@ typedef enum FfiBedrockStatus {
Finalized,
} FfiBedrockStatus;
typedef struct Option_u64 Option_u64;
/**
* FFI-owned indexer.
*
* Has three fields behind `c_void` (so that cbindgen never needs to see their Rust layout):
* - An [`IndexerCore`] used to answer queries
* - The background task [`JoinHandle`] that drives ingestion (consuming the block stream so the
* store stays populated)
* - A [`Handle`] to the runtime they live on.
*/
typedef struct IndexerServiceFFI {
void *indexer_handle;
void *indexer_client;
void *core;
void *ingest_handle;
void *runtime_handle;
} IndexerServiceFFI;
/**
@ -83,15 +93,18 @@ typedef struct PointerResult_Runtime__OperationStatus {
} PointerResult_Runtime__OperationStatus;
/**
* Simple wrapper around a pointer to a value or an error.
* Result of [`query_last_block`], returned **inline** (no heap allocation, so
* there is no corresponding `free_*` to call).
*
* Pointer is not guaranteed. You should check the error field before
* dereferencing the pointer.
* `block_id` is only meaningful when `error` is `Ok` *and* `is_some` is
* `true`. An `Ok` result with `is_some == false` means the indexer has no
* finalized block yet (an empty chain) which is distinct from an error.
*/
typedef struct PointerResult_Option_u64_____OperationStatus {
struct Option_u64 *value;
typedef struct LastBlockIdResult {
uint64_t block_id;
bool is_some;
enum OperationStatus error;
} PointerResult_Option_u64_____OperationStatus;
} LastBlockIdResult;
typedef uint64_t FfiBlockId;
@ -411,7 +424,6 @@ typedef struct PointerResult_FfiVec_FfiTransaction_____OperationStatus {
* # 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
*
@ -424,8 +436,7 @@ typedef struct PointerResult_FfiVec_FfiTransaction_____OperationStatus {
* - `config_path` is a valid pointer to a null-terminated C string.
*/
InitializedIndexerServiceFFIResult start_indexer(const struct Runtime *runtime,
const char *config_path,
uint16_t port);
const char *config_path);
/**
* Creates a new [`tokio::runtime::Runtime`].
@ -452,6 +463,20 @@ struct PointerResult_Runtime__OperationStatus new_runtime(void);
*/
enum OperationStatus stop_indexer(struct IndexerServiceFFI *indexer);
/**
* Initializes the FFI's logger.
*
* Wires up `env_logger`, so the library's `log::*` output is controlled by the
* `RUST_LOG` environment variable (e.g. `RUST_LOG=info`). Without this, the
* FFI's log calls go nowhere and since failures are otherwise reported only
* as numeric [`OperationStatus`](crate::errors::OperationStatus) codes, there
* is no other way to see *why* a call failed.
*
* Safe to call multiple times and from any consumer: if a global logger is
* already set, the call is a no-op.
*/
void init_logger(void);
/**
* # Safety
* It's up to the caller to pass a proper pointer, if somehow from c/c++ side
@ -469,16 +494,15 @@ void free_cstring(char *block);
*
* # Returns
*
* A `PointerResult<Option<u64>, OperationStatus>` indicating success or failure.
* A [`LastBlockIdResult`] indicating success or failure. The block id is
* returned inline; nothing needs to be freed.
*
* # Safety
*
* The caller must ensure that:
* - `runtime` is a valid pointer to a [`Runtime`] instance.
* - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
*/
struct PointerResult_Option_u64_____OperationStatus query_last_block(const struct Runtime *runtime,
const struct IndexerServiceFFI *indexer);
struct LastBlockIdResult query_last_block(const struct IndexerServiceFFI *indexer);
/**
* Query the block by id from indexer.
@ -495,15 +519,13 @@ struct PointerResult_Option_u64_____OperationStatus query_last_block(const struc
* # Safety
*
* The caller must ensure that:
* - `runtime` is a valid pointer to a [`Runtime`] instance.
* - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
*/
struct PointerResult_FfiBlockOpt__OperationStatus query_block(const struct Runtime *runtime,
const struct IndexerServiceFFI *indexer,
struct PointerResult_FfiBlockOpt__OperationStatus query_block(const struct IndexerServiceFFI *indexer,
FfiBlockId block_id);
/**
* Query the block by id from indexer.
* Query the block by hash from indexer.
*
* # Arguments
*
@ -517,11 +539,9 @@ struct PointerResult_FfiBlockOpt__OperationStatus query_block(const struct Runti
* # Safety
*
* The caller must ensure that:
* - `runtime` is a valid pointer to a [`Runtime`] instance.
* - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
*/
struct PointerResult_FfiBlockOpt__OperationStatus query_block_by_hash(const struct Runtime *runtime,
const struct IndexerServiceFFI *indexer,
struct PointerResult_FfiBlockOpt__OperationStatus query_block_by_hash(const struct IndexerServiceFFI *indexer,
FfiHashType hash);
/**
@ -539,15 +559,13 @@ struct PointerResult_FfiBlockOpt__OperationStatus query_block_by_hash(const stru
* # Safety
*
* The caller must ensure that:
* - `runtime` is a valid pointer to a [`Runtime`] instance.
* - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
*/
struct PointerResult_FfiAccount__OperationStatus query_account(const struct Runtime *runtime,
const struct IndexerServiceFFI *indexer,
struct PointerResult_FfiAccount__OperationStatus query_account(const struct IndexerServiceFFI *indexer,
FfiAccountId account_id);
/**
* Query the trasnaction by hash from indexer.
* Query the transaction by hash from indexer.
*
* # Arguments
*
@ -562,10 +580,8 @@ struct PointerResult_FfiAccount__OperationStatus query_account(const struct Runt
*
* The caller must ensure that:
* - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
* - `runtime` is a valid pointer to a [`Runtime`] instance.
*/
struct PointerResult_FfiOption_FfiTransaction_____OperationStatus query_transaction(const struct Runtime *runtime,
const struct IndexerServiceFFI *indexer,
struct PointerResult_FfiOption_FfiTransaction_____OperationStatus query_transaction(const struct IndexerServiceFFI *indexer,
FfiHashType hash);
/**
@ -585,10 +601,8 @@ struct PointerResult_FfiOption_FfiTransaction_____OperationStatus query_transact
*
* The caller must ensure that:
* - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
* - `runtime` is a valid pointer to a [`Runtime`] instance.
*/
struct PointerResult_FfiVec_FfiBlock_____OperationStatus query_block_vec(const struct Runtime *runtime,
const struct IndexerServiceFFI *indexer,
struct PointerResult_FfiVec_FfiBlock_____OperationStatus query_block_vec(const struct IndexerServiceFFI *indexer,
struct FfiOption_u64 before,
uint64_t limit);
@ -604,16 +618,14 @@ struct PointerResult_FfiVec_FfiBlock_____OperationStatus query_block_vec(const s
*
* # Returns
*
* A `PointerResult<FfiVec<FfiBlock>, OperationStatus>` indicating success or failure.
* A `PointerResult<FfiVec<FfiTransaction>, OperationStatus>` indicating success or failure.
*
* # Safety
*
* The caller must ensure that:
* - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
* - `runtime` is a valid pointer to a [`Runtime`] instance.
*/
struct PointerResult_FfiVec_FfiTransaction_____OperationStatus query_transactions_by_account(const struct Runtime *runtime,
const struct IndexerServiceFFI *indexer,
struct PointerResult_FfiVec_FfiTransaction_____OperationStatus query_transactions_by_account(const struct IndexerServiceFFI *indexer,
FfiAccountId account_id,
uint64_t offset,
uint64_t limit);
@ -621,9 +633,14 @@ struct PointerResult_FfiVec_FfiTransaction_____OperationStatus query_transaction
/**
* Frees the resources associated with the given ffi account.
*
* Takes ownership of the whole allocation produced by a `query_*` call: the
* outer `Box<FfiAccount>` (the `PointerResult.value` pointer) *and* its inner
* data buffer. Passing the struct by value previously freed only the inner
* buffer and leaked the outer box.
*
* # Arguments
*
* - `val`: An instance of `FfiAccount`.
* - `val`: The `*mut FfiAccount` returned in `PointerResult.value`.
*
* # Returns
*
@ -632,12 +649,18 @@ struct PointerResult_FfiVec_FfiTransaction_____OperationStatus query_transaction
* # Safety
*
* The caller must ensure that:
* - `val` is a valid instance of `FfiAccount`.
* - `val` is a pointer to an `FfiAccount` produced by this library and not yet freed.
*/
void free_ffi_account(struct FfiAccount val);
void free_ffi_account(struct FfiAccount *val);
/**
* Frees the resources associated with the given ffi block.
* Frees the resources owned by an `FfiBlock` value.
*
* This frees the block's transaction bodies (the only heap-owning field); the
* header/status fields are `Copy`. It operates on the struct by value because
* it is an element-level helper, used both for the vector path
* ([`free_ffi_block_vec`]) and the optional path ([`free_ffi_block_opt`]) in
* neither case is an `FfiBlock` itself wrapped in its own outer box.
*
* # Arguments
*
@ -650,16 +673,20 @@ void free_ffi_account(struct FfiAccount val);
* # Safety
*
* The caller must ensure that:
* - `val` is a valid instance of `FfiBlock`.
* - `val` is a valid instance of `FfiBlock` produced by this library and not yet freed.
*/
void free_ffi_block(struct FfiBlock val);
/**
* Frees the resources associated with the given ffi block option.
*
* Takes ownership of the whole allocation produced by a `query_*` call: the
* outer `Box<FfiBlockOpt>` (the `PointerResult.value` pointer), the inner
* `Box<FfiBlock>` (when present), and that block's transaction bodies.
*
* # Arguments
*
* - `val`: An instance of `FfiBlockOpt`.
* - `val`: The `*mut FfiBlockOpt` returned in `PointerResult.value`.
*
* # Returns
*
@ -668,16 +695,20 @@ void free_ffi_block(struct FfiBlock val);
* # Safety
*
* The caller must ensure that:
* - `val` is a valid instance of `FfiBlockOpt`.
* - `val` is a pointer to an `FfiBlockOpt` produced by this library and not yet freed.
*/
void free_ffi_block_opt(FfiBlockOpt val);
void free_ffi_block_opt(FfiBlockOpt *val);
/**
* Frees the resources associated with the given ffi block vector.
*
* Takes ownership of the whole allocation produced by a `query_*` call: the
* outer `Box<FfiVec<FfiBlock>>` (the `PointerResult.value` pointer), the
* vector's backing buffer, and every block within it.
*
* # Arguments
*
* - `val`: An instance of `FfiVec<FfiBlock>`.
* - `val`: The `*mut FfiVec<FfiBlock>` returned in `PointerResult.value`.
*
* # Returns
*
@ -686,9 +717,9 @@ void free_ffi_block_opt(FfiBlockOpt val);
* # Safety
*
* The caller must ensure that:
* - `val` is a valid instance of `FfiVec<FfiBlock>`.
* - `val` is a pointer to an `FfiVec<FfiBlock>` produced by this library and not yet freed.
*/
void free_ffi_block_vec(struct FfiVec_FfiBlock val);
void free_ffi_block_vec(struct FfiVec_FfiBlock *val);
/**
* Frees the resources associated with the given ffi transaction.
@ -711,9 +742,13 @@ void free_ffi_transaction(struct FfiTransaction val);
/**
* Frees the resources associated with the given ffi transaction option.
*
* Takes ownership of the whole allocation produced by a `query_*` call: the
* outer `Box<FfiOption<FfiTransaction>>` (the `PointerResult.value` pointer),
* the inner `Box<FfiTransaction>` (when present), and its body.
*
* # Arguments
*
* - `val`: An instance of `FfiOption<FfiTransaction>`.
* - `val`: The `*mut FfiOption<FfiTransaction>` returned in `PointerResult.value`.
*
* # Returns
*
@ -722,16 +757,21 @@ void free_ffi_transaction(struct FfiTransaction val);
* # Safety
*
* The caller must ensure that:
* - `val` is a valid instance of `FfiOption<FfiTransaction>`.
* - `val` is a pointer to an `FfiOption<FfiTransaction>` produced by this library and not yet
* freed.
*/
void free_ffi_transaction_opt(struct FfiOption_FfiTransaction val);
void free_ffi_transaction_opt(struct FfiOption_FfiTransaction *val);
/**
* Frees the resources associated with the given vector of ffi transactions.
*
* Takes ownership of the whole allocation produced by a `query_*` call: the
* outer `Box<FfiVec<FfiTransaction>>` (the `PointerResult.value` pointer), the
* vector's backing buffer, and every transaction within it.
*
* # Arguments
*
* - `val`: An instance of `FfiVec<FfiTransaction>`.
* - `val`: The `*mut FfiVec<FfiTransaction>` returned in `PointerResult.value`.
*
* # Returns
*
@ -740,9 +780,9 @@ void free_ffi_transaction_opt(struct FfiOption_FfiTransaction val);
* # Safety
*
* The caller must ensure that:
* - `val` is a valid instance of `FfiVec<FfiTransaction>`.
* - `val` is a pointer to an `FfiVec<FfiTransaction>` produced by this library and not yet freed.
*/
void free_ffi_transaction_vec(struct FfiVec_FfiTransaction val);
void free_ffi_transaction_vec(struct FfiVec_FfiTransaction *val);
bool is_ok(const enum OperationStatus *self);

View File

@ -1,36 +0,0 @@
use std::net::SocketAddr;
use url::Url;
use crate::OperationStatus;
#[derive(Debug, Clone, Copy)]
pub enum UrlProtocol {
Http,
Ws,
}
impl std::fmt::Display for UrlProtocol {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Http => write!(f, "http"),
Self::Ws => write!(f, "ws"),
}
}
}
pub(crate) fn addr_to_url(protocol: UrlProtocol, addr: SocketAddr) -> Result<Url, OperationStatus> {
// Convert 0.0.0.0 to 127.0.0.1 for client connections
// When binding to port 0, the server binds to 0.0.0.0:<random_port>
// but clients need to connect to 127.0.0.1:<port> to work reliably
let url_string = if addr.ip().is_unspecified() {
format!("{protocol}://127.0.0.1:{}", addr.port())
} else {
format!("{protocol}://{addr}")
};
url_string.parse().map_err(|e| {
log::error!("Could not parse indexer url: {e}");
OperationStatus::InitializationError
})
}

View File

@ -1,14 +1,9 @@
use std::{ffi::c_char, path::PathBuf};
use crate::{
IndexerServiceFFI, Runtime,
api::{
PointerResult,
client::{UrlProtocol, addr_to_url},
},
client::{IndexerClient, IndexerClientTrait as _},
errors::OperationStatus,
};
use futures::StreamExt as _;
use indexer_core::{IndexerCore, config::IndexerConfig};
use crate::{IndexerServiceFFI, Runtime, api::PointerResult, errors::OperationStatus};
pub type InitializedIndexerServiceFFIResult = PointerResult<IndexerServiceFFI, OperationStatus>;
@ -18,7 +13,6 @@ pub type InitializedIndexerServiceFFIResult = PointerResult<IndexerServiceFFI, O
/// # 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
///
@ -33,10 +27,9 @@ pub type InitializedIndexerServiceFFIResult = PointerResult<IndexerServiceFFI, O
pub unsafe extern "C" fn start_indexer(
runtime: *const Runtime,
config_path: *const c_char,
port: u16,
) -> InitializedIndexerServiceFFIResult {
// SAFETY: The caller must ensure the validness of the `runtime` and `config_path` pointers.
unsafe { setup_indexer(runtime, config_path, port) }.map_or_else(
unsafe { setup_indexer(runtime, config_path) }.map_or_else(
InitializedIndexerServiceFFIResult::from_error,
InitializedIndexerServiceFFIResult::from_value,
)
@ -57,7 +50,6 @@ pub extern "C" fn new_runtime() -> PointerResult<Runtime, OperationStatus> {
/// # 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
///
@ -71,7 +63,6 @@ pub extern "C" fn new_runtime() -> PointerResult<Runtime, OperationStatus> {
unsafe fn setup_indexer(
runtime: *const Runtime,
config_path: *const c_char,
port: u16,
) -> Result<IndexerServiceFFI, OperationStatus> {
let user_config_path = PathBuf::from(
unsafe { std::ffi::CStr::from_ptr(config_path) }
@ -81,7 +72,7 @@ unsafe fn setup_indexer(
OperationStatus::InitializationError
})?,
);
let config = indexer_service::IndexerConfig::from_path(&user_config_path).map_err(|e| {
let config = IndexerConfig::from_path(&user_config_path).map_err(|e| {
log::error!("Failed to read config: {e}");
OperationStatus::InitializationError
})?;
@ -90,22 +81,30 @@ unsafe fn setup_indexer(
// `tokio::runtime::Runtime` instance.
let runtime = unsafe { &*runtime };
let indexer_handle = runtime
.block_on(indexer_service::run_server(config, port))
.map_err(|e| {
log::error!("Could not start indexer service: {e}");
OperationStatus::InitializationError
})?;
let core = IndexerCore::new(config).map_err(|e| {
log::error!("Could not initialize indexer core: {e}");
OperationStatus::InitializationError
})?;
let indexer_url = addr_to_url(UrlProtocol::Ws, indexer_handle.addr())?;
let indexer_client = runtime
.block_on(IndexerClient::new(&indexer_url))
.map_err(|e| {
log::error!("Could not start indexer client: {e}");
OperationStatus::InitializationError
})?;
// The block stream writes each parsed block into the store as a side effect
// of being polled, so we spawn a task that simply drains it. There are no
// subscribers — queries read the store directly via `core()`.
let ingest_core = core.clone();
let ingest_handle = runtime.spawn(async move {
let mut block_stream = std::pin::pin!(ingest_core.subscribe_parse_block_stream());
while let Some(result) = block_stream.next().await {
if let Err(e) = result {
log::error!("Indexer ingestion error: {e:#}");
}
}
log::warn!("Indexer block stream ended");
});
Ok(IndexerServiceFFI::new(indexer_handle, indexer_client))
Ok(IndexerServiceFFI::new(
core,
ingest_handle,
runtime.handle().clone(),
))
}
/// Stops and frees the resources associated with the given indexer service.

View File

@ -0,0 +1,14 @@
/// Initializes the FFI's logger.
///
/// Wires up `env_logger`, so the library's `log::*` output is controlled by the
/// `RUST_LOG` environment variable (e.g. `RUST_LOG=info`). Without this, the
/// FFI's log calls go nowhere — and since failures are otherwise reported only
/// as numeric [`OperationStatus`](crate::errors::OperationStatus) codes, there
/// is no other way to see *why* a call failed.
///
/// Safe to call multiple times and from any consumer: if a global logger is
/// already set, the call is a no-op.
#[unsafe(no_mangle)]
pub extern "C" fn init_logger() {
let _ignore_me = env_logger::try_init();
}

View File

@ -1,7 +1,7 @@
pub use result::PointerResult;
pub mod client;
pub mod lifecycle;
pub mod logging;
pub mod memory;
pub mod query;
pub mod result;

View File

@ -1,8 +1,7 @@
use indexer_service_protocol::{AccountId, HashType};
use indexer_service_rpc::RpcClient as _;
use indexer_service_protocol::AccountId;
use crate::{
IndexerServiceFFI, Runtime,
IndexerServiceFFI,
api::{
PointerResult,
types::{
@ -15,6 +14,45 @@ use crate::{
errors::OperationStatus,
};
/// Result of [`query_last_block`], returned **inline** (no heap allocation, so
/// there is no corresponding `free_*` to call).
///
/// `block_id` is only meaningful when `error` is `Ok` *and* `is_some` is
/// `true`. An `Ok` result with `is_some == false` means the indexer has no
/// finalized block yet (an empty chain) — which is distinct from an error.
#[repr(C)]
pub struct LastBlockIdResult {
pub block_id: u64,
pub is_some: bool,
pub error: OperationStatus,
}
impl LastBlockIdResult {
const fn error(error: OperationStatus) -> Self {
Self {
block_id: 0,
is_some: false,
error,
}
}
const fn none() -> Self {
Self {
block_id: 0,
is_some: false,
error: OperationStatus::Ok,
}
}
const fn some(block_id: u64) -> Self {
Self {
block_id,
is_some: true,
error: OperationStatus::Ok,
}
}
}
/// Query the last block id from indexer.
///
/// # Arguments
@ -23,34 +61,29 @@ use crate::{
///
/// # Returns
///
/// A `PointerResult<Option<u64>, OperationStatus>` indicating success or failure.
/// A [`LastBlockIdResult`] indicating success or failure. The block id is
/// returned inline; nothing needs to be freed.
///
/// # Safety
///
/// The caller must ensure that:
/// - `runtime` is a valid pointer to a [`Runtime`] instance.
/// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn query_last_block(
runtime: *const Runtime,
indexer: *const IndexerServiceFFI,
) -> PointerResult<Option<u64>, OperationStatus> {
pub unsafe extern "C" fn query_last_block(indexer: *const IndexerServiceFFI) -> LastBlockIdResult {
if indexer.is_null() {
log::error!("Attempted to query a null indexer pointer. This is a bug. Aborting.");
return PointerResult::from_error(OperationStatus::NullPointer);
return LastBlockIdResult::error(OperationStatus::NullPointer);
}
let indexer = unsafe { &*indexer };
let client = indexer.client();
let runtime = unsafe { &*runtime };
runtime
.block_on(client.get_last_finalized_block_id())
.map_or_else(
|_| PointerResult::from_error(OperationStatus::ClientError),
PointerResult::from_value,
)
indexer.core().store.get_last_block_id().map_or_else(
|e| {
log::error!("Failed to query last block id: {e:#}");
LastBlockIdResult::error(OperationStatus::ClientError)
},
|opt| opt.map_or_else(LastBlockIdResult::none, LastBlockIdResult::some),
)
}
/// Query the block by id from indexer.
@ -67,11 +100,9 @@ pub unsafe extern "C" fn query_last_block(
/// # Safety
///
/// The caller must ensure that:
/// - `runtime` is a valid pointer to a [`Runtime`] instance.
/// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn query_block(
runtime: *const Runtime,
indexer: *const IndexerServiceFFI,
block_id: FfiBlockId,
) -> PointerResult<FfiBlockOpt, OperationStatus> {
@ -82,24 +113,23 @@ pub unsafe extern "C" fn query_block(
let indexer = unsafe { &*indexer };
let client = indexer.client();
let runtime = unsafe { &*runtime };
indexer.core().store.get_block_at_id(block_id).map_or_else(
|e| {
log::error!("Failed to query block by id: {e:#}");
PointerResult::from_error(OperationStatus::ClientError)
},
|block_opt| {
let block_ffi = block_opt.map_or_else(FfiBlockOpt::from_none, |block| {
let block: indexer_service_protocol::Block = block.into();
FfiBlockOpt::from_value(block.into())
});
runtime
.block_on(client.get_block_by_id(block_id))
.map_or_else(
|_| PointerResult::from_error(OperationStatus::ClientError),
|block_opt| {
let block_ffi = block_opt.map_or_else(FfiBlockOpt::from_none, |block| {
FfiBlockOpt::from_value(block.into())
});
PointerResult::from_value(block_ffi)
},
)
PointerResult::from_value(block_ffi)
},
)
}
/// Query the block by id from indexer.
/// Query the block by hash from indexer.
///
/// # Arguments
///
@ -113,11 +143,9 @@ pub unsafe extern "C" fn query_block(
/// # Safety
///
/// The caller must ensure that:
/// - `runtime` is a valid pointer to a [`Runtime`] instance.
/// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn query_block_by_hash(
runtime: *const Runtime,
indexer: *const IndexerServiceFFI,
hash: FfiHashType,
) -> PointerResult<FfiBlockOpt, OperationStatus> {
@ -128,15 +156,18 @@ pub unsafe extern "C" fn query_block_by_hash(
let indexer = unsafe { &*indexer };
let client = indexer.client();
let runtime = unsafe { &*runtime };
runtime
.block_on(client.get_block_by_hash(HashType(hash.data)))
indexer
.core()
.store
.get_block_by_hash(hash.data)
.map_or_else(
|_| PointerResult::from_error(OperationStatus::ClientError),
|e| {
log::error!("Failed to query block by hash: {e:#}");
PointerResult::from_error(OperationStatus::ClientError)
},
|block_opt| {
let block_ffi = block_opt.map_or_else(FfiBlockOpt::from_none, |block| {
let block: indexer_service_protocol::Block = block.into();
FfiBlockOpt::from_value(block.into())
});
@ -159,11 +190,9 @@ pub unsafe extern "C" fn query_block_by_hash(
/// # Safety
///
/// The caller must ensure that:
/// - `runtime` is a valid pointer to a [`Runtime`] instance.
/// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn query_account(
runtime: *const Runtime,
indexer: *const IndexerServiceFFI,
account_id: FfiAccountId,
) -> PointerResult<FfiAccount, OperationStatus> {
@ -174,23 +203,29 @@ pub unsafe extern "C" fn query_account(
let indexer = unsafe { &*indexer };
let client = indexer.client();
let runtime = unsafe { &*runtime };
runtime
.block_on(client.get_account(AccountId {
value: account_id.data,
}))
// `account_current_state` is the only async store call; drive it on the
// runtime the indexer was started on.
let account_id = AccountId {
value: account_id.data,
};
indexer
.runtime_handle()
.block_on(
indexer
.core()
.store
.account_current_state(&account_id.into()),
)
.map_or_else(
|_| PointerResult::from_error(OperationStatus::ClientError),
|acc| {
let acc_lee: lee::Account = acc.try_into().expect("Source is in blocks, must fit");
PointerResult::from_value(acc_lee.into())
|e| {
log::error!("Failed to query account: {e:#}");
PointerResult::from_error(OperationStatus::ClientError)
},
|account| PointerResult::from_value(account.into()),
)
}
/// Query the trasnaction by hash from indexer.
/// Query the transaction by hash from indexer.
///
/// # Arguments
///
@ -205,10 +240,8 @@ pub unsafe extern "C" fn query_account(
///
/// The caller must ensure that:
/// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
/// - `runtime` is a valid pointer to a [`Runtime`] instance.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn query_transaction(
runtime: *const Runtime,
indexer: *const IndexerServiceFFI,
hash: FfiHashType,
) -> PointerResult<FfiOption<FfiTransaction>, OperationStatus> {
@ -219,15 +252,18 @@ pub unsafe extern "C" fn query_transaction(
let indexer = unsafe { &*indexer };
let client = indexer.client();
let runtime = unsafe { &*runtime };
runtime
.block_on(client.get_transaction(HashType(hash.data)))
indexer
.core()
.store
.get_transaction_by_hash(hash.data)
.map_or_else(
|_| PointerResult::from_error(OperationStatus::ClientError),
|e| {
log::error!("Failed to query transaction: {e:#}");
PointerResult::from_error(OperationStatus::ClientError)
},
|tx_opt| {
let tx_ffi = tx_opt.map_or_else(FfiOption::<FfiTransaction>::from_none, |tx| {
let tx: indexer_service_protocol::Transaction = tx.into();
FfiOption::<FfiTransaction>::from_value(tx.into())
});
@ -252,10 +288,8 @@ pub unsafe extern "C" fn query_transaction(
///
/// The caller must ensure that:
/// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
/// - `runtime` is a valid pointer to a [`Runtime`] instance.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn query_block_vec(
runtime: *const Runtime,
indexer: *const IndexerServiceFFI,
before: FfiOption<u64>,
limit: u64,
@ -267,21 +301,26 @@ pub unsafe extern "C" fn query_block_vec(
let indexer = unsafe { &*indexer };
let client = indexer.client();
let runtime = unsafe { &*runtime };
let before_std = before.is_some.then(|| unsafe { *before.value });
runtime
.block_on(client.get_blocks(before_std, limit))
indexer
.core()
.store
.get_block_batch(before_std, limit)
.map_or_else(
|_| PointerResult::from_error(OperationStatus::ClientError),
|e| {
log::error!("Failed to query block batch: {e:#}");
PointerResult::from_error(OperationStatus::ClientError)
},
|block_vec| {
PointerResult::from_value(
block_vec
.into_iter()
.map(Into::into)
.collect::<Vec<_>>()
.map(|block| {
let block: indexer_service_protocol::Block = block.into();
block.into()
})
.collect::<Vec<FfiBlock>>()
.into(),
)
},
@ -299,16 +338,14 @@ pub unsafe extern "C" fn query_block_vec(
///
/// # Returns
///
/// A `PointerResult<FfiVec<FfiBlock>, OperationStatus>` indicating success or failure.
/// A `PointerResult<FfiVec<FfiTransaction>, OperationStatus>` indicating success or failure.
///
/// # Safety
///
/// The caller must ensure that:
/// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance.
/// - `runtime` is a valid pointer to a [`Runtime`] instance.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn query_transactions_by_account(
runtime: *const Runtime,
indexer: *const IndexerServiceFFI,
account_id: FfiAccountId,
offset: u64,
@ -321,25 +358,24 @@ pub unsafe extern "C" fn query_transactions_by_account(
let indexer = unsafe { &*indexer };
let client = indexer.client();
let runtime = unsafe { &*runtime };
runtime
.block_on(client.get_transactions_by_account(
AccountId {
value: account_id.data,
},
offset,
limit,
))
indexer
.core()
.store
.get_transactions_by_account(account_id.data, offset, limit)
.map_or_else(
|_| PointerResult::from_error(OperationStatus::ClientError),
|e| {
log::error!("Failed to query transactions by account: {e:#}");
PointerResult::from_error(OperationStatus::ClientError)
},
|tx_vec| {
PointerResult::from_value(
tx_vec
.into_iter()
.map(Into::into)
.collect::<Vec<_>>()
.map(|tx| {
let tx: indexer_service_protocol::Transaction = tx.into();
tx.into()
})
.collect::<Vec<FfiTransaction>>()
.into(),
)
},

View File

@ -100,9 +100,14 @@ impl From<&FfiAccount> for indexer_service_protocol::Account {
/// Frees the resources associated with the given ffi account.
///
/// Takes ownership of the whole allocation produced by a `query_*` call: the
/// outer `Box<FfiAccount>` (the `PointerResult.value` pointer) *and* its inner
/// data buffer. Passing the struct by value previously freed only the inner
/// buffer and leaked the outer box.
///
/// # Arguments
///
/// - `val`: An instance of `FfiAccount`.
/// - `val`: The `*mut FfiAccount` returned in `PointerResult.value`.
///
/// # Returns
///
@ -111,9 +116,15 @@ impl From<&FfiAccount> for indexer_service_protocol::Account {
/// # Safety
///
/// The caller must ensure that:
/// - `val` is a valid instance of `FfiAccount`.
/// - `val` is a pointer to an `FfiAccount` produced by this library and not yet freed.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn free_ffi_account(val: FfiAccount) {
let orig_val: indexer_service_protocol::Account = val.into();
pub unsafe extern "C" fn free_ffi_account(val: *mut FfiAccount) {
if val.is_null() {
log::error!("Trying to free a null pointer. Exiting");
return;
}
// Reclaim the outer box, then convert to drop the inner data buffer.
let boxed = unsafe { Box::from_raw(val) };
let orig_val: indexer_service_protocol::Account = (*boxed).into();
drop(orig_val);
}

View File

@ -2,7 +2,7 @@ use indexer_service_protocol::{BedrockStatus, Block, BlockHeader, HashType, Sign
use crate::api::types::{
FfiBlockId, FfiHashType, FfiOption, FfiSignature, FfiTimestamp, FfiVec,
transaction::free_ffi_transaction_vec, vectors::FfiBlockBody,
transaction::free_transaction_vec_value, vectors::FfiBlockBody,
};
#[repr(C)]
@ -91,7 +91,13 @@ impl From<FfiBedrockStatus> for BedrockStatus {
}
}
/// Frees the resources associated with the given ffi block.
/// Frees the resources owned by an `FfiBlock` value.
///
/// This frees the block's transaction bodies (the only heap-owning field); the
/// header/status fields are `Copy`. It operates on the struct by value because
/// it is an element-level helper, used both for the vector path
/// ([`free_ffi_block_vec`]) and the optional path ([`free_ffi_block_opt`]) — in
/// neither case is an `FfiBlock` itself wrapped in its own outer box.
///
/// # Arguments
///
@ -104,7 +110,7 @@ impl From<FfiBedrockStatus> for BedrockStatus {
/// # Safety
///
/// The caller must ensure that:
/// - `val` is a valid instance of `FfiBlock`.
/// - `val` is a valid instance of `FfiBlock` produced by this library and not yet freed.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn free_ffi_block(val: FfiBlock) {
// We don't really need all the casts, but just in case
@ -121,16 +127,18 @@ pub unsafe extern "C" fn free_ffi_block(val: FfiBlock) {
#[expect(clippy::let_underscore_must_use, reason = "No use for this Copy type")]
let _: BedrockStatus = val.bedrock_status.into();
unsafe {
free_ffi_transaction_vec(ffi_tx_ffi_vec);
};
free_transaction_vec_value(ffi_tx_ffi_vec);
}
/// Frees the resources associated with the given ffi block option.
///
/// Takes ownership of the whole allocation produced by a `query_*` call: the
/// outer `Box<FfiBlockOpt>` (the `PointerResult.value` pointer), the inner
/// `Box<FfiBlock>` (when present), and that block's transaction bodies.
///
/// # Arguments
///
/// - `val`: An instance of `FfiBlockOpt`.
/// - `val`: The `*mut FfiBlockOpt` returned in `PointerResult.value`.
///
/// # Returns
///
@ -139,37 +147,32 @@ pub unsafe extern "C" fn free_ffi_block(val: FfiBlock) {
/// # Safety
///
/// The caller must ensure that:
/// - `val` is a valid instance of `FfiBlockOpt`.
/// - `val` is a pointer to an `FfiBlockOpt` produced by this library and not yet freed.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn free_ffi_block_opt(val: FfiBlockOpt) {
if val.is_some {
let value = unsafe { Box::from_raw(val.value) };
// We don't really need all the casts, but just in case
// All except `ffi_tx_ffi_vec` is Copy types, so no need for Drop
let _ = BlockHeader {
block_id: value.header.block_id,
prev_block_hash: HashType(value.header.prev_block_hash.data),
hash: HashType(value.header.hash.data),
timestamp: value.header.timestamp,
signature: Signature(value.header.signature.data),
};
let ffi_tx_ffi_vec = value.body;
#[expect(clippy::let_underscore_must_use, reason = "No use for this Copy type")]
let _: BedrockStatus = value.bedrock_status.into();
pub unsafe extern "C" fn free_ffi_block_opt(val: *mut FfiBlockOpt) {
if val.is_null() {
log::error!("Trying to free a null pointer. Exiting");
return;
}
// Reclaim the outer box, then the inner block box (if any).
let opt = unsafe { Box::from_raw(val) };
if opt.is_some {
let block = unsafe { Box::from_raw(opt.value) };
unsafe {
free_ffi_transaction_vec(ffi_tx_ffi_vec);
};
free_ffi_block(*block);
}
}
}
/// Frees the resources associated with the given ffi block vector.
///
/// Takes ownership of the whole allocation produced by a `query_*` call: the
/// outer `Box<FfiVec<FfiBlock>>` (the `PointerResult.value` pointer), the
/// vector's backing buffer, and every block within it.
///
/// # Arguments
///
/// - `val`: An instance of `FfiVec<FfiBlock>`.
/// - `val`: The `*mut FfiVec<FfiBlock>` returned in `PointerResult.value`.
///
/// # Returns
///
@ -178,10 +181,16 @@ pub unsafe extern "C" fn free_ffi_block_opt(val: FfiBlockOpt) {
/// # Safety
///
/// The caller must ensure that:
/// - `val` is a valid instance of `FfiVec<FfiBlock>`.
/// - `val` is a pointer to an `FfiVec<FfiBlock>` produced by this library and not yet freed.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn free_ffi_block_vec(val: FfiVec<FfiBlock>) {
let ffi_block_std_vec: Vec<_> = val.into();
pub unsafe extern "C" fn free_ffi_block_vec(val: *mut FfiVec<FfiBlock>) {
if val.is_null() {
log::error!("Trying to free a null pointer. Exiting");
return;
}
// Reclaim the outer box, then the backing buffer and each block.
let boxed = unsafe { Box::from_raw(val) };
let ffi_block_std_vec: Vec<_> = (*boxed).into();
for block in ffi_block_std_vec {
unsafe {
free_ffi_block(block);

View File

@ -463,9 +463,13 @@ pub unsafe extern "C" fn free_ffi_transaction(val: FfiTransaction) {
/// Frees the resources associated with the given ffi transaction option.
///
/// Takes ownership of the whole allocation produced by a `query_*` call: the
/// outer `Box<FfiOption<FfiTransaction>>` (the `PointerResult.value` pointer),
/// the inner `Box<FfiTransaction>` (when present), and its body.
///
/// # Arguments
///
/// - `val`: An instance of `FfiOption<FfiTransaction>`.
/// - `val`: The `*mut FfiOption<FfiTransaction>` returned in `PointerResult.value`.
///
/// # Returns
///
@ -474,48 +478,32 @@ pub unsafe extern "C" fn free_ffi_transaction(val: FfiTransaction) {
/// # Safety
///
/// The caller must ensure that:
/// - `val` is a valid instance of `FfiOption<FfiTransaction>`.
/// - `val` is a pointer to an `FfiOption<FfiTransaction>` produced by this library and not yet
/// freed.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn free_ffi_transaction_opt(val: FfiOption<FfiTransaction>) {
if val.is_some {
let value = unsafe { Box::from_raw(val.value) };
match value.kind {
FfiTransactionKind::Public => {
let body = unsafe { Box::from_raw(value.body.public_body) };
let std_body: PublicTransaction = body.into();
drop(std_body);
}
FfiTransactionKind::Private => {
let body = unsafe { Box::from_raw(value.body.private_body) };
let std_body: PrivacyPreservingTransaction = body.into();
drop(std_body);
}
FfiTransactionKind::ProgramDeploy => {
let body = unsafe { Box::from_raw(value.body.program_deployment_body) };
let std_body: ProgramDeploymentTransaction = body.into();
drop(std_body);
}
pub unsafe extern "C" fn free_ffi_transaction_opt(val: *mut FfiOption<FfiTransaction>) {
if val.is_null() {
log::error!("Trying to free a null pointer. Exiting");
return;
}
// Reclaim the outer box, then the inner transaction box (if any).
let opt = unsafe { Box::from_raw(val) };
if opt.is_some {
let tx = unsafe { Box::from_raw(opt.value) };
unsafe {
free_ffi_transaction(*tx);
}
}
}
/// Frees the resources associated with the given vector of ffi transactions.
/// Frees the resources owned by an `FfiVec<FfiTransaction>` value (the backing
/// buffer and each transaction), without owning an outer box.
///
/// # Arguments
///
/// - `val`: An instance of `FfiVec<FfiTransaction>`.
///
/// # Returns
///
/// void.
///
/// # Safety
///
/// The caller must ensure that:
/// - `val` is a valid instance of `FfiVec<FfiTransaction>`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn free_ffi_transaction_vec(val: FfiVec<FfiTransaction>) {
/// This is the element-level helper shared by the block free path
/// ([`crate::api::types::block::free_ffi_block`], whose body is a transaction
/// vector held by value) and the public [`free_ffi_transaction_vec`] entry
/// point (which first reclaims the outer box).
pub(crate) fn free_transaction_vec_value(val: FfiVec<FfiTransaction>) {
let ffi_tx_std_vec: Vec<_> = val.into();
for tx in ffi_tx_std_vec {
unsafe {
@ -524,6 +512,35 @@ pub unsafe extern "C" fn free_ffi_transaction_vec(val: FfiVec<FfiTransaction>) {
}
}
/// Frees the resources associated with the given vector of ffi transactions.
///
/// Takes ownership of the whole allocation produced by a `query_*` call: the
/// outer `Box<FfiVec<FfiTransaction>>` (the `PointerResult.value` pointer), the
/// vector's backing buffer, and every transaction within it.
///
/// # Arguments
///
/// - `val`: The `*mut FfiVec<FfiTransaction>` returned in `PointerResult.value`.
///
/// # Returns
///
/// void.
///
/// # Safety
///
/// The caller must ensure that:
/// - `val` is a pointer to an `FfiVec<FfiTransaction>` produced by this library and not yet freed.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn free_ffi_transaction_vec(val: *mut FfiVec<FfiTransaction>) {
if val.is_null() {
log::error!("Trying to free a null pointer. Exiting");
return;
}
// Reclaim the outer box, then the backing buffer and each transaction.
let boxed = unsafe { Box::from_raw(val) };
free_transaction_vec_value(*boxed);
}
fn cast_validity_window(window: ValidityWindow) -> [u64; 2] {
[
window.0.0.unwrap_or_default(),

View File

@ -1,33 +0,0 @@
use std::{ops::Deref, sync::Arc};
use anyhow::{Context as _, Result};
use log::info;
pub use url::Url;
pub trait IndexerClientTrait: Clone {
async fn new(indexer_url: &Url) -> Result<Self>;
}
#[derive(Clone)]
pub struct IndexerClient(Arc<jsonrpsee::ws_client::WsClient>);
impl IndexerClientTrait for IndexerClient {
async fn new(indexer_url: &Url) -> Result<Self> {
info!("Connecting to Indexer at {indexer_url}");
let client = jsonrpsee::ws_client::WsClientBuilder::default()
.build(indexer_url)
.await
.context("Failed to create websocket client")?;
Ok(Self(Arc::new(client)))
}
}
impl Deref for IndexerClient {
type Target = jsonrpsee::ws_client::WsClient;
fn deref(&self) -> &Self::Target {
&self.0
}
}

View File

@ -1,95 +1,75 @@
use std::{ffi::c_void, net::SocketAddr};
use std::ffi::c_void;
use indexer_service::IndexerHandle;
use crate::client::IndexerClient;
use indexer_core::IndexerCore;
use tokio::{runtime::Handle, task::JoinHandle};
/// FFI-owned indexer.
///
/// Has three fields behind `c_void` (so that cbindgen never needs to see their Rust layout):
/// - An [`IndexerCore`] used to answer queries
/// - The background task [`JoinHandle`] that drives ingestion (consuming the block stream so the
/// store stays populated)
/// - A [`Handle`] to the runtime they live on.
#[repr(C)]
pub struct IndexerServiceFFI {
indexer_handle: *mut c_void,
indexer_client: *mut c_void,
core: *mut c_void,
ingest_handle: *mut c_void,
runtime_handle: *mut c_void,
}
impl IndexerServiceFFI {
#[must_use]
pub fn new(
indexer_handle: indexer_service::IndexerHandle,
indexer_client: IndexerClient,
) -> Self {
pub fn new(core: IndexerCore, ingest_handle: JoinHandle<()>, runtime_handle: Handle) -> Self {
Self {
// Box the complex types and convert to opaque pointers
indexer_handle: Box::into_raw(Box::new(indexer_handle)).cast::<c_void>(),
indexer_client: Box::into_raw(Box::new(indexer_client)).cast::<c_void>(),
core: Box::into_raw(Box::new(core)).cast::<c_void>(),
ingest_handle: Box::into_raw(Box::new(ingest_handle)).cast::<c_void>(),
runtime_handle: Box::into_raw(Box::new(runtime_handle)).cast::<c_void>(),
}
}
/// Helper to take ownership back.
/// Borrow the [`IndexerCore`] to run a query against its store.
#[must_use]
pub fn into_parts(mut self) -> (Box<IndexerHandle>, Box<IndexerClient>) {
let Self {
indexer_handle,
indexer_client,
} = &mut self;
let indexer_handle_boxed = unsafe { Box::from_raw(indexer_handle.cast::<IndexerHandle>()) };
let indexer_client_boxed = unsafe { Box::from_raw(indexer_client.cast::<IndexerClient>()) };
// Assigning nulls to prevent double free on drop, since ownership is transferred to caller
*indexer_handle = std::ptr::null_mut();
*indexer_client = std::ptr::null_mut();
(indexer_handle_boxed, indexer_client_boxed)
}
/// Helper to get indexer handle addr.
#[must_use]
pub const fn addr(&self) -> SocketAddr {
let indexer_handle = unsafe {
self.indexer_handle
.cast::<IndexerHandle>()
.as_ref()
.expect("Indexer Handle must be non-null pointer")
};
indexer_handle.addr()
}
/// Helper to get indexer handle ref.
#[must_use]
pub const fn handle(&self) -> &IndexerHandle {
pub const fn core(&self) -> &IndexerCore {
unsafe {
self.indexer_handle
.cast::<IndexerHandle>()
self.core
.cast::<IndexerCore>()
.as_ref()
.expect("Indexer Handle must be non-null pointer")
.expect("IndexerCore must be a non-null pointer")
}
}
/// Helper to get indexer client ref.
/// Borrow the runtime handle to `block_on` an async store query.
#[must_use]
pub const fn client(&self) -> &IndexerClient {
pub const fn runtime_handle(&self) -> &Handle {
unsafe {
self.indexer_client
.cast::<IndexerClient>()
self.runtime_handle
.cast::<Handle>()
.as_ref()
.expect("Indexer Client must be non-null pointer")
.expect("Runtime handle must be a non-null pointer")
}
}
}
// Implement Drop to prevent memory leaks
// Implement Drop to stop ingestion and free the boxed resources.
impl Drop for IndexerServiceFFI {
fn drop(&mut self) {
let Self {
indexer_handle,
indexer_client,
core,
ingest_handle,
runtime_handle,
} = self;
if !indexer_handle.is_null() {
drop(unsafe { Box::from_raw(indexer_handle.cast::<IndexerHandle>()) });
if !ingest_handle.is_null() {
// Stop the background ingestion task before tearing down the core.
let handle = unsafe { Box::from_raw(ingest_handle.cast::<JoinHandle<()>>()) };
handle.abort();
drop(handle);
}
if !indexer_client.is_null() {
drop(unsafe { Box::from_raw(indexer_client.cast::<IndexerClient>()) });
if !core.is_null() {
drop(unsafe { Box::from_raw(core.cast::<IndexerCore>()) });
}
if !runtime_handle.is_null() {
drop(unsafe { Box::from_raw(runtime_handle.cast::<Handle>()) });
}
}
}

View File

@ -5,7 +5,6 @@ pub use indexer::IndexerServiceFFI;
pub use runtime::Runtime;
pub mod api;
mod client;
mod errors;
mod indexer;
mod runtime;