Skip to content

Commit a6586cc

Browse files
timsaucerahirner
andauthored
Example: FFI Table Provider as dynamic module loading (#13183)
* Initial commit for example on using FFI Table provider in rust as a module loading system * Add license text * formatting cargo.toml files * Correct typos in readme * Update documentation per PR feedback * Add additional documentation to example * Do not publish example * apply prettier * Add text describing async calls * Update datafusion-examples/examples/ffi/ffi_example_table_provider/Cargo.toml Co-authored-by: Alexander Hirner <[email protected]> --------- Co-authored-by: Alexander Hirner <[email protected]>
1 parent 4f82371 commit a6586cc

File tree

10 files changed

+432
-44
lines changed

10 files changed

+432
-44
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ members = [
4747
"datafusion/substrait",
4848
"datafusion/wasmtest",
4949
"datafusion-examples",
50+
"datafusion-examples/examples/ffi/ffi_example_table_provider",
51+
"datafusion-examples/examples/ffi/ffi_module_interface",
52+
"datafusion-examples/examples/ffi/ffi_module_loader",
5053
"test-utils",
5154
"benchmarks",
5255
]
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<!---
2+
Licensed to the Apache Software Foundation (ASF) under one
3+
or more contributor license agreements. See the NOTICE file
4+
distributed with this work for additional information
5+
regarding copyright ownership. The ASF licenses this file
6+
to you under the Apache License, Version 2.0 (the
7+
"License"); you may not use this file except in compliance
8+
with the License. You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing,
13+
software distributed under the License is distributed on an
14+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
KIND, either express or implied. See the License for the
16+
specific language governing permissions and limitations
17+
under the License.
18+
-->
19+
20+
# Example FFI Usage
21+
22+
The purpose of these crates is to provide an example of how one can use the
23+
DataFusion Foreign Function Interface (FFI). See [API Docs] for detailed
24+
usage.
25+
26+
This example is broken into three crates.
27+
28+
- `ffi_module_interface` is a common library to be shared by both the module
29+
to be loaded and the program that will load it. It defines how the module
30+
is to be structured.
31+
- `ffi_example_table_provider` creates a library to exposes the module.
32+
- `ffi_module_loader` is an example program that loads the module, gets data
33+
from it, and displays this data to the user.
34+
35+
## Building and running
36+
37+
In order for the program to run successfully, the module to be loaded must be
38+
built first. This example expects both the module and the program to be
39+
built using the same build mode (debug or release).
40+
41+
```shell
42+
cd ffi_example_table_provider
43+
cargo build
44+
cd ../ffi_module_loader
45+
cargo run
46+
```
47+
48+
[api docs]: http://docs.rs/datafusion-ffi/latest
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
18+
[package]
19+
name = "ffi_example_table_provider"
20+
version = "0.1.0"
21+
edition = { workspace = true }
22+
publish = false
23+
24+
[dependencies]
25+
abi_stable = "0.11.3"
26+
arrow = { workspace = true }
27+
arrow-array = { workspace = true }
28+
arrow-schema = { workspace = true }
29+
datafusion = { workspace = true }
30+
datafusion-ffi = { workspace = true }
31+
ffi_module_interface = { path = "../ffi_module_interface" }
32+
33+
[lib]
34+
name = "ffi_example_table_provider"
35+
crate-type = ["cdylib", 'rlib']
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
18+
use std::sync::Arc;
19+
20+
use abi_stable::{export_root_module, prefix_type::PrefixTypeTrait};
21+
use arrow_array::RecordBatch;
22+
use datafusion::{
23+
arrow::datatypes::{DataType, Field, Schema},
24+
common::record_batch,
25+
datasource::MemTable,
26+
};
27+
use datafusion_ffi::table_provider::FFI_TableProvider;
28+
use ffi_module_interface::{TableProviderModule, TableProviderModuleRef};
29+
30+
fn create_record_batch(start_value: i32, num_values: usize) -> RecordBatch {
31+
let end_value = start_value + num_values as i32;
32+
let a_vals: Vec<i32> = (start_value..end_value).collect();
33+
let b_vals: Vec<f64> = a_vals.iter().map(|v| *v as f64).collect();
34+
35+
record_batch!(("a", Int32, a_vals), ("b", Float64, b_vals)).unwrap()
36+
}
37+
38+
/// Here we only wish to create a simple table provider as an example.
39+
/// We create an in-memory table and convert it to it's FFI counterpart.
40+
extern "C" fn construct_simple_table_provider() -> FFI_TableProvider {
41+
let schema = Arc::new(Schema::new(vec![
42+
Field::new("a", DataType::Int32, true),
43+
Field::new("b", DataType::Float64, true),
44+
]));
45+
46+
// It is useful to create these as multiple record batches
47+
// so that we can demonstrate the FFI stream.
48+
let batches = vec![
49+
create_record_batch(1, 5),
50+
create_record_batch(6, 1),
51+
create_record_batch(7, 5),
52+
];
53+
54+
let table_provider = MemTable::try_new(schema, vec![batches]).unwrap();
55+
56+
FFI_TableProvider::new(Arc::new(table_provider), true)
57+
}
58+
59+
#[export_root_module]
60+
/// This defines the entry point for using the module.
61+
pub fn get_simple_memory_table() -> TableProviderModuleRef {
62+
TableProviderModule {
63+
create_table: construct_simple_table_provider,
64+
}
65+
.leak_into_prefix()
66+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
18+
[package]
19+
name = "ffi_module_interface"
20+
version = "0.1.0"
21+
edition = "2021"
22+
publish = false
23+
24+
[dependencies]
25+
abi_stable = "0.11.3"
26+
datafusion-ffi = { workspace = true }
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
18+
use abi_stable::{
19+
declare_root_module_statics,
20+
library::{LibraryError, RootModule},
21+
package_version_strings,
22+
sabi_types::VersionStrings,
23+
StableAbi,
24+
};
25+
use datafusion_ffi::table_provider::FFI_TableProvider;
26+
27+
#[repr(C)]
28+
#[derive(StableAbi)]
29+
#[sabi(kind(Prefix(prefix_ref = TableProviderModuleRef)))]
30+
/// This struct defines the module interfaces. It is to be shared by
31+
/// both the module loading program and library that implements the
32+
/// module. It is possible to move this definition into the loading
33+
/// program and reference it in the modules, but this example shows
34+
/// how a user may wish to separate these concerns.
35+
pub struct TableProviderModule {
36+
/// Constructs the table provider
37+
pub create_table: extern "C" fn() -> FFI_TableProvider,
38+
}
39+
40+
impl RootModule for TableProviderModuleRef {
41+
declare_root_module_statics! {TableProviderModuleRef}
42+
const BASE_NAME: &'static str = "ffi_example_table_provider";
43+
const NAME: &'static str = "ffi_example_table_provider";
44+
const VERSION_STRINGS: VersionStrings = package_version_strings!();
45+
46+
fn initialization(self) -> Result<Self, LibraryError> {
47+
Ok(self)
48+
}
49+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
18+
[package]
19+
name = "ffi_module_loader"
20+
version = "0.1.0"
21+
edition = "2021"
22+
publish = false
23+
24+
[dependencies]
25+
abi_stable = "0.11.3"
26+
datafusion = { workspace = true }
27+
datafusion-ffi = { workspace = true }
28+
ffi_module_interface = { path = "../ffi_module_interface" }
29+
tokio = { workspace = true, features = ["rt-multi-thread", "parking_lot"] }
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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+
18+
use std::sync::Arc;
19+
20+
use datafusion::{
21+
error::{DataFusionError, Result},
22+
prelude::SessionContext,
23+
};
24+
25+
use abi_stable::library::{development_utils::compute_library_path, RootModule};
26+
use datafusion_ffi::table_provider::ForeignTableProvider;
27+
use ffi_module_interface::TableProviderModuleRef;
28+
29+
#[tokio::main]
30+
async fn main() -> Result<()> {
31+
// Find the location of the library. This is specific to the build environment,
32+
// so you will need to change the approach here based on your use case.
33+
let target: &std::path::Path = "../../../../target/".as_ref();
34+
let library_path = compute_library_path::<TableProviderModuleRef>(target)
35+
.map_err(|e| DataFusionError::External(Box::new(e)))?;
36+
37+
// Load the module
38+
let table_provider_module =
39+
TableProviderModuleRef::load_from_directory(&library_path)
40+
.map_err(|e| DataFusionError::External(Box::new(e)))?;
41+
42+
// By calling the code below, the table provided will be created within
43+
// the module's code.
44+
let ffi_table_provider =
45+
table_provider_module
46+
.create_table()
47+
.ok_or(DataFusionError::NotImplemented(
48+
"External table provider failed to implement create_table".to_string(),
49+
))?();
50+
51+
// 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();
54+
55+
let ctx = SessionContext::new();
56+
57+
// Display the data to show the full cycle works.
58+
ctx.register_table("external_table", Arc::new(foreign_table_provider))?;
59+
let df = ctx.table("external_table").await?;
60+
df.show().await?;
61+
62+
Ok(())
63+
}

0 commit comments

Comments
 (0)