Assemblies and Metadata
.NET Assemblies 可以定义为是包含 metadata,managed code以及managed resources的字节流(理论上说,一个Assembly可以包含多个这样的"字节流",这种情况下,一个"字节流"就被称为一个Module,而这样的Assembly 就称为 Multi-module Assembly,但是这种情况很罕见。所以本文中讨论的都是单个module的Assembly)。大部分的assembly都存储为文件,也有一些可以存在别的地方,例如实现stored procedures的assembly可以存储在SQL数据库里面。Metadata是.NET Assembly的基本、强制组成部分,对于assembly自身、所有托管的函数、所有托管的类型以及托管类型的成员,Metadata都是自动生成的。
一个例子:
// SampleLib.cpp
// build with :"CL /LD /clr:safe SampleLib.cpp
namespace MyNamespace
{
public ref class SampleClass
{
bool SampleField1;
int SampleField2;
public:
SampleClass():SampleField1(true),SampleField2(1234){}
void SampleMethod(){}
void StaticSampleMethod(){}
};
}// MyNamesapce
使用CL /LD /clr:safe SampleLib.cpp 命令编译上面的代码得到SampleLib.dll,然后使用Windows SDK自带的ILDASM工具分析它,得到下图的结果:
Assembly Manifests
上图中,除了MyNamespace.SampleClass之外,还有一个manifest,这个manifest也是assembly的关键且强制组成部分。manifest里面包含了所有assembly层面的metadata。里面包含了该assembly的标识符以及与其他assemblies和其他文件的依赖关系。
上例中的manifest如下(双击上图中的manifest可以得到)
上面的manifest中,.assembly extern mscorlib以及后面花括号里面的东西表示SampleLib在运行时需要mscorlib 4.0版。.assembly SampleLib以及花括号里面的内容定义SampleLib的标识符。后面的其他内容则表示assembly的其他方面信息,例如.corflags 0x00000001 //IL only 表示该assembly中只包含平台独立的托管代码。
Metadata APIs
Metadata并不是只能被像ILDASM那样的工具所使用。很多其他的地方也使用Metadata。例如,visual studio里面的智能输入提示(IntelliSense)工具、编译器也使用metadata确保源代码里面的类型以及类型成员确实存在。
如果你想要实现一个基于Metadata的工具,那么你也需要获得Metadata的访问权限。有两套主要的reflection API可以用来访问Metadata,一套是非托管的API,一套是托管的。托管的reflection api是.NET FRAMEWORK类库的一部分。托管的api几乎跟非托管的一样强大,但是使用起来要方便得多。所以我们主要讨论托管的reflection api。
这类API大多在System::Reflection命名空间中。托管API的一个基本的类型是System::Reflection::Assembly,这个类代表了一个加载了的Assembly,调用静态函数Assembly::GetExecutingAssembly就可以获得当前正在运行的代码所属的Assembly,AppDomain::CurrentDomain->GetAssemblies()以数组的形式返回所有加载了的Assembly。
除了静态的Metadata外,System::Reflection::Assembly还可以提供加载的Assembly的来源的信息。例如Location属性告诉里assembly在文件系统的什么地方可以找到,CodeBase返回assembly加载的URL地址,这个属性可以和Location不同,因为从网络上下载的Assembly会再本地有缓存,Location只的是本地的缓存,而CodeBase则是URL。FullName属性返回一个包含了assembly标识符的字符串
例子:
//DumpAssemblyInfo.cpp
//build with "CL /clr:safe DumpAssemblyInfo.cpp"
#using "SampleLib.dll"
using namespace System;
using namespace System::Reflection;
int main()
{
MyNamespace::SampleClass^ obj = gcnew MyNamespace::SampleClass();
for each(Assembly^ a in AppDomain::CurrentDomain->GetAssemblies())
{
Console::WriteLine(a->FullName);
Console::WriteLine("\t"+a->CodeBase);
Console::WriteLine("\t"+a->Location);
}
}
Assembly Identity
所有的assemblies都有一个标识符(FullName),如上例的输出结果,这个标识符由四个部分组成:
- Simple Name:就是assembly的文件名(去掉拓展名,也可以有assembly的名字与文件名不一致,这里忽略这种情况)
- Version
- Culture:for localization support
- PublicKeyToken:null or 64-bit hexadecimal number.Assembly names with PublicKeyToken != null are called strong names
Assembly loading and deployment
Assemblies 通常在由这个Assembly所定义类型的对象在托管代码中第一次被使用到的时候自动加载(发生在JIT编译期)。所以里面的示例代码中,只要我们去掉MyNamespace::SampleClass^ obj = gcnew MyNamespace::SampleClass();这一行,即使#using "SampleLib.dll"还保留在源代码中,SampleLib.dll也不会被加载。
当JIT编译器决定要加载某一个Assembly的时候,它首先去查看那个引用其他assembly的assembly的manifest。例如上例中,DumpAssemblyInfo.exe要引用SampleLib.dll,那么编译器就去查看DumpAssemblyInfo.exe的manifest,然后发现里面有这一段:
这一段信息就等效于以下SampleLib的名称:
SampleLib,Version=0.0.0.0,Culture=neutral,PublicKeyToken=null
如果引用的个拥有strong name的assembly,那么manifest里面还会包含PublicKeyToken的信息。(例如前面引用了mscorlib的)
不管是哪一种情况(strong name or not),被引用Assembly的路径都没有在引用Assembly中指定。为了找到被引用Assembly的路径,需要使用Assembly Resolution算法。
如果被引用的assembly没有使用strong name,那么resolver使用简化的算法。算法使用application base directory(即Exe文件所处的文件夹,或者使用特殊的方法指定的其他路径)。解析器先在这个base directory里寻找,如果没找到,再去与要加载的Assembly的简称一致的子文件夹中去找(上例中为命名为SampleLib的子文件夹)。除非特别设定,否则不会在去找其他的子文件夹了。
The Global Assembly Cache (GAC)
使用了strong name的Assemblies可以安装在机器级的库里面(即整个机器中的其他assembly都可以引用它们)。这个库就称为GAC。例如所有FCL Assemblies都安装在GAC中。
Version Redirections
Manual Assembly Loading
以下代码可以手动加载assembly:
Assembly^ Assembly::Load(String^ assemblyName);
Assembly^ Assembly::LoadFrom(String^ url);
在运行时对类型或者类型成员使用metadata
与普通的数据库类似,Assembly中包含元数据表(metadata tables)。Metadata tables exist for type definition,method definition and many other abstractions. The structures for all tables start with a 32-bit column called a metadata token.这种metadata token可以认为是数据库表里面的"主键"。To establish relationships between the different abstractions stored in metadata tables,an approach similar to using foreign keys in databases is used: the structure of various metadata tables contains columns that store metadata tokens of other tables.
Managed Reflection API 包含了metadata 中可以存储的各种抽象的类(即metadata中可以存储的各种东西,managed reflection api中都包含了对于的类用来访问这些数据)。包括类型定义、方法(全局的方法或者托管类型的成员方法)、字段。这些类型通过"has-a"关系联系在一起。Assembly "has"类型定义,类型定义"has"字段和方法,方法"has"参数,等等。为了从一种类型浏览到另一种类型,每种类型都有一些以Get为前缀的方法。下面的代码使用Assembly::GetTypes iterate through一个assmebly里面所有定义的类型:
动态类型实例化
除了获取类型的静态信息之外,还可以动态的实例化一个类型。这个功能的一个典型应用是创建plug-in objects,方法是使用Activator::CreateInstance(Type^)静态方法
运行时类型成员信息
For each of these different kinds of type members,the Managed Reflection API contains a class that delivers type-specific metadata. These classes are FieldInfo,MethodInfo,ConstructorInfo,PropertyInfo and EventInfo from the System::Reflection namespace.
Dynamic Member Access
所谓DMA,就是如果有一个MethodInfo对象,那么可以通过MethodInfo::Invoke调用这个函数;如果有FieldInfo对象,就可以通过GetValue /SetValue获取和设置这个字段的值;PropertyInfo/EventInfo也有类似的成员存在。
通过这样运行绑定的方法访问成员显然要比直接调用方法或者直接访问字段要慢得多(对于没有参数也没有返回值的函数调用,DMA要慢300倍,如果有参数或者返回值,可能还要再乘以好几倍)。
虽然DMA效率很低下,但是有时候用来做一些helper方法还是很方便的。例如著名的Serialize和Deserialize方法就是通过DMA实现的。以下代码是简化版本的Serialize和Deserialize
// customSerialization.cpp
// CL /clr:safe customSerialization.cpp
using namespace System;
using namespace System::IO;
using namespace System::Reflection;
ref struct Person
{
String^ Name;
int Age;
};
void Serialize(Stream^ strm,Object^ o)
{
if(!strm)
throw gcnew ArgumentNullException("strm");
if(!o)
throw gcnew ArgumentNullException("o");
BinaryWriter^ bw = gcnew BinaryWriter(strm);
try
{
Type^ t = o->GetType();
bw->Write(t->AssemblyQualifiedName);
array<FieldInfo^>^ fields = t->GetFields();
for each(FieldInfo^ fi in fields)
{
if(fi->FieldType == int::typeid)
bw->Write((int)fi->GetValue(o));
else if(fi->FieldType == String::typeid)
bw->Write((String^)fi->GetValue(o));
else
throw gcnew NotSupportedException();
}
}
finally
{
bw->Close();
}
}
Object^ Deserialize(Stream^ strm)
{
if(!strm)
throw gcnew ArgumentNullException("strm");
Object^ o;
BinaryReader^ br = gcnew BinaryReader(strm);
try
{
String^ type = br->ReadString();
Type^ t = Type::GetType(type);
o = Activator::CreateInstance(t);
array<FieldInfo^>^ fields = t->GetFields();
for each(FieldInfo^ fi in fields)
{
if(fi->FieldType==int::typeid)
fi->SetValue(o,br->ReadInt32());
else if(fi->FieldType==String::typeid)
fi->SetValue(o,br->ReadString());
else
throw gcnew NotSupportedException();
}
}
finally
{
br->Close();
}
return o;
}
int main()
{
array<Byte>^ bytes = gcnew array<Byte>(1024);
Person^ p = gcnew Person();
p->Name = "Pual";p->Age = 35;
Serialize(gcnew MemoryStream(bytes),p);
Person^ p2 = (Person^)Deserialize(gcnew MemoryStream(bytes));
Console::WriteLine(p2->Name);
Console::WriteLine(p2->Age);
};
Access to Private members
一般来说用不到私有成员,但是像Serialize这样的方法就需要动态访问私有成员。访问的方法还是使用GetFields函数。GetFields方法有带有参数的重载函数,这个重载函数带有BindingFlags参数,BindingFlags是枚举类型(包括Public/NonPublic/Instance)
Attibutes
对于前面关于serialization的例子,一个很有意思的扩展是剔除特定的字段。与其他的.NET语言一样,C++/CLI没有任何关键字可以指定不要serialize某个字段。但是,.NET允许我们扩展语言,是的我们可以给某个类型(或类型成员以及assembly的其他多种目标)提供附加的metadata。这可以通过attributes实现。
An attribute is sepecified within square brackets and (typically) appplies to the item that follows the attribute.As an example,the following code could be used to apply a DoNotSerializeThisField attribute to a field.
ref struct Person
{
String^ Name;
[DoNotSerializeThisField]// or [DoNotSerializeThisFieldAttribute]
int Age;
};
要支持这个属性也相当简单,只要定义一个继承了System::Attribute的类就可以了。
ref class DoNotSerializeThisFieldAttribute : public Attribute{};
注意,上面定义的类名以Attribute为后缀,这符合命名规范,而应用这个属性的时候,即使省略这个后缀也可以。
可以是用Attribute来限定Attribute的使用对象,例如下面的定义将DoNotSerializeThisFieldAttribute限定为只能应用到Field上面:
[AttributeUsage(AttributeTargets::Field,AllowMultiple=false)]
ref class DoNotSerializeThisFieldAttribute : public Attribute{};
上面的代码仅仅将属性应用到某个对象上,但是还没有提供实现属性所指示功能的方法。要实现属性的功能,必须使得属性能够在运行时被发现。这可以通过System::Reflection::ICustomAttributeProvider接口实现。托管Reflection API中的所有提供代表metadata类型的类都实现了这个接口,例如:System::Type,System::Assembly,System::FieldInfo,System::MethodInfo. ICustomAttributeProvider有个IsDefined方法可以用来检查一个属性有没有被应用,下面是一个例子:
array<FieldInfo^>^ fields = t->GetFields();
for each (FieldInfo^ fi in fields)
{
if ( fi->IsDefined(DoNotSerializeThisFieldAttribute::typeid,false))
continue;
//…
}
System::Runtime::Serialization
实际使用的时候不需要自己去定义序列化的实现方法,可以直接使用FCL中现成的功能。实现这些功能的API在System::Runtime::Serialization.这写API也是基于属性的。要标记一个类型是"可序列化的",需要将System::SerializationAttribute应用到类上,如果要在序列化时剔除某些特定的字段,可以对字段应用System::NonSerializableAttribute属性。要定制序列化行为,可以实现ISerializable接口。
序列化设计三个抽象:要序列化的对象、流、格式化器(Formatter)。其中Formatter定义以何种格式将对象序列化到流。FCL特工两种序列化器:BinaryFormatter 和 SoapFormatter。(完整路径是System::Runtime::Serialization::Formatters::Binary::BinaryFormatter 和 System::Runtime::Serialization::Formatters::Soap::SoapFormatter)。
//FCLSerialization.cpp
// build with "CL /clr:safe FCLSerialization.cpp"
using namespace System;
#using <System.Runtime.Serialization.Formatters.Soap.dll>
using namespace System::Runtime::Serialization::Formatters::Soap;
[Serializable]
ref struct Person
{
String^ Name;
[NonSerialized]
int Age;
};
int main()
{
Person^ p = gcnew Person();
p->Name = "Bruce";
p->Age = 35;
SoapFormatter^ sf = gcnew SoapFormatter();
sf->Serialize(Console::OpenStandardOutput(),p);
}
结果:
Summary
- How you can give an assembly a unique identity
- How assemblies are loaded
-
How you can consume the metadata of an assembly
- Metadata are burned into the assembly at build time
-
At runtime,metadata is consumed to provide type-agnostic services.
- The CLR used metadata to implement services like garbage collection,JIT compilation,and object graph serialization.
-
Using the Managed Reflection API,you can implement your own metadata-based runtime services
- By implementing custom attibutes,you can define your own metadata.
- Most metadata is bound to type definitions