بالنسبة لي ، بدأ الأمر قبل ست سنوات ونصف ، عندما تم جرني ، بإرادة القدر ، إلى مشروع مغلق واحد. المشروع الذي - لا تسأل ، لن أقول. لا يسعني إلا أن أقول أن فكرته كانت بسيطة مثل أشعل النار: قم بتضمين الواجهة الأمامية للغة في IDE. حسنًا ، كما حدث مؤخرًا في QtCreator ، في CLion (بمعنى ما) ، وما إلى ذلك. والفكرة ، إذا جاز التعبير ، كانت حرفياً في الهواء (وكان الإكمال التلقائي للرمز المدمج في واجهة برمجة تطبيقات clang كما أشار إلى Be) ، كان عليك فقط أن تأخذه وتفعله. ولكن ، كما قال بورومير ، "لا يمكنك أن تأخذه ، و ...". لذلك حدث في هذه الحالة. لمزيد من التفاصيل - ويلكوم تحت القط.
أولا عن الخير
الفوائد من استخدام clang كمحلل مضمن في IDE C ++ ، بالطبع. في النهاية ، لا تقتصر وظائف IDE على تحرير الملفات فقط. هذه قاعدة بيانات من الشخصيات ومهام التنقل والتبعيات وأكثر من ذلك بكثير. وهنا يوجه مترجم كامل إلى ارتفاعه الكامل ، لأن التغلب على كل قوة المعالج المسبق والقوالب في محلل مكتوب ذاتيًا نسبيًا بسيطًا نسبيًا هو مهمة غير تافهة. لأنه عادةً ما يتعين عليك تقديم الكثير من التنازلات ، مما يؤثر بشكل واضح على جودة تحليل التعليمات البرمجية. من يهتم - يمكن أن ننظر ، على سبيل المثال ، إلى المحلل اللغوي المدمج في QtCeator هنا: محلل Qt Creator C ++
في نفس المكان ، في التعليمات البرمجية المصدر لـ QtCreator ، يمكنك أن ترى أن ما سبق ليس كل ما يتطلبه IDE من المحلل اللغوي. بالإضافة إلى ذلك ، تحتاج على الأقل إلى:
- تسليط الضوء على بناء الجملة (المعجمية والدلالية)
- جميع أنواع التلميحات "على الطاير" مع عرض المعلومات على الرمز
- تلميحات حول ما هو الخطأ في التعليمات البرمجية وكيفية إصلاحها / تكميلها
- استكمال الكود في مجموعة متنوعة من السياقات
- الأكثر إعادة هيكلة
لذلك ، على الفوائد المذكورة سابقًا (خطيرة حقًا!) ، تنتهي الإيجابيات ويبدأ الألم. لفهم هذا الألم بشكل أفضل ، يمكنك أولاً مشاهدة تقرير Anastasia Kazakova ( anastasiak2512 ) حول ما هو مطلوب بالفعل من محلل التعليمات البرمجية المدمج في IDE:
جوهر المشكلة
لكن الأمر بسيط ، على الرغم من أنه قد لا يكون واضحًا للوهلة الأولى. باختصار ، إذن: clang هو مترجم . ويشير إلى الشفرة كمترجم . وزاد من حقيقة أن الشفرة أعطيت له بالفعل مكتملة ، وليس كعب الملف المفتوح الآن في محرر IDE. لا يحب المترجمون أجزاء من الملفات ، مثل الإنشاءات غير المكتملة ، والمعرفات المكتوبة بشكل غير صحيح ، وإعادة التشغيل بدلاً من العودة ، والمسرات الأخرى التي يمكن أن تنشأ هنا والآن في المحرر. بالطبع ، قبل التجميع ، سيتم تنظيف كل هذا ، وإصلاحه ، وتوفيقه. ولكن هنا والآن ، في المحرر ، هذا ما هو عليه. وفي هذا الشكل ، يصل المحلل اللغوي المدمج في IDE إلى الطاولة كل 5-10 ثوانٍ. وإذا كانت النسخة المكتوبة ذاتيًا منها "تفهم" تمامًا أنها تتعامل مع منتج شبه منتهي ، فعندئذ لا داعي للقلق. وفوجئت كثيرا. إن ما يحدث نتيجة لمثل هذه المفاجأة يعتمد على "، كما يقولون.
لحسن الحظ ، clang متسامح إلى حد ما مع أخطاء التعليمات البرمجية. ومع ذلك ، قد تكون هناك مفاجآت - اختفاء الإضاءة الخلفية فجأة ، منحنى الإكمال التلقائي ، والتشخيصات الغريبة. يجب أن تكون مستعدًا لكل هذا. بالإضافة إلى ذلك ، الرنة ليست قاتلة. له الحق في عدم قبول أي شيء في رؤوس المترجم ، والذي يستخدم هنا والآن لبناء المشروع. الخصائص الجوهرية الصعبة ، والامتدادات غير القياسية وغيرها ، الميزات ... - كل هذا يمكن أن يؤدي إلى أخطاء التحليل في أكثر الأماكن غير المتوقعة. وبالطبع الأداء. تحرير ملف قواعد في Boost.Spirit أو العمل على مشروع قائم على llvm سيكون من دواعي سروري. ولكن ، عن كل شيء بمزيد من التفصيل.
كود الجاهزة
لنفترض أنك بدأت مشروعًا جديدًا. أنتجت بيئتك فراغًا افتراضيًا لـ main.cpp ، وفيه كتبت:
#include <iostream> int main() { foo(10) }
بصراحة ، الكود ، من وجهة نظر C ++ ، غير صالح. لا يوجد تعريف للدالة foo (...) في الملف ، لم يكتمل السطر ، إلخ. ولكن ... لقد بدأت للتو. هذا الرمز له الحق في هذا النوع. كيف ينظر هذا الرمز إلى IDE مع محلل مكتوب ذاتيًا (في هذه الحالة CLion)؟

