1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
//! This crate provides a compatibility layer for [Sea-ORM][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`]:
//!
//! ```rust
//! use async_ffi::BorrowingFfiFuture;
//! use sea_orm::DatabaseConnection;
//! use sea_orm_ffi::FfiConnection;
//! # use std::sync::Arc;
//! # async fn async_main() {
//!
//! // 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
//! # type Symbol<T> = T;
//! 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:
//!
//! ```rust
//! 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()`][sea_orm::TransactionTrait::begin]
//! function. This should be sufficient to provide transaction support - but not via
//! the [`TransactionTrait`]. While it would be possible to write the
//! [`transaction()`][sea_orm::TransactionTrait::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][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()`](refinery_core::Runner::run_async) with the connection.
//!
//! # Changelog
//!
//! Please see [releases] for a list of changes per release.
//!
//!
//! [`async-compat`]: async_compat
//! [`async-ffi`]: async_ffi
//! [`async-std`]: https://crates.io/crates/async-std
//! [`libloading`]: https://crates.io/crates/libloading
//! [`refinery`]: https://crates.io/crates/refinery
//! [`sea-orm`]: sea_orm
//! [`smol`]: https://crates.io/crates/smol
//! [`sqlx`]: sea_orm::sqlx
//! [`tokio`]: https://crates.io/crates/tokio
//!
//! [`AsyncMigrate`]: refinery_core::AsyncMigrate
//! [`ConnectionTrait`]: sea_orm::ConnectionTrait
//! [`DatabaseTransaction`]: sea_orm::DatabaseTransaction
//! [`Future`]: std::future::Future
//! [`TransactionTrait`]: sea_orm::TransactionTrait
//! [`Value`]: sea_orm::Value
//!
//! [issue]: https://codeberg.org/proto-x/sea-orm-ffi/issues
//! [pull request]: https://codeberg.org/proto-x/sea-orm-ffi/pulls
//! [releases]: https://codeberg.org/proto-x/sea-orm-ffi/releases
pub use ;