- Kubernetes源碼剖析
- 鄭東旭
- 4627字
- 2020-07-23 17:12:18
2.7 gengo代碼生成核心實現
Kubernetes的代碼生成器都是在k8s.io/gengo包的基礎上實現的。我們在前面介紹了deepcopy-gen、defaulter-gen、conversion-gen、openapi-gen、go-bindata等代碼生成器的用法。代碼生成器都會通過一個輸入包路徑(--input-dirs)參數,根據gengo的詞法分析、抽象語法樹等操作,最終生成代碼并輸出(--output-file-base)。gengo代碼目錄結構如下。

gengo代碼目錄結構說明如下。
● args:代碼生成器的通用flags參數。
● examples:包含deepcopy-gen、defaulter-gen、import-boss、set-gen等代碼生成器的生成邏輯。
● generator:代碼生成器通用接口Generator。
● namer:命名管理,支持創建不同類型的名稱。例如,根據類型生成名稱,定義type foo string,能夠生成func FooPrinter(f*foo){Print(string(*f))}。
● parser:代碼解析器,用來構造抽象語法樹。
● types:類型系統,用于數據類型的定義及類型檢查算法的實現。
2.7.1 代碼生成邏輯與編譯器原理
gengo的代碼生成邏輯與編譯器原理非常類似,大致可分為如下幾個過程,gengo代碼生成原理如圖2-5所示。

圖2-5 gengo代碼生成原理
gengo代碼生成原理的流程如下。
(1)Gather The Info:收集Go語言源碼文件信息及內容。
(2)Lexer/Parser:通過Lexer詞法分析器進行一系列詞法分析。
(3)AST Generator:生成抽象語法樹。
(4)Type Checker:對抽象語法樹進行類型檢查。
(5)Code Generation:生成代碼,將抽象語法樹轉換為機器代碼。
2.7.2 收集Go包信息
Go語言沒有用預處理器、宏定義或#define聲明來控制指定平臺,相反,Go語言標準庫提供了go/build工具,該工具支持Go語言的構建標簽(Build Tag)機制來構建約束條件(Build Constraint)。我們在看Kubernetes源碼時經常會看到類似于//+build linux darwin的包注釋信息,這就是Go語言編譯時的約束條件,其也被稱為條件編譯。
Go語言的條件編譯有兩種定義方法,分別介紹如下。
● 構建標簽:在源碼里添加注釋信息,比如//+build linux,該標簽決定了源碼文件只在Linux平臺上才會被編譯。
● 文件后綴:改變Go語言代碼文件的后綴,比如foo_linux.go,該后綴決定了源碼文件只在Linux平臺上才會被編譯。
另外,go/build工具有幾個重要的類型和方法,其中Context類型指定構建上下文環境,例如GOARCH、GOOS、GOROOT、GOPATH等;Package類型用于描述Go包信息;Import方法導入指定的包,返回該包的Package指針類型,用于收集有關Go包的信息。它們用于處理Go項目目錄結構、源碼、語法、基本操作等。
gengo收集Go包信息可分為兩步:第1步,為生成的代碼文件設置構建標簽;第2步,收集Go包信息并讀取源碼內容。詳細過程如下。
1.為生成的代碼文件設置構建標簽
代碼路徑:vendor/k8s.io/gengo/args/args.go


在Default函數中定義了默認的GeneratedBuildTag字符串,在每次構建時,代碼生成器會將GeneratedBuildTag作為構建標簽打入生成的代碼文件中。每個代碼生成器都會通過Packages功能執行該操作,以deepcopy-gen代碼生成器為例,代碼示例如下:
代碼路徑:vendor/k8s.io/gengo/examples/deepcopy-gen/generators/deepcopy.go

deepcopy-gen代碼生成器中的Packages函數將GeneratedBuildTag字段進行拼接,每一個通過deepcopy-gen代碼生成器生成的代碼文件(如zz_generated.deepcopy.go),第1行總是構建標簽。最后生成代碼的構建標簽如下:

!ignore_autogenerated在Kubernetes中表示該文件是由代碼生成器自動生成的,不需要人工干預或人工編輯該文件。
2.收集Go包信息并讀取源碼內容
代碼路徑:vendor/k8s.io/gengo/args/args.go


代碼生成器通過--input-dirs參數指定傳入的Go包路徑,通過build.Import方法收集Go包的信息,build.Import支持多種模式,其中build.ImportComment用于解析import語句后的注釋信息;build.FindOnly用于查找包所在的目錄,不讀取其中的源碼內容。代碼函數層級為b.AddDir→b.importPackage→b.addDir。代碼示例如下:
代碼路徑:vendor/k8s.io/gengo/parser/parse.go

