G-W*_*Wiz 30 c c# compiler-construction clr build-process
我作为C#开发人员度过了我的职业生涯.作为一名学生,我偶尔使用C但没有深入研究它的编译模型.最近我跳上了潮流,开始研究Objective-C.我的第一步只让我意识到我先前存在的知识中的漏洞.
根据我的研究,C/C++/ObjC编译需要预先声明所有遇到的符号.我也理解建筑是一个两步的过程.首先,将每个单独的源文件编译为单个目标文件.这些目标文件可能具有未定义的"符号"(通常对应于头文件中声明的标识符).其次,将目标文件链接在一起以形成最终输出.这是一个非常高级的解释,但它足以满足我的好奇心.但是我也希望对C#构建过程有类似的高级理解.
问: C#构建过程如何解决头文件的需求?我想也许编译步骤可以进行两遍?
(编辑:此处跟进问题)在使用库时C/C++/Objective-C与C#的比较如何?)
Eri*_*ert 94
更新:这个问题是我2010年2月4日博客的主题.谢谢你这个好问题!
让我为你解释一下.在最基本的意义上,编译器是"双程编译器",因为编译器经历的阶段是:
1)生成元数据.2)IL的产生.
元数据是描述代码结构的所有"顶级"内容.命名空间,类,结构,枚举,接口,委托,方法,类型参数,形式参数,构造函数,事件,属性等.基本上,除了方法体之外的一切.
IL是方法体中的所有东西 - 实际的命令式代码,而不是关于代码结构的元数据.
第一阶段实际上是通过源上的大量传递来实现的.它的方式不止两个.
我们要做的第一件事就是获取源文本并将其分解为令牌流.也就是说,我们进行词法分析来确定
class c : b { }
Run Code Online (Sandbox Code Playgroud)
是类,标识符,冒号,标识符,左卷曲,右卷曲.
然后我们进行"顶级解析",我们验证令牌流定义了一个语法正确的C#程序.但是,我们跳过解析方法体.当我们点击一个方法体时,我们只是通过令牌开始,直到我们得到匹配的近似卷曲.我们稍后会回来; 我们只关心获取足够的信息来生成元数据.
然后我们做一个"声明"传递,我们在程序中记下每个命名空间的位置和类型声明.
然后我们执行一个传递,我们验证声明的所有类型在其基类型中没有循环.我们需要首先执行此操作,因为在每个后续传递中,我们都需要能够在不必处理周期的情况下使用类型层次结构.
然后我们做一个传递,我们验证泛型类型的所有泛型参数约束也是非循环的.
然后我们做一个传递,我们检查每个类型的每个成员 - 类的方法,结构的字段,枚举值等 - 是否一致.枚举中没有循环,每个重写方法都会覆盖实际上是虚拟的内容,依此类推.此时,我们可以计算所有接口的"vtable"布局,使用虚方法的类,等等.
然后我们做一个传递,我们计算出所有"const"字段的值.
此时,我们有足够的信息来发出此程序集的几乎所有元数据.我们仍然没有关于迭代器/匿名函数闭包或匿名类型的元数据的信息; 我们做得那么晚.
我们现在可以开始生成IL.对于每个方法体(以及属性,索引器,构造函数等),我们将词法分析器重绕到方法体开始的位置并解析方法体.
一旦解析了方法体,我们就会进行初始的"绑定"传递,我们尝试确定每个语句中每个表达式的类型.然后我们在每个方法体上进行一堆传递.
我们首先运行一个pass来将循环转换为gotos和labels.
(接下来几次传球会找不好的东西.)
然后我们运行一个pass以查找已弃用类型的使用,以获取警告.
然后我们运行一个传递,搜索我们尚未发布元数据的匿名类型的使用,并发出它们.
然后我们运行一个搜索表达式树的不良用途的传递.例如,在表达式树中使用++运算符.
然后我们运行一个传递,查找正文中定义但未使用的所有局部变量来报告警告.
然后我们运行一个传递,在迭代器块中查找非法模式.
然后我们运行可达性检查器,给出关于无法访问的代码的警告,并告诉你什么时候忘记了非void方法结束时的返回.
然后我们运行一个传递,验证每个goto都以一个合理的标签为目标,并且每个标签都以可达到的goto为目标.
然后我们运行一个传递,在使用之前检查所有本地都是明确分配的,注意哪些局部变量是匿名函数或迭代器的外部变量,以及哪些匿名函数在可达代码中.(这个传球做得太多了.我一直想重构它一段时间.)
在这一点上,我们已经完成了寻找不好的东西,但是在我们入睡之前我们还有更多的通行证.
接下来,我们运行一个传递,检测COM对象上的调用缺少ref参数并修复它们.(这是C#4中的新功能.)
然后我们运行一个查找"new MyDelegate(Foo)"形式的东西的传递,并将其重写为对CreateDelegate的调用.
然后我们运行一个传递,将表达式树转换为在运行时创建表达式树所必需的工厂方法调用序列.
然后我们运行一个传递,将所有可空算术重写为测试HasValue的代码,依此类推.
然后我们运行一个传递,找到表单base.Blah()的所有引用,并将它们重写为执行对基类方法的非虚拟调用的代码.
然后我们运行一个查找对象和集合初始值设定项的传递,并将它们转换为适当的属性集,依此类推.
然后我们运行一个查找动态调用的传递(在C#4中)并将它们重写为使用DLR的动态调用站点.
然后我们运行一个查找已删除方法的调用的传递.(即,没有实际实现的部分方法,或者没有定义条件编译符号的条件方法.)这些方法被转换为无操作.
然后我们查找无法访问的代码并将其从树中删除.代码生成IL没有意义.
然后我们运行一个优化传递,重写琐碎的"是"和"as"运算符.
然后我们运行一个优化传递,查找switch(常量)并将其重写为直接分支到正确的case.
然后我们运行一个pass,它将字符串连接转换为对String.Concat的正确重载的调用.
(啊,回忆.最后两次传递是我加入编译团队时我所做的第一件事.)
然后我们运行一个传递,它将命名和可选参数的使用重写为调用,其中副作用都以正确的顺序发生.
然后我们运行一个优化算术的传递; 例如,如果我们知道M()返回一个int,并且我们有1*M(),那么我们只需将其转换为M().
然后我们生成此方法首先使用的匿名类型的代码.
然后我们将本体中的匿名函数转换为闭包类的方法.
最后,我们将迭代器块转换为基于交换机的状态机.
然后我们为我们刚刚计算出的变换树发出IL.
非常简单!
ang*_*son 39
我看到这个问题有多种解释.我回答了解决方案内的解释,但让我用我知道的所有信息填写.
"头文件元数据"存在于已编译的程序集中,因此您添加引用的任何程序集都将允许编译器从这些程序集中提取元数据.
至于尚未编译的东西,当前解决方案的一部分,它将进行两遍编译,首先读取命名空间,类型名称,成员名称,即.除了代码之外的一切.然后当它检出时,它将读取代码并编译它.
这允许编译器知道存在什么和不存在什么(在其Universe中).
要查看有效的双程编译器,请测试以下具有3个问题的代码,两个与声明相关的问题和一个代码问题:
using System;
namespace ConsoleApplication11
{
class Program
{
public static Stringg ReturnsTheWrongType()
{
return null;
}
static void Main(string[] args)
{
CallSomeMethodThatDoesntExist();
}
public static Stringg AlsoReturnsTheWrongType()
{
return null;
}
}
}
Run Code Online (Sandbox Code Playgroud)
请注意,编译器只会抱怨Stringg它找不到的两种类型.如果你修复了那些,那么它会抱怨在Main方法中调用的方法名称,它无法找到.
它使用参考程序集中的元数据.它包含一个完整的类型声明,与您在头文件中找到的相同.
它是一个双向编译器完成其他任务:您可以在一个源文件中使用一个类型,然后再在另一个源代码文件中声明它.