وإذا نقرت على المصباح الكهربائي ، فيمكنك أن ترى هذا:

مثل IDE ، معرفة شيء ، أم ، المزيد حول ما يحدث ، يقدم الخيار المتوقع للغاية: إنشاء وظيفة من سياق الاستخدام. عرض رائع ، على ما أعتقد. كيف يتصرف IDE القائم على رنة (في هذه الحالة ، Qt Creator 4.7)؟

وما هو المقترح لتصحيح الوضع؟ لكن لا شيء! إعادة التسمية القياسية فقط!

سبب هذا السلوك بسيط للغاية: بالنسبة للغة ، هذا النص مكتمل (ولا يمكن أن يكون أي شيء آخر). وهو يبني AST على أساس هذا الافتراض. ثم كل شيء بسيط: يرى clang معرفًا غير معرف سابقًا. هذا نص في لغة C ++ (وليس لغة C). لا توجد افتراضات حول طبيعة المعرف - لم يتم تعريفه ، لذلك جزء من الرمز غير صالح. وفي AST لهذا الخط لا يظهر شيء. هي فقط ليست هناك. وما ليس في AST من المستحيل تحليله. إنه لأمر مخز ومزعج.
يأتي المحلل اللغوي المدمج في IDE من بعض الافتراضات الأخرى. لأنه يعلم أن الرمز لم ينته. أن المبرمج يتسرع الآن في التفكير وأن الأصابع خلفها ليس لديها وقت. لذلك ، لا يمكن تحديد جميع المعرفات. هذه التعليمات البرمجية ، بالطبع ، غير صحيحة من وجهة نظر المعايير العالية لجودة المترجم ، لكن المحلل اللغوي يعرف ما يمكن القيام به مع مثل هذه التعليمات البرمجية ويقدم خيارات. خيارات معقولة للغاية.
على الأقل حتى الإصدار 3.7 (ضمناً) ، حدثت مشاكل مماثلة في هذا الرمز:
#include <iostream> class Temp { public: int i; }; template<typename T> class Foo { public: int Bar(Temp tmp) { Tpl(tmp); } private: template<typename U> void Tpl(U val) { Foo<U> tmp(val); tmp. } int member; }; int main() { return 0; }
داخل أساليب فئة القالب ، لم يعمل الإكمال التلقائي المستند إلى clang. بقدر ما تمكنت من معرفة ذلك ، كان السبب في تحليل القوالب مرتين. يتم تشغيل الإكمال التلقائي في clang في المسار الأول ، عندما لا تكون المعلومات حول الأنواع المستخدمة بالفعل كافية. في clang 5.0 (بناءً على ملاحظات الإصدار) ، تم إصلاح هذا.
بطريقة أو بأخرى ، المواقف التي لا يتمكن فيها المترجم من بناء AST الصحيح (أو استخلاص الاستنتاجات الصحيحة من السياق) في الشفرة المحررة قد تكون كذلك. وفي هذه الحالة ، لن يرى IDE ببساطة الأجزاء المقابلة من النص ولن يتمكن من مساعدة المبرمج بأي شكل من الأشكال. وهو بالطبع ليس عظيماً. القدرة على العمل بفاعلية مع التعليمات البرمجية غير الصحيحة هو ما يحتاجه المحلل اللغوي في IDE ، وما لا يحتاجه المحول البرمجي العادي على الإطلاق. لذلك ، يمكن أن يستخدم المحلل اللغوي في IDE العديد من الاستدلال ، والتي يمكن أن تكون عديمة الفائدة بالنسبة للمترجم فحسب ، ولكنها ضارة أيضًا. ولتنفيذ وضعي تشغيل فيه - حسنًا ، ما زلت بحاجة إلى إقناع المطورين.
"هذا الدور مسيء!"
عادةً ما يكون IDE للمبرمج واحدًا (حسنًا ، اثنان) ، ولكن هناك الكثير من المشاريع وسلاسل الأدوات. وبالطبع ، لا أريد القيام بأي إيماءات إضافية من أجل التبديل من سلسلة الأدوات إلى سلسلة الأدوات ، من مشروع إلى آخر. بنقرة واحدة أو نقرتان ، وتغيير تكوين البناء من Debug إلى Release ، والمترجم من MSVC إلى MinGW. لكن محلل التعليمات البرمجية في IDE يظل كما هو. ويجب عليه ، مع نظام البناء ، التبديل من تكوين إلى آخر ، من سلسلة أدوات إلى أخرى. يمكن أن تكون سلسلة الأدوات نوعًا من الغريبة أو الصليب. ومهمة المحلل هنا هي الاستمرار في تحليل الشفرة بشكل صحيح. إذا أمكن مع الحد الأدنى من الأخطاء.
الرنة آكلة اللحوم بما فيه الكفاية. يمكن إجبارها على قبول ملحقات المحول البرمجي من Microsoft ، مترجم gcc. يمكن تمرير الخيارات في شكل هذه المترجمات ، وسوف يفهمها clang حتى. لكن كل هذا لا يضمن أن clang ستقبل أي عنوان من giblets التي تم جمعها من خزان gcc. أي __builtin_intrinsic_xxx قد يصبح حجر عثرة بالنسبة له. أو لغة تبني أن الإصدار الحالي من رنة في IDE ببساطة لا يدعم. على الأرجح ، لن يؤثر هذا على جودة بناء AST للملف الذي تم تحريره حاليًا. لكن بناء قاعدة شخصية عالمية أو حفظ الرؤوس سابقة التجهيز يمكن أن ينهار. وقد تكون هذه مشكلة خطيرة. قد تتحول مشكلة مماثلة إلى رمز مشابه ليس في رؤوس سلاسل الأدوات أو الأطراف الثالثة ، ولكن في رؤوس أو رموز مصدر المشروع. بالمناسبة ، كل هذا سبب مهم بما يكفي لإخبار نظام البناء (و IDE) بشكل صريح عن ملفات العناوين لمشروعك "الغريبة". يمكن أن تجعل الحياة أسهل.
مرة أخرى ، تم تصميم IDE في الأصل لاستخدامه مع برامج التحويل المختلفة والإعدادات وسلاسل الأدوات والمزيد. مصمم للتعامل مع التعليمات البرمجية ، وبعض عناصرها غير مدعومة. تعد دورة إصدار IDE (ليس كل :)) أقصر من تلك الخاصة بالمترجمين ، وبالتالي ، هناك إمكانية لسحب ميزات جديدة بسرعة أكبر والاستجابة للمشكلات الموجودة. في عالم المترجمين ، كل شيء مختلف قليلاً: دورة الإصدار هي سنة على الأقل ، ويتم حل مشكلات التوافق عبر المترجم عن طريق التجميع المشروط ويتم تمريرها إلى أكتاف المطور. لا يجب أن يكون المترجم عالميًا وحيويًا - حيث أن تعقيده مرتفع بالفعل. رنة ليست استثناء.
الكفاح من أجل السرعة
هذا الجزء من الوقت الذي يقضيه في IDE ، عندما لا يجلس المبرمج في المصحح ، يقوم بتحرير النص. ورغبته الطبيعية هنا هي جعله مريحًا (بخلاف ذلك لماذا IDE؟ هل يمكنني أن أتعايش مع المفكرة!) الراحة ، على وجه الخصوص ، تتضمن سرعة رد الفعل العالية للمحرر على التغييرات النصية والضغط على مفاتيح الاختصار. كما لاحظت Anastasia بشكل صحيح في تقريرها ، إذا لم تستجب البيئة بمظهر قائمة أو قائمة الإكمال التلقائي ، بعد خمس ثوانٍ من الضغط على Ctrl + Space ، فهذا أمر فظيع (جربها بنفسك على محمل الجد). في الأرقام ، هذا يعني أن المحلل اللغوي المدمج في IDE لديه حوالي ثانية واحدة لتقييم التغييرات في الملف وإعادة بناء AST ، وواحد ونصف أو اثنين آخر ليقدم للمطور خيارًا حساسًا للسياق. الثانية. حسنا ، ربما اثنان. بالإضافة إلى ذلك ، فإن السلوك المتوقع هو أنه إذا قام المطور بتغيير اسم .h ، ثم تحول إلى .cpp-shnik ، فإن التغييرات التي تم إجراؤها ستكون "مرئية". تم فتح الملفات هنا في النوافذ المجاورة. والآن حساب بسيط. إذا كان clang ، الذي تم إطلاقه من سطر الأوامر ، يمكن أن يتعامل مع شفرة المصدر في حوالي عشر إلى عشرين ثانية ، فأين السبب في الاعتقاد أنه عند إطلاقه من IDE ، فإنه سيتعامل مع شفرة المصدر بشكل أسرع بكثير ويتناسب مع تلك الثانية أو الثانية؟ أي أنها ستعمل ترتيبًا للحجم بشكل أسرع؟ بشكل عام ، يمكن أن ينتهي هذا ، لكنني لن أفعل ذلك.
حوالي عشر إلى عشرين ثانية للمصدر ، بالطبع ، أبالغ. على الرغم من أنه إذا تم تضمين بعض API الثقيل هناك أو ، على سبيل المثال ، boost.spirit مع Hana في وضع الاستعداد ، ثم يتم استخدام كل هذا بنشاط في النص ، فإن 10-20 ثانية لا تزال قيمًا جيدة. ولكن حتى إذا كانت AST جاهزة بعد ثوانٍ من ثلاثة أو أربعة بعد إطلاق المحلل اللغوي المدمج - فقد مضى وقت طويل بالفعل. شريطة أن تكون عمليات الإطلاق هذه منتظمة (للحفاظ على نموذج الرمز والفهرس في حالة متسقة ، تسليط الضوء ، موجه ، وما إلى ذلك) ، وكذلك عند الطلب - يعد اكتمال الرمز أيضًا إطلاق المحول البرمجي. هل من الممكن تقليل هذا الوقت بطريقة أو بأخرى؟ لسوء الحظ ، في حالة استخدام clang كمحلل ، ليس هناك الكثير من الاحتمالات. السبب: هذه أداة طرف ثالث لا يمكن فيها إجراء تغييرات ( بشكل مثالي ). وهذا يعني ، الحفر في رمز clang باستخدام perftool ، وتحسين وتبسيط بعض الفروع - هذه الميزات غير متاحة وعليك أن تفعل ما تقدمه واجهة برمجة التطبيقات الخارجية (في حالة استخدام libclang ، فهي ضيقة جدًا).
الحل الأول ، الواضح ، والواقع هو الحل الوحيد لاستخدام الرؤوس المتراكمة والمولدة ديناميكيًا. مع التنفيذ المناسب ، يكون الحل قاتلاً. يزيد من سرعة التجميع في بعض الأحيان على الأقل. جوهرها بسيط: تجمع البيئة جميع رؤوس الأطراف الثالثة (أو الرؤوس خارج جذر المشروع) في ملف .h واحد ، وتجعل pch من هذا الملف ، ثم تتضمن هذا pch ضمنيًا في كل مصدر. بالطبع ، يظهر تأثير جانبي واضح: في التعليمات البرمجية المصدر ( في مرحلة التحرير ) ، يمكن رؤية الرموز التي لم يتم تضمينها فيه. ولكن هذه تهمة السرعة. علي أن أختار. وكل شيء سيكون على ما يرام ، إن لم يكن لمشكلة صغيرة واحدة: لا تزال clang مترجمًا. وكونه مترجمًا ، فهو لا يحب الأخطاء في التعليمات البرمجية. وإذا حدث فجأة (فجأة! - انظر القسم السابق) وجود أخطاء في الرؤوس ، فلن يتم إنشاء ملف .pch. على الأقل كان ذلك صحيحًا حتى الإصدار 3.7. هل تغير أي شيء في هذا الصدد منذ ذلك الحين؟ لا أدري ، هناك شك بأن لا. للأسف ، لم يعد هناك أي فرصة للتحقق.
للأسف ، الخيارات البديلة غير متاحة للسبب نفسه: clang هو مترجم وشيء "في حد ذاته". التدخل بنشاط في عملية إنشاء AST ، بطريقة ما جعلها تدمج AST من قطع مختلفة ، والحفاظ على قواعد الرموز الخارجية و te & te - للأسف ، كل هذه الميزات غير متوفرة. فقط API الخارجية ، فقط المتشددين والإعدادات المتاحة من خلال خيارات التجميع. ثم تحليل AST الناتج. إذا كنت تجلس على C ++ - إصدار واجهة برمجة التطبيقات ، فستتوفر فرص أكثر قليلاً. على سبيل المثال ، يمكنك اللعب مع FrontendActions المخصصة ، وإعداد إعدادات أدق لخيارات الترجمة ، وما إلى ذلك. ولكن في هذه الحالة ، لن تتغير النقطة الرئيسية - سيتم ترجمة النص المحرر (أو المفهرس) بشكل مستقل عن الآخرين وبشكل كامل. هذا كل شيء. النقطة.
ربما (ربما!) في يوم من الأيام ، سيكون هناك شوكة من عشبة المنبع مصممة خصيصًا للاستخدام كجزء من IDE. ربما. ولكن في الوقت الحالي ، كل شيء كما هو. لنفترض أن دمج فريق Qt Creator (إلى المرحلة "النهائية") مع libclang استغرق سبع سنوات. لقد جربت QtC 4.7 بمحرك قائم على libclang - أعترف ، أنا شخصياً أحب النسخة القديمة (على النسخة المكتوبة ذاتيًا) ببساطة أكثر لأنها تعمل بشكل أفضل في حالاتي: فهي تطالب وتسلط الضوء ، وكل شيء آخر. لن أتعهد بتقدير عدد الساعات البشرية التي قضوها في هذا التكامل ، لكنني أجرؤ على اقتراح أنه خلال هذا الوقت سيكون من الممكن إنهاء محللي الخاص. بقدر ما أستطيع أن أقول (من خلال المؤشرات غير المباشرة) ، فإن الفريق الذي يعمل على CLion ينظر بحذر نحو التكامل مع libclang / clang ++. لكن هذه افتراضات شخصية بحتة. يعد التكامل على مستوى بروتوكول خادم اللغة خيارًا مثيرًا للاهتمام ، ولكن على وجه التحديد في حالة C ++ ، أميل إلى اعتبار هذا أكثر كمسكن للأسباب المذكورة أعلاه. فهو ببساطة ينقل المشاكل من مستوى التجريد إلى مستوى آخر. ولكن ربما أكون مخطئا ل LSP - المستقبل. دعنا نرى. ولكن على أي حال ، فإن حياة مطوري IDEs الحديثة لـ C ++ مليئة بالمغامرات - مع clang كخلفية ، أو بدونها.