通過build.Import方法獲得Go包信息以后,就可以得到包下面的所有源碼文件的路徑了,將所有Go源碼內容讀入內存中,等待Lexer詞法解析器的下一步處理,代碼示例如下:


2.7.3 代碼解析
Go語言的優勢在于它是一個靜態類型語言,語法很簡單,與動態類型語言相比更簡單一些。幸運的是,Go語言標準庫支持代碼解析功能,而Kubernetes在該基礎上進行了功能封裝。代碼解析流程可分為3步,gengo代碼解析流程如圖2-6所示。

圖2-6 gengo代碼解析流程
代碼解析流程:第1步,通過標準庫go/tokens提供的Lexer詞法分析器對代碼文本進行詞法分析,最終得到Tokens;第2步,通過標準庫go/parser和go/ast將Tokens構建為抽象語法樹(AST);第3步,通過標準庫go/types下的Check方法進行抽象語法樹類型檢查,完成代碼解析過程。
1.Lexer詞法分析器
Go語言標準庫提供了go/tokens詞法分析器(Lexical Analyzer,簡稱Lexer,也被稱為掃描器)。詞法分析是將字符序列轉換為Tokens(或稱Token序列、單詞序列)的過程。其工作原理是對輸入的代碼文本進行詞法分析,將一個個字符以從左到右的順序讀入,根據構詞規則識別單詞,最終得到Token(單詞)。Token是語言中的最小單位,它可以是變量、函數、運算符或數字。
例如“x*i+1”文本表達式,通過Lexer詞法分析器處理后得到Token序列。Lexer詞法分析器示例如圖2-7所示。

圖2-7 Lexer詞法分析器示例
2.Parse解析器
通過Lexer詞法分析器得到Token序列以后,它將被傳遞給Parser解析器。解析器是編譯器的一個階段,它將Token序列轉換為抽象語法樹(AST,Abstract Syntax Tree)。抽象語法樹也被稱為語法樹(Syntax Tree),是編程語言源碼的抽象語法結構的樹狀表現形式,樹上的每個節點都表示源碼中的一種結構。
抽象語法樹是源碼的結構化表示。在抽象語法樹中,我們能夠看到程序結構,例如函數和常量聲明。可通過Go語言標準庫go/ast打印出完整的抽象語法樹結構。Parse解析器示例如圖2-8所示。

圖2-8 Parse解析器示例
3.Type-Checking類型檢查
通過Parser解析器得到抽象語法樹之后,需要對抽象語法樹中定義和使用的類型進行檢查。對每一個抽象語法樹節點進行遍歷,在每個節點上對當前子樹的類型進行驗證,進而保證不會出現類型錯誤。通過Go語言標準庫go/types下的Check方法進行抽象語法樹檢查。
另外,抽象語法樹一般有多種遍歷方式,比如深度優先搜索(DFS)遍歷和廣度優先搜索(BFS)遍歷等。
4.代碼解析過程實現
通過上面的內容,可以知道實現代碼解析需要通過Lexer詞法分析器、Parser解析器和Type-Checking類型檢查。理解上面的內容后,下面來看看gengo的代碼解析實現,代碼示例如下:
代碼路徑:vendor/k8s.io/gengo/parser/parse.go

首先,通過token.NewFileSet實例化得到token.FileSet對象,該對象用于記錄文件中的偏移量、類型、原始字面量及詞法分析的數據結構和方法等,如圖2-7所示的Lexer詞法分析器示例中,可以看到Token序列數據。得到Tokens后,在addFile函數中,使用parser.ParseFile解析器對Tokens數據進行處理,Parser解析器將傳入兩種標識,其中parser.DeclarationErrors表示報告聲明錯誤,parser.ParseComments表示解析代碼中的注釋并將它們添加到抽象語法樹中。最終得到抽象語法樹結構。
得到抽象語法樹結構后,就可以對其進行類型檢查了,通過Go語言標準庫go/types下的Check方法進行檢查,會對檢查過程進行一些優化,使程序執行得更快,代碼示例如下:

