65.9K
CodeProject 正在变化。 阅读更多。
Home

黑客攻击 Mono C# 编译器。

starIconstarIconstarIconstarIconstarIcon

5.00/5 (11投票s)

2010年10月6日

CPOL

7分钟阅读

viewsIcon

59329

downloadIcon

1479

描述如何从 C# 解析树转储信息。

关键词 

C# 解析树,C# 编译器,mono

下载 hacking-mono-csharp-compiler.zip - 924.51 KB

重要更新

原始文章描述了如何获取 Mono C# 编译器生成的 C# 代码的解析树。不幸的是,Mono C# 编译器代码太难使用了。幸运的是,另一个项目 CSharpDevelop (http://www.icsharpcode.net/OpenSource/SD/Default.aspx) 提供了更好的 C# 解析解决方案。我尝试的项目位于

SharpDevelop_4.0.0.6721_Beta3_Source\src\Libraries\NRefactory\NRefactory.sln

只需删除一个名为 GlobalAssembyInfo.cs 的文件并编译解决方案。将 NRefactoryDemo 项目设置为启动项目。请参阅截图。我还需要更多工作来比较 CSharpDevelop 解决方案和 Mono 解决方案。

 nrefactorydemo.jpg

什么是 C# Mono 编译器?  

Mono 项目提供了开源的 C# 编译器,以及其他软件,如 .Net 基本类库实现、.Net 虚拟机等。事实上,Mono 提供了多种编译器:gmc、dmc 分别用于 C# 的 3.0 和 4.0 版本。这些编译器源自同一源代码,但以不同的配置进行编译。Mono 编译器相对完整,很可能可以解析实践中遇到的绝大多数 C# 源代码。

用例   

开源 C# 编译器在多种场景下都很有用。其中一类大型应用程序是源代码分析。这类应用程序包括 IntelliSense、代码度量和源代码分析应用程序。C# 编译器还可以用于代码生成。

替代方案   

目前没有可用的替代 C# 代码的编译器或解析器。 .Net BCL 提供了解析 C# 代码的接口,但这些接口并未实现。如果安装了 Visual Studio,可以尝试使用它提供的某些未文档化的 COM 接口进行解析。

使用 Mono C# 编译器的挑战  

使用 Mono C# 编译器最大的挑战在于它并非以代码重用性为目标进行设计,也不是作为库来使用的。虽然 Mono C# 编译器可以工作,但使用它的代码需要库用户进行全局理解。我在实践中遇到的问题包括:
  1. 某些成员变量或类需要是公共的,但却是受保护的;
  2. 编译器对代码的某些遍历会撤销解析树对象上的各种字段,从而破坏了部分解析树。
  3. 需要从解析器一直追溯到解析树来找出某些信息存储在哪里,因为有很多字段的命名具有误导性。
  4. 第四个挑战是用于表示解析树的类的数量庞大,以及深度继承层次结构。在熟悉了代码之后,这成了最大的挑战。

目标   

我的目标是展示如何使用 C# 解析器从解析树中提取信息。我不会涉及 Mono C# 编译器中的代码生成。我将展示两个应用程序。第一个是解析树浏览器。使用此应用程序,您可以以交互方式将代码文本部分与解析树关联起来。此应用程序对于需要理解解析器代码的任何场景都很有用。第二个应用程序是从解析树中提取部分内容,用于搜索源代码的应用程序。

准备使用 Mono C# 编译器代码。

您可以下载包含 Mono 编译器修改的代码,也可以从头开始下载 Mono。

  • 选项 1 (我的代码):我的代码可作为 Visual Studio 2010 项目使用,其中包含生成的 cs-parser.cs 以及我在本文中解释的其他修改。请参阅文章开头处的下载链接。
  • 选项 2 (从头开始):  

    下载 mono
    编译 jay 解析器生成器:mono-2.6.7\mcs\jay
    从 mono-2.6.7\mcs\mcs\cs-parser.jay 生成 cs-parser.cs
    编译 mono-2.6.7\mcs\mcs\dmcs.csproj 中的 dmcs 项目

我发现在 Ubuntu Linux 上编译完整的 Mono 项目最容易。我使用 apt-get 安装了额外的库,然后运行了 configure 和 make。文件 cs-parser.cs 出现在 mono-2.6.7\mcs\mcs\ 目录中。我将此文件复制到了 Windows 上的 Visual Studio 环境中。

理解代码   

 方法 1

