Skip to content

Commit 1cc9bcd

Browse files
FFI: return underlying trait type when converting from FFI structs (#18672)
## Which issue does this PR close? - Addresses part of #18671 but does not close it. - Minor: removes `return_type` in `FFI_ScalarUDF` since it will never be called due to the `return_field_from_args` implementation. ## Rationale for this change This PR adds the concept of a `library_marker_id` to the FFI crate. The reason for this is described in the `README` text as part of the PR. In our current use of the FFI library we get into issues where we round trip FFI structs back to their original library that now have FFI wrappers when they are no longer needed. ## What changes are included in this PR? - Adds a method to find the memory address of a static constant in the library. - Adds a check in methods that are creating Foreign FFI structs to see if we are actually in the local library or are actually foreign. - Replaces the `From<> for Foreign` to `From<> for Arc<dyn ...>`. The actual use case is essentially always to use these structs as their implementation of the trait they back. - Adds unit tests and a method to mock being in foreign code - Removed an unused function call in `FFI_ScalarUDF` ## Are these changes tested? - Code is tested against existing unit tests and with `datafusion-python`. - Added unit tests - Coverage report compared to `main`: <img width="1146" height="788" alt="coverage-report" src="https://github.com/user-attachments/assets/f422ee20-35d6-4a10-ae96-c38b59e26acb" /> ## Are there any user-facing changes? This does change the API for the FFI functions. --------- Co-authored-by: Renato Marroquin <[email protected]>
1 parent 710d14b commit 1cc9bcd

File tree

26 files changed

+924
-340
lines changed

26 files changed

+924
-340
lines changed

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

datafusion-examples/examples/ffi/ffi_module_loader/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,5 @@ publish = false
2424
[dependencies]
2525
abi_stable = "0.11.3"
2626
datafusion = { workspace = true }
27-
datafusion-ffi = { workspace = true }
2827
ffi_module_interface = { path = "../ffi_module_interface" }
2928
tokio = { workspace = true, features = ["rt-multi-thread", "parking_lot"] }

datafusion-examples/examples/ffi/ffi_module_loader/src/main.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use datafusion::{
2323
};
2424

2525
use abi_stable::library::{development_utils::compute_library_path, RootModule};
26-
use datafusion_ffi::table_provider::ForeignTableProvider;
26+
use datafusion::datasource::TableProvider;
2727
use ffi_module_interface::TableProviderModuleRef;
2828

2929
#[tokio::main]
@@ -49,13 +49,13 @@ async fn main() -> Result<()> {
4949
))?();
5050

5151
// In order to access the table provider within this executable, we need to
52-
// turn it into a `ForeignTableProvider`.
53-
let foreign_table_provider: ForeignTableProvider = (&ffi_table_provider).into();
52+
// turn it into a `TableProvider`.
53+
let foreign_table_provider: Arc<dyn TableProvider> = (&ffi_table_provider).into();
5454

5555
let ctx = SessionContext::new();
5656

5757
// Display the data to show the full cycle works.
58-
ctx.register_table("external_table", Arc::new(foreign_table_provider))?;
58+
ctx.register_table("external_table", foreign_table_provider)?;
5959
let df = ctx.table("external_table").await?;
6060
df.show().await?;
6161

datafusion/ffi/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ semver = "1.0.27"
5858
tokio = { workspace = true }
5959

6060
[dev-dependencies]
61+
datafusion = { workspace = true, default-features = false, features = ["sql"] }
6162
doc-comment = { workspace = true }
6263

6364
[features]

datafusion/ffi/README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,75 @@ In this crate we have a variety of structs which closely mimic the behavior of
101101
their internal counterparts. To see detailed notes about how to use them, see
102102
the example in `FFI_TableProvider`.
103103

