Sunday, March 27, 2011

Sharing data in .Net 4 (Let’s make our life easier!)

Sharing large volumes of data between .Net processes running in a single machine has lacked true enterprise-level support from the core CLR and foundation classes for a long time. Since .Net 1.x there have been several solutions which fell short of perfect for different reasons.

Using available .Net IPC features such as Remoting, IP sockets or Message Queues lacked the performance required to quickly transfer or make accessible large volumes of data between several processes (establishing port connections, serializing and deserializing data, and splitting large volumes into small chunks, ends up being slow, hard work and mostly error-prone).

Looking at the native Win32 API, the solution of choice to support sharing large volumes of data between processes has always been usage of Memory Mapped Files - MMF (with or without backup from persistent storage, i.e. files in the file system). Let’s face it, this is the core mechanism used by the OS to load, among others, DLLs into the address space of different processes, minimizing the need to continually read the same files over and over, and bypassing duplication of data in memory (by the way, in kernel mode, memory mapped files are materialized as section objects).
Indeed memory mapped files could have been used in .Net (1.x and 2.0) in the past, albeit with some effort. Let’s thus compare two scenarios: P/Invoke our way to native resources in .Net 2.0 (we could also create a COM server and access it through interop features, which would achieve nothing other than adding an extra layer into the native Win32 API) or using the new .Net 4.0 support for Memory Mapped Files.

Scenario 1: The P/Invoke way

First we need to start out by importing the required Win32 API calls. If we are working with persisted file support then we need at least 3 different calls to start us out.
First, we need to access the file stored in file system (if we are working with non-persisted MMF this call won’t be required).

[ DllImport("kernel32", SetLastError=true, CharSet=CharSet.Auto) ]
public static extern IntPtr CreateFile(
    String lpFileName,
    int dwDesiredAccess,
    int dwShareMode,
    IntPtr lpSecurityAttributes,
    int dwCreationDisposition,
    int dwFlagsAndAttributes,
    IntPtr hTemplateFile);
Then, we need to map that file into an MMF object.
[ DllImport("kernel32", SetLastError=true, CharSet=CharSet.Auto) ]
public static extern IntPtr CreateFileMapping(
   IntPtr hFile,
   IntPtr lpAttributes,
   int flProtect,
   int dwMaximumSizeLow,
   int dwMaximumSizeHigh,
   String lpName);
And finally, we need to map a view of that file into the process.
[ DllImport("kernel32", SetLastError=true) ]
public static extern IntPtr MapViewOfFile(
   IntPtr hFileMappingObject,
   int dwDesiredAccess,
   int dwFileOffsetHigh,
   int dwFileOffsetLow,
   int dwNumBytesToMap);
On the other hand, if we are handling the process that is accessing a previously created MMF, we would then replace the CreateFile and CreateFileMapping calls by a single method.
[ DllImport("kernel32", SetLastError=true, CharSet=CharSet.Auto) ]
public static extern IntPtr OpenFileMapping(
   int dwDesiredAccess,
   bool bInheritHandle,
   String lpName);
At the end, cleaning up resources would require us to strive even further and continue with our P/Invoke process and import even more calls, namely
[ DllImport("kernel32", SetLastError=true) ]
public static extern bool UnmapViewOfFile(IntPtr lpBaseAddress);
to release process-bound view and finally
[ DllImport("kernel32", SetLastError=true) ]
public static extern bool CloseHandle(IntPtr handle);
to release OS-implicit handles.
After all this work, we would still be required to work our way through unmanaged-to-managed datatype magic and we would most definitely end up we code such as this just to create the MMF.

public MemoryMappedFileWin32(String fileName, MapProtection protection,
            MapAccess access, long maxSize, String name)
{
  IntPtr hFile = IntPtr.Zero;
            try
            {
                m_hMap = MMFNative.OpenFileMapping((int)access, false, name);
                if (m_hMap == NULL_ACCESS)
                {
                    hFile = MMFNative.CreateFile(fileName, (int)access, 0,
                        IntPtr.Zero, OPEN_ALWAYS, 0, IntPtr.Zero);
                    if ((hFile != NULL_HANDLE) && (hFile != (System.IntPtr)INVALID_HANDLE_VALUE))
                    {
                        m_hMap = MMFNative.CreateFileMapping(
                            hFile, IntPtr.Zero, (int)protection,
                            0,(int)(maxSize & 0xFFFFFFFF), name);
                        if (m_hMap == NULL_HANDLE)
                            throw new FileMapException(Marshal.GetHRForLastWin32Error());
                    }
                    else
                        throw new FileMapException(Marshal.GetHRForLastWin32Error());
                }
            }
            finally
            {
                if ((hFile != NULL_HANDLE) && (hFile != (System.IntPtr)INVALID_HANDLE_VALUE))
                    MMFNative.CloseHandle(hFile);
            }
}
Afterwards, we would need to map the MMF object into our process view, and we would end up with something of the sorts.