Show/Hide Toolbars

XSharp

Navigation: X# 文档 > X# 提示和技巧

编译器生成的特殊类和代码

Scroll Prev Top Next More

 

编译器可能会生成一些特殊的优化类。其中一些类由 Roslyn 生成(如 Lambda 表达式的类或异步代码的状态机)。其他则由 X# 编译器生成。

下面是这些类的一些示例(如果使用 IlSpy 等工具打开 X# 编译器程序集,就可以看到这些类)

 

 

用途

Xs$PSZLiteralsTable

如果应用程序中的代码看起来像这样,编译器就会生成该类:

 
LoadLibrary(PSZ(_CAST, "RICHED20.DLL")) // inside GUI classes

 

由于我们在编译时无法 "知道 "PSZ 的生命周期,因此我们在该类中创建了一个静态字段,并将生成的 PSZ 值(值类型)赋值给该字段。这样,在应用程序的整个生命周期内,PSZ 都将 "活着"。

如果您知道在调用 WIN32 api 后将不再需要 PSZ,那么您最好将 PSZ(_CAST 替换为 String2Psz()。这将确保 PSZ 值在创建它的函数结束后被销毁。

代码如下:

FUNCTION TestMe() AS VOID
  LoadLibrary(PSZ(_CAST, "RICHED20.DLL"))  
  RETURN

生成以下内容

public unsafe static void TestMe()
{
  VOWin32APILibrary.Functions.LoadLibrary((IntPtr)(void*)Xs$PSZLiteralsTable._$psz_$0);
}

以及下面的 PSZ 表:

internal static class Xs$PSZLiteralsTable
{
  internal static readonly __Psz _$psz_$0 = new __Psz("RICHED20.DLL");
}

如您所见,PSZ 值已存储在表中。请注意,每个 PSZ 变量都包含一个指向运行时使用 String2Mem 函数分配的静态内存的指针。因此,这些静态内存块将在应用程序的整个生命周期内分配。

 

如果将代码改为使用 String2Psz()

FUNCTION TestMe() AS VOID
  LoadLibrary(String2Psz("RICHED20.DLL"))  
  RETURN

那么结果将是

 

public unsafe static void TestMe()
{
  List<IntPtr> pszList = new List<IntPtr>();
  try
  {
      VOWin32APILibrary.Functions.LoadLibrary((IntPtr)(void*)new __Psz(CompilerServices.String2Psz("RICHED20.DLL", pszList)));
  }
  finally
  {
      CompilerServices.String2PszRelease(pszList);
  }
}

可以看到,编译器现在生成了一个局部变量(一个 IntPtr 列表),该变量在最后被传递给一个运行时函数,该函数在函数结束时负责删除分配的内存。为了确保这一点,我们添加了 try ... finally。

Xs$SymbolTable

如果您在代码中使用文字符号,编译器会生成该类。应用程序中的每个符号在该类中都有一个字段。在 System 类中有 21 个字面符号,反编译时可以看到:

internal static class Xs$SymbolTable

{
  internal static readonly __Symbol _init = new __Symbol("INIT");
  internal static readonly __Symbol _concurrencycontrol = new __Symbol("CONCURRENCYCONTROL");
  internal static readonly __Symbol _notify = new __Symbol("NOTIFY");
.
.
  internal static readonly __Symbol _unknown = new __Symbol("UNKNOWN");
  internal static readonly __Symbol _resourcestring = new __Symbol("RESOURCESTRING");
}

这与 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]
internal static T $PCallGetDelegate<T>(IntPtr p)
{
  return (T)(object)Marshal.GetDelegateForFunctionPointer(p, typeof(T));
}

简而言之:它接收一个函数指针(p),并获取一个 T 类型的委托。然后使用该委托调用 API 函数。

如果你不明白,请不要担心。我们自己也是花了好长时间才创建出来的!

$PCallNative$<FunctionName>$<suffix>

这是一个为 PCallNative 结构生成的委托。返回类型是通用参数的类型,而参数类型是从参数类型派生出来的。参数名称为 $param1、$param2 等。

因此,Test 函数中的以下代码

  LOCAL p AS IntPtr        
  P := GetProcAddress(hDLL, "MyFunc")
  PCallNative<INT> (p,1,2,3)

将生成这样一个委托:

[CompilerGenerated]
internal delegate int $PCallNative$Test$0(int $param1, int $param2, int $param3);

Functions.$Init1
Functions.$Init2

Functions.$Init3

Functions.$Exit
<Module>.$AppInit()
<Module>.$AppExit()

函数类内的这些特殊方法是为了调用 Init 和 Exit 程序而生成的。有关这方面的更多信息,请参阅启动代码主题

<Module>.RunInitProcs()

<Module> 类中的这个特殊方法由编译器生成,当你使用 XSharpLoadLibrary() 函数动态加载程序集时,它将在运行时被调用。当动态加载 DLL 时,它会调用所有初始程序。

<>ClassName

Roslyn 编译器会生成以 <> 前缀开头的特殊类,用于 lambda 表达式和 codeblock。

如果查看 VORDDClasses 程序集,你会发现很多这样的例子。您可能需要将 ILSpy 设置为显示 IL 而不是 C# 或 XSharp 代码,否则这些类将被工具隐藏。

如果您在 C# 模式下查看 RDD 类,它将显示如下内容:
 

RddClasses1

 

如果将 ILSpy 切换到 IL 模式,就会显示如下图所示:

 

RddClasses2

 

正如你所看到的,DbServer 类内部现在有很多嵌套类。<>c 类包含不需要从函数或方法访问局部变量的代码块。 在 DbServer 类中,该类有大约 25 个方法,每个方法都是一个代码块。

名称为 <>c_DisplayClass<nn> 的类包含的代码块需要从定义它们的函数或方法中访问局部变量。

编译器已检测到这一点,并将局部变量移出函数/方法,使其成为编译器生成类中的字段,这样代码块就可以访问它们了。在 Clipper 和 VO 中,这些变量被称为 "分离局部变量"。

 

例如,DisplayClass56_0 包含 Average 函数的变量:

.class nested private auto ansi sealed beforefieldinit '<>c__DisplayClass56_0'
  extends [mscorlib]System.Object
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
      01 00 00 00
  )
  // Fields
  .field public int32 iCount
  .field public class [XSharp.RT]XSharp.__Array acbExpr
  .field public class [XSharp.RT]XSharp.__Array aResults
  .field public valuetype [XSharp.RT]XSharp.__Usual cbKey
  .field public valuetype [XSharp.RT]XSharp.__Usual uValue
 
  // Methods
  .method public hidebysig specialname rtspecialname
      instance void .ctor () cil managed
  {
 
} // end of class <>c__DisplayClass56_0