2.7.4 類型系統
gengo的類型系統(Type System)在Go語言本身的類型系統之上歸類并添加了幾種類型。gengo的類型系統在Go語言標準庫go/types的基礎上進行了封裝。
提示:go/types是Go語言程序的類型檢查器,由Robert Griesemer設計。在Go語言1.5版本中,它成為Go語言標準庫的一部分。它也是Go語言標準庫中最復雜的包之一,完全掌握并使用它需要深入了解Go語言程序的結構。
gengo類型系統提供如下類型:
代碼路徑:vendor/k8s.io/gengo/types/types.go


所有的類型都通過vendor/k8s.io/gengo/parser/parse.go的walkType方法進行識別。gengo類型系統中的Struct、Map、Pointer、Interface等,與Go語言提供的類型并無差別。下面介紹一下gengo與Go語言不同的類型,例如Builtin、Alias、DeclarationOf、Unknown、Unsupported及Protobuf。另外,Signature并非是一個類型,它依賴于Func函數類型,用來描述Func函數的接收參數信息和返回值信息等。
1.Builtin(內置類型)
Builtin將多種Base類型歸類成一種類型,以下幾種類型在gengo中統稱為Builtin類型。
● 內置字符串類型——string。
● 內置布爾類型——bool。
● 內置數字類型——int、float、complex64等。
2.Alias(別名類型)
Alias類型是Go 1.9版本中支持的特性,代碼示例如下:

代碼第2行,通過等于(=)符號,基于一個類型創建了一個別名。這里的T2相當于T1的別名。但在Go語言標準庫的reflect(反射)包識別T2的原始類型時,會將它識別為Struct類型,而無法將它識別為Alias類型。原因在于,Alias類型在運行時是不可見的,詳情請參考Go語言官方提議(參見鏈接[2])。
如何讓Alias類型在運行時可被識別呢?答案是因為gengo依賴于go/types的Named類型,所以要讓Alias類型在運行時可被識別,在聲明時將TypeName對象綁定到Named類型即可。
3.DeclarationOf(聲明類型)
DeclarationOf并不是嚴格意義上的類型,它是聲明過的函數、全局變量或常量,但并未被引用過,代碼示例如下:
代碼路徑:pkg/apis/abac/v1beta1/register.go

例如,在register.go中,AddToScheme變量在聲明后未被其他對象引用過,則可以認為它是DeclarationOf類型的。
4.Unknown(未知類型)
當對象匹配不到以上所有類型的時候,它就是Unknown類型的。
5.Unsupported(未支持類型)
當對象屬于Unknown類型時,則會設置該對象為Unsupported類型,并在其使用過程中報錯。
6.Protobuf(Protobuf類型)
由go-to-protobuf代碼生成器單獨處理的類型。
2.7.5 代碼生成
編譯器生成的代碼一般是二進制代碼,而Kubernetes的代碼生成器生成的是Go語言代碼。下面了解一下gengo的Generator接口,接口定義如下:
代碼路徑:vendor/k8s.io/gengo/generator/generator.go

Generator接口字段說明如下。
● Name:代碼生成器的名稱,返回值為生成的目標代碼文件名的前綴,例如deepcopy-gen代碼生成器的目標代碼文件名的前綴為zz_generated.deepcopy。
● Filter:類型過濾器,過濾掉不符合當前代碼生成器所需的類型。
● Namers:命名管理器,支持創建不同類型的名稱。例如,根據類型生成名稱。
● Init:代碼生成器生成代碼之前的初始化操作。
● Finalize:代碼生成器生成代碼之后的收尾操作。
● PackageVars:生成全局變量代碼塊,例如var(…)。
● PackageConsts:生成常量代碼塊,例如consts(…)。
● GenerateType:生成代碼塊。根據傳入的類型生成代碼。
● Imports:獲得需要生成的import代碼塊。通過該方法生成Go語言的import代碼塊,例如import(…)。
● Filename:生成的目標代碼文件的全名,例如deepcopy-gen代碼生成器的目標代碼文件名為zz_generated.deepcopy.go。
● FileType:生成代碼文件的類型,一般為golang,也有protoidl、api-violation等代碼文件類型。
Kubernetes目前提供的每個代碼生成器都可以實現以上方法。如果代碼生成器沒有實現某些方法,則繼承默認代碼生成器(DefaultGen)的方法,DefaultGen定義于vendor/k8s.io/gengo/generator/default_generator.go中。
下面以deepcopy-gen代碼生成器為例,詳細講解其代碼生成原理,執行命令如下:

