sea_orm_ffi/lib.rs
1//! This crate provides a compatibility layer for [Sea-ORM][sea_orm] when crossing a
2//! Rust-to-Rust FFI boundary.
3//!
4//! # Example
5//!
6//! On the host side, you first create your database connection, which we can then
7//! convert into an [`FfiConnection`]:
8//!
9//! ```rust
10//! use async_ffi::BorrowingFfiFuture;
11//! use sea_orm::DatabaseConnection;
12//! use sea_orm_ffi::FfiConnection;
13//! # use std::sync::Arc;
14//! # async fn async_main() {
15//!
16//! // See Sea-ORM's documentation on how to obtain a database connection
17//! let conn: DatabaseConnection = todo!();
18//!
19//! // Create FFI-safe connection wrapper.
20//! // While this can be used on the host side as well, it is recommended you avoid this
21//! // as it will likely be less performant.
22//! // Therefore, you might want to wrap `conn` in an `Arc` to share it with the host.
23//! let ffi_conn = FfiConnection::new(Box::new(conn));
24//!
25//! // Obtain the plugin function that needs the database connection via libloading or similar
26//! # type Symbol<T> = T;
27//! let plugin_function: Symbol<
28//! extern "C" fn(&FfiConnection) -> BorrowingFfiFuture<'_, ()>
29//! > = todo!();
30//!
31//! // Call the plugin function
32//! plugin_function(&ffi_conn).await;
33//! # }
34//! ```
35//!
36//! On the plugin side, you can treat [`FfiConnection`] like any other database connection:
37//!
38//! ```rust
39//! mod comment {
40//! use sea_orm::entity::prelude::*;
41//!
42//! #[derive(Clone, Debug, DeriveEntityModel)]
43//! #[sea_orm(table_name = "comment")]
44//! pub struct Model {
45//! #[sea_orm(primary_key)]
46//! pub id: i64,
47//! pub author: String,
48//! pub comment: String
49//! }
50//!
51//! #[derive(Debug, DeriveRelation, EnumIter)]
52//! pub enum Relation {}
53//!
54//! impl ActiveModelBehavior for ActiveModel {}
55//! }
56//! use comment::Entity as Comment;
57//!
58//! use async_ffi::{BorrowingFfiFuture, FutureExt as _};
59//! use sea_orm::EntityTrait as _;
60//! use sea_orm_ffi::FfiConnection;
61//!
62//! async fn print_comments(conn: &FfiConnection) {
63//! let Ok(comments) = Comment::find().all(conn).await else {
64//! eprintln!("Failed to load comments");
65//! return;
66//! };
67//! for comment in comments {
68//! println!("{comment:?}");
69//! }
70//! }
71//!
72//! #[no_mangle]
73//! extern "C" fn plugin_function(conn: &FfiConnection) -> BorrowingFfiFuture<'_, ()> {
74//! print_comments(conn).into_ffi()
75//! }
76//! ```
77//!
78//! # How it works
79//!
80//! Sea-ORM relies on two main traits for its database connection: [`ConnectionTrait`]
81//! and [`TransactionTrait`]. The former one is really nice because it is _`dyn`
82//! compatible_. This is also sometimes called _object safe_.
83//!
84//! What that means is that we build pointers to the trait without knowing the specific
85//! type. For example, we can write `&dyn ConnectionTrait` or `Box<dyn ConnectionTrait>`.
86//! In order to pass things via an FFI boundary, we need a type with a stable ABI. ABI
87//! stands for Application Binary Interface, and unfortunately Rust's default ABI is not
88//! guaranteed to be stable. We must therefore use another, stable ABI. Otherwise, it
89//! would be inherently unsafe to load plugins compiled with a different Rust compiler
90//! than the plugin host. Luckily, we can use a pointer to `Box<dyn ConnectionTrait>` to
91//! pass it through an FFI boundary.
92//!
93//! The only problem is that that pointer is not ABI stable, so the plugin must treat
94//! this pointer as _opaque_, and only pass its value back to the plugin host. We
95//! therefore create C-ABI functions in the plugin host that call the functions from
96//! [`ConnectionTrait`] and pass those function pointers to the plugin alongside the
97//! connection pointer. The plugin can then pass the connection pointer to these function
98//! pointers to call the various functions.
99//!
100//! But what about function arguments? And return types? Good question!
101//!
102//! The most complicated type here is definitely [`Future`]. Luckily, the problem of
103//! using async code with Rust-to-Rust FFI has been solved by [`async-ffi`]. Well,
104//! almost. Because [`tokio`] uses some thread-local storage unavailable to plugins
105//! loaded with [`libloading`], any attempt to use tokio-specific futures will panic.
106//! The only runtimes supported by [`sea-orm`]/[`sqlx`] in the current release are
107//! [`async-std`], which is deprecated, and [`tokio`]. We therefore rely on
108//! [`async-compat`] to inject a tokio runtime, regardless of the executor that polls
109//! the futures. In the future, when [`sqlx`] is released with support for [`smol`],
110//! this can and will be removed.
111//!
112//! The other types are fairly easy to bridge accross the FFI boundary. But, since none
113//! of them are ABI stable by themselves, we have to convert them to and from ABI stable
114//! types first. This is made possible by the `proxy` feature of [`sea-orm`] that allows
115//! constructing our own database responses. We cannot use opaque pointers here as those
116//! types are designed to be created on one side and read on the other side of the FFI
117//! boundary.
118//!
119//! But what about the [`TransactionTrait`]? Well, this one is unfortunately not _`dyn`
120//! compatible_, which is the unfortunate side effect of it having a function that
121//! accepts generic parameters. However, we can still bridge the functions that do not
122//! take the generic parameters - namely the [`begin()`][sea_orm::TransactionTrait::begin]
123//! function. This should be sufficient to provide transaction support - but not via
124//! the [`TransactionTrait`]. While it would be possible to write the
125//! [`transaction()`][sea_orm::TransactionTrait::transaction] method on the plugin side,
126//! there is no way to create a [`DatabaseTransaction`] from any ABI stable type. Instead,
127//! the [`FfiConnection::begin()`] function returns a [`FfiTransaction`], which again
128//! implements [`ConnectionTrait`], so you can use it on the plugin side just like you
129//! would use [`DatabaseTransaction`] on the host side.
130//!
131//! # Features
132//!
133//! [Sea-ORM][sea_orm] makes heavy usage of features, including for the [`Value`] enum
134//! which is one of the types bridged by this crate. Therefore, all features that enable
135//! types must not only be enable for [`sea-orm`] but also for this crate. At the time
136//! of writing, [`sea-orm`] supports more types than we do. Feel free to open an [issue]
137//! or [pull request] if you are missing a feature.
138//!
139//! Note that there will not be a compile error when there is a feature mismatch. Instead,
140//! any attempt to use types unsupported by this crate will result in a panic at runtime.
141//!
142//! ## Migrations
143//!
144//! Additionally, we have a `refinery` feature that enables support for migrations using
145//! the [`refinery`] crate. When this feature is enabled, [`FfiConnection`] implements
146//! [`AsyncMigrate`] so you can call
147//! [`Runner::run_async()`](refinery_core::Runner::run_async) with the connection.
148//!
149//! # Changelog
150//!
151//! Please see [releases] for a list of changes per release.
152//!
153//!
154//! [`async-compat`]: async_compat
155//! [`async-ffi`]: async_ffi
156//! [`async-std`]: https://crates.io/crates/async-std
157//! [`libloading`]: https://crates.io/crates/libloading
158//! [`refinery`]: https://crates.io/crates/refinery
159//! [`sea-orm`]: sea_orm
160//! [`smol`]: https://crates.io/crates/smol
161//! [`sqlx`]: sea_orm::sqlx
162//! [`tokio`]: https://crates.io/crates/tokio
163//!
164//! [`AsyncMigrate`]: refinery_core::AsyncMigrate
165//! [`ConnectionTrait`]: sea_orm::ConnectionTrait
166//! [`DatabaseTransaction`]: sea_orm::DatabaseTransaction
167//! [`Future`]: std::future::Future
168//! [`TransactionTrait`]: sea_orm::TransactionTrait
169//! [`Value`]: sea_orm::Value
170//!
171//! [issue]: https://codeberg.org/proto-x/sea-orm-ffi/issues
172//! [pull request]: https://codeberg.org/proto-x/sea-orm-ffi/pulls
173//! [releases]: https://codeberg.org/proto-x/sea-orm-ffi/releases
174
175mod backend;
176mod connection;
177mod db_err;
178mod exec_result;
179#[cfg(feature = "refinery")]
180mod migrate;
181mod option;
182mod proxy_row;
183mod result;
184mod statement;
185mod string;
186mod time;
187mod transaction;
188mod value;
189mod vec;
190
191pub use self::{connection::FfiConnection, transaction::FfiTransaction};