Konflikt mutable/immutable reference po wprowadzeniu parametru czasu życia

Konflikt mutable/immutable reference po wprowadzeniu parametru czasu życia
elwis
  • Rejestracja:ponad 18 lat
  • Ostatnio:6 dni
0

Cześć, uczę się Rusta i próbuję poprawnie użyć parametru czasu życia. Poniższa funkcja i wywołanie działały prawidłowo, zanim nie dodałem parametrów czasu życia:

Kopiuj
fn reading<'a>(input : &mut dyn Read, tr : &'a mut TermReader<'a>) {
    let mut buff : [u8;10] = [0;10];

    while match input.read(&mut buff) {
        Ok(len) => { tr.accept(&buff[0..len]) },
        Err(e) => {
            println!("ERROR: {}", e);
            false
        }
    } {}
}

wywołanie:

Kopiuj
 let mut tr = TermReader::new(initial_keys, Some(KeyAction::Action(ac_elsekey)));
 reading(&mut input, &mut tr);
 println!("{}", tr.args.join(","));

Po dodaniu parametru czasu życia otrzymuję następujący komunikat:

Kopiuj
error[E0499]: cannot borrow `*tr` as mutable more than once at a time
  --> src/main.rs:125:22
   |
121 | fn reading<'a>(input : &mut dyn Read, tr : &'a mut TermReader<'a>) {
   |            -- lifetime `'a` defined here
...
125 |         Ok(len) => { tr.accept(&buff[0..len]) },
   |                      ^^^^^^^^^^^^^^^^^^^^^^^^
   |                      |
   |                      `*tr` was mutably borrowed here in the previous iteration of the loop
   |                      argument requires that `*tr` is borrowed for `'a`

error[E0502]: cannot borrow `tr.args` as immutable because it is also borrowed as mutable
  --> src/main.rs:201:20
   |
200 |     reading(&mut input, &mut tr);
   |                         ------- mutable borrow occurs here
201 |     println!("{}", tr.args.join(","));
   |                    ^^^^^^^^^^^^^^^^^
   |                    |
   |                    immutable borrow occurs here
   |                    mutable borrow later used here

Co robię nie tak? Dlatego bez lifetime'a było ok, a teraz nie jest? Jak to naprawić?


edytowany 2x, ostatnio: elwis
Patryk27
Moderator
  • Rejestracja:ponad 17 lat
  • Ostatnio:ponad rok
  • Lokalizacja:Wrocław
  • Postów:13042
4

Gdyby ten kod się skompilował, mógłbyś łatwo doprowadzić do use-after-free:

Kopiuj
struct TermReader<'a> {
    data: &'a [u8],
}

impl<'a> TermReader<'a> {
    fn accept(&mut self, data: &'a [u8]) {
        self.data = data;
    }
}

fn reading<'a>(input : &mut dyn Read, tr : &'a mut TermReader<'a>) {
    /* ... */
}

fn main() {
    let mut tr = TermReader {
        data: &[]
    };
    
    reading(&mut Cursor::new(vec![1, 2, 3]), &mut tr);

    // dokąd teraz wskazuje tr.data?
}

