Rust ile yazılmış bir JVM
(andreabergia.com)- Rust öğrenmek için yapılan yan proje
rjvm, öğrenme amaçlı oyuncak bir JVM olsa da gerçek.classvejardosyalarını okuyup Java bytecode'unu çalıştıracak seviyeye kadar uygulanmış - Desteklenmeyen özellikler thread, reflection, annotation, I/O, JIT derleyici ve string interning olsa da kontrol akışı, nesne oluşturma, metot çağrıları, exception ve garbage collection dahil
- Örnekler, OpenJDK 7'nin gerçek
rt.jardosyasını kullanıyor;java.lang.StackTraceElementgibi sınıflar gerçek JDK içinden yükleniyor - Kod, üç Rust crate'ine ayrılıyor:
reader,vm,vm_cli;.classayrıştırmadan çağrı yığınına, native metotlara, değer ve nesne modellemeye kadar uzanıyor - Son kilometre taşı stop-the-world semispace copying collector oldu; hedefe ulaşıldıktan sonra proje durduruluyor ve bilinen hataları düzeltme planı bulunmuyor
Öğrenme amaçlı Rust JVM rjvm
rjvm, Rust öğrenmek için yapılmış oyuncak bir Java Virtual Machine- Kod GitHub üzerinde açık olarak bulunuyor
- Ciddi bir JVM implementasyonundan çok öğrenme amaçlı bir çalışma olduğu için şu özellikleri desteklemiyor
- thread
- reflection
- annotation
- I/O
- JIT derleyici
- string interning
- Generics başlangıçta desteklenmeyenler listesinde yer alıyordu ama pratikte çalışıyor
Ne kadar uygulanmış?
rjvm, basit bir parser olmanın ötesine geçip çeşitli JVM davranışlarını doğrudan uyguluyorif,forgibi kontrol akışı- ilkel tipler ve nesne oluşturma
- sanal metot ve statik metot çağrıları
- exception işleme
- garbage collection
jardosyalarından sınıf çözümleme
- Test kodunda
Throwable,Exception,StackTraceElementkullanan örnekler bulunuyor - Gerçek I/O olmadığı için
System.out.printlnyerinetempPrintadlı bir native metot kullanılıyor - Örnekler, OpenJDK 7'nin gerçek
rt.jardosyasını kullanıyor vejava.lang.StackTraceElementde gerçek JDK'dan alınıyor
Üç Rust crate'i
- Proje standart bir Rust projesi ve üç crate'ten oluşuyor
reader:.classdosyalarını okuyup içeriklerini modelleyen tipleri içeriyorvm: kod çalıştırabilen sanal makineyi bir kütüphane olarak sunuyorvm_cli:javaçalıştırılabilir dosyasına benzeyen basit bir komut satırı başlatıcısı içeriyor
readercrate'ini ayrı bir depoya ayırıp crates.io üzerinde yayımlama seçeneği değerlendiriliyor
.class dosyası ayrıştırma
- Java kodu
javacile derlenip.classdosyasına dönüşür ve geneldezipformatındaki.jardosyalarıyla dağıtılır - Java kodunu çalıştırmak için önce derleyicinin ürettiği bytecode'u içeren
.classdosyasını yüklemek gerekir - Sınıf dosyasında çalıştırma ve tip çözümleme için gereken bilgiler de bulunur
- sınıf adı ve kaynak dosya adı gibi metadata
- üst sınıf adı
- implemente edilen arayüzler
- alanlar, alan tipleri ve annotation'lar
- metot descriptor'ları,
throwsbölümü, annotation'lar ve generics bilgileri - bytecode, exception handler tablosu, satır numarası tablosu
readercrate'i sınıf dosyasını ayrıştırıp sınıfı ve içeriğini modelleyen bir Rust struct döndürüyor
Metot çalıştırma ve çağrı yığını
vmcrate'inin temel API'siVm::invokeve metot çalıştırmak için kullanılıyor- Çalışan her metot,
CallStackiçinde birCallFramesahibi mainçalıştırıldığında çağrı yığını başlangıçta boştur ve yürütme için yeni bir frame oluşturulur- Her fonksiyon çağrısında çağrı yığınına yeni bir frame eklenir; metot çalışması bitince ilgili frame kaldırılır
- Metotların çoğu Java bytecode'u ile uygulanmış olsa da
rjvmnative metotları da destekliyor- Native metotlar, Java bytecode'u yerine doğrudan JVM'nin kendisi tarafından uygulanan metotlardır
System::currentTimeMillis,System::arraycopy,Throwable::fillInStackTracebuna örnekrjvmiçinde bunlar Rust fonksiyonları olarak uygulanıyor
- JVM, stack tabanlı sanal makine olduğu için bytecode komutları çoğunlukla değer yığını üzerinde çalışır
- Her çağrı frame'ine bir değer yığını ve indekslerle tanımlanan yerel değişkenler kümesi eşlik eder
Değer ve nesne modelleme
Value, yerel değişkenlerde, stack öğelerinde ve nesne alanlarında saklanabilen değerleri modelliyorValue, Rustenumolarak uygulanmış ve şu durumları içeriyorUninitializedInt(i32)Long(i64)Float(f32)Double(f64)Object(AbstractObject<'a>)Null
- Rust
enum'ları gibi sum type yapıları, tek bir değerin birden fazla tipten biri olabilmesini ifade etmek için çok uygun - Nesne depolama ilk başta, sınıf referansı ve alan değerlerini taşıyan
Vec<Value>tabanlı basit birObjectstruct'ı olarak başladı - Garbage collector uygulandıktan sonra yapı, pointer ve cast kullanımının yoğun olduğu daha düşük seviyeli bir implementasyona dönüştü
- Şu anki
AbstractObject, gerçek nesneleri veya dizileri modelliyor; birkaç header word'ü ve alan değerlerini içeren bir byte array pointer'ı şeklinde
Bytecode komutlarının çalıştırılması
- Metot yürütme, bytecode komutlarını tek tek işleme sürecidir
- JVM komutları 200'den fazladır ve bytecode içinde 1 byte ile kodlanır
- Birçok komutun ardından argüman gelir ve bazı komutlar değişken uzunluktadır
rjvm, Java bytecode komutlarınıInstructiontipiyle modelliyor- Metot çalıştırılırken değer yığını ve yerel değişken dizisi tutulur, bir sonraki çalıştırılacak komutun adresi olan program counter ise 0 olarak başlatılır
- Normalde komut yürütüldükten sonra program counter bir sonraki komuta ilerler, ancak jump komutları onu başka bir konuma taşıyabilir
- Jump komutları
if,for,whilegibi akış kontrol yapılarının uygulanmasında kullanılır - Başka metotları çağıran komutlar ayrı bir aile oluşturur
- Hangi metodun çağrılacağını belirleme yöntemleri arasında sanal çözümleme ve statik çözümleme bulunur
- Hedef metot çözümlendikten sonra çağrı yığınına yeni bir frame eklenir ve yürütme başlar
- Dönüş değeri
voiddeğilse bu değer stack'e push edilir ve yürütme sürer
Exception işleme
- Exception'lar normal kontrol akışını bozar, metottan erken dönüşe yol açar ve çağrı yığını boyunca yayılabildiği için implementasyonu karmaşıktır
- Her
catchbloğu, metodun exception table yapısındaki tek bir girdiye karşılık gelir - Exception table girdisinde handler bulmak için gereken bilgiler yer alır
- geçerli olduğu program counter aralığı
catchbloğunun ilk komut adresi- ilgili bloğun yakaladığı exception sınıfının adı
CallFrame::execute_instruction, komut yürütme sonucunu ifade etmek için RustResultkullanır- Komut yürütme sonucu dört duruma ayrılır
- başarılıdır ve mevcut metot yürütmesi devam eder
- başarılıdır ve
returnkomutu olduğu için mevcut metot dönüş değeriyle birlikte sona erer - dahili VM hatası nedeniyle yürütme başarısız olur
- bir Java exception'ı fırlatıldığı için yürütme başarısız olur
- Metot yürütme döngüsü komutu ayrıştırır, program counter'ı sonraki adrese taşır ve sonra komutu çalıştırır
- Exception oluşursa mevcut komut konumuna uygun bir exception handler aranır
- handler yoksa exception çağırana iletilir
- handler varsa exception nesnesi tekrar stack'e push edilir ve yürütme
catchhandler konumunda devam eder
- Rust
Resultve pattern matching, bu davranışı kod yapısı olarak ifade etmek için çok uygundur
Garbage collection
rjvm'in son büyük kilometre taşı garbage collector implementasyonuydu- Seçilen algoritma stop-the-world semispace copying collector
- Thread olmadığı için stop-the-world yaklaşımı doğal olarak geçerlidir
- Implementasyon, Cheney's algorithm'in daha basit bir varyantı ve kod gc.rs içinde bulunuyor
- Bu yaklaşım kullanılabilir belleği iki semispace'e böler
- biri nesne tahsisi için kullanılan aktif alan olur
- diğeri kullanılmayan alan olarak kalır
- aktif alan dolduğunda yaşayan nesneler diğer semispace'e kopyalanır
- tüm nesne referansları yeni kopyaları gösterecek şekilde güncellenir
- iki semispace'in rolleri değiştirilir
- Bu prosedür blue-green deployment'a benzer bir yaklaşımla açıklanıyor
- Algoritmanın artıları ve eksileri nettir
- en fazla belleğin yarısı kullanılabildiği için büyük bellek israfı yaratır
- pointer artırma temelli olduğu için allocation çok hızlıdır
- nesneleri kopyalayıp sıkıştırdığı için bellek parçalanmasıyla uğraşmak gerekmez
- nesne sıkıştırma, cache line kullanımını iyileştirerek performansı artırabilir
- Gerçek Java VM'leri genellikle G1 veya parallel GC gibi daha gelişmiş generational garbage collector yapıları kullanır
Projenin bittiği nokta
rjvmyapılırken Rust ve sanal makine implementasyonu hakkında çok şey öğrenildi; özellikle gerçekten çalışan bir garbage collector yapabilmek tatmin ediciydi- Garbage collector çok olgun değil ama gerçekten çalışıyor
- Başlangıçtaki hedefe ulaşıldığı için proje burada duruyor
- Bilinen hatalar var ama bunları düzeltme planı yok
- Rust, JVM implementasyonu sırasında keyifli bir dil oldu; sonraki yazılarda
rjvmimplementasyonu ve JVM'nin çalışma biçimi daha ayrıntılı ele alınacak
1 yorum
Hacker News görüşleri
Çöp toplayıcı uygulamasındaki zor kısım, tüm referansların düzgün biçimde kök olarak yakalandığından emin olmak; özellikle de taşımalı toplayıcılarda bu daha da zor
do_garbage_collectionmetoduunsafeolarak işaretlenmiş[1], ancak bunu çağıran tarafın güvenli biçimde çağırabilmesi için neyi garanti etmesi gerektiğine dair bir açıklama yokHeap’e olan tüm referansların kök olarak yakalandığını nasıl garanti ettiklerini merak ediyorum ve bu önemsiz bir mesele değil[2][3][4]
Ayrıca depoyu klonlayıp
cargo testçalıştırdım ve tüm testler şu hatayla başarısız oldu:should be able to add entries to the classpath: InvalidEntry(".../vm/rt.jar"):vm/tests/integration/real_code_tests.rs:15:10[1] https://github.com/andreabergia/rjvm/blob/be9c54066c64a82879...
[2] https://manishearth.github.io/blog/2021/04/05/a-tour-of-safe...
[3] https://without.boats/blog/shifgrethor-iii/
[4] https://coredumped.dev/2022/04/11/implementing-a-safe-garbag...
Bu yüzden o yığını dolaşıp parametreleri ve yerel değişkenleri bularak kök olarak kullanabiliyor
Bu yaklaşımın performans maliyeti var, ancak çöp toplayıcı izlemesi çok daha basit hale geliyor ve coroutine ya da continuation gibi eşzamanlılık ve kontrol akışı ilkel özelliklerini uygulamak da kolaylaşıyor
Güzel bir proje, tebrikler
Ancak “desteklenmeyenler: generics” kısmı biraz tuhaf geldi
JVM’de generics desteğinin tam olarak ne anlama gelmesi gerektiğini merak ediyorum
Bytecode düzeyinde type erasure nedeniyle her şeyi
Object, yani referans türü olarak düşünmek yeterli değil mi? Yoksa kastedilen sınıf tanımı ayrıştırıcısı mı? Öyle olsa bile, temel sözdiziminin ötesinde sınıf dosyasının geçerli olup olmadığını doğrulayan bir mantık görünmüyorAslında yapılması gereken, sınıf, metot ve alanların generic bilgisini taşıyan Signature özniteliğini okumak kadar basit (https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.ht...)
Hatta az önce test ettim, aşağıdaki kod da çalışıyor :-)
public class Generic { public static void main(String[] args) { List strings = new ArrayList(10); strings.add("hey"); strings.add("hackernews"); for (String s : strings) { tempPrint(s); } } private static native void tempPrint(String value); }checkcastişlemini kastediyordur: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.ht...Örneğin
final Main value = list.get(0);gibi bir kod yazarsanız üretilirhttp://henrikeichenhardt.blogspot.com/2013/05/how-are-java-g...
Buna karşılık string interning olmaması çok tuhaf. Uygulaması da oldukça önemsiz; bu olmadan buna JVM demek zor
String’lerin referans temelinde eşit hale gelmesi önemlidir ve JLS’nin bir parçasıdır
Thread’lerin olmaması ise tüm girişimi oyuncak proje seviyesine indiriyor
Gerçekten harika. 1992'de Java üzerinde çalışmaya katıldığımda adı Oak idi ve içinde bulunduğum grup, tüm işletim sistemini Java ile yazma yönünü hedefliyordu
Fikir, yalnızca gereken en küçük kısmı “makine dili”, yani native method olarak bırakıp gömülü işletim sisteminin saldırı yüzeyini azaltmaktı
Java başlangıçta TV'ler veya ev aletleri gibi yerlerde çalışmayı hedefliyordu ve o dönemde native method'lar Rust ile değil C ile yazılıyordu
Rust ile yapılmış bir JVM, bu sürecin tamamına oldukça güçlü bir bellek güvenliği katıyor
İşletim sistemi mantığının önemli bir kısmını Java veya Kotlin ile yazıyor, ama aynı zamanda native koddan oluşan çok sayıda sistem servisini de karıştırıyor ve bunları meşhur ya da kötü şöhretli Binder IPC ile birbirine bağlıyor
[0] - https://en.wikipedia.org/wiki/Singularity_(operating_system)
[1] - https://en.wikipedia.org/wiki/Midori_(operating_system)
Go ile yazılmış bir JVM 17 implementasyonu olarak https://jacobin.org/ da bakmaya değer
Jacobins, 1790'larda Fransız Devrimi döneminin devrimci siyasi kulübüydü ve aynı zamanda https://jacobin.com adresindeki derginin adı
Bu signature'daki lifetime parametreleri yüzünden sınıra dayanıp dayanmadığını merak ediyorum
fn execute_instruction(&mut self, vm: &mut Vm<'a>, call_stack: &mut CallStack<'a>, instruction: Instruction) -> Result, MethodCallFailed<'a>>ResultiçindekiErrvaryantına bir lifetime eklenip bu lifetime invariant olduğunda, buradavmvecall_stacknedeniyle olduğu gibi, genelde?operatörünü veya erken dönüşü kullanamazsınız[1]Bu durumda hata işleme daha ayrıntılı ve okunması zor hale geliyor; pratikte de deneyim böyle miydi diye merak ediyorum
[1] https://users.rust-lang.org/t/nll-and-early-return-not-allow...
'a,vmveyacall_stack'in mutable referanslarında kullanılmadığı için sorun bu değil gibi görünüyorSorun invariant olmanın kendisinden çok Rust'ın mutable referansların lifetime'ını çıkarsama biçimi, ama bu kod bundan kaçınıyor
O halde
VMveCallStacküzerindeki'anın ne anlama geldiğini anlamıyorumSınırsız keyfi bir lifetime, hatta
'staticbile oluşturulabiliyorsa[1][2][3], aslında hiçbir kısıt getirmiyor demektirBu lifetime parametresi burada ne işe yarıyor ve neden kaldırılmıyor, merak ediyorum
[1] https://github.com/andreabergia/rjvm/blob/be9c54066c64a82879...
[2] https://github.com/andreabergia/rjvm/blob/be9c54066c64a82879...
[3] https://github.com/andreabergia/rjvm/blob/be9c54066c64a82879...
Harika bir öğrenme projesi ve yazarın eğlenerek yaptığını görmek güzel
Bir VM'i sıfırdan implement etmek gerçekten çok keyiflidir ve ben de geçmişte böyle işler yaparken çok şey öğrendim
Garbage collection eklemek istersen MMTk'ye de bakabilirsin (https://www.mmtk.io/)
Birden fazla VM'e takılabilecek şekilde tasarlanmış, yüksek kaliteli toplama algoritmaları var ve Rust ile yazılmış
Oyuncak bir projede kullanmayı denemiştim ama Mac kullandığım için vazgeçtim
Çok iyi yapılmış
VM yapmak her zaman eğlencelidir ve Rust'ın type system'i ile birleşince ilginç bir öğrenme deneyimi olmuştur diye düşünüyorum
İş arıyorsan Twitter, Mastodon veya şirket e-postası üzerinden iletişime geçebilirsin. Buradaki kullanıcı kimliğine bakarak seni bulabilirler
Böyle harika projeleri görünce insan kendini ezilmiş hissediyor.
Buna benzer bir şeyi deneyebilmek için Rust’a nasıl başlamak ve temelleri ne kadar öğrenmek gerektiğini, keşke asıl yazar anlatsa.
Konudan çok sapmak istemem ama son zamanlarda kişisel olarak bu duyguyla epey mücadele ediyorum.
Yaklaşık 10 yıldır profesyonel yazılım geliştiricisi olarak çalışıyorum; mevcut unvanıma ve gerçekten ürün çıkarabilme becerime bakınca yetkin olduğumu ve sahte bir geliştirici olmadığımı da biliyorum.
Ama son zamanlarda geliştirici bloglarını okuyunca, sanki yeterince şey bilmiyormuşum ve “gerçek” bir geliştirici değilmişim gibi eziliyorum.
Sanırım kafamda ideal bir geliştirici profili oluşturup kendimi o hayali ölçütle kıyasladığım için böyle hissediyorum.
Derin bilgiye sahip olup kendini açık ve öz biçimde ifade eden insanlara hayranlık duyarken, neden ben öyle değilim diye de düşünüyorum.
İşten sonra ailemle ilgilenince başka bir şey yapacak neredeyse hiç enerjim kalmıyor; programlamanın hayatın tamamı olmadığını da biliyorum ama yine de daha çok öğrenmek ve gelişmek istiyorum.
Bunun ne sağlıklı ne de makul olduğunu biliyorum ama son zamanlarda üzerimden atması zor bir duygu.
Eskiden biraz VM deneyimim vardı ve birkaç yıl önce blogumda bununla ilgili kısa bir yazı dizisi de yazmıştım.
Önceki işimde, bir müşterinin çok sıra dışı sorununu çözmek için biraz JVM bytecode’u kurcalamam da gerekmişti.
Ayrıca birkaç yıl önce harika https://craftinginterpreters.com/ kitabını okudum ve oradan ilham aldım.
Ama bu proje kesinlikle büyük ve karmaşıktı.
Çok zaman aldı ve pek çok yan projem gibi birkaç kez bir kenara bırakıldı, ama sonunda bitirmiş olmaktan mutluyum :-)
Son zamanlarda socket konusuna derinlemesine girdim; daha 2 hafta önce elimde sadece man sayfaları, dokümantasyon ve blog yazılarını üstünkörü okuyarak edinilmiş yüksek seviyeli bir anlayış vardı.
Ağ temellerini anlamak istediğim için bulabildiğim kadar çok şey okuyup incelemeye karar verdim ve bir hafta sonra Python ile C’de socket kodu yazabilecek kadar öğrenmiştim.
Python’ı zaten oldukça iyi bildiğim için, derinlemesine girdikten sonra
socketskütüphanesine bakmak bana çok daha anlamlı geldi.Eğer teknoloji A’yı dil X ile daha iyi yapmak istiyorsanız, teknoloji A hakkında olabildiğince çok okuyup izlemeyi ve dil Y ile bir şeyler yapmayı tavsiye ederim.
Sonra dil X’e döndüğünüzde, teknoloji A’nın etrafındaki kavramlara zaten oldukça aşina olmuş olursunuz.
Basit bir dil VM’inde bellekte nesne gösterimi, bir bytecode interpreter, basit bir çöp toplayıcı ve bir yükleyici olur.
Bytecode interpreter’ı; bir yığın, bu yığın üzerinde fonksiyonları temsil etme yöntemi ve her bytecode’u yorumlayıp program sayacını ilerleten bir döngü olarak görebilirsiniz.
Eğer bu süre 0 saatse, elbette bu kınanacak bir şey değil ve odaklanmanız gereken başka şeyler de olabilir; ama yıllar boyunca yan projelerine haftada ortalama 10-20 saat ayıran birinin etkileyici sonuçlar çıkarması da şaşırtıcı değil.
Benzer bir projeyi biraz utanmazca tanıtmak gerekirse: https://github.com/tenaf0/rust-jvm3
386(486) AT klonları için ücretsiz bir işletim sistemi yapıyorum. Sadece hobi amaçlı; GNU kadar büyük ve profesyonel bir şey değil :-)
osdev.org, sandpile.org, RBIL ve freevga da bakmaya değer.
En büyük baş ağrısı donanım desteği.
Güvenilir port I/O veya belgelenmemiş donanım hileleri gibi tarifler içeren iyi eski basılı kitaplar da çok var.
Intel® 64 and IA-32 Architectures Software Developer’s Manual Combined Volumes: 1, 2A, 2B, 2C, 2D, 3A, 3B, 3C, 3D, and 4
Microsoft MS-DOS Programmer's Reference; buna real-mode BIOS çağrıları da dahil.
PC Interrupts
Undocumented PC
PC Intern
Programmer's Guide To The EGA, VGA, And Super VGA Cards
Graphics Programming Black Book Special Edition
Ayrıca tek çekirdekli, mikrokernel ve hibrit dönemlerinden sonraki işletim sistemi geliştirme ilerlemelerini denemek de değerli olabilir.
seL4 gibi capability tabanlı yapılar, capability ve mükemmel IPC dahil olmak üzere performans ve güvenlik açısından özünde birçok avantaj taşır.
POSIX uyumluluk katmanı da önemli. Thread veya process kavramı olmayan gömülü işletim sistemleri bile POSIX uygulayabilir.
Hypervisor, Intel VT-[xd] varsa çok daha kolay eklenebilir; yoksa emülasyona geri düşebilirsiniz. Çeviri tabanlı emülasyonun performansı çok iyidir.
Interrupt handler’ları genelleştirip hızlandırma, race condition’lardan kaçınma ve lock-free kalıpları kullanma konusunda ustalaşmanız gerekir.
x87 ve MMX dahil olmak üzere desteklenmeyen komutları yeniden yazmak veya trap’lemek de gerekir.
Saf mikrokernel’lerin başarısız olmasının nedeni, birden çok kaynağı işlemsel biçimde sıralayıp yönetmenin getirdiği artan karmaşıklıktı.
Mikrokernel mimarisinin teorik olarak güvenlik ve işletim açısından büyük avantajları var, ama saf haliyle yaygın biçimde yerleşmedi.