feat: custom Rust UDFs via arrow-ffi (alternative to #4283) [experimental]#4459
Draft
andygrove wants to merge 1 commit into
Draft
feat: custom Rust UDFs via arrow-ffi (alternative to #4283) [experimental]#4459andygrove wants to merge 1 commit into
andygrove wants to merge 1 commit into
Conversation
…ip ci] Adds custom Rust scalar UDF support using arrow-only FFI surfaces, as suggested by paleolimbot and timsaucer in feedback on apache#4283. This is an alternative implementation for comparison; see apache#4283 for the bespoke-ABI version. Two ABI flavors are provided side-by-side so reviewers can compare: 1. C ABI (sedona-style): pure C-callable struct of function pointers, parameterized only by Arrow C Data Interface (FFI_ArrowSchema / FFI_ArrowArray). Decoupled from datafusion versions; future-portable to C/C++. Modeled on apache/sedona-db's SedonaCScalarKernel header. 2. datafusion-ffi (FFI_ScalarUDF): wraps user's ScalarUDFImpl as FFI_ScalarUDF. Inherits full ScalarUDFImpl surface (variadic signatures, type coercion, metadata-aware return types) for free, at the cost of a major-version pin against datafusion-ffi. A single library may export either or both; loader walks both discovery functions. `comet-test-udfs` exposes `add_one_c` (C ABI) and `add_one_df` (datafusion-ffi) and the e2e suite drives both through Spark. Scope is intentionally minimal for comparison: scalar-only, happy-path e2e tests only (no panic/error/signature-mismatch coverage). The Scala JVM API mirrors apache#4283 exactly so the comparison is apples-to-apples. Pieces: - native/comet-udf-sdk: SDK with both ABI flavors + export macros - native/comet-test-udfs: cdylib exposing one UDF per ABI - native/core/src/execution/rust_udf: loader, cache, ImportedCScalarUdf - native/core/src/comet_rust_udf_bridge.rs: JNI for validateLibrary/listUdfs - native/proto: RustUdfCall message - spark/.../udf: CometRustUDF.register, registry, exception classes, JNI bridge stub - spark/.../serde/CometScalaUDF: dispatch ScalaUDF to RustUdfCall when udfName is in the registry
paleolimbot
reviewed
May 27, 2026
Member
paleolimbot
left a comment
There was a problem hiding this comment.
Thank you for putting this together!
Comment on lines
+544
to
+545
| // Filesystem path of the cdylib. | ||
| string library_path = 2; |
Member
There was a problem hiding this comment.
I see the motivation for having this primarily live as shared objects! I am not sure if there's an opportunity / whether it's any easier to bundle these UDFs as .jars or Python packages but that could be implemented as a future field of this struct.
Comment on lines
+548
to
+549
| // Expected return type, declared at register time on the JVM side. | ||
| DataType return_type = 4; |
Member
There was a problem hiding this comment.
Is a fixed return type a requirement? (DataFusion lets you compute this on demand but perhaps there's a limiation here)
Comment on lines
+130
to
+135
| /// Per-execution instance produced by [`CometCScalarKernel::new_impl`]. | ||
| /// | ||
| /// Not thread-safe; the caller must serialize access. Typically used on | ||
| /// one thread for one batch then dropped. | ||
| #[repr(C)] | ||
| pub struct CometCScalarKernelImpl { |
Comment on lines
+18
to
+23
| //! datafusion-ffi flavor. | ||
| //! | ||
| //! Discovery returns a list of `FFI_ScalarUDF` values produced by | ||
| //! `datafusion_ffi`. The host imports each via | ||
| //! `ForeignScalarUDF::try_from`, yielding a `ScalarUDFImpl` it can plug | ||
| //! straight into its existing planner — no further adaptation needed. |
Member
There was a problem hiding this comment.
Also cool! (And hard to argue with the compact implementation!)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Which issue does this PR close?
Companion / alternative to #4283. Both PRs are drafts intended for comparison, not for merging.
Rationale for this change
This PR adds custom Rust scalar UDF support using only arrow-stable FFI surfaces, as suggested in feedback on #4283 by @paleolimbot and @timsaucer. The goal is to compare two ABI strategies side-by-side:
feat: custom Rust UDFs [experimental] [skip ci] #4283 — bespoke C ABI with custom type tags + IPC bytes +
comet_udf_describethis PR — arrow-only FFI, with two flavors offered side-by-side:
C ABI (sedona-style) — pure C-callable struct of function pointers, parameterized only by Arrow's C Data Interface (
FFI_ArrowSchema/FFI_ArrowArray). No DataFusion types appear in the FFI surface, so user libraries are decoupled from datafusion versions and the ABI is portable to C/C++. Modeled on apache/sedona-db'sSedonaCScalarKernel.datafusion-ffi (
FFI_ScalarUDF) — wraps the user'sScalarUDFImplasFFI_ScalarUDF. The user's library inherits the fullScalarUDFImplsurface (variadic signatures, type coercion, metadata-aware return types) for free, at the cost of a major-version pin againstdatafusion-ffi.A single library may export either or both flavors; the host loader walks both discovery functions. The test cdylib exposes
add_one_cvia the C ABI andadd_one_dfvia datafusion-ffi, and the end-to-end Spark suite drives both.The scope is intentionally minimal vs #4283 — scalar-only, happy-path e2e tests only — to keep the diff focused on the ABI strategy comparison. JVM API surface mirrors #4283 exactly so the JVM side is apples-to-apples.
What changes are included in this PR?
native/comet-udf-sdk— public SDK with both ABI flavors behind cargo features (c-abi,df-abi, both default-on). ProvidesCometCScalarUdftrait +comet_c_udf_export!macro for the C flavor;comet_df_udf_export!for datafusion-ffi.native/comet-test-udfs— test cdylib exposing one UDF per ABI.native/core/src/execution/rust_udf—loader(libloading + ABI version probe + dual-discovery), process-widecache,ImportedCScalarUdfadapter wrapping the C ABI asScalarUDFImpl. The datafusion-ffi side needs no adapter —Arc<dyn ScalarUDFImpl>::from(&FFI_ScalarUDF)is built-in.RustUdfCallproto inexpr.proto(field 71) + planner branch increate_exprresolving (library_path, name) against the cache and dispatching through whichever ABI registered the kernel.CometRustUdfBridge/comet_rust_udf_bridge.rs) for driver-sidevalidateLibrary/listUdfs.CometRustUDF.register,CometRustUdfRegistry, typed exception classes). TheQueryPlanSerdepath:CometScalaUDF.convertfirst checks the registry — if the udfName is registered, it emitsRustUdfCall; otherwise it falls back to the existing JVM codegen dispatcher.The Scala/Java files live under
spark/.../udf/(rather thancommon/.../udf/as in #4283) to avoid theNativeBase-relocation churn. This is the only intentional structural divergence from #4283.How are these changes tested?
From<&FFI_ScalarUDF>.CometRustUdfSuite) — 2 tests, one per ABI:add_one_c(C ABI) andadd_one_df(datafusion-ffi). Both pass, gated on-Dcomet.test.udfs.lib=<path>.What this PR is not