Contoh parsing kode C ++ menggunakan libclang dengan Python

Pada satu proyek C ++ pribadi, saya perlu mendapatkan informasi tentang jenis objek saat runtime. C ++ memiliki mekanisme Run-Time Type Information (RTTI) bawaan, dan tentu saja pikiran pertama adalah untuk menggunakannya, tetapi saya memutuskan untuk menulis implementasi saya karena saya tidak ingin menarik seluruh mekanisme built-in, karena saya hanya memerlukan sebagian kecil dari fungsinya. Saya juga ingin mencoba dalam praktiknya fitur-fitur baru C ++ 17, yang tidak terlalu saya kenal.


Dalam posting ini saya akan menyajikan contoh bekerja dengan parser libclang dengan Python.


Saya akan menghilangkan detail tentang melepaskan RTTI saya. Penting bagi kami dalam hal ini hanya poin-poin berikut:


  • Setiap kelas atau struktur yang dapat memberikan informasi tentang jenisnya harus mewarisi antarmuka IRttiTypeIdProvider ;
  • Di setiap kelas tersebut (jika tidak abstrak), Anda harus menambahkan makro RTTI_HAS_TYPE_ID , yang menambahkan bidang statis penunjuk jenis ke objek RttiTypeId . Jadi, untuk mendapatkan pengenal tipe, Anda dapat menulis MyClass::__typeId atau memanggil metode getTypeId instance spesifik dari kelas saat runtime.

Contoh:


 #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 }; 

Itu sudah mungkin untuk bekerja dengan ini, tetapi setelah beberapa waktu saya perlu mendapatkan informasi tentang bidang kelas ini: nama bidang, offset dan ukuran. Untuk mengimplementasikan semua ini, Anda harus secara manual membentuk struktur dengan deskripsi setiap bidang kelas yang diminati di suatu tempat di file .cpp. Setelah menulis beberapa makro, deskripsi jenis dan bidangnya mulai terlihat seperti ini:


 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) ) 

Dan ini hanya untuk 4 kelas. Masalah apa yang bisa diidentifikasi?


  1. Saat menyalin blok kode yang ditempel secara manual, Anda bisa kehilangan nama kelas ketika mendefinisikan bidang (kami menyalin blok dari SourceNode ke DestinationNode, tetapi lupa mengubah SourceNode ke DestinationNode di salah satu bidang). Kompiler akan melewatkan segalanya, aplikasi mungkin tidak jatuh, tetapi informasi tentang bidang tersebut akan salah. Dan jika Anda merekam atau membaca data berdasarkan informasi dari bidang seperti itu, maka semuanya akan meledak (kata mereka, tetapi saya tidak ingin memeriksanya sendiri).
  2. Jika Anda menambahkan bidang ke kelas dasar, maka SEMUA catatan harus diperbarui.
  3. Jika Anda mengubah nama atau urutan bidang di kelas, maka Anda harus ingat untuk memperbarui nama dan urutan di footcloth kode ini.

Tetapi yang utama adalah semua ini harus ditulis secara manual. Ketika datang ke kode monoton, saya menjadi sangat malas dan mencari cara untuk menghasilkannya secara otomatis, bahkan jika itu membutuhkan lebih banyak waktu dan usaha daripada penulisan manual.


Python membantu saya dengan ini, saya menulis skrip di atasnya untuk menyelesaikan masalah seperti itu. Tapi kita berhadapan bukan hanya dengan teks templat, tetapi dengan teks yang dibangun berdasarkan kode sumber C ++. Kami membutuhkan alat untuk mendapatkan informasi tentang kode C ++, dan libclang akan membantu kami dalam hal ini.


libclang adalah antarmuka C tingkat tinggi untuk Dentang. Menyediakan API untuk alat untuk mem-parsing kode sumber dalam pohon sintaksis abstrak (AST), memuat AST yang sudah diuraikan, melintasi AST, memetakan lokasi sumber fisik ke elemen-elemen dalam AST, dan toolset Dentang lainnya.