Problem wynika z tego, że buff żyje jedynie przez chwilę, wewnątrz reading(), a tworząc lifetime &'a mut TermReader<'a> (z funkcją .accept(), jak domyślam, odnoszącą się też do 'a) mówisz, że buff musi żyć tyle samo (albo dłużej) co tr -- a to jest niemożliwe, bo tr jest tworzony na zewnątrz reading(), przez co siłą rzeczy musi żyć dłużej niż buff (tworzone wewnątrz reading()).

W obecnej postaci ten problem można rozwiązać jedynie poprzez zmianę funkcji accept() tak, aby nie wymagała data: &'a [u8], a raczej data: &[u8] - to upewni się, że to &[u8] (które może żyć krócej niż TermReader) przypadkiem nie zostanie wrzucone do środka TermReadera.

Dlatego bez lifetime'a było ok, a teraz nie jest? Jak to naprawić?

Możesz podrzucić tę wersję, która działała? Strzelam, że Twoje .accept() teraz ma &'a [u8], którego wcześniej nie miało.


edytowany 6x, ostatnio: Patryk27
DR
BTW. Patrząc się na kod z czasem życia mam wrażenie, że w wielu przypadkach można tego w ogóle nie stosować. Możliwe, że przemawia przeze mnie niechęć do tego, bo niby to rozumiem, ale zawsze spędzam dupogodziny jak muszę wykorzystać XD
Patryk27
yeah, u mnie w praktyce większość kodu opiera się właśnie o lifetime elision, bo każde takie explicit 'a przykuwa ekstra uwagę, a nie zawsze jest to potrzebne 😁
elwis
  • Rejestracja:ponad 18 lat
  • Ostatnio:6 dni
0

Teraz funkcja accept wygląda tak:

Kopiuj
impl<'a> TermReader<'a> {
    //...
    
    fn accept(&'a mut self, keys : &[u8]) -> bool {
        if self.key_map.contains_key(&keys[0]) {
            let x = self.key_map[&keys[0]];
            x.run(self, keys)
        } else {
            match self.elsekey {
                Some(x) => x.run(self,keys),
                None => {
                    echo(keys);
                    self.current = self.current.clone() + str::from_utf8(keys).unwrap();
                    true
                }
            }
        }
    }

Dodałem tylko 'a przed mut self, Drugi parametr nie ma żadnego parametru czasu życia. Następujące też nie pomaga:

Kopiuj
    fn accept<'b>(&'a mut self, keys : &'b [u8]) -> bool {

Oryginalnie było:

Kopiuj
fn accept(&mut self, keys : &[u8]) -> bool {

edytowany 2x, ostatnio: elwis
Patryk27
Moderator
  • Rejestracja:ponad 17 lat
  • Ostatnio:ponad rok
  • Lokalizacja:Wrocław
  • Postów:13042
3

Kompilator widząc &'a mut self dochodzi do wniosku, że aby daną funkcję uruchomić, self musi żyć tyle samo ile trzymana w tym selfie referencja - jest to dosyć niecodzienna sytuacja, bo na ogół to referencje wrzucane do struktur żyją dłużej niż te struktury (a nie na odwrót, jak by sugerowało &'a mut self).

Przykładowo, taki kod działa:

Kopiuj
struct Foo<'a> {
    val: &'a str,
}

impl<'a> Foo<'a> {
    fn foo(&mut self) {
        println!("{}", self.val);
    }
}

fn main() {
    let mut foo: Foo<'static> = Foo {
        val: "Hello!",
    };
    
    foo.foo();
}

... ale już wprowadzenie pewnej niepozornej zmiany:

Kopiuj
impl<'a> Foo<'a> {
    fn foo(&'a mut self) {
        println!("{}", self.val);
    }
}

... kończy się:

Kopiuj
error[E0597]: `foo` does not live long enough
  --> src/main.rs:16:5
   |
12 |     let mut foo: Foo<'static> = Foo {
   |                  ------------ type annotation requires that `foo` is borrowed for `'static`
...
16 |     foo.foo();
   |     ^^^^^^^^^ borrowed value does not live long enough
17 | }
   | - `foo` dropped here while still borrowed

Problem wynika z tego, że kompilator widząc foo.foo(); sprawdza bounds wymagane do odpalenia danej funkcji (tj. sprawdza czy uruchomienie jej jest legalne w danym kontekście) - tutaj widzi, że do uruchomienia .foo() trzeba mieć &'a mut self, a z kontekstu dostrzega, że 'a == 'static; ergo - potrzebujemy &'static mut self; no i tutaj pies pogrzebany, bo oczywiście zmienna lokalna nie może zostać zborrowowana jako 'static.

(przy czym 'static sam w sobie nie gra tutaj większej roli - to samo zjawisko można odtworzyć z wykorzystaniem osobnej funkcji.)

Wersja bez explicit lifetime'ów działa, ponieważ jest ona elidowana w trochę inny sposób, do:

Kopiuj
impl<'a> Foo<'a> {
    fn foo<'b>(&'b mut self)
    where
        'a: 'b,
    {
        println!("{}", self.val);
    }
}

... gdzie jedynym wymogiem jest to, aby 'a (tj. coś wrzucone w strukturę) żyło dłużej niż 'b (tj. struktura sama w sobie); co ma sens, bo w końcu Foo (jako "kontener", 'b) jest tworzone zawsze po referencji ('a), więc siłą rzeczy będzie żyło dłużej.

(w porównaniu do &'a mut self, można to sobie wyobrazić jako nie ma potrzeby, aby biblioteka żyła tyle samo ile znajdujące się w niej książki.)

W ramach ekstra ciekawostki dorzucę, że wersja z &'a self (zamiast &'a mut self), być może zaskakująco, działa:

Kopiuj
struct Foo<'a> {
    val: &'a str,
}

impl<'a> Foo<'a> {
    fn foo(&'a self) {
        println!("{}", self.val);
    }
}

fn main() {
    let foo: Foo<'static> = Foo {
        val: "Hello!",
    };
    
    foo.foo();
}

