Unsafe-tilan toiminta
Mikäli osaaminen Rustista rajoittuu sen syntaksin tuntemiseen, voi helposti luulla, että tämä poistaa kaikki kääntäjän kuuluisat turvatakeet pois käytöstä. Tätä samaa saa yllättäen lukea myös erilaisista Rustiin liittyvistä blogipostauksista ja joskus jopa yliopistojen kursseilta. The Rust Programming Language -kirjan tunnollisesti lukenut, aloitteleva Rust-kehittäjäkin kuitenkin tietää jo, että unsafe antaa kehittäjän käyttää viittä tarkasti määriteltyä toimintoa, eikä poista muita kääntäjän ominaisuuksia käytöstä. [1]
Nämä 5 toimintoa ovat:
- Raakaosoittimen osoittaman datan käyttö
- Unsafe-funktion tai -metodin kutsuminen
- Muokattavan staattisen muuttujan käyttö tai sen arvon muuttaminen
- Unsafe-traitin implementointi
- Union-tyypin kentän käyttö
Kehittäjän tulee määrittää koodiin unsafe-blokki, jonka sisällä toimintojen käyttö on mahdollista. Tilan rajoitteet mahdollistavat sen, että sillä voidaan kiertää tietyt kääntäjän rajoitteet muuttumatta kuitenkaan helpoksi pakoluukuksi aloittelevalle Rust-ohjelmoijalle.
fn main() {
// Unsafe funktion kutsuminen vaatii unsafe-blokkia
let x = unsafe { deref_raw_pointer() };
println!("{x}");
}
// Unsafe-funktiossa voi käyttää unsafe-ominaisuuksia
unsafe fn deref_raw_pointer() -> i32 {
let x = 5;
let raw = &x as *const i32;
*raw
}
Tämä tarkoittaa sitä, että mahdolliset muistiturvallisuusongelmat voivat tapahtua vain hyvin rajoitetuilla alueilla koodissa. Näin näiden bugien löytäminen on huomattavasti helpompaa niin koodin arvioinnin aikana kuin myöhemminkin vaikka sovellus olisikin jo käytössä. Lisäksi "turvallinen" Rust toimii unsafe blokin sisällä samalla tavalla kuin sen ulkopuolellakin.
fn main() {
unsafe {
let mut string1 = String::from("test");
let copystring = &mut string;
/*
Lainauksentarkistaja ei salli muuttuvaa ja muuttumatonta
viittausta samaan muuttujaan edes unsafe-tilassa.
*/
println!("{string1} {copystring}");
}
}
Kuva 1: Muuttuvan ja muuttumattoman viittauksen samanaikaisen olemassaolon aiheuttama käännösvirhe.
Tila on kuitenkin olemassa syystä ja unsafe-funktioita sekä raakaosoittima hyödyntämällä on mahdollista saada ongelmia aikaiseksi.
fn main() {
let mut s = String::from("test");
/*
Erillistä unsafe funktiota hyödyntämällä on mahdollista luoda samaan
muistipaikkaan osoittava arvo hyödyntämällä raakaosoittimia
*/
let copystring =
unsafe {
String::from_raw_parts(s.as_mut_ptr(), s.len(), s.capacity())
};
println!("{s} {copystring}");
}
/*
Molemmat arvot pudotetaan ja ohjelma kaatuu, koska arvot ovat samassa
muistiosoitteessa
*/
Kuva 2: Kaksinkertainen muistipaikan vapautus kaataa ohjelman.
use std::ptr::null;
fn main() {
// Raakaosoittimen luominen ei ole unsafe-ominaisuus
let a: *const i32 = null();
// Raakaosoittimen arvon käyttö vaatii unsafe-tilaa.
unsafe {
// Null-osoittimen arvon käyttö aiheuttaa ohjelman kaatumisen
println!("{}", *a);
}
}
Kuva 3: Null-osoittimen arvon käyttö kaataa ohjelman.
Unsafe-tilan käyttökohteet
Mihin unsafe-tilaa on sitten hyödyllistä käyttää? On tärkeää muistaa, että unsafe-tilan käyttö ei ole itsessään huono asia, mikäli se tehdään syystä.
Laiteläheinen ohjelmointi
Omassa käytössä tarve on tullut esille sulautetuissa järjestelmissä, tosin näidenkin tilanne on parantunut Rustin ekosysteemissa sen verran, että nykyään voi käyttää helposti valmiita kirjastoja, jolloin ei itse tarvitse kirjoittaa unsafe-koodia. Myös muussa laiteläheisessä toiminnassa unsafe-tilan käyttö on joskus tarpeen, kun käsitellään muistiosoitteita. Tälläisia ovat esimerkiksi laiteajurit sekä vaikkapa käyttöjärjestelmät. Näissä syntyy usein tilanteita, joissa Rustin kääntäjä ei voi mitenkään taata muistiosoitteiden sisältöä, joten niiden käsittelyyn tarvitaan unsafe-tilaa.
Foreign Function Interface
Toinen käyttökohde on niin kutsuttu FFI (foreign function interface), joka mahdollistaa muiden ohjelmointikielien kanssa yhdessä toimimisen. Rustin kääntäjä ei voi taata muilla kielillä kirjoitetun koodin muistiturvallisuutta, joten ne vaativat unsafe-tilan käyttöä.
Suorituskykyoptimointi
Käyttökohteena voi olla myös suorituskykypullonkaulojen poistaminen. Unsafe-tilassa on mahdollista tehdä käsin sellaisia suorituskykyoptimointeja, joiden käyttö "turvallisessa" Rustissa olisi hankalaa. Tällä hetkellä esimerkiksi SIMD (single instruction multiple data) -käskyjen käyttö vaatii unsafe-tilaa standardikirjaston SIMD abstraktioiden ollessa vielä kokeellisessa vaiheessa.
Unsafe-tilan käyttö kirjastoissa
Tyypillinen tapa käyttää unsafe-tilaa Rustissa on rakentaa unsafe-blokkien ympärille turvallinen API. Tätä käytetään erityisesti kirjastoissa eli crateissa. Kirjaston käyttäjä voi surutta käyttää funktioita, joiden sisäinen toteutus hyödyntää unsafe-tilaa joutummatta kuitenkaan itse käyttämään unsafe-tilaa omassa koodissaan. Tämä on mahdollista, koska Rustissa unsafe-tilaa voi käyttää funktiossa tai metodissa, joka ei itsessään ole unsafe. Rustin ekosysteemissä moni kirjasto mukaanlukien Rustin standardikirjasto käyttääkin unsafe-koodia jossain määrin, mikä ei välttämättä näy kirjastojen käyttäjälle mitenkään. [2].
fn main() {
let x = raw_pointer_value(); // Ei vaadi unsafe blokkia
// Tulosta unsafe-funktiosta turvallisen apin kautta saatu arvo
println!("{x}");
}
/// Tämän funktion ei tarvitse olla unsafe
fn raw_pointer_value() -> i32 {
let a = unsafe { deref_raw_pointer() }; // Vaatii unsafe blokin
a // funktion palautusarvo
}
unsafe fn deref_raw_pointer() -> i32 {
let x = 5;
let raw = &x as *const i32;
*raw
}
Yhteenveto
Unsafe-tilalla on siis aikansa ja paikkansa, eikä sitä tule kohdella automaattisesti huonona asiana. Unsafen tilan vahvuus on juuri se, että se sallii tarkasti rajattujen toimintojen tekemisen selkeästi merkityillä alueilla poistamatta kuitenkaan suurinta osaa kääntäjän turvatoimista käytöstä. Tilaa on myös pakko käyttää tietyissä käyttökohteissa, kuten juuri laiteläheisessä ohjelmoinnissa. On kuitenkin hyvä muistaa, että suurimmassa osassa koodista unsafe-tilaa ei tarvita ja sen liiallinen käyttö ei myöskään ole tavoiteltava asia. Tämä ei kuitenkaan ole usein ongelma tilan rajoitusten takia. Unsafe-koodin pitäminen automaattisesti huonona asiana ja sillä pelottelu erilaisissa opetusmateriaaleissa ei kuitenkaan ole hyödyllistä ja aiheuttaa vain sen, että sen käyttötarkoitusta ei ymmärretä oikein.
Lähteet
1 Unsafe Rust - The Rust Programming Language --- doc.rust-lang.org. https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html
2 Rust Foundation --- foundation.rust-lang.org. https://foundation.rust-lang.org/news/unsafe-rust-in-the-wild-notes-on-the-current-state-of-unsafe-rust
3 Meet Safe and Unsafe - The Rustonomicon --- doc.rust-lang.org. https://doc.rust-lang.org/nomicon/meet-safe-and-unsafe.html