首先通過build.sh腳本,手動構建deepcopy-gen代碼生成器二進制文件,然后將需要生成的包k8s.io/kubernetes/pkg/apis/abac/v1beta1作為deepcopy-gen的輸入源,并在內部進行一系列解析,最終通過-O參數生成名為zz_generated.deepcopy.go的代碼文件。代碼生成流程如圖2-9所示。

圖2-9 代碼生成流程
下面對代碼生成流程進行詳解。
1.實例化generator.Packages對象
deepcopy-gen代碼生成器根據輸入的包的目錄路徑(即輸入源),實例化generator.Packages對象,根據generator.Packages結構生成代碼,代碼示例如下:
代碼路徑:vendor/k8s.io/gengo/examples/deepcopy-gen/generators/deepcopy.go

在deepcopy-gen代碼生成器的Packages函數中,實例化generator.Packages對象并返回該對象。根據輸入源信息,實例化當前Packages對象的結構:PackageName字段為v1beta1,PackagePath字段為k8s.io/kubernetes/pkg/apis/abac/v1beta1。其中,最主要的是GeneratorFunc定義了Generator接口的實現(即NewGenDeepCopy實現了Generator接口方法)。
2.執行代碼生成
在gengo中,generator定義代碼生成器通用接口Generator。通過ExecutePackage函數,調用不同代碼生成器(如deepcopy-gen)的Generator接口方法,并生成代碼。代碼示例如下:
代碼路徑:vendor/k8s.io/gengo/generator/execute.go


ExecutePackage代碼生成執行流程:生成Header代碼塊→生成Imports代碼塊→生成Vars全局變量代碼塊→生成Consts常量代碼塊→生成Body代碼塊。最后,調用assembler.AssembleFile函數,將生成的代碼塊信息寫入zz_generated.deepcopy.go文件,生成pkg/apis/abac/v1beta1/zz_generated.deepcopy.go代碼結構,代碼結構如圖2-10所示。

圖2-10 代碼結構
deepcopy-gen代碼生成器最終生成了代碼文件zz_generated.deepcopy.go,該文件的整體結構可分為如下部分。
(1)Header代碼塊信息,包括build tag和license boilerplate文件(存放開源軟件作者及開源協議等信息),其中license boilerplate文件可以從hack/boilerplate/boilerplate.go.txt中獲取。
(2)Imports代碼塊信息,引入外部包。
(3)Vars全局變量代碼塊信息,當前代碼文件未使用Vars。
(4)Consts常量代碼塊信息,當前代碼文件未使用Consts。
(5)Body代碼塊信息,生成DeepCopy深復制函數。
在生成代碼的過程中,Filter函數和GenerateType函數非常重要。首先介紹一下Filter函數,deepcopy-gen代碼生成器根據Filter類型過濾器篩選需要生成哪些結構,deepcopy-gen的Filter類型過濾器實現如下:
代碼路徑:vendor/k8s.io/gengo/examples/deepcopy-gen/generators/deepcopy.go


可以看到Filter→copyableType的實現,deepcopy-gen代碼生成器只篩選出了類型為Struct結構的數據(即只為Struct結構的數據生成DeepCopy函數)。
然后介紹GenerateType函數,其根據傳入的類型生成Body代碼塊信息。內部通過Go語言標準庫text/template模板語言渲染出生成的Body代碼塊信息。代碼示例如下:

generator.NewSnippetWriter內部封裝了text/template模板語言,通過將模板應用于數據結構來執行模板。SnippetWriter對象在實例化時傳入模板指令的標識符(即指令開始為$,指令結束為$,有時候也會使用{{}}作為模板指令的標識符)。例如:

SnippetWriter通過Do函數加載模板字符串,并執行渲染模板。模板指令中的點(“.”)表示引用args參數傳遞到模板指令中。模板指令中的(“|”)表示管道符,即把左邊的值傳遞給右邊。
- Facebook Application Development with Graph API Cookbook
- Instant Zepto.js
- Oracle 12c中文版數據庫管理、應用與開發實踐教程 (清華電腦學堂)
- INSTANT Weka How-to
- 薛定宇教授大講堂(卷Ⅳ):MATLAB最優化計算
- 網站構建技術
- Java網絡編程核心技術詳解(視頻微課版)
- Python深度學習原理、算法與案例
- Test-Driven Development with Django
- 軟件工程基礎與實訓教程
- .NET 4.0面向對象編程漫談:應用篇
- 大學計算機應用基礎(Windows 7+Office 2010)(IC3)
- 你好!Java
- Mastering MeteorJS Application Development
- 深度剖析ApacheDubbo核心技術內幕