Rust dilini öğrenmek ve etkili şekilde kullanabilmek için pek çok kaynaktan yararlanıyorum. Cuma günleri düzenli olarak bülten gönderen This Week in Rust'tan Amazon'dan aldığım kitaplara, Udemy eğitimlerinden kişisel web sitelerine kadar birçok kaynak...Bazen bu kaynaklardan yakaladığım bilgileri defterlere not aldığımı fark ettim. Deftere not almak o kadar da teknolojik sayılmayan ama etkili bir öğrenme yöntemidir aslında. Bu dokümanda ilgili notları tekrar etmek ve bir düzene koymak açısından oluşturuldu.
- cargo.toml, bir rust projesinin kendisi ve bağımlılıkları hakkında çeşitli bilgiler tutar. Dosyanın içeriği kolayca okunabilir bir formattadır ve yorum satırların eklenmesine de izin verir niteliktedir. TOML'ın açılımı ise Tom's Obvious, Minimal Language 'tir. Yazarın adı Tom Preston-Werner.
- Rust'ın ilgi çeken yazımlarından birisi Turbo Fish olarak adlandırılıyor. ::<> şeklindeki bu yazımda <> balığı, :: ise o hızla ilerlerken arkasında bıraktığı kabarcıkları ifade etmekte :D
- String::new(); heap'te bir referans açar. Ayrıca String, Smart Pointer'dır.
- Ownership kuralları:
- Bir değer (value) tek bir değişken (variable) tarafından sahiplenilir.
- Değişken sahibi scope dışına çıktığında tuttuğu değe yok olur (deallocate)
- Bir t anında sadece tek bir sahip (owner) olabilir.
- Double Free, memory corruption'a yol açan bir durumdur. Aynı değere refere eden iki String değişken düşünelim. Scope sonlanırken kurallara göre her ikisi de deallocate edilmeye çalışılır. Bu durum Double Free olarak adlandırılıyor. Rust buna göre bir değerin sahipliğinin tek bir değişkende olmasını garanti eder. Örneğin aşağıdaki input değerinin sahipliği s'de olduğu için derlenmez.
use std::io;
#[allow(unused_variables)]
fn main() {
let mut input=String::new();
let mut s=input; // The ownership of the string is moved to the variable s
io::stdin().read_line(&mut input);
}
- Rust dilinde tüm string'ler UTF8 formatındadır. Dolayısıyla bir karakter veri 1 byte'tan fazla yer tutabilir. Bunun sebebi Unicode kullanımıdır. Örneğin emojiler, japon harfleri. Buna göre aşağıdaki kod farklı çalışır.
fn main() {
let emojis=String::from("🍔🍟🍨🍯");
let slice=&emojis[..4];
println!("{}",slice); // Sadece 🍔 döner
let slice=&emojis[..8];
println!("{}",slice); // 🍔🍟 döner
}
- Tüm dosya ve klasörler module'dür. Rust projesinin kendisi ise crate olarak adlandırılır. Rust proje hiyerarşisinde birden fazla dosya olabilir ki her biri birer module'dür. Ayrıca bu dosyalar klasörler içinde yer alabilir ki bu klasörler de ayrıca module'dür. Dosya veya klasör şeklindeki bir modülü uygulamada kullanmak istediğimizde mod anahtar kelimesini kullanırız. Bazen klasörler içinde gördüğümüz mod.rs dosyasının bir kullanım amacı o klasörden public olarak açılacak diğer enstrümanların tanımlandığı yer olmasıdır.
- Bulunduğumuz modülden bir üst modüle ulaşmak istediğimizde super operatörünü kullanabiliriz. Bazen de crate:: şeklinde kullanımlara rastlarız. crate, bulunduğumuz projenin root module'ünü işaret eder.
mod http {
mod request {
use super::method::Method; // http modülüne çık, oradan method'a geç, oradan da public Method enum tipine ulaş gibi.
struct Request {
}
}
mod method {
pub enum Method {}
}
}
- Her dosya esasında bir module'dür demiştik. Yani server.rs şeklinde bir dosya açmak, mod server şeklinde bir module açmakla aynı şeydir. Ayrı bir dosya açtığımızda genellikle main fonksiyonunun olduğu yere de mod bildimi yapılır. Yani server.rs için main.rs içinde mod server; şeklinde bir tanım eklenir. Sebebi nedir biliyor musunuz? Derleyici, mod server; yazan yeri mod server { } olarak kabul edip içeriğini server.rs içeriği ile doldurur. Sanki önyüzde app bileşeni içerisinde diğer bileşenleri tag olarak eklemek gibidir.
- Örnek bir klasör yapısı ile modül kullanımına bakabiliriz.
server
--->src
--->main.rs
--->server.rs (module)
--->tcp (module)
------>package.rs (sub module)
------>parser.rs (sub module)
------>mod.rs
- Rust, exception handling gibi bir mekanizma içermez. Bunun yerine olası tüm durumların değerlendirilmesini ister. Result<T,Err> ile recoverable hataların kontrolünü ele alabiliriz. Birde kurtarılamayan unrecoverable ve programı sonlandıran hata durumları vardır. Rust her iki durumu ayrı ayrı ele alırken pekçok dilde hepsi aynı istisna yönetimi mekanizması ile kontrol edilmeye çalışılır.
- Sonsuz döngülerde label kullanarak break çağrısı sonrası nereye sıçrayacağımızı söyleyebiliriz.
fn main() {
'outer: loop {
'inner: loop {
break 'outer; // outer olarak isimlendirilmiş loop'a sıçramamızı sağlar.
}
}
}
- Bir rust programının çalıştığı klasörü platform bağımsız bulabiliriz. Bunun için env! makrosundan ve geçerli bir parametreden yararlanırız. Bu sayede örneğin çalıştığı yerdeki bir klasörü de ele alabiliriz. Mesela bir web server yazdığımızı düşünelim. static dosyaların olduğu path'e ulaşmak için bu yolu kullanabiliriz.
use std::env;
#[allow(unused_variables)]
fn main(){
// let default_path=env!("CARGO_MANIFEST_DIR");
let default_path=format!("{}/public",env!("CARGO_MANIFEST_DIR"));
// Envrionment ile public_path tanımı yapılmışsa kullan yoksa default_path'i kullan
let public_path=env::var("PUBLIC_PATH").unwrap_or(default_path);
}
- Rust, Referans içeren struct türlerinden açık bir şekilde (explicitly) lifetime belirtilmesini ister. Öyle ki rust ortamında tüm referansların bir yaşam ömrü vardır.
- Rust'ın memory safety ve thread safety konusunda uyguladığı kurallar aşağıdaki durumların oluşmasını engeller. Üstelik bunlar derleme zamanında tespit edilir.
- Data races : İki veya daha fazla thread'in eş zamanlı olarak aynı veriye erişmesi ve thread'ler den en az birisinin mutation halinde kalması durumunda oluşur.
- Dangling pointers : Aynı veri bölgesini işaret eden iki referanstan ilki deallocate olduktan sonra boşalan veri bloğunu işaret etmeye devam eden diğer refaransa verilen isimdir.
- Use after free : Referans edilen bir bellek bölgesinin serbest bırakıldıktan sonra kullanılmaya çalışılması halidir.
- Double free : Aynı pointer için birden çok defa serbest bırakma (free) operasyonu icra edilmesi durumudur.
- No pointer dereferences :
- Buffer overflows :
- Ödünç alma (borrowing) kurallına göre n tane immutable referans ödünç alımı mümkünken herhangi bir t anında sadece 1 tane değiştirilebilir (mutable) referans ödünç alınabilir. Bu sayede data races ihlallerinin önüne geçilir.
fn main() {
// n tane immutable mümkündür
let mut input = String::new();
let s1=&input;
let s2=&input;
println!("{},{}",s1,s2);
// Derlenmez!
// let mut s1=&mut input;
// let s2=&input;
// Bu da derlenmez!
// let mut s1=&mut input;
// let mut s2=&mut input;
}
- Rust, C dilindeki gibi belleğin verimli, düşük maliyetli kullanımı ile yüksek performans ve Java'da ki gibi memory safe bir ortamın oluşmasını garanti edecek şekilde tasarlanmış bir derleyici kullanır.
- 2006 yılında Mozilla çalışanlarından Grayon Hoore tarafından kişisel bir proje olarak başlamıştır. 2010'da Mozilla'nın bir araştırma projesi haline gelir. 2015 yılında Rust 1.0 sürümü yayınlanır. 2020'de Mozilla'dan Rust Foundation'a geçer ki bu konsorsiyumun kurucu ortakları AWS, Google, Huawei, Microsoft ve elbette moz://a' dır.
- Amazon tarafından yapılan bir araştırmaya göre enerji tüketimi ve işlem süreleri açısından en verimli diller arasında C'den sonra 2nci sırada gelmektedir. Bu anlamda Java, Go, C#, Python gibi yüksek seviyeli dilleri geride bırakmıştır.
- Trait'ler nesneler arasında fonksiyon paylaşımının bir yoludur ve diğer dillerdeki interface türüne oldukça benzerler.
- Rust'ta standart hata yönetiminde Result tipi kullanılır. Result sonuçlarını pattern matching ile kontrol altına alabiliriz. Unwrap fonksiyonu ile doğrudan Result sonucu alınır ama hata oluştuysa program çöker. Bu yüzden çok tercih edilmemelidir. ? operatörü ile Result bir hata içeriyorsa kolayca yakalanır lakin ?'in olduğu fonksiyonun hata nesnesini döndürmesini gerektirir.
fn main() {
// Klasik yol
match work_result {
Ok(r) => do_something(r),
Err(e) => handle_error(e)
}
// Option gibi Unwrap kullanımı. Lakin hata varsa program çöker
process_result.unwrap()
}
// ? operatörü ile
fn apply() -> MyError {
do_something()?;
}
- Modül bildirimlerinde modül içerisindeki enstrümanlara daha kolay ulaşmak için prelude standardı kullanılabilir. Örneğin,
mod db;
mod prelude{
pub use somelib::prelude::*;
pub const WIDTH:i32=80;
pub const HEIGHT:i32=50;
pub use crate::db::*;
}
use prelude::*;
fn main(){
}
- Modüller ağaç yapısı şeklinde organize olurlar. crate:: ifadesi ile root modüle çıkılır. Toml dosyasına sahip her tür rust örneği bir crate dir.
- cargo run çağrımı arkasından kendi komut satırı argümanlarımızı göndermek istersek rust'ın diğer komut satırı enstrümanları ile karışmamaları için -- ifadesinden yararlanabiliriz. cargo run -- -silent -on gibi
- cargo aracı modülleri paralel olarak derleyebilir.
- Pek çok dilde tek bir string türü vardır. Rust'ın iki string türü ile ilgilendiğini söyleyebiliriz. Birisi kendi değerine sahip çıkan ve heap'i kullanan String, diğeri de bir String içeriğindeki parçaları ifade edebilen referans string, yani &str.
- Rust Standard Library (std olarak kısaltabiliriz) işletim sistemi içerisinde sistem çağrıları yapmak için (syscalls) libc veya muadili bir aracı kullanır. Doğrudan sistem fonksiyonları da çağırabilir. Ayrıca 1.59.0 sürümü ile birlikte assembler kodlarını çalıştırmak da mümkündür.
- Rust Standard Library platform bağımsızdır.
- Rust Standard Library içerisindeki modüller Syscalls-Oriented ve Computation-Oriented olarak iki gruba ayrılır.
- Syscalls-Oriented: Sistem donanımı ve kaynaklarını doğrudan yönetmek için kullanılan modülleri içerir.
- Computation-Oriented: Verinin gösterimi, modellenmesi, işlenmesi, hata yönetimi, temel veri türleri gibi modülleri içerir.
- Rust'ın array veri yapısı value-type türündendir ve stack bellek bölgesinde sabit uzunlukta olacak şekilde kullanılır.
- Bir fonksiyona gönderilecek parametre derleme zamanında belli değilse any tipi kullanılabilir.
- isize ve usize tipleri 32 bit sistemlerde 32 bit(4 byte), 64 bit sistemlerde ise 64 bit(8 byte) uzuğunluğundadır.
- Bellek yönetimi denilince şunları düşünebiliriz;
- static memory allocation (stack)
- dynamic memory allocation (heap)
- memory deallocation (bir değişkenin scope dışına çıkması sonrası destructor'un çalışması)
- clone, copy işlemleri
- raw ve smart pointer'ların yönetimi
- Rust dilinde raw pointer'lar çok sık kullanılmazlar. Mutable ve immutable tanımlanabilirler ve mutlaka unsafe kod blokları içerisinde ele alınırlar. Dolayısıyla derleyici bellek güvenliğinin sorumluluğunu üstüne almaz, bunu programcıya bırakır.
- Bazen nesnelerin bellekteki sabit lokasyonlarda kalmasını ve hiçbir şekilde taşınmamasını (move) isteyebiliriz. Kendisini referans eden bağlı listeler'de (Linked List) olduğu gibi. Bu gibi durumlarda için Rust, Pin
veri tipini sunmaktadır.
- Rust dilinde versiyonlama Major.Minor.Patch formatında yapılır. 1.4.12 gibi. Büyük değişikliklerde Major sürüm artar. Yeni fonksiyon veya özelliklerin eklenmesinde ise Minor sürüm artar. Var olan sürümdeki bug fix'ler için Patch versiyonu artırılır. Yeni bir minor versiyon çıkılması halinde patch değeri de sıfırlanır. Yani 1.2.3 şeklindeki bir sürüme yeni özellikler eklendiyse yeni sürüm 1.3.0 olacaktır.
- Bir rust kütüphanesini crates.io'ya alırken TOML dosyasında mutlaka olması gereken bazı bilgiler vardır.
- authors: yazar bilgileri
- description: ürünün ne yaptığı hakkında kısa bir açıklama.
- homepage: ürüne ait web sayfası (en kötü ihtimalle github adresi)
- repository: kaynak kodun yer aldığı github reposu
- readme: projede bir Readme.md dosyası olmalıdır.
- keywords: ürünü sınıflandıran takılar (tag) için
- categories: ürünün dahil olduğu kategori/kategoriler (crates.io dakiler kullanılabilir)
- license : MIT or Apache 2.0 gibi bir lisanslama bilgisi. Lisanlama var ise proje kaynak klasöründe COPYING dosyası ve license klasörü ile içeriği de olmalıdır.
- Bir rust projesinde birden fazla binary kullanmak istersek kaynak dosyaları src/bin klasörü altında toplamamız yeterlidir.
- Rust'ın stable, beta ve nightly sürümleri arasında kolayca geçişler yapılıp istenilen sürüm ile çalışılabilir.
# stable sürümü yüklemek için
rustup install stable
# beta sürümünü yüklemek için
rustup install beta
# nightly build sürümlerden yüklemek için
rustup install nightly
# hatta belli bir günün nightly sürümünü yüklemek için
rustup install nightly-2022-04-13
# Belli bir sürüme geçmek için rustup default nightly
# Var olan sürümü güncellemek için
rustup update
# Güncel sürümü öğrenmek için
rustc --version
# mevcut sürümleri görmek için
rustup show
- Rust'ın 1.60.0 sürümü ile birlikte build işlemlerine ait bazı ölçümleri gösteren bir cargo parametresi eklendi.
cargo build --timings
Bu çalıştırma işlemi sonrasında cargo-timing bilgilerini içeren html dosyaları target/cargo-timings klasörü altına atılıyor.
- Bir rust binary'sinin bellek görüntüsüne bakmak için xxd aracından yararlanılabilir. intro klasöründe yer alan basit örnek için;
cargo build
cd target/debug
xxd -g1 intro
- Her rust programı bir process olarak açılır ve işletim sistemince sağlanan bir sanal bellek alanına yerleşir. JVM, V8 ve Go'nun bellek kullanım tasarımları ile karşılaştırıldığında generational veya karmaşık alt yapılardan oluşmaz. Bir GC mekanizması yoktur ancak bellek yönetimi için Ownership, Resource Acquisition is Initialization (RAII), Borrowing & Borrow Checker, Variable Lifetimes ve Smart Pointers kullanır.
- Derleme zamanında boyutu tahmin edilemeyen her veri heap'te tutulur = Dynamic Data Ancak istersek sabit uzunlukta veriler için Box< T > smart pointer'ını kullanarak Heap üstünde de yer ayrılmasını (allocation) sağlayabiliriz.
- Veri boyutları derleme zamanında bilinen veriler stack üstünde durur. Thread başına bir Stack söz konusudur. Fonksiyon çerçeveleri (Function Framews), primitive tipler, struct veri türü ve pointer'lar burada durur.
- Normal koşullarda tüm değerler stack üstünde tutulur ancak String veya Vector gibi dinamik büyüyen içerikler varsa veya kasıtlı olarak Box< T > ile açıkça heap üstünde yer ayrılmasını istersek bu durum geçerli olmaz.
- Bir Rust programının temel bellek açılımı düşünüldüğünde aşağıdakileri söyleyebiliriz.
- Main fonksiyonu stack üstünde *main frame" de yaşar.
- Her fonksiyon çağrımı Stack'e bir frame-block olarak eklenir. Fonksiyonun kullandığı tüm parametreler(return parametresi dahil) fonksiyon için açılan bu frame içerisine dahil edilir. Bilindiği üzere Stack Last-In-First-Out ilkesine göre çalışır.
- Static değerlerin hepsi tipine bakılmaksızın doğrudan Stack üstünde saklanır.
- Dinamik tipler heap üstünde konumlanır ve stack tarafından smart pointer'lar ile referans edilir.
- Statik veriler içeren bir Struct, heap üzerinde tutulur ve herhangi bir dinamik değer içeriyorsa bu değer heap üzerinde tutulurken, işaretçisi stack'te struct için ayrılan alanda yer alır.
- Bir fonksiyon başka bir fonksiyonu çağırdığında, çağırılan fonksiyon Stack'in en üstüne frame olarak alınır ve LIFO prensibine göre ilgili fonksiyondan dönüş sağlandığında Stack'ten çıkartılır.
- Main işlevi sonlandığında heap'teki nesneler de Garbage Collector'lerin aksina anında yok edilir.
- Rust'ın ownership kuralları her ne kadar katı olsa da değişken sahipliğinin taşınabilmesi için yollar da sunar. Kopyalama yoluyla veya referans vererek.
- Rust, C++'tan beri gelen RAII (Resource Acquisition is initialization) prensibini de kullanır. Buna göre bir değişken oluştuğunda değeri onunla ilişkilendirilir ve değişken scope dışına çıktığında destructor'u çağırılıp sahip olduğu kaynaklarla beraber yok olur. Bu yüzden manuel olarak belleği boşaltmaya gerek kalmaz ve memory leak problemleri yaşanmaz.
- Borrowing'de amaç kaynağın sahipliğini (ownership) almadan kullanabilmektir.
- Smart pointer kavramı da C++'tan gelmiştir. Normal pointer'ların aksine referans ettikleri verinin aynı zamanda sahibidirler.
- Bir rust projesinde çalıştırılabilir ve yeniden kullanılabilir nitelikli modüllerin belirtilmesinde cargo.toml dosyasındaki bildirimlerden yararlanabiliriz. Aşağıda görüldüğü gibi. Projenin imagix klasörü bir library olarak ele alınırken imager.rs modülü executable binary olarak işaretlenmiştir.
[package]
name = "imager"
version = "0.1.0"
edition = "2021"
[lib]
name="imagix"
path="src/imagix/mod.rs"
[[bin]]
name="imager"
path="src/imager.rs"
- toml dosyasına cargo ile komut satırından paket yüklemek de mümkündür. Örneğin aşağıdaki komut ile projeye axum, tokio, serde ve minijinja küfeleri eklenir. Üstelik bazı feature'ları ile birlikte.
cargo add axum tokio -F tokio/full serde -F serde/derive minijinja -F minijinja/builtins
- Kendi yazdığımız makroların neye dönüştüğünü görmek için cargo-expand aracından yararlanabiliriz. Bu araç nightly modda çalışır ve dolayısıyla çalışma zamanını nightly moda çevirmek gerekir. Gerekli adımlar aşağıdaki gibidir.
# Aracın install edilmesi için.
cargo install cargo-expand
# Nightly moda geçmek için (Öncesinde install etmek gerekebilir)
rustup install nightly
rustup default nightly
# Örneği çalıştırırken
cargo expand
# Çalışma zamanını stable moduna çevirmek için
rustup default stable
- İsimlendirme Standartları (Naming Conventions) Bu standartlar bir kodun okunurluğunu artırma ve takımların küresel olarak aynı standartlarda çalışması açısından kıymetlidir. Rust için önerilen isimlendirme kuralları aşağıdaki gibi özetlenebilir.
Kaynak | Standart |
---|---|
Modüller (Modules) | snake_case |
Tipler (Types) | UpperCamelCase |
Traits | UpperCamelCase |
Enum değerleri | UpperCamelCase |
Metot/Fonksiyonlar | snake_case |
Makrolar (Macros) | snake_case! |
Yere Değişkenler (Local Variables) | snake_case |
Değişmezler (Constants) | SCREAMING_SNAKE_CASE |
Statik Değişkenler (Statics) | SCREAMING_SNAKE_CASE |
Ömür Parametreleri (Lifetimes) | lowercase |
Generic tip parametreleri | UpperCamelCase |
- C#, Java gibi nesne yönelimli dillerde yapıcı metotlar (Constructors) genelde özel ve tek tipte tanımlanırlar. Örneğin C# dilinde dönüş tipi olmayan ve sınıfla aynı isme sahip metotlar constructor olarak kabul edilir. Rust tarafında nesne başlatıcısı fonksiyonlara ait özel bir tanım yoktur ancak yaygın olarak new veya with_more_details gibi isimlendirilmiş metotlar bu amaca hizmet edecek şekilde değerlendirilirler.
Bu konuda dokuz önemli kural var. En nihayetinde Rust, veriler üzerinde olabilecek beklenmeye güncellemeleri en aza indirmek istediği için aşağıdaki kurallara sahiptir. Yüksek bellek güvenliği ve tutarlılığı bu şekilde sağlanır, olası kaçaklar henüz derleme aşamasında tespit edilir.
- Ownership Kuralları 2. Bir value belli bir anda tek bir değişken, yapı, vektör vb tarafından sahiplenilebilir 3. Bir değeri başka bir değişkene atadığımızda, fonksiyona pasladığımızda, vektöre eklediğimiz vb hallerde o değeri taşımız oluruz (moved) Bu duruma eski değer artık kullanılamaz. 4. Bunlara karşın bir değere sadece okuma amaçlı erişen sayısız referansı oluşturabilir ve aynı zaman diliminde kullanabiliriz.
- Borrowing Kuralları 6. Bir referans var olan bir değeri sahiplenmişse taşınamaz. 7. Bir değerin referansını o anda onu kullanan herhangibir read-only referans yoksa değiştirebilir (mutable) tanımlayabiliriz. Lakin t anında sadece bir tane mutable referans olabilir. 8. Bir değere ister mutable ister immutable referans varsa bu değeri sahibi aracılığı ile değiştiremeyiz. 9. Sayılar, boolean'lar, karakterler, içerdiği elamanları kopyalanbilir olan array ve tuble türler tarafından işaret edilen değerler taşınmak (move) yerine kopyalanırlar.
- Lifetimes Kuralları 11. Bir değişken kapsam (scope) dışına çıktığında sahibi olduğu değer düşürülür (Memory Cleaning) 12. Değişken değerleri eğer onları halen referans eden aktif elemanlar varsa bellekten düşmezler (drop) 13. Bir değere yapılan referanslar, referans bulundukları değerden daha uzun ömürlü olamaz.