Skip to content

poc(kernel): pure-Go (CGO_ENABLED=0) dynamic loader — control + data plane, no latency regression - #438

Draft
msrathore-db wants to merge 3 commits into
mainfrom
kernel-dynamic-loader-poc
Draft

poc(kernel): pure-Go (CGO_ENABLED=0) dynamic loader — control + data plane, no latency regression#438
msrathore-db wants to merge 3 commits into
mainfrom
kernel-dynamic-loader-poc

Conversation

@msrathore-db

@msrathore-db msrathore-db commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Draft / proof-of-concept. Not for merge as-is. Opened to validate the dynamic-loading approach in-repo. See internal/backend/kernel/DYNAMIC_LOADER_POC.md.

What this proves

The shipped kernel backend static-links libdatabricks_sql_kernel.a through cgo, forcing CGO_ENABLED=1, a C toolchain on every builder, and no cross-compilation — the blockers the SEA/kernel release design flags against making SEA the default. This PoC drives the closed-source kernel from a pure-Go (CGO_ENABLED=0) binary by dlopen-ing the kernel shared library at run time via purego — the model gosnowflake uses for its own closed-source core.

Now complete: both control plane AND data plane, verified live on pecotesting.

Control plane

dlopen → config → session open → execute → query-id / affected-rows → teardown. No cgo.

Data plane (the previously-blocked part — now solved)

arrow-go v12's cdata importer is itself cgo, so it can't run under CGO_ENABLED=0. cdata_pure.go is a pure-Go port of that importer's C-Data import path: reads the flat ArrowSchema/ArrowArray structs via unsafe, invokes each struct's release callback via purego.SyscallN, builds arrow.Records zero-copy with array.NewData. dynamic_rows.go pulls batches through the dlopen'd kernel_result_stream_* and scans with the same arrowscan scanner the cgo path uses — so values are identical by construction.

Verified live (TestDynamicLoaderDataPlane, CGO_ENABLED=0): scalars, null, decimal-as-exact-string, high-precision DECIMAL(38,2) byte-exact (9999999999999999999999999999.99 — proves buffer import correctness), date/timestamp/binary/float, nested array/map/struct as JSON, empty result set, and 100k rows across batches.

Latency: no regression (head-to-head on pecotesting)

Identical 500k-row × 3-col query, same drain loop, same scanner; both kernel artifacts built from the same pinned rev with tls-rustls. BenchmarkCgoLargeResult vs BenchmarkDynLargeResult:

Path ns/op (500k rows)
cgo static 7.91 s
purego dyn 6.17 / 6.61 / 8.59 s (repeated)

End-to-end dominated by warehouse + network → statistically indistinguishable; purego is never meaningfully slower and crosses no per-cell cgo boundary. No regression.

Test matrix (all green)

  • Default pure-Go suite (CGO_ENABLED=0, no tags): go test ./... — pass.
  • Dynamic-tagged (CGO_ENABLED=0 -tags databricks_kernel_dynamic): unit pass; e2e/data-plane pass live on pecotesting.
  • cgo-tagged (CGO_ENABLED=1 -tags databricks_kernel): unit pass; full TestKernelE2E* passes live on pecotesting, 0 failures — confirms the existing static path is not regressed.

No user CUJ change

WithUseKernel and the existing build tags are untouched. The loader lives behind a new, separate tag databricks_kernel_dynamic; the default pure-Go build is unchanged and does not pull in purego. No kernel source change (its Cargo.toml already declares cdylib).

Files

  • dynamic_loader.go — purego dlopen + C ABI binding (control plane + result stream)
  • cdata_pure.go, cdata_pure_finalizer.go — pure-Go Arrow C-Data importer
  • dynamic_rows.godriver.Rows over the pure-Go importer
  • dynamic_bench_test.go, cgo_bench.go, cgo_bench_test.go — head-to-head benchmarks
  • dynamic_loader_test.go — control + data-plane e2e
  • DYNAMIC_LOADER_POC.md — full writeup

Still PoC (must change before merge)

  • Local replace for purego so the branch builds offline (real PR pins v0.10.2 via proxy).
  • dylib path via env var (real code resolves via rpath / documented env var).
  • Hand-mirrored cKernelError/C-Data struct layouts (real PR adds unsafe.Sizeof/Offsetof asserts).
  • cdata_pure.go ports the type cases the kernel emits; union/run-end-encoded and the ArrowArrayStream reader are intentionally omitted (kernel uses next_batch pull).

This pull request and its description were written by Isaac.

Proof-of-concept that the closed-source SEA kernel can be driven from a
PURE-Go binary — no cgo, no C compiler, no static linking — by dlopen-ing
the kernel SHARED library at run time via ebitengine/purego, instead of
static-linking libdatabricks_sql_kernel.a through cgo at build time.

Why: the shipped kernel backend links a static .a via cgo, which forces
CGO_ENABLED=1, a C toolchain on every builder, and breaks Go's free
cross-compilation — the three blockers the SEA/kernel release design
calls out against ever making SEA the default backend. Dynamic loading
removes all three. It's the model gosnowflake uses for its own
closed-source native core.

Verified live against a warehouse, built CGO_ENABLED=0: dlopen -> config
-> session_open -> execute -> real server query id -> teardown, all with
no cgo. No kernel source change is needed — the kernel's Cargo.toml
already declares crate-type cdylib and the C ABI fns are #[no_mangle]
extern "C", so the .dylib already exports them.

