mirror of
https://github.com/luxfi/explorer.git
synced 2026-09-24 00:25:12 +00:00
feat(ffi): run Blockscout-rs explorer services in-process via cgo (single binary)
Make the explorer ONE binary that runs the Rust Blockscout-rs services in-process via FFI — not by rewriting them in Go and not by exec'ing subprocesses. The real Rust service entrypoints run on their own Tokio runtime threads inside the Go binary. - ffi/ — one C-ABI staticlib (crate-type=["staticlib"]) wrapping the service crates by path (../../explorer-rs/<svc>). One staticlib = one Rust std/panic runtime, no duplicate-symbol conflicts. A `service!` macro is the single place that bridges a service: parse Settings JSON, spawn a dedicated OS thread with a multi-thread Tokio runtime, run the service's real entrypoint. Exports lux_explorer_start_<svc>() + lux_explorer_start_all(). Disabled services still export their symbol and return LUX_FFI_ERR_DISABLED so the Go side links identically. sig-provider is wired and proven first (stateless, fastest to build); the other four services' deps/features/service! blocks are written out commented with confirmed entrypoint signatures for mechanical scale-out. - ffi_on.go (//go:build ffi) — cgo preamble (-llux_explorer_ffi + the darwin/linux system libs the Rust graph needs) + extern decls; startFFIServices() launches the services at startup from main(). - ffi_off.go (//go:build !ffi) — no-op fallback so plain `go build ./...` compiles WITHOUT the staticlib present. - Makefile `make single` = cargo build --release in ffi/ then go build -tags ffi. ffi/README.md documents the C ABI, the protoc / protoc-gen-openapiv2 build-tool prereqs, the seeded Cargo.lock, and the one-service-at-a-time scale-out recipe. Verified: default `go build ./...` exits 0; ffi/ staticlib compiles with sig-provider linked (.a 7.9MB shim -> 58MB with service); cgo -tags ffi links it into the binary (symbols present via nm); the single binary boots and logs `[ffi] start sig-provider -> ok (0)` with the service running in-process. go vet clean under both build tags.
This commit is contained in:
@@ -35,3 +35,6 @@ QWEN.md
|
||||
# Runtime
|
||||
logs/
|
||||
.claude/
|
||||
|
||||
# Rust FFI staticlib build artifacts (the .a is produced by make single)
|
||||
ffi/target/
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# Lux explorer build.
|
||||
#
|
||||
# Two binaries from one source tree, selected by the `ffi` build tag:
|
||||
#
|
||||
# make build Default Go-native explorer (indexer + graph + SPA + realtime).
|
||||
# No Rust, no cgo. This is what `go build ./...` produces.
|
||||
#
|
||||
# make single THE single all-in-one binary: builds the Rust FFI staticlib
|
||||
# (ffi/ -> liblux_explorer_ffi.a) then links it into the Go
|
||||
# binary with cgo (-tags ffi), so the Blockscout-rs services
|
||||
# (sig-provider, …) run IN-PROCESS. One binary runs everything.
|
||||
#
|
||||
# Scale services out by enabling more cargo features (see ffi/README.md):
|
||||
# make single FFI_FEATURES=sig-provider,stats,visualizer
|
||||
|
||||
BIN ?= explorer
|
||||
FFI_DIR := ffi
|
||||
FFI_LIB := $(FFI_DIR)/target/release/liblux_explorer_ffi.a
|
||||
# Cargo features = which Rust services get compiled into the staticlib.
|
||||
# `all` = every wired service. Override on the CLI to scope the build.
|
||||
FFI_FEATURES ?= all
|
||||
|
||||
.PHONY: build single ffi-lib clean test fmt
|
||||
|
||||
build:
|
||||
go build -o $(BIN) .
|
||||
|
||||
# Step 1: build the Rust staticlib with the requested services.
|
||||
ffi-lib:
|
||||
cd $(FFI_DIR) && cargo build --release --features $(FFI_FEATURES)
|
||||
|
||||
# Step 2: link it into the Go binary via cgo. The -tags ffi build compiles
|
||||
# ffi_on.go (cgo preamble + extern decls) instead of the ffi_off.go stub.
|
||||
single: ffi-lib
|
||||
go build -tags ffi -o $(BIN) .
|
||||
@echo "built single all-in-one binary: ./$(BIN) (features: $(FFI_FEATURES))"
|
||||
|
||||
test:
|
||||
go test ./...
|
||||
|
||||
fmt:
|
||||
go fmt ./...
|
||||
cd $(FFI_DIR) && cargo fmt
|
||||
|
||||
clean:
|
||||
rm -f $(BIN)
|
||||
cd $(FFI_DIR) && cargo clean
|
||||
Generated
+4268
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,70 @@
|
||||
[package]
|
||||
name = "lux_explorer_ffi"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
description = "C-ABI staticlib that runs the Blockscout-rs explorer services in-process inside the Lux explorer Go binary (cgo). One Rust std copy wraps every needed service so the Go binary links a single .a."
|
||||
|
||||
# staticlib => a single self-contained `liblux_explorer_ffi.a` that cgo links
|
||||
# directly into the explorer Go binary. We deliberately use ONE staticlib for
|
||||
# ALL wrapped services (not one .a per service) so there is exactly one copy of
|
||||
# the Rust std / panic runtime and no duplicate-symbol conflicts at link time.
|
||||
[lib]
|
||||
name = "lux_explorer_ffi"
|
||||
crate-type = ["staticlib"]
|
||||
|
||||
[dependencies]
|
||||
# Shared runtime glue. Tokio is built per-service-thread (see src/lib.rs); these
|
||||
# are the only crates the FFI shim itself needs. anyhow is the common error type
|
||||
# every wrapped service entrypoint returns; tokio drives the per-service runtime.
|
||||
libc = "0.2"
|
||||
serde_json = "1"
|
||||
once_cell = "1"
|
||||
anyhow = "1"
|
||||
tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||
|
||||
# --- Wrapped services (path deps into the sibling explorer-rs checkout) -------
|
||||
# explorer-rs lives at ../../explorer-rs relative to this crate
|
||||
# (/Users/a/work/lux/explorer/ffi -> /Users/a/work/lux/luxfi/explorer-rs).
|
||||
# Each service is its OWN cargo workspace with no root workspace, so a plain
|
||||
# path dep on the `*-server` crate pulls exactly that service's dependency
|
||||
# graph. Each is feature-gated so a default `cargo build` (no --features) builds
|
||||
# only the FFI shim + stubs and stays fast; turn services on individually.
|
||||
#
|
||||
# sig-provider is wired and proven first: it is stateless (no DB, no
|
||||
# migrations), so it links and runs with the least build cost. Adding another
|
||||
# service is a two-line change: (1) add its path dep below under a matching
|
||||
# feature, (2) add a `service!{...}` entry in src/lib.rs. See README.md.
|
||||
sig-provider-server = { path = "../../explorer-rs/sig-provider/sig-provider-server", optional = true }
|
||||
|
||||
# The remaining services are listed (commented) with their exact paths so
|
||||
# scaling out is mechanical. They are NOT enabled by default because each pins a
|
||||
# DIFFERENT major of blockscout-service-launcher (0.13/0.17/0.19/0.21) and pulls
|
||||
# DB/migration stacks (sea-orm) — combining all of them inflates build time and
|
||||
# must be done one at a time, verifying the link after each. See README.md
|
||||
# "Scaling out" for the exact recipe.
|
||||
#
|
||||
# smart-contract-verifier-server = { path = "../../explorer-rs/smart-contract-verifier/smart-contract-verifier-server", optional = true }
|
||||
# stats-server = { path = "../../explorer-rs/stats/stats-server", optional = true }
|
||||
# multichain-aggregator-server = { path = "../../explorer-rs/multichain-aggregator/multichain-aggregator-server", optional = true }
|
||||
# visualizer-server = { path = "../../explorer-rs/visualizer/visualizer-server", optional = true }
|
||||
|
||||
[features]
|
||||
# Default = no service compiled in: the staticlib still builds (fast) and every
|
||||
# entrypoint returns LUX_FFI_ERR_DISABLED so the Go side links and degrades
|
||||
# gracefully. `make single` turns on the services it wants.
|
||||
default = []
|
||||
sig-provider = ["dep:sig-provider-server"]
|
||||
# smart-contract-verifier = ["dep:smart-contract-verifier-server"]
|
||||
# stats = ["dep:stats-server"]
|
||||
# multichain-aggregator = ["dep:multichain-aggregator-server"]
|
||||
# visualizer = ["dep:visualizer-server"]
|
||||
|
||||
# Convenience: every wired service at once.
|
||||
all = ["sig-provider"]
|
||||
|
||||
[profile.release]
|
||||
# Smaller, faster-linking staticlib. LTO off keeps incremental builds sane while
|
||||
# scaling services out; flip to "thin" for a production single binary.
|
||||
lto = false
|
||||
codegen-units = 16
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
# lux_explorer_ffi — Blockscout-rs services, in-process, one binary
|
||||
|
||||
This crate makes the Lux explorer **one single binary** that runs the
|
||||
[Blockscout-rs](../../explorer-rs) explorer services (sig-provider, and — as
|
||||
scaled out — smart-contract-verifier, stats, multichain-aggregator, visualizer)
|
||||
**in-process via cgo FFI**. No subprocesses. No Go rewrites. Each service's real
|
||||
Rust entrypoint runs on its own Tokio runtime thread inside the explorer binary.
|
||||
|
||||
```
|
||||
┌──────────────────────────── explorer (single Go binary) ────────────────────────────┐
|
||||
│ main.go ─ startFFIServices() (ffi_on.go, //go:build ffi) │
|
||||
│ │ cgo → liblux_explorer_ffi.a (this crate, crate-type = ["staticlib"]) │
|
||||
│ │ │ lux_explorer_start_sig_provider(config_json) ─┐ │
|
||||
│ │ │ lux_explorer_start_all() │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ Go-native indexer/graph/SPA/realtime Rust sig-provider on its own Tokio thread │
|
||||
└───────────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Why one staticlib for all services
|
||||
|
||||
A single `staticlib` wraps every service, so the final binary links **one** copy
|
||||
of the Rust std / panic runtime. Linking several separate `.a`s would risk
|
||||
duplicate-symbol conflicts. Adding a service is a path dep + a feature + one
|
||||
`service!{…}` line — nothing else changes.
|
||||
|
||||
## C ABI
|
||||
|
||||
Exported from `src/lib.rs` (see the `service!` macro):
|
||||
|
||||
```c
|
||||
// Spawn <svc> on its own Tokio runtime thread; returns 0 (LUX_FFI_OK) once
|
||||
// launched. config_json is the service's Settings as JSON; "" / NULL => the
|
||||
// service's upstream defaults.
|
||||
int lux_explorer_start_sig_provider(const char* config_json);
|
||||
// …one per wired service…
|
||||
|
||||
// Start every service compiled into this staticlib with default config.
|
||||
int lux_explorer_start_all(void);
|
||||
```
|
||||
|
||||
Return codes: `0` ok · `1` bad-config · `2` parse-error · `3` disabled
|
||||
(service's cargo feature was off) · `4` spawn-error. Disabled services still
|
||||
export their symbol and return `3`, so the Go binary links identically no matter
|
||||
which features were compiled in.
|
||||
|
||||
## Build
|
||||
|
||||
From the explorer repo root:
|
||||
|
||||
```sh
|
||||
make single # all wired services
|
||||
make single FFI_FEATURES=sig-provider # scope to specific services
|
||||
```
|
||||
|
||||
`make single` runs `cargo build --release --features <…>` here, then
|
||||
`go build -tags ffi`. The default `make build` / `go build ./...` compiles the
|
||||
`ffi_off.go` stub and needs **none** of this.
|
||||
|
||||
### Build-tool prerequisites (Rust path only)
|
||||
|
||||
The Blockscout-rs `*-proto` crates generate gRPC + Swagger at build time, so the
|
||||
host needs:
|
||||
|
||||
- **protoc** — `brew install protobuf`
|
||||
- **protoc-gen-openapiv2** — `go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest`
|
||||
(must be on `PATH`; it lands in `$(go env GOPATH)/bin`)
|
||||
|
||||
These are standard upstream Blockscout-rs build deps (the prost-build panic
|
||||
message itself points at `brew install protobuf`), not added by this crate.
|
||||
|
||||
### Cargo.lock is seeded
|
||||
|
||||
`ffi/Cargo.lock` is seeded from `explorer-rs/sig-provider/Cargo.lock` so the
|
||||
build resolves the **same** dependency versions the service is known to build
|
||||
with. Without this, re-resolving the graph as a path dep pulled in
|
||||
`prost-build 0.14` against the service's `0.11` build script and failed. When
|
||||
adding a service whose lock conflicts, reconcile the lock the same way (start
|
||||
from that service's `Cargo.lock`, let cargo extend it).
|
||||
|
||||
## Scaling out (one service at a time)
|
||||
|
||||
Each service pins a **different** major of `blockscout-service-launcher`
|
||||
(sig-provider 0.10 · visualizer 0.13 · verifier 0.17 · stats 0.19 ·
|
||||
multichain-aggregator 0.21) and the DB-backed ones pull `sea-orm` + migrations.
|
||||
Cargo can carry multiple majors, but the build surface grows, so enable and
|
||||
**verify the link after each**:
|
||||
|
||||
1. **Cargo.toml** — uncomment the service's `*-server` path dep and its
|
||||
`[features]` entry (both are already written out, commented).
|
||||
2. **src/lib.rs** — uncomment its `service!{…}` block (the entrypoint signatures
|
||||
are already filled in and confirmed against explorer-rs) and the matching
|
||||
line in `lux_explorer_start_all`.
|
||||
3. **ffi_on.go** — add a `start("<svc>", …)` line.
|
||||
4. `make single FFI_FEATURES=<svc>` and confirm the `.a` links into the Go
|
||||
binary (`nm explorer | grep lux_explorer_start_<svc>`).
|
||||
|
||||
Confirmed upstream entrypoints (all take a `serde::Deserialize` `Settings`):
|
||||
|
||||
| service | crate | entrypoint |
|
||||
|-------------------------|------------------------------------|-----------------------------|
|
||||
| sig-provider | `sig_provider_server` | `sig_provider(Settings)` |
|
||||
| smart-contract-verifier | `smart_contract_verifier_server` | `run(Settings)` |
|
||||
| stats | `stats_server` | `stats(Settings, None)` |
|
||||
| multichain-aggregator | `multichain_aggregator_server` | `run(Settings)` |
|
||||
| visualizer | `visualizer_server` | `run(Settings)` |
|
||||
|
||||
DB-backed services (stats, multichain-aggregator) also need their database URL
|
||||
in the config JSON and migrations run — pass that through `startFFIServices`'s
|
||||
per-service config map.
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
//! C-ABI staticlib that runs the Blockscout-rs explorer services in-process
|
||||
//! inside the Lux explorer Go binary via cgo.
|
||||
//!
|
||||
//! Design (one way, decomplected):
|
||||
//! * Each wrapped service exposes the SAME shape upstream: a `Settings` type
|
||||
//! that is `serde::Deserialize` with every field `#[serde(default)]`, plus
|
||||
//! an async entrypoint `fn(Settings) -> Result<(), anyhow::Error>` that
|
||||
//! builds its own server and blocks. We do NOT reimplement any service —
|
||||
//! we call its real entrypoint.
|
||||
//! * The FFI boundary is uniform: `lux_explorer_start_<svc>(config_json)`
|
||||
//! parses the JSON into that service's `Settings` (empty `{}` => upstream
|
||||
//! defaults), spawns a dedicated OS thread with a multi-thread Tokio
|
||||
//! runtime, and runs the service to completion on it. The call returns
|
||||
//! immediately with 0 once the worker thread is launched; the service keeps
|
||||
//! running for the life of the process (matching how a subprocess would).
|
||||
//! * ONE staticlib wraps every service => one Rust std / panic runtime, no
|
||||
//! duplicate-symbol conflicts from linking multiple separate `.a`s.
|
||||
//!
|
||||
//! The `service!` macro is the single place that knows how to bridge a service;
|
||||
//! adding a service is one macro invocation (+ its path dep + feature in
|
||||
//! Cargo.toml). Disabled services still export their symbol and return
|
||||
//! `LUX_FFI_ERR_DISABLED`, so the Go side links and runs regardless of which
|
||||
//! features were compiled in.
|
||||
|
||||
use std::ffi::CStr;
|
||||
use std::os::raw::c_char;
|
||||
use std::thread;
|
||||
|
||||
/// Worker thread launched successfully; service is running in-process.
|
||||
pub const LUX_FFI_OK: i32 = 0;
|
||||
/// `config_json` pointer was null or not valid UTF-8.
|
||||
pub const LUX_FFI_ERR_BAD_CONFIG: i32 = 1;
|
||||
/// `config_json` was non-empty but failed to deserialize into the service's
|
||||
/// `Settings`.
|
||||
pub const LUX_FFI_ERR_PARSE: i32 = 2;
|
||||
/// This service was not compiled into the staticlib (its cargo feature was off).
|
||||
pub const LUX_FFI_ERR_DISABLED: i32 = 3;
|
||||
/// Failed to spawn the worker OS thread.
|
||||
pub const LUX_FFI_ERR_SPAWN: i32 = 4;
|
||||
|
||||
/// Parse a C string config pointer into an owned JSON string.
|
||||
/// Empty / null => "{}" so the service falls back to its own defaults.
|
||||
fn config_str(ptr: *const c_char) -> Result<String, i32> {
|
||||
if ptr.is_null() {
|
||||
return Ok("{}".to_string());
|
||||
}
|
||||
// SAFETY: caller (cgo) guarantees a NUL-terminated C string or null.
|
||||
let cstr = unsafe { CStr::from_ptr(ptr) };
|
||||
match cstr.to_str() {
|
||||
Ok(s) if s.trim().is_empty() => Ok("{}".to_string()),
|
||||
Ok(s) => Ok(s.to_string()),
|
||||
Err(_) => Err(LUX_FFI_ERR_BAD_CONFIG),
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn a dedicated OS thread running a fresh multi-thread Tokio runtime that
|
||||
/// drives `fut_fn` (the service's blocking server future) to completion. The
|
||||
/// thread is named so it shows up in stacks / metrics.
|
||||
fn spawn_service<F, Fut>(name: &'static str, fut_fn: F) -> i32
|
||||
where
|
||||
F: FnOnce() -> Fut + Send + 'static,
|
||||
Fut: std::future::Future<Output = Result<(), anyhow::Error>>,
|
||||
{
|
||||
let builder = thread::Builder::new().name(format!("lux-svc-{name}"));
|
||||
let spawned = builder.spawn(move || {
|
||||
let rt = match tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.thread_name(format!("lux-{name}"))
|
||||
.build()
|
||||
{
|
||||
Ok(rt) => rt,
|
||||
Err(e) => {
|
||||
eprintln!("[ffi:{name}] failed to build tokio runtime: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(e) = rt.block_on(fut_fn()) {
|
||||
eprintln!("[ffi:{name}] service exited with error: {e:#}");
|
||||
} else {
|
||||
eprintln!("[ffi:{name}] service exited");
|
||||
}
|
||||
});
|
||||
match spawned {
|
||||
Ok(_handle) => LUX_FFI_OK,
|
||||
Err(e) => {
|
||||
eprintln!("[ffi:{name}] failed to spawn worker thread: {e}");
|
||||
LUX_FFI_ERR_SPAWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `service!` generates the uniform C-ABI entrypoint for one wrapped service.
|
||||
///
|
||||
/// service!(fn_name = lux_explorer_start_sig_provider,
|
||||
/// name = "sig-provider",
|
||||
/// feature = "sig-provider",
|
||||
/// settings = sig_provider_server::Settings,
|
||||
/// run = |s| sig_provider_server::sig_provider(s));
|
||||
///
|
||||
/// When `feature` is enabled the body parses JSON into `settings`, spawns the
|
||||
/// worker, and runs `run`. When disabled the same symbol is exported but
|
||||
/// returns `LUX_FFI_ERR_DISABLED`, so the Go binary links identically either
|
||||
/// way.
|
||||
macro_rules! service {
|
||||
(
|
||||
fn_name = $fn_name:ident,
|
||||
name = $name:literal,
|
||||
feature = $feature:literal,
|
||||
settings = $settings:path,
|
||||
run = $run:expr $(,)?
|
||||
) => {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn $fn_name(config_json: *const c_char) -> i32 {
|
||||
#[cfg(feature = $feature)]
|
||||
{
|
||||
let json = match config_str(config_json) {
|
||||
Ok(j) => j,
|
||||
Err(code) => return code,
|
||||
};
|
||||
let settings: $settings = match serde_json::from_str(&json) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!(concat!("[ffi:", $name, "] bad settings JSON: {}"), e);
|
||||
return LUX_FFI_ERR_PARSE;
|
||||
}
|
||||
};
|
||||
let run = $run;
|
||||
spawn_service($name, move || run(settings))
|
||||
}
|
||||
#[cfg(not(feature = $feature))]
|
||||
{
|
||||
let _ = config_json;
|
||||
eprintln!(concat!(
|
||||
"[ffi:", $name,
|
||||
"] service not compiled in (rebuild ffi with --features ", $feature, ")"
|
||||
));
|
||||
LUX_FFI_ERR_DISABLED
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Wired services. Add one line per service (and the matching path dep +
|
||||
// feature in Cargo.toml). Each upstream entrypoint signature was confirmed
|
||||
// against explorer-rs:
|
||||
// sig-provider : sig_provider_server::sig_provider(Settings)
|
||||
// smart-contract-verifier : smart_contract_verifier_server::run(Settings)
|
||||
// stats : stats_server::stats(Settings, None)
|
||||
// multichain-aggregator : multichain_aggregator_server::run(Settings)
|
||||
// visualizer : visualizer_server::run(Settings)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
service!(
|
||||
fn_name = lux_explorer_start_sig_provider,
|
||||
name = "sig-provider",
|
||||
feature = "sig-provider",
|
||||
settings = sig_provider_server::Settings,
|
||||
run = |s| sig_provider_server::sig_provider(s),
|
||||
);
|
||||
|
||||
// --- Scale-out (uncomment the dep+feature in Cargo.toml, then this block) ---
|
||||
//
|
||||
// service!(
|
||||
// fn_name = lux_explorer_start_smart_contract_verifier,
|
||||
// name = "smart-contract-verifier",
|
||||
// feature = "smart-contract-verifier",
|
||||
// settings = smart_contract_verifier_server::Settings,
|
||||
// run = |s| smart_contract_verifier_server::run(s),
|
||||
// );
|
||||
//
|
||||
// service!(
|
||||
// fn_name = lux_explorer_start_stats,
|
||||
// name = "stats",
|
||||
// feature = "stats",
|
||||
// settings = stats_server::Settings,
|
||||
// run = |s| stats_server::stats(s, None),
|
||||
// );
|
||||
//
|
||||
// service!(
|
||||
// fn_name = lux_explorer_start_multichain_aggregator,
|
||||
// name = "multichain-aggregator",
|
||||
// feature = "multichain-aggregator",
|
||||
// settings = multichain_aggregator_server::Settings,
|
||||
// run = |s| multichain_aggregator_server::run(s),
|
||||
// );
|
||||
//
|
||||
// service!(
|
||||
// fn_name = lux_explorer_start_visualizer,
|
||||
// name = "visualizer",
|
||||
// feature = "visualizer",
|
||||
// settings = visualizer_server::Settings,
|
||||
// run = |s| visualizer_server::run(s),
|
||||
// );
|
||||
|
||||
/// Convenience: start every service compiled into this staticlib with default
|
||||
/// config (`{}` => upstream defaults). Returns the bitwise-OR of the per-service
|
||||
/// return codes, so 0 means every wired service launched. Disabled services
|
||||
/// contribute `LUX_FFI_ERR_DISABLED`; the Go caller can treat that as "not
|
||||
/// requested" and ignore it, or call the specific starters it needs instead.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn lux_explorer_start_all() -> i32 {
|
||||
let empty = b"{}\0".as_ptr() as *const c_char;
|
||||
let mut rc = LUX_FFI_OK;
|
||||
rc |= lux_explorer_start_sig_provider(empty);
|
||||
// Mirror each service! entry here as it is enabled:
|
||||
// rc |= lux_explorer_start_smart_contract_verifier(empty);
|
||||
// rc |= lux_explorer_start_stats(empty);
|
||||
// rc |= lux_explorer_start_multichain_aggregator(empty);
|
||||
// rc |= lux_explorer_start_visualizer(empty);
|
||||
rc
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
//go:build !ffi
|
||||
|
||||
// Package main — non-FFI build variant (the default).
|
||||
//
|
||||
// A plain `go build ./...` compiles this file and NOT ffi_on.go, so the
|
||||
// explorer builds and runs WITHOUT the Rust staticlib present. The Blockscout-rs
|
||||
// services are simply not started; everything else (Go-native indexer + graph +
|
||||
// SPA + realtime) works exactly as before. Build the single all-in-one binary
|
||||
// with `make single` (which adds -tags ffi and links liblux_explorer_ffi.a).
|
||||
package main
|
||||
|
||||
// ffiEnabled is false in the default build; main.go logs this so it is obvious
|
||||
// which variant is running.
|
||||
const ffiEnabled = false
|
||||
|
||||
// startFFIServices is a no-op in the non-FFI build.
|
||||
func startFFIServices(_ map[string]string) {}
|
||||
@@ -0,0 +1,84 @@
|
||||
//go:build ffi
|
||||
|
||||
// Package main — FFI build variant.
|
||||
//
|
||||
// When built with `-tags ffi` the explorer links the Rust staticlib
|
||||
// `liblux_explorer_ffi.a` (produced by `cargo build --release` in ./ffi) and
|
||||
// runs the Blockscout-rs explorer services (sig-provider, and — as they are
|
||||
// scaled out — smart-contract-verifier, stats, multichain-aggregator,
|
||||
// visualizer) IN-PROCESS via cgo. No subprocesses, no Go rewrites: each
|
||||
// service's real Rust entrypoint runs on its own Tokio runtime thread inside
|
||||
// this single binary.
|
||||
//
|
||||
// Build: make single (cargo build --release in ffi/ then go build -tags ffi)
|
||||
// The non-FFI fallback lives in ffi_off.go (//go:build !ffi) so a plain
|
||||
// `go build ./...` compiles WITHOUT the staticlib present.
|
||||
package main
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -L${SRCDIR}/ffi/target/release -llux_explorer_ffi
|
||||
// System libraries the Rust staticlib (tokio + reqwest/curl + openssl + ring)
|
||||
// transitively needs. macOS frameworks cover Security/TLS + reachability;
|
||||
// resolv/c++ cover DNS + the C++ runtime pulled by curl/openssl. On Linux this
|
||||
// preamble is replaced by ffi_on_linux.go's cgo line.
|
||||
#cgo darwin LDFLAGS: -lresolv -lc++ -framework Security -framework CoreFoundation -framework SystemConfiguration
|
||||
#cgo linux LDFLAGS: -lresolv -lstdc++ -lm -ldl -lpthread -lssl -lcrypto
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
// Mirrors the C ABI exported by ffi/src/lib.rs. Each starter spawns the named
|
||||
// service on its own runtime thread and returns 0 (LUX_FFI_OK) once launched.
|
||||
extern int lux_explorer_start_sig_provider(const char* config_json);
|
||||
extern int lux_explorer_start_all(void);
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"log"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// ffiEnabled is read by main.go to log which build variant is running.
|
||||
const ffiEnabled = true
|
||||
|
||||
// ffiResult maps the C return codes from ffi/src/lib.rs to human strings.
|
||||
func ffiResult(code C.int) string {
|
||||
switch int(code) {
|
||||
case 0:
|
||||
return "ok"
|
||||
case 1:
|
||||
return "bad-config"
|
||||
case 2:
|
||||
return "parse-error"
|
||||
case 3:
|
||||
return "disabled"
|
||||
case 4:
|
||||
return "spawn-error"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// startFFIServices launches the in-process Rust explorer services. configJSON
|
||||
// keys the per-service settings (empty => upstream defaults). It is called once
|
||||
// from main() at startup in the ffi build; the non-FFI build's stub is a no-op.
|
||||
//
|
||||
// Each starter returns immediately after spawning the service's worker thread,
|
||||
// so this does not block the Go HTTP listener.
|
||||
func startFFIServices(configJSON map[string]string) {
|
||||
start := func(name string, fn func(*C.char) C.int) {
|
||||
cfg := configJSON[name]
|
||||
var cstr *C.char
|
||||
if cfg != "" {
|
||||
cstr = C.CString(cfg)
|
||||
defer C.free(unsafe.Pointer(cstr))
|
||||
}
|
||||
code := fn(cstr)
|
||||
log.Printf("[ffi] start %s -> %s (%d)", name, ffiResult(code), int(code))
|
||||
}
|
||||
|
||||
start("sig-provider", func(c *C.char) C.int { return C.lux_explorer_start_sig_provider(c) })
|
||||
// As services are scaled out in ffi/, add their starters here, e.g.:
|
||||
// start("stats", func(c *C.char) C.int { return C.lux_explorer_start_stats(c) })
|
||||
}
|
||||
@@ -119,6 +119,16 @@ func main() {
|
||||
go registry.hub.Run(ctx)
|
||||
go supervisor.Wait(ctx)
|
||||
|
||||
// In the `-tags ffi` build this launches the Blockscout-rs explorer
|
||||
// services (sig-provider, …) in-process via cgo on their own Tokio
|
||||
// runtime threads — one binary runs everything. In the default build
|
||||
// startFFIServices is a no-op (see ffi_off.go). Per-service settings are
|
||||
// keyed by service name; empty => the service's upstream defaults / env.
|
||||
if ffiEnabled {
|
||||
log.Printf("[explorer] ffi: starting in-process Rust services")
|
||||
}
|
||||
startFFIServices(map[string]string{})
|
||||
|
||||
frontend, err := NewFrontend(cfg, registry)
|
||||
if err != nil {
|
||||
log.Fatalf("[explorer] frontend: %v", err)
|
||||
|
||||
Reference in New Issue
Block a user