Pointers, Stack en Heap
Arnout van Kempen over rommelen in een digitale wereld.
We hebben inmiddels aardig wat Rust behandeld en hoewel we grote overeenkomsten met C hebben gezien, zijn we weg gebleven van pointers. Ja, een borrow in Rust met een & lijkt verdacht veel op een pointer in C. Immers, als je in C zoiets had als
int a = 10;
int *p = &a;
int b = *p;
Dan was a een variabele met inhoud 10, &a het adres van a, en *p de waarde op het adres van p, in dit geval dus weer 10. In Rust lijkt dat er best op, maar Rust werkt op een hoger abstractieniveau. Daardoor is een dereferentie met * niet nodig, maar is het ook minder vanzelfsprekend om het adres p in beeld te krijgen. Dat levert zoiets op:
let a = 10;
let p = &a;
let b = p;
println!("Adres van a (via p): {:?}", p);
println!("Waarde van a (via p): {}", p);
De & levert een borrow op, volgens de al besproken borrow-regels. Op machineniveau bestaat het concept “borrow” niet, dus daar gebeurt wat ook in C gebeurt: p krijgt het adres waar a is opgeslagen. Maar omdat Rust dat als een borrow beschouwd, zal het automatisch p terugvertalen naar de waarde van a, die immers “geleend” is. Je hoeft dus geen * voor p te zetten om de waarde 10 aan b toe te wijzen (het mag wel!). Maar als je echt het adres wil weten waar a staat, zal je iets aparts moeten doen. De beide println! macro’s illustreren dat. Alleen door {:?} te gebruiken, wat specifiek bedoeld is voor debugging, krijg je het onderliggende adres te zien. Met {} gaat Rust er automatisch van uit dat je niet het adres bedoelt, maar de geleende inhoud van dat adres. Heel veilig allemaal, want zo kan de borrow checker zijn werk doen en worden zinloze geheugenfouten voorkomen.
Maar, wat nu als we toch echt een pointer willen hebben en geen borrow? Bijvoorbeeld in onze simpele AI die we in C maakten. Daar hadden we een linked list, in de vorm van een binary tree, van nodes die aan elkaar hingen via pointers. Moeten we dat in Rust via een heel andere wijze oplossen?
Mogelijk kan dat, maar Rust kent wel degelijk pointers, net als C. Maar dan wel veilige pointers. We moeten hier nog eens duiken in de verschillende soorten geheugen die een programma in Rust kent: de stack en de heap. Ik noemde dit al eerder toen we strings bespraken, maar nogal terloops. Ook bij ownership kwam het indirect al aan de orde, maar nu we aan pointers beginnen wordt het toch echt essentieel om deze begrippen goed helder te krijgen.
De stack is een deel van het geheugen dat voor de CPU snel toegankelijk is, maar waar het programma altijd exact moet weten hoe groot variabelen zijn. Je kan je de stack voorstellen als een stapel van bekende objecten. Als iets nieuws wordt toegevoegd komt het bovenop de stapel en als je iets niet meer nodig hebt wordt het van bovenaf weer weggehaald. Omdat van alle elementen bekend is hoe groot ze zijn, is ook bekend waar ze beginnen. Je kan dus snel bij data komen via de basis van de stack + een index. Als programmeur, ook op assembly niveau, hoef je niet te weten waar die stack precies is, zolang je maar weet welke index vanaf het startpunt je moet gebruiken. Geheugenfouten doen zich hier in principe dus niet voor en Rust neemt ook nauwelijks maatregelen om hier fouten te voorkomen.
De heap is “een hoop”, een berg data met heel wat minder structuur. Dat maakt dit tot het meest flexibele deel van het geheugen, maar je moet ook meer werk maken van het bijhouden wat je waar hebt opgeslagen. De computer kent blokken geheugen toe als je daar om vraagt en geeft ze weer vrij als je daar om vraagt. In C levert dat de ruimte voor geheugenfouten, waar je er gegarandeerd heel wat van gaat maken als je in C iets spannenders doet dan “Hello world!”. In Rust krijg je die fouten niet, maar dat komt omdat je door met name de borrow checker in het gareel wordt gedwongen. De heap is, behalve foutgevoelig, vooral extreem flexibel en iets minder snel dan de stack.
Overigens, voor de volledigheid, er is nog een geheugengebied. Hier leven de literals: je programma-code zelf. Een string-literal, bijvoorbeeld “dit is een string”, wordt niet naar de stack of de heap gekopieerd, maar blijft gewoon in je programma-code staan. De lengte kan toch niet veranderen, en dus is een simpele verwijzing naar het adres in je code genoeg.
Wat is nu een pointer? In Rust is een pointer nooit zomaar een pointer. We zagen al de stringslice, type &str. Dit is in feite een pointer naar een string, maar wel een slimme pointer: niet alleen het begin van de string staat er in, maar ook de lengte van de string. In C was je afhankelijk van een NULL aan het einde van een string om dat einde te kennen. In Rust is geen afsluiter nodig, omdat de lengte in de “slimme pointer” staat.
Als je in C een stuk geheugen nodig had, van de heap dus, dan deed je dat bijvoorbeeld met
int *arr = (int *)malloc(5 * sizeof(int));
if (arr == NULL) {
printf("Geheugentoewijzing mislukt\n");
return 1;
}
for (int i = 0; i < 5; i++) {
arr[i] = i * i;
}
for (int i = 0; i < 5; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
free(arr);
De variabele arr is een pointer naar het gealloceerde geheugen. Als deze niet NULL is, kan je verder naar dat geheugen verwijzen via arr en een index. Arr zelf wordt op de stack geplaatst, alle verwijzingen zijn naar de heap. Dezelfde code in Rust is
let n = 5;
let mut arr = Vec::new();
for i in 0..n {
arr.push(i * i);
}
for i in 0..n {
println!("arr[{}] = {}", i, arr[i]);
}
Al het geheugenbeheer doet Rust via ownership, lifetimes, etcetera. Maar het resultaat is hetzelfde. Arr is in feite een pointer, een behoorlijk slimme in dit geval want het is een vector en komt op de stack. De data zelf komt in de heap.
Dat Rust veel van je overneemt maakt het makkelijker om geen fouten te maken, maar het betekent ook dat je wat meer informatie moet hebben dan alleen een adres. Bij een stringslice bijvoorbeeld de lengte van de string. En dat betekent weer dat Rust een hele reeks soorten pointers kent. Daarover volgende keer meer.
Wie mee wil doen met #klooienmetcomputers kan dat doen via GitHub. Maak een account op github.com en zoek naar Abmvk/kmc. Het account Abmvk volgen kan ook. Lezers zijn vrij te gebruiken wat ze willen en om zelf zaken toe te voegen of aan te passen, vragen te stellen of commentaar te leveren.
Gerelateerd
Over bits & bytes
Arnout van Kempen over rommelen in een digitale wereld.
En arrays dan?
Arnout van Kempen over rommelen in een digitale wereld.
Typecasting in COBOL
Arnout van Kempen over rommelen in een digitale wereld.
Gewone variabelen
Arnout van Kempen over rommelen in een digitale wereld.
Bestanden in soorten en maten
Arnout van Kempen over rommelen in een digitale wereld.