From 7ba4c32725637994dd15d4d4ce79ec0b35d7c140 Mon Sep 17 00:00:00 2001 From: Sebastian Bugge Date: Fri, 29 Dec 2023 18:03:16 +0100 Subject: [PATCH] Add new post. --- content/posts/eierskap.md | 313 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 content/posts/eierskap.md diff --git a/content/posts/eierskap.md b/content/posts/eierskap.md new file mode 100644 index 0000000..f439d04 --- /dev/null +++ b/content/posts/eierskap.md @@ -0,0 +1,313 @@ +--- +title: "Eierskap: Ikke bare en \"Rust-greie\"" +date: 2023-12-29T18:01:35+0100 +draft: false +toc: false +images: +tags: + - rust + - systemprogrammering +--- + +Jeg kom nylig over et artig eksempel av erierskap i praksis. + +```c +#include + +char* example() { + return "Hello world!"; +} + +int main() { + printf("%s\n", example()); + return 0; +} +``` + +Her har har vi funksjonen `example` som returnerer `"Hello world!"`. Denne +verdien printes i funksjonen `main`. + +```bash +$ clang -O3 example.c +$ ./a.out +Hello world! +``` + +Kult! Programmet printer "Hello world!" :) Hva om vi gjør en liten +modifikasjon? + +```c +#include +#include + +char* example() { + char out[13] = "Hello "; + return strcat(out, "world!"); +} + +int main() { + printf("%s\n", example()); + return 1; +} +``` + +Her har vi bare erstattet `"Hello world!"` med `strcat("Hello ", "world!")`. +Det burde vel ikke gjøre noen forskjell. + +```bash +$ clang -O3 example.c +$ ./a.out +d␃Z +``` + +Hv..a? Programmet printer bare masse rare tegn?! + +## Hva skjedde? +Som mange andre rare observasjoner innen programmering, er årsaken til dette +resultatet hvordan programmet håndterer minne. I C er en +[streng](https://en.wikipedia.org/wiki/String_(computer_science)) en +[tabell](https://en.wikipedia.org/wiki/Array_(data_structure)) med bokstaver +som avsluttes med en [nullbyte](https://en.wikipedia.org/wiki/Null_character). +Om en streng tilordnes en variabel, er verdien til varabelen en +[peker](https://en.wikipedia.org/wiki/Pointer_(computer_programming)) til den +første bokstaven i strengen. + +Okay, så hva er det som skjer? Når man oppretter en ny variabel, lagres +variabelverdien øverst i den nåværende +[kallstakken](https://en.wikipedia.org/wiki/Call_stack). Denne verdien kan +enten være hele verdien som lagres (hvis verdien f.eks. er en int), eller en +peker til hvor i minnet verdien til variabelen faktisk ligger. + +Hvis man oppretter en tabell inni en funksjon, opprettes minnet som er +nødvendig for denne tabellen øverst i den nåværende kallstakken. Verdien til +pekeren er minneadressen til det første elementet av tabellen. Hvis man +returnerer fra denne funksjonen, deallokeres tabellen sammen med resten av +kallstakken. Hvis man returnerer en peker til en tabell som er lagret i +stakken, peker denne pekeren nå på udefinert minne. Det er dette som skjer i +eksempel nummer 2. + +Okay, men hva er da greia med det første eksempelet? Hvorfor fungerer dette som +forventet? Vi kan få et lite hint om vi printer ut adressene til de ulike +variablene :) + +```c +#include + +int main() { + char string[13] = "Hello world!"; + printf("Streng-addresse: %p\n", string); + printf("Peker-addresse: %p\n", &string); + return 0; +} + +// Resultat: +// Streng-addresse: 0x7ffc98bf8730 +// Peker-addresse: 0x7ffc98bf8730 +``` + +Pekeren og strengen er på samme adresse. Strengen ble allokert i stakken. + +```c +#include + +int main() { + char* string = "Hello world!"; + printf("Streng-addresse: %p\n", string); + printf("Peker-addresse: %p\n", &string); + return 0; +} + +// Resultat +// Streng-addresse: 0x595c41bd1004 +// Peker-addresse: 0x7ffeed215580 +``` + +Pekeren og strengen er på helt forskjellige steder i minnet! I dette tilfellet +er det en illusjon at strengen opprettes i stakken. Selv om strengen først +tilordnes i funksjonen, allokeres strengen ved programstart i en minnesegmentet +[data](https://en.wikipedia.org/wiki/Data_segment). Minnet forblir tilgjengelig +til programmet avsluttes. + +## Hva har dette med eierskap å gjøre? +[Eierskap](https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html) er et +konsept i [Rust](https://www.rust-lang.org/) som baserer seg på at alle verdier +kun har én eier. Eieren er ansvarlig for frigjøringen av minnet. Eierskapet kan +overføres eller lånes bort. Om eierskapet lånes bort, beholder den opprinnelige +eieren pliktene som kommer med eierskapet etter at utlånet er omme. Om +eierskapet gis bort, overføres eierpliktene til den nye eieren. Til gjengjeld +kan den opprinnelige eieren ikke lenger akksessere verdien. Verdier kan +nemmelig kun aksesseres dersom man enten eier verdien eller om verdien er lånt +bort til deg. På denne måten unngår man mulighetene for vanlige minnefeil som +[minnelekasjer](https://en.wikipedia.org/wiki/Memory_leak), [hengende +pekere](https://en.wikipedia.org/wiki/Dangling_pointer) og +[dataløp](https://en.wikipedia.org/wiki/Race_condition). + +Eksempelet vi så på i stad, var et eksempel på en hengende peker. Vi prøver å +lese fra en minneadresse som ikke lenger er gyldig. La oss prøve å gjennskape +det første eksempelet i Rust. + +```rust +fn example() -> &str { + return "Hello world!"; +} + +fn main() { + println!("{}", example()); +} +``` + +Funksjonen example returnerer nå en referanse til en streng. Hvis vi prøver å +kompilere dette får vi derimot en feilmelding. + +```rust +error[E0106]: missing lifetime specifier + --> src/main.rs:1:17 + | +1 | fn example() -> &str { + | ^ expected named lifetime parameter + | + = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from +help: consider using the `'static` lifetime + | +1 | fn example() -> &'static str { + | +++++++ + +For more information about this error, try `rustc --explain E0106`. +``` + +Rust gir oss faktisk en gangske forståelig feilmelding! Referanser er navnet på +de lånte verdiene jeg snakket om tidligere. Disse trenger et såkalt "lifetime". +Dette er en verdi som forteller kompilatoren hvor lenge en referanse maksimalt +kan "leve", altså hvor lenge eieren til variabelen har tenkt å holde på den. +Lifetimes er i de aller fleste tilfeller definert implisitt, men noen ganger må +vi spesifisere dem selv. På denne måten kan man unngå situasjoner der man har +en referanse til en verdi som ikke lenger finnes, eller på andre ord: At +variabellånet varer lengre enn det orginale eierskapet av variabelen. + +Om du husker tilbake på eksempelet vi prøver å gjenskape, var strengen lagret i +datasegmentet av minnet. Dette er en statisk minneregion som allokeres ved +programstart og deallokeres ved programslutt. Referanser til denne +minneregionen kan markeres med lifetimen `static`. + +```rust +fn example() -> &'static str { + return "Hello world!"; +} + +fn main() { + println!("{}", example()); +} +``` + +Dette programet kompilerer og kjører uten problemer! Det printer det samme som +det første eksempelet vårt! + +```bash +$ cargo run --release + Compiling stack-string v0.1.0 (/home/kaholaz/code/rust/stack-string) + Finished release [optimized] target(s) in 0.18s + Running `target/release/stack-string` +Hello world! +``` + +La oss prøve oss på eksempel nummer to! + +```rust +fn example() -> &'static str { + return "Hello " + "world!"; +} + +fn main() { + println!("{}", example()); +} +``` + +Å nei :( Vi får igjen en feilmelding når vi prøver å kompilere... + +```rust +error[E0369]: cannot add `&str` to `&str` + --> src/main.rs:2:21 + | +2 | return "Hello " + "world!"; + | -------- ^ -------- &str + | | | + | | `+` cannot be used to concatenate two `&str` strings + | &str + | + = note: string concatenation requires an owned `String` on the left +help: create an owned `String` from a string reference + | +2 | return "Hello ".to_owned() + "world!"; + | +++++++++++ + +For more information about this error, try `rustc --explain E0369`. +``` + +I Rust finnes det flere typer som representerer strenger. Vi har vært innom +`&str`, men nå støter vi på en ny en: `String`. Mens `&str` representerer en streng +som vi låner, representerer `String` en streng vi eier. Det betyr at vi nå har +ansvaret for minnet strengen opptar, og at minnet automatisk blir deallokert +for oss når vi returnerer fra den nåværende funksjonen. For at det ikke skal +skje, må vi overføre eierskapet. En måte å gjøre dette på, er ved å returnere +verdien. Når vi har implementert forslaget fra kompilatoren og endret +returtypen ser programmet vårt slikt ut: + +```rust +fn example() -> String { + return "Hello ".to_owned() + "world!"; +} + +fn main() { + println!("{}", example()); +} +``` + +Dette kompilerer og kjører uten problemer! + +## Kan dette overføres til C? +Vi har sett at man kan unngå å gjøre enkle feil når det kommer til +minnebehandling ved å tenkte på eierskap i Rust. Hvordan kan vi bruke dette til +å fikse feilen vi hadde i C programmet vårt? + +```c +#include +#include +#include + +char* example() { + char* out = malloc(13); + strcpy(out, "Hello "); + return strcat(out, "world!"); +} + +int main() { + char* ex = example(); + printf("%s\n", ex); + + free(ex); + return 0; +} +``` + +Her må vi fohåndsallokere plassen til strengen i +[heapen](https://en.wikipedia.org/wiki/Manual_memory_management), samt passe på +at vi frigjør minnet etter at vi er ferdig med det. Legg merke til at selv om +ikke må tenke på eierskap, oppstår de samme problemene som Rust-kompilatoren +advarte oss om. Hvis man returnerer en peker til en verdi som kanskje +forsvinner før pekeren gjør det, kan man få en hengende peker. Hvis man ikke +tenker på hvem som har ansvaret for å deallokere minnet til verdier, kan man få +minnelekasjer. + +I motsetning til i Rust, gjør ikke C noen forskjell på pekere som vi låner og +pekere vi eier. Det betyr at det kan være uklarheter når det kommer til om det +er vårt ansvar å frigjøre minnet pekeren peker til. + +## Konklusjon +Selv om C ikke har et innebygd system for eierskap og livstider som Rust, betyr +det ikke at konseptet ikke har noen verdi når det kommer til utvikling av +programmer skrevet i C. C gir deg frihet til å programmere på lavt nivå. Til +gjengjeld krever det at man gjør en bevisst innsats for å unngå vanlige feil +ved håndtering av minne. Personlig har min forståelese og bevisthet knyttet til +minne blitt bedre etter at jeg nå har vasset litt rundt i Rustverdenen. Jeg +håper derfor at du (om du allerede ikke har gjort det) tar sjansen og dupper +tåa di nedi du også! Kanskje det lønner seg for deg og?