JVM yang Ditulis dengan Rust
(andreabergia.com)rjvm, proyek sampingan untuk belajar Rust, adalah JVM mainan untuk pembelajaran, tetapi sudah diimplementasikan sampai tingkat bisa membaca.classdanjarsungguhan serta menjalankan bytecode Java- Fitur yang tidak didukung adalah thread, reflection, annotation, I/O, kompiler JIT, dan string interning, tetapi mencakup alur kontrol, pembuatan objek, pemanggilan metode, exception, dan garbage collection
- Contohnya menggunakan
rt.jarasli dari OpenJDK 7, sehingga kelas sepertijava.lang.StackTraceElementdimuat dari JDK sungguhan - Kodenya dibagi menjadi tiga crate Rust:
reader,vm, danvm_cli, yang mencakup mulai dari parsing.classhingga call stack, native method, serta pemodelan nilai dan objek - Tonggak terakhirnya adalah stop-the-world semispace copying collector, dan setelah target tercapai proyek dihentikan tanpa rencana memperbaiki bug yang sudah diketahui
rjvm, Rust JVM untuk pembelajaran
rjvmadalah Java Virtual Machine mainan yang dibuat untuk belajar Rust- Kodenya dipublikasikan di GitHub
- Karena ini lebih merupakan implementasi untuk tujuan belajar daripada implementasi JVM yang serius, fitur berikut tidak didukung
- thread
- reflection
- annotation
- I/O
- kompiler JIT
- string interning
- Generic awalnya masuk daftar yang tidak didukung, tetapi ternyata sebenarnya berfungsi
Sejauh mana implementasinya
rjvmbukan sekadar parser sederhana, tetapi juga mengimplementasikan langsung berbagai perilaku JVM- alur kontrol seperti
ifdanfor - tipe primitif dan pembuatan objek
- pemanggilan metode virtual dan metode statis
- penanganan exception
- garbage collection
- resolusi kelas dari file
jar
- alur kontrol seperti
- Kode pengujian mencakup contoh yang menggunakan
Throwable,Exception, danStackTraceElement - Karena tidak ada I/O sungguhan,
tempPrintdigunakan sebagai native method sebagai penggantiSystem.out.println - Contohnya memakai
rt.jarasli dari OpenJDK 7, danjava.lang.StackTraceElementjuga diambil dari JDK sungguhan
Tiga crate Rust
- Proyek ini adalah proyek Rust standar dan terdiri dari tiga crate
reader: membaca file.classdan berisi tipe untuk memodelkan isinyavm: menyediakan mesin virtual dalam bentuk library yang dapat menjalankan kodevm_cli: berisi launcher command line sederhana yang mirip executablejava
- Ada pertimbangan untuk memisahkan crate
readerke repositori terpisah dan menerbitkannya ke crates.io
Parsing file .class
- Kode Java dikompilasi dengan
javacmenjadi file.class, dan biasanya didistribusikan dalam file.jaryang berformatzip - Untuk menjalankan kode Java, pertama-tama perlu memuat file
.classyang berisi bytecode hasil keluaran kompiler - File kelas juga memuat informasi yang diperlukan untuk eksekusi dan resolusi tipe
- metadata seperti nama kelas dan nama file sumber
- nama superclass
- interface yang diimplementasikan
- field, tipe field, dan annotation
- descriptor metode, klausa
throws, annotation, dan informasi generic - bytecode, tabel exception handler, dan tabel nomor baris
- Crate
readermem-parsing file kelas dan mengembalikan Rust struct yang memodelkan kelas dan isinya
Eksekusi metode dan call stack
- API utama dari crate
vmadalahVm::invoke, yang digunakan untuk menjalankan metode - Setiap metode yang sedang berjalan memiliki satu
CallFramedi dalamCallStack - Saat
maindijalankan, call stack awalnya kosong, lalu frame baru dibuat untuk eksekusi - Setiap pemanggilan fungsi menambahkan frame baru ke call stack, dan ketika eksekusi metode selesai frame tersebut dihapus
- Sebagian besar metode diimplementasikan dengan bytecode Java, tetapi
rjvmjuga mendukung native method- Native method adalah metode yang tidak diimplementasikan dengan bytecode Java, melainkan langsung oleh JVM itu sendiri
- Contohnya
System::currentTimeMillis,System::arraycopy, danThrowable::fillInStackTrace - Di
rjvm, ini diimplementasikan sebagai fungsi Rust
- JVM adalah mesin virtual berbasis stack, sehingga instruksi bytecode terutama bekerja di atas value stack
- Setiap call frame terhubung ke value stack dan sekumpulan variabel lokal yang diidentifikasi lewat indeks
Pemodelan nilai dan objek
Valuememodelkan nilai yang dapat disimpan dalam variabel lokal, elemen stack, atau field objekValuediimplementasikan sebagai Rustenumdan mencakup status berikutUninitializedInt(i32)Long(i64)Float(f32)Double(f64)Object(AbstractObject<'a>)Null
- Sum type seperti Rust
enumsangat cocok untuk menyatakan bahwa satu nilai bisa menjadi salah satu dari beberapa tipe - Penyimpanan objek awalnya dimulai dari struct
Objectsederhana berbasisVec<Value>yang memuat referensi kelas dan nilai field - Setelah implementasi garbage collector, itu berubah menjadi implementasi tingkat lebih rendah yang banyak memakai pointer dan cast
- Saat ini
AbstractObjectmemodelkan objek nyata atau array, berupa pointer ke array byte yang berisi beberapa header word dan nilai field
Eksekusi instruksi bytecode
- Eksekusi metode adalah proses menangani instruksi bytecode satu per satu
- Instruksi JVM berjumlah lebih dari 200 dan dikodekan sebagai 1 byte dalam bytecode
- Banyak instruksi diikuti argumen, dan sebagian instruksi memiliki panjang variabel
rjvmmemodelkan instruksi bytecode Java dengan tipeInstruction- Saat metode dijalankan, value stack dan array variabel lokal dipertahankan, lalu program counter yang menunjukkan alamat instruksi berikutnya diinisialisasi ke 0
- Biasanya setelah sebuah instruksi dieksekusi, program counter berpindah ke instruksi berikutnya, tetapi instruksi lompat dapat memindahkannya ke posisi lain
- Instruksi lompat digunakan untuk mengimplementasikan pernyataan kontrol alur seperti
if,for, danwhile - Ada kelompok instruksi tersendiri untuk memanggil metode lain
- Cara menentukan metode yang akan dipanggil mencakup lookup virtual dan lookup statis
- Setelah metode target di-resolve, frame baru ditambahkan ke call stack dan eksekusi dimulai
- Jika nilai kembalian bukan
void, nilai itu akan di-push ke stack dan eksekusi dilanjutkan
Penanganan exception
- Exception memutus alur kontrol normal, menyebabkan return lebih awal dari metode, dan bisa merambat sepanjang call stack, sehingga implementasinya rumit
- Setiap blok
catchberkorespondensi dengan satu entri tabel exception milik metode - Entri tabel exception berisi informasi yang diperlukan untuk menemukan handler
- rentang program counter yang berlaku
- alamat instruksi pertama dari blok
catch - nama kelas exception yang ditangkap blok tersebut
CallFrame::execute_instructionmemakaiResultdari Rust untuk menyatakan hasil eksekusi instruksi- Hasil eksekusi instruksi dibagi menjadi empat status
- berhasil dan melanjutkan eksekusi metode saat ini
- berhasil dan, karena instruksi
return, metode saat ini selesai beserta nilai kembaliannya - eksekusi gagal karena galat internal VM
- eksekusi gagal karena exception Java dilempar
- Loop eksekusi metode mem-parsing instruksi, memindahkan program counter ke alamat berikutnya, lalu mengeksekusi instruksi
- Jika exception terjadi, handler exception yang cocok untuk posisi instruksi saat ini akan dicari
- jika tidak ada handler, exception diteruskan ke pemanggil
- jika ada handler, objek exception di-push kembali ke stack dan eksekusi dilanjutkan dari posisi handler
catch
Resultdan pattern matching di Rust sangat cocok untuk mengekspresikan perilaku ini dalam struktur kode
Garbage collection
- Tonggak besar terakhir
rjvmadalah implementasi garbage collector - Algoritme yang dipilih adalah stop-the-world semispace copying collector
- Karena tidak ada thread, pendekatan stop-the-world berlaku secara alami
- Implementasinya adalah variasi yang lebih sederhana dari algoritme Cheney, dan kodenya ada di gc.rs
- Pendekatan ini membagi memori yang tersedia menjadi dua semispace
- satu sebagai area aktif untuk alokasi objek
- satu lagi dibiarkan sebagai area yang tidak digunakan
- ketika area aktif penuh, objek yang masih hidup disalin ke semispace lain
- semua referensi objek diperbarui agar menunjuk ke salinan yang baru
- peran kedua semispace kemudian ditukar
- Prosedur ini dianalogikan dengan pendekatan yang mirip blue-green deployment
- Algoritme ini memiliki kelebihan dan kekurangan yang jelas
- hanya setengah dari memori maksimum yang bisa digunakan, sehingga pemborosan memori cukup besar
- alokasi sangat cepat karena hanya menaikkan pointer
- karena objek disalin dan dipadatkan, tidak perlu menangani fragmentasi memori
- pemadatan objek dapat meningkatkan kinerja lewat pemanfaatan cache line yang lebih baik
- VM Java nyata umumnya menggunakan garbage collector generasional yang lebih canggih seperti G1 atau parallel GC
Titik akhir proyek
- Dengan membuat
rjvm, banyak hal dipelajari tentang Rust dan implementasi mesin virtual, terutama kepuasan karena berhasil membuat garbage collector yang benar-benar bekerja - Garbage collector ini belum terlalu matang, tetapi benar-benar berfungsi
- Karena target awal telah tercapai, proyek dihentikan sampai di sini
- Ada bug yang sudah diketahui, tetapi tidak ada rencana untuk memperbaikinya
- Rust adalah bahasa yang menyenangkan untuk dipakai membangun JVM, dan di tulisan berikutnya implementasi
rjvmserta cara kerja JVM akan dibahas lebih detail
1 komentar
Komentar Hacker News
Bagian sulit dalam implementasi garbage collection adalah memastikan semua referensi benar-benar tertangkap sebagai root, terutama pada collector yang memindahkan objek
Metode
do_garbage_collectionditandai sebagaiunsafe[1], tetapi tidak ada penjelasan tentang apa yang harus dijamin oleh pihak pemanggil agar bisa memanggilnya dengan amanSaya penasaran bagaimana mereka menjamin bahwa semua referensi ke heap tertangkap sebagai root, dan ini bukan masalah sepele[2][3][4]
Selain itu, setelah meng-clone repositori dan menjalankan
cargo test, semua test gagal dengan errorshould 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...
Karena itu, stack tersebut bisa ditelusuri untuk menemukan parameter dan variabel lokal, lalu menggunakannya sebagai root
Pendekatan seperti ini punya biaya performa, tetapi tracing untuk garbage collection menjadi jauh lebih sederhana, dan primitive konkurensi maupun alur kontrol seperti coroutine atau continuation juga lebih mudah diimplementasikan
Proyek yang bagus, selamat
Namun bagian “yang tidak didukung: generics” terasa agak aneh
Saya penasaran dalam arti apa JVM harus mendukung generics
Pada level bytecode, karena type erasure, bukankah semuanya bisa dianggap sederhana sebagai
Object, yaitu tipe referensi? Atau yang dimaksud parser definisi class? Meski begitu, selain sintaks dasar, sepertinya tidak ada logika untuk memeriksa apakah class file validYang sebenarnya perlu dilakukan hanya membaca atribut Signature yang memuat informasi generics untuk class, method, dan field (https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.ht...)
Saya baru saja mencobanya, dan kode di bawah ini juga berjalan :-)
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); }checkcast: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.ht...Misalnya, kode seperti
final Main value = list.get(0);akan menghasilkannyahttp://henrikeichenhardt.blogspot.com/2013/05/how-are-java-g...
Sebaliknya, tidak adanya string interning terasa sangat aneh. Implementasinya juga cenderung sepele, dan tanpa ini sulit menyebutnya JVM
Kesamaan string berdasarkan referensi itu penting dan merupakan bagian dari JLS
Tidak adanya thread membuat seluruh upaya ini terasa setingkat proyek mainan
Benar-benar keren. Saat saya bergabung mengerjakan Java pada 1992, namanya masih Oak, dan grup tempat saya berada sedang melihat arah untuk menulis seluruh sistem operasi dengan Java
Idenya adalah mengurangi permukaan serangan sistem operasi tertanam dengan hanya menyisakan bagian minimum yang diperlukan sebagai “bahasa mesin”, yaitu metode native
Awalnya Java ditujukan untuk berjalan di tempat seperti TV atau peralatan rumah tangga, dan saat itu metode native dikerjakan dengan C, bukan Rust
JVM yang dibuat dengan Rust menambahkan keamanan memori yang cukup kuat ke seluruh proses ini
Banyak logika sistem operasi ditulis dengan Java atau Kotlin, tetapi pada saat yang sama juga bercampur dengan banyak layanan sistem berbasis kode native, dan semuanya dihubungkan lewat Binder IPC yang terkenal, atau terkenal buruk
[0] - https://en.wikipedia.org/wiki/Singularity_(operating_system)
[1] - https://en.wikipedia.org/wiki/Midori_(operating_system)
Implementasi JVM 17 yang ditulis dengan Go, https://jacobin.org/, juga layak dilihat
Jacobins adalah klub politik revolusioner pada masa Revolusi Prancis tahun 1790-an, dan juga nama majalah di https://jacobin.com
Saya penasaran apakah mereka terbentur batasan karena parameter lifetime pada signature ini
fn execute_instruction(&mut self, vm: &mut Vm<'a>, call_stack: &mut CallStack<'a>, instruction: Instruction) -> Result, MethodCallFailed<'a>>Jika Anda menambahkan lifetime ke varian
ErrdariResultdan lifetime itu invariant, seperti biasanya terjadi di sini karenavmdancall_stack, Anda biasanya jadi tidak bisa memakai operator?atau melakukan early return[1]Akibatnya penanganan error menjadi lebih bertele-tele dan sulit dibaca; saya penasaran apakah memang seperti itu pengalamannya
[1] https://users.rust-lang.org/t/nll-and-early-return-not-allow...
'atidak dipakai pada referensi mutable untukvmataucall_stackMasalahnya bukan invariance itu sendiri, melainkan cara Rust menginfer lifetime referensi mutable, dan kode ini menghindarinya
Kalau begitu, saya tidak tahu apa makna
'ayang ditempelkan padaVMdanCallStackJika bisa dibuat dengan lifetime arbitrer yang tidak dibatasi, bahkan
'static[1][2][3], berarti tidak ada constraint apa pun yang diterapkanSaya penasaran apa yang dilakukan parameter lifetime ini di sini, dan mengapa tidak dihapus saja
[1] https://github.com/andreabergia/rjvm/blob/be9c54066c64a82879...
[2] https://github.com/andreabergia/rjvm/blob/be9c54066c64a82879...
[3] https://github.com/andreabergia/rjvm/blob/be9c54066c64a82879...
Ini proyek belajar yang bagus, dan menyenangkan melihat penulisnya menikmatinya
Mengimplementasikan VM dari nol itu benar-benar menyenangkan, dan dulu saya banyak belajar saat mengerjakan hal semacam itu
Jika ingin mencoba menambahkan garbage collection, ada baiknya melihat MMTk (https://www.mmtk.io/)
Ada algoritma koleksi berkualitas tinggi yang dirancang agar bisa dipasang ke berbagai VM, dan ditulis dengan Rust
Saya sempat ingin memakainya untuk proyek mainan, tapi menyerah karena memakai Mac
Dibuat dengan sangat baik
Membuat VM selalu menyenangkan, dan jika digabungkan dengan sistem tipe Rust, sepertinya ini menjadi pengalaman belajar yang menarik
Jika sedang mencari pekerjaan, silakan hubungi saya lewat Twitter, Mastodon, atau email perusahaan. Seharusnya bisa ditemukan dari ID pengguna saya di sini
Saat melihat proyek sekeren ini, rasanya kewalahan
Saya ingin penulis aslinya menjelaskan bagaimana memulai Rust dan seberapa jauh perlu menguasai dasar-dasarnya sampai bisa mencoba hal seperti ini
Saya tidak ingin terlalu melenceng, tetapi secara pribadi belakangan ini saya cukup bergulat dengan perasaan seperti ini
Saya sudah bekerja sebagai pengembang perangkat lunak profesional hampir 10 tahun, dan melihat jabatan saya saat ini serta kemampuan saya untuk benar-benar merilis produk, saya tahu saya kompeten dan bukan pengembang palsu
Namun belakangan ini, saat membaca blog pengembang, saya merasa kewalahan seolah-olah saya tidak cukup tahu dan bukan pengembang “sungguhan”
Sepertinya perasaan ini muncul karena saya membuat sosok pengembang ideal di kepala, lalu membandingkan diri saya dengan standar khayalan itu
Sambil mengagumi orang-orang yang punya banyak pengetahuan mendalam dan mampu mengungkapkannya dengan jelas dan ringkas, saya jadi bertanya-tanya mengapa saya tidak seperti itu
Setelah selesai bekerja dan mengurus keluarga, saya hampir tidak punya energi untuk melakukan hal lain, dan saya juga tahu pemrograman bukan segalanya, tetapi tetap ada keinginan untuk belajar lebih banyak dan berkembang
Saya tahu ini tidak sehat dan tidak rasional, tetapi belakangan ini perasaan itu sulit disingkirkan
Dulu saya punya sedikit pengalaman dengan VM, dan beberapa tahun lalu pernah menulis seri tulisan pendek di blog
Di pekerjaan sebelumnya, saya juga pernah sedikit mengutak-atik bytecode JVM untuk menyelesaikan masalah pelanggan yang sangat spesifik
Beberapa tahun lalu saya juga membaca https://craftinginterpreters.com/ yang sangat bagus, dan mendapat ide dari sana
Namun proyek ini jelas besar dan kompleks
Ini memakan banyak waktu, dan seperti banyak proyek sampingan saya, sempat beberapa kali terbengkalai, tetapi saya senang akhirnya selesai :-)
Baru-baru ini saya mendalami socket, dan dua minggu lalu saya hanya punya pemahaman tingkat tinggi yang didapat dari membaca sekilas halaman manual, dokumentasi, tulisan blog, dan sebagainya
Karena ingin memahami dasar-dasar networking, saya memutuskan untuk membaca sebanyak mungkin, dan seminggu kemudian saya sudah belajar cukup untuk menulis kode socket dengan Python dan C
Karena saya cukup memahami Python, setelah mendalaminya, melihat library
socketsjadi lebih mudah dipahamiJika Anda ingin lebih mahir pada teknologi A dengan bahasa X, saya menyarankan untuk membaca atau menonton sebanyak mungkin tentang teknologi A, lalu mencoba membuat sesuatu dengan bahasa Y
Setelah itu, ketika kembali ke bahasa X, Anda sudah cukup banyak menguasai konsep di sekitar teknologi A
VM untuk bahasa sederhana mungkin memiliki representasi objek di memori, interpreter bytecode, garbage collector sederhana, dan loader
Interpreter bytecode bisa dilihat sebagai stack, cara merepresentasikan fungsi di atas stack itu, serta loop yang menafsirkan setiap bytecode dan menggerakkan program counter
Jika 0 jam, tentu itu bukan sesuatu yang patut disalahkan dan ada banyak hal lain yang bisa difokuskan, tetapi tidak mengherankan jika orang yang selama bertahun-tahun rata-rata mengerjakan proyek sampingan 10–20 jam per minggu menghasilkan sesuatu yang mengesankan
Sekalian promosi tanpa malu-malu proyek serupa: https://github.com/tenaf0/rust-jvm3
Saya sedang membuat sistem operasi gratis untuk klon 386(486) AT. Ini hanya hobi, bukan sesuatu yang besar dan profesional seperti gnu :-)
osdev.org, sandpile.org, RBIL, dan freevga juga layak dijadikan referensi
Sakit kepala terbesarnya adalah dukungan hardware
Ada juga banyak buku kertas vintage yang bagus berisi resep seperti port I/O yang andal atau trik hardware yang tidak terdokumentasi
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, termasuk pemanggilan BIOS real-mode
PC Interrupts
Undocumented PC
PC Intern
Programmer's Guide To The EGA, VGA, And Super VGA Cards
Graphics Programming Black Book Special Edition
Perkembangan pengembangan sistem operasi setelah era kernel monolitik, microkernel, dan hibrida juga layak dieksperimenkan
Struktur berbasis capability seperti seL4 memiliki sejumlah keunggulan mendasar dalam performa dan keamanan, termasuk capability dan IPC yang unggul
Lapisan kompatibilitas POSIX juga penting. Sistem operasi embedded yang tidak memiliki konsep thread atau proses pun bisa mengimplementasikan POSIX
Hypervisor jauh lebih mudah ditambahkan jika ada Intel VT-[xd], dan jika tidak ada, bisa mundur ke emulasi. Emulasi berbasis translasi memiliki performa yang sangat baik
Anda perlu mahir menggeneralisasi dan mempercepat interrupt handler, menghindari race condition, dan menggunakan pola lock-free
Juga perlu menulis ulang atau menjebak instruksi yang tidak didukung, termasuk x87 dan MMX
Alasan microkernel murni gagal adalah karena kompleksitas untuk mengurutkan dan mengelola berbagai resource secara transaksional menjadi besar
Arsitektur microkernel secara teori memiliki keunggulan besar dalam keamanan dan operasional, tetapi dalam bentuk murninya tidak pernah tersebar luas