مثال على تحليل كود C ++ باستخدام libclang في Python

في مشروع C ++ شخصي واحد ، كنت بحاجة للحصول على معلومات حول أنواع الكائنات في وقت التشغيل. لدى C ++ آلية معلومات نوع وقت التشغيل (RTTI) مضمّنة ، وبالطبع كان الفكر الأول هو استخدامها ، لكنني قررت أن أكتب عملي لأنني لا أريد سحب الآلية المدمجة بالكامل ، لأنني كنت بحاجة إلى جزء صغير فقط من وظائفها. أردت أيضًا تجربة الميزات الجديدة لـ C ++ 17 ، والتي لم أكن أعرفها بشكل خاص.


في هذا المنشور سوف أقدم مثالاً على العمل مع محلل libclang في بيثون.


تفاصيل تنفيذ RTTI الخاص بي سوف أغفل. من المهم بالنسبة لنا في هذه الحالة النقاط التالية فقط:


  • يجب أن ترث كل فئة أو بنية يمكنها توفير معلومات حول نوعها واجهة IRttiTypeIdProvider ؛
  • في كل فئة من هذه RTTI_HAS_TYPE_ID (إذا لم تكن مجردة) ، يجب عليك إضافة الماكرو RTTI_HAS_TYPE_ID ، الذي يضيف حقل ثابت لمؤشر النوع إلى كائن RttiTypeId . وبالتالي ، للحصول على معرف الكتابة ، يمكنك كتابة MyClass::__typeId أو استدعاء الأسلوب getTypeId مثيل محدد للفئة في وقت التشغيل.

مثال:


 #pragma once #include <string> #include "RTTI.h" struct BaseNode : public IRttiTypeIdProvider { virtual ~BaseNode() = default; bool bypass = false; }; struct SourceNode : public BaseNode { RTTI_HAS_TYPE_ID std::string inputFilePath; }; struct DestinationNode : public BaseNode { RTTI_HAS_TYPE_ID bool includeDebugInfo = false; std::string outputFilePath; }; struct MultiplierNode : public BaseNode { RTTI_HAS_TYPE_ID double multiplier; }; struct InverterNode : public BaseNode { RTTI_HAS_TYPE_ID }; 

كان من الممكن بالفعل العمل مع هذا ، ولكن بعد بعض الوقت كنت بحاجة للحصول على معلومات حول حقول هذه الفئات: اسم الحقل ، الإزاحة والحجم. لتنفيذ كل هذا ، سيكون عليك تكوين هيكل يدويًا مع وصف لكل حقل من فئة الاهتمام في مكان ما في ملف .cpp. بعد كتابة عدة وحدات ماكرو ، بدأ وصف النوع وحقوله في الشكل التالي:


 RTTI_PROVIDER_BEGIN_TYPE(SourceNode) ( RTTI_DEFINE_FIELD(SourceNode, bypass) RTTI_DEFINE_FIELD(SourceNode, inputFilePath) ) RTTI_PROVIDER_END_TYPE() RTTI_PROVIDER_BEGIN_TYPE(DestinationNode) ( RTTI_DEFINE_FIELD(DestinationNode, bypass) RTTI_DEFINE_FIELD(DestinationNode, includeDebugInfo) RTTI_DEFINE_FIELD(DestinationNode, outputFilePath) ) RTTI_PROVIDER_END_TYPE() RTTI_PROVIDER_BEGIN_TYPE(MultiplierNode) ( RTTI_DEFINE_FIELD(MultiplierNode, bypass) RTTI_DEFINE_FIELD(MultiplierNode, multiplier) ) RTTI_PROVIDER_END_TYPE() RTTI_PROVIDER_BEGIN_TYPE(InverterNode) ( RTTI_DEFINE_FIELD(InverterNode, bypass) ) 

وهذا هو فقط لمدة 4 فصول. ما هي المشاكل التي يمكن تحديدها؟


  1. عند نسخ كتل لصق الشفرة يدويًا ، يمكنك أن تغيب عن اسم الفئة عند تحديد الحقل (قمنا بنسخ الكتلة من SourceNode إلى DestinationNode ، لكننا نسيت تغيير SourceNode إلى DestinationNode في أحد الحقول). سيقوم المترجم بتخطي كل شيء ، وقد لا يسقط التطبيق ، لكن المعلومات حول الحقل ستكون غير صحيحة. وإذا قمت بتسجيل أو قراءة البيانات بناءً على معلومات من هذا الحقل ، فسوف ينفجر كل شيء (يقولون ذلك ، لكنني لا أريد التحقق من ذلك بنفسي).
  2. إذا قمت بإضافة حقل إلى الفئة الأساسية ، فيجب تحديث جميع السجلات.
  3. إذا قمت بتغيير اسم الحقول الموجودة في الفصل أو ترتيبها ، فيجب أن تتذكر تحديث الاسم والترتيب في مجموعة الرموز هذه.

