Skip to main content

Crate sea_orm_ffi

Crate sea_orm_ffi 

Source
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’s ConnectionTrait.
FfiTransaction
An FFI-safe implementation of sea-orm’s DatabaseTransaction.