Files
explorer/ffi_on.go
zooqueen 7156dfdd7a feat(single-binary): zip front router + reverse-proxy to in-process Blockscout-rs services
Make the explorer ONE Go binary fronted by github.com/hanzoai/zip (Fiber v3 /
fasthttp) that runs the Blockscout-rs Rust services IN-PROCESS via the existing
cgo FFI staticlib and reverse-proxies to each. Least-work: the Rust services are
not rewritten/ported — their real entrypoints run on Tokio threads inside the
process, keeping their own HTTP servers on loopback.

Front router (front.go): zip owns the public listener; routes in order
  /api/<prefix>/*  -> httputil.ReverseProxy -> 127.0.0.1:<svc_port> (prefix stripped)
  /*               -> the existing net/http explorer mux via zip.AdaptNetHTTP
Security headers + CORS become zip middleware (replaces withSecurity).

Config (services: block, ServicesConfig/resolveServices in config.go+services.go):
single source of truth for enabled services, loopback ports (base 8050,
HTTP=base+2n/gRPC=+1), URL prefixes, and per-service settings JSON; both the
proxy and the FFI launcher read it. startFFIServices now takes resolvedService
and feeds each service its server addr (+ DB url when set).

Status: sig-provider WIRED + PROVEN end-to-end — one process owns :8090 and
:8050, curl /api/sig/health -> {"status":"SERVING"} and the real REST API
/api/sig/api/v1/abi/function -> 200. visualizer + smart-contract-verifier are
stateless and ready to wire (recipe in ffi/README.md). stats +
multichain-aggregator stay DISABLED: blockscout-service-launcher's DB layer is
Postgres-only (initialize_postgres pins DatabaseBackend::Postgres) — no SQLite
without a Rust rewrite — and stats also needs a populated Blockscout indexer
Postgres. The Go side already passes database.url+migrations through, so they
enable with config alone once a Postgres exists.

Build: make single (cargo build ffi/ + go build -tags ffi) unchanged; default
go build ./... is the Go-native binary (ffi_off.go stub, no cgo). go build ./...
and go test ./... pass; make single links the sig-provider symbol and boots.
2026-06-23 21:20:29 -07:00

103 lines
4.0 KiB
Go

//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"
}
}
// ffiStarters maps an FFI service name to its C entrypoint. Adding a service
// is one line here plus its service!{…} block in ffi/src/lib.rs and its
// dep+feature in ffi/Cargo.toml (see ffi/README.md). Disabled services still
// export their symbol and return LUX_FFI_ERR_DISABLED, so this table can list
// every service unconditionally — the Go binary links identically regardless
// of which cargo features were compiled in.
var ffiStarters = map[string]func(*C.char) C.int{
"sig-provider": func(c *C.char) C.int { return C.lux_explorer_start_sig_provider(c) },
// Scale-out (uncomment as the matching service!{…} is enabled in ffi/):
// "smart-contract-verifier": func(c *C.char) C.int { return C.lux_explorer_start_smart_contract_verifier(c) },
// "stats": func(c *C.char) C.int { return C.lux_explorer_start_stats(c) },
// "multichain-aggregator": func(c *C.char) C.int { return C.lux_explorer_start_multichain_aggregator(c) },
// "visualizer": func(c *C.char) C.int { return C.lux_explorer_start_visualizer(c) },
}
// startFFIServices launches each resolved in-process Rust service by calling
// its C entrypoint with the settings JSON resolveServices rendered (server
// addr/port + optional DB url). 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 zip HTTP listener.
func startFFIServices(svcs []resolvedService) {
for _, s := range svcs {
fn, ok := ffiStarters[s.Name]
if !ok {
log.Printf("[ffi] no entrypoint for service %q (rebuild ffi with its feature?)", s.Name)
continue
}
var cstr *C.char
if s.SettingsJSON != "" {
cstr = C.CString(s.SettingsJSON)
}
code := fn(cstr)
if cstr != nil {
C.free(unsafe.Pointer(cstr))
}
log.Printf("[ffi] start %s on :%d -> %s (%d)", s.Name, s.HTTPPort, ffiResult(code), int(code))
}
}