C में लिखा गया WebAssembly VM विकसित करना
(irreducible.io)- एक डेवलपर ने 6 महीनों तक शाम और वीकेंड लगाकर C-आधारित WebAssembly VM Semblance बनाया, और इसे जल्दी खत्म हो जाने वाले साइड प्रोजेक्ट की बजाय स्पेसिफिकेशन को गहराई से समझने वाले दीर्घकालिक लर्निंग प्रोजेक्ट के रूप में आगे बढ़ाया
- WebAssembly ब्राउज़र में अविश्वसनीय कोड को native के करीब की गति से चलाने के लिए एक मानक bytecode है, और Fastly·Shopify·Zed जैसे उदाहरणों के साथ इसका उपयोग अब ब्राउज़र के बाहर भी बढ़ रहा है
- Semblance का लक्ष्य WebAssembly Core Specification सीखना और Wasmtime जैसे इंडस्ट्रियल runtime में योगदान देने की तैयारी करना था, इसलिए सभी opcode लागू करना या core test suite पास करना इसके दायरे में शामिल नहीं था
- इंटरप्रेटर
.wasmफ़ाइल के magic bytes और version 1 की पुष्टि करने के बाद section decoding, import resolution, module instantiation, export function lookup, stack और activation frame initialization, और opcode switch loop execution के क्रम में काम करता है - यह अभी पूर्ण runtime नहीं है, इसलिए धीमा है और इसमें memory leak तथा malicious module vulnerability की संभावना बनी हुई है, लेकिन Hello, World! चलाकर इसने स्पेसिफिकेशन सीखने का लक्ष्य हासिल कर लिया
WebAssembly को क्यों चुना
- WebAssembly virtual stack machine के लिए एक मानक instruction set और bytecode format है
- इसकी शुरुआत Emscripten के C/C++ to JavaScript compiler से हुई थी
- Emscripten ने LLVM IR को JavaScript में बदलकर C और C++ कोड को वेब पर चलने योग्य बनाया
- ब्राउज़र डेवलपर्स और Emscripten प्रोजेक्ट ने performance optimization के लिए generated code को JavaScript के एक सरल subset तक सीमित किया, और इसी subset को asm.js के रूप में standardize किया गया
- बाद में JavaScript parsing overhead से बचने के लिए Wasm नाम का एक स्वतंत्र bytecode format डिज़ाइन किया गया
- हाल के वर्षों में ब्राउज़र के बाहर भी WebAssembly का उपयोग बढ़ा है
- Fastly का Edge Compute और Shopify का Functions, WebAssembly engine के ऊपर बने हैं
- Zed का extension system भी Wasm-आधारित है
- Bytecode Alliance ब्राउज़र के बाहर WebAssembly उपयोग को समर्थन देने के लिए इंडस्ट्री-व्यापी प्रयासों का नेतृत्व कर रहा है, और Wasmtime runtime तथा WASI standardization पर काम कर रहा है
- WASI, WebAssembly modules को POSIX-जैसे host calls के माध्यम से host operating system से संवाद करने देता है
- यह standard input/output, filesystem, network जैसी बाहरी interfaces के साथ interaction का मानक तरीका देता है
साइड प्रोजेक्ट से लर्निंग प्रोजेक्ट तक
- बार-बार नए साइड प्रोजेक्ट शुरू करने और कुछ ही हफ्तों में रुचि खोकर दूसरे प्रोजेक्ट पर चले जाने का एक पैटर्न था
- एक ऐसे बड़े प्रोजेक्ट की ज़रूरत थी जिस पर लंबे समय तक ध्यान टिक सके और जो रोज़मर्रा के काम से नीचे के स्तर के computing stack को छूता हो
- दिशा के अभाव में छोड़ दिए गए साइड प्रोजेक्ट बहुत थे, इसलिए ऐसा प्रोजेक्ट चाहिए था जिसमें स्पष्ट लक्ष्य और दिखाई देने वाला परिणाम हो
- यह महसूस करने के बाद कि generalist engineering profile स्थिर काम पाने पर असर डालती है, लेखक T-shaped skillset बना सकने वाला कोई विशेषज्ञता क्षेत्र खोज रहा था
- WebAssembly में custom system calls के साथ अपनी computing environment डिज़ाइन करने की क्षमता, platform engineering tool के रूप में आकर्षक लगी
Semblance के लक्ष्य और दायरा
- प्रोजेक्ट का नाम Semblance है, और यह C में लिखा गया WebAssembly interpreter है
- मुख्य लक्ष्य WebAssembly Core Specification से परिचित होना था
- लंबे समय में Wasmtime जैसे इंडस्ट्रियल runtime में योगदान दे सकने लायक WebAssembly ज्ञान बनाना भी लक्ष्य था
- चूंकि यह सीखने के उद्देश्य वाला प्रोजेक्ट था, इसलिए सभी opcode लागू करने या core test suite पास करने का इरादा नहीं था
- “Hello, World!” चला पाना ही पर्याप्त सफलता मानदंड था
- लेखक ने माना कि उसके पास पेशेवर C development का अनुभव नहीं है और C कोड की गुणवत्ता आदर्श नहीं है, तथा code review का स्वागत है
Hello, World! execution flow
- उदाहरण C कोड बाहरी
putsफ़ंक्शन घोषित करता है औरhello()में"Hello, World!\n"प्रिंट करता है - Clang से WebAssembly बनाया गया
--target=wasm32-nostdlib-Wl,--no-entry-Wl,--export-all-Wl,--allow-undefined-O3
- बनी हुई
hello.wasmकोsemblance hello.wasm --invoke helloसे चलाया गया - प्रोग्राम शुरू होते समय stack पर command-line arguments, decoded WebAssembly module, और runtime state रखने वाले store के लिए memory initialize की जाती है
- command-line arguments parse किए जाते हैं, और गलत होने पर प्रोग्राम बंद हो जाता है
बाइनरी decoding और module structure
- इंटरप्रेटर पहले WebAssembly binary format को decode करता है
- फ़ाइल की शुरुआत में
"\0asm"magic bytes की जांच की जाती है - binary format version
1है या नहीं, यह verify किया जाता है - इसके बाद शामिल sections decode किए जाते हैं
- decoding पूरी होने पर
WasmModulestructure मेंhello.wasmके functions, types, imports, data आदि भर जाते हैंtypesfuncstablesmemsglobalselemsdatasstartimportsexportscustomsmeta
import resolution और env::puts
- Semblance इस समय केवल एक host function देता है:
env::puts - host function import को store में native function pointer असाइन करके, और बने हुए
funcaddrको module केimportsarray में डालकर संभाला जाता है hostcall_putsदिए गए i32 offset का उपयोग करता है- store की पहली memory instance से data pointer लिया जाता है
- offset स्थान की string को
printf("%s", ...)से standard output पर प्रिंट किया जाता है
- import list को iterate करते समय अगर module name
"env"और item name"puts"मिलता है, तोregister_hostcall_putsके जरिए host function register किया जाता है
इंस्टैंशिएशन और opcode execution
- import resolution के बाद module को instantiate किया जाता है
- instantiation में validation stage शामिल होती है
- type checking और module का सही रूप में होना जांचने वाले test किए जाते हैं
- memory, global, और table initialize किए जाते हैं
- अगर
startfunction हो तो उसे call किया जाता है
- instantiation का परिणाम
WasmModuleInstहोता है, जिसमें runtime-time structures और इस्तेमाल के लिए तैयार exports होते हैं --invokecommand-line argument से दिए गए नाम के आधार पर export function खोजा जाता है- exports को iterate करके नाम से मेल खाने वाला
WasmExportInstलौटाया जाता है - फिर जांचा जाता है कि लौटाया गया export function है या नहीं
- exports को iterate करके नाम से मेल खाने वाला
- function call के लिए stack और activation frame initialization ज़रूरी है
- मुख्य execution हिस्सा opcode switch loop है
- function body की Wasm instructions को क्रम से पढ़ा जाता है
- opcode के अनुसार stack और store पर operations किए जाते हैं
- उदाहरण के तौर पर
i32.const,i64.const,f32.const,f64.const,i32.ge_s,call,nop,unreachable,endको handle किया जाता है - जिन opcode को handle नहीं किया गया, उनके लिए
"unhandled opcode [...]"प्रिंट कर trap लौटाया जाता है
call instruction और host function call
WasmOpCallhandling,callopcode के immediate valuefuncidxका उपयोग करके store से function instance खोजती है- function type information के आधार पर stack से ज़रूरी arguments pop किए जाते हैं
- function instance के प्रकार को देखकर यह तय किया जाता है कि वह Wasm function है या native host function
- अगर वह host function है, तो संग्रहीत
hostfuncfunction pointer को native call के रूप में चलाया जाता है - अगर return value हो, तो उसे फिर stack पर push किया जाता है
hellofunction केendopcode तक पहुंचने पर opcode switch loop समाप्त होता है और नियंत्रणmainमें लौट आता है
नतीजे और बची हुई सीमाएँ
- execution result standard output पर इस तरह दिखाई देता है
Hello, World!Ok []
- यह प्रोजेक्ट एक साधारण “Hello, World!” प्रोग्राम चला सकता है
- opcode coverage अभी पूर्ण नहीं है
- कोड बिखरा हुआ है, धीमा है, और इसमें memory leak हैं
- यह malicious modules के प्रति असुरक्षित हो सकता है
- फिर भी, इससे WebAssembly Core Specification के बारे में बहुत कुछ सीखा गया और एक इंजीनियर के रूप में परिचित क्षेत्र से बाहर जाने में मदद मिली
- अपना interpreter बनाने के बाद लेखक को लगा कि अब उसके पास Wasmtime जैसे इंडस्ट्रियल runtime में योगदान देने लायक WebAssembly ज्ञान है
- आगे कुछ और opcode जोड़े जा सकते हैं, लेकिन इस प्रोजेक्ट को मुख्य प्रोजेक्ट की तरह आगे बढ़ाने का चरण अब पूरा करना चाहते हैं
1 टिप्पणियां
Hacker News की राय
मैंने पहले Scheme में एक Wasm interpreter बनाया था, इसलिए और लोगों को इसे खुद implement करते देखना अच्छा लग रहा है
यह जितना लगता है उतना मुश्किल नहीं है, इसलिए spec को एक बार देखकर कोशिश करना अच्छा रहेगा, और मज़ा लेने लायक स्तर तक करना ही काफी है—हर instruction को implement करना ज़रूरी नहीं
उसी तरह के implementer के नज़रिए से एक टिप: spec-test में कई अजीब text Wasm formats होते हैं जिनका compile तरीका सहज नहीं लगता, लेकिन
wast2jsonconverter इस्तेमाल करें तो सामान्य binary Wasm files के साथ एक सरल JSON description भी बनाई जा सकती हैइस test suite को जितनी जल्दी चला सकें, उतना iteration speed और accuracy में मदद मिलती है
सब कुछ काम करने लायक बनाने में थोड़ा समय लगा, लेकिन एक बार सफल होने के बाद ऐसी अवस्था तक बहुत जल्दी पहुँचे जहाँ लगभग कुछ भी चला सकते थे, और भले ही test suite पूरी तरह complete नहीं है, यह लगभग 95% तक cover कर देता है: https://github.com/WebAssembly/testsuite
मैंने ये tests पहले देखे थे, लेकिन इन्हें कैसे इस्तेमाल करना है समझ नहीं आने से बहुत झुंझलाहट और भ्रम हुआ था
Wasm को सीधे interpret करने के संबंध में यह paper काफ़ी दिलचस्प लगा: https://arxiv.org/abs/2205.01183
उसी के आधार पर https://github.com/peterseymour/winter बनाया गया, और इससे पता चला कि Wasm उतना सरल नहीं है जितना पहले लगता है
शुरुआती स्तर का सवाल: जब आप सीधे target के लिए code नहीं लिख रहे हों, तब interpreter debugging कैसे की जाती है, यह जानना चाहता हूँ
यह भी जानना चाहता हूँ कि opcode strings को fuzzing करना कितनी दूर तक मदद करता है, और server-side Wasm engine तथा browser-based engine के बीच व्यावहारिक अंतर कितना बड़ा है, और एक को दूसरे में बदलने में कितना काम लगता है
दिलचस्प approach है और शानदार काम है
जो लोग मुख्य हिस्सा देखना चाहते हैं, उनके लिए वह ज़्यादातर इस file में है: https://github.com/irrio/semblance/blob/main/src/wrun.c
सोचने पर लगता है कि अगर project के standard interface के रूप में Wasm-C-API(https://github.com/WebAssembly/wasm-c-api) अपनाया गया होता तो अच्छा रहता
यह पहले से ही एक C API है, और Wasmer, V8, wasmi जैसे ज़्यादातर Wasm runtimes इसे अपनाते हैं, इसलिए उस API से परिचित developers इसे और आसानी से आज़मा सकते हैं
अगर लेखक Wasm से पर्याप्त परिचित है और Wasmer में योगदान देना चाहता है, तो patches या improvements का स्वागत है
उसने कहा, “VC funding पाने वाली Wasmer ने non-profit organization के नाम WebAssembly को trademark कराने की कोशिश की थी, इस पर समझ में आने वाली चिंता,” और यह गलती स्वीकार करने जैसा है
Installed-Size: 266 MB— यह आखिर है क्याhttps://github.com/wasmerio/wasmer/issues/2615 को छोड़ दिया गया था और बाद में अपने-आप बंद हो गया
थोड़ा अधिक विवादास्पद सवाल है, लेकिन जानना चाहता हूँ कि क्या preliminary tail-call instructions जोड़ने का इरादा है
Wasm spec की तरफ़ से इसे बहुत “high-level” कहकर ठुकरा दिया गया था, लेकिन C committee ने भी कभी Dennis Ritchie के प्रस्ताव को ठुकराया था
फिर भी मेरा दांव अब भी Ritchie की तरफ़ है, और लगता है Rob Pike ने भी उसी दिशा में दांव लगाया था
नहीं तो Golang क्यों बनाया जाता? अगर calls high-level होने पर ही tail calls भी high-level हैं
Orca को एक बार देखना अच्छा रहेगा। लगता है आप वहाँ अच्छा योगदान दे सकते हैं
[0] https://orca-app.dev
मैंने भी नए और चमकदार चीज़ों के बीच भटकने के बजाय एक ही project पर ध्यान केंद्रित करने का फैसला किया है, लेकिन अपवाद के तौर पर AI assistants से छोटे-मोटे काम करवा रहा हूँ
कम से कम इतना तो कहूँगा कि यह काफ़ी निराशाजनक है
और C से call करते समय कई arguments वगैरह संभालने के लिए
libffiजैसी library इस्तेमाल करने का सुझाव भी देने वाला थाजब WebAssembly को zed जैसी जगहों में plugin API के रूप में इस्तेमाल किया जाता है, तो यह जानना दिलचस्प होगा कि plugin developers अपने code को कैसे debug करते हैं
उदाहरण के लिए, क्या breakpoint debugging संभव है, और अगर code crash हो जाए तो क्या stack trace मिल सकता है