... a wynika to z mechanizmu wariancji - jest to dosyć złożony temat (którego nie warto zgłębiać na początku, ale przydaje się później), ale tl;dr:

  • każdy lifetime niesie ze sobą pewną ekstra informację mówiącą o tym, czy w miejsce danego lifetime'u można podrzucić inny lifetime (np. mniejszy albo większy),
  • wewnątrz &'a T, 'a jest kowariantne (tj. pozwala ono na wykorzystanie innego lifetime'u w miejsce 'a),
  • wewnątrz &'a mut T, 'a jest inwariantne (tj. nie pozwala ono na wykorzystanie innego lifetime'u w miejsce 'a),
  • stąd mając &'a mut self (gdzie 'a = static), kompilator wymaga &'static mut self, ale już &'a self pozwala na auto-reborrow i podstawienie w miejsce 'a lifetime'u krótszego, zgodnego z cyklem życia zmiennej lokalnej foo; cała ta mechanika istnieje po to, aby nie dało się wrzucić czegoś żyjącego krócej w coś żyjącego dłużej.

Jak wspomniałem, trafiłeś na dosyć niecodzienny problem - pytaj śmiało i wytykaj palcami, jeśli coś jest nadal niejasne :-)


edytowany 4x, ostatnio: Patryk27
DR
  • Rejestracja:około 12 lat
  • Ostatnio:około godziny
  • Postów:1131
2

Ja jeszcze od siebie dodam zajebisty IMO talk o lifetime i jak to działa https://m.youtube.com/watch?v=rAl-9HwD858

Oraz wariancje o których wspomina @Patryk27 https://m.youtube.com/watch?v=iVYWDIW71jk

Ogólnie polecam pana, na jego stronie można też dodawać tematy/głosować na już dodane by zrobił o tym stream.

elwis
  • Rejestracja:ponad 18 lat
  • Ostatnio:6 dni
0

Dzięki wielkie za materiały i wyjaśnienia. Teraz wiem w czym rzecz. Trochę na ślepo próbowałem te lifetimy robić, a rzecz była w tym, że x.run(self, keys) odnosi się do funkcji typu fn run (&self, tr: &'a mut TermReader<'a>, keys: &[u8]) -> bool (i dalej jeszcze handler, wskaźnik na funkcję tego samego prototypu), co powiela błąd ze złym typem. Zrozumiałem, że wystarczy rozebrać strukturę, która jest przeładowana (brzydki nawyk z C):

Kopiuj
struct TermReader<'a> {
    key_map : KeyBind<'a>,
    elsekey : Option<KeyAction<'a>>,
    current : String,
    hint_gen : ShCommands,
    hints : Option<ExcerptIter<'a, String>>, // <- ten iterator wymaga lifetime'a
    pub args : Vec<String>                   // Pozostałe wynikaja z tego, że prototyp handlera
}                                            // musi je uwzględnić

Rozbiję sobie to na 2,3 struktury i handlery będę mieć osobno. To powinno uprościć zależoności i rozwiązać problem. Tylko z ciekawości dopytam, czy choć teoretycznie jest możliwe zaimplementowanie takiego handlera, bez dzielenia tego na osobne struktury (oczywiście nie używając unsafe). Chyba też możliwym rozwiązaniem byłoby użycie Rc zamiast referencji?


edytowany 6x, ostatnio: elwis
Patryk27
Moderator
  • Rejestracja:ponad 17 lat
  • Ostatnio:ponad rok
  • Lokalizacja:Wrocław
  • Postów:13042
1

Hmm, a nie wystarczyłoby fn run<'a, 'b>(&self, tr: &'a mut TermReader<'b>, keys: &[u8]) -> bool?


elwis
  • Rejestracja:ponad 18 lat
  • Ostatnio:6 dni
0

O, nawet udało mi się usunąć problem z lifetimem, trzeba było tylko typ handlera zdefiniować jako for <'a, 'b> fn(&'b mut TermReader<'a>, &[u8]) -> bool;… Tylko wciąż dostaję błąd:

Kopiuj
error[E0502]: cannot borrow `tr.args` as immutable because it is also borrowed as mutable
   --> src/main.rs:177:20
    |
176 |     reading(&mut input, &mut tr);
    |                         ------- mutable borrow occurs here
177 |     println!("{}", tr.args.join(","));
    |                    ^^^^^^^^^^^^^^^^^
    |                    |
    |                    immutable borrow occurs here
    |                    mutable borrow later used here

Tego trochę nie czaję, bo tr przecież nie jest referencją… Chyba, że println! robi z niego referencję?

