2 पॉइंट द्वारा GN⁺ 2025-01-13 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • C23 तक standard C लगातार बदलता रहा है, लेकिन Walter Bright का मानना है कि constant-expression evaluation, forward references और header dependency जैसी पुरानी असुविधाएँ अब भी development flow को बाधित करती हैं
  • D compiler में built-in C compiler ImportC नई implementation होने का फायदा उठाकर constant-expression वाली जगहों पर शर्तें पूरी करने वाले functions को compile time पर चला सकता है
  • CTFE संभव हो जाए तो _Static_assert(sum(3, 4) == 7,...) जैसे unit tests को अलग executable के बिना हर compilation पर चलाया जा सकता है
  • standard C में declaration order की पाबंदी forward declarations की पुनरावृत्ति और code को उल्टे क्रम में रखने की नौबत लाती है, लेकिन ImportC global declaration order से कम बंधा हुआ तरीका अपनाता है
  • __import dex; की तरह .c file की declarations को सीधे import करने से अलग .h files की ज़रूरत घटती है, और header तथा implementation के mismatch से पैदा होने वाला debugging burden भी कम होता है

standard C में बची हुई असुविधाएँ

  • standard C में C23 तक नियमित सुधार हुए हैं, लेकिन Walter Bright के अनुसार अब भी कुछ हिस्से ऐसे हैं जिन्हें ठीक नहीं किया गया है
  • Dlang community ने D programming language compiler के अंदर एक C compiler embed किया है, और इस compiler को ImportC कहा जाता है
  • ImportC को शुरू से नया बनाया गया, इसलिए modern compiler technology के साथ C की कमियों को संभालने की गुंजाइश थी
  • लेख में चार बिंदुओं पर बात की गई है
    • constant-expression evaluation
    • compile time unit tests
    • declarations के forward references
    • declarations import करना

compile time function execution और constant-expression evaluation

  • C simple expressions को constant folding के जरिए compile time पर calculate कर सकता है, लेकिन standard C में functions को compile time पर execute नहीं किया जा सकता
  • उदाहरण code enum E { A = 3, B = 4, C = sum(5, 6) }; को gcc से compile करने पर error आता है कि C का enumerator value integer constant नहीं है
  • ImportC वही code compile कर सकता है
  • दिशा यह है कि C syntax में जहाँ-जहाँ constant-expression आता है, वहाँ compiler functions को compile time पर execute कर सके
  • हालाँकि वह function I/O, mutable global variables तक पहुँच, system calls जैसी चीजें नहीं करनी चाहिए

हर compilation पर चलने वाले unit tests

  • compile time function evaluation यानी CTFE संभव हो जाए तो unit testing का तरीका भी बदल सकता है
  • C code में unit tests कम दिखने की एक वजह यह है कि अलग build target और executable बनाकर उसे चलाना झंझट भरा होता है
  • उदाहरण code _Static_assert(sum(3, 4) == 7, "test #1"); को gcc से compile करने पर error आता है कि static assertion के अंदर की expression constant नहीं है
  • ImportC इस code को compile कर सकता है
  • यह तरीका compile time पर चल सकने वाले functions के unit tests को आसान बनाता है
    • अलग build की ज़रूरत नहीं
    • अतिरिक्त काम की ज़रूरत नहीं
    • हर बार code compile होने पर tests चल जाते हैं
  • ImportC test suite में भी इस तरीके का व्यापक उपयोग होता है

forward references से होने वाली declaration की पुनरावृत्ति

  • standard C में compiler lexical order में पहले आई declarations ही जानता है, इसलिए forward references की अनुमति नहीं होती
  • उदाहरण code में अगर floo() बाद में define हुए dex() को पहले call करे, तो gcc dex की implicit declaration और actual definition के type conflict की error देता है
  • floo और dex का क्रम बदलने पर वही code सामान्य रूप से compile हो जाता है
  • ImportC global declarations किसी भी क्रम में आएँ, उन्हें स्वीकार कर सकता है
  • जब forward references नहीं होते, तो हर forward definition के लिए अलग declaration जोड़नी पड़ती है
    • पहले char dex(char *s, int i); जैसी declaration लिखनी होती है
    • फिर बाद में function definition दोबारा लिखनी पड़ती है
  • यह तरीका अनावश्यक दोहराव पैदा करता है, और programmer को leaf functions ऊपर तथा global interface functions नीचे रखने जैसी उल्टी व्यवस्था करने पर मजबूर करता है
  • इसकी आलोचना करते हुए कहा गया है कि यह अखबार को नीचे से ऊपर पढ़ने जैसा है, जो समझ में नहीं आता

