Type-safe Managed wrappers for kernel32!GetProcAddress

Pinvoke is cool in managed code, but sometimes you need to get straight at kernel32!GetProcAddress. For example, maybe you need dynamic control over which unmanaged dll you want to load. But that returns a  void*, which is a major no-no in managed code.  Here's a helper class I wrote (as part of the growing unmanaged code support in MDbg) that provides a pretty managed veneer over LoadLibrary, GetProcAddress, and FreeLibrary.

Sample usage may be:

    using(UnmanagedLibrary lib = new UnmanagedLibrary("kernel32")  // becomes call to LoadLibrary
   { 
      Action<String> function = lib.GetUnmanagedFunction<Action<String>>("DeleteFile"); // GetProcAddress
      function(@"c:\tmp.txt");
   } // implict call to lib.Dispose, which calls FreeLibrary.

At the end of the day, that's what you'd expect for managed code. But there were actually a lot of little things that had to be smoothed over to make it work right.

  1. It provides type-safe delegate wrappers over GetProcAddress.  Marshal.GetDelegateForFunctionPointer is a good start, but it's not type-safe. Type-safety requires generic-constraints and a little fancy footwork (due to CS0702).
  2. Converts win32 errors to exceptions in the places you'd expect for a natural .NET usage. This is easily solved with Marshal.GetHRForLastWin32Error(),                Marshal.ThrowExceptionForHR.
  3. It uses SafeHandles for the unmanaged library. See Brian Grunkmeyer's post and this MSDN article for more on safe handles.
  4. It wraps the the raw pinvokes to LoadLibrary, GetProcAddress, and FreeLibrary.
  5. It uses IDisposable. Since the underlying unmanaged resource is using a SafeHandle (which has a finalizer), the wrapper class does not need a finalizer.

Big disclaimer!!
Be careful of calling kernel32!FreeLibrary from managed code! This is unsafe and can crash if done wrong.  FreeLibrary forcibly unloads the dll, and this can mean dangling pointers for both the delegates you get back (which wrap function pointers into the dll) and any objects returned from those delegates that are implemented the dll.

1. The delegates you get back wrap unmanaged function pointers in the dll. Once you unload the dlls, those delegates are now referring to dangling pointers. Invoking them may crash or do random things.

2. If you GetProcAddress a function that gives you back an object that is implemented in the unmanaged dll (such as an IUnknown), then you need to ensure all that nothing will call on that object once FreeLibrary is called.  For example, if you load A.dll, get a function that returns an IUnknown instance which is implemented in A.dll, you're in dangerous waters. COM-Interop may call AddRef/Release/QueryInterface on that IUnknown at random times (I think the only real issue in the current implementation is a call to Release). So how do you ensure that the CLR doesn't call Release after you've called FreeLibrary? There are a few rocket-science techniques, but overall it's a very hard problem. 

Here's the code:

 
    /// <summary>
    /// Utility class to wrap an unmanaged DLL and be responsible for freeing it.
    /// </summary>
    /// <remarks>This is a managed wrapper over the native LoadLibrary, GetProcAddress, and
    /// FreeLibrary calls.
    /// </example>
    public sealed class UnmanagedLibrary : IDisposable
    {
        #region Safe Handles and Native imports
        // See https://msdn.microsoft.com/msdnmag/issues/05/10/Reliability/ for more about safe handles.
        [SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)]        
        sealed class SafeLibraryHandle : SafeHandleZeroOrMinusOneIsInvalid
        {
            private SafeLibraryHandle() : base(true) { }

            protected override bool ReleaseHandle()
            {
                return NativeMethods.FreeLibrary(handle);
            }
        }

        static class NativeMethods
        {
            const string s_kernel = "kernel32";
            [DllImport(s_kernel, CharSet = CharSet.Auto, BestFitMapping = false, SetLastError = true)]
            public static extern SafeLibraryHandle LoadLibrary(string fileName);

            [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
            [DllImport(s_kernel, SetLastError = true)]
            [return: MarshalAs(UnmanagedType.Bool)]
            public static extern bool FreeLibrary(IntPtr hModule);

            [DllImport(s_kernel)]
            public static extern IntPtr GetProcAddress(SafeLibraryHandle hModule, String procname);
        }
        #endregion // Safe Handles and Native imports

        /// <summary>
        /// Constructor to load a dll and be responible for freeing it.
        /// </summary>
        /// <param name="fileName">full path name of dll to load</param>
        /// <exception cref="System.IO.FileNotFound">if fileName can't be found</exception>
        /// <remarks>Throws exceptions on failure. Most common failure would be file-not-found, or
        /// that the file is not a  loadable image.</remarks>
        public UnmanagedLibrary(string fileName)
        {
            m_hLibrary = NativeMethods.LoadLibrary(fileName);
            if (m_hLibrary.IsInvalid)
            {
                int hr = Marshal.GetHRForLastWin32Error();
                Marshal.ThrowExceptionForHR(hr);
            }
        }

        /// <summary>
        /// Dynamically lookup a function in the dll via kernel32!GetProcAddress.
        /// </summary>
        /// <param name="functionName">raw name of the function in the export table.</param>
        /// <returns>null if function is not found. Else a delegate to the unmanaged function.
        /// </returns>
        /// <remarks>GetProcAddress results are valid as long as the dll is not yet unloaded. This
        /// is very very dangerous to use since you need to ensure that the dll is not unloaded
        /// until after you're done with any objects implemented by the dll. For example, if you
        /// get a delegate that then gets an IUnknown implemented by this dll,
        /// you can not dispose this library until that IUnknown is collected. Else, you may free
        /// the library and then the CLR may call release on that IUnknown and it will crash.</remarks>
        public TDelegate GetUnmanagedFunction<TDelegate>(string functionName) where TDelegate : class
        {
            IntPtr p = NativeMethods.GetProcAddress(m_hLibrary, functionName);

            // Failure is a common case, especially for adaptive code.
            if (p == IntPtr.Zero)
            {
                return null;
            }
            Delegate function = Marshal.GetDelegateForFunctionPointer(p, typeof(TDelegate));

            // Ideally, we'd just make the constraint on TDelegate be
            // System.Delegate, but compiler error CS0702 (constrained can't be System.Delegate)
            // prevents that. So we make the constraint system.object and do the cast from object-->TDelegate.
            object o = function;

            return (TDelegate)o;
        }

        #region IDisposable Members
        /// <summary>
        /// Call FreeLibrary on the unmanaged dll. All function pointers
        /// handed out from this class become invalid after this.
        /// </summary>
        /// <remarks>This is very dangerous because it suddenly invalidate
        /// everything retrieved from this dll. This includes any functions
        /// handed out via GetProcAddress, and potentially any objects returned
        /// from those functions (which may have an implemention in the
        /// dll).
        /// </remarks>
        public void Dispose()
        {
            if (!m_hLibrary.IsClosed)
            {
                m_hLibrary.Close();
            }
        }

        // Unmanaged resource. CLR will ensure SafeHandles get freed, without requiring a finalizer on this class.
        SafeLibraryHandle m_hLibrary;

        #endregion
    } // UnmanagedLibrary