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).
Z toho jsem teda dosti zmateny...
1) K cemu je alokace na halde, kdyz se objekt po skonceni stackframu dealokuje? Nebo po zkopirovani "ukazatele" nekam vem uz dealokace neprobehne? To mi tam chybi zminene... Pokud to tak je, tak se vlastne objekty na halde vytvari jakoby v implicitne pouzitem shared_ptr?
2) Doted jsem si myslel, ze Rust dela silny type-checking. Jak je tedy mozne ulozit hodnotu typu Box<Complex> do promenne typu Complex? Nebo se dela nejaky autounboxing/implicitni konverze jako v Jave u "Primitivnich" typu?
3) Mluvit o tom, ze Drop je ekvivalent destruktoru, by asi vyzadovalo trochu hlubsi analyzu. Pokud mam tridni hierarchii, tak destruktor (narozdil od vsech ostatnich virtualnich metod) vola pro vsechny nadrazene datove typy automaticky. Pokud je metoda drop() jen virtualni metodou jako kazda jina, pak by bylo nutne volat vzdycky na konci super::drop(), ne?
Mozna velka cast meho nepochopeni prameni z toho, ze nemam v krvi model vlastnictvi objektu a podobne Rustoviny, ale rad bych si to ujasnil.
Pokusím se odpovědět podle toho, co jsem o Rustu nasbíral z předchozích článků, snad správně:
1. Mohu ho například přesunout jinam, třeba dovnitř jiného objektu, který jsem dostal parametrem.
2. Vypadá to na unboxing. (Jak to krásně sedí k typu Box…)
3. Good point, taky by mě to zajímalo.
Jen si nejsem úplně jist s obecným tvrzením „Pokud mam tridni hierarchii, tak destruktor (narozdil od vsech ostatnich virtualnich metod) vola pro vsechny nadrazene datove typy automaticky.“ – znám jazyky, které mají třídní hierarchii, ale destruktor je metoda jako každá jiná, a tedy volání nadřazených destruktorů není automatické.
Rustovina vlastnictví objektu, může být zajímavé, je to takový přirozený kompromis mezi neměnným objektem, kdy se pro výsledek jakékoliv operace alokuje nový objekt, a odkazu, kdy se paměť nerealokuje, ale zase objekt může měnit něco mimo zorné pole programátora. Vlastnictví zabraňuje přístupu k objektu ostatním a zároveň umožňuje a zjednodušuje efektivní správu paměti. Že to nenapadlo už někoho dříve, kdo to vymyslel, byl génius.
Dlouhe zimni vecery mame nabiledni, a proto s radosti odkazu na (extremne) dlouhe a (extremne) zajimave cteni: http://joeduffyblog.com/2016/11/30/15-years-of-concurrency/ . Ten pan pise o tom, jak v Microsoft Research cca 10 vyvijeli OS a programovaci jazyk uplne od piky, neomezeni jakoukoli zpetnou kompatibilitou a zazitymi vzorci. Bohuzel to pred par lety hodil Microsoft k ledu, ale poznatky, ke kterym dospeli, jsou jak z rise pohadek =) A Rust se u nich inspiroval hodne (nebo oni ke konci projektu u Rustu, to nevim :) ).
Kazdopadne vlastnictvi objektu tam trochu odlisnym zpusobem resili taky.
Dobré otázky! Zkusím odpovědět, snad to bude dávat smysl:
1) Box je zcela nejjednodušší způsob alokace paměti na heapu, proto jsem ho dneska použil, ale pro bod 1 se hodí Rc (jen pro jedno vlákno) či Arc (pro sdílení mezi více vlákny), možná i Weak, ne Box (viz příště). Rc je vlastně velmi jednoduchá podoba GC s počítáním referencí, dtto Arc, který však dělá inkrementaci/dekrementaci atomicky a tedy potenciálně zdržuje.
2) to není zcela přesně vlastnost jazyka (nějaká "magie" v sémantice), ale existuje trait Deref, který je implementován mj. i Boxem (ale i zmíněným Rc) - https://doc.rust-lang.org/stable/std/ops/trait.Deref.html
3) v Rustu se po zavolani drop() daného typu rekurzivně volá drop pro všechny atributy (pokud to má význam). Třídní hiearchii, resp. to co si pod tím názvem představujeme my Javisti/C++saři apod, tu Rust nemá :-) [se všemi z toho plynoucími důsledky, ale to je asi věcí zvyku]
Já taky děkuju, protože přesně tyto otázky mě napadaly zpočátku taky (Rust je v tomto hodně odlišný), ale po nějaké době, kdy si člověk říká W.F???, to nakonec docvakne :-) Zrovna u té alokace na haldě se totiž spojuje hned několik konceptů - typový systém, správa paměti, trait deref a hlavně ownership.
Hmm, komu, že to holduješ? Holt lidi na internetu si nedají říct, a musej pořád psát špatně česky, a ještě svoje chyby opakovat několikrát v jednom příspěvku.
Pokud však nechceš jen špatně česky trolovat, tak prosím napiš, jaká základní filozofie (co si pod tím představuješ) a jak byla dokázána jako špatná. Já to totiž nevidím. (Naopak vidím spoustu jazyků, které se snaží programování zjednodušovat na nesprávných místech, což typicky vede k run-time chybám, které by mohly být odchyceny již compile-time.)
No nemusíš, svůj obraz si buduješ sám. Na velmi slušnou prosbu o vysvětlení svého postoje odpovídáš "vykuř si", a "nemam chuť vzdělávat kretena". To je naprosto učebnicový trolling. Takže to beru tak, že žádný názor nemáš, jen chceš za každou cenu diskutovat. Jako troll. (Vlastně kecám, kdybych to tak bral, tak bych na tebe nereagoval. Tím, že jsi svůj názor vepsal pod pěkný odborný článek o Rustu, jsem doufal, že o tématu alespoň něco víš, a že se o to chceš podělit.)