header files के बिना declarations import करना

  • C में external module इस्तेमाल करने के लिए आम तौर पर .h file में declarations बनाई जाती हैं, और .c file में उन्हें include किया जाता है
  • उदाहरण संरचना इस तरह है
    • floo.c में dex.h include किया जाता है और dex() call होता है
    • dex.h में char dex(char *s, int i); declare किया जाता है
    • dex.c में dex.h include किया जाता है और dex() define किया जाता है
  • हर external module के लिए .h file बनाना दोहराव वाला काम बढ़ाता है
  • अगर .h file, .c file से ठीक-ठीक मेल न खाए, तो क्या गलत है यह ढूँढने में बहुत समय लग सकता है
  • प्रस्तावित तरीका dex.c को सीधे import करने का है
    • floo.c में __import dex; इस्तेमाल किया जाता है
    • dex.c में सिर्फ char dexx(char *s, int i) { return s[i]; } जैसी definition रखी जाती है
  • इस तरीके में .h file लिखने की बिल्कुल ज़रूरत नहीं रहती
  • ImportC इस तरीके को भी support करता है

संबंधित दस्तावेज़

  • ImportC: ImportC दस्तावेज़
  • D Language: D भाषा दस्तावेज़

1 टिप्पणियां

 
GN⁺ 2025-01-13
Hacker News की राय
  • C के अलावा कोई भाषा इस्तेमाल करते समय जिन चीज़ों की सबसे ज़्यादा कमी महसूस होती है, उनमें से एक header files हैं
    public/private, interface/implementation का बहुत साफ़ अलगाव C code में मुझे खास तौर पर पसंद है। किसी library की सिर्फ़ .h file सरसरी तौर पर देखकर भी उसका इस्तेमाल समझ आ जाना अच्छा लगता है, और आम तौर पर .h में usage docs होते हैं, .c के साथ duplication नहीं होता। documentation को .c में भी रखा जा सकता है, लेकिन इस्तेमाल करने वाले के नज़रिए से interface पढ़ना कहीं कम सुखद हो जाता है

    • यह दावा मुझे हमेशा अजीब लगा है, क्योंकि दूसरी भाषाएँ इस समस्या को tools से हल करती हैं और मुझे वह बेहतर लगता है
      उदाहरण के लिए Rust में library interface और usage देखने के लिए cargo doc --open काफ़ी है। पूरा public API searchable रूप में अपने-आप generate हो जाता है, और header व source के बीच code duplicate या manually maintain करने की ज़रूरत नहीं रहती
    • मेरे हिसाब से header files 70 के दशक के resource-constrained platforms से निपटने के लिए एक कमज़ोर hack जैसी हैं
      वे सिर्फ़ conventions मानने पर ही सही से काम करती हैं, और Ada जैसी भाषा से तुलना करें, जहाँ interface और implementation specs अच्छे से design किए गए हैं और बार-बार parsing की ज़रूरत नहीं पड़ती, तो ये फीकी लगती हैं। C इस्तेमाल करना मुझे पसंद है, लेकिन यह हिस्सा बेहतर design होना चाहिए था
    • सहमत भी हूँ और असहमत भी
      C की header files text को जस का तस चिपकाने वाली व्यवस्था हैं, इसलिए यह “लगभग काम करता हुआ दिखता है” वाली चीज़ है। दूसरी ओर Ada में packages और package bodies की अवधारणा थी; package header file के बराबर और package body implementation के बराबर था। बहुत पहले जब मैंने Ada इस्तेमाल किया था, तब package body implementation तैयार न होने पर भी सब लोग package के आधार पर compile कर सकते थे, इसलिए implementation से पहले भी interface को align किया जा सकता था। एक और दिशा में, Python का import file system पर natural रूप से map होता है और C की include semantics से नहीं जूझना पड़ता, इसलिए “header file” की भूमिका में मुझे अच्छा लगता है
    • लगता है इसके पीछे सोचने के तरीके का अंतर है
      header file होने पर interface को implementation से अलग चीज़ के रूप में सोचने की आदत बनती है। इसलिए इस तरीके के आदी लोगों को यह विचार असहज लगता है कि interface किसी tool से generate हो। interface implementation का byproduct नहीं, बल्कि अलग से सोच-समझकर design की जाने वाली चीज़ है, और कुछ लोगों के लिए यह implementation से भी अधिक महत्वपूर्ण है। इसके उलट, auto-generated docs के आदी लोगों को यह असहज लगता है कि interface, implementation source नामक single source of truth से generate न हो। जिन भाषाओं में अलग interface file होती है और जिनमें नहीं होती, दोनों को लंबे समय तक इस्तेमाल करने पर लोग अपने-अपने camps में जम जाते हैं और उलटे तरीके से सोचने की संवेदना भूल जाते हैं
    • ज़्यादातर compiled module languages में यह पहले से संभव है
      Modula-2, Modula-3, Ada, Standard ML, Caml Light, OCaml, F#, D की तरह अलग रख सकते हैं, या Object Pascal, D, Haskell, Java, C#, F#, Swift, Go, Rust की तरह text या graphical tools से generate कर सकते हैं। इन सभी में stronger types, faster compilation और proper namespaces हैं। Rust और Swift toolchains में अभी सुधार की गुंजाइश है, लेकिन Bell Labs के बाहर जो हो रहा था उसकी तुलना में C tools हमेशा primitive रहे। अगर AT&T इसे commercial रूप से exploit कर पाता तो इतिहास अलग होता, लेकिन नतीजा यह हुआ कि पके हुए संतरे के बजाय हमें मुफ़्त नींबू मिला। फिर भी C के लिए TypeScript जैसा कुछ बनाया गया, और आजकल boundary-checking collection types व proper modules भी support होते हैं
  • इस क्षेत्र में लेखक के पास कहीं ज़्यादा ज्ञान और अनुभव है, इसलिए उत्सुकता है कि वे निम्न समस्याएँ कैसे हल करेंगे
    constant expression evaluation अगर सिर्फ़ translation unit के भीतर किया जाए तो सरल हो जाता है, लेकिन code repetition के बिना किए जा सकने वाले काम बहुत सीमित हो जाते हैं। compile-time unit tests test को macro के रूप में व्यक्त कर सकें तो कुछ हद तक संभव हैं, और पिछला item जुड़ जाए तो आसान हो जाते हैं। declarations के forward references पर कड़ा विरोध हो सकता है, क्योंकि compiler 1-pass से 2-pass में बदलता है और performance impact आता है। विशाल codebases compile करने और translation unit parallelization तक करने वालों के लिए इसे स्वीकारना मुश्किल हो सकता है। declarations import करना compatibility तोड़ने वाला बदलाव है। C में variable define करने के बाद .c file import करके, variable बदलकर उसी .c को फिर import करने जैसे तरीके से template जैसी चीज़ implement की है। SQLite C Amalgamation के आगे कई definitions रखकर internal functions expose करने के लिए functions जोड़े हैं; ऐसे सारे use cases टूटते लगते हैं। जानना चाहता हूँ कि इन समस्याओं का कोई समाधान है या नहीं

    • यह बात सही है कि evaluate होने वाली function का source code compiler को दिखना चाहिए
      C में यह #include से किया जा सकता है, और D में जिस module में ज़रूरी code है उसे import करके किया जाता है। tests को macro के रूप में व्यक्त करने का तरीका तब सही नहीं है जब आप functions को test करना चाहते हों। example समझाने के लिए सरल रखा गया था, असल इस्तेमाल कहीं अधिक जटिल हो सकता है। performance के लिहाज़ से D, C compiler से तेज़ compile होता है; मुख्य वजह यह है कि C preprocessor कई passes मांगने वाला fossil-जैसा ढांचा है, और D #include के बजाय import का उपयोग करता है, इसलिए .h files को बार-बार compile नहीं करता। D की strategy parsing और semantic analysis को अलग करना है; यह थोड़ा धीमा हो सकता है, लेकिन duplicate declarations को फिर compile करके एक में fold करने की cost नहीं होती। compile-time function execution ज़्यादा इस्तेमाल करने पर bottleneck बन सकता है, पर हल्के इस्तेमाल में performance ठीक रहती है। अगर आप C में templates को hack से implement कर रहे हैं, तो आप पहले ही भाषा की सीमा पार कर चुके हैं और आपको अधिक powerful language चाहिए। D में top-tier metaprogramming है, और दूसरी template languages अक्सर D के रास्ते पर चलती दिखती हैं
    • निजी तौर पर forward references मुझे पसंद नहीं हैं, क्योंकि वे code पढ़ना मुश्किल बना देते हैं
      अब यह भरोसा नहीं रह जाता कि dependency graph topological sorted order में रखा गया है
    • दूसरी भाषाएँ header files या forward declarations के बिना भी ठीक चलती दिखती हैं, तो विरोध क्यों है यह समझना मुश्किल है
      यह भी उत्सुकता है कि modern C compilers सच में अभी भी single-pass हैं या नहीं
  • लेख में constant expression evaluation का उदाहरण काफी सरल है, लेकिन ज्यादा जटिल मामलों में compiler की speed और memory usage काफी खराब हो सकती है और इसका इस्तेमाल करने के लिए शायद virtual machine की जरूरत पड़ेगी
    इसलिए यह समझ में आता है कि इसे standard में डालने के लिए “बहुत जटिल” माना गया होगा। C++20 में define किए गए अजीब मिश्रण की बजाय अच्छा होता अगर C++ या C declaration import की दिशा में जाते। जैसे #import "string.c" as str की तरह किसी module को किसी symbol के रूप में import करना, और file के सभी non-static symbols को str.trim(" Hello World "); की तरह access करने देना। अलग से, __import dex; की तरह file path explicit न होना मुझे पसंद नहीं है। इस मामले में पता नहीं चलता कि dex.d import हो रहा है या dex.c

    • दूसरी लोकप्रिय languages भी यह कर सकती हैं, और D में compile-time function execution बहुत लोकप्रिय और उपयोगी feature है
      ज्यादा इस्तेमाल करेंगे तो जाहिर है compile time और memory खर्च होगी। virtual machine की बात करें तो constant folding खुद पहले से ही एक virtual machine है, और इसमें function call handling को extend किया जा रहा है। C की semantics सरल है, इसलिए यह उतना खराब नहीं है। आपके सुझाए import तरीके लगभग वही हैं जो D import करता है: https://dlang.org/spec/module.html#import-declaration. dex.d है या dex.c, यह समस्या सच में आती है, और इसका जवाब import path setting है। C compiler के include path जैसा
    • ज्यादातर वास्तविक C compilers optimization के लिए पहले से ही कुछ हद तक compile-time evaluation कर रहे होंगे
      C में पहले से ही constant expressions हैं। बड़ी बाधा यह है कि compiler को function के source code तक access चाहिए, इसलिए शायद इसे उसी translation unit के functions तक सीमित रखना पड़ेगा। और कई compiler representatives वाली committee को ऐसी constant evaluation की semantics पर सहमत होना पड़ेगा—यह लोगों से जुड़ी समस्या शायद कहीं ज्यादा बड़ी हो सकती है
    • यह असल में C++ के constexpr feature से लगभग 1:1 जैसा दिखता है
      लगभग सभी C compilers पहले से C++ compilers भी हैं, इसलिए C में constexpr functions और evaluation support करना इतना बुरा हो सकता है या नहीं, इस पर संदेह है
  • C code के लिए unit tests हमेशा लिखता हूं
    अच्छा build system इस्तेमाल करें और थोड़ा boilerplate स्वीकार करें तो यह मुश्किल नहीं है। npy library के tests में test_load_uint8() में npy_load("tests/npy/uint8.npy") call किया जाता है, dimensions, size, type को assert किया जाता है और फिर npy_free होता है, और main में PRINT_RUN(test_load_uint8); की तरह run किया जाता है। preprocessor से tests का कुछ हिस्सा generate भी किया जा सकता है, लेकिन मैं इसे simple रखना पसंद करता हूं

    • वह function I/O करता हुआ दिखता है, इसलिए compile-time test के रूप में काम नहीं करेगा
      ImportC compiler के unit test examples _Static_assert(sizeof(struct S22079){1,2,3} == sizeof(int)*3, "ok"); और _Static_assert(sizeof(struct S22079){1,2,3}.a == sizeof(int), "ok"); जैसे हैं। semantics को compile time पर check किया जाता है, इसलिए link और run करने की जरूरत नहीं होती। tests जितने ज्यादा हों, यह तरीका उतना तेज हो जाता है, और test suite जितनी जल्दी चलती है, productivity उतनी बढ़ती है
    • मैं भी लगभग इसी तरीके से करता हूं
      एक समय fancy unit test frameworks को गहराई से देखा था, लेकिन एहसास हुआ कि खास फायदा नहीं है, और लगभग इसी रूप पर settle हो गया। एक बात जो दूसरों को खराब लग सकती है, वह यह है कि static functions call करने के लिए मैं test किए जा रहे .c file को खुशी से #include करता हूं। सिर्फ एक .c file include करता हूं। साथ ही #include से पहले छोटा preprocessor handling करता हूं, ताकि कोई “release mode” में build करके assert को disable न कर दे, इसलिए NDEBUG defined न रहे
    • tests को जितना संभव हो आसानी से run करने लायक बनाना सचमुच tests वाली codebase तक पहुंचने में बहुत मदद करता है
      मैं मिलता-जुलता कुछ इस्तेमाल कर रहा हूं: https://github.com/ensisoft/detonator/blob/master/base/test_.... boost.test.minimal से काफी कुछ लिया है और मूल रूप से यह single header था, लेकिन समय के साथ एक single translation unit जोड़नी पड़ी। मेरा निष्कर्ष है कि अगर codebase को ऐसी स्थिति में रखा जाए जहां tests हमेशा pass होते रहें, तो testing tool में error reporting या fault tolerance जैसी complexity की जरूरत काफी कम रह जाती है
  • compile-time unit tests उतने ही खराब विचार हैं जितना “unused imports/variables/results” को warning नहीं बल्कि error बनाना
    यह developer से control छीनता है, और आखिरकार काम पूरा करने के लिए bureaucratic process से गुजरने पर मजबूर करने वाला “nanny feature” है। ऐसे build-failing tests “लगता है अब पूरा हो गया” वाली build के लिए अच्छे हैं, लेकिन 99% builds, यानी “अभी काम चल रहा है” वाली builds के लिए ठीक नहीं हैं। यह ऐसा है जैसे कहना, “जब तक drill नहीं हटाओगे, table saw इस्तेमाल नहीं कर सकते”

    • इस हिस्से से सहमत होना मुश्किल है
      अगर आपने कोई विचार व्यक्त करने की कोशिश की और compile-time test बताता है कि वह गलत है, तो शायद वह विचार अभी अधूरा है या आपने उस expression के सभी परिणामों पर पर्याप्त सोच-विचार नहीं किया है। यह Haskell की type checking जैसा है। जो program type check pass नहीं करता, वह compile नहीं हो सकता, और programmer को हमेशा केवल पूर्ण विचार ही व्यक्त करने के लिए मजबूर करता है। सिद्धांत रूप में इससे अधिक सोच-समझकर बने programs मिल सकते हैं। हालांकि इससे लिखना कठिन हो जाता है, क्योंकि programmer को उन कोनों तक भी खोजने के लिए मजबूर किया जाता है जिनके बारे में वह “जानता है कि invalid हैं लेकिन परवाह नहीं करता”
    • ऐसे compile-time tests static_assert के ज्यादा करीब हो सकते हैं, और library function के incompatible uses पकड़ने में बहुत मूल्यवान हैं
      मुझे यह काफी अच्छा idea लगता है
  • “leaf फ़ंक्शन पहले आते हैं और global interface फ़ंक्शन आख़िर में आते हैं” — मेरे लिए यह उल्टा है
    कई वजहों से मैं कोड को topological sort order में लिखना पसंद करता हूँ। यह फ़ंक्शन के अंदर कोड लिखने के तरीके जैसा लगता है, यह साफ़ करता है कि किसी module में फ़ंक्शन्स कहाँ रखे जाने चाहिए, और सबसे ज़रूरी बात, module के अंदर code snippets के बीच circular dependencies बहुत स्पष्ट हो जाती हैं। circular dependencies codebase को और उलझा देती हैं और module को स्वतंत्र unit के रूप में समझना मुश्किल बनाती हैं, इसलिए मुझे वे ख़ास पसंद नहीं हैं। Python में इससे ऐसी समस्याएँ भी बन सकती हैं जो execution से पहले दिखाई नहीं देतीं[0], और circular import इतने आम हैं कि लगता है मौजूदा type checkers उनकी diagnostics को default रूप से बंद रखते हैं[1]। C, OCaml, SML जैसी भाषाएँ, जो forward references को support नहीं करतीं, circular dependencies पर “principle of least surprise” लागू करने देती हैं। OCaml में अगर let rec fn1 = .. and fn2 = .. की तरह declare न किया जाए तो functions के बीच recursive dependency भी प्रतिबंधित है; लिखते समय यह थोड़ा परेशान कर सकता है, लेकिन पढ़ते समय यह महत्वपूर्ण जानकारी बन जाती है
    [0]: https://gist.github.com/Mark24Code/2073470277437f2241033c200...
    [1]: https://microsoft.github.io/pyright/#/configuration?id=type-... (reportImportCycles देखें)

  • एक व्याख्या यह है कि compiler केवल lexically पहले आई चीज़ों को जानता है, इसलिए leaf फ़ंक्शन पहले और global interface फ़ंक्शन आख़िर में आने वाला “bottom-up” order बनता है; लेकिन Python जैसी forward references की अनुमति देने वाली भाषाओं में भी यह order आम है[0]
    जिज्ञासा है कि यह forward references की अनुमति न देने वाली भाषाओं की विरासत है, या फिर किसी ख़ास तरह के code के लिए यह सच में ज़्यादा समझ में आने वाला तरीका है
    [0] https://stackoverflow.com/a/73131538

    • Python में forward references नहीं होते
      बस फ़ंक्शन body के अंदर identifiers को तब तक resolve नहीं किया जाता जब तक फ़ंक्शन खुद execute नहीं होता, और उस समय तक module scope की सारी चीज़ें defined हो चुकी होती हैं। फ़ंक्शन body में कोई भी name डालकर module load करके इसे खुद देख सकते हैं
    • मेरे code में public interface हमेशा ऊपर आता है, और implementation details फ़ाइल के अंत की ओर चली जाती हैं
      कोई सख़्त rule भी नहीं है, लेकिन मुझे ऐसा पढ़ना ज़्यादा साफ़ और समझदारी भरा लगता है। खासकर जब implementation बड़ी हो, तो मैं यह नहीं चाहता कि पहले उसे पूरा scroll करने के बाद ही मूल रूप से यह क्या करता है, वह दिखे। उत्सुक हूँ कि दूसरे लोग कैसे करते हैं
  • C से जिन “स्पष्ट चीज़ों” की उम्मीद है, उनमें pointer और length रखने वाले slice type का support, global state इस्तेमाल करने वाले APIs के reentrant और संभव हो तो thread-safe versions, Go या Zig के defer या GCC के cleanup attribute जैसी किसी चीज़ का standardization, और Unicode व UTF-8 के लिए portable support शामिल हैं

    • इनमें से ज़्यादातर भाषा खुद का काम नहीं, बल्कि standard library में चाही जाने वाली सुविधाएँ नहीं हैं क्या?
  • C syntax में जहाँ-जहाँ constant expression आती है, वहाँ compiler को compile time पर फ़ंक्शन execute कर पाने की बात—जब तक फ़ंक्शन I/O, mutable global variable access, system calls आदि न करे—आसानी से टूट सकती है
    बस कोई बहुत लंबे समय तक चलने वाला pure function चुन लें। उदाहरण के लिए, अगर int busybeaver(int n) {...} n-state busy beaver machine की maximum lifetime लौटाने वाला pure function हो, तो int x = busybeaver(99); जैसा code समस्या बन जाता है

  • C23 में constexpr है, लेकिन अभी इसे functions पर नहीं लगाया जा सकता
    हालांकि एक proposal है: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2976.pdf