दस्तावेज़
आर्किटेक्चर
यह पेज उन ऐप डेवलपर्स के लिए है जो जानना चाहते हैं कि Rust या TSX में लिखे ऐप में count.set(1) से लेकर स्क्रीन पर किसी लेबल के बदलने तक क्या होता है।
ऐप लिखने के लिए इसकी ज़रूरत नहीं है, लेकिन यह समझाता है कि NSH ऐसा व्यवहार क्यों करता है: कंपोनेंट सिर्फ़ एक बार क्यों चलते हैं, JS के व्यस्त रहने पर भी एनिमेशन स्मूद क्यों रहते हैं, और मुख्य थ्रेड आपके कोड का इंतज़ार कभी क्यों नहीं करता।
#बड़ी तस्वीर
#बिल्डिंग: सिर्फ़ आपका कोड JS बनता है
nsh build, nsh dev और बाकी कमांड Rust में लिखे टूलचेन (Node.js के बिना) से आपके ऐप को बंडल करते हैं:
- Oxc TypeScript टाइप हटाता है।
- NSH का JSX कंपाइलर उसी पास में JSX को नेटिव रेंडरर के सीधे कॉल में बदल देता है। स्टैटिक props और चिल्ड्रन एक बार सेट होते हैं; जो एक्सप्रेशन बदल सकते हैं (जो कोई फ़ंक्शन कॉल करते हैं या कोई प्रॉपर्टी पढ़ते हैं) वे बाइंडिंग बन जाते हैं जिन्हें रेंडरर फ़ॉलो करता है। कंपोनेंट साधारण फ़ंक्शन कॉल बन जाते हैं, जिन्हें एक props ऑब्जेक्ट मिलता है और उसके बदलने वाले props गेटर होते हैं।
- rolldown आपके मॉड्यूल को एक ES मॉड्यूल में बंडल करता है। NSH के मॉड्यूल (
nsh,nsh/mobile,nsh/base, …) के इम्पोर्ट इम्पोर्ट ही रहते हैं: वे मॉड्यूल नेटिव हैं और रनटाइम उन्हें उपलब्ध कराता है। बंडल में सिर्फ़ आपके ऐप का कोड होता है; काउंटर उदाहरण 1 KB से भी कम JS का है। - बंडल को बाइटकोड (
bundle.nshc) में कंपाइल किया जाता है, जो सोर्स से तेज़ लोड होता है, और सभी चरणों में एक सोर्स मैप बनाया जाता है ताकि स्टैक ट्रेस आपकी.tsxलाइनों की ओर इशारा करें।
जो इमेज और फ़ॉन्ट आप इम्पोर्ट करते हैं वे असेट के रूप में इकट्ठा होते हैं, और आपके एडिटर के लिए टाइप डिक्लेरेशन .nsh/types में लिखे जाते हैं।
Rust में लिखे ऐप में कोई बंडल नहीं होता: Cargo हर टारगेट का रनर आपके क्रेट को लिंक करके बनाता है, और nsh.json जिस रूट कंपोनेंट का नाम देता है वह इंजन पर नेटिव रूप से चलता है। नीचे की हर बात इस पर भी लागू होती है, फ़र्क बस इतना है कि आपके कंपोनेंट का कोड Rust में चलता है, जबकि TSX ऐप में वह QuickJS में चलता।
#रनिंग: हर ऐप के लिए दो थ्रेड
हर चल रहा ऐप (iOS पर NshAppView, Android पर NshView, पैकेज्ड ऐप की विंडो) एक इंजन है जिसका अपना इंजन थ्रेड होता है, और TSX के लिए अपना QuickJS VM भी। कोई ग्लोबल सिंगलटन नहीं हैं, इसलिए एक होस्ट ऐप में कई ऐप साथ-साथ चल सकते हैं।
#इंजन थ्रेड
आपके ऐप के लॉजिक से जुड़ा सब कुछ यहीं होता है:
- आपका कोड यहीं चलता है: कंपोनेंट, इवेंट हैंडलर, टाइमर, इफ़ेक्ट। Rust कंपोनेंट नेटिव रूप से चलते हैं; TSX QuickJS में चलता है।
- रिएक्टिव ग्राफ़ (सिग्नल, कम्प्यूटेड वैल्यू, इफ़ेक्ट, ओनर, कॉन्टेक्स्ट) नेटिव है। इसके नोड नेटिव क्लास के QuickJS ऑब्जेक्ट हैं, इसलिए गार्बेज कलेक्टर उनके ज़रिए रेफ़रेंस देख पाता है, लेकिन डिपेंडेंसी ट्रैक करना और अपडेट शेड्यूल करना Rust में होता है (यह Solid 1.9 के एल्गोरिदम का पालन करता है)।
- रेंडरर जिसे आपका कंपाइल किया हुआ JSX कॉल करता है, नेटिव है। वह नोड बनाता है और हर बदलाव को एक op में बदलकर सीधे नेटिव क्यू में डालता है: न JSON, न हर prop के लिए अलग मैसेज।
- कोर सभी नोड का एक शैडो ट्री रखता है। जब JS का एक टर्न खत्म होता है, तो वह कतार में लगे सभी ops एक साथ लेकर कमिट करता है: Rust कंपोनेंट रेंडर करता है (
nsh/baseके और ऐप के अपने), padding और margin मेंenv()इनसेट हल करता है, बदले हुए टेक्स्ट दोबारा बनाता है, Taffy से लेआउट करता है (flexbox और grid, डिफ़ॉल्ट रूप से कॉलम और बिना shrink के), फ़्रेम और स्टाइल की तुलना (diff) करता है, और एक MountBatch बनाता है: यानी नेटिव व्यू के लिए ज़रूरी ऑपरेशनों की सूची।
#मुख्य थ्रेड
प्लेटफ़ॉर्म का मुख्य (UI) थ्रेड हर MountBatch को एक ही हॉप में लागू करता है: व्यू बनाना, उन्हें इन्सर्ट करना, स्टाइल और फ़्रेम सेट करना, और लेआउट हो चुका टेक्स्ट सौंपना। इसके अलावा वह सिर्फ़ वही करता है जो उसी पर होना ज़रूरी है:
- टेक्स्ट ड्रॉ करना, जिसका लेआउट इंजन थ्रेड पर पहले ही हो चुका है;
- नेटिव एनिमेशन:
nshके एनिमेटेड वैल्यू फ़्रेम-दर-फ़्रेम चलाए जाते हैं (iOS परCADisplayLink, Android पर UI-थ्रेड एनिमेटर) और सीधे व्यू में लिखे जाते हैं; - जेस्चर और स्क्रॉलिंग: टच इवेंट बनकर इंजन थ्रेड को भेजे जाते हैं;
scrollValueऔरpanउसी फ़्रेम में एनिमेटेड वैल्यू को फ़ीड करते हैं।
मुख्य थ्रेड JS का इंतज़ार कभी नहीं करता। जब सिस्टम को मौके पर ही जवाब चाहिए (क्या यह टेक्स्ट बदल सकता है, क्या बैक बटन इस स्क्रीन को पॉप कर सकता है, क्या यह शीट बंद हो सकती है), तो जवाब पहले से prop के रूप में माउंट कर दिया गया होता है, और JS को उसके बारे में बाद में इवेंट से पता चलता है। व्यस्त इंजन थ्रेड अपडेट में देरी कर सकता है, लेकिन वह स्क्रॉलिंग, एनिमेशन या सिस्टम के जेस्चर को फ़्रीज़ नहीं कर सकता।
#टैप से स्क्रीन तक
main thread engine thread main thread
─────────── ───────────────────────────────────────────────────── ───────────
tap on a view ─ event ─▶ handler runs: count.set(count() + 1)
reactive graph: the bindings that read count() run again
renderer: one op per changed binding → native queue
JS turn ends → core commit: render, text, layout, diff
→ MountBatch (e.g. 1 Text item) ─ hop ─▶ apply: draw the new text
काउंटर उदाहरण में, Mac पर रिलीज़ बिल्ड में एक टैप की लागत JS और कमिट मिलाकर लगभग 0.12 ms है, और इससे मुट्ठी भर माउंट आइटम बनते हैं (टेस्टिंग देखें)।
Rust ऐप में हैंडलर एक Rust क्लोज़र होता है (count.set(count.get() + 1)), और इसके बाद वही ग्राफ़, कमिट और बैच चलते हैं, बीच में कोई इंटरप्रेटर नहीं होता।
इस डिज़ाइन से तीन गुण निकलते हैं:
- एक टर्न एक कमिट है। हैंडलर चाहे जितने सिग्नल सेट करे, व्यू एक बार में, एक साथ बदलते हैं।
- कंपोनेंट एक बार चलते हैं। सिर्फ़ बाइंडिंग दोबारा चलती हैं; किसी कंपोनेंट का री-रेंडर नहीं होता और कोई वर्चुअल DOM diff नहीं होता। जो diff बचता है वह कोर का है, फ़्रेम और स्टाइल पर, Rust में।
- सिर्फ़ बदलाव भेजे जाते हैं। जो स्टाइल और फ़्रेम नहीं बदले वे नहीं भेजे जाते; नए व्यू को कोई डिफ़ॉल्ट वैल्यू नहीं मिलती।
#टेक्स्ट: एक बार लेआउट, मुख्य थ्रेड से बाहर
टेक्स्ट अक्सर UI का सबसे महँगा हिस्सा होता है। NSH उसे इंजन थ्रेड पर प्लेटफ़ॉर्म के अपने टेक्स्ट इंजन से लेआउट करता है, इसलिए वह बिल्कुल नेटिव टेक्स्ट जैसा दिखता है:
- prepare: एक
<Text>और उसके नेस्टेड स्पैन मिलकर एक एट्रिब्यूटेड स्ट्रिंग बन जाते हैं (Apple प्लेटफ़ॉर्म पर CoreText, Android परPrecomputedText/StaticLayout); - measure: लेआउट जो चौड़ाइयाँ माँगता है उन पर माप लिया जाता है; माप कंटेंट और चौड़ाई के आधार पर कैश होते हैं, इसलिए सिर्फ़ रंग बदलने पर दोबारा माप नहीं होता;
- layout: अंतिम साइज़ पर।
मुख्य थ्रेड को अपरिवर्तनीय (immutable) नतीजा मिलता है और वह सिर्फ़ उसे ड्रॉ करता है। <Text> में नेस्ट किया गया टेक्स्ट एक स्पैन होता है, व्यू नहीं; <Text> के बाहर की स्ट्रिंग कुछ रेंडर नहीं करतीं।
#लिस्ट
List नेटिव रूप से वर्चुअलाइज़्ड है। कोर तय करता है कि कौन सी रो व्यूपोर्ट के पास हैं, पोज़िशन, कॉलम और स्टिकी हेडर की गणना करता है, और सेल को रीसायकल करता है। जब किसी सेल को दूसरा आइटम चाहिए होता है, तो रनटाइम उसे बाइंड करता है (आपके रो फ़ंक्शन के item() और index() एक्सेसर बदल जाते हैं); जो फ़्रेम कोई नई रो नहीं दिखाते वे JS को बिल्कुल कॉल नहीं करते। लिस्ट देखें।
#रनटाइम और क्या-क्या करता है
- नेटवर्किंग:
fetchऔर बाकी ग्लोबल API नेटिव क्लास हैं। रिक्वेस्ट प्लेटफ़ॉर्म के HTTP स्टैक (NSURLSession,HttpURLConnection) तक जाती हैं, और दोनों दिशाओं में स्ट्रीमिंग होती है। - इमेज मुख्य थ्रेड से बाहर, व्यू के साइज़ पर डाउनलोड और डिकोड होती हैं, और मेमोरी कैश के साथ।
- सीमाएँ और क्षमताएँ रनटाइम द्वारा Rust में लागू की जाती हैं: सैंडबॉक्स और क्षमताएँ देखें।
- हॉट मॉड्यूल रिप्लेसमेंट भी नेटिव है: पैच रनटाइम द्वारा एक मिलीसेकंड से काफ़ी कम समय में लागू होते हैं (डेव सर्वर और डीबगिंग देखें)।
#तीन लेयर
NSH का UI तीन लेयर में बना है, जैसा GPUI Kit का होता है:
| लेयर | NSH | GPUI Kit | यह क्या है |
|---|---|---|---|
| प्रिमिटिव | nsh-core और रनटाइम (nsh, nsh/mobile, nsh/desktop) | gpui | होस्ट एलिमेंट, स्टाइल, लेआउट, रेंडरिंग, रिएक्टिविटी, एनवायरनमेंट |
| बेस कंपोनेंट | nsh-base (nsh/base) | gpui-base | बिना स्टाइल वाले कंपोनेंट जिनके पास व्यवहार, स्टेट, फ़ोकस, कीबोर्ड और एक्सेसिबिलिटी होती है, और जो अपना लुक ऐप से लेते हैं |
| कंपोनेंट लाइब्रेरी | आपके ऐप की, या कम्युनिटी की | gpui-component | स्टाइल वाले कंपोनेंट: बटन, कार्ड, लुक वाली रो |
पहली दो लेयर Native Shell हैं। तीसरी आपकी है: ऐप अपनी कंपोनेंट लाइब्रेरी nsh/base और प्रिमिटिव पर बनाता है, TSX में या अपने क्रेट में Rust कंपोनेंट के रूप में, या कम्युनिटी की प्रकाशित की हुई लाइब्रेरी इस्तेमाल करता है, या सीधे ऐप में अपना UI स्टाइल करता है।
#क्रेट
NSH एक Rust वर्कस्पेस है। ऐप लिखने के लिए आपको उसे कभी बिल्ड नहीं करना पड़ता (डिस्ट्रीब्यूशन में CLI और पहले से बने रनटाइम आते हैं), लेकिन ये नाम लॉग और डॉक्स में दिखते हैं:
| क्रेट | यह क्या है |
|---|---|
nsh-core | प्लेटफ़ॉर्म-स्वतंत्र कोर: शैडो ट्री, स्टाइल, Taffy लेआउट, टेक्स्ट मर्जिंग, लिस्ट वर्चुअलाइज़ेशन, माउंट आइटम; CSS वैल्यू और Tailwind; नेटिव एनिमेशन ग्राफ़ (वैल्यू, इंटरपोलेशन, timing / spring / decay); Rust कंपोनेंट का फ़्रेमवर्क और थीम; रनटाइम के Rust मॉड्यूल (nsh/mobile का BackHandler, …)। |
nsh-base | nsh/base, बिना स्टाइल वाले बेस कंपोनेंट (NSH का gpui-base): नेविगेशन, डायलॉग, कंट्रोल और स्टेटस बार; Rust ऐप इसे nsh_base के रूप में इस्तेमाल करते हैं। |
nsh-js | QuickJS होस्ट और NSH का रनटाइम: रिएक्टिव ग्राफ़, रेंडरर, हर नेटिव मॉड्यूल, ग्लोबल API, हॉट मॉड्यूल रिप्लेसमेंट, सीमाएँ; और इन सबके TypeScript डिक्लेरेशन। |
nsh-engine | इंजन थ्रेड: nsh-js और nsh-core को चलाता है और हर फ़्रेम में एक बैच प्लेटफ़ॉर्म को सौंपता है। |
nsh-package | साइन किए हुए पैकेज: कुंजियाँ, सर्टिफ़िकेट, रिवोकेशन, ट्रस्ट स्टोर। |
nsh-bundler | बंडलर: rolldown, Oxc, NSH का JSX कंपाइलर, सोर्स मैप, Tailwind क्लास जाँच। |
nsh-dev | डेव सर्वर और उसका क्लाइंट। |
nsh-platform-ios | UIKit बैकएंड (iOS, सिम्युलेटर, Mac Catalyst): व्यू माउंटिंग, CoreText टेक्स्ट इंजन, इनपुट; एम्बेडिंग SDK। |
nsh-platform-android | Android बैकएंड: libnsh.so और Java रनटाइम (NshView, व्यू, इमेज, नेटवर्किंग)। |
nsh-cli | nsh कमांड। |
डिपेंडेंसी की दिशा nsh-core ← nsh-js ← nsh-engine ← प्लेटफ़ॉर्म है; nsh-base nsh-core पर बना है। कोर किसी प्लेटफ़ॉर्म या JS इंजन पर निर्भर नहीं करता, और इसी वजह से nsh test असली रनटाइम को किसी भी मशीन पर हेडलेस चला पाता है।