ولكن الشيء الرئيسي هو أنه يجب كتابة كل هذا يدويًا. عندما يتعلق الأمر بمثل هذا الرمز الرتيب ، أصبحت كسولًا للغاية وأبحث عن طريقة لإنشاءه تلقائيًا ، حتى لو استغرق الأمر وقتًا وجهدًا أكبر من الكتابة اليدوية.


بيثون تساعدني في هذا ، أكتب نصوصًا عليه لحل مثل هذه المشكلات. لكننا لا نتعامل فقط مع نص القالب ، ولكن مع نص مبني على أساس شفرة المصدر C ++. نحتاج إلى أداة للحصول على معلومات حول رمز C ++ ، وسوف يساعدنا libclang في هذا.


libclang هي واجهة C رفيعة المستوى لـ Clang. يوفر واجهة برمجة التطبيقات (API) لأدوات تحليل شفرة المصدر في شجرة بناء الجملة المجردة (AST) ، وتحميل AST التي تم تحليلها بالفعل ، واجتياز AST ، ورسم خرائط مواقع المصادر الفعلية لعناصر داخل AST ، ومجموعة أدوات Clang الأخرى.

كما يلي من الوصف ، توفر libclang واجهة C ، وللتعامل معها من خلال Python ، تحتاج إلى مكتبة ملزمة (ملزمة). في وقت كتابة هذا المقال ، لم تكن هناك مكتبة رسمية من هذا النوع لبيثون ، ولكن من الموقع غير الرسمي ، يوجد https://github.com/ethanhs/clang .


تثبيته من خلال مدير الحزمة:


 pip install clang 

يتم تزويد المكتبة بتعليقات في الكود المصدري. ولكن لفهم جهاز libclang ، تحتاج إلى قراءة وثائق libclang . لا توجد أمثلة كثيرة لاستخدام المكتبة ، ولا توجد تعليقات تشرح لماذا يعمل كل شيء على هذا النحو ، وليس هناك أي شيء آخر. سيكون لدى أولئك الذين لديهم خبرة بالفعل في libclang أسئلة أقل ، لكنني شخصياً لم يكن لدي مثل هذه التجربة ، لذلك اضطررت للحفر بعمق في الكود والتجويف في المصحح.


لنبدأ بمثال بسيط:


 import clang.cindex index = clang.cindex.Index.create() translation_unit = index.parse('my_source.cpp', args=['-std=c++17']) for i in translation_unit.get_tokens(extent=translation_unit.cursor.extent): print (i.kind) 

هنا يتم إنشاء كائن من نوع Index يمكنه تحليل ملف برمز C ++. طريقة parse تُرجع كائنًا من النوع TranslationUnit ، وهذه وحدة لترجمة الشفرة. TranslationUnit هي عقدة AST ، وكل عقدة AST تخزن معلومات حول موضعها في الكود المصدر (المدى). نحن ندخل جميع الرموز في TranslationUnit ونطبع نوع هذه الرموز المميزة (خاصية لطيفة).


على سبيل المثال ، خذ شفرة C ++ التالية:


 class X {}; class Y {}; class Z : public X {}; 

نتيجة تنفيذ البرنامج النصي
 TokenKind.KEYWORD TokenKind.IDENTIFIER TokenKind.PUNCTUATION TokenKind.PUNCTUATION TokenKind.PUNCTUATION TokenKind.KEYWORD TokenKind.IDENTIFIER TokenKind.PUNCTUATION TokenKind.PUNCTUATION TokenKind.PUNCTUATION TokenKind.KEYWORD TokenKind.IDENTIFIER TokenKind.PUNCTUATION TokenKind.KEYWORD TokenKind.IDENTIFIER TokenKind.PUNCTUATION TokenKind.PUNCTUATION TokenKind.PUNCTUATION 

الآن دعونا نتعامل مع AST. قبل كتابة كود Python ، دعونا نرى ما يجب أن نتوقعه من محلل clang. تشغيل رنة في وضع تفريغ AST:


 clang++ -cc1 -ast-dump my_source.cpp 