No user CUJ change: WithUseKernel and the existing build tags are
untouched; this lives behind a NEW, separate tag (databricks_kernel_dynamic)
and the default pure-Go build does not pull in purego.

Scope: CONTROL plane only (DML/DDL happy path). The DATA plane (Arrow
result batches) is deliberately excluded because arrow-go/v12's cdata
importer is itself a cgo package — closing it needs a separate decision
(purego C-Data importer, arrow-go v18, or a thin cgo shim). Documented in
DYNAMIC_LOADER_POC.md along with the run-time lib-discovery and
glibc/musl follow-ups.

PoC-only shortcuts (not for merge as-is): a local `replace` for purego so
the branch builds offline, dylib path via env var, and a hand-mirrored
KernelError struct layout. All flagged in the doc.

Co-authored-by: Isaac
Extends the CGO_ENABLED=0 purego kernel PoC from control-plane-only to a
FULL working path, including result-row fetching, verified live on
pecotesting with a head-to-head latency benchmark.

Data plane (the previously-blocked part): arrow-go v12's cdata importer
is cgo, so it can't be used from CGO_ENABLED=0. cdata_pure.go is a
pure-Go port of that importer's C-Data *import* path — it reads the flat
ArrowSchema/ArrowArray structs via unsafe, invokes each struct's release
callback with purego.SyscallN, and builds arrow.Records zero-copy with
array.NewData. dynamic_rows.go pulls batches through the dlopen'd
kernel_result_stream_* and scans them with the SAME arrowscan scanner
the cgo rows.go uses, so values are identical to the cgo backend by
construction.

Verified live on pecotesting (TestDynamicLoaderDataPlane, CGO_ENABLED=0):
scalars, null, decimal-as-exact-string, high-precision DECIMAL(38,2)
returning byte-exact (proves buffer import correctness), temporal,
binary, nested array/map/struct as JSON, empty result set, and 100k rows
across multiple batches.

Latency (BenchmarkDynLargeResult vs BenchmarkCgoLargeResult, identical
500k-row query, both kernel artifacts built from the same pinned rev with
tls-rustls): cgo static 7.91s/op, purego dynamic 6.17-8.59s/op — end to
end dominated by warehouse+network, statistically indistinguishable, no
regression. The purego per-cell path crosses no cgo boundary.

Test matrix, all green: default pure-Go suite; dynamic-tagged
(CGO_ENABLED=0) unit + live e2e; cgo-tagged (CGO_ENABLED=1) unit + full
live TestKernelE2E* (0 failures, confirms the existing static path is
not regressed).

New files (all behind databricks_kernel_dynamic except the cgo bench,
which is behind cgo && databricks_kernel):
- cdata_pure.go / cdata_pure_finalizer.go: pure-Go C-Data importer
- dynamic_rows.go: driver.Rows over the pure-Go importer
- dynamic_bench_test.go / cgo_bench.go / cgo_bench_test.go: head-to-head
  benchmarks

Still PoC (unchanged from prior commit): local purego replace for offline
builds, dylib path via env, hand-mirrored structs. Documented in
DYNAMIC_LOADER_POC.md.

Co-authored-by: Isaac
@msrathore-db msrathore-db changed the title poc(kernel): pure-Go (CGO_ENABLED=0) dynamic loader for the kernel C ABI poc(kernel): pure-Go (CGO_ENABLED=0) dynamic loader — control + data plane, no latency regression Aug 11, 2026
Follow-up to the kernel work: with cgo accepted and the Arrow import
path settled on cgo + arrow-go cdata (zero-copy C-Data, the ADBC
driver-manager model), this PoCs the remaining open question — packaging
the closed-source kernel as a SHARED library loaded at run time instead
of a static .a baked into every binary.

New build tag databricks_kernel_dynlib (added alongside databricks_kernel)
selects cgo_dynlib_darwin.go, which links libdatabricks_sql_kernel.dylib
with -l + an rpath instead of naming the .a. The static cgo_darwin.go is
guarded with !databricks_kernel_dynlib so the two never both compile. The
Arrow C-Data import path (rows.go, arrow-go cdata) is unchanged — dynamic
vs static is invisible above the link layer.

Verified live on pecotesting (darwin/arm64):
- otool -L shows the binary references @rpath/libdatabricks_sql_kernel.dylib
  externally (not baked in); binary size drops ~61MB (static .a) -> ~12MB.
- 20/20 TestKernelE2E* subtests pass through the runtime-loaded dylib.
- Negative proof: moving the dylib away fails at load with
  "dyld: Library not loaded: @rpath/libdatabricks_sql_kernel.dylib" and
  prints the rpath search order; restoring it works again.
- Static .a path and default pure-Go build both still build unchanged.

One real gotcha surfaced + fixed: cargo's default dylib install_name is
the absolute build path (not relocatable); it must be set to
@rpath/libdatabricks_sql_kernel.dylib (install_name_tool -id, or a link
arg in the kernel build).

DYNAMIC_LINK_RELEASE.md documents the full release plan: .so publishing
on the kernel release line (serves ODBC too), soname/major + a
kernel_abi_version() load check for versioning, why ODBC and Go can run
different kernel versions (separate processes), and a phased
static->dynamic recommendation that keeps the CGO_ENABLED=0 pure-Go
Thrift fallback intact (no user CUJ change).

PoC scope: darwin/arm64 only (linux $ORIGIN + windows DLL noted as the
remaining per-OS work); dylib staged locally and gitignored.

Co-authored-by: Isaac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant