Hopp til innhold

Rust

et kompilert språk som sjekker minnebruken for deg

Hva det er

Rust er et programmeringsspråk for arbeidet som tradisjonelt gjøres i C og C++, der et program styrer sitt eget minne og ofte lenkes inn i programvare skrevet i andre språk. Kompilatoren lager maskinkode på forhånd. Versjon 1.0 ble kunngjort den 15. mai 2015 som et språk som forener kontroll over ytelse på lavt nivå med garantier for trygg minnebruk, oppnådd uten en søppelinnsamler og uten et kjøretidsmiljø, og det er nettopp det som lar et Rust-bibliotek tre inn i stedet for et C-bibliotek.

Minnet styres gjennom eierskap (ownership), et sett regler som kompilatoren sjekker. Hver verdi har én eier om gangen, og når eieren går ut av virkeområdet sitt (scope), blir verdien droppet og minnet frigjort, på et punkt som ligger fast når programmet kompileres, og ikke når en søppelinnsamler neste gang ser etter. Kode som trenger en verdi uten å ta den, låner en referanse, og lånereglene tillater til enhver tid enten én referanse som kan endre verdien, eller et hvilket som helst antall som bare leser den, og de krever at hver referanse er gyldig. Et program som bryter en regel, kompilerer ikke. De samme reglene utelukker datakappløp (data races), så to tråder som skriver til de samme dataene uten synkronisering, gir en kompileringsfeil og ikke en feil som dukker opp av og til.

Rust har ikke unntak (exceptions). En funksjon som kan feile på en forventet måte, returnerer et Result, som holder enten en verdi eller en feil, og i mange tilfeller krever kompilatoren at den som kaller, tar hånd om feilen før koden bygger. En feil som programmet ikke kan komme seg etter, som å lese forbi slutten av en array, går i stedet gjennom makroen panic!.

Statisk analyse er konservativ, så kompilatoren avviser noen programmer som ville gått fint, og noe arbeid, som å styre maskinvare eller kalle et annet språk, kan ikke sjekkes i det hele tatt. For de tilfellene finnes unsafe, som låser opp fem ekstra evner, blant dem å dereferere en rå peker og å kalle en funksjon merket som usikker, noe som som standard gjelder hver funksjon deklarert fra C. Det slår ikke av lånesjekkeren (borrow checker); det markerer linjene der programmereren, og ikke kompilatoren, går god for at minnet brukes trygt, så en minnefeil kan bare bo inne i en unsafe-blokk. Rust-boken råder til å holde slike blokker små og pakke dem inn i et trygt grensesnitt, og slik er deler av standardbiblioteket bygd.

Det kommer en ny kompilator hver sjette uke. Endringer som ville ødelagt eksisterende kode, venter på en utgave (edition), og en ny utgave kommer omtrent hvert tredje år. Det finnes fire av dem: 2015, 2018, 2021 og 2024, og den nyeste ble stabil den 20. februar 2025 med Rust 1.85.0. Hver pakke oppgir utgaven sin i Cargo.toml, hver kompilator bygger alle utgavene som kom før den, og pakker med ulike utgaver lenkes sammen, så et prosjekt går over når det selv velger det. Cargo, verktøyet for avhengigheter og bygging, følger med språket, rustfmt holder kodestilen konsekvent, og biblioteker fra tredjeparter kommer som pakker som kalles crates.

Hvorfor det er viktig

I C og C++ kompilerer en peker som brukes etter at minnet den peker på, er frigjort, eller to tråder som skriver til de samme dataene, uten protester, og så feiler programmet senere, av og til og ofte langt fra årsaken. Rust gjør slike programmer til kompileringsfeil, og fordi sjekkene kjører når programmet bygges, betaler det ferdige programmet ingenting for dem.

Uten en søppelinnsamler og uten et kjøretidsmiljø å ta med seg kan Rust-kode ligge under programvare skrevet i andre språk. Alt som kan kalle C, kan kalle en kjerne i Rust gjennom et C-grensesnitt, så én godt testet implementasjon kan betjene klienter på flere plattformer. Rust-boken nevner kommandolinjeverktøy, nettjenester, innebygde enheter og store deler av nettleseren Firefox blant bruksområdene i produksjon.

Kostnaden betales på forhånd. Koden må formes slik at kompilatoren kan se at den er trygg, og det betyr ofte å bestemme hvem som eier en verdi, i stedet for å sende referanser fritt rundt. Boken sier selv at eierskap tar tid å venne seg til. Når kompilatoren avviser et program den ikke kan verifisere, er svaret som regel en annen struktur og ikke en vei rundt sjekken.