启动编译器的代码位于 driver.cs 中。我查看了该代码,了解如何实例化解析树:

        static ModuleContainer ParseFile(string fileName){
            string[] args = new String [] {fileName, "--parse"};
            CompilerCallableEntryPoint.Reset();
            Mono.CSharp.Driver d = Mono.CSharp.Driver.Create(args, false, new ConsoleReportPrinter());
            d.Compile();
            return RootContext.root;
        }

            string fileName = @"monitor.cs"
            ModuleContainer parseTree = ParseFile(fileName);
 

我应该注意的一点是,解析树出现在名为 RootContext 的类中的静态字段。如果我们解析多个文件并且静态字段没有得到适当的清理,这种方法可能会产生问题。为了使此代码能从外部项目运行,我不得不将几个类和成员字段的范围修改为 public。在运行解析器之前,最好通过调用 CompilerCallableEntryPoint.Reset() 来清除状态。

用于表示解析树信息的类非常多。我花了四个多小时才接触到其中的每一个类并添加了额外的接口。我建议使用本文提供的应用程序来浏览由特定代码生成的表示。例如,C# 类由 Class 对象实例表示。Class 作为成员变量包含方法列表(class Method)、构造函数(class Constructor)、字段(class Field)等。
在浏览代码时,通常不清楚特定信息存储在哪里。作为一个具体例子,考虑方法参数的类型存储在哪里这个问题。具体来说,“void foo(Bar b)”中的 Bar 存储在哪里?要回答这个问题,我们实际上需要查看 cs-parser.jay 中的解析器代码。C# 文件 cs-parsers.cs 是自动生成的,程序员无法理解。在 cs-parser.jay 中查找关键字 method,我发现方法是通过 method_declaration 进行解析的。反过来,方法的参数通过 method_header 进行解析。
检查 method_header 表明参数的类型是 FullNamedExpression,它存储在 Method 对象中。
method_header
    : opt_attributes
      opt_modifiers
      member_type
      method_declaration_name
    ...

    method = new Method (current_class, generic, (FullNamedExpression) $3, (int) $2, name, ...


然后我查看 Method 实现类,发现我可以通过类似这样的代码获得方法参数的类型:

			foreach (var maybeParam in m.Parameters.FixedParameters)
                        {
                            var param = (maybeParam as Parameter);
                            var typeName = "?";
                            if (param.TypeName is TypeNameExpression)
                            {
                                typeName = (param.TypeName as TypeNameExpression).name;
                            }
                            else
                            {
                                typeName = (param.TypeName as TypeLookupExpression).name;
                            }
                            //more to do ??? yes, TypeExpression (see class diagram)
                        } 


在查看此代码后,我不确定是否正确处理了获取表达式名称的所有情况。FullNamedExpression 是一个抽象类,我需要找到所有子类并正确获取类型的名称。使用 Visual Studio 的类图工具,我可以了解需要处理哪些类。最好有一个名为 Name 的抽象 getter 属性,并在 FullNamedExpression 的子类中重写它。

fullnamedexprhiearchy.jpg

 方法 2

作为第二个例子,考虑“using System.Collections;”等 using 指令存储在哪里这个问题。建议的方法如下。创建两个最初相同的示例,但第二个示例包含 using 指令。然后将两个示例的解析树转储到两个文本文件中,并运行 diff 来查找差异。我通过两种不同的方法生成解析树:一种使用反射,一种使用接口。反射的优点是易于实现。但它的缺点是转储大量数据。作为替代,我实现了一种不同的策略:一种只列出我所请求的信息。表示解析器信息的每个类都需要实现一个接口并导出有趣的数据片段。

应用程序 1.  

我想展示的应用程序是一个已解析的 C# 代码浏览器。我已经描述了在 Mono C# 编译器代码中找到路径的技术。对于所有相关的类,我将实现一个名为 IVisitable 的接口,这将帮助我导航解析树。
正如我之前提到的,有两种模式:

  • 第一种使用接口,并导出我告诉它导出的内容
  • 第二种使用反射,并禁止导出我告诉它禁止导出的内容

这两种模式是互补的,并在截图中有展示。

为了演示此应用程序的用法,请考虑“Hello world”程序。我只想将解析树转储到 TreeView 控件中,并查看解析树的 C# 表示。

via-visitor-code.jpg 

 via-reflection.png

 

 

 

本文仍在进行中。请尽快回来查看完成版文章。欢迎评论或更正。


© . All rights reserved.