Skip to main content
Keksit
Käytämme evästeitä analytiikkaan, markkinointiin ja sen kohdentamiseen. Voit lukea selosteen täältä.
16.10.2024 | Teknologia

Unsafe-tila Rust ohjelmointikielessä

Rust-ohjelmointikielen suosio on jatkuvassa kasvussa ja se näkyykin erilaisina kursseina ja blogipostauksina aiheeseen liittyen. Näissä on kuitenkin yksi asia, joka pistää aina allekirjoittanutta silmään ja se on Rustin unsafe-ominaisuuden täydellinen väärinymmärtäminen. Käydään siis tässä blogipostauksessa läpi, mitä tuo pelottavalta kuulostava avainsana oikeastaan tarkoittaa ja mitä käyttökohteita sillä on.

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}");
    }
}

Muuttuvan ja muuttumattoman viittauksen samanaikainen olemassaolo aiheuttaa käännösvirheenKuva 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
*/

saman arvon pudotus kahteen kertaan kaataa ohejlmanKuva 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);
    }
}

null arvon omaava osoitin kaataa ohjelmanKuva 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

Tuomas Rinne
Kirjoittanut Tuomas Rinne

Piditkö tästä artikkelista? Anna sille taputus!