Sebagai berikut dari deskripsi, libclang menyediakan antarmuka-C, dan untuk bekerja dengannya melalui Python Anda membutuhkan pustaka pengikat (binding). Pada saat menulis posting ini tidak ada perpustakaan seperti resmi untuk Python, tetapi dari tidak resmi ada ini https://github.com/ethanhs/clang .


Instal melalui manajer paket:


 pip install clang 

Perpustakaan disediakan dengan komentar dalam kode sumber. Tetapi untuk memahami perangkat libclang, Anda perlu membaca dokumentasi libclang . Tidak banyak contoh menggunakan perpustakaan, dan tidak ada komentar yang menjelaskan mengapa semuanya berjalan seperti ini, dan tidak sebaliknya. Mereka yang sudah memiliki pengalaman dengan libclang akan memiliki lebih sedikit pertanyaan, tetapi saya pribadi tidak memiliki pengalaman seperti itu, jadi saya harus menggali lebih dalam ke dalam kode dan melihat-lihat dalam debugger.


Mari kita mulai dengan contoh sederhana:


 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) 

Di sini objek tipe Index dibuat yang dapat mem-parsing file dengan kode C ++. Metode parse mengembalikan objek bertipe TranslationUnit , ini adalah unit terjemahan kode. TranslationUnit adalah simpul AST, dan setiap simpul AST menyimpan informasi tentang posisinya dalam kode sumber (luas). Kami mengulangi semua token di TranslationUnit dan mencetak jenis token ini (properti jenis).


Misalnya, ambil kode C ++ berikut:


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

Hasil eksekusi skrip
 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 

Sekarang mari kita menangani AST. Sebelum menulis kode Python, mari kita lihat apa yang harus kita harapkan dari parser dentang. Jalankan dentang dalam mode dump AST:


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

Hasil perintah
 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 

Di sini CXXRecordDecl adalah jenis simpul yang mewakili deklarasi kelas. Anda mungkin memperhatikan bahwa ada lebih banyak node daripada kelas dalam file sumber. Ini karena node referensi diwakili oleh tipe yang sama, yaitu node yang merupakan tautan ke node lain. Dalam kasus kami, menentukan kelas dasar adalah referensi. Saat membongkar pohon ini, Anda dapat menentukan simpul referensi menggunakan bendera khusus.


Sekarang kami akan menulis skrip yang mencantumkan kelas dalam file sumber:


 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) 

Nama kelas disimpan di properti spelling . Untuk berbagai jenis node, nilai spelling mungkin berisi beberapa jenis pengubah, tetapi untuk deklarasi kelas atau struktur, itu berisi nama tanpa pengubah.


Hasil Eksekusi:


 X Y Z 

Saat parsing, AST clang juga mem-parsing file yang terhubung melalui #include . Coba tambahkan #include <string> ke sumber, dan di dump Anda akan mendapatkan 84 ribu baris, yang jelas sedikit banyak untuk menyelesaikan masalah kami.


Untuk melihat dump AST dari file-file tersebut melalui baris perintah, lebih baik hapus semua #include . Bawa mereka kembali ketika Anda mempelajari AST dan mendapatkan gagasan tentang hierarki dan jenis dalam file yang menarik.


Dalam skrip, untuk memfilter hanya AST milik file sumber, dan tidak terhubung melalui #include , Anda dapat menambahkan fungsi penyaringan berikut oleh file:


 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) 

Sekarang Anda bisa melakukan ekstraksi bidang. Selanjutnya, saya akan memberikan kode lengkap yang menghasilkan daftar bidang dengan mempertimbangkan warisan akun dan menghasilkan teks sesuai dengan templat. Tidak ada yang spesifik untuk dentang di sini, jadi tidak ada komentar.


Kode skrip lengkap
 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))) 

Skrip ini tidak mempertimbangkan apakah kelas memiliki RTTI. Oleh karena itu, setelah menerima hasilnya, Anda harus menghapus secara manual blok yang menggambarkan kelas tanpa RTTI. Tapi ini agak sepele.


Saya harap seseorang akan berguna dan menghemat waktu. Semua kode diposting di GitHub .

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


All Articles