No i jeszcze jedna rzecz się nie kompiluje:

Kopiuj
fn ac_elsekey<'a, 'b>(tr: &'b mut TermReader<'a>, keys: &[u8]) -> bool
        where 'a : 'b {
    if tr.args.len() > 0 {
        echo(keys);
        tr.current = tr.current.clone() + str::from_utf8(keys).unwrap();
        return true
    }

    let trial = tr.current.clone() + str::from_utf8(keys).unwrap();
    match tr.hint_gen.for_prefix(&trial) {
        Ok(mut newhints) => { 
            if newhints.len() > 0 {
                let first = newhints.get().unwrap();
                Term.echo(first.get(tr.current.len()..).unwrap().as_bytes())
                    .endline()
                    .hmove(-((first.len() - tr.current.len() - 1) as i32));
                
                tr.current = tr.current.clone() + str::from_utf8(keys).unwrap();
                tr.hints = Some(newhints);
            }
        },
        Err(_) => {}
    }

    true
}

Wali błędem:

Kopiuj
error[E0623]: lifetime mismatch
   --> src/main.rs:152:28
    |
134 | fn ac_elsekey<'a, 'b>(tr: &'b mut TermReader<'a>, keys: &[u8]) -> bool
    |                           ----------------------
    |                           |
    |                           these two types are declared with different lifetimes...
...
152 |                 tr.hints = Some(newhints);
    |                            ^^^^^^^^^^^^^^ ...but data from `tr` flows into `tr` here

i do tego

Kopiuj
error[E0308]: mismatched types
   --> src/main.rs:201:71
    |
201 |     let mut tr = TermReader::new(initial_keys, Some(KeyAction::Action(ac_elsekey)));
    |                                                                       ^^^^^^^^^^ one type is more general than the other
    |
    = note: expected fn pointer `for<'a, 'b, 'r> fn(&'b mut TermReader<'a>, &'r [u8]) -> _`
               found fn pointer `for<'r> fn(&mut TermReader<'_>, &'r [u8]) -> _`

Czego nie rozumiem, bo KeyAction::Action jest zdefiniowane tak:

Kopiuj
type KAHandler = for <'a, 'b> fn(&'b mut TermReader<'a>, &[u8]) -> bool;

#[derive(Clone,Copy)]
enum KeyAction {
    Action(KAHandler),
}

elwis
  • Rejestracja:ponad 18 lat
  • Ostatnio:6 dni
1

Żeby rozwiązać problem trzeba było zrezygnować z mutowalnej referencji na TermReader i zamiast tego przejmować własność i zwracać nowy obiekt, bo inaczej w pętli Ok(len) => tr.accept(&cfg, &buff[0..len]), jest interpretowane jako wielokrotne pożyczenie do zapisu. Brzmi rozsądnie, mniejsza o to czemu wcześniej tego nie było. Ostatecznie rozbiłem TermReader na dwie struktury:

Kopiuj
struct TermCfg {
    key_map : KeyBind,
    elsekey : Option<KeyAction>,
    hints : ShCommands,
}

// ...

struct TermReader<'a> {
    current : String,
    pub args : Vec<String>,
    chint : Option<ExcerptIter<'a, String>>
}

Funkcja accept ma wtedy taką formę:

Kopiuj
    pub fn accept<'b>(self, cfg : &'b TermCfg, keys : &[u8]) -> (bool, TermReader<'b>)
            where 'a : 'b {
        if cfg.key_map.contains_key(&keys[0]) {
            let x = cfg.key_map[&keys[0]];
            x.run(self, cfg, keys)
        } else {
            match cfg.elsekey {
                Some(x) => x.run(self, cfg, keys),
                None => {
                    echo(keys);
                    let nc = self.current.clone() + str::from_utf8(keys).unwrap();
                    (true, self.with_current(nc))
                }
            }
        }
    }

Odpowiednio więc:

Kopiuj
type KAHandler = for <'a> fn(TermReader<'a>, &'a TermCfg, &[u8]) -> (bool, TermReader<'a>);

Natomiast wywoływane to wszystko jest tak:

Kopiuj
fn reading(input : &mut dyn Read, cfg : TermCfg) -> Vec<String> {
    let mut buff : [u8;10] = [0;10];
    let mut tr = TermReader::new();

    while match input.read(&mut buff) {
        Ok(len) => { let (cont, ntr) = tr.accept(&cfg, &buff[0..len]);
                     tr = ntr;
                     cont},
        Err(e) => {
            println!("ERROR: {}", e);
            false
        }
    } {}

    tr.args
}

I się kompiluje. ;)


Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.