Clang 之旅--[翻譯]新增自定義的 attribute

weixin_34041003發表於2018-04-17

Clang 之旅系列文章:
Clang 之旅--使用 Xcode 開發 Clang 外掛
Clang 之旅--[翻譯]新增自定義的 attribute
Clang 之旅--實現一個自定義檢查規範的 Clang 外掛

前言

這是 Clang 之旅系列的第二篇,自己想要完成的需求是:在編譯階段檢查某個方法的引數與返回值的型別相同,如果型別不一致的話能丟擲編譯錯誤的提示。需要接觸到 Clang 中關於 attribute 處理的程式碼,所以這篇先來翻譯官方文件中新增自定義的 attribute 這一節,不得不說,雖然 Clang 的文件可以說是很標杆了,但是總有一種看了後面忘了前面的感覺,可能是 Clang 比較龐大,涉及專有詞彙比較多的原因,所以我會偏向意譯多一點,試圖用更加易懂的表達組織語言,也是加深自己的記憶吧。

怎樣新增 attribute

attribute 是一種可以附加到程式結構中的資料形式,允許開發人員傳遞資訊給編譯器來實現各種需求。例如,attribute 可以用來改變在程式構造時生成的程式碼,或者用來提供額外的資訊給靜態分析。本文件講解如何新增一個自定義的 attribute 到 Clang 中。現有 attribute 列表的文件可以在這裡找到。

attribute 基礎知識

Clang 中的 attribute 涉及到三個階段:解析 attribute 、從已解析的 attribute 轉換成語法樹上的 attribute、對 attribute 進行處理。
attribute 的解析可以採用多種語法形式,例如 GNU、C++ 11 和 Microsoft 形式,還由 attribute 提供的其他資訊來確定。最終,解析好的 attribute 用一個 AttributeList 物件來表示。這些解析好的 attribute 會鏈成一個 attribute 鏈,加到宣告或者定義上。attribtue 的解析是由 Clang 自動完成的,除了那些關鍵字 attribute。關鍵字的解析和 AttributeList 物件的生成必須由我們手動完成。
最後,Sema::ProcessDeclAttributeList() 帶著 Decl 型別和 AttributeList 型別的引數被呼叫,此時解析好的 attribute 就會被轉化成語法樹上的 attribute。這個處理依賴於 attribute 的屬性定義和語義要求。最後的結果就是語法樹上的 attribute 物件可以從 Decl 物件獲取到,也就是通過呼叫 Decl::getAttr<T>() 來獲取。
語法樹上的 attribute 的結構同樣也受到 Attr.td 檔案中的定義所限制。這個定義會自動生成 attribute 的實現所用到的功能,包括生成 clang::Attr 的子類、解析器所用到的資訊和某些 attribute 自動進行的語義分析等等。

include/clang/Basic/Attr.td

新增新的 attribute 到 Clang 的第一個步驟就是把其定義新增到 include/clang/Basic/Attr.td。這個定義必須從 Attr 或者其子類繼承。大多數 attribute 會直接從 InheritableAttr 繼承,InheritableAttr 指定了這個 attribute 可以通過它所關聯的 Decl 稍後進行重宣告。如果這個 attribute 是作用於型別而不是宣告,那麼這種 attribute 應該從 TypeAttr 派生,並且通常不會被賦予 AST 表示(注意本文件並不講解生成型別所用的 attribute)。一個繼承於 IgnoredAttr 的 attribute 會被解析,但是會在被使用的時候產生一個 "被忽略的屬性" 的警告,這種處理方法在某個屬性支援別的前端而不支援 Clang 的情況下是很有用的。
這個定義能指定 attribute 的一些關鍵部分,比如 attribute 的名字、attribute 支援的拼寫、attribute 的引數等等。Attr 型別中的大多數成員變數都不需要派生定義,預設的就足夠了。但是,每個 attribute 都需要至少指定 拼寫列表、subject 列表和文件列表。

拼寫

所有 attribute 都需要指定一個拼寫列表,表示拼寫 attribute 的方式。比如某個 attribute 可能會包含關鍵字拼寫, C++11 拼寫和 GNU 拼寫。空的拼寫列表也是允許的並且可能對隱式建立的 attribute 有用。以下是支援的拼寫的表格:

拼寫 描述
GNU 用 GNU 風格 __attribute__((attr)) 語法和位置拼寫
CXX11 用 C++ 風格 [[attr]] 語法拼寫。如果該 attribute 是由 Clang 所使用的,那麼應該設定名稱空間為 "clang"
Declspec 用 Microsoft 風格 __declspec(attr) 語法拼寫
Keyword 這個 attribute 用關鍵字的方式拼寫,並且需要自定義解析
GCC 指定兩種拼寫:首先是 GNU 風格拼寫;然後是 C++ 風格拼寫,名稱空間為 gnu。只能為支援 GCC 的 attribute 指定這個拼寫。
Pragma attribute 用 #pragma 的形式拼寫,並且需要在前處理器中執行自定義的處理。如果該 attribute 是由 Clang 所使用的,那麼應該設定名稱空間為 "clang"。需要注意這個拼寫並不能被用於宣告語句中。
Subjects

