使用 Reflection.Emit 实现的简单 Orca 克隆






4.89/5 (15投票s)
一个使用动态类型和数据绑定来显示 MSI 文件的 WPF 应用程序。
引言
本文的目的不是提供一个完整的 Orca 替代品,而是展示 Reflection.Emit 和基于反射的 WPF 发现机制如何结合起来解决当数据结构在编译时未知时需要显示数据的问题。
背景
MSI SDK 定义了近 100 个表及其包含的列。构建一个查看器应用程序的一种可能方法是将所有已知表建模为类。这种方法有两个严重的缺点。第一个是工作量很大,第二个也是更重要的一点是,许多 MSI 文件包含自定义表,这些表无法通过这种方法显示。一个更好的解决方案(也是本文提出的解决方案)是使用 MSI 数据库函数检索数据库的模式并动态创建所需的类。
Using the Code
示例项目包含三个类
TypeBuilder
这是一个用于即时创建动态类型的帮助类。实例方法 GetTypeFromPropertyList
接受一个名称/类型对数组作为输入,并返回创建的类的类型。该类内部缓存已创建的类型,以便每个动态类型只创建一次。缓存查找的标准是所有输入名称/类型对的相等性。这是唯一一个内部使用 Reflection.Emit 的类。
MsiReader
此类内部使用 Windows Installer 的 自动化接口来检索 MSI 数据库中找到的数据。该类使用传入的 TypeBuilder
实例即时创建动态类型。该类公开三个公共方法
GetTableNames
:返回安装程序文件中包含的表列表。GetTableContent
:返回一个动态创建的对象数组,这些对象用表的内容填充。该方法需要表的名称作为输入。GetBinaryContent
:返回表示数据库中二进制数据的字节数组。
二进制数据与基本类型(整数和字符串)处理方式不同的原因是大小和性能。简单类型直接从数据库读取并复制到动态创建的对象中。由于二进制数据块可能非常大,因此将所有数据读取并将其全部内容复制到模型中没有意义。而是将数据库中位置的引用复制到模型中。此数据库引用由 BinaryConentDescription
类(它是 MsiReader
内的一个私有嵌套类)和 IBinaryConentDescription
(它是其公开表示)表示。然后可以将该引用传递到 GetBinaryContent
方法以检索实际数据。
MainWindow
此类的主要职责是显示加载的数据库的内容并响应用户输入。ListBox
(左窗格)显示数据库中找到的表。GridView
(右窗格)显示所选表的内容。该类有一个私有方法(ProcessMsiFile
),该方法创建一个 MsiReader
类的实例,检索数据,并用该数据设置 WPF 的 DataContext
。其余处理由 WPF 的数据绑定魔术完成。
上图显示了与模型绑定的控件。模型是在 ProcessMsiFile
方法中创建的一个匿名类。
以下序列图显示了这三个类和 Windows Installer 如何交互:
动态类型
以下显示了数据库表之一(TextStyle
)如何映射到 C#,最后又如何映射到 TypeBuilder
类发出的 IL 代码
Column | 类型 | 键 | 可空 |
TextStyle |
标识符 |
Y | N |
FaceName |
文本 |
N | N |
大小 |
整数 |
N | N |
Color |
DoubleInteger |
N | Y |
StyleBits |
整数 |
N | Y |
生成的表示 TextStyle
表的动态类型在 C# 中如下所示
class dyn_<guid>
{
public dyn_<guid>(string p1, string p2, int p3, int? p4, int? p5)
{
m_TextStyle = p1;
m_FaceName = p2;
m_Size = p3;
m_Color = p4;
m_StyleBits = p5;
}
string m_TextStyle;
string m_FaceName;
int m_Size;
int? m_Color;
int? m_StyleBits;
public string TextStyle
{
get
{
return m_TextStyle;
}
}
public string FaceName
{
get
{
return m_FaceName;
}
}
public int Size
{
get
{
return m_Size;
}
}
public int? Color
{
get
{
return m_Color;
}
}
public int? StyleBits
{
get
{
return m_StyleBits;
}
}
}
请注意,数据库类型与 .NET 类型的映射方式如下。
最后,在 IL 中也是如此(为保持列表简短,移除了 FaceName
和 StyleBits
)
// Fields
.field private string m_TextStyle
.field private int32 m_Size
.field private valuetype [mscorlib]System.Nullable`1<int32> m_Color
// Methods
.method public hidebysig specialname rtspecialname
instance void .ctor (
string p1,
int32 p3,
valuetype [mscorlib]System.Nullable`1<int32> p4
) cil managed
{
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ldarg.0
IL_0007: ldarg.1
IL_0008: stfld string MSIExplorer.dyn_abc::m_TextStyle
IL_0014: ldarg.0
IL_0015: ldarg.3
IL_0016: stfld int32 MSIExplorer.dyn_abc::m_Size
IL_001b: ldarg.0
IL_001c: ldarg.s p4
IL_001e: stfld valuetype [mscorlib]System.Nullable`1<int32>
MSIExplorer.dyn_abc::m_Color
IL_002b: ret
} // end of method dyn_abc::.ctor
.method public hidebysig specialname
instance string get_TextStyle () cil managed
{
IL_0000: ldarg.0
IL_0001: ldfld string MSIExplorer.dyn_abc::m_TextStyle
IL_0006: ret
} // end of method dyn_abc::get_TextStyle
.method public hidebysig specialname
instance int32 get_Size () cil managed
{
IL_0000: ldarg.0
IL_0001: ldfld int32 MSIExplorer.dyn_abc::m_Size
IL_0006: ret
} // end of method dyn_abc::get_Size
.method public hidebysig specialname
instance valuetype [mscorlib]System.Nullable`1<int32>
get_Color () cil managed
{
IL_0000: ldarg.0
IL_0001: ldfld valuetype [mscorlib]System.Nullable`1<int32>
MSIExplorer.dyn_abc::m_Color
IL_0006: ret
} // end of method dyn_abc::get_Color
// Properties
.property instance string TextStyle()
{
.get instance string MSIExplorer.dyn_abc::get_TextStyle()
}
.property instance int32 Size()
{
.get instance int32 MSIExplorer.dyn_abc::get_Size()
}
.property instance valuetype [mscorlib]System.Nullable`1<int32> Color()
{
.get instance valuetype [mscorlib]System.Nullable`1<int32>
MSIExplorer.dyn_abc::get_Color()
}
在网格中,类型看起来是这样的
这也是我在 TypeBuilder
类中编写 IL 代码时所采用的方法。我首先写了上面的 C# 类,然后使用 ILSpy 查看生成的 IL,并将其复制到源文件中。将原始 IL 代码转换为 Reflection.Emit 调用并非难事。
有了以上知识,应该很容易理解下面的代码
private Type CreateTypeFromPropertyList
(Tuple<string, Type>[] properties, string typeName)
{
Emit.TypeBuilder tb = mb.DefineType(typeName, TypeAttributes.Public);
var fields = new List<Emit.FieldBuilder>();
foreach (var prop in properties)
{
// field
Emit.FieldBuilder fb = tb.DefineField("m_" + prop.Item1,
prop.Item2, FieldAttributes.Private);
fields.Add(fb);
// property
Emit.PropertyBuilder pb = tb.DefineProperty(prop.Item1,
PropertyAttributes.HasDefault, prop.Item2, null);
Emit.MethodBuilder mbPropGetAccessor = tb.DefineMethod("get_" + prop.Item1,
MethodAttributes.Public | MethodAttributes.SpecialName |
MethodAttributes.HideBySig, prop.Item2, Type.EmptyTypes);
Emit.ILGenerator propGetIL = mbPropGetAccessor.GetILGenerator();
propGetIL.Emit(Emit.OpCodes.Ldarg_0);
propGetIL.Emit(Emit.OpCodes.Ldfld, fb);
propGetIL.Emit(Emit.OpCodes.Ret);
pb.SetGetMethod(mbPropGetAccessor);
}
// constructor ctor(f1, f2, f3, ...)
Emit.ConstructorBuilder ctor2 = tb.DefineConstructor(
MethodAttributes.Public, CallingConventions.Standard,
properties.Select(a => a.Item2).ToArray());
Emit.ILGenerator ctor2IL = ctor2.GetILGenerator();
ctor2IL.Emit(Emit.OpCodes.Ldarg_0);
ctor2IL.Emit(Emit.OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes));
foreach (Emit.FieldBuilder fb in fields)
{
ctor2IL.Emit(Emit.OpCodes.Ldarg_0);
ctor2IL.Emit(Emit.OpCodes.Ldarg, (byte)fields.FindIndex(a => a == fb) + 1);
ctor2IL.Emit(Emit.OpCodes.Stfld, fb);
}
ctor2IL.Emit(Emit.OpCodes.Ret);
return tb.CreateType();
}
注意与原始 IL 相比,顺序已更改。原因是我想减少所需的循环次数,同时不影响可读性。第一个循环按顺序创建字段、属性和属性 getter,第二个循环在构造函数体中发出设置字段的代码。
对于初始测试,我使用了 Reflection-Emit 生成程序集的能力,并使用 ILSpy 和 PEVerify.exe 检查了其内容。
关注点
.NET 数组
对我来说最大的惊喜之一是数组在 Reflection 和数据绑定方面的运作方式。第一个天真的方法是收集数据并执行 ToArray
来返回数据。当 WPF 的数据绑定无法使用此方法时,出现了一个大惊喜。
public object[] GetTableContent(string tableName)
{
var resList = new List<object>();
// fill resList with content
...
// this compiles but doesn't work
return resList.ToArray();
// this works as expected
var arr = Array.CreateInstance(rowType, resList.Count);
Array.Copy(resList.ToArray(), arr, resList.Count);
return (object[])arr;
}
使用调试器无法看到这两个数组之间的差异,它们都显示为 object[]
类型,但起作用的那个实际上是一个动态类型的数组。
DataGrid 和可空类型
另一个恼人的 bug/feature 是 DataGrid
在排序方面无法正确处理 可空类型。数据库中的一些整数是可空的,因此在动态类中表示为 int?
。对于这些列,排序就会停止工作!
LINQ 和内存消耗
我遇到的第三个问题是内存消耗。保存大型二进制文件(> 100 MB)时,可能会抛出 OutOfMemoryException
。这发生在 X86 平台调试构建运行时。在 X64 上运行程序时,限制远远超出我在 MSI 文件中找到的任何内容。
之所以会这么早发生这种情况,可能是因为我将流数据(从安装程序 API 返回为字符串)转换为字节数组的方式。我使用了以下 LINQ 代码
<string buffer>.SelectMany(c => BitConverter.GetBytes(c)).ToArray();
这可能是使用非安全代码来提高性能和降低内存不足风险的一个领域。
懒加载
另一个有趣的观察是,通过在适当的位置插入 Lazy<T>
成员,可以在应用程序中轻松实现表的延迟加载。
private void ProcessMsiFile(string fileName)
{
MsiReader msiReader = new MsiReader(builder, fileName);
this.DataContext = new
{
FileName = Path.GetFileName(fileName),
Tables =
(from tableName in msiReader.GetTableNames()
select new
{
TableName = tableName,
Rows = new Lazy<object[]>
(() => msiReader.GetTableContent(tableName))
//TODO:Testing - no lazy loading
//Rows = new { Value =
// msiReader.GetTableContent(tableName)},
}).ToArray()
};
}
采用此技巧的原因是大型 MSI 文件加载相对较慢。另请注意,通过将 Lazy<T>
插入到 ProcessMsiFile
函数中,上述序列图 不再反映 MsiReader
实例的生存时间。
结论
这里展示的代码绝不能用于实际的生产程序。这里的目标是通过利用 .NET Framework 中一些更不常用的 API,用几百行代码解决一个相对复杂的问题。这也是为什么示例程序缺少许多功能,使其无法真正有用。其中一些最明显的功能是支持修改数据库以及查找/替换功能。尽管这两个功能将非常有用,但我认为如果添加它们,我将失去本文的目的,从而失去当前实现的简洁性。