refactor!: adopt reworked indexer FFI

This commit is contained in:
erhant 2026-06-19 15:34:42 +03:00
parent eda2bee08a
commit 8ff0168825
6 changed files with 6714 additions and 1277 deletions

View File

@ -30,18 +30,21 @@ This will reduce friction when working on the project.
The module does **not** start the indexer on load — something must invoke its `start_indexer` method (via the module-viewer's invoke panel, or another module / basecamp over the Logos protocol):
```c
start_indexer(config_path, port)
start_indexer(config_path)
```
- `config_path`**absolute** path to a JSON config (see
[`config/indexer_config.json`](config/indexer_config.json)). It must be absolute: the module runs inside the `logos_host` subprocess, whose working directory is not your shell's.
- `port`**legacy** parameter, still required by the pinned `indexer_ffi`. Logos-protocol consumers do not use it; pass any value as a **string** (e.g. `"8779"`). It is slated for removal once the FFI drops it.
On success it returns `0`; a non-zero return is the FFI `OperationStatus` (e.g. `2 = InitializationError`). Once started, consume the indexer through the query methods below.
> [!CAUTION]
> [!TIP]
>
> The FFI does not log, so a non-zero `OperationStatus` code is all you get on failure.
> By default the indexer is silent. Call `init_logger(level)` once (e.g.
> `init_logger("info")`; accepts `off`/`error`/`warn`/`info`/`debug`/`trace`) to
> surface the indexer's own `log` output — useful since a failed call otherwise
> only reports a numeric `OperationStatus`. Logging is scoped to the indexer
> crates, and the first call wins.
### Query methods (the Logos-protocol API)

7892
flake.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@
# The LEZ indexer Rust FFI lib + header come from this flake's `indexer`
# package output (/lib/libindexer_ffi.* + /include/indexer_ffi.h). It is a
# prebuilt Nix derivation, so mkExternalLib consumes it directly (no build).
logos-execution-zone.url = "github:logos-blockchain/logos-execution-zone?ref=main";
logos-execution-zone.url = "git+https://github.com/logos-blockchain/logos-execution-zone?ref=erhant/fix-indexer-ffi";
};
outputs =

View File

