最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

rust, sqlite and self referential struct - Stack Overflow

programmeradmin5浏览0评论

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();
}
发布评论

评论列表(0)

  1. 暂无评论