blob: e1321107a8525677122a4b1c70a9883ed07d590f [file] [view]
<!---
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
(extension_why_ffi)=
# Why FFI
The DataFusion in Python project is designed to allow users to extend its
functionality in a few core areas. Ideally many users would like to package
their extensions as a Python package and easily integrate that package with
this project. This page describes the problem that makes the obvious approach
fail, and the approach this project uses instead.
## The primary issue
Suppose you wish to use DataFusion and you have a custom data source that can
produce tables that can then be queried against, similar to how you can
register a {ref}`CSV <io_csv>` or {ref}`Parquet <io_parquet>` file. In
DataFusion terminology, you likely want to implement a
{ref}`Custom Table Provider <io_custom_table_provider>`. In an effort to make
your data source as performant as possible and to utilize the features of
DataFusion, you may decide to write your source in Rust and then expose it
through [PyO3](https://pyo3.rs) as a Python library.
At first glance, it may appear the best way to do this is to add the
`datafusion-python` crate as a dependency, provide a `PyTable`, and then to
register it with the `SessionContext`. Unfortunately, this will not work.
When you produce your code as a Python library and it needs to interact with
the DataFusion library, at the lowest level they communicate through an
Application Binary Interface (ABI). The acronym sounds similar to API
(Application Programming Interface), but it is distinctly different.
The ABI sets the standard for how these libraries can share data and functions
between each other. One of the key differences between Rust and other
programming languages is that Rust does not have a stable ABI. What this means
in practice is that if you compile a Rust library with one version of the
`rustc` compiler and I compile another library to interface with it but I use a
different version of the compiler, there is no guarantee the interface will be
the same.
In practice, this means that a Python library built with `datafusion-python` as
a Rust dependency will generally **not** be compatible with the DataFusion
Python package, even if they reference the same version of
`datafusion-python`. If you attempt to do this, it may work on your local
computer if you have built both packages with the same optimizations. This can
sometimes lead to a false expectation that the code will work, but it
frequently breaks the moment you try to use your package against the released
packages.
You can find more information about the Rust ABI in their
[online documentation](https://doc.rust-lang.org/reference/abi.html).
## The FFI approach
Rust supports interacting with other programming languages through its Foreign
Function Interface (FFI). The advantage of using the FFI is that it enables you
to write data structures and functions that have a stable ABI. That allows you
to use Rust code with C, Python, and other languages. In fact, the
[PyO3](https://pyo3.rs) library uses the FFI to share data and functions
between Python and Rust.
The approach we are taking in the DataFusion in Python project is to
incrementally expose more portions of the DataFusion project via FFI
interfaces. This allows users to write Rust code that does **not** require the
`datafusion-python` crate as a dependency, expose their code in Python via
PyO3, and have it interact with the DataFusion Python package.
Early adopters of this approach include
[delta-rs](https://delta-io.github.io/delta-rs/) who has adapted their Table
Provider for use in `datafusion-python` with only a few lines of code. Also,
the DataFusion Python project uses the existing definitions from
[Apache Arrow CStream Interface](https://arrow.apache.org/docs/format/CStreamInterface.html)
to support importing **and** exporting tables. Any Python package that supports
reading the Arrow C Stream interface can work with DataFusion Python out of the
box! You can read more about working with Arrow sources in the
{ref}`Data Sources <user_guide_data_sources>` page.
To learn more about the Foreign Function Interface in Rust, the
[Rustonomicon](https://doc.rust-lang.org/nomicon/ffi.html) is a good resource.
## Inspiration from Arrow
DataFusion is built upon [Apache Arrow](https://arrow.apache.org/). The
canonical Python Arrow implementation,
[pyarrow](https://arrow.apache.org/docs/python/index.html), provides an
excellent way to share Arrow data between Python projects without performing
any copy operations on the data, using a well defined set of interfaces — see
their [stream interface](https://arrow.apache.org/docs/format/CStreamInterface.html).
The [Rust Arrow implementation](https://github.com/apache/arrow-rs) also
supports these `C` style definitions via the Foreign Function Interface. Beyond
transferring data, `pyarrow` goes one step further and makes the interfaces
themselves easy to share in Python, by exposing PyCapsules that contain the
expected functionality.
Two lessons we leverage from the Arrow project in DataFusion Python are:
- We reuse the existing Arrow FFI functionality wherever possible.
- We expose PyCapsules that contain an FFI stable struct.
You can learn more about PyCapsules from the official
[Python online documentation](https://docs.python.org/3/c-api/capsule.html).
PyCapsules have excellent support in PyO3 already; the
[PyO3 online documentation](https://pyo3.rs/main/doc/pyo3/types/struct.pycapsule)
is a good source for more details on using PyCapsules in Rust.
## If FFI does not yet cover what you need
:::{note}
Suppose you needed to expose some other features of DataFusion and you could
not wait for the upstream repository to implement the FFI approach we describe.
In this case you decide to create your dependency on the `datafusion-python`
crate instead.
As we discussed, this is not guaranteed to work across different compiler
versions and optimization levels. If you wish to go down this route, there are
two approaches we have identified you can use.
1. Re-export all of `datafusion-python` yourself with your extensions built in.
2. Carefully synchronize your software releases with the `datafusion-python` CI
build system so that your libraries use the exact same compiler, features,
and optimization level.
We currently do not recommend either of these approaches as they are difficult
to maintain over a long period. Additionally, they require a tight version
coupling between libraries.
The better path is to open an issue describing what you need exposed. The FFI
surface in the [datafusion-ffi] crate grows in response to these.
:::
[datafusion-ffi]: https://crates.io/crates/datafusion-ffi