Sunday, April 10, 2005

 

Some notes on GAC of DotNet

1.
GAC 中的引用计数机制

by flier
from http://www.blogcn.com/User8/flier_lu/index.html?id=4046367&run=.09D4C2F

GAC 中的引用计数机制

在传统的 Windows 环境中,造成 DLL Hell 灾难的因素,除了不同版本 DLL 可以在不同目录下共存且被载入的优先顺序不可知以外,还有一大问题就是在卸载程序时如何对待程序使用到的公共 DLL。如果卸载程序顺手删掉放在 Windows/System32 目录下的自己用到的公共组件,则可能造成其他以来于此组件的程序无法正常使用;但如果置之不理,长此以往必然会造成 Windows 系统目录的无限制膨胀。对象我这样喜欢乱装软件的人来说,硬盘上 Windows 系统的生命期很大程度上取决于系统目录下垃圾的膨胀速度,呵呵。一般使用半年或者更长时间,Windows 就会膨胀到不可接受的地步,也就意味着痛苦的重装系统又要开始了。:P
好在 CLR 在设计时就重复考虑到了这类问题,为 GAC 提供了基于引用计数的垃圾组件清扫机制。每个强签名组件在安装到 GAC 的时候,都可以指定一个引用数据;而在删除时,也将给出相同的引用数据,否则无法正常删除;同时 GAC 可以根据引用数据的内容判断某个组件是否仍然被使用,在合适的时候清除不被使用的垃圾组件。而这一切都归功于 GAC 的引用计数机制,Junfeng Zhang 在 GAC Assembly Trace Reference 一文中详细介绍了这一机制。
而我在前端时间的一片文章《使用 Fusion API 控制 GAC》里面曾经做了错误的介绍,实在是当时没有很好理解这一概念。:P 再次感谢 Junfeng Zhang 的帮助,呵呵。

http://weblogs.asp.net/junfeng/archive/2004/09/13/228651.aspx

在实现上,GAC 的 Fusion API 提供了 CreateInstallReferenceEnum 函数,能够获取针对某个 Assembly 的引用枚举器接口 IInstallReferenceEnum,进而通过其 IInstallReferenceEnum::GetNextInstallReferenceItem 方法获取引用项接口 IInstallReferenceItem,最终调用 IInstallReferenceItem::GetReference 方法获得引用数据 FUSION_INSTALL_REFERENCE 结构。
上述函数和接口定义如下:
以下内容为程序代码:

