编译器可能会生成一些特殊的优化类。其中一些类由 Roslyn 生成(如 Lambda 表达式的类或异步代码的状态机)。其他则由 X# 编译器生成。
下面是这些类的一些示例(如果使用 IlSpy 等工具打开 X# 编译器程序集,就可以看到这些类)
类 |
用途 |
Xs$PSZLiteralsTable |
如果应用程序中的代码看起来像这样,编译器就会生成该类:
由于我们在编译时无法 "知道 "PSZ 的生命周期,因此我们在该类中创建了一个静态字段,并将生成的 PSZ 值(值类型)赋值给该字段。这样,在应用程序的整个生命周期内,PSZ 都将 "活着"。 如果您知道在调用 WIN32 api 后将不再需要 PSZ,那么您最好将 PSZ(_CAST 替换为 String2Psz()。这将确保 PSZ 值在创建它的函数结束后被销毁。 代码如下: FUNCTION TestMe() AS VOID 生成以下内容 public unsafe static void TestMe() 以及下面的 PSZ 表: internal static class Xs$PSZLiteralsTable 如您所见,PSZ 值已存储在表中。请注意,每个 PSZ 变量都包含一个指向运行时使用 String2Mem 函数分配的静态内存的指针。因此,这些静态内存块将在应用程序的整个生命周期内分配。
如果将代码改为使用 String2Psz() FUNCTION TestMe() AS VOID 那么结果将是
public unsafe static void TestMe() 可以看到,编译器现在生成了一个局部变量(一个 IntPtr 列表),该变量在最后被传递给一个运行时函数,该函数在函数结束时负责删除分配的内存。为了确保这一点,我们添加了 try ... finally。 |
Xs$SymbolTable |
如果您在代码中使用文字符号,编译器会生成该类。应用程序中的每个符号在该类中都有一个字段。在 System 类中有 21 个字面符号,反编译时可以看到: internal static class Xs$SymbolTable { 这与 Visual Objects 中传递符号的方式非常相似。 当使用程序集中的第一个符号时,就会创建所有符号,之后使用文字符号的速度会非常快。符号存储在运行时的静态表中,符号值只包含该表中的偏移量。比较 2 个符号就像比较 2 个数字,因此速度非常快。 |
<AssemblyName>.Functions |
Dotnet 没有函数或全局变量的概念。因此,X# 编译器会在每个程序集中创建一个静态类,其中包含代码中每个函数或过程的静态方法。 该类的名称源自输出程序集的名称: MyFile.DLL 将包含一个 MyFile.Functions 类 MyFile.EXE 将包含一个 MyFile.Exe.Functions 类 如果输出程序集名称包含内嵌点,那么这些点将在函数类名称中用下划线字符代替: MyApp.Main.EXE 将包含一个类名 MyApp_Main.EXE.Functions |
Functions$<ModuleName>$ |
只要您的代码使用了 STATIC FUNCTION、STATIC DEFINE、STATIC GLOBAL(其可见性仅限于同一文件内),编译器就会为每个模块(PRG 文件)生成一个单独的类,其中 PRG 文件的名称将用于 <Modulename> 中,因此 Application1.exe 中的文件 Start.Prg 将生成一个类名 Application1.Exe.Functions$Start$ |
$PCall$<FunctionName>$<suffix> |
如果您的代码包含 PCALL() 结构,那么编译器将根据方法/函数名称生成一个特殊的委托,并将该委托作为使用 PCALL() 的类型内部的嵌套对象。因此,函数中的 PCALL() 将导致函数类中的嵌套委托,而 Window 类方法中的 PCALL() 将导致 Windows 类中的嵌套委托。 委托的返回类型、参数名称和类型均源自传递给 PCALL() 的类型指针的函数声明。
例如,VOGUIClasses 程序集在 Window 类中包含一个 $PCall$DeleteTrayIcon$430 函数,在 Functions 类中包含一个 $PCall$__InitFunctionPointer$28 函数。 如果您查看 __InitFunctionPointer 过程中的原始代码,它看起来像这样: IF !PCALL(gpfnInitCommonControlsEx, @icex) 生成的代码如下所示:
if (!$PCallGetDelegate<$PCall$__InitFunctionPointer$28>(gpfnInitCommonControlsEx)(&icex))
$PCallGetDelegate 函数是编译器生成的一个特殊函数,看起来像这样: [CompilerGenerated] 简而言之:它接收一个函数指针(p),并获取一个 T 类型的委托。然后使用该委托调用 API 函数。 如果你不明白,请不要担心。我们自己也是花了好长时间才创建出来的! |
$PCallNative$<FunctionName>$<suffix> |
这是一个为 PCallNative 结构生成的委托。返回类型是通用参数的类型,而参数类型是从参数类型派生出来的。参数名称为 $param1、$param2 等。 因此,Test 函数中的以下代码 LOCAL p AS IntPtr 将生成这样一个委托: [CompilerGenerated] |
Functions.$Init1 Functions.$Init3 Functions.$Exit |
函数类内的这些特殊方法是为了调用 Init 和 Exit 程序而生成的。有关这方面的更多信息,请参阅启动代码主题 |
<Module>.RunInitProcs() |
<Module> 类中的这个特殊方法由编译器生成,当你使用 XSharpLoadLibrary() 函数动态加载程序集时,它将在运行时被调用。当动态加载 DLL 时,它会调用所有初始程序。 |
<>ClassName |
Roslyn 编译器会生成以 <> 前缀开头的特殊类,用于 lambda 表达式和 codeblock。 如果查看 VORDDClasses 程序集,你会发现很多这样的例子。您可能需要将 ILSpy 设置为显示 IL 而不是 C# 或 XSharp 代码,否则这些类将被工具隐藏。 如果您在 C# 模式下查看 RDD 类,它将显示如下内容:
如果将 ILSpy 切换到 IL 模式,就会显示如下图所示:
正如你所看到的,DbServer 类内部现在有很多嵌套类。<>c 类包含不需要从函数或方法访问局部变量的代码块。 在 DbServer 类中,该类有大约 25 个方法,每个方法都是一个代码块。 名称为 <>c_DisplayClass<nn> 的类包含的代码块需要从定义它们的函数或方法中访问局部变量。 编译器已检测到这一点,并将局部变量移出函数/方法,使其成为编译器生成类中的字段,这样代码块就可以访问它们了。在 Clipper 和 VO 中,这些变量被称为 "分离局部变量"。
例如,DisplayClass56_0 包含 Average 函数的变量: .class nested private auto ansi sealed beforefieldinit '<>c__DisplayClass56_0' Average 方法内部的代码块显然访问了 5 个局部变量(iCount、acbExpr、aResults、cbKey 和 uValue)。
如果查看 DbServer 的 Average() 方法,你会看到如下代码块
如果您查看 Average() 的反编译代码(C# 模式),您会看到如下内容: __DbServerEval(new new 后面的整个 {} 是一个匿名代码块表达式 该表达式中的 Cb$Eval$ 字段是一个委托,其中包含代码块的代码。 该表达式中的 CB$Src 字段包含代码块的源代码,因此在运行时您可以看到编译时代码块的源代码(这是在 2.3.0 版中引入的)。
代码块的实际主体(从 iCount++ 到 return default(__Usual) 的部分)实际上存储在 <>c__DisplayClass56_0 的方法中。这个代码块中需要的所有变量实际上并没有作为变量存储在 Average() 中,而是作为 <>c__DisplayClass56_0 的字段存储。
如果您不明白,请不要担心。我们也是花了一段时间才理解并自己创建的! |
Xs$Args |
只要您的代码包含使用所谓 CLIPPER 调用约定的函数或方法,X# 编译器就会创建以特殊方式处理参数的代码: 例如,运行时中的函数 Str()。该函数声明了以下参数:
编译器认为这是 CLIPPER 调用约定,因为所有 3 个参数都是可选的。
为该函数生成的 C# 版本 IL 代码是 [ClipperCallingConvention(new string[] { "nNumber", "nLength", "nDecimals" })] 如你所见,函数现在只有一个参数,即一个 usuals 数组。 参数名称存储在 ClipperCallingConvention 类型的属性中。Visual Studio 和 XIDE 中的 intellisense 会使用该属性来显示参数名。 现在,编译器会在生成方法的主体中声明一个变量,其中包含数组的长度(传递的参数数,您也可以在运行时使用 PCount()请求)。编译器还会生成一个与参数同名的局部变量,并用传递的值(基于 0 的数组元素)或 NIL 初始化每个变量。 在方法的主体中,你会看到一个 try finally。在 finally 子句中有如下代码: finally 产生这段代码的原因是 Str() 中的某个地方分配了 nLength。Str() 不知道变量是通过值传递还是通过引用传递。 如果值是通过引用传递的,那么必须更新 Xs$Args 中的数组元素,这正是此处发生的情况。 调用 Str() 的代码现在负责将数组中的值赋回本地,如果该值是通过引用传递的
|