Click here to Skip to main content
Click here to Skip to main content

Avoiding IDisposable while still working with unmanaged resources

, 10 Oct 2005 CPOL
Rate this:
Please Sign up or sign in to vote.
The IDisposable pattern and a way to avoid it.

Sample Image - managed_shell_link.gif

Introduction.

This article discusses the infamous IDisposable pattern and offers a way to avoid it.

Contents

Managed and unmanaged resources

In .NET, the Common Language Runtime (CLR, for short) manages the memory for you. However, most of the other resources should be managed by the programmer. This is usually done by implementing the class' finalizer and/or by implementing the IDisposable interface. More on it can be found in the following MSDN articles:

and also in following The Code Project articles:

To sum it up, Microsoft provides four models for an object:

  • Simple - a completely managed object that neither uses unmanaged resources nor reference objects that in turn use unmanaged resources.
  • Finalizable - a managed object that references unmanaged resources and releases them in its finalizer method (it looks like a C++ destructor in C#).
  • Disposable - a managed object that either uses unmanaged resources, or references objects that in turn use unmanaged resources; the resources are released in the IDisposable.Dispose method.
  • Both - a managed object that uses unmanaged resources or reference objects that in turn use unmanaged resources; in the best scenario, it releases them in the IDisposable.Dispose method; in the worst scenario, it releases the resources in the finalizer.

To simplify the life of the C# programmer, they included the using statement that makes code dealing with disposable objects cleaner.

Why I don't like all the patterns but Simple

To me, it is obvious that the Finalizable model is unacceptable for real world applications. They live in shared multi-user environments. Imagine that your finalizable object holds, for instance, a file handle for an unknown period of time. A user starts your application and works, another user starts another instance of the application but gets an error message because the file is blocked by the first one.

Disposable objects and objects that implement the Both model are better, but I have problems with them. It is not always clear if the object is disposable. Did you know that System.Drawing.Drawing2d.Matrix objects are disposable? I found this out only when I discovered a memory leak in a program of mine. But the worst thing is that I sometimes forget to use the using statement, and this is the real problem!

A bad thing is that too many objects in the BCL are disposable. In fact, most UI objects are. No wonder many people consider the System.Windows.Forms namespace evil. A good thing about the UI library from the upcoming Windows Vista is that the System.Windows namespace is managed and does not contain disposable objects.

Can we, developers, avoid using IDisposable and finalizers while continuing to use legacy code, platform invoke, and COM interop? I'm not sure if it is possible in 100% of cases, but let me show you an example of how it can be done. The idea is to "shadow" all managed data structures in managed classes and group all interop calls in a couple of (huge) methods. This example provides an ability for .NET programs to deal with shell links, also known as shortcuts.

Shell links

The Windows Shell is a program that interacts with users and lets them run programs, copy files, and perform some routine tasks. The default Shell for Windows is the Windows Explorer, but it can be any other program like the Program Manager, LiteStep, or even cmd.exe.

Windows Explorer's executable file, explorer.exe, is actually a host for the Shell. Most of the Shell code resides in a big dynamically loaded library named shell32.dll, which is also a COM in-process server.

The Shell works with co-called shell items. A shell item can be a file, a network connection, a printer. Items group to folders. A folder can be a file folder, in which case it contains only file items, or a virtual folder, in which case it may contain non-file items. A file item can be a file, a file folder, or a shell link.

A link is a file system entity that refers to another file. Unix-like systems have symbolic links, special directory entries that function as links. Windows 95, where shell links first appeared, used a modification of the old MS-DOS FAT file system. This filesystem never had anything like symbolc links, so Microsoft decided to use files with the *.LNK extension as links. Though the NTFS filesystem has always been supporting hard-links (files with multiple names), and nowadays supports true symbolic links, the Shell still uses LNK files as links. Because it is the Shell, not the filesystem, that provides facilities to manage links, we call LNK files shell links.

A good Windows application should work with shell links transparently. And because we all want our .NET applications to also be good Windows applications, we need somehow to deal with shell links.

From the developer's point of view, a shell link is a COM object that implements IShellLink, IPersistFile, and some other interfaces. An LNK file is a persistent (serialized) representation of such an object.

IPersistFile is an old OLE2 interface, and is nothing in particular. On the contrary, IShellLink is a very unusual beast.

First of all, it is not a single interface; it's actually two separate interfaces. Remember, in Win16 times, we had only one version of each API function, but while creating Windows NT, Microsoft decided to split each function that dealt with strings in two versions: ones with an A postfix for "narrow" or ANSI text, and ones with a W postfix for "wide" or UNICODE (actually, it is UTF-16 now, but in 1993, it was true UNICODE) text. However, for the 32-bit version of the OLE2 subsystem, the designers made a wise decision to use only UNICODE strings to avoid duplication of COM interfaces.

But later, developers of Windows 95, for some reason, violated this rule and defined for the new Shell, some COM-compatible interfaces that worked with ANSI strings. One of such interfaces is the interface with GUID {000214EE-0000-0000-C000-000000000046}, known as the original IShellLink. The original shell link object implements the IPersistFile interface anyway, and why they made such a mistake, sorry, decision, is above my understanding.

The developers of Windows NT 4.0 Shell realized this problem, and fixed it by renaming IShellLink to IShellLinkA and introducing a new interface with the GUID {000214F9-0000-0000-C000-000000000046}. This interface is a copy of IShellLinkA; its name obviously is IShellLinkW and the only difference between them is that the latter works with UNICODE strings.

The funniest thing is that if you work with shell links on NT-based Operating Systems (I faced it on Windows 2000), the COM object sometimes wants you to use the IShellLinkA interface. I.e., if you create a shell link object and query for IShellLinkW, this call sometimes fails, but a subsequent query for IShellLinkA succeeds. What circumstances lead to that is also above my knowledge.

All right, let's write a class that interops with the shell and gives us the ability to work with shell links from managed code.

Shell link object implementation

You can find many implementations of the shell link object on the Internet, or even here on The Code Project. However, they all use similar scenarios:

  1. Create an instance of the shell link COM object in the constructor or another initialization method;
  2. Use it and the interfaces to implement load, save, set, and retrieve the link's properties;
  3. Release the COM object in either the IDisposable.Dispose implementation or in the finalizer.

I.e., these objects implement the Both model and hold the reference to the COM object while active. I am going to show you how to avoid it and make the shell link object fully managed, at least most of its lifetime.

The IShellLink interface implements a number of GetXXX/SetXXX methods we can consider as "properties". It also has a "true" method, Resolve, to verify if the file the link points to is still available, and to fix the link, if not. Object persistence is implemented through the IPersistFile interface. So our ShellLink object is going to have a set of public properties that correspond to the GetXXX/SetXXX methods and the two methods - Load and Save. The code does not cover the full shell link functionality. I have omitted the IShellLink.GetIDList method because it works with non-file items, and also omitted the IShellLink.SetRelativePath method and left its implementation to you.

Let's begin from the Arguments property:

private string arguments;

public string Arguments
{
    get
    {
        return arguments;
    }

    set
    {
        arguments = Str(value);
    }
}

The only interesting point here is the call to the Str() function in the property's setter. This is a helper function that returns a String.Empty object if it receives a null reference. Actually, I use here the Null Object Design Pattern.

private static string Str(string str)
{
    return (null == str) ? String.Empty : str;
}

The same way, I define the Description, Path, and WorkingDirectory properties. I also define the non-string property named Hotkey:

private short hotkey;

public short Hotkey
{
    get
    {
        return hotkey;
    }

    set
    {
        hotkey = value;
    }
}

According to MSDN's IShellLink.GetHotKey method description, the hot key is a 16-bit value that consists of a virtual key code in the low-order byte and a set of modifier flags in the high-order byte. Ideally, I should have created a class or a structure to simplify this combination, but didn't want to waste my time. It should be easy, and you can do it yourself.

There's also a ShowCmd property I have defined like this:

private ShowCommand showCmd;

public ShowCommand ShowCmd
{
    get
    {
        return showCmd;
    }

    set
    {
        showCmd = value;
    }
}

The ShowCommand enumeration is defined as:

public enum ShowCommand: int
{
    ShowNormal = 1,
    ShowMaximized = 3,
    ShowMinimized = 7
}

and its values correspond to the respective SW_xxx constants from the Platform SDK header file winuser.h.

The most interesting things are the IconPath and IconIndex properties. From the shell link COM object point of view, there are two methods: GetIconLocation and SetIconLocation. And the icon location consists of a path to the file that contains the icon and the icon's index in the file's resource. The icon index is equal 0 for files with a *.ICO extension.

I have implemented these two properties much the same way as the rest, but treat them a bit differently.

Now, let's see the Load method implementation. The idea is to perform all the interop calls inside this method as an atomic transaction so you can work with the shell link object just like you work with strings, never bothering to call Dispose or worrying to have memory leaks.

First, let me declare the GUID of the shell link COM object:

private static readonly Guid CLSID_ShellLink = 
               new Guid("00021401-0000-0000-C000-000000000046");

Now the code that uses this GUID (comments omitted for brevity):

[SecurityPermission(SecurityAction.Demand, UnmanagedCode=true)]
public void Load(string linkFileName)
{
    if(null == linkFileName)
        throw new ArgumentNullException("linkFileName", 
                  "A name of the link file cannot be null");
    if(!File.Exists(linkFileName))
        throw new FileNotFoundException("Link not found", linkFileName);
    new FileIOPermission(FileIOPermissionAccess.Read | 
        FileIOPermissionAccess.PathDiscovery, linkFileName).Demand();
    object sl = null;
    try
    {
        Type slType = Type.GetTypeFromCLSID(CLSID_ShellLink);
        sl = Activator.CreateInstance(slType);
        IPersistFile pf = sl as IPersistFile;
        pf.Load(linkFileName, 0);
        int showCmd;
        StringBuilder builder = new StringBuilder(INFOTIPSIZE);
        IShellLinkW shellLinkW = sl as IShellLinkW;
        if(null == shellLinkW)
        {
            IShellLinkA shellLinkA = sl as IShellLinkA;
            if(null == shellLinkA)
                ThrowInvalidComObjectException();
            shellLinkA.GetArguments(builder, builder.Capacity);
            this.arguments = builder.ToString();
            shellLinkA.GetDescription(builder, builder.Capacity);
            this.description = builder.ToString();
            shellLinkA.GetHotkey(out this.hotkey);
            shellLinkA.GetIconLocation(builder, builder.Capacity, out this.iconIndex);
            this.iconPath = builder.ToString();
            Win32FindDataA wfd;
            shellLinkA.GetPath(builder, builder.Capacity, out wfd, SLGP_UNCPRIORITY);
            this.path = builder.ToString();
            shellLinkA.GetShowCmd(out showCmd);
            shellLinkA.GetWorkingDirectory(builder, builder.Capacity);
            this.workingDirectory = builder.ToString();
        }
        else
        {
            shellLinkW.GetArguments(builder, builder.Capacity);
            this.arguments = builder.ToString();
            shellLinkW.GetDescription(builder, builder.Capacity);
            this.description = builder.ToString();
            shellLinkW.GetHotkey(out this.hotkey);
            shellLinkW.GetIconLocation(builder, builder.Capacity, out this.iconIndex);
            this.iconPath = builder.ToString();
            Win32FindDataW wfd;
            shellLinkW.GetPath(builder, builder.Capacity, out wfd, SLGP_UNCPRIORITY);
            this.path = builder.ToString();
            shellLinkW.GetShowCmd(out showCmd);
            shellLinkW.GetWorkingDirectory(builder, builder.Capacity);
            this.workingDirectory = builder.ToString();
        }
        this.showCmd = (ShowCommand)showCmd;
    }
    finally
    {
        if(null != sl)
            Marshal.ReleaseComObject(sl);
    }
    GC.KeepAlive(this);
}

The method demands execution of unmanaged code using the SecurityPermissionAttribute class. Note that the ability to call unmanaged code is also being requested at the assembly level, so the sample code won't work in a partially trusted environment, even in a LocalIntranet security zone.

First we check the parameters - if we received null instead of a file name and if the link file really exists. Then we demand ability to read the shell link file. I perform these security checks because CLR won't do it for me as it does for BCL classes, and the end user would get a COM exception instead of a security exception.

Then, I instantiate the shell link COM object. I do it inside a try/finally block, so even if some code fails, the COM object will be released. Then I query for the IPersistFile interface that is defined in the code later.

I prepare a couple of variables for later use, the most notable is the builder. It's going to be used as the buffer for strings. Its size, the constant INFOTIPSIZE, is taken from the Platform SDK, and declared as:

private const int INFOTIPSIZE = 1024;

Note that in theory, the buffer of this size may not be enough because MSDN states that UNICODE Win32 API functions that perform file operations can operate with file paths as long as 32000 characters (http://msdn.microsoft.com/library/default.asp?url=/library/en-us/fileio/fs/findfirstfileex.asp). So far, I have never seen file names like that.

Now the most interesting part. I first query the shell link COM object for the IShellLinkW interface. If the query fails, I request the IShellLinkA interface. If both queries fail, I am (or better say, the end user is) in real trouble and throw an exception. If one of the interfaces is available, I call the proper GetXXX methods and fill the private fields with the returned values. This is actually the work that the interop should have done and it does for "normal" COM objects.

Both branches of the code look identical; all the difference is in the way the marshaler passes the strings and Win32FindDataX structures between managed and unmanaged code. And these structures are actually versions of the WIN32_FIND_DATA structure. I don't need the values from them, so I define the structures like this (the ANSI version):

[StructLayoutAttribute(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
internal struct Win32FindDataA
{
    private uint dwFileAttributes;
    private FILETIME ftCreationTime;
    private FILETIME ftLastAccessTime;
    private FILETIME ftLastWriteTime;
    private uint nFileSizeHigh;
    private uint nFileSizeLow;
    private uint dwReserved0;
    private uint dwReserved1;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=260)]
    private string cFileName;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=14)]
    private string cAlternateFileName;
}