104+
## Memory Management
105+
106+
One of the advantages of Rust is the ownership model, which means programmers
107+
_usually_ do not need to worry about memory management. When interacting with
108+
foreign code, this is not necessarily true. If you review the structures in
109+
this crate, you will find that many of them implement the `Drop` trait and
110+
perform a foreign call.
111+
112+
Suppose we have a `FFI_CatalogProvider`, for example. This struct is safe to
113+
pass across the FFI boundary, so it may be owned by either the library that
114+
produces the underlying `CatalogProvider` or by another library that consumes
115+
it. If we look closer at the `FFI_CatalogProvider`, it has a pointer to
116+
some private data. That private data is only accessible on the producer's
117+
side. If you attempt to access it on the consumer's side, you may get
118+
segmentation faults or other bad behavior. Within that private data is the
119+
actual `Arc<dyn CatalogProvider`. That `Arc<>` must be freed, but if the
120+
`FFI_CatalogProvider` is only owned on the consumer's side, we have no way
121+
to access the private data and free it.
122+
123+
To account for this, most structs in this crate have a `release` method that
124+
is used to clean up any privately held data. This calls into the producer's
125+
side, regardless of if it is called on either the local or foreign side.
126+
Most of the structs in this crate carry atomic reference counts to the
127+
underlying data, and this is straight forward. Some structs like the
128+
`FFI_Accumulator` contain an inner `Box<dyn Accumulator>`. The reason for
129+
this is that we need to be able to mutably access these based on the
130+
`Accumulator` trait definition. For these we have slightly more complicated
131+
release code based on whether it is being dropped on the local or foreign side.
132+
Traits that use a `Box<>` for their underlying data also cannot implement
133+
`Clone`.
134+
135+
## Library Marker ID
136+
137+
When reviewing the code, many of the structs in this crate contain a call to
138+
a `library_marker_id`. The purpose of this call is to determine if a library is
139+
accessing _local_ code through the FFI structs. Consider this example: you have
140+
a `primary` program that exposes functions to create a schema provider. You
141+
have a `secondary` library that exposes a function to create a catalog provider
142+
and the `secondary` library uses the schema provider of the `primary` program.
143+
From the point of view of the `secondary` library, the schema provider is
144+
foreign code.
145+
146+
Now when we register the `secondary` library with the `primary` program as a
147+
catalog provider and we make calls to get a schema, the `secondary` library
148+
will return a FFI wrapped schema provider back to the `primary` program. In
149+
this case that schema provider is actually local code to the `primary` program
150+
except that it is wrapped in the FFI code!
151+
152+
We work around this by the `library_marker_id` calls. What this does is it
153+
creates a global variable within each library and returns a `usize` address
154+
of that library. This is guaranteed to be unique for every library that contains
155+
FFI code. By comparing these `usize` addresses we can determine if a FFI struct
156+
is local or foreign.
157+
158+
In our example of the schema provider, if you were to make a call in your
159+
primary program to get the schema provider, it would reach out to the foreign
160+
catalog provider and send back a `FFI_SchemaProvider` object. By then
161+
comparing the `library_marker_id` of this object to the `primary` program, we
162+
determine it is local code. This means it is safe to access the underlying
163+
private data.
164+
165+
Users of the FFI code should not need to access these function. If you are
166+
implementing a new FFI struct, then it is recommended that you follow the
167+
established patterns for converting from FFI struct into the underlying
168+
traits. Specifically you should use `crate::get_library_marker_id` and in
169+
your unit tests you should override this with
170+
`crate::mock_foreign_marker_id` to force your test to create the foreign
171+
variant of your struct.
172+
104173
[apache datafusion]: https://datafusion.apache.org/
105174
[api docs]: http://docs.rs/datafusion-ffi/latest
106175
[rust abi]: https://doc.rust-lang.org/reference/abi.html

datafusion/ffi/src/catalog_provider.rs

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ pub struct FFI_CatalogProvider {
7070
/// Internal data. This is only to be accessed by the provider of the plan.
7171
/// A [`ForeignCatalogProvider`] should never attempt to access this data.
7272
pub private_data: *mut c_void,
73+
74+
/// Utility to identify when FFI objects are accessed locally through
75+
/// the foreign interface. See [`crate::get_library_marker_id`] and
76+
/// the crate's `README.md` for more information.
77+
pub library_marker_id: extern "C" fn() -> usize,
7378
}
7479