每個 attribute 都有一個或者多個 subject。如果 attribute 被使用到了一個不在 subject 列表上的 subject,就會自動顯示診斷資訊。 這個資訊是警告還是錯誤是由 attribute 中的 SubjectList 決定的,預設的是警告。顯示給使用者的診斷資訊將根據 subject 列表自動確定,但是也可以在 SubjectList 中指定自定義診斷引數。不符合 subject 列表導致的診斷資訊要麼是 diag::warn_attribute_wrong_decl_type,要麼是 diag::err_attribute_wrong_decl_type。具體引數的列舉值可以從 include/clang/Sema/AttributeList.h 找到。如果先前未使用的 Decl 節點被新增到 SubjectList 中,則可能需要更新用於自動確定 utils/TableGen/ClangAttrEmitter.cpp 中的診斷引數的邏輯。
所有在 SubjectList 中的 subject 要麼是在 DeclNodes.td 中定義的 Decl 節點,要麼就是在 StmtNodes.td 中定義的 statement 節點。不過,可以生成 SubsetSubject 物件來建立更加複雜的 subject。每個這樣的物件都有一個它所屬的基本物件(必須是一個 Decl 或 Stmt 節點,而不是一個 SubsetSubject 節點),還有一些自定義程式碼在確定某個 attribute 是否屬於該物件時被呼叫。例如,一個 NonBitField SubsetSubject 關聯到 FieldDecl 類,同時會測試給定的 FieldDecl 是否是一個位欄位。當在 SubjectList 中指定了一個 SubsetSubject 時必須同時提供一個自定義的診斷資訊引數。
attribute 的 subject 列表會在 HasCustomParsing 設為 1 的情況下自動進行診斷檢查。

文件

所有的 attribute 都必須具有某種形式的文件。文件是通過每天執行的伺服器端程式在公共伺服器上生成的。通常來說,attribute 的文件是在 include/clang/Basic/AttrDocs.td 中單獨定義的,以文件屬性命名。
如果 attribute 不是通用的,或者是隱式建立的沒有對應拼寫的 attribuet,則可以將文件列表變數設定為 Undocumented。否則,該 attribute 應將其文件新增到 AttrDocs.td。
文件屬性是從 Documentation tablegen 型別繼承而來的,所有的派生型別都必須建立一個文件類別和設定文件本身內容。此外,它還可以為 attribute 指定一個自定義的標題,否則會選擇預設的標題。
現在有四種預先定義好的文件類別:DocCatFunction 對應函式的 attribute,DocCatVariable 對應到變數的 attribute,DocCatType 對應型別的 attribute,DocCatStmt 對應宣告的 attribute。自定義文件類別應該用於具有類似功能的 attribute 組。自定義類別非常適合用來為組中的 attribute 提供概述資訊。
文件內容(包括 attribute 的內容或者類別的內容)是用 reStructuredText(RST)格式寫的。
在編寫該 attribute 的文件之後,應該對其在本地對其進行測試,以確保在伺服器上生成文件不會有問題。本地測試需要重新構建 clang-tblgen。要生成 attribute 文件,請執行以下命令:

clang-tblgen -gen-attr-docs -I /path/to/clang/include /path/to/clang/include/clang/Basic/Attr.td -o /path/to/clang/docs/AttributeReference.rst

在本地進行測試時,不要對 AttributeReference.rst 提交更改。該檔案是由伺服器自動生成的,並且對該檔案所做的任何更改都將被覆蓋。

引數

attribute 可以選擇指定可以傳遞給 attribute 的引數列表。attribute 的引數指定 attribute 的解析形式和語義形式。例如,如果 Args[StringArgument<"Arg1">, IntArgument<"Arg2">],那麼 __attribute__((myattribute("Hello", 3))) 就是一個合法的使用方式;這個 attribute 在解析時要求有兩個引數:一個 string 型別一個 integer 型別。
每個引數都有個名字和一個用來指定這個引數是否為可選的標誌。引數關聯的 C++ 型別由引數定義型別確定。如果現有引數型別不足,則可以建立新型別,但需要修改 utils/TableGen/ClangAttrEmitter.cpp 才能正確支援該新型別。

其他屬性

