Partager via


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

Comments

  • Anonymous
    January 07, 2007
    Would it be worthwhile to wrap the delegate returned in another delegate, which first checked if the library's been disposed or otherwise unloaded before invoking the unmanaged function? In this way, I think you could avoid problem #1. It would require some work, but reflecting over the delegate type should allow you to do some lightweight codegen to construct the function call, I think.

  • Anonymous
    January 08, 2007
    KFarmer - actually Brian Grunkmeyer and I talked about exactly that! A big drawback is that to get a type-safe delegate signature, you'd need to emit delegate wrappers on the fly (emit, lcg) or play some very aggressive (and unfriendly) games with generics. That's actually really heavy weight relative to what's here.   If we could have just handed back a proxy object that had a type-safe function invocation overload, then this would be easy - but .NET doesn't support that like C++ does. So then we have to make a trade-off: performance for safety checks.  In this sample, I decided closer to performance.

  • as you noted, it also doesn't solve #2, which I think is by far the scarier one. with #2 outstanding, I think the safety increase of just solving #1 is marginal.
  • It would be very easy for it to give a false sense of security. Instead, by closely modelling the win32 APIs, this is much simpler to understand and hopefully thus makes it much easier to be aware of the problems.
  • It's would be a lot of extra work and complexity (at least tripple the size) for something that if used properly, would never be necessary.
  • Anonymous
    February 13, 2007
    How do I call function with 2,3,n number of parmeters each one is a different type ?

  • Anonymous
    February 15, 2007
    Shai - are you referring to a varargs function like printf?

  • Anonymous
    March 01, 2007
    Is it possible to set the calling convention for delegates to unmanaged functions obtained this way?