Recently I started playing around with rust ffi. I’m trying to wrap a small postgres driver and provide a c interop interface. I’m using the sqlx
pg driver under the hood.
I’m having the following piece of code.
static mut SQLX4K: OnceLock<Sqlx4k> = OnceLock::new();
#[derive(Debug)]
struct Sqlx4k<'a> {
runtime: Runtime,
pool: PgPool,
tx_id: RwLock<Vec<u8>>,
tx: &'a mut [*mut Transaction<'a, Postgres>],
}
impl<'a> Sqlx4k<'a> {
fn tx_begin(&mut self) -> i64 {
let tx = self.runtime.block_on(self.pool.begin()).unwrap();
let id = { self.tx_id.write().unwrap().pop().unwrap() };
let tx = Box::new(tx);
let tx = Box::leak(tx);
self.tx[id as usize] = tx;
id.into()
}
fn tx_commit(&mut self, tx: i64) {
let id = tx;
let tx = unsafe { *Box::from_raw(self.tx[id as usize]) };
self.tx[id as usize] = null_mut();
self.runtime.block_on(tx.commit()).unwrap();
self.tx_id.write().unwrap().push(id as u8)
}
fn tx_rollback(&mut self, tx: i64) {
let id = tx;
let tx = unsafe { *Box::from_raw(self.tx[id as usize]) };
self.tx[id as usize] = null_mut();
self.runtime.block_on(tx.rollback()).unwrap();
self.tx_id.write().unwrap().push(id as u8)
}
fn tx_query(&mut self, tx: i64, sql: &str) {
let id = tx;
unsafe {
let mut tx = Box::from_raw(self.tx[id as usize]);
let query = tx.fetch_optional(sql);
let _result = self.runtime.block_on(query);
};
}
}
The main concept is that I have a static memory space that I store all the necessary parts of the custom wrapper driver.
Then I expose a C function like:
#[no_mangle]
pub extern "C" fn sqlx4k_tx_begin() -> c_long {
unsafe { SQLX4K.get_mut().unwrap() }.tx_begin().into()
}
#[no_mangle]
pub extern "C" fn sqlx4k_tx_commit(tx: c_long) {
unsafe { SQLX4K.get_mut().unwrap() }.tx_commit(tx);
}
#[no_mangle]
pub extern "C" fn sqlx4k_tx_rollback(tx: c_long) {
unsafe { SQLX4K.get_mut().unwrap() }.tx_rollback(tx);
}
#[no_mangle]
pub extern "C" fn sqlx4k_tx_query(tx: c_long, sql: *const c_char) -> *mut Sqlx4kResult {
let sql = c_chars_to_str(sql).unwrap();
unsafe { SQLX4K.get_mut().unwrap() }.tx_query(tx, sql);
ok()
}
My main goal is to support transactions.
So for this particular reason I decided to create a mutable slice and store all the generated transactions. Also, for each transaction I have an id
that I return to the C
code as a reference.
The problem is that each time I call the tx_query
method I get this functionality requires a Tokio context
error
Also my Cargo.toml
:
[dependencies]
once_cell = { version = "1.19.0" }
tokio = { version = "1.38.0", features = ["full"] }
sqlx = { version = "0.7.4", features = [
"runtime-tokio", # Use the tokio runtime without enabling a TLS backend.
"postgres", # Add support for the Postgres database server.
] }
Here is the backtrace:
thread '<unnamed>' panicked at /Users/smyrgeorge/.cargo/registry/src/index.crates.io-6f17d22bba15001f/sqlx-core-0.7.4/src/pool/connection.rs:169:13:
this functionality requires a Tokio context
stack backtrace:
0: 0x101065ff4 - std::backtrace_rs::backtrace::libunwind::trace::h6de1cbf3f672a4f8
at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/../../backtrace/src/backtrace/libunwind.rs:105:5
1: 0x101065ff4 - std::backtrace_rs::backtrace::trace_unsynchronized::hd0de2d5ef13b6f4d
at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/../../backtrace/src/backtrace/mod.rs:66:5
2: 0x101065ff4 - std::sys_common::backtrace::_print_fmt::h2a33510d9b3bb866
at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/sys_common/backtrace.rs:68:5
3: 0x101065ff4 - <std::sys_common::backtrace::_print::DisplayBacktrace as core::fmt::Display>::fmt::h01b2beffade888b2
at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/sys_common/backtrace.rs:44:22
4: 0x10100048c - core::fmt::rt::Argument::fmt::h5ddc0f22b2928899
at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/core/src/fmt/rt.rs:142:9
5: 0x10100048c - core::fmt::write::hbadb443a71b75f23
at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/core/src/fmt/mod.rs:1153:17
6: 0x10104988c - std::io::Write::write_fmt::hc09d7755e3ead5f0
at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/io/mod.rs:1843:15
7: 0x101069a48 - std::sys_common::backtrace::_print::h3cd1786cbb1caf0f
at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/sys_common/backtrace.rs:47:5
8: 0x101069a48 - std::sys_common::backtrace::print::h28349e5c25acbac7
at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/sys_common/backtrace.rs:34:9
9: 0x101069394 - std::panicking::default_hook::{{closure}}::hd24b6196784d991e
10: 0x101068f60 - std::panicking::default_hook::hfcec80a2720c8c73
at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/panicking.rs:292:9
11: 0x10106a2a4 - std::panicking::rust_panic_with_hook::h84760468187ddc85
at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/panicking.rs:779:13
12: 0x101069d20 - std::panicking::begin_panic_handler::{{closure}}::he666a5eb600a7203
at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/panicking.rs:649:13
13: 0x101069cb0 - std::sys_common::backtrace::__rust_end_short_backtrace::h592f44d2bf9f843f
at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/sys_common/backtrace.rs:171:18
14: 0x101069ca4 - rust_begin_unwind
at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/panicking.rs:645:5
15: 0x1010849b4 - core::panicking::panic_fmt::h98bbf7bdf4994454
at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/core/src/panicking.rs:72:14
16: 0x101033624 - sqlx_core::rt::missing_rt::hd58e9fd585cd27b3
17: 0x1010474e8 - <sqlx_core::pool::connection::PoolConnection<DB> as core::ops::drop::Drop>::drop::hbaea548330ec54f2
18: 0x1010142f8 - core::ptr::drop_in_place<alloc::boxed::Box<sqlx_core::transaction::Transaction<sqlx_postgres::database::Postgres>>>::h65e711f5dba2104f
19: 0x101014498 - _sqlx4k_tx_fetch_all
20: 0x100fd7ea0 - kfun:#main(){}
at /Users/smyrgeorge/dev/projects/test/sqlx4k/src/nativeMain/kotlin/Main.kt:59:5
21: 0x100ff8040 - _Init_and_run_start
22: 0x100ff820c - _main
The strange is that the tx_commit
and tx_rollback
function work ok.
What is the origin of the problem.
Keep in mind that the tokio
runtime is constructed manually and all futures are handled with runtime.block_on(...)
.
Also I have to mention that I started this project just to obtain experience in rust ffi.
So, there is a lot of unsafe code.
I also have an implementation that makes use of a HashMap
(to store the transactions).
But, i just wanted to try something another approach (with less locks)
PS: I understand that may be some memory issues in this piece of code. I’m just trying to understand what is happening
Async functions need an executor to run. Instead of calling an executor all the time, most Rust code that uses tokio would initialize the executor at the start of the program and then everything within it would just behave normally. To initialize the executor, you can use the tokio::main
macro on your main function:
#[tokio::main]
async fn main() {
// Call your other code...
}
Note that if you’re exposing your functionality as an FFI for a C program where the main function is started by C, you might want to construct the runtime directly, and expose this code through a separate “init” function that the C code will have to call before invoking any async-related code.
0