Average 方法内部的代码块显然访问了 5 个局部变量(iCount、acbExpr、aResults、cbKey 和 uValue)。

 

如果查看 DbServer 的 Average() 方法,你会看到如下代码块

 
SELF:__DBServerEval( { || iCount += 1, __IterateForSum( acbExpr, aResults ) }.......)

 

 

如果您查看 Average() 的反编译代码(C# 模式),您会看到如下内容:

__DbServerEval(new
                  {
                      Cb$Eval$ = (<>F<__Usual>)delegate
                      {
                          iCount++;
                          VORDDClasses.Functions.__IterateForSum(acbExpr, aResults);
                          return default(__Usual);
                      },
                      Cb$Src$ = "{ || iCount += 1, __IterateForSum( acbExpr, aResults ) }"
                  },...........................)

new 后面的整个 {} 是一个匿名代码块表达式

该表达式中的 Cb$Eval$ 字段是一个委托,其中包含代码块的代码。

该表达式中的 CB$Src 字段包含代码块的源代码,因此在运行时您可以看到编译时代码块的源代码(这是在 2.3.0 版中引入的)。

 

代码块的实际主体(从 iCount++ 到 return default(__Usual) 的部分)实际上存储在 <>c__DisplayClass56_0 的方法中。这个代码块中需要的所有变量实际上并没有作为变量存储在 Average() 中,而是作为 <>c__DisplayClass56_0 的字段存储。

 

如果您不明白,请不要担心。我们也是花了一段时间才理解并自己创建的!

Xs$Args

只要您的代码包含使用所谓 CLIPPER 调用约定的函数或方法,X# 编译器就会创建以特殊方式处理参数的代码: 例如,运行时中的函数 Str()。该函数声明了以下参数:
 
FUNCTION Str(nNumber,nLength,nDecimals) AS STRING

 

编译器认为这是 CLIPPER 调用约定,因为所有 3 个参数都是可选的。

 

为该函数生成的 C# 版本 IL 代码是

[ClipperCallingConvention(new string[] { "nNumber", "nLength", "nDecimals" })]
public static string Str([CompilerGenerated] params __Usual[] Xs$Args)
{
  int num = (Xs$Args != null) ? Xs$Args.Length : 0;
  __Usual nNumber = (num >= 1) ? Xs$Args[0] : __Usual._NIL;
  __Usual nLength = (num >= 2) ? Xs$Args[1] : __Usual._NIL;
  __Usual nDecimals = (num >= 3) ? Xs$Args[2] : __Usual._NIL;

如你所见,函数现在只有一个参数,即一个 usuals 数组。

参数名称存储在 ClipperCallingConvention 类型的属性中。Visual Studio 和 XIDE 中的 intellisense 会使用该属性来显示参数名。

现在,编译器会在生成方法的主体中声明一个变量,其中包含数组的长度(传递的参数数,您也可以在运行时使用 PCount()请求)。编译器还会生成一个与参数同名的局部变量,并用传递的值(基于 0 的数组元素)或 NIL 初始化每个变量。

在方法的主体中,你会看到一个 try finally。在 finally 子句中有如下代码:

finally
  {
      if (num >= 2)
      {
          Xs$Args[1] = nLength;
      }
  }

产生这段代码的原因是 Str() 中的某个地方分配了 nLength。Str() 不知道变量是通过值传递还是通过引用传递。 如果值是通过引用传递的,那么必须更新 Xs$Args 中的数组元素,这正是此处发生的情况。

调用 Str() 的代码现在负责将数组中的值赋回本地,如果该值是通过引用传递的