نتيجة القيادة
 TranslationUnitDecl 0xaaaa9b9fa8 <<invalid sloc>> <invalid sloc> |-TypedefDecl 0xaaaa9ba880 <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128' | `-BuiltinType 0xaaaa9ba540 '__int128' |-TypedefDecl 0xaaaa9ba8e8 <<invalid sloc>> <invalid sloc> implicit __uint128_t 'unsigned __int128' | `-BuiltinType 0xaaaa9ba560 'unsigned __int128' |-TypedefDecl 0xaaaa9bac48 <<invalid sloc>> <invalid sloc> implicit __NSConstantString '__NSConstantString_tag' | `-RecordType 0xaaaa9ba9d0 '__NSConstantString_tag' | `-CXXRecord 0xaaaa9ba938 '__NSConstantString_tag' |-TypedefDecl 0xaaaa9e6570 <<invalid sloc>> <invalid sloc> implicit __builtin_ms_va_list 'char *' | `-PointerType 0xaaaa9e6530 'char *' | `-BuiltinType 0xaaaa9ba040 'char' |-TypedefDecl 0xaaaa9e65d8 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list 'char *' | `-PointerType 0xaaaa9e6530 'char *' | `-BuiltinType 0xaaaa9ba040 'char' |-CXXRecordDecl 0xaaaa9e6628 <my_source.cpp:1:1, col:10> col:7 referenced class X definition | |-DefinitionData pass_in_registers empty aggregate standard_layout trivially_copyable pod trivial literal has_constexpr_non_copy_move_ctor can_const_default_init | | |-DefaultConstructor exists trivial constexpr needs_implicit defaulted_is_constexpr | | |-CopyConstructor simple trivial has_const_param needs_implicit implicit_has_const_param | | |-MoveConstructor exists simple trivial needs_implicit | | |-CopyAssignment trivial has_const_param needs_implicit implicit_has_const_param | | |-MoveAssignment exists simple trivial needs_implicit | | `-Destructor simple irrelevant trivial needs_implicit | `-CXXRecordDecl 0xaaaa9e6748 <col:1, col:7> col:7 implicit class X |-CXXRecordDecl 0xaaaa9e6800 <line:3:1, col:10> col:7 class Y definition | |-DefinitionData pass_in_registers empty aggregate standard_layout trivially_copyable pod trivial literal has_constexpr_non_copy_move_ctor can_const_default_init | | |-DefaultConstructor exists trivial constexpr needs_implicit defaulted_is_constexpr | | |-CopyConstructor simple trivial has_const_param needs_implicit implicit_has_const_param | | |-MoveConstructor exists simple trivial needs_implicit | | |-CopyAssignment trivial has_const_param needs_implicit implicit_has_const_param | | |-MoveAssignment exists simple trivial needs_implicit | | `-Destructor simple irrelevant trivial needs_implicit | `-CXXRecordDecl 0xaaaa9e6928 <col:1, col:7> col:7 implicit class Y `-CXXRecordDecl 0xaaaa9e69e0 <line:5:1, col:21> col:7 class Z definition |-DefinitionData pass_in_registers empty standard_layout trivially_copyable trivial literal has_constexpr_non_copy_move_ctor can_const_default_init | |-DefaultConstructor exists trivial constexpr needs_implicit defaulted_is_constexpr | |-CopyConstructor simple trivial has_const_param needs_implicit implicit_has_const_param | |-MoveConstructor exists simple trivial needs_implicit | |-CopyAssignment trivial has_const_param needs_implicit implicit_has_const_param | |-MoveAssignment exists simple trivial needs_implicit | `-Destructor simple irrelevant trivial needs_implicit |-public 'X' `-CXXRecordDecl 0xaaaa9e6b48 <col:1, col:7> col:7 implicit class Z 

هنا CXXRecordDecl هو نوع العقدة التي تمثل إعلان الفصل. قد تلاحظ أن هناك مثل هذه العقد أكثر من الفئات الموجودة في الملف المصدر. وذلك لأن العقد المرجعية ممثلة بنفس النوع ، أي العقد التي هي وصلات إلى العقد الأخرى. في حالتنا ، تحديد الفئة الأساسية هو المرجع. عند تفكيك هذه الشجرة ، يمكنك تحديد العقدة المرجعية باستخدام إشارة خاصة.


سنقوم الآن بكتابة نص يسرد الفئات في الملف المصدر:


 import clang.cindex import typing index = clang.cindex.Index.create() translation_unit = index.parse('my_source.cpp', args=['-std=c++17']) def filter_node_list_by_node_kind( nodes: typing.Iterable[clang.cindex.Cursor], kinds: list ) -> typing.Iterable[clang.cindex.Cursor]: result = [] for i in nodes: if i.kind in kinds: result.append(i) return result all_classes = filter_node_list_by_node_kind(translation_unit.cursor.get_children(), [clang.cindex.CursorKind.CLASS_DECL, clang.cindex.CursorKind.STRUCT_DECL]) for i in all_classes: print (i.spelling) 

