Skip to main content

datafusion_ffi/
physical_optimizer.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use std::ffi::c_void;
19use std::sync::Arc;
20
21use async_trait::async_trait;
22use datafusion_common::config::ConfigOptions;
23use datafusion_common::error::Result;
24use datafusion_physical_optimizer::{PhysicalOptimizerContext, PhysicalOptimizerRule};
25use datafusion_physical_plan::ExecutionPlan;
26use stabby::string::String as SString;
27use tokio::runtime::Handle;
28
29use crate::config::FFI_ConfigOptions;
30use crate::execution_plan::FFI_ExecutionPlan;
31use crate::util::FFI_Result;
32use crate::{df_result, sresult_return};
33
34/// A stable struct for sharing [`PhysicalOptimizerContext`] across FFI boundaries.
35///
36/// This provides access to configuration options for optimizer rules that need
37/// extended context beyond the plan itself.
38#[repr(C)]
39#[derive(Debug)]
40pub struct FFI_PhysicalOptimizerContext {
41    pub config_options:
42        unsafe extern "C" fn(&FFI_PhysicalOptimizerContext) -> FFI_ConfigOptions,
43
44    /// Release the memory of the private data.
45    pub release: unsafe extern "C" fn(&mut FFI_PhysicalOptimizerContext),
46
47    /// Internal data. Only accessed by the provider.
48    pub private_data: *const c_void,
49}
50
51unsafe impl Send for FFI_PhysicalOptimizerContext {}
52unsafe impl Sync for FFI_PhysicalOptimizerContext {}
53
54struct OptimizerContextPrivateData {
55    config: ConfigOptions,
56}
57
58impl FFI_PhysicalOptimizerContext {
59    pub fn new(context: &dyn PhysicalOptimizerContext) -> Self {
60        let private_data = Box::new(OptimizerContextPrivateData {
61            config: context.config_options().clone(),
62        });
63        let private_data = Box::into_raw(private_data) as *const c_void;
64
65        Self {
66            config_options: context_config_options_fn,
67            release: context_release_fn,
68            private_data,
69        }
70    }
71
72    fn inner(&self) -> &OptimizerContextPrivateData {
73        unsafe { &*(self.private_data as *const OptimizerContextPrivateData) }
74    }
75}
76
77impl Drop for FFI_PhysicalOptimizerContext {
78    fn drop(&mut self) {
79        unsafe { (self.release)(self) }
80    }
81}
82
83unsafe extern "C" fn context_config_options_fn(
84    ctx: &FFI_PhysicalOptimizerContext,
85) -> FFI_ConfigOptions {
86    FFI_ConfigOptions::from(&ctx.inner().config)
87}
88
89unsafe extern "C" fn context_release_fn(ctx: &mut FFI_PhysicalOptimizerContext) {
90    if !ctx.private_data.is_null() {
91        unsafe {
92            let _ = Box::from_raw(ctx.private_data as *mut OptimizerContextPrivateData);
93        }
94        ctx.private_data = std::ptr::null();
95    }
96}
97
98/// Reconstructed [`PhysicalOptimizerContext`] on the consumer side of FFI.
99///
100/// `StatisticsRegistry` is not plumbed because it contains trait object vtables
101/// that are only valid within the originating library.
102struct ForeignOptimizerContext {
103    config: ConfigOptions,
104}
105
106impl PhysicalOptimizerContext for ForeignOptimizerContext {
107    fn config_options(&self) -> &ConfigOptions {
108        &self.config
109    }
110}
111
112/// A stable struct for sharing [`PhysicalOptimizerRule`] across FFI boundaries.
113#[repr(C)]
114#[derive(Debug)]
115pub struct FFI_PhysicalOptimizerRule {
116    pub optimize: unsafe extern "C" fn(
117        &Self,
118        plan: &FFI_ExecutionPlan,
119        config: FFI_ConfigOptions,
120    ) -> FFI_Result<FFI_ExecutionPlan>,
121
122    pub name: unsafe extern "C" fn(&Self) -> SString,
123
124    pub schema_check: unsafe extern "C" fn(&Self) -> bool,
125
126    /// Used to create a clone on the rule. This should
127    /// only need to be called by the receiver of the plan.
128    pub clone: unsafe extern "C" fn(plan: &Self) -> Self,
129
130    /// Release the memory of the private data when it is no longer being used.
131    pub release: unsafe extern "C" fn(arg: &mut Self),
132
133    /// Return the major DataFusion version number of this rule.
134    pub version: unsafe extern "C" fn() -> u64,
135
136    pub optimize_with_context: unsafe extern "C" fn(
137        &Self,
138        plan: &FFI_ExecutionPlan,
139        context: &FFI_PhysicalOptimizerContext,
140    ) -> FFI_Result<FFI_ExecutionPlan>,
141
142    /// Internal data. This is only to be accessed by the provider of the rule.
143    /// A [`ForeignPhysicalOptimizerRule`] should never attempt to access this data.
144    pub private_data: *mut c_void,
145
146    /// Utility to identify when FFI objects are accessed locally through
147    /// the foreign interface.
148    pub library_marker_id: extern "C" fn() -> usize,
149}
150
151unsafe impl Send for FFI_PhysicalOptimizerRule {}
152unsafe impl Sync for FFI_PhysicalOptimizerRule {}
153
154struct RulePrivateData {
155    rule: Arc<dyn PhysicalOptimizerRule + Send + Sync>,
156    runtime: Option<Handle>,
157}
158
159impl FFI_PhysicalOptimizerRule {
160    fn inner(&self) -> &Arc<dyn PhysicalOptimizerRule + Send + Sync> {
161        let private_data = self.private_data as *const RulePrivateData;
162        unsafe { &(*private_data).rule }
163    }
164
165    fn runtime(&self) -> Option<Handle> {
166        let private_data = self.private_data as *const RulePrivateData;
167        unsafe { (*private_data).runtime.clone() }
168    }
169}
170
171unsafe extern "C" fn optimize_fn_wrapper(
172    rule: &FFI_PhysicalOptimizerRule,
173    plan: &FFI_ExecutionPlan,
174    config: FFI_ConfigOptions,
175) -> FFI_Result<FFI_ExecutionPlan> {
176    let runtime = rule.runtime();
177    let rule = rule.inner();
178    let plan: Arc<dyn ExecutionPlan> = sresult_return!(plan.try_into());
179    let config = sresult_return!(ConfigOptions::try_from(config));
180    let optimized_plan = sresult_return!(rule.optimize(plan, &config));
181
182    FFI_Result::Ok(FFI_ExecutionPlan::new(optimized_plan, runtime))
183}
184
185unsafe extern "C" fn optimize_with_context_fn_wrapper(
186    rule: &FFI_PhysicalOptimizerRule,
187    plan: &FFI_ExecutionPlan,
188    context: &FFI_PhysicalOptimizerContext,
189) -> FFI_Result<FFI_ExecutionPlan> {
190    let runtime = rule.runtime();
191    let inner = rule.inner();
192    let plan: Arc<dyn ExecutionPlan> = sresult_return!(plan.try_into());
193    let config = sresult_return!(ConfigOptions::try_from(unsafe {
194        (context.config_options)(context)
195    }));
196    let foreign_ctx = ForeignOptimizerContext { config };
197    let optimized_plan = sresult_return!(inner.optimize_with_context(plan, &foreign_ctx));
198
199    FFI_Result::Ok(FFI_ExecutionPlan::new(optimized_plan, runtime))
200}
201
202unsafe extern "C" fn name_fn_wrapper(rule: &FFI_PhysicalOptimizerRule) -> SString {
203    let rule = rule.inner();
204    rule.name().into()
205}
206
207unsafe extern "C" fn schema_check_fn_wrapper(rule: &FFI_PhysicalOptimizerRule) -> bool {
208    rule.inner().schema_check()
209}
210
211unsafe extern "C" fn release_fn_wrapper(rule: &mut FFI_PhysicalOptimizerRule) {
212    unsafe {
213        debug_assert!(!rule.private_data.is_null());
214        let private_data = Box::from_raw(rule.private_data as *mut RulePrivateData);
215        drop(private_data);
216        rule.private_data = std::ptr::null_mut();
217    }
218}
219
220unsafe extern "C" fn clone_fn_wrapper(
221    rule: &FFI_PhysicalOptimizerRule,
222) -> FFI_PhysicalOptimizerRule {
223    let runtime = rule.runtime();
224    let rule = Arc::clone(rule.inner());
225
226    let private_data =
227        Box::into_raw(Box::new(RulePrivateData { rule, runtime })) as *mut c_void;
228
229    FFI_PhysicalOptimizerRule {
230        optimize: optimize_fn_wrapper,
231        optimize_with_context: optimize_with_context_fn_wrapper,
232        name: name_fn_wrapper,
233        schema_check: schema_check_fn_wrapper,
234        clone: clone_fn_wrapper,
235        release: release_fn_wrapper,
236        version: super::version,
237        private_data,
238        library_marker_id: crate::get_library_marker_id,
239    }
240}
241
242impl Drop for FFI_PhysicalOptimizerRule {
243    fn drop(&mut self) {
244        unsafe { (self.release)(self) }
245    }
246}
247
248impl FFI_PhysicalOptimizerRule {
249    /// Creates a new [`FFI_PhysicalOptimizerRule`].
250    pub fn new(
251        rule: Arc<dyn PhysicalOptimizerRule + Send + Sync>,
252        runtime: Option<Handle>,
253    ) -> Self {
254        if let Some(rule) = (Arc::clone(&rule) as Arc<dyn std::any::Any>)
255            .downcast_ref::<ForeignPhysicalOptimizerRule>()
256        {
257            return rule.rule.clone();
258        }
259
260        let private_data = Box::new(RulePrivateData { rule, runtime });
261        let private_data = Box::into_raw(private_data) as *mut c_void;
262
263        Self {
264            optimize: optimize_fn_wrapper,
265            optimize_with_context: optimize_with_context_fn_wrapper,
266            name: name_fn_wrapper,
267            schema_check: schema_check_fn_wrapper,
268            clone: clone_fn_wrapper,
269            release: release_fn_wrapper,
270            version: super::version,
271            private_data,
272            library_marker_id: crate::get_library_marker_id,
273        }
274    }
275}
276
277/// This wrapper struct exists on the receiver side of the FFI interface, so it has
278/// no guarantees about being able to access the data in `private_data`. Any functions
279/// defined on this struct must only use the stable functions provided in
280/// FFI_PhysicalOptimizerRule to interact with the foreign rule.
281#[derive(Debug)]
282pub struct ForeignPhysicalOptimizerRule {
283    name: String,
284    rule: FFI_PhysicalOptimizerRule,
285}
286
287unsafe impl Send for ForeignPhysicalOptimizerRule {}
288unsafe impl Sync for ForeignPhysicalOptimizerRule {}
289
290impl From<&FFI_PhysicalOptimizerRule> for Arc<dyn PhysicalOptimizerRule + Send + Sync> {
291    fn from(rule: &FFI_PhysicalOptimizerRule) -> Self {
292        if (rule.library_marker_id)() == crate::get_library_marker_id() {
293            return Arc::clone(rule.inner());
294        }
295
296        let name: String = unsafe { (rule.name)(rule).into() };
297        Arc::new(ForeignPhysicalOptimizerRule {
298            name,
299            rule: rule.clone(),
300        }) as Arc<dyn PhysicalOptimizerRule + Send + Sync>
301    }
302}
303
304impl Clone for FFI_PhysicalOptimizerRule {
305    fn clone(&self) -> Self {
306        unsafe { (self.clone)(self) }
307    }
308}
309
310#[async_trait]
311impl PhysicalOptimizerRule for ForeignPhysicalOptimizerRule {
312    fn optimize(
313        &self,
314        plan: Arc<dyn ExecutionPlan>,
315        config: &ConfigOptions,
316    ) -> Result<Arc<dyn ExecutionPlan>> {
317        let config_options: FFI_ConfigOptions = config.into();
318        let plan = FFI_ExecutionPlan::new(plan, None);
319
320        let optimized_plan = unsafe {
321            df_result!((self.rule.optimize)(&self.rule, &plan, config_options))?
322        };
323        (&optimized_plan).try_into()
324    }
325
326    fn optimize_with_context(
327        &self,
328        plan: Arc<dyn ExecutionPlan>,
329        context: &dyn PhysicalOptimizerContext,
330    ) -> Result<Arc<dyn ExecutionPlan>> {
331        let ffi_context = FFI_PhysicalOptimizerContext::new(context);
332        let plan = FFI_ExecutionPlan::new(plan, None);
333
334        let optimized_plan = unsafe {
335            df_result!((self.rule.optimize_with_context)(
336                &self.rule,
337                &plan,
338                &ffi_context
339            ))?
340        };
341        (&optimized_plan).try_into()
342    }
343
344    fn name(&self) -> &str {
345        &self.name
346    }
347
348    fn schema_check(&self) -> bool {
349        unsafe { (self.rule.schema_check)(&self.rule) }
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use std::sync::Arc;
356
357    use arrow::datatypes::{DataType, Field, Schema};
358    use datafusion_common::config::ConfigOptions;
359    use datafusion_common::error::Result;
360    use datafusion_physical_optimizer::{
361        ConfigOnlyContext, PhysicalOptimizerContext, PhysicalOptimizerRule,
362    };
363    use datafusion_physical_plan::ExecutionPlan;
364    use datafusion_physical_plan::operator_statistics::StatisticsRegistry;
365
366    use super::*;
367    use crate::execution_plan::tests::EmptyExec;
368
369    #[derive(Debug)]
370    struct NoOpRule {
371        schema_check: bool,
372    }
373
374    impl PhysicalOptimizerRule for NoOpRule {
375        fn optimize(
376            &self,
377            plan: Arc<dyn ExecutionPlan>,
378            _config: &ConfigOptions,
379        ) -> Result<Arc<dyn ExecutionPlan>> {
380            Ok(plan)
381        }
382
383        fn name(&self) -> &str {
384            "no_op_rule"
385        }
386
387        fn schema_check(&self) -> bool {
388            self.schema_check
389        }
390    }
391
392    /// A rule that returns an error from `optimize` but succeeds when
393    /// called via `optimize_with_context`, proving the context path is taken.
394    #[derive(Debug)]
395    struct ContextAwareRule;
396
397    impl PhysicalOptimizerRule for ContextAwareRule {
398        fn optimize(
399            &self,
400            _plan: Arc<dyn ExecutionPlan>,
401            _config: &ConfigOptions,
402        ) -> Result<Arc<dyn ExecutionPlan>> {
403            Err(datafusion_common::DataFusionError::Plan(
404                "optimize should not be called directly".to_string(),
405            ))
406        }
407
408        fn optimize_with_context(
409            &self,
410            plan: Arc<dyn ExecutionPlan>,
411            _context: &dyn PhysicalOptimizerContext,
412        ) -> Result<Arc<dyn ExecutionPlan>> {
413            Ok(plan)
414        }
415
416        fn name(&self) -> &str {
417            "context_aware_rule"
418        }
419
420        fn schema_check(&self) -> bool {
421            true
422        }
423    }
424
425    fn create_test_plan() -> Arc<dyn ExecutionPlan> {
426        let schema =
427            Arc::new(Schema::new(vec![Field::new("a", DataType::Float32, false)]));
428        Arc::new(EmptyExec::new(schema))
429    }
430
431    #[test]
432    fn test_round_trip_ffi_physical_optimizer_rule() -> Result<()> {
433        for expected_schema_check in [true, false] {
434            let rule: Arc<dyn PhysicalOptimizerRule + Send + Sync> = Arc::new(NoOpRule {
435                schema_check: expected_schema_check,
436            });
437
438            let mut ffi_rule = FFI_PhysicalOptimizerRule::new(rule, None);
439            ffi_rule.library_marker_id = crate::mock_foreign_marker_id;
440
441            let foreign_rule: Arc<dyn PhysicalOptimizerRule + Send + Sync> =
442                (&ffi_rule).into();
443
444            assert_eq!(foreign_rule.name(), "no_op_rule");
445            assert_eq!(foreign_rule.schema_check(), expected_schema_check);
446        }
447
448        Ok(())
449    }
450
451    #[test]
452    fn test_round_trip_optimize() -> Result<()> {
453        let rule: Arc<dyn PhysicalOptimizerRule + Send + Sync> =
454            Arc::new(NoOpRule { schema_check: true });
455
456        let mut ffi_rule = FFI_PhysicalOptimizerRule::new(rule, None);
457        ffi_rule.library_marker_id = crate::mock_foreign_marker_id;
458
459        let foreign_rule: Arc<dyn PhysicalOptimizerRule + Send + Sync> =
460            (&ffi_rule).into();
461
462        let plan = create_test_plan();
463        let config = ConfigOptions::new();
464
465        let optimized = foreign_rule.optimize(plan, &config)?;
466        assert_eq!(optimized.name(), "empty-exec");
467
468        Ok(())
469    }
470
471    #[test]
472    fn test_local_bypass() -> Result<()> {
473        let rule: Arc<dyn PhysicalOptimizerRule + Send + Sync> =
474            Arc::new(NoOpRule { schema_check: true });
475
476        // Without mock marker, local bypass should return the original rule
477        let ffi_rule = FFI_PhysicalOptimizerRule::new(rule, None);
478        let recovered: Arc<dyn PhysicalOptimizerRule + Send + Sync> = (&ffi_rule).into();
479        let any_ref: &dyn std::any::Any = &*recovered;
480        assert!(any_ref.downcast_ref::<NoOpRule>().is_some());
481
482        // With mock marker, should wrap in ForeignPhysicalOptimizerRule
483        let rule2: Arc<dyn PhysicalOptimizerRule + Send + Sync> =
484            Arc::new(NoOpRule { schema_check: true });
485        let mut ffi_rule2 = FFI_PhysicalOptimizerRule::new(rule2, None);
486        ffi_rule2.library_marker_id = crate::mock_foreign_marker_id;
487        let recovered2: Arc<dyn PhysicalOptimizerRule + Send + Sync> =
488            (&ffi_rule2).into();
489        let any_ref2: &dyn std::any::Any = &*recovered2;
490        assert!(
491            any_ref2
492                .downcast_ref::<ForeignPhysicalOptimizerRule>()
493                .is_some()
494        );
495
496        Ok(())
497    }
498
499    #[test]
500    fn test_clone() -> Result<()> {
501        let rule: Arc<dyn PhysicalOptimizerRule + Send + Sync> =
502            Arc::new(NoOpRule { schema_check: true });
503
504        let ffi_rule = FFI_PhysicalOptimizerRule::new(rule, None);
505        let cloned = ffi_rule.clone();
506
507        let name1: String = unsafe { (ffi_rule.name)(&ffi_rule).into() };
508        let name2: String = unsafe { (cloned.name)(&cloned).into() };
509        assert_eq!(name1, name2);
510
511        Ok(())
512    }
513
514    #[test]
515    fn test_foreign_rule_rewrap_bypass() -> Result<()> {
516        // When creating an FFI wrapper from a ForeignPhysicalOptimizerRule,
517        // it should return the inner FFI rule rather than double-wrapping.
518        let rule: Arc<dyn PhysicalOptimizerRule + Send + Sync> =
519            Arc::new(NoOpRule { schema_check: true });
520
521        let mut ffi_rule = FFI_PhysicalOptimizerRule::new(rule, None);
522        ffi_rule.library_marker_id = crate::mock_foreign_marker_id;
523
524        let foreign_rule: Arc<dyn PhysicalOptimizerRule + Send + Sync> =
525            (&ffi_rule).into();
526
527        // Now wrap the foreign rule back into FFI - should not double-wrap
528        let re_wrapped = FFI_PhysicalOptimizerRule::new(foreign_rule, None);
529        let name: String = unsafe { (re_wrapped.name)(&re_wrapped).into() };
530        assert_eq!(name, "no_op_rule");
531
532        Ok(())
533    }
534
535    #[test]
536    fn test_optimize_with_context_round_trip() -> Result<()> {
537        let rule: Arc<dyn PhysicalOptimizerRule + Send + Sync> =
538            Arc::new(ContextAwareRule);
539
540        let mut ffi_rule = FFI_PhysicalOptimizerRule::new(rule, None);
541        ffi_rule.library_marker_id = crate::mock_foreign_marker_id;
542
543        let foreign_rule: Arc<dyn PhysicalOptimizerRule + Send + Sync> =
544            (&ffi_rule).into();
545
546        let plan = create_test_plan();
547        let config = ConfigOptions::new();
548        let context = ConfigOnlyContext::new(&config);
549
550        let optimized = foreign_rule.optimize_with_context(plan, &context)?;
551        assert_eq!(optimized.name(), "empty-exec");
552
553        Ok(())
554    }
555
556    /// Tests that `optimize_with_context` works even when the caller supplies a
557    /// statistics registry. The registry cannot survive the FFI round-trip (it
558    /// contains trait object vtables that are library-local), so the provider
559    /// side will always see `None`. This test verifies the context-aware path
560    /// still succeeds in that scenario.
561    #[test]
562    fn test_optimize_with_context_with_registry() -> Result<()> {
563        let rule: Arc<dyn PhysicalOptimizerRule + Send + Sync> =
564            Arc::new(ContextAwareRule);
565
566        let mut ffi_rule = FFI_PhysicalOptimizerRule::new(rule, None);
567        ffi_rule.library_marker_id = crate::mock_foreign_marker_id;
568
569        let foreign_rule: Arc<dyn PhysicalOptimizerRule + Send + Sync> =
570            (&ffi_rule).into();
571
572        struct ContextWithRegistry {
573            config: ConfigOptions,
574            registry: StatisticsRegistry,
575        }
576
577        impl PhysicalOptimizerContext for ContextWithRegistry {
578            fn config_options(&self) -> &ConfigOptions {
579                &self.config
580            }
581
582            fn statistics_registry(&self) -> Option<&StatisticsRegistry> {
583                Some(&self.registry)
584            }
585        }
586
587        let ctx = ContextWithRegistry {
588            config: ConfigOptions::new(),
589            registry: StatisticsRegistry::default_with_builtin_providers(),
590        };
591
592        let plan = create_test_plan();
593        // The optimize_with_context path works, but the registry is not
594        // available on the provider side (it will be None).
595        let optimized = foreign_rule.optimize_with_context(plan, &ctx)?;
596        assert_eq!(optimized.name(), "empty-exec");
597
598        Ok(())
599    }
600}