Tizim dasturlash deb inson foydalanuvchilar uchun emas, boshqa dasturlar foydalanishi uchun dastur tuzishga aytiladi, va shuning uchun katta tezlik va xavfsizlikni talab qiladi. Rust xotira bilan qanday ishlashi, va xotira xavfsizligini qanday ta’minlashini shu maqolada gaplashamiz.
Rust dasturlash tili uchun motivatsiya
(Bu qismdagi ko’p narsa soddalashtirilgan)
Python yuqori-daraja dasturlash tilida ma’lumotlar qanday saqlanishiga nazar solaylik. Masalan, Pythonda daraxtlarni saqlamoqchi bo’lsak, quyidagi classni ishlatishimiz mumkin:
@dataclass(slots=True)
class Tree:
value: int
left: Tree | None = None
right: Tree | None = None
t1 = Tree(1)
t2 = Tree(2)
t3 = Tree(3, t1, t2)
Lekin kompyuterning xotirasi chiziqlidir. Xotira shunchaki bytelar ketma ketligi. Python qandaydir qilib bu operatsiyalarni chiziqli xotiradan bytelarni o’qishga aylantiryapti. Lekin qanday qilib? Kod ishga tushganda aslida nima bo’ladi? Bu juda ham quyi-daraja savoldir. Oddiygina dataclass bo’lgan kod shunchaki xotirani qaysidir qismlarini o’qish yoki yozishga aylanadi.
Xotira bytelar ketma-ketligi bo’lsa ham, dasturlar xotirani bo’laklarga bo’lish mexanizmini ishlatadi. Python ham shu usulda ishlaydi. Xotira odatda stack va heapdan tashkil topgan, lekin biz asosan heap haqida gaplashamiz. Boshida butun xotira ishlatilmagan bo’ladi. Shundang so’ng dasturga xotira kerak bo’lsa, u mexanizm orqali xotiradan nechtadir katak (byte) so’rashi mumkin. Bu xotira ajtratish deyiladi. Har bir katakni faqat bir marta ajratish mumkin. Lekin, dastur xotira qaytarib berishi ham mumkin. Shunda qaytarilgan kataklarni keyinchalik yana qayta ajratish mumkin bo’ladi.
Avvalambor, bittagina Tree
uchun xotirada 4ta katak ajratiladi deb qaraylik:
t1 = Tree(1)
Kattaroq daraxt strukturalarni esa xotiradan yana kataklar ajratib, va left va right uchun ko’rsatkichlar ishlatib tuzadi. Har safar yangi Tree
yasalganida xotiradan bytelar ajratiladi, lekin left
va right
shunchaki ko’rsatkichdir.
t1 = Tree(1)
t2 = Tree(2)
t3 = Tree(3, t1, t2)
Bularning barchasini Python o’zining bo’yniga oladi!
Garbage collection
Demak Python xotirani ajratishni o’zi boshqaradi, lekin agar u xotira qismi qayta ishlatilmasachi? Masalan, boyagi kodni funksiya ichiga solib uni 100 ming marta chaqiramiz:
def example():
t1 = Tree(1)
t2 = Tree(2)
t3 = Tree(3, t1, t2)
for _ in range(100000):
example()
Funksiya qaytgandan so’ng t1, t2, t3
dan foydalanishning iloji qolmaydi. Agar xotirani ajrataversak nima bo’ladi? Balkim 100 ming kichik sondir, lekin yana ham kattaroq son tanlasakchi? Xotirani hammasini ishlatib qo’yamiz, ajratishga bo’sh kataklar qolmaydi. Bundan yaxshiroq yo’li bo’lishi kerak, biz shunchaki xotira ajrataverib lekin uni qaytarib bermasdan davom eta olmaymiz.
Bu muammoni Python yaxshi biladi, va shuning uchun Garbage collection (GC) deb nomlangan amalni bajaradi. Orqa fonda, Python ishlatishni iloji yo’q bo’lgan xotira qismlarini o’ziga qaytarib oladi. Python dastur ishlash vaqtida ko’rsatkichlarni quvlab qaysi obyektlarni (xotira qismlarini) ishlatishni iloji yo’qligini dinamik hisoblab yuradi.
Masalan, tepadagi funksiya yakuniga yetgandan so’ng, t3
ko’rsatib turgan Tree
ni ishlatishni boshqa iloji qolmaydi! t1
va t2
ni t3
dan foydalanib olsa bo’ladi (t1.left
va t2.left
deb yozib), lekin t3
ni hech qayerdan ishlatib bo’lmasligi uchun t1
va t2
ni ham ishlatib bo’lmaydi deb xulosa qiladi. Shundan so’ng, Python bu obyektlar egallab turgan xotira qisimini o’sha obyektlardan olib qo’yadi va keyinchalik boshqa yangi obyektlarga ishlatishi mumkin. Python bu narsalarni to’liq control qiladi.
Diagramma bu safar kataklar bilan emas, yuqoriroq darajada chizilgan:
Garbage collection:
- Yuqori-daraja dasturlash (qo’lda xotira ajratish va qaytarib berish shart emas)
- Xotira bilan ishlashdagi xatolarni oldini oladi. Buni keyingi qismda ko’rib o’tamiz. Xotirani noto’g’ri ishlatib qo’yish juda yomon narsalarga olib kelishi mumkin
- GC qachon sodir bo’lishi yoki qancha vaqt olishini bilib bo’lmaydi. Masalan, katta xotira qismini ishlatish iloji yo’qligini tushunib yetishiga vaqt ketishi mumkin, va buni topgandan keyin, o’shani tozalashga, va rekursiv uni qismlarini tozalashga rosa ko’p vaqt ketib qolishi mumkin. Dastur ozgina vaqtga qotib qolishi mumkin. Shuning uchun high-performance kerak bo’lgan dasturlarda GC yaxshi fikr emas. Bu GC pause deyiladi
- Xotirani isrof qilishi mumkin. Ishlatilmayotgan qismini birdaniga qaytarib olmasligi mumkin, va u qismlar bir qancha vaqt ishlatilmay qolib ketadi. Qaysi obyektlarni ishlatish iloji borligini hisoblash uchun ham GC o’zi xotira ishlatadi
Xotira bilan qo’lda ishlash
Endi C++ tili haqiga gaplashaylik. Bu tilda GC yo’qligi uchun, aynan qaysi xotira qismini qancha vaqt ishlatishimizning boshqaruvini bizga beradi. C++dagi std::vector
ma’lumot tuzilmasi haqida gaplashamiz. Endi muhim qismlarga kelyapmiz.
Vector nima? Bu juda ko’p ishlatiladigan ma’lumot tuzilmasi. Qisqa qilib aytganda, o’sa oladigan ma’lumotlar bloki.
void example() {
vector<string> vector;
...
vector.push_back(new_elem);
}
data
bu ajratilgan xotira blokiga ko’rsatkich. capacity
bu o’sha blok ichiga nechta element sig’ishi. length
esa bu hozirgi vectorning ichida nechta element saqlangan ekanligi. .push_back(...)
vectorning oxiriga yangi element qo’shadi. Bunda 2xil holat bor. Agar xotira bloki oxirida joy bo’lsa, shunchaki o’sha yerga qo’yadi.
Aks xolda agar blok to’lgan bo’lsa, ya’ni bo’sh joy qolmagan bo’lsa: vector yangi kattaroq blokni xotiradan ajaratadi, barcha elementlarni yangi blokka ko’chiradi, va yangi elementni oxiriga qo’shadi. Vector o’sa oladi deb shunga aytgan edik. Lekin, C++ eski blokni o’zi o’chirib tashlamaydi. Bu ishni vectorning o’zi bajarishi shart.
Nima xato ketishi mumkin?
Endi murakkabroq kodga qaraylik:
void example() {
vector<string> vector;
...
auto& elem = vector[0];
vector.push_back(some_string);
print(elem);
}
Kod elem
nomli o’zgaruvchiga vectordagi birinchi elementga ko’rsatkich saqlaydi. Kod ishga tushganda, elem 0-indexdagi elementning addressini saqlaydi. tizim dasturlashda ko’rsatkichlar juda ko’p ishlatiladi.
Lekin, keyin biz vectorga yangi element qo’shamiz. Agarda vectorimiz to’lgan bo’lsa, u yangi xotira bloki ajratishi kerak bo’ladi. Bu xolda nima bo’ladi? Barcha elementlarni yangi xotira qismiga ko’chiradi va data
ni ham yangi blokka ko’rsatkich qiladi. Lekin, elem
ko’rsatkich o’zgarmasdan qoladi!
print(elem)
da esa biz elem
ni ishlatmoqchimiz, lekin u ko’rsatib turgan xotira qismidagi elementlar endi yo’q! Qaytarib berilgan (freed) xotirani o’qimoqchi bo’ladi. Bu osiliq ko’rsatkich (dangling pointer) deyiladi. Va osiliq ko’rsatkichni o’qish use-after-free deyiladi.
Osiliq ko’rsatkichni o’qisa nima sodir bo’ladi? Bu xolda compiler dastur qanday ishlashi haqida hech qanday kafolat bermaydi. Har qanday narsa sodir bo’lishi mumkin. Ko’p xolda dastur crash bo’ladi. Boshqa xollarda, agar dasturning boshqa qismi u xotira qismini qayta ajratgan bo’lsa, chiqindi ma’lumotni o’qishi mumkin. Agar osiliq ko’rsatkichka dastur yozsa, unda dasturning boshqa qismining muhim ma’lumotini ustiga yozib yuborishi mumkin. Shundan ko’p CVElar kelib chiqadi.
Last year, we showed that more than 70% of our severe security bugs are memory safety problems. That is, mistakes with pointers in the C or C++ languages which cause memory to be misinterpreted. Google security blog
As was pointed out in our previous post, the root cause of approximately 70% of security vulnerabilities that Microsoft fixes and assigns a CVE (Common Vulnerabilities and Exposures) are due to memory safety issues. This is despite mitigations including intense code review, training, static analysis, and more. Microsoft security response center
Rust nima va u qanday ishlaydi?
Uzoqdan qaraganda, Rust bu boshqa dasturlash tillaridan yaxshi idealarni birlashtirishga harakat qiladi. Ayniqsa, Rust ko’p qismlarini funksional dasturlash tillaridan olgan, Haskell, Ocamlga o’xshash. Rust yuqori-darajalik imperative va funksional dasturlash tillari qiladigan ishlarni ko’pini qila oladi, lekin undan tashqari C/C++ tillariga o’xshab quyi-darajadagi ishlarni ham qila oladi.
Rustning maqsadi shu tillarni birlashtirish va compile-vaqtida xotira bilan xavfli ish qilishimizdan to’xtatishi, masalan yuqoridagi C++da ko’rgan xavfli koddan.
Rust haqida ozgina tarix:
- 2010-yildan beri ustida ish olib boriladi, 2015-yil 1.0 versiya chiqarilgan
- Mozilla Servo browser engine yozish uchun ishlatgan, keyinchalik Firefoxga kiritilgan
- Rust bu yagona “tizimlar dasturlash tili”, quyidagilarni beradigan:
- Quyi-daraja boshqaruv, ya’ni zamonaviy C++
- Kuchli xavfsizlik kafolatlari
- Sanoaviy rivojlanish va qo’llab-quvvatash
- Ko’p katta kompaniyalar productionda Rust ishlatmoqda
- 2021-yilda Rust foundation shakllangan, Amazon, Google, Huawei, Meta, Microsoft, Mozilla va boshqa kompaniyalarni ichiga oladi
Aynan nima xato ketdi?
Rust nima haqida? Rustni tushunishning eng oson yo’li avvalga kodda nima xato ketganini tushunish deb o’ylayman.
Demak, bizda elem
bor edi, va u data
ko’rsatib turgan xotira qismiga ko’rsatib turgan edi. Bu aliasing deyiladi.
Bir vaqtda bir nechta ko’rsatkich xotiraning bir xil qismiga ko’rsatishi.
Aliasingning o’zi muammo emas. Muammo shundaki, bu buyerda boshqa bir narsa bilan birga ishlatilgan: mutation. elem
ko’rsatib turgan ma’lumotni xolatini o’zgartiryapmiz: elem
ko’rsatgan xotirani ajratilgan xoldan qaytarib berilgan (freed) xolga o’tgazyapmiz. Aliasingni deb, bitta ko’rsatkich (data
)dan ma’lumotni o’zgartirganimiz boshqa aliased ko’rsatkichdan (elem
) ma’lumotni o’qiy olmasligimizga olib keldi. Rustning asosiy g’oyasi shu narsaga yo’l qo’ymaslik. Agar Rust haqida faqat bitta narsa bilmoqchi bo’lsangiz, unda shuni biling.
Umuman olganda bu rost, lekin keyinchalik ko’ramizki buning nozik joylari bor. Rust aliasing+mutationni taqiqlagandan so’ng C/C++ tillarida doim sodir bo’ladigan rosa keng xotira xatoliklarini oldini oladi. Lekin rust bu taqiqni qanday amalga oshiradi?
Egalik tizimi (ownership) va borrow (vaqtinchalik egalik qilish) tizimi orqali. Birinchi bularni odamlar va kitob analogi bilan ko’ramiz, keyin esa kodda amalda ko’ramiz.
Egalik tizimi
Egalik bu oddiy tushuncha. Bir narsaga egalik qilish nimani anglatishini hammamiz tushunsak kerak. Rust dasturlash tilida esa agar men bir narsani egalik qilsam, bu degani dasturdagi boshqa hech kim - boshqa funksiyalar, ko’p threadlik dasturda boshqa threadlar - hech kim bu narsaga tega olmasligi kerak - na o’qiy olish, na o’zgartirish. Men bu narsa (xotira) bilan xohlagan ishimni qila olaman va boshqalar bunga ta’sir qila olmaydi va men ham boshqalarga ta’sir qilmayman.
Masalan men shu kitobni egasiman deylik. Men u kitobga xohlagan narsa qilishim mumkin, xohlasam o’qiyman, xohlasam ustiga yozib tashlayman. Va xohlasam boshqasiga beraman - bu Rustda move deyiladi. Boshqasiga berganimdan so’ng, u kitobga hech narsa qila olmayman, men endi uning egasi emasman, endi faqat u uning egasi va bu huquqlarga ega. Va agar kitob bilan ishi bitsa, shunchaki kitobni tashlab yuborishi ham mumkin - bu rustda dropping deyiladi. Tashlangandan so’ng, u kitob uchun ajratilgan xotira qismi qaytarib olinadi.
Egalik tizimi bilan:
- mutationga yo’l qo’yiladi
- aliasing mumkin emas
Endi namunaviy kodlarni ko’ramiz.
fn take(arg: Vec<int>) {
// ...
}
fn give() {
let mut vec = Vec::new();
vec.push(1);
vec.push(2);
take(vec);
}
Rustdagi Vec turi bu C++dagi vector. Uni ichiga 1 va 2ni joylayapmiz. Keyin esa take
funksiyani chaqiryapmiz:
fn take(arg: Vec<int>) {
// ^~~~~~~~ shunchaki Vec<int> deb
// yozilsa unga egalikni talab qiladi
take
endi vec
ga egalik qiladi. Bu nimani anglatadi? Aslida, xotirada vec
ning nusxasi olinadi, argument qilib berganimiz uchun. Va u nusxasi avvalgi data
ga ko’rsatib turadi hali ham. Lekin, give
ishlatayotgan eski vec
endi ishlatib bo’lmas xolatga o’tadi.
take
funksiya yangi arg
ni olgandan so’ng, uni ustida xohlagan ishini qila oladi. Va shundan so’ng, take funksiya ortga qaytganida (return bo’lganida), arg
scopedan chiqib ketadi va shuning uchun drop bo’ladi, va arg
ajratgan xotira blokini qaytarib beradi.
Endi give
ga qaytganimizda nima bo’ladi? Eski vec
ishlatgan bytelari hali ham eski joyida turadi. data
hali ham eski xotiraga ko’rsatkich bo’ladi, lekin bu xotira qismini agar ishlatsa use-after-free bo’ladi! Chunki arg
drop bo’lganda u xotirani qaytarib berdi.
Lekin biz avval egalikni berib yuborganimiz uchun, Rustning turlar tizimi (type system) uni ishlatishga yo’l qo’ymaydi. Masalan, vecdan har qanday usulda foydalanmoqchi bo’lsak:
// ...
take(vec);
vec.push(2); // error: vec has been moved
Compiler bizga type-checking xatolik chiqaradi. Har qanday xolatda biror narsaga faqat bittagina funksiya egalik qiladi.
GC-lik tillarda shunga o’xshash xatoga duch kelganmisiz? use-after-free xato emas, chunki GC tillar unga yo’l qo’ymaydi, aksincha, bir obyektni bir funksiyaga berib yuborish, va u funksiya qaytgandan keyin ishlatib qo’yish? Masalan, Pythonda:
def take(file):
file.close()
f = open('myfile.txt', 'w')
take(f)
f.write("hello") # runtime error: I/O operation on a closed file
Rustning egalik tizimi nafaqat xotiraga tegishli, balki logik xatolarni ham oldini olishi mumkin. Undan tashqari, Rust mutable va immutable o’zgaruvchilarni orasida qattiq farqlaydi va bu ham shunday xatolarni qilmaslikka yordam beradi. Rustning kuchli turlash tizimi (strong typing system) haqida keying postda gaplashamiz.
Bu xotira bilan ishlashdagi use-after-free, double-move va boshqa xatoliklarni oldini oladi. Lekin C++da ko’rgan xatoligimizni oldini oladimi?
void example() {
vector<string> vector;
...
auto& elem = vector[0];
vector.push_back(new_elem);
print(elem);
}
Bu kodda lekin biz juda qiziq operatsiyani ishlatganmiz: elem
nomli ko’rsatkich ishlatyapmiz. Va endi ko’rsatkichlar haqida gaplashamiz.
Shared borrow (&Type)
Egalik tizimi yaxshi, lekin bizni rosa cheklab qo’yadi. Masalan, yuqoridagi C++ kodni hozircha Rustda yoza olmaymiz. Shuning uchun, rustda borrowing tizimi mavjud. Borrowing tizimi Rustning egalik tizimini kengaytiradi va C/C++da dasturchilar qiladigan ishlarning ko’pini qamraydi.
Borrow - vaqtincha olib turmoq, bir narsani keyinchalik qaytarib berish maqsadida olib turish degani. Borrow ko’rsatkichlarga o’xshaydi, lekin borrowning 2xil turi bor: shared borrow va mutable borrow.
Shared borrowni xohlagancha yasashimiz mumkin, lekin shared borrow orqali faqat o’qiy olamiz. Ya’ni, kitobni o’qishga xohlagancha odamga ruxsat bera olamiz, lekin hech qaysinisi yozishiga ruxsat bermaymiz. Chunki, har bir shared borrow bitta obyektga ko’rsatib turadi, bitta odam yozsa boshqasida ko’rinib qoladi, va bu aliased mutation bo’lib qoladi. Undan tashqari, shared borrow amal qilayotgan davrda kitobning egasi ham kitobga yoza olmay qoladi. Shuning uchun, mutationga ruxsat mumkin emas, lekin xohlagancha aliasing qilish mumkin.
Shared borrow:
- mutation
- aliasing
Mutable borrow (&mut Type)
Borrowning 2-nchi usuli. Mutable borrow bizga kitobga yozishga ham ruxsat beradi. Mutable borrow orqali checklangan “aliased mutation” qilishimiz mumkin. Biz kimdirga kitobni berib turyapmiz, va o’sha odam unga xohlaganini qilishi mumkin drop qilishdan tashqari - chunki bizga qaytarib berishi kerak ishlatib bo’lganidan so’ng. Endi, bu borrowning cheklovi katta:
- Bir vaqtning o’zida ham shared borrow ham mutable borrow yasash mumkin emas
- Bir vaqtning o’zida 2ta yoki ko’proq mutable borrow yasash mumkim emas
- Mutable borrow amal qilayotgan vaqtda kitobning egasi ham foydalana olmaydi
Ya’ni, bir vaqtning o’zida faqat bitta odam kitobni ishlatishi mumkin.
Bu egalikni berib yuborishga o’xshab ketishi mumkin, lekin undan asosiy farqi u odamga qaysi obyektni bergan bo’lsak bizga huddi o’shani qaytarib berishi shart bo’ladi, egalikni berib yuborsak drop qilib yuborishi mumkin. Bizga qaytarib berganidan so’ng, bizda yana to’liq egalik tiklanadi.
Yana bir farqi, move qilganimizda obyektni nusxasi olinadi, lekin mutable borrow qilganimizda obyektga ko’rsatkich olinadi.
Mutable borrow:
- mutation
- aliasing
Borrowga misollar
Aslida, bu g’oyalar uchun qisqa va tushunarli misol topish qiyin, chunki bular eng foydali joylaridan biri multi-threaded dasturlardadir, va multi-threading bilan ko’pchilik ishlamagan bo’lsa kerak.
Endi 2lovi borrow turi uchun misollarga o’tsak.
fn lender() {
let mut vec = Vec::new();
vec.push(1);
vec.push(2);
use(&vec);
// ...
}
fn use(arg: &Vec<int>) {
// ^~~~~~~~~
// shared borrow
// ...
}
Rustda o’zgaruvchilar immutabledir, mutable qilish uchun mut
so’zini qo’shishimiz kerak. Agar o’zgaruvchi immutable bo’lmasa, unda unga mutable borrow yasay olmaymiz.
Ampersand orqali shared borrow yasash mumkin. Endi use(...)
ning parametr turida &
bor, va bu Rustda borrow qabul qilishini anglatadi. use
argument berishda ham &
ishlatganmiz. use
chaqirilganda unga vec
ni borrow qilib beramiz, va bu aslida ko’rsatkichdir. Lekin, use
vectorni o’zgartira olmaydi, faqatgina o’qishi mumkin. Agar o’zgartirmoqchi bo’lsangiz, Rust kodimizni compile qilishdan bosh tortadi:
fn use(arg: &Vec<int>) {
arg.push(1); // error: cannot borrow '*arg' as mutable
arg[1] += 2; // error: cannot borrow '*arg' as mutable
}
Endi, mutable borrowni ko’raylik:
fn push_all(from: &Vec<int>, to: &mut Vec<int>) {
// ^~~~~~~~~~~~~
// mutable borrow
for elem in from.iter() {
to.push(*elem);
}
}
Diagrammada for-loopning birinchi aylanishni tugatgandan so’ngi xolat ko’rsatilgan:
Bu funksiyada birincha vectordagi elementlarni ikkinchi vectorning oxiriga qo’shib chiqyapmiz. 2-vectorni o’zgartirayotganimiz uchun uni mutable borrow qilishimiz shart. .push(...)
funksiyasi vector mutable bo’lishini majburlaydi, chaqira olishimiz uchun. Lekin, for-loop orqali elementlarni ko’rib chiqish uchun mutable bo’lishi shart emas. *elem
orqali ko’rsatkichni ichini o’qish va undan nusxa olish uchun ham from
vectorni o’zgartirishimiz shart emas. Shuning uchun, from
vectorni shared borrow qilib olsak bo’ladi. elem
aslida from
vectorning ichidagi elementga shared borrowdir (ko’rsatkichdir), shuning uchun uni o’qish uchun *
ishlatishimiz shart. Bu C++dagi avvalgi misolimizga o’xshaydi.
Endi savol: agar from
va to
vector aslida bitta vectorga ko’rsatkich (borrow) bo’lsachi? Unda kodimiz qanday o’zini tutadi? Tasavvur qilaylik, u vectorda [1, 2, 3]
elementlar bor va capacity = 3
. Kodimiz o’zini har xil tutishi mumkin, tilni qanday ishlashiga bog’liq, lekin mana bu bo’lishi mumkin xolatlardan biri:
elem
vectorning birinchi elementiga ko’rsatkich bo’ladielem
ni vectorning oxiriga qo’shamiz- Vectorning capacitysi to’lgani uchun yangi xotira ajtratadi, va eski xotira blokini qaytarib beradi (free qiladi).
elem
1ta keyingi katakka o’tadi, lekin hali ham eski xotira blokidagi!- Lekin
elem
ko’rsatayotgan eski xotira bloki o’chib ketgan!
Shunday qilib, use-after-free kelib chiqadi! Nima sodir bo’lganini tushunish ozgina qiyin bo’lishi mumkin, lekin, ham bir vectorni ustidan loop qilish, va o’sha vectorni o’zgartirish yaxshi g’oya emasligini tushunsangiz kerak.
Demak, from
va to
bitta vector bo’lishiga yo’l qo’ymasligimiz kerak! Rust buni qanday amalga oshiradi? Shu vaqtgacha o’rgangan qoidalarimiz bilan:
fn caller() {
let mut vec = ...;
push_all(&vec, &mut vec);
// error: cannot have both mutable and shared references at the same time
}
Bir vaqtda ham shared, ham mutable borrow qilish mumkin emas. Shuning uchun, bunday xatolik kelib chiqishidan avval, kodimizni o’zi compile bo’lmaydi!
Biz &mut T
qilganimizda, u ma’lumotni olishning yagona usuli o’sha yangi borrowdan bo’ladi, funksiya yakuniga yetgunicha. Boshqa borrowdan, yoki egalik qilayotgan o’zgaruvchidan ham tega olmaymiz. Na o’qiy olamiz, na o’zgartira.
Xulosa
Demak, Rust - Java, Python va OCaml, kabi yuqori darajali dasturlash featurelariga ega, lekin C/C++dagi quyi-darajali boshqaruvni ham qila oladi. Rustda kuchli xavfsizlik kafolatlari bor. Va Rust xotira xavfsizligini egalik va borrow tizimi orqali uddalaydi.
Rustning asli
Rust tilining butun ma’nosi bu “xavfsiz” ekanligida. Rust C++ga o’xshash systems-dasturlash uchun, lekin bizga xavfsizlikni kafolatlaydi. Lekin, haqiqatdan ham xavfsiz ekanligi qanday bilamiz? Va “xavfsiz” degani nima degani aslida?
Nimaga bu savolga javob berish oson emasligini tushuntirish uchun, ozgina ortga qaytishimiz kerak.
Rust mutation va aliasingni bir vaqtda ishlatishga ruxsat bermaydi dedik, lekin haqiqatda esa, Rust cheklamsiz aliasing+mutationga ruxsat bermaydi. Bazida, afsuski, biz mutation+aliasingni ishlatishga majburmiz. Aniqroq misollarda aytganda:
-
Ko’rsatkichli ma’lumot tuzilmalari: ayniqsa ikkili bog’lamli ro’yxatlar haqida ayniqsa internetda Rustni rosa yomonlashadi, chunki uni Rustda yozish haddan qiyin. Ko’p ma’lumot tuzilmalari ichki ko’rsatkichlar ishlatadi, tugunlar boshqa tugunlarga ko’rsatkich saqlaydi. Va bu ko’rsatkichlarni ko’p o’zgartirishimiz kerak bo’ladi. Bunday tuzilmalar systems-dasturlashda ko’p ishlatiladi. Tepada Rust haqida o’tgan narsalarimiz bilan bu tuzilmalarni yozishni iloji yo’q
-
Sinxronlash mexanizmlari: qulflar, kanallar, semaforlar
-
Xotira boshqaruvi: Rustda GC yo’q bo’lgani uchun xotira boshqaruvi qo’lda qilinishi kerak, lekin ba’zida oz-moz avtomatik yechimlardan foydalanmoqchi bo’lishingiz mumkin, masalan, Reference counting. Bular fundamental ko’rinishda mutation va aliasingni birga ishlatishga majbur
Xavfli modullar
Rustdagi ko’p kod tilning “xavfsiz” qismida yozilgan - biz shu paytgacha ko’rgan Rustga o’xshab, qoidalarga amal qilib. Lekin, ba’zi foydali modullar bor va ular o’zi ichida Rustning xavfsiz qismidan tashqarida ishlaydi. Va agar u modullarning kodini ichiga qarasangiz, bunga o’xshagan kod ko’rasiz:
pub fn try_borrow(&self) -> Ref<T> {
match BorrowRef::new(&self.borrow) {
Some(b) => Ref {
_value: unsafe { &*self.value.get() },
// ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
_borrow: b,
},
// ...
}
}
U kodlarning ichida shunda “xavfli bloklar” bo’ladi. U blokning ma’nosi: bu qismdagi kodni Rust tushunmaydi lekin dasturchi u kod xavfsiz ekanligiga va’da beradi. Bu bloklar ko’p xolda “xom ko’rsatkichlar"ni (*T) ishlatishadi, va Rustning sistemasi bularni aliasing va mutationga tekshirmaydi. Endi, bu modullarning mualliflari quyidagini rostligiga va’da berishadi: bu modullar xavfli usulda yozilgan bo’lsa ham, baribir Rust dasturlash tiliga xavfsiz qo’shimcha bo’la oladi, va bu modullardan foydalanayotgan dasturchilarning kodi xavfsiz bo’ladi.
Va u juda katta va’da! Ko’p xollarda xatolar chiqishi mumkin. Va shunday sodir bo’lgan voqealardan biri 2015-yildagi misol. Rust 1.0ning ommaga chiqishidan bir necha hafta oldin, Fearless Concurrency nomli blog chiqarilgan. U blogda Rustning mualliflari Rust qanday xavfsiz til ekanligini maqtashgan, va shular ichida yangi thread::scoped
nomli kuchli modulni ham maqtashgan. Lekin, bir necha kundan so’ng bu modul ichida xato topilgan! Bu haqida bu yerda o’qishingiz mumkin. Qisqa qilib aytganda:
thread::scope
modulning o’zi aslida xavfsiz- lekin boshqa modullar bilan birga ishlatilganda xavfsizlik buziladi!
Bu qanday sodir bo’lishini ko’rish uchun, misol keltiraman.
fn make_null() -> *const int {
unsafe { std::ptr::null() }
}
fn read_ptr(ptr: *const int) -> int {
unsafe { *ptr }
}
fn main() {
let ptr = make_null();
let value = read_ptr(ptr);
}
make_null
:*const int
turli null ko’rsatkich qaytaradi. Xavfsiz Rustda ko’rsatkichni o’qishning iloji yo’q, shuning uchun null ko’rsatkich yasash xavfsizread_ptr
: bu berilgan ko’rsatkichni o’qiydi. Xavfsiz Rustda, ko’rsatkichlar yasashni iloji yo’q, shuning uchun bu funksiyani chaqirishni iloji yo’q, va shuning uchun bu funksiya ham xavfsiz
Lekin, bu funksiyalarni birga ishlatganimizda null ko’rsatkichni o’qiy olamiz! Ya’ni, 2ta xavfli blok bizga xavfsiz ko’rinsa ham, aslida birga qo’shilganda xavfli bo’lib qolishi mumkin. Bu misolda funksiyalar foydasiz ish qilyapti, lekin bu foydali kod orasida ham bo’lishi mumkin. Bu nafaqat bitta fayl, yoki bitta modul ichida bo’lishi mumkin, balki modullar aro ham bo’lishi mumkin. Shuning uchun, Rustni xavfli yoki xavfsiz emas ekanligini bilish uchun, butun koddagi - barcha ishlatilgan kutubxonalardagi - xavfsiz ekanligi bir vaqtda isbotlash shart.
Endi bunday xavfsizlik buglari oldini olish haqida gaplashsak bo’ladi. RustBelt proyekti aynan shu maqsadda qilingan: Rust dasturlash tili va standard kutubxonalarini xavfsiz ekanligini isbotlash, va Rust tilining mualliflariga keyinchalik tilning xavfsizligini buzmasdan yangiliklar qo’sha olishini qo’llab quvvatlash. Lekin, nega bu qiyin va yillab vaqt ketgan?
Gap shundaki, ko’p dasturlash tillari uchun ishlatiladigan sintaktik xavfsizlik (Syntactic safety) metodini Rust uchun qo’llab bo’lmaydi. Sintaktik xavfsizlik haqida kitoblardan o’rgansangiz bo’ladi, lekin qisqa qilib aytganda: bu metod fundamental usulda dastur hech qachon xavfli amallar ishlatishiga ruxsat bermaydi. Tildagi nima xavfli va nima xavfsiz ekanligi aniq aytilgan bo’ladi, va turlar tizimi ham shuning atrofida tuziladi. Rust esa xavfli bloklar yozishimizga ruxsat beradi va shuning uchun butun boshli Sintaktik xavfsizlik metodini ishlatib bo’lmaydi. Shundan so’ng bizda savol paydo bo’ladi: xavfsiz degani o’zi nimani anglatadi?
Semantik xavfsizlik
Bu savolga javob berish uchun Semantik xavfsizlik nomli usul ishlatiladi. Buning uchun xavfsizlik kontraktini yasaymiz. Bu katta qoidalar birlashmasi, va buning ichiga kod nimalar qilishi mumkin va mumkin emasligi yoziladi. Agar xotira xavfsizligini yoki boshqa xavfsizlikni ta’minlamoqchi bo’lsak, qoidalar ostida bunday xavfli amallarning iloji yo’q qilamiz. Bu kontraktning ichida tepada ko’rgan qarama qarshi tanlovlarni qaysi biriga ruxsat berishimizni tanlab chiqsak bo’ladi. Shunda, xavfsiz kod deganda shu kontraktga mos kodni tushunamiz. Bu kontrakt ostida:
- Xavfsiz yozilgan Rust kod avtomatik tarzda kontraktga mos bo’lishi kerak. Ya’ni, Rustning xavfsiz kod qoidalari ruxsat bergan barcha narsaga bu kontrakt ham ruxsat berishi kerak
- Xavfli modullarni barchasi bu kontrakt qoidalariga mos ekanligini qo’lda isbotlashimiz kerak bo’ladi
Hali ham xavfli modullarni xavfsiz ekanligini isbotlab chiqishimiz kerak, lekin endi barchasini birdaniga olib isbotlashimiz shart emas, birma bir isbotlab chiqsak bo’ladi. Agar 2ta modul kontraktga mos bo’lsa, unda ularni birga ishlatish ham kontraktni buzmaydi. Agar 2ta modulni birga ishlatganda kontrakt buzilsa, demak u modularning qaysidiri kontraktga mos emas bo’ladi.
Lekin muammo shundaki, bu xavfsizlik kontraktini biz o’zimiz yasaymiz, va uni qanday yasaganimizga qarab ba’zi modullar xavfsiz bo’ladi, va boshqalari xavfli bo’ladi (kontraktga mos kelmaydi). Ikki xil modul ikki xil kontraktga mos bo’lsa, unda ularni birlashtira olmaymiz! Shunday kontrakt topish kerakki, barcha modullar u kontraktga mos ekanligini isbotlay olishimiz kerak. Shuning uchun ham bu soha qiziq - eng “yaxshi” xavfsizlik kontraktini topish.
Separation logic
Savolga javob berishning boshlang’ich nuqtasi Separation logic bo’lgan:
- Separation logic bu Hoare logicning kengmaytmasi va bizga ko’rsatkichli dasturlar haqida mulohaza qilishimizga imkon beradi
- Ko’plab isbotlov va analiz dasturlariga ta’sir o’tgazgan
- Separation logic = Egalik tizimi. Separation logic ichida Rustning egalik tizimining asoslari bor bo’lgan uchun Rust dasturlarining modellash uchun a’lo darajada mos keladi
Lekin separation logic bitta emas! Uning har xil kengmaytma va o’zgartirilgan variantlari bor, va agar biz ikki xil modulni ikki xil separation logicda isbotlasak, unda isbotni birlashtira olmaymiz, tepada aytganimizday. Shuning uchun Iris project tashkil topgan, aynan Rust uchun mos separation logicning varianti yasash maqsadida:
- Iris bu dasturlar haqida mulohaza qilish uchun butun framework
- Coq nomli isbotlov dasturini ishlatadi. Bu dasturdagi isbotlarning barchasi kompyuter orqali avtomatik tekshiriladi. Shuning uchun dasturchilar bir biriga ishonishi shart emas va butun dunyo bo’yicha hamma projectga qatnashishi mumkin!
Afsuski, bu mavzular haqida mening bilimim cheklangan, va shu bilan maqolani yakunlayman.
References
Bu maqola bir leksiyasiga asoslangan.
Shifrlangan
-----BEGIN AGE ENCRYPTED FILE----- YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLQ3V5V2VaZEZwRnN6Wi90 T2UxQTB2S3dpcld6YnNxdDJxTTVzbUpTZ2xjCldzRkFSeG9ubTc2VC9obitVV2RS NWtHRFBoclRoYldWNkM3TnpxN1NvMEkKLS0tIEgwd2taOEo5ZmJVYmVsZFVKVG9Z RVNSR1Ixenc5Y2E2eFRuUXFaTHNWNVEKGfd4BIYPH0Ua5Uc6DtMs3a/g9Hsg2hE6 t/nMP44gvSwrD9pY2hzNuBbkXtDamcxdoYM76rbGsZjG/YmsClJif9+XSPYArhhj Ws/DBPgdbaY+0AgguazjA7q0NSvunbVUPu2MrZTuLpEwcff7v/cC5gse9V7exXtW le+fBzlnnaouM8w= -----END AGE ENCRYPTED FILE-----