public class Fusion
{
[ComImport, Guid("582dac66-e678-449f-aba6-6faaec8a9394"[img]/images/wink.gif[/img],
InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
public interface IInstallReferenceItem
{
[PreserveSig]
int GetReference(
out IntPtr ppRefData,
uint dwFlags, IntPtr pvReserved);
}

[ComImport, Guid("56b1a988-7c0c-4aa2-8639-c3eb5a90226f"[img]/images/wink.gif[/img],
InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
public interface IInstallReferenceEnum
{
[PreserveSig]
int GetNextInstallReferenceItem(
out IInstallReferenceItem ppRefItem,
uint dwFlags, IntPtr pvReserved);
}

public const string DLL_NAME = "fusion.dll";

[DllImportAttribute(DLL_NAME)]
public static extern int CreateInstallReferenceEnum(
out IInstallReferenceEnum ppRefEnum,
IAssemblyName pName,
uint dwFlags,
IntPtr pvReserved);
}



使用方法比较简单,唯一需要注意的是 IInstallReferenceItem::GetReference 方法取得的是一个指向 FUSION_INSTALL_REFERENCE 结构的指针,因此需要通过 Marshal.PtrToStructure 强行将被其指向内存转换为托管结构。
以下内容为程序代码:

public class ReferenceCollection : IEnumerable
{
private readonly AssemblyName _name;

private ArrayList _refs;

public void Refresh()
{
if(_refs == null)
_refs = new ArrayList();
else
_refs.Clear();

Fusion.IInstallReferenceEnum refEnum;

ComUtil.ComCheck(Fusion.CreateInstallReferenceEnum(out refEnum, _name._name, 0, IntPtr.Zero));

Fusion.IInstallReferenceItem item;

while(ComUtil.SUCCEEDED(refEnum.GetNextInstallReferenceItem(out item, 0, IntPtr.Zero)) && item != null)
{
IntPtr pRef;

ComUtil.ComCheck(item.GetReference(out pRef, 0, IntPtr.Zero));

Fusion.FUSION_INSTALL_REFERENCE objRef = (Fusion.FUSION_INSTALL_REFERENCE)
Marshal.PtrToStructure(pRef, typeof(Fusion.FUSION_INSTALL_REFERENCE));

_refs.Add(new InstallReference(objRef));
}
}



定义了引用数据的 FUSION_INSTALL_REFERENCE 结构,是 GAC 引用计数的核心数据所在。

dwFlags 字段表示此引用计数的类型,目前来说只用到一个必设的 ASSEMBLYINFO_FLAG_INSTALLED 标志;
guidScheme 字段内容是几个预定义 GUID 之一,表示此结构内容的意义,后面详细解释;
szIdentifier 字段则是系统级别的应用程序标识符;
szNonCannonicalData 字段则是应用程序级别的组件标识符;
以下内容为程序代码:

public class Fusion
{
public enum ASSEMBLYINFO_FLAG
{
ASSEMBLYINFO_FLAG_INSTALLED = 1,
ASSEMBLYINFO_FLAG_PAYLOADRESIDENT = 2
}

public static readonly Guid FUSION_REFCOUNT_UNINSTALL_SUBKEY_GUID = new Guid("{8cedc215-ac4b-488b-93c0-a50a49cb2fb8}"[img]/images/wink.gif[/img];
public static readonly Guid FUSION_REFCOUNT_FILEPATH_GUID = new Guid("{b02f9d65-fb77-4f7a-afa5-b391309f11c9}"[img]/images/wink.gif[/img];
references when you remove this.
public static readonly Guid FUSION_REFCOUNT_OPAQUE_STRING_GUID = new Guid("{2ec93463-b0c3-45e1-8364-327e96aea856}"[img]/images/wink.gif[/img];
public static readonly Guid FUSION_REFCOUNT_MSI_GUID = new Guid("{25df0fc1-7f97-4070-add7-4b13bbfd7cb8}"[img]/images/wink.gif[/img];

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
public struct FUSION_INSTALL_REFERENCE
{
public uint cbSize; // The size of the structure in bytes.
public uint dwFlags; // Reserved, must be zero.
public Guid guidScheme; // contains one of the pre-defined guids. The entity that adds the reference.
public string szIdentifier; // unique identifier for app installing this assembly.
public string szNonCannonicalData; // data is description; relevent to the guid above. A string that is only understood by the entity that adds the reference. The GAC only stores this string.
}
}



当 guidScheme 内容为 FUSION_REFCOUNT_MSI_GUID 时,表示此 Assembly 是通过 MSI 安装并进行维护的,其他字段内容 szIdentifier = "MSI", szNonCannonicalData = "Windows Installer"。通过 gacutil /lr 命令我们可以发现绝大多数 .NET Framework 自带的 Assembly 都是通过这种方式安装的。不过在后面要提到的手工安装中并不能使用这个为 MSI 内置的 Scheme。
当 guidScheme 内容为 FUSION_REFCOUNT_UNINSTALL_SUBKEY_GUID 时,szIdentifier 内容字符串被认为是系统添加删除程序面板中项目的 ID。如果 GAC 工具没有在注册表键 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall 下发现相同的 ID,则可以认为此 Assembly 的引用者已经不存在。
当 guidScheme 内容为 FUSION_REFCOUNT_FILEPATH_GUID 时,szIdentifier 内容字符串被认为是一个文件名。如果 GAC 工具发现此文件不存在,则可以认为此 Assembly 的引用者已经不存在。
最后当 guidScheme 内容为 FUSION_REFCOUNT_OPAQUE_STRING_GUID 时,Assemlby 的安装和引用者可以任意为 szIdentifier 和 szNonCannonicalData 赋值,自行维护 Assembly 的引用生命周期。

在安装和卸载 Assembly 时,都有一个参数可以显式参数 pRefData 指定引用数据。
以下内容为程序代码:

[ComImport, Guid("e707dcde-d1cd-11d2-bab9-00c04f8eceae"[img]/images/wink.gif[/img],
InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
public interface IAssemblyCache
{
[PreserveSig]
int UninstallAssembly(
uint dwFlags,
[MarshalAs(UnmanagedType.LPWStr)] string pszAssemblyName,
IntPtr pRefData,
out ASM_UNINSTALL_DISPOSITION pulDisposition);

//...

[PreserveSig]
int InstallAssembly(
ASM_INSTALL_FLAG dwFlags,
[MarshalAs(UnmanagedType.LPWStr)] string pszManifestFilePath,
IntPtr pRefData);
}



使用时可以忽略此参数,也可以使用上述的某种模式来管理引用的生命周期。为了不造成 GAC 的膨胀,推荐所有手工安装都指定一种引用模式。使用方式如下:
以下内容为程序代码:

public class FusionUtil : ...
{
public void Install(string fileName, IntPtr pRef)
{
IAssemblyCache cache;

ComUtil.ComCheck(Fusion.CreateAssemblyCache(out cache, 0));

ComUtil.ComCheck(cache.InstallAssembly(Fusion.ASM_INSTALL_FLAG.IASSEMBLYCACHE_INSTALL_FLAG_REFRESH, fileName, pRef));
}

public void Install(string fileName)
{
Install(fileName, IntPtr.Zero);
}

public void Install(string fileName, InstallReference objRef)
{
IntPtr pRef = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(Fusion.FUSION_INSTALL_REFERENCE)));

Marshal.StructureToPtr((Fusion.FUSION_INSTALL_REFERENCE)objRef, pRef, false);
try
{
Install(fileName, pRef);
}
finally
{
Marshal.DestroyStructure(pRef, typeof(Fusion.FUSION_INSTALL_REFERENCE));

Marshal.FreeCoTaskMem(pRef);
}
}

public UninstallDisposition Uninstall(string assemblyName, IntPtr pRef)
{
IAssemblyCache cache;

ComUtil.ComCheck(Fusion.CreateAssemblyCache(out cache, 0));

ASM_UNINSTALL_DISPOSITION disposition;

ComUtil.ComCheck(cache.UninstallAssembly(0, assemblyName, pRef, out disposition));

return (UninstallDisposition)disposition;
}

public UninstallDisposition Uninstall(string assemblyName)
{
return Uninstall(assemblyName, IntPtr.Zero);
}

public UninstallDisposition Uninstall(string assemblyName, InstallReference objRef)
{
IntPtr pRef = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(Fusion.FUSION_INSTALL_REFERENCE)));

Marshal.StructureToPtr((Fusion.FUSION_INSTALL_REFERENCE)objRef, pRef, false);
try
{
return Uninstall(assemblyName, pRef);
}
finally
{
Marshal.DestroyStructure(pRef, typeof(Fusion.FUSION_INSTALL_REFERENCE));

Marshal.FreeCoTaskMem(pRef);
}
}
}



<< Home

This page is powered by Blogger. Isn't yours?