7580
unsafe impl Send for FFI_CatalogProvider {}
@@ -116,7 +121,7 @@ unsafe extern "C" fn register_schema_fn_wrapper(
116121
) -> RResult<ROption<FFI_SchemaProvider>, RString> {
117122
let runtime = provider.runtime();
118123
let provider = provider.inner();
119-
let schema = Arc::new(ForeignSchemaProvider::from(schema));
124+
let schema: Arc<dyn SchemaProvider + Send> = schema.into();
120125

121126
let returned_schema =
122127
rresult_return!(provider.register_schema(name.as_str(), schema))
@@ -169,6 +174,7 @@ unsafe extern "C" fn clone_fn_wrapper(
169174
release: release_fn_wrapper,
170175
version: super::version,
171176
private_data,
177+
library_marker_id: crate::get_library_marker_id,
172178
}
173179
}
174180

@@ -195,6 +201,7 @@ impl FFI_CatalogProvider {
195201
release: release_fn_wrapper,
196202
version: super::version,
197203
private_data: Box::into_raw(private_data) as *mut c_void,
204+
library_marker_id: crate::get_library_marker_id,
198205
}
199206
}
200207
}
@@ -209,9 +216,14 @@ pub struct ForeignCatalogProvider(pub(crate) FFI_CatalogProvider);
209216
unsafe impl Send for ForeignCatalogProvider {}
210217
unsafe impl Sync for ForeignCatalogProvider {}
211218

212-
impl From<&FFI_CatalogProvider> for ForeignCatalogProvider {
219+
impl From<&FFI_CatalogProvider> for Arc<dyn CatalogProvider + Send> {
213220
fn from(provider: &FFI_CatalogProvider) -> Self {
214-
Self(provider.clone())
221+
if (provider.library_marker_id)() == crate::get_library_marker_id() {
222+
return Arc::clone(unsafe { provider.inner() });
223+
}
224+
225+
Arc::new(ForeignCatalogProvider(provider.clone()))
226+
as Arc<dyn CatalogProvider + Send>
215227
}
216228
}
217229

@@ -298,9 +310,10 @@ mod tests {
298310
.unwrap()
299311
.is_none());
300312

301-
let ffi_catalog = FFI_CatalogProvider::new(catalog, None);
313+
let mut ffi_catalog = FFI_CatalogProvider::new(catalog, None);
314+
ffi_catalog.library_marker_id = crate::mock_foreign_marker_id;
302315

303-
let foreign_catalog: ForeignCatalogProvider = (&ffi_catalog).into();
316+
let foreign_catalog: Arc<dyn CatalogProvider + Send> = (&ffi_catalog).into();
304317

305318
let prior_schema_names = foreign_catalog.schema_names();
306319
assert_eq!(prior_schema_names.len(), 1);
@@ -335,4 +348,26 @@ mod tests {
335348
let returned_schema = foreign_catalog.schema("second_schema");
336349
assert!(returned_schema.is_some());
337350
}
351+
352+
#[test]
353+
fn test_ffi_catalog_provider_local_bypass() {
354+
let catalog = Arc::new(MemoryCatalogProvider::new());
355+
356+
let mut ffi_catalog = FFI_CatalogProvider::new(catalog, None);
357+
358+
// Verify local libraries can be downcast to their original
359+
let foreign_catalog: Arc<dyn CatalogProvider + Send> = (&ffi_catalog).into();
360+
assert!(foreign_catalog
361+
.as_any()
362+
.downcast_ref::<MemoryCatalogProvider>()
363+
.is_some());
364+
365+
// Verify different library markers generate foreign providers
366+
ffi_catalog.library_marker_id = crate::mock_foreign_marker_id;
367+
let foreign_catalog: Arc<dyn CatalogProvider + Send> = (&ffi_catalog).into();
368+
assert!(foreign_catalog
369+
.as_any()
370+
.downcast_ref::<ForeignCatalogProvider>()
371+
.is_some());
372+
}
338373
}

datafusion/ffi/src/catalog_provider_list.rs

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ pub struct FFI_CatalogProviderList {
5858
/// Internal data. This is only to be accessed by the provider of the plan.
5959
/// A [`ForeignCatalogProviderList`] should never attempt to access this data.
6060
pub private_data: *mut c_void,
61+
62+
/// Utility to identify when FFI objects are accessed locally through
63+
/// the foreign interface. See [`crate::get_library_marker_id`] and
64+
/// the crate's `README.md` for more information.
65+
pub library_marker_id: extern "C" fn() -> usize,
6166
}
6267