Attr 的定義還具有其他變數來控制 attribute 的行為。其中有很多是用於特殊用途的,超出了本文件的範圍,但有一些還是值得提上一嘴的。
如果 attribute 的解析形式更加複雜或者和語義形式不同,則可以將 HasCustomParsing 變數設定為 1,並且可以針對特殊情況修改 Parser::ParseGNUAttributeArgs() 中的解析程式碼。請注意,這僅適用於具有 GNU 拼寫的 attribute;__declspec 拼寫的 attribute 現在是忽略這個標誌的,並由 Parser::ParseMicrosoftDeclSpec 負責解析。
請注意,把 HasCustomParsing 設定為 1 將不再使用通用的 attribute 處理邏輯,需要額外的處理來確保該 attribute 能使用。
如果該 attribute 不通過模板宣告例項化,則將 Clone 成員變數設定為 0。預設情況下,所有的 attribute 都將通過模板進行例項化。
不需要 AST 節點的 attribute 應該將 ASTNode 變數設定為 0 以避免汙染 AST。請注意,從 TypeAttrIgnoredAttr 繼承的類都不會自動生成 AST 節點。所有其他屬性預設會生成一個 AST 節點。該 AST 節點是 attribute 的語義表示。
LangOpts 變數指定了 attribute 所需的語言選項列表。例如,所有的 CUDA-specific 的 attribuet 都將 LangOpts 欄位指定為 [CUDA],並且當 CUDA 語言選項未啟用時,會發出“attribute ignored”的警告診斷。由於語言選項不是自動生成的節點,因此必須手動建立新的語言選項,並應指定 LangOptions 類所使用的拼寫。
可以基於 attribute 的拼寫列表為該 attribute 生成自定義的存取器。例如,如果某個 attribute 有兩種不同的拼寫:'foo' 和 'bar',則可以建立訪問器:[Accessor<"isFoo", [GNU<"Foo">]>, Accessor<"isBar",[GNU<"Bar">]>]。這些存取器將在該 attribute 的語義形式上生成,不接受任何引數並返回一個布林值。
不需要自定義語義分析的 attribute 應該將 SemaHandler 變數設為 0。請注意,任何從 IgnoredAttr 繼承的 attribute 都不會自動進行語義處理。所有其他 attribute 都使用預設的語義處理。沒有語義處理的 attribute 都不會有解析好的 attribute Kind 列舉器。
指定 Target 的 attribute 可能會與不同 Target 的 attribute 共用一個拼寫。例如,ARM 和 msp430 Target 都有一個拼寫為 GNU<"interrupt"> 的 attribute,但各自有不同的解析方式和語義要求。為了支援這個特性,繼承自 TargetSpecificAttribute 的 attribute 可以指定 ParseKind 變數。這個變數在共用拼寫的所有引數之間應該是相同的,並且對應於解析 attribute 的 Kind 的列舉器。這允許 attribute 共用一種解析型別,但具有不同的語義屬性。例如,AttributeList::AT_Interrupt 是共用的解析型別,但 ARMInterruptAttr 和 MSP430InterruptAttr 是各自的語義屬性。
預設情況下,當宣告為 merging attribute 時,該 attributes 不會被複制。但是,如果在此合併階段中可以複製某個 attribute,那麼將 DuplicatesAllowedWhileMerging 變數設定為 1,該 attribute 就會被合併。
預設情況下,attribute 的引數在上下文中被解析。如果應該在上下文中解析 attribute 的引數(類似於解析 sizeof 表示式的引數的方式),請將 ParseArgumentsAsUnevaluated 設定為 1

樣板程式碼

宣告 attribute 的所有的語義處理都在檔案 lib/Sema/SemaDeclAttr.cpp 中,並且通常都從 ProcessDeclAttribute() 函式開始。如果這個 attribute 是一個“簡單的” attribute,也就是說這個 attribute 除了自動生成的內容之外不需要自定義的語義處理,那麼就新增 handleSimpleAttribute<YourAttr>(S, D, Attr); 函式到 switch 語句中。否則,編寫一個新的 handleYourAttr() 函式,並將其新增到 switch 語句中。不要直接在 case 語句中實現處理邏輯。
除非 attribute 的定義中另有規定,否則將自動處理解析 attribute 的常見語義檢查,包括診斷不屬於給定 Decl 的解析的 attribute、確保傳遞正確的最小數量的引數等等。
如果 attribute 要加上額外的警告,那麼在 include/clang/Basic/DiagnosticGroups.td 檔案中定義一個 DiagGroup。如果只有一個診斷資訊的話,直接在 DiagnosticSemaKinds.td 檔案中使用 InGroup<DiagGroup<"your-attribute">> 也是可以的。
所有為你自定義的 attribute 所生成的診斷資訊,包括自動生成的(比如 subject 和引數個數),都應該有一個對應的測試用例。

語義處理

大多數 attribute 被實現為對編譯器有一定的影響。例如,修改生成程式碼的方式,或為分析過程新增額外的語義檢查等,將 attribute 的定義和轉換新增到該 attribute 的語義表示中,剩下的就是實現 attribute 的自定義邏輯。
可以使用 hasAttr<T>() 方法來查詢 clang::Decl 物件中是否有 attribute。可以使用 getAttr<T> 來獲取一個指向 attribute 的指標。

PS:我建了一個 Clang & LLVM 微信交流群,可以交流 Clang 相關問題、iOS 相關問題、發招聘資訊,拒絕廣告、拒絕刷屏。想要加入的可以加我微信拉你進群~


698554-424a2c140c26d505.jpeg
微信二維碼

相關文章