अभी-अभी आपको मिली legacy C++ codebase, अब क्या करें?
(gaultier.github.io)- अगर आपने एक बड़ी और जटिल legacy C++ codebase संभाली है, तो लक्ष्य “clean code” नहीं, बल्कि security, developer experience, correctness और performance को स्वीकार्य स्तर तक लाना है
- शुरुआत संगठन की सहमति और समय-सीमा हासिल करने, supported platforms को README में लिखने, और local build व tests को स्थिर रूप से पास कराने से होती है
- build/test speed में सुधार dependency tests हटाने, अनावश्यक targets साफ करने,
moldजैसे fast linker आज़माने जैसे low-difficulty improvements से मापते हुए करना चाहिए - unused code और unsupported platform code कम करने के बाद
clang-tidy,cppcheck,clang-format,-fsanitize=address,undefined, CI के साथ automatic verification loop बनाना चाहिए - rewrite या modern C++ standard अपनाना लक्ष्य नहीं, साधन हैं; memory-safe language में rewrite भी तभी विचार योग्य है जब उसके लिए मजबूत कारण हों
शुरुआत में code से ज़्यादा लोगों और प्रक्रिया को व्यवस्थित करें
- legacy C++ codebase सुधारना ऑफिस के बाद अकेले धक्का देने वाला काम या लंबा “death march” नहीं, बल्कि sustainable software engineering work होना चाहिए
- managers, colleagues और non-technical stakeholders को समस्या, समाधान और समय-सीमा आसान भाषा में समझानी चाहिए
- अगर नए कर्मचारी को local build और पहला contribution करने में 3 हफ्ते लगते हैं, तो इसे कुछ मिनटों तक घटाने का लक्ष्य प्रस्तावित किया जा सकता है
- अगर simple fuzzing setup कुछ सेकंड में app को 253 बार crash कर देता है, तो यह production environment में risk समझाने का आधार बनता है
- अगर deployment 8 साल से unsupported FreeBSD 9 build server पर निर्भर है, तो server failure होने पर deployment रुकने की स्थिति है
- अगर industry-standard linter undefined behavior को तुरंत पकड़ लेता है जो production bug की वजह बना, तो हर change पर उसे चलाने की पर्याप्त वजह है
- अगर manually copy और modify की गई encryption library के कारण vulnerability impact पता नहीं चल सकता, तो dependencies साफ करना और automatic alerts ज़रूरी हैं
- किन approaches से बचना है, यह भी स्पष्ट है
- tests नहीं हैं, फिर भी 2 हफ्तों तक latest C++ standard में full upgrade करना
- महीनों तक अलग branch में बड़े changes बनाना और उम्मीद करना कि वे कभी merge हो जाएंगे
- यह मानकर शुरुआत से पूरी rewriting शुरू करना कि कुछ हफ्तों में खत्म हो जाएगी
- “codebase improvement” शुरू करना, बिना यह जाने कि क्या और कब तक करना है
सुरक्षित सुधार का तरीका
- हर change छोटा और incremental होना चाहिए, और change से पहले व बाद app चलना चाहिए तथा tests और linter पास होने चाहिए
- urgent bug fixes पहले की तरह संभव होने चाहिए; improvement work की वजह से वे रुकने नहीं चाहिए
- हर change measurable improvement होना चाहिए, और non-experts को भी समझाया या दिखाया जा सके
- अगर priority या budget की वजह से पूरा काम रुक भी जाए, तो शुरुआत से पहले की तुलना में measurable net benefit बचना चाहिए
supported platforms को README में स्पष्ट करें
- README में officially supported
<architecture>-<operating-system>pairs लिखने चाहिए- उदाहरण:
x86_64-linux,aarch64-darwin
- उदाहरण:
- यह list सभी supported platforms पर build होने की जांच का आधार है, और बाद में unsupported platforms के code हटाने का आधार बनती है
- ज़रूरत हो तो ARMv6, ARMv7 जैसे architecture versions भी स्पष्ट किए जा सकते हैं
- supported platforms की list इन सवालों का जवाब देने में मदद करती है
- क्या floating point, SIMD, SHA256 hardware support पर निर्भर किया जा सकता है
- क्या 32-bit support ज़रूरी है
- क्या यह big-endian platforms पर चलता है
- क्या
charके 7-bit होने की संभावना ध्यान में रखनी चाहिए
- developer workstations भी इस list में अनिवार्य रूप से शामिल होने चाहिए
local build और tests को पहले stabilize करें
- सफल product की core C++ codebase भी कई बार स्थिर रूप से compile नहीं होती; लक्ष्य “कभी-कभार होने वाला build” नहीं, बल्कि सभी supported platforms पर consistent build है
- सबसे अच्छी स्थिति यह है कि developer machine पर सीधे build और run किया जा सके
- project इतना बड़ा है कि RAM कम पड़ती है, तो build के लिए बड़ा server किराए पर लिया जा सकता है
io_uringजैसी platform-specific API चाहिए, तो shim implement किया जा सकता है या workstation की virtual machine में build किया जा सकता है- फिर भी direct local build सबसे अच्छा विकल्प है
- अगर tests नहीं हैं, तो code change से पहले tests लिखने चाहिए
- real-world में चल रहे program के input और output को capture कर end-to-end tests बनाना सबसे आसान शुरुआत है
- ऐसे tests मौजूदा behavior के सही होने की guarantee नहीं देते, लेकिन changes के समय regressions घटाते हैं
- अगर test suite है लेकिन कुछ tests fail हो रहे हैं, तो पहले उन्हें disable करें, और भले ही पूरे tests में कई घंटे लगें, उन्हें पास होने की स्थिति तक लाएं
build और test करने का तरीका document करें
- README में app को build और test करने का तरीका लिखना चाहिए
- आदर्श रूप एक build command और एक test command है
- शुरुआत में procedure जटिल हो तो commands को
build.sh,test.shमें wrap करके complexity छिपाई जा सकती है - लक्ष्य यह है कि C++ expert न होने वाला व्यक्ति भी बिना सवाल पूछे build और tests चला सके
- project structure या architecture documentation बाद में, अनावश्यक code हटाने के बाद करना बेहतर है
कम कठिनाई वाले तरीकों से build और test time घटाएं
- build system को पूरी तरह बदलने या heroic optimization करने के बजाय, पहले low-difficulty improvements मापने चाहिए
- पहले जांचने योग्य बातें
- क्या dependencies के tests हर बार build और run किए जा रहे हैं
unittest++को CMake subproject के रूप में इस्तेमाल करने पर test framework के tests तक हर बार build/run होने का मामला रहा है
- क्या dependencies के example programs build और run किए जा रहे हैं
mbedtlsमें भी CMake variable से example build बंद किया जा सकता था
- जब आपका project किसी दूसरे project का subproject बनकर शामिल होता है, तो क्या tests और examples default build होते हैं
MYPROJECT_TESTजैसे build variables को default में disabled रखना और केवल direct development के समय enable करना recommended है
- क्या third-party dependency पूरी build हो रही है, जबकि असल में उसका छोटा हिस्सा ही इस्तेमाल होता है
mbedtlsगैर-ज़रूरी हिस्सों को बंद करने के लिए कई compile-time flags देता है
- क्या target dependencies गलत हैं, जिससे छोटे change पर भी सबकुछ फिर build होता है
- कई build systems dependency graph output कर सकते हैं
moldजैसे fast linker को आज़माएं- संभव हो तो compilers की तुलना भी करें
- कुछ projects में clang, gcc से 2 गुना तेज रहा, और कुछ projects में कोई फर्क नहीं था
- क्या dependencies के tests हर बार build और run किए जा रहे हैं
- अतिरिक्त तौर पर जिन चीज़ों को experiment किया जा सकता है, लेकिन benefit छोटा या negative हो सकता है
- LTO off/on/thin
- debug info अलग करना
- Make और Ninja की तुलना
- file system type और settings
- अगर build बहुत लंबा चलता है, तो code modify करना ही practical नहीं रहता
अनावश्यक code हटाएं
- ऐसे मामले रहे हैं जहां codebase का 30% से अधिक हिस्सा पूरी तरह dead code था, और ऐसा code compile व refactoring cost लगातार बढ़ाता है
- हटाने के तरीके
- compiler की
-Wunused-xxxwarnings इस्तेमाल करें- उदाहरण:
-Wunused-function - अधिकतर मामलों में delete, rebuild और test से काम हो जाता है, लेकिन कभी-कभी यह गलत function call हुए bug का symptom हो सकता है, इसलिए पूरी automation में सावधानी चाहिए
- उदाहरण:
cppcheckजैसे linter से unused functions या class fields खोजें- inheritance और virtual functions में false positives अधिक हो सकते हैं, लेकिन compiler से छूटे unused elements मिल सकते हैं
- linker से हर function को अलग section में रखवाकर unused sections removal output कराने का तरीका भी है, लेकिन standard library function noise बहुत होने से यह practical नहीं हो सकता
- generated assembly और source code की तुलना करने का तरीका virtual functions के लिए ठीक नहीं बैठता
- compiler की
- supported platforms list का उपयोग करके unsupported platform code हटाएं
- ऐसे project में पुराना Solaris support code जो वास्तव में केवल FreeBSD पर चलता था
- code में अपना random number generator होना, जबकि real platforms पर हमेशा random generator मौजूद है
- code जो POSIX 2001 unsupported cases के लिए था, जबकि वह केवल modern Linux और macOS पर चलता है
- big-endian CPU है या नहीं जांचकर byte swap करने वाला code
- कई साल पहले डाला गया लेकिन कभी real feature तक न पहुंचा code
- code deletion की बड़ी PR build time reduction और maintenance cost reduction दोनों दिखा सकती है
linter, formatter और sanitizer लगाएं
- linter में बहुत ज्यादा rules enable न करें; basic कुछ rules से development lifecycle में शामिल करना चाहिए
clang-tidy,cppcheckउपयोगी हो सकते हैं, लेकिन slow और noisy भी हो सकते हैं- linter न होना विकल्प नहीं है; पहली बार चलाने पर compiler warnings से न पकड़ी गई कई real problems मिल सकती हैं
- code formatting को उचित समय पर एक बार में apply करना चाहिए
- active branches न हों, इसका इंतज़ार करना चाहिए ताकि भयानक merge conflicts से बचा जा सके
- style debates में समय न लगाकर
clang-formatजैसे tool से पूरी codebase को बिना exception format करें - settings भी साथ commit करें
- sanitizer मुश्किल से मिलने वाले ऐसे bugs पकड़ने के लिए ज़रूरी है जो production को प्रभावित कर सकते हैं
- default के रूप में
-fsanitize=address,undefinedrecommend है - आम तौर पर false positives नहीं होते, इसलिए detect होने पर fix करना चाहिए
- tests भी sanitizer enabled रखकर चलाएं ताकि problems मिलें
- performance budget अनुमति दे तो कुछ sanitizers enabled रखकर production run पर भी विचार किया जा सकता है
- default के रूप में
- deployment compiler sanitizer support न भी करे, तो development और testing में clang जैसे compiler का उपयोग किया जा सकता है
- sanitizer enable करने पर लंबे समय से छिपे bugs और memory leaks सामने आ सकते हैं, और fix करने में काफी work व refactoring लग सकती है
- संभव हो तो testing के समय third-party dependencies को भी sanitizer enabled रखकर compile करना बेहतर है, ताकि dependency के अंदर की problems भी मिलें
CI से automate करें
- CI अब तक set किए गए linter, formatting, tests आदि को clean environment में automate करता है
- हर change पर production binary generate करने में सक्षम होना चाहिए
- ज्यादातर CI systems multiple platform matrix support करते हैं, इसलिए README में supported platforms list सचमुच build होती है या नहीं, जांचा जा सकता है
- सामान्य pipeline
make all test lint fmtजितनी simple हो सकती है - linter और sanitizer द्वारा report की गई problems pipeline failure में बदलनी चाहिए; वरना कोई उन्हें ठीक नहीं करेगा
code को धीरे-धीरे सरल करें
- stable build/test/verification loop बन जाने के बाद code को थोड़ा-थोड़ा simplify किया जा सकता है
- अगर complex class वास्तव में pointer allocate करके केवल null है या नहीं जांचने की भूमिका निभा रही थी, तो ऐसे cases में उसे practically boolean से replace किया जा सकता है
- इस stage में जितना simplify करेंगे, उतने और simplification points खुलेंगे, इसलिए time limit तय करना मुश्किल है और conservative judgement चाहिए
- लक्ष्य security, correctness, performance जैसी concrete value होनी चाहिए; “clean code” जैसे subjective criteria से बचना चाहिए
- C++ standard upgrade लक्ष्य नहीं, साधन है
- उदाहरण के लिए manual iterator increment code को
for (auto x : items)loop में बदलने में मदद कर सकता है - अगर सिर्फ
std::clampचाहिए, तो उसे खुद लिखना बेहतर हो सकता है
- उदाहरण के लिए manual iterator increment code को
memory-safe language में rewrite के लिए अलग judgement चाहिए
- कुछ हिस्सों को memory-safe language में दोबारा लिखना संभव विकल्प है, लेकिन सावधानियां बहुत हैं
- केवल मजबूत कारण होने पर ही आगे बढ़ना चाहिए
dependency management में source build-केंद्रित approach को प्राथमिकता दें
- C++ में consistent dependency management नहीं है, और कई projects system package manager इस्तेमाल करते हैं
- system package dependency approach की समस्याएं
- install instructions OS, distribution और distribution version पर निर्भर होते हैं
- Ubuntu 20.04 से 22.04 पर जाने पर package versions बदल जाते हैं और 100 dependencies एक साथ upgrade करनी पड़ सकती हैं
- जो third-party dependency package के रूप में उपलब्ध नहीं है, उसे अंततः source से build करना पड़ता है
- package desired flags के साथ build नहीं होता
- sanitizers, LTO,
-march, debug info, frame pointer, C++ ABI differences आदि problem बन सकते हैं
- sanitizers, LTO,
- audit, development और debugging के समय currently used exact version का source देखना कठिन है
- dependency patch करके फिर build करना कठिन है
- macOS, Ubuntu, FreeBSD के बीच exactly same package versions इस्तेमाल करना कठिन है
- automated BOM generation कठिन हो जाता है
- static/dynamic library form जैसी ज़रूरी package variants उपलब्ध नहीं हो सकतीं
- Conan, vcpkg जैसे C++ package managers भी improvement हो सकते हैं, लेकिन constraints हैं
- external dependencies की वजह से CI complex और slow हो सकता है
- सभी package versions उपलब्ध नहीं हो सकते
- Conan का mbedtls
2.16.12से2.23.0पर jump करने का उदाहरण है
- Conan का mbedtls
- required OS या architecture support न हो सकता है
- recommended तरीका है git submodule से dependencies लाकर source से compile करना
- simple है
- manual vendoring की तुलना में git history और diff का उपयोग किया जा सकता है
- dependency version commit level पर exact पता चलता है
- single dependency upgrade
git checkoutसे संभव है - सभी platforms पर काम करता है
- हर dependency के लिए compile flags और compiler चुने जा सकते हैं
- C++ experience न होने पर भी developers git जानते हैं
- recursively काम करता है
- CMake के
add_subdirectoryयाgit submodule foreach makeसे हर submodule build किया जा सकता है - submodule कठिन हो तो Neovim की तरह एक script dependencies fetch और build करने वाला तरीका भी संभव है
- dependency graph बहुत बड़ा हो तो Buck2 जैसे hybrid local/remote build system और build artifacts reuse की ज़रूरत पड़ सकती है
- Go, Rust जैसी compiled languages के package managers source से compile करने वाली approach इस्तेमाल करते हैं
पाठकों के सुझावों से मजबूत किए गए बिंदु
- tests पर और जोर देने की ज़रूरत है, और C++ test suite को sanitizer के तहत चलाना चाहिए ताकि false sense of safety न बने
- अगर vcpkg requirements और cross compilation पूरा करता है, तो वह git submodule से बेहतर विकल्प हो सकता है
- Nix C++ dependency manager की भूमिका निभा सकता है, लेकिन complexity और slowness समस्या हैं
- अगर साल में केवल एक bug fix करते हैं, तो large-scale refactoring investment judgement का मामला है, लेकिन dead code removal और sanitizer rare fixes में भी value रखते हैं
- code removal उन cases पर केंद्रित होना चाहिए जहां static analysis से call न होना confirm हो; संदेह हो तो न हटाना सुरक्षित है
- virtual methods में call target runtime पर तय होता है, इसलिए static analysis से हटाना कठिन है
- sales, product managers और users से feature/platform usage के बारे में 15 मिनट की बातचीत technical work को काफी घटा सकती है
- LLM में code डालकर सवाल पूछने का तरीका legally safe होना चाहिए और संभव हो तो local पर चलना चाहिए; results को सावधानी से लेना चाहिए
- code analysis tools से diagrams और class relationships generate कर overall structure समझने का idea भी है
- अगर source control system नहीं है, तो step 0 code को VCS में डालना है
- कुछ राय CI को step 1 मानती हैं, और local faster होने पर भी यह valid perspective है
- बहुत कम इस्तेमाल होने वाले platforms हटाने से combinatorial complexity घटती है और बड़ी simplification संभव हो सकती है
- complete reproducible build आम C++ codebase में realistic नहीं हो सकता, लेकिन reliable build realistic है
- पूरे code formatting commit के लिए git को configured किया जा सकता है कि वह specific commit को blame में ignore करे, जिससे history tracking burden घटता है
- VCS history statistics से frequently changed areas और साथ बदलने वाली files खोजने का idea भी है
- यह approach सिर्फ C++ नहीं, दूसरे legacy codebases पर भी लागू की जा सकती है; sanitizer जैसे C++-specific हिस्सों को छोड़ दें
Working effectively with Legacy Coderelevant advice वाली recommended book है- real value पर focus करना चाहिए, लेकिन बड़ी C++ codebase को security perspective से देखें तो कई vulnerabilities मिल सकती हैं और यह financial gain नहीं बल्कि risk reduction के रूप में value है
1 टिप्पणियां
Hacker News की राय
कुछ सलाह अच्छी है, और कुछ थोड़ी विवादास्पद भी। कई विशाल C++ projects विरासत में संभालने के बाद, शुरुआत में पक्का बड़ा फायदा देने वाले काम कुछ ही लगते हैं
पहले reproducible build बनाएं, और Docker या अपनी पसंद के packaging tool से build environment को wrap करें ताकि tools और dependencies स्पष्ट और reproducible रहें
-Wallके साथ warning-free build कराना खराब code, undefined behavior और bugs को सामने लाता है, और बाद में अगर मैं कोई संदिग्ध code डालूं तो तुरंत warning दिख सकती हैशुरुआत में
valgrindजैसे tool से read/write errors जांचना भी stability के लिहाज से आसान जीत है, और पूरी structure समझने से पहले refactoring को local रखना ज्यादा सुरक्षित हैintegrity hash के बिना बदलने वाले image tags को point करने वाला या
apt-getcall करने वाला Dockerfile आम है, और दोनों ही थोड़ा अलग state में खत्म होने की गुंजाइश बहुत छोड़ते हैंसीखना कठिन है, लेकिन Bazel या Nix जैसे solutions कहीं बेहतर आधार देते हैं
stage 2 सभी tests को sanitizers के तहत चलाकर fail होने वाली items mark करना है, stage 3 उन sanitizer failures को ठीक करना है, और stage 4 बाकी काम है
containers अच्छे हैं, लेकिन problems न होने का दिखावा करने के तरीके के रूप में बेहद खराब हैं
dependencies को पूरा bundle करना चाहिए, और ऐसी स्थिति से बचना चाहिए जहां
/usr/libमें अरबों चीजें सही version में install हों तभी सब चले-Wallके साथ clean build करना ठीक है, लेकिन production build में-Wall -Werrorडालने का मैं कड़ा विरोध करता हूंcompiler warnings में से कुछ opinion जैसे होते हैं, और नया compiler version अगर नई warning जोड़ दे तो जो code पहले clean था वह अचानक reject हो सकता है
अगर
-Werrorजरूरी है, तो इसे सिर्फ debug build में इस्तेमाल करना बेहतर हैमैं 2 और 3 का क्रम बदलूंगा। CI, linting, automatic formatting वगैरह पहले setup करना, कुछ उखाड़ने से पहले आना चाहिए
क्योंकि अभी पता नहीं कि क्या हटाना चाहिए, और हटाने पर क्या नतीजा निकलेगा
linters और static analysis tools यह समझने में बहुत insight देते हैं कि program के किस हिस्से पर काम की जरूरत है
आज static analysis tool जिन जगहों को flag करता है, वे अक्सर बाद में STL concepts को reimplement करने वाले functions, classes या पूरी files हटाने की जगहें होती हैं
hand-rolled iterator library, hand-rolled smart pointers, C string functions के इस्तेमाल आदि को STL algorithms, असली smart pointers और C++ string classes से बदला जा सकता है
लेकिन code scan करने से पहले यह साफ नहीं दिखता, और fast test build या deployment की दिशा में धकेलने से पहले नतीजों का आकलन करना भी मुश्किल है
जहां कमी होती है, वहां basic smoke tests जोड़ता हूं
यह काम धीमा नहीं करता, बल्कि तेज करता है। ठीक-ठाक coverage मिल जाए तो refactoring करते समय बहुत तेजी से आगे बढ़ सकते हैं, इसलिए छोटे investment पर बड़ा return मिलता है
बड़े C++ projects में code generation scripts के अलावा, आम तौर पर code generation के लिए data इकट्ठा करने को code parse करने वाली scripts भी होती हैं
automatic formatting उन्हें तोड़ सकती है, और मैंने तो ऐसे cursed projects भी देखे हैं जहां user public header files को brittle scripts से parse करता था
बहुत संभावना है कि आप इधर-उधर कई जगह बदलाव करेंगे, और local में बहुत सारे changes जमा हो जाएं तो गलती से mixed-concerns वाला commit बन सकता है
उस समय CI रक्षक बन जाता है
अगर उस रास्ते पर जाते हैं, तो कम-से-कम branch को squash करके merge करने के लिए तैयार होने तक तुरंत delete न करें; comment out करके diff को simple रखना बेहतर है
code understanding tools और techniques गायब हैं। पहले Source Navigator नाम का Tcl/Tk में लिखा tool इस्तेमाल किया था, जो codebase indexing के लिए बहुत अच्छा था
current method की call hierarchy देख सकते थे, और उससे UML sequence diagram बना सकते थे
इसी तरह का एक tool नीचे वाला Source Insight भी है
और notes जरूरी हैं। मुख्य बात है ऐसे लिखना जैसे किसी को सिखा रहे हों
वर्षों में code समझने में काफी अच्छा हो गया, और एक समय अकेले ही 4–5 exchanges पर रोज करीब 200 million dollars के scale पर trade करने वाले Java algorithmic trading codebase को actively support और develop करता था
उस code की documentation 35MB थी, और key-person risk को छोड़ दें तो responsibility अच्छी लगी
सच कहूं तो ज्यादातर बड़े codebases में overengineering और duplication बहुत होता है
[1] References in "Source Insight" https://d4.alternativeto.net/6S4rr6_0rutCUWnpHNhVq7HMs8GTBs6...
university के computer architecture class में हमें यह अवशेष जबरन इस्तेमाल कराया गया था, क्योंकि इसमें ऐसा अजीब feature set और ARM emulator support था जो कहीं और नहीं था
हमने इसे bare-metal ARM assembly programming के लिए इस्तेमाल किया था
std::cmakeको standard library में डालना game changer होगा—इस हिस्से पर सचमुच पेट में मरोड़-सी महसूस हुईयह सलाह समझ आती है कि जो चीज़ कंपनी या open source project द्वारा advertise और sell किए जाने वाले features देने के लिए बिल्कुल ज़रूरी नहीं है, उसे chainsaw से काटकर हटा दो, लेकिन यह बेहद जोखिम भरा है
असल में यह Chesterton’s Fence वाली समस्या है; जब तक आप पूरी तरह नहीं समझते कि कोई चीज़ software में क्यों आई थी और अभी software का इस्तेमाल कैसे होता है, आप उसे हटा नहीं सकते
सबसे बुरे हाल में, लगभग एक महीने बाद release करने पर users पाएँगे कि कोई अहम feature सूक्ष्म तरीके से टूट गया है, और फिर यह trace करने में कई दिन लगेंगे कि वह ठीक-ठीक कैसे टूटा
CI, linters, fuzzing, automatic formatting वगैरह जोड़ना अच्छा विचार है, लेकिन मुश्किल भी है
अगर एक व्यक्ति मुख्यतः VIM इस्तेमाल करता है, दूसरा emacs, तीसरा QTCreator, और कोई VSCode, तो सबको एक ही पेज पर लाना बहुत कठिन हो जाता है
अगर नए tools install करना optional step है, तो असल में वह होगा ही नहीं; और अगर project खोलते ही linter 2000+ warnings दिखा दे, तो वह भी मददगार नहीं है
cpplintयाclang-tidy, fuzzers को IDE में integrated नहीं देखा, और हर keystroke पर auto-run करने के लिए वे बहुत धीमे हैंबस automatic formatting कभी-कभी integrated होती है
ये चीज़ें हर किसी के development environment से स्वतंत्र रूप से command line पर की जा सकती हैं, इसलिए सिर्फ़ इस वजह से कि दो text editors इस्तेमाल हो रहे हैं, शुरू करने से पहले ही हार मत मानिए; कम से कम कोशिश तो करनी चाहिए
C++ में अगर team कोई tool install ही नहीं करेगी, तो मुश्किलें तय हैं। tools को containerize करके आसान बनाना भी विचार करने लायक है
हमें professionals की तरह काम करना चाहिए, और अगर काम के लिए IDE से लेकर toolchain तक सेटअप करना ज़रूरी है, तो उसे सीखकर इस्तेमाल करना चाहिए
“मेरे computer, मेरे IDE, मेरी पसंद के तरीके में build होकर चलता है” — यह software से ज़्यादा कोई craft piece जैसा है
local hook install करना optional हो, तब भी PR को CI में fail करा देंगे तो लोग अंततः कर ही लेंगे
ये चीज़ें सचमुच basic हैं, लेकिन CI और infrastructure को ठीक से संभालने की knowledge gap काफ़ी बड़ी दिखती है
tests, lint, fuzzing, formatting, YAML format validation, EOF newline missing check आदि local पर run करने योग्य बनाएँ, ताकि developers push से पहले CI failure रोक सकें
अगर project खोलते समय linter हजारों warnings निकालता है, तो linter जोड़ने वाले developer को वह change merge करने से पहले warnings को 0 करना चाहिए
इसे कुछ warnings या files disable करके, कुछ fix करके, या इनका combination करके हासिल किया जा सकता है
मेरे हिसाब से पहला step पुराने maintainer से contact करना, उनसे मिलकर चाय या beer पिलाना, और अंततः codebase के बारे में बात करना है
पुराने maintainers नाम के ये जादूगर बहुत कुछ बता सकते हैं
बाकी सुझाव, जैसे कई platforms पर चलाना या tests pass कराना, robustness और understanding तक ले जाने वाले अच्छे stress tests लगते हैं
फिर भी मैं पहले उन लोगों से बात करने वाला low-hanging fruit ज़रूर तोड़ना चाहूँगा जो पहले यह रास्ता तय कर चुके हैं
पहले खुद थोड़ा काम करके देखें और कई जगह अटकने के बाद पुराने maintainer से बात करें, तो यह कहीं ज़्यादा productive होगा
वे भी उस effort की सराहना करेंगे
अगर आपको ऐसा codebase मिला है जिसमें tests नहीं हैं, build दो में से एक बार fail होता है, dependency information अस्पष्ट है, और वह सिर्फ़ बहुत पुराने OS वाले एक single server पर build होता है, तो यह पक्का नहीं कि पिछला maintainer सचमुच जादूगर था
यह भी देखना होगा कि सारी समस्याएँ समय की कमी से पैदा हुई थीं, या वह “जादूगर” job security के लिए उसे टूटा हुआ बनाए रखता था, या नई चीज़ें सीखना नहीं चाहता था
यह best case है; आम तौर पर हर सवाल का जवाब बस यही मिलता है: “जब मुझे मिला था, तब भी ऐसा ही था”
काम के दौरान कई bugs और समस्याएँ मिलीं, तो मैंने उन developers से contact करने की कोशिश की जिन्होंने employer के लिए वह custom software बनाया था
पता चला कि उसे एक ही contractor ने लिखा था, और उसकी कई साल पहले मृत्यु हो चुकी थी
defense industry में यह आम है। किसी खास system के लिए बनाए गए custom one-off items बहुत होते हैं, और खासकर hardware side में यह दुर्लभ नहीं कि equipment बनाने वाला engineer बहुत पहले जा चुका हो या retire हो चुका हो
इससे बहुत मदद मिलती है
C++ codebase में tests हों—यह तो मुझे काफ़ी optimistic लगता है
ठीक से समझ नहीं आता कि refactoring या सुधार पर इतना ज़ोर क्यों है। अगर किसी feature को code में ऊपर से जोड़कर add किया जा सकता है, तो बाकी चीज़ों को छुए बिना वैसा ही करना चाहिए
अगर बदलाव काफ़ी बड़ा है, तो legacy code से ज़रूरी हिस्सों को external function call, network layer जोड़ने, या वही code library के रूप में अलग करने जैसे तरीकों से निकाल लें, और बाकी काम नए environment में करें
जब तक आगे कई लोगों को उस code पर काम नहीं करना है और group के लिए आसानी से collaborate करने हेतु कुछ assumptions और standards की ज़रूरत नहीं है, मैं बड़ा refactoring करने की कोशिश नहीं करूंगा
ऊपर से जोड़ने वाला तरीका एक सीमा तक काम करता है, लेकिन एक समय ऐसा आता है जब बदलाव करने पर अचानक कुछ टूट जाता है और code इतना बिखरा होता है कि वजह ढूंढने में ज़रूरत से ज़्यादा समय लग जाता है
मूल पोस्ट का सार यह है कि बड़ा rewrite टालें, लेकिन cohesion बनाए रखते हुए बदलाव आसान करने के लिए साफ़-सफ़ाई और व्यवस्थित करना करें
कभी-कभी वही feature दोनों versions में जोड़ना पड़ता था
जिस code की आख़िरी release जल्द आने वाली है, उसके साथ व्यवहार उस code से बहुत अलग होता है जिसे अगले कई दशकों तक maintain करना और उसमें features जोड़ना है
असलियत में नया feature अक्सर पहले से मौजूद behavior में बदलाव होता है, और अचानक कई जगहों पर भारी refactoring करनी पड़ती है
यह अच्छी सलाहों से भरा thread है। C/C++ तक सीमित न रखते हुए जोड़ना चाहूंगा कि अगर version control system इस्तेमाल करने की गुंजाइश है, तो उसकी पूरी value निकालनी चाहिए
कई teams इसे सिर्फ़ collaboration tool की तरह इस्तेमाल करती हैं, लेकिन यह उससे ज़्यादा कर सकता है
history लेकर एक साधारण database बना लें। RDB होना ज़रूरी नहीं; JSON file या spreadsheet से भी शुरू कर सकते हैं
सिर्फ़ data-driven approach से ही लगभग तुरंत काफ़ी उपयोगी जानकारी निकाली जा सकती है
जो files और functions बार-बार बदले गए हैं, वे आगे के काम के hotspots होने की संभावना रखते हैं, इसलिए unit tests लाने या merge conflicts घटाने के लिए वहीं focus करें
जो files एक-दूसरे से दूर दिखती हैं लेकिन अक्सर साथ बदलती हैं, वे ऐसी implicit structure का संकेत दे सकती हैं जो सिर्फ़ code देखकर सामने नहीं आती
हर module का वास्तविक ownership model भी history से infer किया जा सकता है, और unclear ownership refactoring की ज़रूरत का signal हो सकता है
C/C++ में build time सुधार भी data-driven तरीके से महत्वपूर्ण modules पर focus करके करना चाहिए। यूं ही मनमाने file dependencies हटाने के बजाय, अक्सर बदलने वाले modules को split करें, और header dependencies के साथ जोड़कर असली build time impact को score भी किया जा सकता है
दूसरे developer tools और VCS को integrate करने पर और भी बहुत कुछ किया जा सकता है, और LLM के दौर में project history और metadata को model में डालकर दिलचस्प insights पूछना भी संभव लगता है
बहुत बड़े context window के बिना ऐसा करने के लिए dedicated model engineering की ज़रूरत पड़ सकती है, लेकिन intuition कहता है कि यह आज़माने लायक है
CI, linter, fuzzing, automatic formatting वगैरह जोड़ने की सलाह को और तोड़कर देखना चाहिए
CI को यह guarantee देनी चाहिए कि build सिर्फ़ मेरे computer पर नहीं, बल्कि कहीं और भी होता है, ताकि compile-based regressions रोकी जा सकें
compiler warnings और static analyzers आम तौर पर मुझसे ज़्यादा smart होते हैं, इसलिए अगर pointer से कोई अजीब काम करने पर डरावनी warning आए, तो उसे check करना strong signal है
unit tests को verify करना चाहिए कि important code low level तक ठीक वही कर रहा है जिसकी उम्मीद है, और चूंकि असल में ऐसा न होने की संभावना ज़्यादा है, इसलिए वजह समझनी चाहिए
कोई चीज़ ठीक करने पर दूसरी टूट सकती है, क्योंकि existing code किसी buggy behavior को assumption मानकर लिखा गया हो सकता है
automatic formatting priority नहीं है, और मुझे लगता है कि existing maintainers की style follow करना बेहतर है
inherited C++ codebase का अंतिम चरण memory-safe language में rewrite होना भी शायद फिट न बैठे
जो चीज़ टूटी नहीं है, उसके लिए extra work resources मिलना मुश्किल है; C++ के अलावा additional language knowledge भी चाहिए होगा, और testing ज़्यादा complicated हो सकती है
memory या performance constraints की वजह से कई languages में लिखना suitable न होने की संभावना भी बड़ी है, और सबसे पहले legacy codebase inherit करने का मतलब ही लगभग यह स्वीकार करना है कि time, money, knowledge जैसे resources की कमी के कारण नया लिखना संभव नहीं था
“X में rewrite” सिर्फ़ trend होने के कारण complexity बढ़ाता है
अगर आप codebase के बड़े हिस्से को पहले से C++ में दोबारा लिख रहे हैं, तो ज़्यादा restrictive C++ subset follow करना बेहतर है, और High Integrity C++ ठीक लगता है
अगर latest MISRA standard मिल सके तो वह भी अच्छा हो सकता है
पूरी team को नई language के sharp edges फिर से सिखाने के बजाय, जानी-पहचानी language इस्तेमाल करना और known pitfalls से बचने वाली guidelines enforce करना बेहतर है
लेखक BOM automation, package version management, dependency sources आदि की बहुत आलोचना करने के बाद git submodules को package manager से बेहतर बताता है, यह अजीब है
ऐसी आलोचना करने से पहले vcpkg इस्तेमाल करके देखना चाहिए
कुछ rough edges हैं, लेकिन लगभग सभी चीज़ें vcpkg से intuitive तरीके से पूरी हो जाती हैं
dependency updates git submodules से थोड़े मुश्किल हैं, लेकिन मुझे लगता है यह bug नहीं बल्कि feature जैसा है। dependencies अलग-अलग sandboxes में build होने के बाद specified directory में install होती हैं
vcpkg में official repository के बजाय internal repository को registry के रूप में set करके vendored-in nature बनाए रखा जा सकता है, toolchain को chainload करके fixed flags set से सब कुछ compile कराया जा सकता है, और per-port customization की भी अनुमति है
ऐसी abstractions उपयोगी होती हैं, इसलिए package managers popular हैं, और हर किसी को compile flags, macros, warnings आदि वाली अंतहीन strings खुद handle नहीं करनी पड़तीं
लेख दिलचस्प था और उससे कुछ सीखने को भी मिला। बस यह जिज्ञासा थी कि जब लोग “memory-safe language में फिर से लिखने” की बात करते हैं, तो वे किस भाषा की बात कर रहे होते हैं
क्या मतलब यह है कि कुछ हिस्सों को Go, Java, C# में फिर से लिखा जाए, या फिर यह Rust में फिर से लिखने पर तंज है, जिसमें इनकार की गुंजाइश छोड़ी गई हो
निष्कर्ष पूरी तरह टीम और constraints पर निर्भर करता है। उदाहरण के लिए, क्या garbage collection संभव है, अगर हाँ तो क्या Go अच्छा विकल्प है, क्या security सबसे पहली प्राथमिकता है—ये बातें अहम हैं
ज़्यादातर C++ developers आम तौर पर Rust को आसानी से इस्तेमाल कर सकते हैं और समान performance पा सकते हैं, ऐसा मुझे लगता है
लेकिन कुछ मामलों में शुरुआत से ही project के C++ में होने की कोई अच्छी वजह नहीं थी, और मैंने Java में सफलतापूर्वक rewrite किए गए उदाहरण भी देखे हैं
Apple अपने कुछ C++ code को Swift में फिर से लिख रहा है
जिस भाषा को team या company सहजता से संभाल सके, वह एक अच्छा rule of thumb है
अब हम इस स्तर तक पहुँच गए हैं कि लेखक पर कोई दुष्ट, छिपा हुआ Rust programmer होने का शक किया जाने लगा है। शायद इसलिए कि Rust का ज़िक्र आया ही नहीं था, तो सीधे उसी पर शिकायत नहीं की जा सकती थी