Klasická otázka: co se stane (popř. co se má/může stát), když z destruktoru pošlu referenci na self někam pryč?
Zkusím si odpovědět sám: Rust bez unsafe kódu nedovolí mít dva ukazatele na tentýž objekt. A pokud použiju unsafe operace, mohu takto udělat use-after-free, ale to je už moje blbost.
Chápu to správně?
Logicky by destruktor neměla být metoda, která ruší objekt, uvolňuje paměť, ale která se provede před zrušením objektu, před samotným zrušením objektu by se mělo kontrolovat, zda objekt není někde používán, takže když pošlete referenci na objekt z destruktoru ven, pro interní systém by to mělo znamenat, že objekt je používán a v dalším kroku, po provedení destruktoru ho nezrušit a neuvolnit paměť.
S „logicky by mělo“ se dá snadno narazit, už jen my dva na to máme rozdílný názor. ☺
V jazycích jako Java nebo PHP je to celkem jasné. Tyto jazyky se snaží o memory safety i v sandboxovaném prostředí a nekontroluje se příliš, kam strkám reference, takže moc jiných řešení než nedealokovat objekt nepřipadá v úvahu.
V C++ toto bude bráno jako zodpovědnost programátora a neuklizená reference bude zdrojem UAF.
Rust se zde nepodobá žádnému jazyku, který znám. Snaží se o memory safety, ale je možné to vědomě porušit. (S Javovým Unsafe nebo JNI to srovnávat nelze, to je v sandboxu zakázané.) A hlavně má celkově neobvyklé mechanismy práce s pamětí, takže analogie tu budou dost problematické. Bez unsafe kódu, pokud chápu Rust správně (což ale není jisté), toho ani docílit nejde a s unsafe kódem jsme spíše na úrovni C/C++, takže to možná udělá dangling pointer a následně třeba i UAF.
Tady by se projevila vlastnost Rustu, kterou jsme trosku nakousli tady:
https://www.root.cz/clanky/rust-struktury-n-tice-a-vlastnictvi-objektu/
Rust ma pomerne striktni model vlastnictvi objektu (s tim souvisi move/copy semantika, ktera by se dala pouzit i u destruktoru, copy samozrejme znamena, ze se puvodni objekt muze klidne zrusit) a jde to obejit napriklad raw pointery. Ale tady plati presne to co pises - clovek musi vedet, co ma delat, protoze s raw pointery se dostavame zpatky na uroven cecka :)
Chápu dobře, že se řeší tento problém?
https://github.com/tisnik/presentations/blob/master/rust/100_box_as_arg.rs
Tady je c1 dealokováno dříve, než c2, už ve funkci print_complex (která bez copy traitu pro Complex sebere ownership).
No to je čtvrtá varianta ☺
a. V destruktoru předám referenci na self jinam – jde to vůbec bez raw pointerů bez předání vlastnictví?
b. V destruktoru předám raw pointer na self jinam – bude z něj dangling pointer, že?
c. V destruktoru zkopíruju self jinam – není problém.
d. V destruktoru předám vlastnictví selfu jinam – jde to? Popere se s tím Rust?
Ajo :-) Současný překladač si moc nehlídá, co se v Drop děje. Je to pro něj metoda jako každá jiná, jen se snaží dodržovat kontrolu ownershipu (borrow atd.). Na druhou stranu to "předání jinam" je hodně omezené, protože pokud to bude například jiné vlákno, stejně se použije Arc a metoda Send.
Jinak překladač toho nehlídá víc, například toto je trošku problém (AFAIK se to ale moc nekontroluje nikde):
impl Drop for Complex { fn drop(&mut self) { let c = Complex::new(2.,2.); println!("Dropping complex number: {:}+{:}i", self.real, self.imag); } }
V čem je tu problém? Nekonečný cyklus alokace a dealokace? (Možná spíše rekurze, možná přeletí stack.) Nebo ještě něco jiného?
Ten nekonečný dealokační cyklus sice není hezký, ale tady bych od překladače nečekal, že si to bude hlídat, resp. jeho případnou detekci beru jako bonus. Je to rozhodně nad rámec memory safety. Navíc u Turing-complete jazyka nelze všechny nekonečné cykly detekovat, to bude vždy best-effort.
V Javě by něco takového IMHO způsobilo menší problém – další finalize by se volalo až s dalším GC, ne hned. Bohužel by se ale takový objekt neměl šanci dostat do další generace, takže by se to dělo celkem často. Při ukončení aplikace by se pak nejspíš poslední finalize prostě nezavolal.
Tak dokonce v Javě se to nebude volat tak často, protože objekty s finalize se alokují rovnou ve starší generaci. Asi aby se nekomplikovala implementace kopírovacího GC pro nejmladší generaci. (V nejmladší generaci se předpokládá velké procento odpadu, proto kopírovací GC.) Ale toto je už dost implementation-specific.
tady na tom si GC v Jave 7 taky posmakne (ted me napada, jestli pro parallel GC nema byt to pocitadlo atomicke...)
public class Test { private static int finalizeCount = 0; protected void finalize() { System.out.println("finalizing #" + (finalizeCount++)); System.gc(); new Test(); } public static void main(String[] args) { for (int i=0;i<10;i++) new Test(); System.gc(); try { Thread.sleep(50000); } catch(InterruptedException ex) { Thread.currentThread().interrupt(); } } }
No jistě – když tam explicitně zavoláš System.gc(), tak se asi začne uklízet i ve starších generacích. A když jsou ty objekty alespoň dva, bude se to od prvního finalize() uklízet agresivně cyklicky vzájemně. Při finalize() prvního objektu je volání GC celkem bezvýznamné, ale potom se vyrobí pohrobek v new Test(). Při finalize() druhého objektu se spustí GC, který uklidí pohrobka (a asi ho zařadí do fronty k finalizaci), a následně se vytvoří nový pohrobek. A toto se pak opakuje, dokud JVM běží, nebo dokud to nenaruší nějaká výjimka (např. GC overhead limit).