Stabilitet er den andre halvdelen av avtalen. Siden 1.0 har endringer som bryter eksisterende kode, i stor grad vært utelukket, så en endring som kunne ødelagt gammel kode, kommer bare gjennom en utgave som en pakke velger å ta i bruk.

Slik ser det ut i praksis

I porteføljen er Rust kjernen under en native meldingsklient for macOS: den godt testede kjernen til protokollen er skrevet i Rust og eksponerer et C-grensesnitt, og klienten, 11 141 linjer Swift fordelt på 53 filer, lenker den inn som et statisk arkiv kompilert fra en låst oppstrømsrevisjon. Låsingen betyr noe fordi C-grensesnittet er kontrakten: en kjerne i bevegelse endrer kontrakten uten at én linje Swift endres. Alt C-grensesnittet returnerer, har en eierskapsregel, og å ta feil av én gir en lekkasje eller en krasj uten noen kompilatoradvarsel imellom. Derfor eier en tynn wrapper hver peker og hver streng som krysser grensen, og ikke noe lag over den ser en rå peker.

Å lenke den kjernen inn i en applikasjon betyr å levere alt den er avhengig av, og det er temaet for en lisenskontroll av 644 Rust-crates. Hver crate deklarerer lisensen sin som et uttrykk, og sjekken går gjennom hvert uttrykk i hele avhengighetsgrafen mot en tillatelsesliste ved hvert bygg. Den leser OR som et valg mellom lisenser og AND som en plikt til å overholde begge, og alt uten treff får bygget til å feile. Fordi en sjekk som aldri har feilet, ser lik ut som en som ikke kan feile, ble den med vilje gjort til å feile to ganger: én gang ved å skrive om lisensen til en crate til et vilkår listen avviser, og én gang ved å flytte den låste kjernerevisjonen uten å generere lisenskontrollen på nytt. Den leser bare deklarerte metadata og er ikke juridisk rådgivning.

Dette nettstedet har ingen egen Rust-kildekode. Det møter språket på avstand, gjennom Ruff, som er skrevet i Rust og sjekker hvert Python-skript en commit rører.

Hvor det går galt

Det meste av trøbbelet med Rust ligger i kantene av det kompilatoren sjekker: aritmetikk i et optimalisert bygg, grensen mot et annet språk, og kode som kommer inn med en avhengighet.

Aritmetikken går rundt i et utgivelsesbygg. En teller som stoppet med en panikk ved overflyt under testing, blir i det stille til et lite tall i produksjon. I feilsøkingsbygg (debug builds) sjekker Rust heltallsaritmetikk for overflyt og går i panikk. Et bygg med --release utelater sjekken og går rundt, så 256 i en u8 blir 0, og boken regner det som en feil å stole på at verdien går rundt. Der en verdi kan flyte over, bør du velge oppførselen eksplisitt: checked_* returnerer None, saturating_* stopper ved grensen, og wrapping_* går rundt med vilje.

fn add_stock(on_hand: u8, delivered: u8) -> Option<u8> {
    // None when the sum does not fit, in debug and release alike
    on_hand.checked_add(delivered)
}

Minnefeilene kommer tilbake ved C-grensen. Et program der Rust-siden bygger uten én eneste advarsel, lekker minne eller krasjer inne i et kall til et annet språk. Funksjoner som er deklarert i en extern-blokk, er usikre å kalle fordi det andre språket ikke håndhever noen av reglene til Rust, så hvem som frigjør hver peker, er en konvensjon kompilatoren ikke kan se. Gi én wrapper ansvaret for hver peker og hver streng som krysser, skriv ned hver eierskapsregel ved siden av kallet, og eksponer bare et trygt grensesnitt over den.

En panikk når C, og prosessen avbrytes. En Rust-funksjon som kalles fra C, går i panikk, og i stedet for en feilmelding stopper hele programmet. Siden Rust 1.81, utgitt den 5. september 2024, blir prosessen avbrutt når en panikk prøver å forlate en funksjon deklarert med det vanlige "C"-grensesnittet. Gjør feil om til returverdier før de når grensen, og deklarer en funksjon med "C-unwind" bare der koden på den andre siden forventer at stakken blir avviklet (unwinding).

En rutinemessig oppdatering endrer lisensen for hele bygget. En versjonsoppgradering tre nivåer ned tar inn en crate med en lisens produktet ikke kan godta, og ingenting i bygget sier fra. Hver crate deklarerer sin egen lisens, og forpliktelsene legges sammen over hele grafen. Sjekk uttrykket til hver crate mot en tillatelsesliste ved hvert bygg, la alt uten treff feile, og bryt sjekken med vilje én gang for å bevise at den slår ut.

Les mer

Ordliste