sea-orm-ffi 0.1.3

Compatibility layer for Sea-ORM when crossing a Rust-to-Rust FFI boundary
Documentation
use crate::{
	backend::FfiBackend,
	connection::{
		ConnectionVTable, execute_impl, execute_unprepared_impl, query_all_impl,
		query_one_impl
	},
	db_err::FfiDbErr,
	exec_result::FfiExecResult,
	proxy_row::{FfiOptionalProxyRow, FfiProxyRow},
	result::FfiResult,
	statement::FfiStatement,
	string::StringPtr,
	vec::VecPtr
};
use async_compat::Compat;
use async_ffi::{BorrowingFfiFuture, FfiFuture, FutureExt};
use async_trait::async_trait;
use futures_util::future::FutureExt as _;
use sea_orm::{
	ConnectionTrait, DatabaseBackend, DatabaseTransaction, DbErr, ExecResult,
	QueryResult, Statement
};
use std::{ffi::c_void, future::Future, mem::ManuallyDrop};

/// Common safety documentation for [`TransactionPtr`] functions.
macro_rules! doc_safety {
	() => {
		concat!(
			"\n",
			"# Safety\n",
			"\n",
			"This function is only safe to be called on the same side of the FFI ",
			"boundary that was used to construct the connection pointer using the ",
			"[`new`](TransactionPtr::new) function, or any binary that was compiled ",
			"using the exact same dependencies and the exact same compiler and linker."
		)
	};
}

/// A pointer to [`DatabaseTransaction`].
///
/// This pointer must be treated as opaque on the side of the FFI boundary that did not
/// create the pointer.
#[repr(transparent)]
struct TransactionPtr(*mut c_void);

impl TransactionPtr {
	fn new(inner: Box<Compat<DatabaseTransaction>>) -> Self {
		Self(Box::into_raw(inner) as _)
	}

	/// Access the data stored in this pointer
	#[doc = doc_safety!()]
	unsafe fn get(&self) -> &DatabaseTransaction {
		let ptr: *mut Compat<DatabaseTransaction> = self.0.cast();
		(*ptr).get_ref()
	}

	/// Return the inner type this pointer points to.
	#[doc = doc_safety!()]
	#[allow(clippy::wrong_self_convention)] // well, `Drop` only gives `&mut Self`
	unsafe fn into_inner(&mut self) -> Box<Compat<DatabaseTransaction>> {
		let ptr: *mut Compat<DatabaseTransaction> = self.0.cast();
		Box::from_raw(ptr)
	}
}

/// The FFI callback type that is called when an [`FfiTransaction`] is [drop]ped.
type DropTransaction = extern "C" fn(&mut TransactionPtr);

/// Drop the transaction that this pointer points to.
#[doc = doc_safety!()]
extern "C" fn drop_transaction(ptr: &mut TransactionPtr) {
	drop(unsafe { ptr.into_inner() });
}

/// The FFI callback type that is called when
/// [`commit()`](FfiTransaction::commit)
/// is called on [`FfiTransaction`].
type CommitTransaction =
	extern "C" fn(&mut TransactionPtr) -> FfiFuture<FfiResult<(), FfiDbErr>>;

/// FFI callback for [`commit()`](FfiTransaction::commit).
#[doc = doc_safety!()]
extern "C" fn commit_transaction(
	ptr: &mut TransactionPtr
) -> FfiFuture<FfiResult<(), FfiDbErr>> {
	let conn = unsafe { ptr.into_inner() };
	Compat::new(
		async move { conn.into_inner().commit().await.map_err(Into::into).into() }
	)
	.into_ffi()
}

/// FFI callback for [`rollback()`](FfiTransaction::rollback).
#[doc = doc_safety!()]
extern "C" fn rollback_transaction(
	ptr: &mut TransactionPtr
) -> FfiFuture<FfiResult<(), FfiDbErr>> {
	let conn = unsafe { ptr.into_inner() };
	Compat::new(async move {
		conn.into_inner()
			.rollback()
			.await
			.map_err(Into::into)
			.into()
	})
	.into_ffi()
}

/// FFI callback for [`get_database_backend()`](ConnectionTrait::get_database_backend).
#[doc = doc_safety!()]
extern "C" fn get_database_backend(ptr: &TransactionPtr) -> FfiBackend {
	unsafe { ptr.get() }.get_database_backend().into()
}

/// FFI callback for [`execute()`](ConnectionTrait::execute).
#[doc = doc_safety!()]
extern "C" fn execute(
	ptr: &TransactionPtr,
	stmt: FfiStatement
) -> BorrowingFfiFuture<'_, FfiResult<FfiExecResult, FfiDbErr>> {
	execute_impl(unsafe { ptr.get() }, stmt)
}

