Skip to content

fix: preserve provided arguments during FFI object construction - #24723

Merged
timsaucer merged 1 commit into
apache:mainfrom
timsaucer:fix/ffi-constructor-argument-drop
Aug 28, 2026
Merged

fix: preserve provided arguments during FFI object construction#24723
timsaucer merged 1 commit into
apache:mainfrom
timsaucer:fix/ffi-constructor-argument-drop

Conversation

@timsaucer

Copy link
Copy Markdown
Member

Which issue does this PR close?

Rationale for this change

Three datafusion-ffi constructors unwrap an already-foreign input and return its original handle, dropping the arguments passed alongside without an error or a warning:

  • FFI_LogicalExtensionCodec::new — discards task_ctx_provider
  • FFI_PhysicalExtensionCodec::new — discards task_ctx_provider
  • FFI_TableProvider::new_with_ffi_codec — discards logical_codec

The consequence is that a consumer which imports a foreign codec can never rebind it. Re-wrapping with a different provider compiles, runs, and has no effect, so the handle keeps resolving against whatever session it was first built with. In datafusion-python that shows up as decode callbacks resolving names against a pre-fork session: a UDF registered after the fork is invisible to them, and the config they see is a stale snapshot.

There is a second failure mode with the same root cause. The provider is held as a Weak, so a consumer that cannot rebind must keep the original session alive artificially or the capsule starts failing with TaskContextProvider went out of scope over FFI boundary.

The two sibling constructors that hit the same case already do the opposite — FFI_QueryPlanner::new_with_ffi_codecs and FFI_SessionRef::new_with_ffi_codecs both adopt the supplied codecs on the unwrap path, and the former documents that guarantee explicitly. This PR makes the other three consistent with them.

What changes are included in this PR?

On the already-foreign path, each of the three constructors now clones the original handle and overwrites the relevant #[repr(C)] field before returning it, matching FFI_QueryPlanner::new_with_ffi_codecs:

if let Some(codec) = (Arc::clone(&codec) as Arc<dyn Any>)
    .downcast_ref::<ForeignLogicalExtensionCodec>()
{
    let mut codec = codec.0.clone();
    codec.task_ctx_provider = task_ctx_provider.into();
    return codec;
}

The runtime argument is a deliberate exception. Unlike the codecs and the task context provider, runtime lives in private_data, which belongs to the library that owns the handle — this side cannot write it without an ABI change. FFI_SessionRef::new_with_ffi_codecs already takes the same position ("retaining its original private data and runtime"). Rather than leave that silent, all three constructors now document it, alongside the new adopt-on-unwrap guarantee.

No public signatures change, and no behavior changes on the non-foreign path.

Are these changes tested?

Yes — five new unit tests, one per behavior, in each affected module's own test module. All five fail on main and pass here.

  • ffi_logical_extension_codec_rebind_adopts_task_ctx_provider
  • ffi_logical_extension_codec_rebind_releases_original_session — covers the dangling-Weak failure mode: session A is dropped after the rebind, and the handle stays usable
  • ffi_physical_extension_codec_rebind_adopts_task_ctx_provider
  • test_rebind_foreign_table_provider_adopts_logical_codec
  • test_rebind_foreign_query_planner_adopts_codecs — a control over the already-correct sibling, so the two paths stay in agreement

Worth flagging for reviewers, since it is easy to write a test here that silently proves nothing: impl From<&FFI_LogicalExtensionCodec> for Arc<dyn LogicalExtensionCodec> compares library_marker_id first and returns the original local Arc on a match, so within one library the foreign branch is never reached. Each test overrides library_marker_id with crate::mock_foreign_marker_id and asserts the import really did produce a Foreign* wrapper before exercising the rebind.

cargo test -p datafusion-ffi --all-features passes (152 tests).

Are there any user-facing changes?

Yes, a behavior change, though it replaces a silent no-op with the documented intent.

Callers that pass a task_ctx_provider or logical_codec to these constructors alongside an already-foreign input previously had that argument ignored; it now takes effect. Anything relying on the old handle being returned untouched would see the change — but since the old path gave no way to observe or opt into that, it is hard to depend on deliberately.

Downstream, this lets datafusion-python drop the workaround in apache/datafusion-python#1677, which retains the pre-fork SessionContext purely to keep the Weak valid.

🤖 Generated with Claude Code

