Skip to content

[POC] Expose PlanExecutor through FFI - #2

Draft
kyli87 wants to merge 4 commits into
kyli/plan-migration-v0-scanfrom
kyli/plan-migration-v0-ffi
Draft

kyli87 wants to merge 4 commits into
kyli/plan-migration-v0-scanfrom
kyli/plan-migration-v0-ffi

Conversation

@kyli87

@kyli87 kyli87 commented May 18, 2026

Copy link
Copy Markdown
Owner

[Disclaimer] Primarily AI-generated code without much cleanup. Goal was just to identify and showcase tricky parts of the implementation.

Summary

This PR exposes the ability to provide a plan executor through FFI. Specifically:

  • FfiPlanExecutor implements the PlanExecutor interface and wraps an upcall function pointer to allow plan execution to happen in an external language (i.e Java).
  • DeclarativePlanNodes are serialized as proto when passed across the FFI boundary. This PR adds the proto definitions and necessary build steps.
  • Expose repr(c) types that Java-side can use to pass data down to Rust, namely CPlanResultWrapper, CPlanResult, and nested types allow Java to construct the result of plan execution and return it to Rust side.

Memory Management

  • Data returned from Java-side plan execution must outlive the duration of the upcall (since the Java call is unaware how long the Rust side will hold the result for). This means we can not simply use a scoped arena and that memory management of Java-allocated objects must happen on the Rust side.
  • The general pattern for this is to provide a free upcall function pointer to Rust that can be invoked when the Rust-side object is dropped.
    • To simplify this slightly, this POC chose to use a single free function on the PlanResultWrapper rather than a separate free function on each possible return struct.

Other Callouts

  • Error handling still needs to be ironed out. Currently, Rust receives Java exceptions as Strings. As a workaround, this POC just maps these strings to the Rust error type

How was this change tested?

AI generated tests - just a POC

@github-actions

github-actions Bot commented May 18, 2026

Copy link
Copy Markdown

PR title does not match the required pattern. Please ensure you follow the conventional commits spec.

Your title should start with feat:, fix:, chore:, docs:, perf:, refactor:, test:, or ci:, and if it's a breaking change that should be suffixed with a ! (like feat!:), and then a 1-72 character brief description of your change.

Title: [POC] Expose PlanExecutor through FFI
PR title does not match the required pattern. Please ensure you follow the conventional commits spec.

Your title should start with feat:, fix:, chore:, docs:, perf:, refactor:, test:, or ci:, and if it's a breaking change that should be suffixed with a ! (like feat!:), and then a 1-72 character brief description of your change.

Title: [POC] Expose PlanExecutor through FFI
PR title does not match the required pattern. Please ensure you follow the conventional commits spec.

Your title should start with feat:, fix:, chore:, docs:, perf:, refactor:, test:, or ci:, and if it's a breaking change that should be suffixed with a ! (like feat!:), and then a 1-72 character brief description of your change.

Title: [POC] Expose PlanExecutor through FFI
PR title does not match the required pattern. Please ensure you follow the conventional commits spec.

Your title should start with feat:, fix:, chore:, docs:, perf:, refactor:, test:, or ci:, and if it's a breaking change that should be suffixed with a ! (like feat!:), and then a 1-72 character brief description of your change.

Title: [POC] Expose PlanExecutor through FFI

@@ -0,0 +1,79 @@
syntax = "proto3";

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Proto is the most convenient option for passing plan nodes across FFI, however we need to figure out how to keep the proto in sync between Rust and Java (i.e some way for Java side to import the proto?)

Comment thread ffi/src/plan/executor.rs
/// Construct only as a [`CNextBytes::Some`] payload; the [`Drop`] impl (which invokes
/// `free`) runs whenever a value of this type goes out of scope on the Rust side.
#[repr(C)]
pub struct EngineAllocatedBytes {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Custom type that allows us to free the Java-allocated memory for each byte array

Comment thread ffi/src/plan/executor.rs
Comment on lines +103 to +114
pub struct CEngineDataIterator {
pub state: NullableCvoid,
pub next: extern "C" fn(state: NullableCvoid) -> CNextEngineData,
}

/// An engine-implemented streaming iterator of byte buffers. See [`CEngineDataIterator`] for
/// state semantics; cleanup is performed by [`CPlanResultWrapper::free`].
#[repr(C)]
pub struct CBytesIterator {
pub state: NullableCvoid,
pub next: extern "C" fn(state: NullableCvoid) -> CNextBytes,
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Provides state needed for Rust to move the iterator forward. Rather than adding another free function to the iter, this POC chooses to use the free function on the wrapping PlanResultWrapper. Slightly sketchy, but simplifies the code a bit.

Comment thread ffi/src/plan/executor.rs
/// recognized -- it maps to [`Error::FileNotFound`] so that kernel code paths which
/// branch on `FileNotFound` (e.g. snapshot loading retry logic) behave correctly when
/// the engine reports a missing file.
fn engine_error_from_message(msg: String) -> Error {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error handling needs to be fleshed out more, currently this is just a workaround. There is kernel logic that expects specific Error types to be returned for things like FileNotFound

Comment thread ffi/src/plan/executor.rs
state,
free,
} = (self.callback)(self.context, slice);
let cleanup = PlanResultCleanup { state, free };

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A "guard" that gets passed to each iter so that the entire PlanResult (and all nested structs) gets cleaned up when the iterator is dropped.

Comment thread ffi/src/lib.rs
/// returned engine is a [`PlanBasedEngine`] backed by it; otherwise it is a [`DefaultEngine`].
#[cfg(feature = "default-engine-base")]
fn get_default_engine_impl(
fn build_engine_impl(

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to allow optionally specifying a plan_executor, in which case we will build a PlanBasedExecutor instead.

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