I'm new to Rust and facing some issue with what I learnt is called "self-referential structs", though not because I absolutely want to define a self referential struct but because a lib I'm using seems to force me into this design.
I started using the sqlite crate to store some data in a db. Wanting to abstract a little bit the design, I extracted the sqlite code into a separate class (let's call it SqliteController) :
pub struct SqliteController {
con: sqlite::Connection,
}
impl SqliteController {
pub fn new(path: &str) -> SqliteController {
let con = sqlite::open(path).unwrap();
Self::create_schema(&con);
SqliteController { con: con }
}
...
so far so good, but it was a naive impl and very slow, so let's use prepared statements then ! This works :
use sqlite::State;
pub struct PreparedSqliteController<'conn> {
_con: &'conn sqlite::Connection,
some_statement: sqlite::Statement<'conn>,
}
impl<'conn> PreparedSqliteController<'conn> {
pub fn new(con: &sqlite::Connection) -> PreparedSqliteController {
Self::create_schema(con);
PreparedSqliteController {
_con: con,
some_statement: con.prepare("SELECT xxx;").unwrap(),
}
}
pub fn create_schema(_con: &sqlite::Connection) {}
pub fn exec_some_statement(&mut self) {
while let Ok(State::Row) = self.some_statement.next() {}
}
}
fn main() {
let con = sqlite::open(":memory:").unwrap();
let mut sqlite_controller = PreparedSqliteController::new(&con);
sqlite_controller.exec_some_statement();
}
But it means the caller needs to build the connection for us, which IMHO kinda defeats the idea of building an abstraction.
I would really like to have the same constructor signature as the first impl : but everytime I try to build the connection from a path inside the constructor, I get some errors. (this fails because a sqlite::Statement keeps a ref to the connection making this struct a self referential struct)
e.g :
pub struct PreparedSqliteController<'conn> {
con: sqlite::Connection,
some_statement: sqlite::Statement<'conn>,
}
impl<'conn> PreparedSqliteController<'conn> {
pub fn new(path: &str) -> PreparedSqliteController {
let con = sqlite::open(path).unwrap();
Self::create_schema(&con);
PreparedSqliteController {
con: con,
some_statement: con.prepare("SELECT 1;").unwrap()
}
}
pub fn create_schema(_con: &sqlite::Connection) {}
}
fn main() {
let mut _sqlite_controller = PreparedSqliteController::new(":memory:");
}
david@yoda:~/dev/rust/sqlite_prep$ cargo check
Checking sqlite_prep v0.1.0 (/home/david/dev/rust/sqlite_prep)
error[E0515]: cannot return value referencing local variable `con`
--> src/main.rs:14:9
|
14 | / PreparedSqliteController {
15 | | con: con,
16 | | some_statement: con.prepare("SELECT 1;").unwrap()
| | --- `con` is borrowed here
17 | | }
| |_________^ returns a value referencing data owned by the current
function
error[E0382]: borrow of moved value: `con`
--> src/main.rs:16:29
|
10 | let con = sqlite::open(path).unwrap();
| --- move occurs because `con` has type `Connection`,
which does not implement the `Copy` trait
...
15 | con: con,
| --- value moved here
16 | some_statement: con.prepare("SELECT 1;").unwrap()
| ^^^ value borrowed here after move
Some errors have detailed explanations: E0382, E0515.
For more information about an error, try `rustc --explain E0382`.
error: could not compile `sqlite_prep` (bin "sqlite_prep") due to 2 previous errors
I understand the idea : when moving back the result from ::new() to the variable _sqlite_controller in main, the ref stored in the struct would not point anymore to the correct location.
I tried this, but no, does not work.
pub struct PreparedSqliteController<'conn> {
con: sqlite::Connection,
some_statement: Option<sqlite::Statement<'conn>>,
}
impl<'conn> PreparedSqliteController<'conn> {
pub fn new(path: &str) -> PreparedSqliteController {
let con = sqlite::open(path).unwrap();
Self::create_schema(&con);
PreparedSqliteController {
con: con,
some_statement: None
}
}
pub fn create_schema(_con: &sqlite::Connection) {}
pub fn init(&'conn mut self) {
self.some_statement = Some(self.con.prepare("SELECT 1;").unwrap());
}
}
fn main() {
let mut sqlite_controller = PreparedSqliteController::new(":memory:");
sqlite_controller.init();
}
david@yoda:~/dev/rust/sqlite_prep$ cargo check
Checking sqlite_prep v0.1.0 (/home/david/dev/rust/sqlite_prep)
error[E0597]: `sqlite_controller` does not live long enough
--> src/main.rs:27:5
|
26 | let mut sqlite_controller =
PreparedSqliteController::new(":memory:");
| --------------------- binding `sqlite_controller` declared
here
27 | sqlite_controller.init();
| ^^^^^^^^^^^^^^^^^ borrowed value does not live long enough
28 | }
| -
| |
| `sqlite_controller` dropped here while still borrowed
| borrow might be used here, when `sqlite_controller` is dropped and
runs the destructor for type `PreparedSqliteController<'_>`
For more information about this error, try `rustc --explain E0597`.
error: could not compile `sqlite_prep` (bin "sqlite_prep") due to 1
previous error
Although here I'm a little bit skeptical : both the borrowed ref in the statement and the instance should be dropped at the same time, shouldn't it ? If so, maybe my knowledge of the borrow checker and lifetime is too limited. Is there a fix for this ?
I've read elsewhere I could maybe use some 3rd party crate (e.g : ouroboros, owningref), but how would you solve this problem with the language itself ? Is the only way the unsafe way ? e.g :
pub struct PreparedSqliteController<'conn> {
con: sqlite::Connection,
some_statement: Option<sqlite::Statement<'conn>>,
}
impl<'conn> PreparedSqliteController<'conn> {
pub fn new(path: &str) -> PreparedSqliteController {
let con = sqlite::open(path).unwrap();
Self::create_schema(&con);
PreparedSqliteController {
con: con,
some_statement: None
}
}
pub fn create_schema(_con: &sqlite::Connection) {}
pub fn init(&mut self) {
self.some_statement = unsafe {
let ptr: *mut sqlite::Connection = &mut self.con;
Some(ptr.as_ref().unwrap().prepare("SELECT 1;").unwrap())
}
}
}
fn main() {
let mut sqlite_controller = PreparedSqliteController::new(":memory:");
sqlite_controller.init();
}
Update : as pointed out in the comments, several issues there :
- order of field destruction could in theory lead to use after free (contrary to c++, order of destruction is order of declaration)
- lifetime does not make sense (well, being new to lifetime, I'm trusting the authors here as I'm not sure I make entirely sense of them yet)
- the instance cannot be moved (as the refs would be lost). This is kinda ok with the current use case as expressed in the code, but it can be improved.
@user4815162342 pointed out more issues with the unsafe code and linked to this blog post that gives the full solution.
Here's an attempt for a better solution (with one caveat)
use sqlite::State;
pub struct PreparedSqliteController {
some_statement: sqlite::Statement<'static>, // using another lifetime and annotating PreparedSqliteController would not make sense here
_con: Box<sqlite::Connection>, // we use Box so PreparedSqliteController.some_statement can be moved without losing the ref (*)
}
impl PreparedSqliteController {
pub fn new(path: &str) -> PreparedSqliteController {
let con = sqlite::open(path).unwrap();
Self::create_schema(&con);
let con = Box::new(con); // heap allocation so refs to it outlive any move of PreparedSqliteController
let stmt = con.prepare("SELECT 1;").unwrap();
let stmt = unsafe { std::mem::transmute(stmt) }; // bypass the borrow checker who does not support this
PreparedSqliteController {
some_statement: stmt,
_con: con, // (*) problem here as we move the Box :
// could be caught by the borrow checker (MIR catches this problem)
// would need to use AliasableBox (more unsafe) to bypass it
}
}
pub fn create_schema(_con: &sqlite::Connection) {}
pub fn exec_some_statement(&mut self) {
while let Ok(State::Row) = self.some_statement.next() {}
}
}
fn main() {
let mut sqlite_controller = PreparedSqliteController::new(":memory:");
sqlite_controller.exec_some_statement();
}