FFI_LogicalExtensionCodec::new, FFI_PhysicalExtensionCodec::new, and
FFI_TableProvider::new_with_ffi_codec unwrap an already-foreign input and
return its original handle, silently dropping the task context provider or
logical codec passed alongside. A consumer that imports a foreign codec can
therefore never rebind it: re-wrapping with a different provider compiles,
runs, and has no effect, so the handle keeps resolving against the session it
was first built with.

Because that provider is held as a Weak, the same gap forces consumers to keep
the original session alive artificially or hit "TaskContextProvider went out of
scope over FFI boundary".

Clone the original handle and overwrite the relevant repr(C) field before
returning it, matching FFI_QueryPlanner::new_with_ffi_codecs and
FFI_SessionRef::new_with_ffi_codecs, which already adopt on this path.

The runtime argument stays an exception: it lives in private_data owned by the
library that holds the handle, so this side cannot write it without an ABI
change. Document that on all three constructors rather than leaving it silent.

Closes apache#24722

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the ffi Changes to the ffi crate label Aug 27, 2026

@timsaucer timsaucer left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I've marked the 3 line behavior change of the PR below.

Comment on lines +314 to +316
let mut codec = codec.0.clone();
codec.task_ctx_provider = task_ctx_provider.into();
return codec;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is the core of the PR.

Comment on lines +300 to +302
let mut codec = codec.0.clone();
codec.task_ctx_provider = task_ctx_provider.into();
return codec;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is the core of the PR.

Comment on lines +584 to +586
let mut provider = provider.0.clone();
provider.logical_codec = logical_codec;
return provider;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is the core of the PR.

@timsaucer
timsaucer marked this pull request as ready for review August 27, 2026 13:21
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.09910% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 81.45%. Comparing base (1038d35) to head (7599f8c).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
datafusion/ffi/src/table_provider.rs 96.15% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main   #24723    +/-   ##
========================================
  Coverage   81.45%   81.45%            
========================================
  Files        1120     1120            
  Lines      401289   401397   +108     
  Branches   401289   401397   +108     
========================================
+ Hits       326874   326973    +99     
- Misses      55296    55299     +3     
- Partials    19119    19125     +6     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@timsaucer timsaucer changed the title fix: FFI constructors adopt arguments when the input is already foreign fix: preserve provided arguments during FFI object construction Aug 27, 2026
@timsaucer
timsaucer requested a lite review from Copilot August 27, 2026 14:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Makes datafusion-ffi constructors consistent when re-wrapping already-foreign handles by ensuring supplied rebinding arguments (task context provider / logical codec) are adopted rather than silently discarded, addressing #24722.

Changes:

  • Update three constructors to clone the foreign handle and overwrite the relevant #[repr(C)] field(s) before returning.
  • Add / expand rustdoc to document adopt-on-unwrap behavior and the deliberate exception for runtime (private data owned by originating library).
  • Add unit tests covering rebinding behavior and the prior dangling-Weak failure mode.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
datafusion/ffi/src/table_provider.rs Adopt logical_codec when re-wrapping an already-foreign table provider; document runtime exception; add rebinding test.
datafusion/ffi/src/query_planner.rs Add control test asserting the already-correct query planner constructor continues to adopt supplied codecs.
datafusion/ffi/src/proto/physical_extension_codec.rs Adopt task_ctx_provider on already-foreign path; document runtime exception; add rebinding test.
datafusion/ffi/src/proto/logical_extension_codec.rs Adopt task_ctx_provider on already-foreign path; document runtime exception; add rebinding tests including releasing the original session.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread datafusion/ffi/src/table_provider.rs

@milenkovicm milenkovicm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

thanks @timsaucer

@timsaucer
timsaucer added this pull request to the merge queue Aug 28, 2026
@timsaucer

Copy link
Copy Markdown
Member Author

Thank you for the review @milenkovicm

Merged via the queue into apache:main with commit 4d3e79e Aug 28, 2026
39 checks passed
@timsaucer
timsaucer deleted the fix/ffi-constructor-argument-drop branch August 28, 2026 12:32
timsaucer added a commit to timsaucer/datafusion that referenced this pull request Aug 28, 2026
…he#24723)

## Which issue does this PR close?

- Closes apache#24722.

## Rationale for this change

Three `datafusion-ffi` constructors unwrap an already-foreign input and
return its original handle, dropping the arguments passed alongside
without an error or a warning:

