Expand description
This crate provides a compatibility layer for Sea-ORM when crossing a Rust-to-Rust FFI boundary.
§Example
On the host side, you first create your database connection, which we can then
convert into an FfiConnection:
use async_ffi::BorrowingFfiFuture;
use sea_orm::DatabaseConnection;
use sea_orm_ffi::FfiConnection;
// See Sea-ORM's documentation on how to obtain a database connection
let conn: DatabaseConnection = todo!();
// Create FFI-safe connection wrapper.
// While this can be used on the host side as well, it is recommended you avoid this
// as it will likely be less performant.
// Therefore, you might want to wrap `conn` in an `Arc` to share it with the host.
let ffi_conn = FfiConnection::new(Box::new(conn));
// Obtain the plugin function that needs the database connection via libloading or similar
let plugin_function: Symbol<
extern "C" fn(&FfiConnection) -> BorrowingFfiFuture<'_, ()>
> = todo!();
// Call the plugin function
plugin_function(&ffi_conn).await;On the plugin side, you can treat FfiConnection like any other database connection:
mod comment {
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, DeriveEntityModel)]
#[sea_orm(table_name = "comment")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
pub author: String,
pub comment: String
}
#[derive(Debug, DeriveRelation, EnumIter)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
}
use comment::Entity as Comment;
use async_ffi::{BorrowingFfiFuture, FutureExt as _};
use sea_orm::EntityTrait as _;
use sea_orm_ffi::FfiConnection;
async fn print_comments(conn: &FfiConnection) {
let Ok(comments) = Comment::find().all(conn).await else {
eprintln!("Failed to load comments");
return;
};
for comment in comments {
println!("{comment:?}");
}
}
#[no_mangle]
extern "C" fn plugin_function(conn: &FfiConnection) -> BorrowingFfiFuture<'_, ()> {
print_comments(conn).into_ffi()
}§How it works
Sea-ORM relies on two main traits for its database connection: ConnectionTrait
and TransactionTrait. The former one is really nice because it is dyn
compatible. This is also sometimes called object safe.
What that means is that we build pointers to the trait without knowing the specific
type. For example, we can write &dyn ConnectionTrait or Box<dyn ConnectionTrait>.
In order to pass things via an FFI boundary, we need a type with a stable ABI. ABI
stands for Application Binary Interface, and unfortunately Rust’s default ABI is not
guaranteed to be stable. We must therefore use another, stable ABI. Otherwise, it
would be inherently unsafe to load plugins compiled with a different Rust compiler
than the plugin host. Luckily, we can use a pointer to Box<dyn ConnectionTrait> to
pass it through an FFI boundary.
The only problem is that that pointer is not ABI stable, so the plugin must treat
this pointer as opaque, and only pass its value back to the plugin host. We
therefore create C-ABI functions in the plugin host that call the functions from
ConnectionTrait and pass those function pointers to the plugin alongside the
connection pointer. The plugin can then pass the connection pointer to these function
pointers to call the various functions.
But what about function arguments? And return types? Good question!
The most complicated type here is definitely Future. Luckily, the problem of
using async code with Rust-to-Rust FFI has been solved by async-ffi. Well,
almost. Because tokio uses some thread-local storage unavailable to plugins
loaded with libloading, any attempt to use tokio-specific futures will panic.
The only runtimes supported by sea-orm/sqlx in the current release are
async-std, which is deprecated, and tokio. We therefore rely on
async-compat to inject a tokio runtime, regardless of the executor that polls
the futures. In the future, when sqlx is released with support for smol,
this can and will be removed.
The other types are fairly easy to bridge accross the FFI boundary. But, since none
of them are ABI stable by themselves, we have to convert them to and from ABI stable
types first. This is made possible by the proxy feature of sea-orm that allows
constructing our own database responses. We cannot use opaque pointers here as those
types are designed to be created on one side and read on the other side of the FFI
boundary.
But what about the TransactionTrait? Well, this one is unfortunately not dyn
compatible, which is the unfortunate side effect of it having a function that
accepts generic parameters. However, we can still bridge the functions that do not
take the generic parameters - namely the begin()
function. This should be sufficient to provide transaction support - but not via
the TransactionTrait. While it would be possible to write the
transaction() method on the plugin side,
there is no way to create a DatabaseTransaction from any ABI stable type. Instead,
the FfiConnection::begin() function returns a FfiTransaction, which again
implements ConnectionTrait, so you can use it on the plugin side just like you
would use DatabaseTransaction on the host side.
§Features
Sea-ORM makes heavy usage of features, including for the Value enum
which is one of the types bridged by this crate. Therefore, all features that enable
types must not only be enable for sea-orm but also for this crate. At the time
of writing, sea-orm supports more types than we do. Feel free to open an issue
or pull request if you are missing a feature.
Note that there will not be a compile error when there is a feature mismatch. Instead, any attempt to use types unsupported by this crate will result in a panic at runtime.
§Migrations
Additionally, we have a refinery feature that enables support for migrations using
the refinery crate. When this feature is enabled, FfiConnection implements
AsyncMigrate so you can call
Runner::run_async() with the connection.
§Changelog
Please see releases for a list of changes per release.
Structs§
- FfiConnection
- An FFI-safe implementation of
sea-orm’sConnectionTrait. - FfiTransaction
- An FFI-safe implementation of
sea-orm’sDatabaseTransaction.