/// FFI callback for [`execute_unprepared()`](ConnectionTrait::execute_unprepared).
#[doc = doc_safety!()]
extern "C" fn execute_unprepared(
	ptr: &TransactionPtr,
	sql: StringPtr
) -> BorrowingFfiFuture<'_, FfiResult<FfiExecResult, FfiDbErr>> {
	execute_unprepared_impl(unsafe { ptr.get() }, sql)
}

/// FFI callback for [`query_one()`](ConnectionTrait::query_one).
#[doc = doc_safety!()]
extern "C" fn query_one(
	ptr: &TransactionPtr,
	stmt: FfiStatement
) -> BorrowingFfiFuture<'_, FfiResult<FfiOptionalProxyRow, FfiDbErr>> {
	query_one_impl(unsafe { ptr.get() }, stmt)
}

/// FFI callback for [`query_all()`](ConnectionTrait::query_all).
#[doc = doc_safety!()]
extern "C" fn query_all(
	ptr: &TransactionPtr,
	stmt: FfiStatement
) -> BorrowingFfiFuture<'_, FfiResult<VecPtr<FfiProxyRow>, FfiDbErr>> {
	query_all_impl(unsafe { ptr.get() }, stmt)
}

/// FFI callback for [`is_mock_connection()`](ConnectionTrait::is_mock_connection).
#[doc = doc_safety!()]
extern "C" fn is_mock_connection(ptr: &TransactionPtr) -> bool {
	unsafe { ptr.get() }.is_mock_connection()
}

/// An FFI-safe implementation of [`sea-orm`'s](sea_orm) [`DatabaseTransaction`].
///
/// Note that there is no FFI-safe way to use
/// [`TransactionTrait`](sea_orm::TransactionTrait). Use the [`FfiConnection::begin()`]
/// or [`FfiConnection::transaction()`] methods instead.
#[repr(C)]
pub struct FfiTransaction {
	ptr: TransactionPtr,
	drop: DropTransaction,
	commit: CommitTransaction,
	rollback: CommitTransaction,
	vtable: ConnectionVTable<TransactionPtr>
}

impl FfiTransaction {
	pub(crate) fn new(inner: DatabaseTransaction) -> Self {
		Self {
			ptr: TransactionPtr::new(Box::new(Compat::new(inner))),
			drop: drop_transaction,
			commit: commit_transaction,
			rollback: rollback_transaction,
			vtable: ConnectionVTable {
				backend: get_database_backend,
				execute,
				execute_unprepared,
				query_one,
				query_all,
				is_mock_connection
			}
		}
	}

	pub fn commit(self) -> impl Future<Output = Result<(), DbErr>> + Send {
		let mut this = ManuallyDrop::new(self);
		(this.commit)(&mut this.ptr)
			.map(|res: FfiResult<(), FfiDbErr>| res.into_result().map_err(Into::into))
	}

	pub fn rollback(self) -> impl Future<Output = Result<(), DbErr>> + Send {
		let mut this = ManuallyDrop::new(self);
		(this.commit)(&mut this.ptr)
			.map(|res: FfiResult<(), FfiDbErr>| res.into_result().map_err(Into::into))
	}
}

impl Drop for FfiTransaction {
	fn drop(&mut self) {
		(self.drop)(&mut self.ptr)
	}
}

// Safety: The Box'ed `DatabaseTransaction` is `Send` and `Sync`,
// and the other types are just extern "C" fn pointers.
unsafe impl Send for TransactionPtr {}
unsafe impl Sync for TransactionPtr {}
unsafe impl Send for FfiTransaction {}
unsafe impl Sync for FfiTransaction {}

#[async_trait]
impl ConnectionTrait for FfiTransaction {
	fn get_database_backend(&self) -> DatabaseBackend {
		self.vtable.get_database_backend(&self.ptr)
	}

	async fn execute(&self, stmt: Statement) -> Result<ExecResult, DbErr> {
		self.vtable.execute(&self.ptr, stmt).await
	}

	async fn execute_unprepared(&self, sql: &str) -> Result<ExecResult, DbErr> {
		self.vtable.execute_unprepared(&self.ptr, sql).await
	}

	async fn query_one(&self, stmt: Statement) -> Result<Option<QueryResult>, DbErr> {
		self.vtable.query_one(&self.ptr, stmt).await
	}

	async fn query_all(&self, stmt: Statement) -> Result<Vec<QueryResult>, DbErr> {
		self.vtable.query_all(&self.ptr, stmt).await
	}

	fn is_mock_connection(&self) -> bool {
		self.vtable.is_mock_connection(&self.ptr)
	}
}