يتم تخزين اسم الفئة في خاصية spelling . بالنسبة لأنواع العقد المختلفة ، قد تحتوي قيمة spelling على بعض معدّلات الكتابة ، ولكن بالنسبة للفئة أو تعريف البنية ، فإنها تحتوي على اسم بدون معدّلات.


نتيجة التنفيذ:


 X Y Z 

عند التحليل ، تقوم AST clang أيضًا بتوزيع الملفات المتصلة عبر #include . حاول إضافة #include <string> إلى المصدر ، وفي التفريغ ستحصل على 84 ألف سطر ، من الواضح أن هذا حل كثيرًا لمشكلتنا.


لعرض تفريغ AST لهذه الملفات من خلال سطر الأوامر ، من الأفضل حذف كل #include . أعدهم عند دراسة AST واحصل على فكرة حول التسلسل الهرمي وأنواع الملفات المهمة.


في البرنامج النصي ، من أجل تصفية AST فقط التي تنتمي إلى الملف المصدر ، وليس متصلاً من خلال #include ، يمكنك إضافة وظيفة التصفية التالية حسب الملف:


 def filter_node_list_by_file( nodes: typing.Iterable[clang.cindex.Cursor], file_name: str ) -> typing.Iterable[clang.cindex.Cursor]: result = [] for i in nodes: if i.location.file.name == file_name: result.append(i) return result ... filtered_ast = filter_by_file(translation_unit.cursor, translation_unit.spelling) 

الآن يمكنك القيام باستخراج الحقول. بعد ذلك ، سأقدم لك الشفرة الكاملة التي تنشئ قائمة من الحقول مع مراعاة الميراث وتولد نصًا وفقًا للقالب. لا يوجد شيء محدد لرنة هنا ، لذلك لا تعليق.


رمز البرنامج النصي الكامل
 import clang.cindex import typing index = clang.cindex.Index.create() translation_unit = index.parse('Input.h', args=['-std=c++17']) def filter_node_list_by_file( nodes: typing.Iterable[clang.cindex.Cursor], file_name: str ) -> typing.Iterable[clang.cindex.Cursor]: result = [] for i in nodes: if i.location.file.name == file_name: result.append(i) return result def filter_node_list_by_node_kind( nodes: typing.Iterable[clang.cindex.Cursor], kinds: list ) -> typing.Iterable[clang.cindex.Cursor]: result = [] for i in nodes: if i.kind in kinds: result.append(i) return result def is_exposed_field(node): return node.access_specifier == clang.cindex.AccessSpecifier.PUBLIC def find_all_exposed_fields( cursor: clang.cindex.Cursor ): result = [] field_declarations = filter_node_list_by_node_kind(cursor.get_children(), [clang.cindex.CursorKind.FIELD_DECL]) for i in field_declarations: if not is_exposed_field(i): continue result.append(i.displayname) return result source_nodes = filter_node_list_by_file(translation_unit.cursor.get_children(), translation_unit.spelling) all_classes = filter_node_list_by_node_kind(source_nodes, [clang.cindex.CursorKind.CLASS_DECL, clang.cindex.CursorKind.STRUCT_DECL]) class_inheritance_map = {} class_field_map = {} for i in all_classes: bases = [] for node in i.get_children(): if node.kind == clang.cindex.CursorKind.CXX_BASE_SPECIFIER: referenceNode = node.referenced bases.append(node.referenced) class_inheritance_map[i.spelling] = bases for i in all_classes: fields = find_all_exposed_fields(i) class_field_map[i.spelling] = fields def populate_field_list_recursively(class_name: str): field_list = class_field_map.get(class_name) if field_list is None: return [] baseClasses = class_inheritance_map[class_name] for i in baseClasses: field_list = populate_field_list_recursively(i.spelling) + field_list return field_list rtti_map = {} for class_name, class_list in class_inheritance_map.items(): rtti_map[class_name] = populate_field_list_recursively(class_name) for class_name, field_list in rtti_map.items(): wrapper_template = """\ RTTI_PROVIDER_BEGIN_TYPE(%s) ( %s ) RTTI_PROVIDER_END_TYPE() """ rendered_fields = [] for f in field_list: rendered_fields.append(" RTTI_DEFINE_FIELD(%s, %s)" % (class_name, f)) print (wrapper_template % (class_name, ",\n".join(rendered_fields))) 

لا يعتبر هذا البرنامج النصي ما إذا كان لدى الفئة RTTI. لذلك ، بعد استلام النتيجة ، سيكون عليك حذف الكتل التي تصف الفئات دون RTTI يدويًا. لكن هذا تافه.


آمل أن يكون شخص ما مفيدًا ويوفر الوقت. يتم نشر كل رمز على جيثب .

Source: https://habr.com/ru/post/ar439270/


All Articles