- `FFI_LogicalExtensionCodec::new` — discards `task_ctx_provider`
- `FFI_PhysicalExtensionCodec::new` — discards `task_ctx_provider`
- `FFI_TableProvider::new_with_ffi_codec` — discards `logical_codec`

The consequence is that a consumer which imports a foreign codec can
never rebind it. Re-wrapping with a different provider compiles, runs,
and has no effect, so the handle keeps resolving against whatever
session it was first built with. In `datafusion-python` that shows up as
decode callbacks resolving names against a pre-fork session: a UDF
registered after the fork is invisible to them, and the config they see
is a stale snapshot.

There is a second failure mode with the same root cause. The provider is
held as a `Weak`, so a consumer that cannot rebind must keep the
original session alive artificially or the capsule starts failing with
`TaskContextProvider went out of scope over FFI boundary`.

The two sibling constructors that hit the same case already do the
opposite — `FFI_QueryPlanner::new_with_ffi_codecs` and
`FFI_SessionRef::new_with_ffi_codecs` both adopt the supplied codecs on
the unwrap path, and the former documents that guarantee explicitly.
This PR makes the other three consistent with them.

## What changes are included in this PR?

On the already-foreign path, each of the three constructors now clones
the original handle and overwrites the relevant `#[repr(C)]` field
before returning it, matching `FFI_QueryPlanner::new_with_ffi_codecs`:

```rust
if let Some(codec) = (Arc::clone(&codec) as Arc<dyn Any>)
    .downcast_ref::<ForeignLogicalExtensionCodec>()
{
    let mut codec = codec.0.clone();
    codec.task_ctx_provider = task_ctx_provider.into();
    return codec;
}
```

The `runtime` argument is a deliberate exception. Unlike the codecs and
the task context provider, `runtime` lives in `private_data`, which
belongs to the library that owns the handle — this side cannot write it
without an ABI change. `FFI_SessionRef::new_with_ffi_codecs` already
takes the same position ("retaining its original private data and
runtime"). Rather than leave that silent, all three constructors now
document it, alongside the new adopt-on-unwrap guarantee.

No public signatures change, and no behavior changes on the non-foreign
path.

## Are these changes tested?

Yes — five new unit tests, one per behavior, in each affected module's
own test module. All five fail on `main` and pass here.

- `ffi_logical_extension_codec_rebind_adopts_task_ctx_provider`
- `ffi_logical_extension_codec_rebind_releases_original_session` —
covers the dangling-`Weak` failure mode: session A is dropped after the
rebind, and the handle stays usable
- `ffi_physical_extension_codec_rebind_adopts_task_ctx_provider`
- `test_rebind_foreign_table_provider_adopts_logical_codec`
- `test_rebind_foreign_query_planner_adopts_codecs` — a control over the
already-correct sibling, so the two paths stay in agreement

Worth flagging for reviewers, since it is easy to write a test here that
silently proves nothing: `impl From<&FFI_LogicalExtensionCodec> for
Arc<dyn LogicalExtensionCodec>` compares `library_marker_id` first and
returns the original local `Arc` on a match, so within one library the
foreign branch is never reached. Each test overrides `library_marker_id`
with `crate::mock_foreign_marker_id` and asserts the import really did
produce a `Foreign*` wrapper before exercising the rebind.

`cargo test -p datafusion-ffi --all-features` passes (152 tests).

## Are there any user-facing changes?

Yes, a behavior change, though it replaces a silent no-op with the
documented intent.

Callers that pass a `task_ctx_provider` or `logical_codec` to these
constructors alongside an already-foreign input previously had that
argument ignored; it now takes effect. Anything relying on the old
handle being returned untouched would see the change — but since the old
path gave no way to observe or opt into that, it is hard to depend on
deliberately.

Downstream, this lets `datafusion-python` drop the workaround in
apache/datafusion-python#1677, which retains the
pre-fork `SessionContext` purely to keep the `Weak` valid.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
timsaucer added a commit that referenced this pull request Aug 28, 2026
…uction (#24723) (#24752)

This is a back port of #24723 onto `branch-55` to support
`datafusion-python` upgrade to 55.1.0. The details can be found in the
linked PR. This is needed for
apache/datafusion-python#1677

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ffi Changes to the ffi crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FFI constructors silently discard arguments when the input is already foreign

4 participants