@ -1,10 +1,10 @@
#pragma once
// The generated indexer_ffi.h carries no include guard, so including it from
// more than one header in the same translation unit redefines every type.
// Wrap it once here (behind #pragma once) and include THIS header instead of
// <indexer_ffi.h> directly, so the FFI types can be shared by both
// lez_indexer_module.h and lez_ffi_marshalling.h.
// Wrap the generated indexer_ffi.h in `extern "C"` (it is a C header) behind a
// single include point, so both lez_indexer_module_impl.cpp and
// lez_ffi_marshalling share the FFI types with C linkage. indexer_ffi.h now
// carries its own `#pragma once`, so this is purely about linkage + one include
// site (include THIS header, not <indexer_ffi.h> directly).
#ifdef __cplusplus
extern "C" {
#endif

View File

@ -32,17 +32,10 @@ LezIndexerModuleImpl::~LezIndexerModuleImpl() {
// === Indexer Lifecycle ===
int64_t LezIndexerModuleImpl::start_indexer(const std::string& config_path, const std::string& port) {
int64_t LezIndexerModuleImpl::start_indexer(const std::string& config_path) {
if (!indexer_service_ffi) {
char* end = nullptr;
const unsigned long parsed = std::strtoul(port.c_str(), &end, 10);
if (end == port.c_str() || *end != '\0' || parsed > 0xFFFF) {
warn("start_indexer", "invalid port");
return -1;
}
const uint16_t port_num = static_cast<uint16_t>(parsed);
InitializedIndexerServiceFFIResult res = ::start_indexer(config_path.c_str(), port_num);
// Null runtime: the FFI creates and owns its own tokio runtime.
InitializedIndexerServiceFFIResult res = ::start_indexer(nullptr, config_path.c_str());
if (is_error(&res.error)) {
warn("start_indexer", "indexer FFI error");
return static_cast<int64_t>(res.error);
@ -54,19 +47,16 @@ int64_t LezIndexerModuleImpl::start_indexer(const std::string& config_path, cons
return 0;
}
void LezIndexerModuleImpl::init_logger(const std::string& level) {
::init_logger(level.c_str());
}
// === Indexer Queries ===
//
// Each method calls the matching query_* FFI function on the handle we already
// hold (no Runtime needed — the pinned FFI carries its own runtime inside the
// handle), marshals the returned C struct into compact JSON, then frees the
// FFI's deep allocations with the matching free_ffi_* function.
//
// Known leak: query_* returns its payload in a `Box::into_raw` *outer* box
// (PointerResult.value), and the free_ffi_* functions free only the inner
// boxes/vectors (they take the struct by value). The pinned FFI provides no
// free for the outer box, and libc free() would be UB if Rust's global
// allocator differs from the system one, so we deliberately leak the small
// (~8-32 byte) outer wrapper per query. Tracked for an upstream fix.
// Each method calls the matching query_* FFI function on the handle we hold,
// marshals the returned C struct into compact JSON, then frees the FFI
// allocation with the matching free_ffi_* function. The frees take the outer
// pointer (PointerResult.value) and reclaim the whole allocation.
std::string LezIndexerModuleImpl::getAccount(const std::string& account_id) {
if (!indexer_service_ffi) {
@ -87,7 +77,7 @@ std::string LezIndexerModuleImpl::getAccount(const std::string& account_id) {
}
const std::string out = jsonToCompactString(ffiAccountToJson(*res.value));
::free_ffi_account(*res.value);
::free_ffi_account(res.value);
return out;
}
@ -114,7 +104,7 @@ std::string LezIndexerModuleImpl::getBlockById(const std::string& block_id) {
if (res.value->is_some && res.value->value) {
out = jsonToCompactString(ffiBlockToJson(*res.value->value));
}
::free_ffi_block_opt(*res.value);
::free_ffi_block_opt(res.value);
return out;
}
@ -140,7 +130,7 @@ std::string LezIndexerModuleImpl::getBlockByHash(const std::string& hash) {
if (res.value->is_some && res.value->value) {
out = jsonToCompactString(ffiBlockToJson(*res.value->value));
}
::free_ffi_block_opt(*res.value);
::free_ffi_block_opt(res.value);
return out;
}
@ -167,7 +157,7 @@ std::string LezIndexerModuleImpl::getTransaction(const std::string& hash) {
if (res.value->is_some && res.value->value) {
out = jsonToCompactString(ffiTransactionToJson(*res.value->value));
}
::free_ffi_transaction_opt(*res.value);
::free_ffi_transaction_opt(res.value);
return out;
}
@ -213,7 +203,7 @@ std::string LezIndexerModuleImpl::getBlocks(const std::string& before, const std
for (uintptr_t i = 0; i < res.value->len; ++i) {
arr.push_back(ffiBlockToJson(res.value->entries[i]));
}
::free_ffi_block_vec(*res.value);
::free_ffi_block_vec(res.value);
return jsonToCompactString(arr);
}
@ -223,15 +213,14 @@ std::string LezIndexerModuleImpl::getLastFinalizedBlockId() {
return {};
}
PointerResult_u64__OperationStatus res = ::query_last_block(handle(indexer_service_ffi));
if (is_error(&res.error) || !res.value) {
warn("getLastFinalizedBlockId", "indexer FFI error");
LastBlockIdResult res = ::query_last_block(handle(indexer_service_ffi));
if (is_error(&res.error) || !res.is_some) {
warn("getLastFinalizedBlockId", "indexer FFI error or no finalized block yet");
return {};
}
// Bare decimal string; the FFI provides no free for the boxed u64 (leaked,
// see the note above).
return u64ToString(*res.value);
// Bare decimal string; the id is returned inline, nothing to free.
return u64ToString(res.block_id);
}
std::string LezIndexerModuleImpl::getTransactionsByAccount(
@ -270,6 +259,6 @@ std::string LezIndexerModuleImpl::getTransactionsByAccount(
for (uintptr_t i = 0; i < res.value->len; ++i) {
arr.push_back(ffiTransactionToJson(res.value->entries[i]));
}
::free_ffi_transaction_vec(*res.value);
::free_ffi_transaction_vec(res.value);
return jsonToCompactString(arr);
}

View File

@ -27,12 +27,12 @@ public:
~LezIndexerModuleImpl();
/// Boot ingestion against the indexer config at `config_path` (must be an
/// ABSOLUTE path — the module runs in a logos_host subprocess), listening on
/// `port`. Idempotent: a second call while already running is a no-op.
/// Returns 0 on success, else the FFI OperationStatus code (-1 on bad port).
/// ABSOLUTE path — the module runs in a logos_host subprocess). Idempotent:
/// a second call while already running is a no-op. Returns 0 on success,
/// else the FFI OperationStatus code.
/// int64_t (not int): the universal codegen marshals int64_t/std::string/bool
/// as scalar wire types; a plain `int` return is treated as a JSON payload.
int64_t start_indexer(const std::string& config_path, const std::string& port);
int64_t start_indexer(const std::string& config_path);
/// Account by 32-byte hex id. The returned JSON omits the id; callers inject
/// the queried id themselves.
@ -60,15 +60,10 @@ public:
std::string getTransactionsByAccount(const std::string& account_id, const std::string& offset, const std::string& limit);
// clang-format on
// Indexer Logging (opt-in)
//
// Installs the indexer FFI's logger (env_logger) so the indexer's Rust `log`
// output surfaces in the host process. Commented out because the underlying
// `::init_logger()` export does not yet exist in the pinned
// logos-execution-zone FFI. Uncomment once the export lands upstream and the
// pin is bumped.
//
// void init_logger();
/// Enable the indexer's logging at `level` (off/error/warn/info/debug/trace;
/// null or unparseable falls back to info). Scoped to the indexer crates
/// only; the first call wins (subsequent calls are no-ops).
void init_logger(const std::string& level);
private:
// IndexerServiceFFI* — opaque here (see class comment); cast in the .cpp.