6368
unsafe impl Send for FFI_CatalogProviderList {}
@@ -94,7 +99,7 @@ unsafe extern "C" fn register_catalog_fn_wrapper(
9499
) -> ROption<FFI_CatalogProvider> {
95100
let runtime = provider.runtime();
96101
let provider = provider.inner();
97-
let catalog = Arc::new(ForeignCatalogProvider::from(catalog));
102+
let catalog: Arc<dyn CatalogProvider + Send> = catalog.into();
98103

99104
provider
100105
.register_catalog(name.into(), catalog)
@@ -138,6 +143,7 @@ unsafe extern "C" fn clone_fn_wrapper(
138143
release: release_fn_wrapper,
139144
version: super::version,
140145
private_data,
146+
library_marker_id: crate::get_library_marker_id,
141147
}
142148
}
143149

@@ -163,6 +169,7 @@ impl FFI_CatalogProviderList {
163169
release: release_fn_wrapper,
164170
version: super::version,
165171
private_data: Box::into_raw(private_data) as *mut c_void,
172+
library_marker_id: crate::get_library_marker_id,
166173
}
167174
}
168175
}
@@ -177,9 +184,14 @@ pub struct ForeignCatalogProviderList(FFI_CatalogProviderList);
177184
unsafe impl Send for ForeignCatalogProviderList {}
178185
unsafe impl Sync for ForeignCatalogProviderList {}
179186

180-
impl From<&FFI_CatalogProviderList> for ForeignCatalogProviderList {
187+
impl From<&FFI_CatalogProviderList> for Arc<dyn CatalogProviderList + Send> {
181188
fn from(provider: &FFI_CatalogProviderList) -> Self {
182-
Self(provider.clone())
189+
if (provider.library_marker_id)() == crate::get_library_marker_id() {
190+
return Arc::clone(unsafe { provider.inner() });
191+
}
192+
193+
Arc::new(ForeignCatalogProviderList(provider.clone()))
194+
as Arc<dyn CatalogProviderList + Send>
183195
}
184196
}
185197

@@ -248,9 +260,11 @@ mod tests {
248260
.register_catalog("prior_catalog".to_owned(), prior_catalog)
249261
.is_none());
250262

251-
let ffi_catalog_list = FFI_CatalogProviderList::new(catalog_list, None);
263+
let mut ffi_catalog_list = FFI_CatalogProviderList::new(catalog_list, None);
264+
ffi_catalog_list.library_marker_id = crate::mock_foreign_marker_id;
252265

253-
let foreign_catalog_list: ForeignCatalogProviderList = (&ffi_catalog_list).into();
266+
let foreign_catalog_list: Arc<dyn CatalogProviderList + Send> =
267+
(&ffi_catalog_list).into();
254268

255269
let prior_catalog_names = foreign_catalog_list.catalog_names();
256270
assert_eq!(prior_catalog_names.len(), 1);
@@ -280,4 +294,28 @@ mod tests {
280294
let returned_catalog = foreign_catalog_list.catalog("second_catalog");
281295
assert!(returned_catalog.is_some());
282296
}
297+
298+
#[test]
299+
fn test_ffi_catalog_provider_list_local_bypass() {
300+
let catalog_list = Arc::new(MemoryCatalogProviderList::new());
301+
302+
let mut ffi_catalog_list = FFI_CatalogProviderList::new(catalog_list, None);
303+
304+
// Verify local libraries can be downcast to their original
305+
let foreign_catalog_list: Arc<dyn CatalogProviderList + Send> =
306+
(&ffi_catalog_list).into();
307+
assert!(foreign_catalog_list
308+
.as_any()
309+
.downcast_ref::<MemoryCatalogProviderList>()
310+
.is_some());
311+
312+
// Verify different library markers generate foreign providers
313+
ffi_catalog_list.library_marker_id = crate::mock_foreign_marker_id;
314+
let foreign_catalog_list: Arc<dyn CatalogProviderList + Send> =
315+
(&ffi_catalog_list).into();
316+
assert!(foreign_catalog_list
317+
.as_any()
318+
.downcast_ref::<ForeignCatalogProviderList>()
319+
.is_some());
320+
}
283321
}

0 commit comments

Comments
 (0)