वे स्पष्ट बातें जो C भाषा को करनी चाहिए
(digitalmars.com)- 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;की तरह.cfile की declarations को सीधे import करने से अलग.hfiles की ज़रूरत घटती है, और 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-expressionevaluation- 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 करे, तो gccdexकी 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 इस्तेमाल करने के लिए आम तौर पर
.hfile में declarations बनाई जाती हैं, और.cfile में उन्हें include किया जाता है - उदाहरण संरचना इस तरह है
floo.cमेंdex.hinclude किया जाता है औरdex()call होता हैdex.hमेंchar dex(char *s, int i);declare किया जाता हैdex.cमेंdex.hinclude किया जाता है औरdex()define किया जाता है
- हर external module के लिए
.hfile बनाना दोहराव वाला काम बढ़ाता है - अगर
.hfile,.cfile से ठीक-ठीक मेल न खाए, तो क्या गलत है यह ढूँढने में बहुत समय लग सकता है - प्रस्तावित तरीका
dex.cको सीधे import करने का हैfloo.cमें__import dex;इस्तेमाल किया जाता हैdex.cमें सिर्फchar dexx(char *s, int i) { return s[i]; }जैसी definition रखी जाती है
- इस तरीके में
.hfile लिखने की बिल्कुल ज़रूरत नहीं रहती - ImportC इस तरीके को भी support करता है
संबंधित दस्तावेज़
- ImportC: ImportC दस्तावेज़
- D Language: D भाषा दस्तावेज़
1 टिप्पणियां
Hacker News की राय
C के अलावा कोई भाषा इस्तेमाल करते समय जिन चीज़ों की सबसे ज़्यादा कमी महसूस होती है, उनमें से एक header files हैं
public/private, interface/implementation का बहुत साफ़ अलगाव C code में मुझे खास तौर पर पसंद है। किसी library की सिर्फ़
.hfile सरसरी तौर पर देखकर भी उसका इस्तेमाल समझ आ जाना अच्छा लगता है, और आम तौर पर.hमें usage docs होते हैं,.cके साथ duplication नहीं होता। documentation को.cमें भी रखा जा सकता है, लेकिन इस्तेमाल करने वाले के नज़रिए से interface पढ़ना कहीं कम सुखद हो जाता हैउदाहरण के लिए Rust में library interface और usage देखने के लिए
cargo doc --openकाफ़ी है। पूरा public API searchable रूप में अपने-आप generate हो जाता है, और header व source के बीच code duplicate या manually maintain करने की ज़रूरत नहीं रहतीवे सिर्फ़ 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 का
importfile 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 में जम जाते हैं और उलटे तरीके से सोचने की संवेदना भूल जाते हैं
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 करने के बाद
.cfile import करके, variable बदलकर उसी.cको फिर import करने जैसे तरीके से template जैसी चीज़ implement की है। SQLite C Amalgamation के आगे कई definitions रखकर internal functions expose करने के लिए functions जोड़े हैं; ऐसे सारे use cases टूटते लगते हैं। जानना चाहता हूँ कि इन समस्याओं का कोई समाधान है या नहींC में यह
#includeसे किया जा सकता है, और D में जिस module में ज़रूरी code है उसे import करके किया जाता है। tests को macro के रूप में व्यक्त करने का तरीका तब सही नहीं है जब आप functions को test करना चाहते हों। example समझाने के लिए सरल रखा गया था, असल इस्तेमाल कहीं अधिक जटिल हो सकता है। performance के लिहाज़ से D, C compiler से तेज़ compile होता है; मुख्य वजह यह है कि C preprocessor कई passes मांगने वाला fossil-जैसा ढांचा है, और D#includeके बजाय import का उपयोग करता है, इसलिए.hfiles को बार-बार 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 के रास्ते पर चलती दिखती हैंअब यह भरोसा नहीं रह जाता कि dependency graph topological sorted order में रखा गया है
यह भी उत्सुकता है कि 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.dimport हो रहा है याdex.cज्यादा इस्तेमाल करेंगे तो जाहिर है 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 में पहले से ही constant expressions हैं। बड़ी बाधा यह है कि compiler को function के source code तक access चाहिए, इसलिए शायद इसे उसी translation unit के functions तक सीमित रखना पड़ेगा। और कई compiler representatives वाली committee को ऐसी constant evaluation की semantics पर सहमत होना पड़ेगा—यह लोगों से जुड़ी समस्या शायद कहीं ज्यादा बड़ी हो सकती है
constexprfeature से लगभग 1:1 जैसा दिखता हैलगभग सभी C compilers पहले से C++ compilers भी हैं, इसलिए C में
constexprfunctions और evaluation support करना इतना बुरा हो सकता है या नहीं, इस पर संदेह हैC code के लिए unit tests हमेशा लिखता हूं
अच्छा build system इस्तेमाल करें और थोड़ा boilerplate स्वीकार करें तो यह मुश्किल नहीं है।
npylibrary के 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 रखना पसंद करता हूं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 किए जा रहे
.cfile को खुशी से#includeकरता हूं। सिर्फ एक.cfile include करता हूं। साथ ही#includeसे पहले छोटा preprocessor handling करता हूं, ताकि कोई “release mode” में build करकेassertको disable न कर दे, इसलिएNDEBUGdefined न रहेमैं मिलता-जुलता कुछ इस्तेमाल कर रहा हूं: 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 हैं लेकिन परवाह नहीं करता”
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
बस फ़ंक्शन body के अंदर identifiers को तब तक resolve नहीं किया जाता जब तक फ़ंक्शन खुद execute नहीं होता, और उस समय तक module scope की सारी चीज़ें defined हो चुकी होती हैं। फ़ंक्शन body में कोई भी name डालकर module load करके इसे खुद देख सकते हैं
कोई सख़्त rule भी नहीं है, लेकिन मुझे ऐसा पढ़ना ज़्यादा साफ़ और समझदारी भरा लगता है। खासकर जब implementation बड़ी हो, तो मैं यह नहीं चाहता कि पहले उसे पूरा scroll करने के बाद ही मूल रूप से यह क्या करता है, वह दिखे। उत्सुक हूँ कि दूसरे लोग कैसे करते हैं
C से जिन “स्पष्ट चीज़ों” की उम्मीद है, उनमें pointer और length रखने वाले slice type का support, global state इस्तेमाल करने वाले APIs के reentrant और संभव हो तो thread-safe versions, Go या Zig के
deferया GCC केcleanupattribute जैसी किसी चीज़ का standardization, और Unicode व UTF-8 के लिए portable support शामिल हैं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