05 编译器驱动与宿主环境
本册目标:回到最外层——csc.exe 的
Main做了什么、命令行如何被解析成CSharpCompilation、CompileAndEmit的分阶段驱动逻辑、VBCSCompiler 共享编译服务器如何工作,以及 MSBuild/MonoDevelop 如何把这一切挂到 IDE 构建里(与《MonoDevelop用户程序编译器调用链分析.md》衔接)。
一、要点速览
- csc 的入口只有 39 行:
Program.Main→DesktopBuildClient.Run(args, RequestLanguage.CSharpCompile, Csc.Run, loader)——先问编译服务器,不行再本地跑。 - 真正的驱动类是
Csc : CSharpCompiler(Shared/Csc.cs),公共流程在CommonCompiler.RunCore:Parse → Declare → Emit 三阶段取诊断、每阶段错误即短路。 - 命令行解析分两层:语言无关
CommonCommandLineParser+ C# 特化CSharpCommandLineParser。 - VBCSCompiler(
Compilers/Server/VBCSCompiler/)通过命名管道接收编译请求,跨进程缓存源文件/引用元数据/分析器,是 IDE 构建提速的关键。 - 宿主侧(MSBuild
Csc任务 / MonoDevelop)通过$(RoslynTargetsPath)定位编译器与任务程序集,通过UseSharedCompilation决定是否走服务器。
二、csc 入口:薄壳 + 双模式
src/Compilers/CSharp/csc/Program.cs全文只有 39 行,核心一行(L33):
returnDesktopBuildClient.Run(args,RequestLanguage.CSharpCompile,Csc.Run,loader);DesktopBuildClient(src/Compilers/Shared/DesktopBuildClient.cs)的行为:
- 解析出编译请求(语言、参数、临时目录);
- 尝试连接共享编译服务器 VBCSCompiler(命名管道);
- 连上→ 把请求发给服务器执行(服务器进程常驻,跨请求复用缓存);
- 连不上/不允许(
/shared:false)→ 回退为本地进程内执行,调用Csc.Run。
Csc(src/Compilers/Shared/Csc.cs:10):
internalsealedclassCsc:CSharpCompilerCSharpCompiler(CSharp/Portable/CommandLine/CSharpCompiler.cs)负责把命令行参数变成编译对象(见 §三),公共驱动逻辑则在语言无关的CommonCompiler(Core/Portable/CommandLine/CommonCompiler.cs)——vbc 与 csc 共享同一套流程,只是子类各自提供语言特化。
三、命令行解析与 Compilation 的构造
3.1 两层解析器
| 文件 | 职责 |
|---|---|
Core/Portable/CommandLine/CommonCommandLineParser.cs | 通用参数:/out、/r:、/define、/nologo、响应文件(@) 展开 |
CSharp/Portable/CommandLine/CSharpCommandLineParser.cs | C# 特化:/langversion、/checked、/unsafe、/nullable 等 |
| 产物类 | CommonCommandLineArguments+CSharpCommandLineArguments(含CompilationOptions、ParseOptions、EmitOptions、源文件/引用列表) |
3.2 CSharpCompiler.CreateCompilation
CSharpCompiler.cs:36的CreateCompilation最终调用(CSharpCompiler.cs:142):
returnCSharpCompilation.Create(Arguments.CompilationName,trees.WhereNotNull(),// 每个源文件已在 ParseFile 中解析成 SyntaxTreeresolvedReferences,// 命令行 /r: + #r 指令合并解析后的 MetadataReferenceArguments.CompilationOptions.WithMetadataReferenceResolver(referenceDirectiveResolver).WithAssemblyIdentityComparer(assemblyIdentityComparer).WithXmlReferenceResolver(xmlFileResolver).WithStrongNameProvider(...).WithSourceReferenceResolver(sourceFileResolver));注意选项即对象:路径解析、强名、doc 注释等行为全部以"解析器对象"注入CompilationOptions——这是 Roslyn 可宿主化(在任意宿主里重新定义文件系统行为)的基础。
四、CommonCompiler.RunCore:分阶段驱动
Core/Portable/CommandLine/CommonCompiler.cs:526RunCore→:633 CompileAndEmit。分阶段取诊断、有错即停(省掉无谓的后续阶段计算):
RunCore(CommonCompiler.cs:526) └─ CompileAndEmit(:633) ├─ compilation.GetDiagnostics(CompilationStage.Parse, ...) // L650 语法错误即停 ├─ (可选)AnalyzerDriver.CreateAndAttachToCompilation // L664 挂分析器 ├─ compilation.GetDiagnostics(CompilationStage.Declare, ...) // L676 声明错误即停 ├─ 确定输出名/PE/PDB/XML 路径 // L684-696 ├─ compilation.Emit(peStream, pdbStream, xmlDocStream, ...) // 04 册的发射全链 └─ 输出剩余诊断(警告)、写 /errorendoutput、返回退出码CompilationStage(Core/Portable/Compilation/CompilationStage.cs)把 01 册的五阶段归并为 Parse/Declare/Emit 三个可停机检查点——这也解释了CSharpCompilation.GetDiagnostics(stage)的语义:按阶段拿诊断,而发射只在最后发生。
五、VBCSCompiler:共享编译服务器
5.1 为什么需要它
命令行每次csc都要:加载所有引用程序集的元数据 → 解析全部源文件 → JIT 编译器自身。IDE 里每次按键触发的隐藏编译(诊断计算)如果都这样跑,开销不可接受。服务器化带来三个跨请求缓存:
| 缓存 | 位置 | 内容 |
|---|---|---|
| 源文本缓存 | Server/VBCSCompiler/请求处理层 | 相同文件不重复读盘/解码 |
| 元数据缓存 | Server/VBCSCompiler/MetadataCache.cs | 引用 dll 的 MetadataReader 复用 |
| 分析器缓存 | Server/VBCSCompiler/AnalyzerConsistencyChecker.cs | 分析器程序集复用 + 一致性校验 |
5.2 结构速览(src/Compilers/Server/VBCSCompiler/)
| 文件 | 职责 |
|---|---|
VBCSCompiler.cs | 服务器进程 Main |
ServerDispatcher.cs | 连接分派循环、空闲关机计时 |
Connection.cs/NamedPipeClientConnection.cs | 命名管道协议(请求/响应帧) |
CompilerRequestHandler.cs | 单个编译请求的执行 |
CSharpCompilerServer.cs/VisualBasicCompilerServer.cs | 语言特化的服务器端编译器 |
BuildProtocolUtil.cs | 客户端-服务器的请求编码 |
协议要点:客户端(DesktopBuildClient)把命令行参数、工作目录、临时目录编码进请求;服务器用它重建编译并返回输出流与退出码。管道名含用户与共享键,因此同机多用户/多构建互不干扰。
5.3 与 MSBuild 的对接(宿主侧实证)
Mono 的 MSBuild(/usr/lib/mono/msbuild/Current/bin/,本工作区mono-install下同构):
Microsoft.Common.tasks:182:<UsingTask TaskName="Microsoft.CodeAnalysis.BuildTasks.Csc" AssemblyFile="$(RoslynTargetsPath)\Microsoft.Build.Tasks.CodeAnalysis.dll" Condition="'$(MSBuildAssemblyVersion)' != ''" />Roslyn/Microsoft.Managed.Core.targets:48-49:UseSharedCompilation默认true;Roslyn/Microsoft.CSharp.Core.targets:140:Csc任务把UseSharedCompilation="$(UseSharedCompilation)"传入。
即:MSBuild → Csc 任务(Microsoft.Build.Tasks.CodeAnalysis.dll)→ BuildClient → VBCSCompiler/csc。MonoDevelop 侧则由RemoteBuildEngineManager.GetCoreGlobalProperties(monodevelop/main/src/core/MonoDevelop.Core/MonoDevelop.Projects.MSBuild/RemoteBuildEngineManager.cs:419)把RoslynTargetsPath作为全局属性传给构建器,决定用哪套 Roslyn。完整调用链见《MonoDevelop用户程序编译器调用链分析.md》。
5.4 语言版本机制
Roslyn/Microsoft.CSharp.Core.targets中的_MaxSupportedLangVersion按目标框架推导(net47→7.3,.NET Core 3.0→8.0,5.0→9.0),随后Csc任务把/langversion:传给编译器;编译器侧由CSharpCommandLineParser解析成LanguageVersion,作用于CSharpParseOptions——词法/语法阶段就按版本裁剪(02 册实验可见)。
六、实验:观察驱动行为
# 1) 直接跑 csc(本地模式),观察退出码与输出mono<RoslynDir>/csc.exe /nologo /t:library /out:a.dll a.cs;echoexit=$?# 2) 共享编译:确认服务器进程被拉起(MSBuild 构建任意项目后)ps-ef|grepVBCSCompiler# 观察 -pipename: 参数# 3) 关闭共享编译对比(MSBuild 日志里 Csc 命令行不同)/usr/bin/msbuild xxx.csproj /p:UseSharedCompilation=false /v:normal# 4) 分阶段诊断:故意写一个语法错误,确认只有 Parse 阶段错误即停,# 再写一个语义错误(拼错类型名),确认能走到 Declare 阶段的诊断七、代码位置索引
| 内容 | 位置(相对roslyn-src/src/) |
|---|---|
csc 入口Main/Run | Compilers/CSharp/csc/Program.cs:33/36-37 |
| 客户端(先服务器后本地) | Compilers/Shared/DesktopBuildClient.cs、Compilers/Shared/BuildClient.cs |
| csc 驱动类 | Compilers/Shared/Csc.cs:10(class Csc : CSharpCompiler) |
| C# 命令行解析 | Compilers/CSharp/Portable/CommandLine/CSharpCommandLineParser.cs |
| Compilation 构造 | Compilers/CSharp/Portable/CommandLine/CSharpCompiler.cs:36(CreateCompilation,CSharpCompilation.Create调用:142) |
公共驱动RunCore/CompileAndEmit | Compilers/Core/Portable/CommandLine/CommonCompiler.cs:526/:633(阶段检查:650/:676) |
| 阶段枚举 | Compilers/Core/Portable/Compilation/CompilationStage.cs |
| 编译服务器 | Compilers/Server/VBCSCompiler/(VBCSCompiler.cs、ServerDispatcher.cs、CompilerRequestHandler.cs、MetadataCache.cs) |
| 宿主:UsingTask/共享编译默认值 | <mono>/lib/mono/msbuild/Current/bin/Microsoft.Common.tasks:182、Roslyn/Microsoft.Managed.Core.targets:48 |
| 宿主:MonoDevelop 传 RoslynTargetsPath | monodevelop/main/src/core/MonoDevelop.Core/MonoDevelop.Projects.MSBuild/RemoteBuildEngineManager.cs:419 |
下一册:06-动手实践与源码修改指南.md——开始改造你自己的编译器。