Declaring the fields private makes the Assembly Analyser and FxCop happy.

After getting the values from the COM object, I translate the ShowCmd property value and release the COM object. The last note is a call to GC.KeepAlive. I do it to guard the shell link object from being garbage collected during the call to the Load method.

The Save method is implemented using the same technique.

I also provide a static method of the ShellLink class that checks if the file with the given name is a shell link or not:

[SecurityPermission(SecurityAction.Demand, UnmanagedCode=true)]
public static bool IsShellLink(string filePath)
{
    if(null == filePath)
        throw new ArgumentNullException(filePath, A name of the file cannot be null);
    if(!File.Exists(filePath))
        throw new FileNotFoundException(Cannot check file that doesn't exist, filePath);

    new FileIOPermission(FileIOPermissionAccess.Read | 
        FileIOPermissionAccess.PathDiscovery, filePath).Demand();

    SHFileInfo sfi = new SHFileInfo();
    SHGetFileInfo(filePath, 0, out sfi, Marshal.SizeOf(sfi), SHGFI_ATTRIBUTES);
    return SFGAO_LINK == (sfi.Attributes & SFGAO_LINK);
}

At the end of the ShellLink class source code, I put a region with all interop declarations and definitions. Though FxCop insists on using a separate class named NativeMethods, I believe it's overkill for a sample.

Now the ShellLink object becomes a simple managed object. I don't have to worry about IDisposable, using statements, and finalizers. And I believe that my code works a little bit faster than IDisposable-based versions because all interop calls are made in a single batch, the COM object gets released immediately, and doesn't consume precious unmanaged memory.

So when you deal with legacy code, try to stick with the rules:

  • Group all calls to the legacy code in one method;
  • Do not hold unmanaged resources; instead, keep managed "shadows" of the legacy code data.

The less classes that implement IDisposable, the better the world is.

License

The code accompanying the article is provided as is, and considered to be public domain. Use it at your own risk. If you find an error, please drop me an e-mail.

The article's accompanying code

The code that illustrates the article is developed using the freeware Open Source #Develop IDE (http://www.icsharpcode.net/OpenSource/SD/Default.aspx) so the package contains a #Develop combine.

I have also provided a nant.build file. To use it, you should have NAnt 0.85 with NUnit 2.2 and NDoc 1.3.1 installed. The build file makes four subprojects: Pvax.Shell.dll that contains the ShellLink class, Pvax.Shell.Tests.dll that contains a couple of unit tests, and two test projects - resolve.exe, a console application, and LinkView.exe, a Windows Forms application. It also runs the unit tests and builds the documentation file Pvax.Shell.chm.

You can also build the debug version of the main library and both test applications using a build.bat file. Thanks to Lutz Roeder (http://www.aisto.com/roeder/dotnet/) for his CommandBar sample library that contains the build.bat that "inspired" me on writing my own.

Sorry guys, I have no Visual Studio, so I cannot provide you with a solution file. You probably can build it yourself; just create four subprojects and add the proper source files there. I know there is a NUnit add-on for VS, so you should be able to run the tests from the IDE.

#Develop's Assembly Analyzer and FxCop say that the assembly Pvax.Shell.dll should be strong named and that the Pvax.Shell namespace contains too little types; the latter also says a lot about imperative security. Don't bother, it's just a sample.

Links

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Alexey A. Popov
Web Developer
Russian Federation Russian Federation
I'm a system administrator from Moscow, Russia. Programming is one of my hobbies. I presume I'm one of the first Russians who created a Web site dedicated to .Net known that time as NGWS. However, the Web page has been abandoned a long ago.

Comments and Discussions

 
GeneralAuthor is definitely philosopher Pinmemberwukas10-Oct-05 3:26 
GeneralIDisposable my friend Pinmembermcarbenay23-Aug-05 4:14 
GeneralRe: IDisposable my friend PinmemberAlexey A. Popov28-Aug-05 20:58 
GeneralArticle Summary PinmemberSteven Campbell22-Aug-05 8:24 
AnswerRe: Article Summary PinsussDavid Roh22-Sep-05 8:39 
GeneralNothing wrong with IDisposable. Pinmemberwout de zeeuw21-Aug-05 2:53 
GeneralRe: Nothing wrong with IDisposable. PinmemberAlexey A. Popov21-Aug-05 9:04 
GeneralRe: Nothing wrong with IDisposable. Pinmemberwout de zeeuw21-Aug-05 9:26 
GeneralRe: Nothing wrong with IDisposable. PinmemberAlexey A. Popov21-Aug-05 9:46 
GeneralRe: Nothing wrong with IDisposable. Pinmemberwout de zeeuw21-Aug-05 13:03 
GeneralRe: Nothing wrong with IDisposable. PinmemberNemanja Trifunovic10-Oct-05 3:45 
GeneralRe: Nothing wrong with IDisposable. PinmemberHal Angseesing16-Nov-05 8:15 
GeneralNice article, bad conclusion... PinmemberMarc Brooks20-Aug-05 9:02 
GeneralRe: Nice article, bad conclusion... PinmemberAlexey A. Popov21-Aug-05 0:13 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.141223.1 | Last Updated 10 Oct 2005
Article Copyright 2005 by Alexey A. Popov
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid