Rust में लिखा गया JVM
(andreabergia.com)- Rust सीखने के लिए बनाए गए side project
rjvmएक सीखने के उद्देश्य वाला toy JVM है, लेकिन इसे असली.classऔरjarपढ़ने और Java bytecode चलाने तक लागू किया गया है - जिन features का support नहीं है वे हैं threads, reflection, annotations, I/O, JIT compiler, और string interning; लेकिन control flow, object creation, method calls, exceptions, और garbage collection शामिल हैं
- उदाहरणों में OpenJDK 7 के असली
rt.jarका उपयोग किया गया है, जिससेjava.lang.StackTraceElementजैसी classes असली JDK से load होती हैं - code को तीन Rust crates—
reader,vm,vm_cli—में बांटा गया है, और यह.classparsing से लेकर call stack, native methods, values और object modeling तक को cover करता है - आखिरी milestone stop-the-world semispace copying collector था; लक्ष्य पूरा होने के बाद project रोक दिया गया और ज्ञात bugs ठीक करने की भी कोई योजना नहीं है
सीखने के लिए Rust JVM rjvm
rjvmRust सीखने के लिए बनाया गया toy Java Virtual Machine है- code GitHub पर public है
- यह किसी गंभीर JVM implementation के बजाय learning purpose की implementation है, इसलिए ये features support नहीं करता
- threads
- reflection
- annotations
- I/O
- JIT compiler
- string interning
- generics शुरू में unsupported list में थे, लेकिन असल में वे काम करते हैं
कितना implement हुआ है
rjvmसिर्फ parser नहीं है, बल्कि कई JVM behaviors को सीधे implement करता हैif,forजैसे control flow- primitive types और object creation
- virtual methods और static methods calls
- exception handling
- garbage collection
jarfiles से class resolution
- test code में
Throwable,Exception,StackTraceElementइस्तेमाल करने वाले examples शामिल हैं - असली I/O नहीं होने के कारण
System.out.printlnकी जगहtempPrintनाम का native method इस्तेमाल किया जाता है - examples OpenJDK 7 के असली
rt.jarका उपयोग करते हैं, औरjava.lang.StackTraceElementभी असली JDK से लिया जाता है
तीन Rust crates
- project एक standard Rust project है और तीन crates से बना है
reader:.classfiles पढ़ने और उनकी contents को model करने वाले types शामिल करता हैvm: code execute कर सकने वाली virtual machine को library के रूप में देता हैvm_cli:javaexecutable जैसा एक simple command-line launcher शामिल करता है
readercrate को अलग repository में अलग करके crates.io पर publish करने पर विचार किया जा रहा है
.class file parsing
- Java code
javacसे compile होकर.classfile बनता है, और आम तौर परzipformat वाली.jarfile के रूप में distribute होता है - Java code चलाने के लिए पहले compiler द्वारा बनाए गए bytecode वाली
.classfile load करनी होती है - class file में execution और type resolution के लिए जरूरी जानकारी साथ में होती है
- class name और source file name जैसे metadata
- superclass name
- implemented interfaces
- fields और field types, annotations
- method descriptors,
throwsclauses, annotations, generics information - bytecode, exception handler table, line number table
readercrate class file को parse करके class और उसकी contents model करने वाला Rust struct return करता है
method execution और call stack
vmcrate की main APIVm::invokeहै, जिसका उपयोग method execution के लिए होता है- execute हो रहे हर method के लिए
CallStackके अंदर एकCallFrameहोता है mainचलाते समय call stack शुरू में empty होता है, और execution के लिए नया frame बनाया जाता है- हर function call पर call stack में नया frame जोड़ा जाता है, और method execution खत्म होने पर वह frame हटाया जाता है
- ज्यादातर methods Java bytecode में implement होते हैं, लेकिन
rjvmnative methods भी support करता है- native methods Java bytecode नहीं होते, बल्कि JVM खुद सीधे implement करता है
- उदाहरण हैं
System::currentTimeMillis,System::arraycopy,Throwable::fillInStackTrace rjvmमें इन्हें Rust functions के रूप में implement किया गया है
- JVM एक stack-based virtual machine है, इसलिए bytecode instructions मुख्य रूप से value stack पर operate करते हैं
- हर call frame से एक value stack और index से पहचाने जाने वाले local variables का set जुड़ा होता है
values और object modeling
Valuelocal variables, stack elements, और object fields में store हो सकने वाली values को model करता हैValueRustenumके रूप में implement है और इसमें ये states शामिल हैंUninitializedInt(i32)Long(i64)Float(f32)Double(f64)Object(AbstractObject<'a>)Null
- Rust के
enumजैसे sum types यह व्यक्त करने के लिए उपयुक्त हैं कि एक value कई types में से एक हो सकती है - object storage शुरू में class reference और field values रखने वाले
Vec<Value>based simpleObjectstruct से शुरू हुआ था - garbage collector implement होने के बाद यह pointers और casts का बहुत उपयोग करने वाली lower-level implementation में बदल गया
- फिलहाल
AbstractObjectवास्तविक objects या arrays को model करता है, और header words के कुछ हिस्सों तथा field values वाले byte array pointer के रूप में है
bytecode instructions execution
- method execution bytecode instructions को एक-एक करके process करने की प्रक्रिया है
- JVM instructions 200 से अधिक हैं, और bytecode में 1 byte में encoded होते हैं
- कई instructions के बाद arguments आते हैं और कुछ instructions variable length के होते हैं
rjvmJava bytecode instructions कोInstructiontype से model करता है- method execution के समय value stack और local variables array maintain किए जाते हैं, और अगली execute होने वाली instruction का address, यानी program counter, 0 पर initialize किया जाता है
- आम तौर पर instruction execution के बाद program counter अगली instruction पर जाता है, लेकिन jump instructions उसे किसी दूसरी position पर ले जा सकते हैं
- jump instructions
if,for,whileजैसे flow control statements implement करने में इस्तेमाल होते हैं - दूसरे methods call करने वाली instructions की अलग series होती है
- कौन सा method call करना है, इसे तय करने के तरीकों में virtual lookup और static lookup आदि शामिल हैं
- target method resolve करने के बाद call stack में नया frame जोड़ा जाता है और execution शुरू होता है
- अगर return value
voidनहीं है, तो return value stack पर push होती है और execution resume होता है
exception handling
- exceptions सामान्य control flow को तोड़ते हैं, method से early return करते हैं और call stack के साथ propagate हो सकते हैं, इसलिए implementation जटिल होती है
- हर
catchblock method की exception table की एक entry से correspond करता है - exception table entry में handler खोजने के लिए जरूरी information होती है
- लागू होने वाली program counter range
catchblock की पहली instruction का address- उस block द्वारा catch की जाने वाली exception class का नाम
CallFrame::execute_instructioninstruction execution result को व्यक्त करने के लिए Rust केResultका उपयोग करता है- instruction execution result चार states में बंटता है
- successful और current method execution जारी रहता है
- successful और
returninstruction होने के कारण current method return value के साथ समाप्त होता है - internal VM error के कारण execution fail हुआ
- Java exception throw होने के कारण execution fail हुआ
- method execution loop instruction parse करता है, program counter को अगले address पर ले जाता है, फिर instruction execute करता है
- exception होने पर current instruction location के अनुसार exception handler खोजा जाता है
- handler न हो तो exception caller तक propagate होती है
- handler हो तो exception object फिर से stack पर push होता है और
catchhandler location से execution जारी रहता है
- Rust का
Resultऔर pattern matching इस behavior को code structure में व्यक्त करने के लिए अच्छे से फिट बैठते हैं
garbage collection
rjvmका आखिरी बड़ा milestone garbage collector implementation था- चुना गया algorithm stop-the-world semispace copying collector है
- threads नहीं होने के कारण stop-the-world तरीका स्वाभाविक रूप से लागू हो जाता है
- implementation Cheney's algorithm का सरल variant है, और code gc.rs में है
- यह तरीका उपलब्ध memory को दो semispace में बांटता है
- एक active area होता है, जिसका उपयोग object allocation के लिए होता है
- दूसरा unused area के रूप में रहता है
- active area भर जाने पर live objects को दूसरे semispace में copy किया जाता है
- सभी object references को नए copy की ओर point करने के लिए update किया जाता है
- दोनों semispace की roles swap हो जाती हैं
- इस प्रक्रिया को blue-green deployment जैसी approach से तुलना करके समझाया गया है
- algorithm के pros और cons स्पष्ट हैं
- maximum memory का आधा उपयोग नहीं किया जा सकता, इसलिए memory waste बड़ा है
- pointer increment करने वाला तरीका होने से allocation बहुत fast है
- objects को copy और compact करता है, इसलिए memory fragmentation handle करने की जरूरत नहीं होती
- object compaction cache line usage बेहतर करके performance बढ़ा सकता है
- वास्तविक Java VM आम तौर पर G1 या parallel GC जैसे अधिक sophisticated generational garbage collectors का उपयोग करते हैं
project का endpoint
rjvmबनाते हुए Rust और virtual machine implementation के बारे में बहुत कुछ सीखा गया, खासकर वास्तव में काम करने वाले garbage collector implementation से संतुष्टि मिली- garbage collector बहुत polished नहीं है, लेकिन असल में काम करता है
- मूल लक्ष्य पूरा हो जाने के कारण project यहीं रोक दिया गया
- ज्ञात bugs हैं, लेकिन उन्हें ठीक करने की कोई योजना नहीं है
- JVM implementation में Rust इस्तेमाल करना आनंददायक रहा, और आगे के posts में
rjvmimplementation और JVM के काम करने के तरीके पर अधिक detail में चर्चा करने की योजना है
1 टिप्पणियां
Hacker News टिप्पणियाँ
garbage collection को implement करने में मुश्किल हिस्सा यह सुनिश्चित करना है कि सभी references ठीक से roots के रूप में पकड़े जाएँ, खासकर moving collector में यह और भी ज़्यादा कठिन होता है
do_garbage_collectionमेथड कोunsafeके रूप में चिह्नित किया गया है[1], लेकिन इसे कॉल करने वाली side को इसे सुरक्षित रूप से कॉल करने के लिए क्या guarantee देनी होगी, इसकी कोई व्याख्या नहीं हैजिज्ञासा है कि heap के सभी references roots के रूप में पकड़े जाते हैं, यह कैसे सुनिश्चित किया जाता है, और यह कोई मामूली समस्या नहीं है[2][3][4]
साथ ही repository को clone करके
cargo testचलाया, तो सभी testsshould be able to add entries to the classpath: InvalidEntry(".../vm/rt.jar")त्रुटि के साथ fail हो गए: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...
इसलिए उस stack को traverse करके parameters और local variables खोजे जा सकते हैं और उन्हें roots की तरह इस्तेमाल किया जा सकता है
इस तरीके की performance cost है, लेकिन garbage collection tracing बहुत अधिक सरल हो जाती है और coroutine या continuation जैसे concurrency/control-flow primitives को implement करना भी आसान हो जाता है
यह एक अच्छा project है, और बधाई
लेकिन “समर्थन नहीं करता: generics” वाला हिस्सा थोड़ा अजीब लगता है
जिज्ञासा है कि JVM में generics को किस अर्थ में support करना चाहिए
bytecode स्तर पर type erasure की वजह से क्या इसे बस
Object, यानी reference type, मानकर नहीं चलना चाहिए? या फिर class definition parser की बात हो रही है? ऐसा हो तब भी basic syntax के अलावा class file के valid होने की जाँच करने वाला logic दिखता नहीं हैवास्तव में करने लायक काम बस इतना है कि class, method और field की generic जानकारी रखने वाला Signature attribute पढ़ लिया जाए (https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.ht...)
अभी तुरंत test करके देखा, नीचे दिया गया code भी काम करता है :-)
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); }checkcastoperation की बात हो: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.ht...उदाहरण के लिए,
final Main value = list.get(0);जैसा code लिखें तो यह generate होता हैhttp://henrikeichenhardt.blogspot.com/2013/05/how-are-java-g...
दूसरी ओर string interning का न होना बहुत अजीब है। इसे implement करना भी काफ़ी trivial है, और इसके बिना इसे JVM कहना मुश्किल है
strings का reference के आधार पर equal हो जाना महत्वपूर्ण है और यह JLS का हिस्सा है
threads का न होना इस पूरी कोशिश को toy project के स्तर तक सीमित कर देता है
वाकई बहुत शानदार। 1992 में जब मैं Java पर काम करने वाली टीम में शामिल हुआ था, तब इसका नाम Oak था, और जिस ग्रुप में मैं था वह Java में पूरा operating system लिखने की दिशा देख रहा था
विचार यह था कि “machine code”, यानी native methods में सिर्फ़ न्यूनतम ज़रूरी हिस्से ही छोड़े जाएँ, ताकि embedded operating system की attack surface कम की जा सके
मूल रूप से Java का लक्ष्य TV या घरेलू electronic devices जैसी जगहों पर चलना था, और उस समय native methods Rust नहीं बल्कि C में लिखे जाते थे
Rust में बना JVM इस पूरी प्रक्रिया में काफ़ी मज़बूत memory safety जोड़ता है
इसमें operating system logic का बड़ा हिस्सा Java या Kotlin में लिखा जाता है, लेकिन साथ ही native code में लिखी गई बहुत-सी system services भी मिली-जुली रहती हैं, और उन्हें मशहूर या बदनाम Binder IPC से आपस में जोड़ा जाता है
[0] - https://en.wikipedia.org/wiki/Singularity_(operating_system)
[1] - https://en.wikipedia.org/wiki/Midori_(operating_system)
Go में लिखे गए JVM 17 implementation के रूप में https://jacobin.org/ भी देखने लायक है
Jacobins 1790 के दशक की French Revolution के दौरान एक क्रांतिकारी political club था, और https://jacobin.com पर मौजूद पत्रिका का नाम भी यही है
सोच रहा हूँ कि क्या इस signature के lifetime parameter की वजह से कहीं सीमा आ गई होगी
fn execute_instruction(&mut self, vm: &mut Vm<'a>, call_stack: &mut CallStack<'a>, instruction: Instruction) -> Result, MethodCallFailed<'a>>अगर
ResultकेErrvariant में lifetime जोड़ दिया जाए और वह lifetime invariant हो, जैसा यहाँvmऔरcall_stackकी वजह से लगता है, तो आम तौर पर?operator या early return का इस्तेमाल नहीं किया जा सकता[1]तब error handling ज़्यादा verbose और पढ़ने में कठिन हो जाती है; जानना चाहूँगा कि क्या व्यवहार में भी ऐसा ही अनुभव रहा
[1] https://users.rust-lang.org/t/nll-and-early-return-not-allow...
'aका इस्तेमालvmयाcall_stackके mutable references के लिए नहीं हो रहा, इसलिए शायद यह वही समस्या नहीं हैसमस्या invariance से ज़्यादा इस बात से जुड़ी लगती है कि Rust mutable references की lifetime कैसे infer करता है, और यह code उससे बच रहा है
ऐसे में समझ नहीं आता कि
VMऔरCallStackपर लगी'aका मतलब क्या हैअगर इसे बिना किसी सीमा वाली मनमानी lifetime, यहाँ तक कि
'staticसे भी बनाया जा सकता है[1][2][3], तो यह वास्तव में कोई constraint नहीं लगातीजानना चाहूँगा कि यह lifetime parameter यहाँ क्या कर रहा है, और इसे हटाया क्यों नहीं गया
[1] https://github.com/andreabergia/rjvm/blob/be9c54066c64a82879...
[2] https://github.com/andreabergia/rjvm/blob/be9c54066c64a82879...
[3] https://github.com/andreabergia/rjvm/blob/be9c54066c64a82879...
यह एक शानदार learning project है, और अच्छा लगा सुनकर कि लेखक इसे मज़े से कर रहा है
VM को scratch से implement करना सच में बहुत मज़ेदार होता है, और मैंने भी पहले ऐसा काम करते हुए बहुत कुछ सीखा है
अगर आप इसमें garbage collection जोड़कर देखना चाहते हैं, तो MMTk पर नज़र डाल सकते हैं (https://www.mmtk.io/)
इसमें कई high-quality collection algorithms हैं जो अलग-अलग VM में plug करने के लिए डिज़ाइन किए गए हैं, और यह Rust में लिखा गया है
मैं इसे एक toy project में इस्तेमाल करना चाहता था, लेकिन Mac होने की वजह से छोड़ना पड़ा
बहुत बढ़िया बनाया है
VM बनाना हमेशा मज़ेदार होता है, और Rust के type system के साथ मिलकर यह एक दिलचस्प learning experience रहा होगा
अगर आप नौकरी ढूँढ़ रहे हैं, तो Twitter, Mastodon, या company email पर संपर्क कर सकते हैं। यहाँ user ID देखकर आपको ढूँढ़ा जा सकेगा
ऐसे शानदार प्रोजेक्ट्स को देखकर थोड़ा दबाव-सा महसूस होता है
अच्छा होता अगर मूल लेखक यह बताते कि Rust कैसे शुरू करें और उसकी बुनियाद किस स्तर तक सीखनी चाहिए ताकि ऐसी चीज़ आज़माने की हिम्मत हो सके
बहुत ज़्यादा भटकना नहीं चाहता, लेकिन निजी तौर पर मैं आजकल इस भावना से काफ़ी जूझ रहा हूँ
मैं लगभग 10 साल से प्रोफेशनल software developer के तौर पर काम कर रहा हूँ, और अपनी मौजूदा भूमिका तथा सचमुच प्रोडक्ट ship करने की क्षमता देखकर जानता हूँ कि मैं सक्षम हूँ और कोई नकली developer नहीं हूँ
लेकिन आजकल developer blogs पढ़कर मैं दबाव में आ जाता हूँ, जैसे मुझे पर्याप्त जानकारी नहीं है और मैं “असली” developer नहीं हूँ
लगता है कि मैंने अपने दिमाग में एक आदर्श developer की छवि बना रखी है, और उसी काल्पनिक मानक से अपनी तुलना करने पर यह भावना पैदा होती है
जिन लोगों के पास गहरा ज्ञान है और जो साफ़ व संक्षेप में बात रख पाते हैं, उनका सम्मान करता हूँ, लेकिन फिर सोचता हूँ कि मैं वैसा क्यों नहीं हूँ
काम खत्म होने के बाद परिवार की देखभाल कर लूँ तो मेरे पास कुछ और करने की ऊर्जा लगभग नहीं बचती, और यह भी जानता हूँ कि programming ही सब कुछ नहीं है, फिर भी और सीखने तथा बढ़ने की इच्छा रहती है
पता है कि यह न तो स्वस्थ है न ही तर्कसंगत, लेकिन इन दिनों इस भावना को झटकना मुश्किल हो रहा है
पहले VM का थोड़ा अनुभव था, और कुछ साल पहले ब्लॉग पर छोटी-सी एक शृंखला भी लिखी थी
पिछली नौकरी में ग्राहकों की एक बहुत अजीब समस्या सुलझाने के लिए JVM bytecode के साथ थोड़ा छेड़छाड़ भी की थी
और कुछ साल पहले शानदार https://craftinginterpreters.com/ पढ़ी थी, वहीं से कुछ ideas मिले थे
लेकिन यह प्रोजेक्ट निश्चित ही बड़ा और जटिल था
इसमें बहुत समय लगा, और मेरी कई side projects की तरह इसे भी कुछ बार छोड़ दिया गया, लेकिन आखिरकार पूरा कर लेने की खुशी है :-)
हाल ही में मैंने sockets को गहराई से पढ़ा, जबकि 2 हफ्ते पहले तक manual pages, docs, blog posts वगैरह को सरसरी तौर पर पढ़कर मिली बस ऊपरी समझ ही थी
networking की बुनियाद समझनी थी, इसलिए तय किया कि जितना हो सके उतना पढ़ूँ, और एक हफ्ते बाद मैं Python और C में socket code लिख सकने लायक सीख गया
Python मुझे काफ़ी अच्छी तरह आती है, इसलिए गहराई में जाने के बाद
socketslibrary को देखना कहीं बेहतर समझ आयाअगर आप तकनीक A को भाषा X में बेहतर करना चाहते हैं, तो मैं सलाह दूँगा कि तकनीक A के बारे में जितना हो सके पढ़ें या देखें, और भाषा Y में कुछ बनाकर देखें
उसके बाद जब भाषा X में लौटेंगे, तो तकनीक A के आसपास के concepts से आप पहले ही काफ़ी परिचित हो चुके होंगे
एक साधारण language VM में memory के अंदर object representation, bytecode interpreter, एक simple garbage collector, और loader होगा
bytecode interpreter को एक stack, उस stack पर functions को दर्शाने के तरीके, और हर bytecode को interpret करके program counter आगे बढ़ाने वाले loop के रूप में देखा जा सकता है
अगर 0 घंटे हैं, तो यह निश्चित ही कोई दोष की बात नहीं है, क्योंकि ध्यान देने के लिए और भी बहुत चीज़ें होती हैं, लेकिन अगर कोई व्यक्ति कई सालों तक औसतन हर हफ्ते 10~20 घंटे side projects पर लगाता रहा है, तो उसका प्रभावशाली नतीजे देना हैरानी की बात नहीं है
इसी तरह के एक प्रोजेक्ट का बेशर्मी से प्रचार करूँ तो: https://github.com/tenaf0/rust-jvm3
मैं 386(486) AT clone के लिए एक मुफ़्त operating system बना रहा हूँ। बस एक hobby है, gnu जितना बड़ा और प्रोफेशनल नहीं :-)
osdev.org, sandpile.org, RBIL, freevga भी देखने लायक हैं
सबसे बड़ा सिरदर्द hardware support है
भरोसेमंद port I/O या undocumented hardware tricks जैसी recipes वाली अच्छी पुरानी कागज़ी किताबें भी बहुत हैं
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, जिसमें real-mode BIOS calls भी शामिल हैं
PC Interrupts
Undocumented PC
PC Intern
Programmer's Guide To The EGA, VGA, And Super VGA Cards
Graphics Programming Black Book Special Edition
साथ ही single kernel, microkernel, और hybrid युग के बाद operating system development में हुई प्रगति पर प्रयोग करना भी सार्थक है
seL4 जैसी capability-based संरचनाओं में capability और बेहतरीन IPC सहित performance और security के लिहाज़ से कई मूलभूत फायदे हैं
POSIX compatibility layer भी महत्वपूर्ण है। threads या process की अवधारणा न रखने वाले embedded operating systems भी POSIX को implement कर सकते हैं
अगर Intel VT-[xd] हो तो hypervisor को जोड़ना बहुत आसान हो जाता है, और न हो तो emulation पर वापस जाया जा सकता है। translation-based emulation की performance बहुत अच्छी होती है
आपको interrupt handlers को generalize और तेज़ बनाने, race conditions से बचने, और lock-free patterns इस्तेमाल करने में दक्ष होना होगा
x87 और MMX समेत unsupported instructions को rewrite या trap करना भी ज़रूरी है
pure microkernel के विफल होने का कारण यह था कि कई resources को transactional तरीके से क्रमबद्ध और प्रबंधित करने की जटिलता बहुत बढ़ गई थी
microkernel architecture में सैद्धांतिक रूप से security और operations के बड़े फायदे हैं, लेकिन अपने शुद्ध रूप में यह व्यापक रूप से स्थापित नहीं हो पाया