# 引言 最近做一個功能想要動態執行C#腳本,就是預先寫好代碼片段,在程式運行時去執行代碼段,比如像這樣(以下代碼為偽代碼): ```csharp string scriptText = "int a = 1;int b = 2; return a+b ;"; var result = Script ...
引言
最近做一個功能想要動態執行C#腳本,就是預先寫好代碼片段,在程式運行時去執行代碼段,比如像這樣(以下代碼為偽代碼):
string scriptText = "int a = 1;int b = 2; return a+b ;";
var result = Script.Run(scriptText);
查閱了一些資料,發現 .Net的開源編譯器平臺 - Roslyn,可以支持這樣的功能。
其實 Roslyn 提供了很多強大的功能,比如:
- 提供了一組豐富的 API,允許開發人員在運行時動態地生成、編譯和執行代碼。這些 API 分為兩類:編譯 API 和工作空間 API。編譯 API 用於分析和生成代碼,工作空間 API 用於與集成開發環境(IDE)進行交互。通過這些 API,開發人員可以構建強大的代碼分析和重構工具。
- 支持對源代碼進行靜態分析,以便在編譯期間檢測潛在的代碼問題。也支持編寫自定義診斷和代碼修複,這使得開發人員可以根據自己的需求創建特定的診斷和修複工具。
- Roslyn 支持 C# 和 VB.NET 兩種編程語言。它提供了一組通用 API,這樣兩種語言之間共用代碼就變得容易。
- Roslyn 與 Visual Studio、Visual Studio Code 和其他支持 C# 和 VB.NET 的 IDE 集成很好。
Roslyn概述
因為現在需要它的動態編譯,動態執行代碼的功能,所以先仔細瞭解一下,看一下它的官方概述(https://github.com/dotnet/roslyn/blob/main/docs/wiki/Roslyn-Overview.md)
因為官方概述是英文版,所以我將他翻譯為了中文:
概述內容包括:
- 介紹
- 公開的編譯器API
- 編譯器流水線功能區域(Compiler Pipeline Functional Areas)
- API層
- 編譯器 API(Compiler APIs)
- 診斷 API(Diagnostic APIs)
- 腳本 API (Scripting APIs)
- 工作區 API(Workspaces APIs)
- 使用語法
- 語法樹(Syntax Trees)
- 語法節點(Syntax Nodes)
- 語法標記(Syntax Token)
- 語法瑣事(Syntax Trivia)
- 跨度(Spans)
- 種類(Kinds)
- 錯誤(Error)
- 使用語義
- 彙編(Compilation)
- 符號(Symbols)
- 語義模型(Semantic Model)
- 使用工作區
- 工作區(Workspace)
- 解決方案,項目和文檔(Solutions, Projects and Documents)
介紹
傳統上來說,編譯器就是黑盒 -- 源代碼進入一端,經過一些神奇的過程,然後輸出目標文件或者彙編代碼。
編譯器會對代碼進行深入的理解,但這些知識只有編譯器實現者才能使用。然而,現在我們越來越多地依賴於集成開發環境(IDE)的功能,如智能提示、重構、智能重命名、查找引用和轉到定義等,以提高工作效率。我們還使用代碼分析工具來改善代碼質量,使用代碼生成工具來輔助構建應用程式。
隨著這些工具變得越來越智能,它們需要訪問編譯器所具有的深層代碼知識。這就是 Roslyn的核心任務:打開這些黑盒子,讓工具和終端用戶能夠分享編譯器對代碼的豐富信息。
通過Roslyn,編譯器成為一個平臺,提供API供工具和應用程式使用,而不僅僅是將源代碼翻譯為目標代碼的工具。這種過渡降低了創建面向代碼的工具和應用程式的門檻,為元編程、代碼生成和轉換、互動式使用C#和VB語言以及將C#和VB嵌入領域特定語言等領域的創新提供了機會。
Roslyn SDK預覽版包含了用於代碼生成、分析和重構的最新語言對象模型的草案。
我們希望在未來的預覽版中包含用於腳本編寫和交互使用C#和Visual Basic的API支持的草案。本文提供了Roslyn的概念概述。更多細節可以在SDK預覽版中的演練和示例中找到。
公開的編譯器API
編譯器流水線功能區域(Compiler Pipeline Functional Areas)
Roslyn通過提供與傳統編譯器流水線相對應的API層,將C#和Visual Basic編譯器的代碼分析暴露給開發者作為使用者。
該流水線的每個階段現在都是一個單獨的組件。首先是解析階段,源代碼被標記化並解析為符合語言語法的語法結構。其次是聲明階段,對源代碼和導入的元數據進行分析,形成命名符號。接下來是綁定階段,將代碼中的標識符與符號進行匹配。最後是發出階段,編譯器構建的所有信息作為一個程式集進行輸出。
針對每個階段,都有一個相應的對象模型,允許訪問該階段的信息。解析階段以語法樹的形式暴露,聲明階段以層次化符號表的形式暴露,綁定階段以顯示編譯器語義分析結果的模型形式暴露,發出階段以生成IL位元組碼的API形式暴露。
編譯器將這些組件組合為一個單一的端到端整體。
為了確保公開的編譯器API足以構建世界一流的IDE功能,將使用這些API重建用於支持Visual Studio vNext中的C#和VB體驗的語言服務。
例如,代碼大綱和格式化功能使用語法樹,對象瀏覽器和導航功能使用符號表,重構和轉到定義使用語義模型,編輯和繼續使用所有這些功能,包括發出API。
這些體驗可以在Visual Studio 2013上通過“Roslyn”終端用戶預覽版中預覽。這個預覽版是為了構建和測試基於Roslyn SDK的應用程式,並用於集成到Visual Studio中。但是,不需要終端用戶預覽版,可以獨立於Visual Studio在自己的應用程式中使用Roslyn API。
API 層
Roslyn由兩個主要的API層組成——編譯器API和工作區API。
編譯器 API(Compiler APIs)
編譯器層包含與編譯器流水線的每個階段對應的對象模型,包括語法和語義信息。編譯器層還包含編譯器單次調用的不可變快照,包括程式集引用、編譯器選項和源代碼文件。
C#語言和Visual Basic語言有兩個不同的API表示。這兩個API在形式上類似,但為每種語言進行了高保真度的定製。
該層不依賴於Visual Studio組件。
診斷 API(Diagnostic APIs)
作為分析的一部分,編譯器可以生成一組診斷信息,涵蓋從語法、語義和明確賦值錯誤到各種警告和信息性診斷的所有內容。
編譯器API層通過可擴展的API公開診斷信息,允許用戶定義的分析器插入到編譯中,並產生用戶定義的診斷,例如由StyleCop或FxCop等工具生成的診斷,與編譯器定義的診斷一起產生。
以這種方式生成診斷信息的好處是與諸如MSBuild和Visual Studio等工具自然集成,這些工具依賴於診斷信息,用於諸如基於策略停止構建、在編輯器中顯示實時波浪線和建議代碼修複等功能。
腳本 API (Scripting APIs)
作為編譯器層的一部分,團隊創建了用於執行代碼片段和累積運行時執行上下文的托管/腳本API。REPL(互動式編程環境)使用這些API。
工作區 API(Workspaces APIs)
工作區層包含Workspace API,用於對整個解決方案進行代碼分析和重構的起點。它有助於將解決方案中的所有項目的信息組織成單個對象模型,並直接訪問編譯器層的對象模型,無需解析文件、配置選項或管理項目之間的依賴關係。
此外,工作區層還提供一組常用的API,用於在類似Visual Studio IDE的宿主環境中實現代碼分析和重構工具,例如“查找所有引用”、“格式化”和“代碼生成”等API。
該層不依賴於Visual Studio組件。
使用語法
編譯器API公開的最基本數據結構是語法樹。這些樹表示源代碼的詞法和語法結構。它們具有兩個重要目的:
- 允許工具(如集成開發環境、插件、代碼分析工具和重構工具)查看和處理用戶項目中源代碼的語法結構。
- 可以讓工具(如重構工具和集成開發環境)以自然的方式創建、修改和重新排列源代碼,而無需直接進行文本編輯。通過創建和操作語法樹,工具可以輕鬆地創建和重新排列源代碼。
語法樹(Syntax Trees)
語法樹是用於編譯、代碼分析、綁定、重構、集成開發環境功能和代碼生成的主要結構。沒有將源代碼首先識別和分類為眾多已知結構化語言元素之一,就無法理解源代碼的任何部分。
語法樹具有三個關鍵屬性。第一個屬性是語法樹以完全保真度保存所有的源信息。這意味著語法樹包含源文本中的每個信息片段,每個語法構造,每個詞法標記,以及包括空格、註釋和預處理指令在內的其他內容。例如,源代碼中提到的每個字面值都會按照其輸入方式進行精確表示。當程式不完整或格式錯誤時,語法樹還會表示源代碼中的錯誤,通過在語法樹中表示被跳過或缺失的標記。
這使得語法樹具有第二個屬性。從解析器獲取的語法樹完全可逆地回到其解析的文本。從任何語法節點,都可以獲取以該節點為根的子樹的文本表示。這意味著語法樹可以用作構建和編輯源代碼的一種方式。通過創建一個樹,實際上已經創建了等效的文本;通過編輯語法樹,從對現有樹的更改創建新的樹,實際上是編輯了文本。
語法樹的第三個屬性是它們是不可變且線程安全的。這意味著一旦獲取了一個樹,它就是代碼當前狀態的快照,並且永遠不會改變。這允許多個用戶在不同線程中同時與相同的語法樹交互,而無需進行鎖定或複製。由於樹是不可變的,不能直接對樹進行修改,工廠方法通過創建樹的其他快照來幫助創建和修改語法樹。語法樹在重用底層節點方面非常高效,因此可以快速重建新版本,並且占用很少的額外記憶體。
語法樹實際上是一種樹形數據結構,其中非終結結構元素作為父節點包含其他元素。每個語法樹由節點、標記和文本附加信息組成。
語法節點(Syntax Nodes)
語法節點是語法樹的主要元素之一。這些節點表示語法構造,例如聲明、語句、子句和表達式。每個語法節點類別由一個派生自 SyntaxNode
的單獨類表示。節點類的集合不可擴展。
所有的語法節點都是語法樹中的非終結節點,這意味著它們始終有其他節點和標記作為子節點。作為另一個節點的子節點,每個節點都有一個可以通過 Parent
屬性訪問的父節點。由於節點和樹是不可變的,節點的父節點永遠不會改變。樹的根節點具有空的父節點。
每個節點都有一個 ChildNodes
方法,它返回一個基於節點在源代碼中的位置的順序列表,包含的是子節點,不包含標記。每個節點還有一組 Descendant
方法,如 DescendantNodes
、DescendantTokens
或 DescendantTrivia
,表示根據該節點為根的子樹中存在的所有節點、標記或附加信息的列表。
此外,每個語法節點子類通過強類型屬性公開相同的子節點。例如,BinaryExpressionSyntax
節點類具有三個特定於二元運算符的附加屬性:Left
、OperatorToken和Right
。Left
和 Right
的類型是 ExpressionSyntax
,OperatorToken
的類型是 SyntaxToken
。
某些語法節點具有可選的子節點。例如,IfStatementSyntax
具有可選的 ElseClauseSyntax
。如果子節點不存在,該屬性將返回 null
。
語法標記(Syntax Token)
語法標記是語言語法的終結符,表示代碼的最小語法片段。它們永遠不是其他節點或標記的父節點。語法標記由關鍵字、標識符、文字和標點符號組成。
為了提高效率,SyntaxToken
類型是CLR值類型。因此,與語法節點不同,只有一個結構用於表示所有類型的標記,其中包含根據所表示的標記類型具有不同含義的屬性組合。
例如,整數文字標記表示一個數值。除了標記跨越的原始源文本之外,文字標記還有一個 Value
屬性,告訴您精確的解碼整數值。由於該屬性可能是多個基本類型之一,因此它的類型為 Object
。
ValueText
屬性提供與 Value
屬性相同的信息;但是,該屬性的類型始終為 String
。在C#源文本中,標識符可能包括 Unicode
轉義字元,但轉義序列本身的語法不被視為標識符名稱的一部分。因此,儘管標記跨越的原始文本包含轉義序列,但 ValueText
屬性不包含它。相反,它包括由轉義所表示的 Unicode
字元。
語法瑣事(Syntax Trivia)
語法註釋表示源文本中對於正常理解代碼而言主要是無關緊要的部分,例如空格、註釋和預處理指令。
由於註釋不是正常語言語法的一部分,並且可以出現在任何兩個標記之間的任何位置,所以它們不作為節點的子節點包含在語法樹中。然而,由於在實現諸如重構等功能時它們很重要,並且為了與源文本保持完全一致,它們確實作為語法樹的一部分存在。
您可以通過檢查標記的 LeadingTrivia
或 TrailingTrivia
集合來訪問註釋。在解析源文本時,註釋序列與標記關聯起來。通常情況下,一個標記擁有在同一行上緊隨其後的所有註釋,直到下一個標記為止。在該行之後的任何註釋與下一個標記關聯。源文件中的第一個標記獲取所有初始註釋,而文件中最後一個註釋序列附加到文件結束標記上,否則文件結束標記的寬度為零。
與語法節點和標記不同,語法註釋沒有父節點。然而,由於它們是樹的一部分,並且每個註釋都與單個標記關聯,您可以使用 Token
屬性訪問與之關聯的標記。
與語法標記一樣,註釋是值類型。單個 SyntaxTrivia
類型用於描述各種註釋。
跨度(Spans)
每個節點、標記或註釋都知道它在源文本中的位置以及它所包含的字元數。文本位置表示為一個32位整數,它是基於零的 Unicode
字元索引。TextSpan
對象由起始位置和字元數兩個整數表示。如果 TextSpan
的長度為零,它表示兩個字元之間的位置。
每個節點都有兩個 TextSpan
屬性:Span
和 FullSpan
。
Span屬性是從節點子樹中第一個標記的起始位置到最後一個標記的結束位置的文本跨度。這個跨度不包括任何前導或尾隨註釋。
FullSpan屬性是包括節點正常跨度以及任何前導或尾隨註釋的文本跨度。
如下代碼段:
if (x > 3)
{
|| // this is bad
|throw new Exception("Not right.");|
// better exception?
||
}
在塊內的語句節點具有由單豎線(|)表示的跨度。它包括字元"throw new Exception(“Not right.”);"
。完整跨度由雙豎線(||)表示。它包括與跨度相同的字元以及與前導和尾隨註釋相關聯的字元。
種類(Kinds)
每個節點、標記或註釋都有一個 RawKind
屬性,類型為 System.Int32
,用於標識表示的確切語法元素。該值可以轉換為特定於語言的枚舉;每種語言,C#或VB,都有一個單獨的 SyntaxKind
枚舉,列出了語法中所有可能的節點、標記和註釋元素。可以通過訪問 CSharpSyntaxKind()
或 VisualBasicSyntaxKind()
擴展方法來自動進行這種轉換。
RawKind
屬性可以輕鬆區分共用同一節點類的語法節點類型。對於標記和註釋,這個屬性是區分一個元素與另一個元素的唯一方式。
例如,一個 BinaryExpressionSyntax
類具有 Left
、OperatorToken
和 Right
作為子節點。Kind
屬性區分是 AddExpression
、SubtractExpression
還是 MultiplyExpression
類型的語法節點。
錯誤(Error)
即使源代碼包含語法錯誤,也會生成一個完整的語法樹,可以迴圈轉換回源代碼。當解析器遇到不符合語言定義語法的代碼時,它會使用兩種技術之一來創建語法樹。
首先,如果解析器期望某種類型的標記,但沒有找到它,它可以在預期的位置將一個缺失的標記插入到語法樹中。缺失的標記表示實際期望的標記,但它的範圍為空,它的 IsMissing
屬性返回 true
。
其次,解析器可能會跳過標記,直到找到可以繼續解析的標記為止。在這種情況下,被跳過的標記將作為一個帶有 SkippedTokens
類型的註釋節點附加到語法樹中。
使用語義
語法樹代表源代碼的詞法和語法結構。儘管僅憑這些信息就足以描述源代碼中的所有聲明和邏輯,但它並不足以確定正在引用的內容。
例如,許多具有相同名稱的類型、欄位、方法和局部變數可能分散在源代碼中。儘管每個標識符都是唯一不同的,但確定它實際引用的內容通常需要對語言規則有深入的瞭解。
源代碼中有表示程式元素的部分,程式也可以引用先前編譯的庫,這些庫打包在程式集文件中。雖然程式集沒有可用的源代碼,因此沒有語法節點或語法樹,但程式仍然可以引用其中的元素。
除了源代碼的語法模型外,語義模型還封裝了語言規則,使您可以輕鬆區分這些元素。
彙編(Compilation)
編譯是用於編譯C#或Visual Basic程式的一切所需的表示,其中包括所有的程式集引用、編譯器選項和源文件。
由於所有這些信息都在一個地方,因此可以更詳細地描述源代碼中包含的元素。編譯將每個聲明的類型、成員或變數表示為符號。編譯包含各種方法,可幫助您查找和關聯在源代碼中聲明的符號或從程式集中作為元數據導入的符號。
與語法樹類似,編譯是不可變的。創建編譯之後,您或其他人都無法對其進行更改。但是,您可以從現有編譯創建一個新的編譯,同時指定所做的更改。例如,您可以創建一個與現有編譯在所有方面都相同的編譯,只是可能包含一個額外的源文件或程式集引用。
符號(Symbols)
符號代表源代碼聲明的獨立元素或作為元數據從程式集導入的元素。每個命名空間、類型、方法、屬性、欄位、事件、參數或局部變數都由一個符號表示。
Compilation
類型上的各種方法和屬性幫助您查找符號。例如,您可以通過其常見的元數據名稱查找已聲明類型的符號。您還可以將整個符號表作為以全局命名空間為根的符號樹進行訪問。
符號還包含了編譯器從源代碼或元數據中確定的其他信息,例如其他引用的符號。每種符號類型都由從 ISymbol 派生的單獨介面表示,每個介面都具有自己的方法和屬性,詳細描述了編譯器收集的信息。許多這些屬性直接引用其他符號。例如,IMethodSymbol
類的 ReturnType
屬性告訴您方法聲明引用的實際類型符號。
符號在源代碼和元數據之間提供了命名空間、類型和成員的共同表示。例如,源代碼中聲明的方法和從元數據導入的方法都由具有相同屬性的 IMethodSymbol 表示。
符號在概念上類似於由 System.Reflection
API 表示的 CLR 類型系統,但它們更豐富,因為它們建模的不僅僅是類型。命名空間、局部變數和標簽都是符號。此外,符號是語言概念的表示,而不是 CLR 概念。它們有很多重疊之處,但也有許多有意義的區別。例如,C# 或 Visual Basic 中的迭代器方法是一個單一的符號。然而,當迭代器方法被翻譯為 CLR 元數據時,它是一個類型和多個方法。
語義模型(Semantic Model)
語義模型表示單個源文件的所有語義信息。您可以使用它來發現以下內容:
- 源代碼中特定位置引用的符號。
- 任何表達式的結果類型。
- 所有診斷信息,包括錯誤和警告。
- 變數在源代碼區域中的流動情況。
- 更加推測性問題的答案。
使用工作區
工作區層是對整個解決方案進行代碼分析和重構的起點。在該層中,工作區 API 幫助您將解決方案中所有項目的信息組織成單一的對象模型,為您提供直接訪問編譯器層對象模型(如源代碼文本、語法樹、語義模型和編譯)的能力,無需解析文件、配置選項或管理項目間的依賴關係。
像集成開發環境(IDE)這樣的宿主環境會為您提供與打開的解決方案相對應的工作區。此外,也可以通過簡單地載入解決方案文件在IDE之外使用這個模型。
工作區(Workspace)
工作區是將解決方案表示為項目集合的活動表示形式,每個項目都包含一組文檔。工作區通常與宿主環境綁定在一起,宿主環境會隨用戶的輸入或屬性操作而不斷變化。
工作區提供對解決方案的當前模型的訪問。當宿主環境發生變化時,工作區會觸發相應的事件,並更新CurrentSolution屬性。例如,當用戶在與源代碼文檔對應的文本編輯器中輸入時,工作區使用事件發出信號,表示解決方案的整體模型已經發生了變化,同時指明哪個文檔被修改。您可以通過分析新模型的正確性、突出顯示重要區域或提出代碼更改建議來對這些變化做出反應。
您還可以創建獨立的工作區,與宿主環境分離或在沒有宿主環境的應用程式中使用。
解決方案,項目和文檔(Solutions, Projects and Documents)
儘管工作區在按鍵時可能會發生變化,但您可以與解決方案模型獨立地進行操作。
解決方案是項目和文檔的不可變模型。這意味著可以共用該模型而無需鎖定或複製。一旦您從工作區的CurrentSolution屬性獲取解決方案實例,該實例將不會發生更改。然而,與語法樹和編譯類似,您可以通過基於現有解決方案和特定更改構建新實例來修改解決方案。要使工作區反映您的更改,必須顯式將更改後的解決方案應用回工作區。
項目是整體不可變解決方案模型的一部分。它代表所有源代碼文檔、解析和編譯選項以及程式集和項目之間的引用。通過項目,您可以訪問相應的編譯,而無需確定項目依賴項或解析任何源文件。
文檔也是整體不可變解決方案模型的一部分。文檔表示單個源文件,您可以從中訪問文件的文本、語法樹和語義模型。
以下圖表顯示了工作區與宿主環境、工具之間的關係以及如何進行編輯。
總結
Roslyn 提供了一套編譯器 API 和工作區 API,可以提供有關您的源代碼的豐富信息,並與 C# 和 Visual Basic 語言完全相容。將編譯器作為平臺的轉變極大降低了創建以代碼為重點的工具和應用程式的門檻。它在元編程、代碼生成和轉換、C# 和 VB 語言的交互使用以及將 C# 和 VB 嵌入領域特定語言等領域創造了許多創新機會。
參考
https://github.com/dotnet/roslyn/blob/main/docs/wiki/Roslyn-Overview.md
作者: Peter.Pan
出處: https://www.cnblogs.com/pandefu/>
關於作者:.Net Framework,.Net Core ,WindowsForm,WPF ,控制項庫,多線程
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出 原文鏈接,否則保留追究法律責任的權利。 如有問題, 可郵件咨詢。