Error on opening MemoryMappedFile in another process, C# compact framework

1

I have created a MemoryMappedFile using C#.Net Compact Framework on WinCE7. When I am trying to open the same MemorymappedFile in another process, I am getting a null file handle.

Here is the code I am using.

namespace MMFWriteDemo
{
    [Serializable]
    public class FileMapIOException : IOException
    {

        private int m_win32Error;
        public int Win32ErrorCode
        {
            get { return m_win32Error; }
        }
        public override string Message
        {
            get
            {
                if (Win32ErrorCode != 0)
                    return base.Message + " (" + Win32ErrorCode + ")";

                return base.Message;
            }
        }    

        public FileMapIOException(int error)
            : base()
        {
            m_win32Error = error;
        }
        public FileMapIOException(string message)
            : base(message)
        {
        }
        public FileMapIOException(string message, Exception innerException)
            : base(message, innerException)
        {
        }

    } // class FileMapIOException

    public enum MapAccess
    {
        FileMapCopy = 0x0001,
        FileMapWrite = 0x0002,
        FileMapRead = 0x0004,
        FileMapAllAccess = 0x001f,
    }

    [Flags]
    public enum MapProtection
    {
        PageNone = 0x00000000,
        // protection - mutually exclusive, do not or
        PageReadOnly = 0x00000002,
        PageReadWrite = 0x00000004,
        PageWriteCopy = 0x00000008,
        // attributes - or-able with protection
        SecImage = 0x01000000,
        SecReserve = 0x04000000,
        SecCommit = 0x08000000,
        SecNoCache = 0x10000000,
    }

    internal class Win32MapApis
    {
        [DllImport("coredll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern IntPtr CreateFile(
           String lpFileName, int dwDesiredAccess, int dwShareMode,
           IntPtr lpSecurityAttributes, int dwCreationDisposition,
           int dwFlagsAndAttributes, IntPtr hTemplateFile);

        [DllImport("coredll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern IntPtr CreateFileMapping(
           IntPtr hFile, IntPtr lpAttributes, int flProtect,
           int dwMaximumSizeLow, int dwMaximumSizeHigh,
           String lpName);

        [DllImport("coredll", SetLastError = true)]
        public static extern bool FlushViewOfFile(
           IntPtr lpBaseAddress, IntPtr dwNumBytesToFlush);

        [DllImport("coredll", SetLastError = true)]
        public static extern IntPtr MapViewOfFile(
           IntPtr hFileMappingObject, int dwDesiredAccess, int dwFileOffsetHigh,
           int dwFileOffsetLow, IntPtr dwNumBytesToMap);

        //[DllImport("coredll", SetLastError = true, CharSet = CharSet.Auto)]
        //public static extern IntPtr OpenFileMapping(
        //   int dwDesiredAccess, bool bInheritHandle, String lpName);

        public static IntPtr OpenFileMapping(uint dwDesiredAccess, bool bInheritHandle, string lpName)
        {
            IntPtr t_pHandle = Win32MapApis.CreateFileMapping(new IntPtr(-1), IntPtr.Zero, (int)MapAccess.FileMapRead, 0, 0, lpName);
            return t_pHandle;
        }

        [DllImport("coredll", SetLastError = true)]
        public static extern bool UnmapViewOfFile(IntPtr lpBaseAddress);

        [DllImport("coredll", SetLastError = true)]
        public static extern bool CloseHandle(IntPtr handle);
        [DllImport("coredll.dll", SetLastError = true)]
        public static extern Int32 GetLastError();
    } // class Win32MapApis

    public class MemoryMappedFile : MarshalByRefObject, IDisposable
    {
        //! handle to MemoryMappedFile object
        private IntPtr _hMap = IntPtr.Zero;
        private MapProtection _protection = MapProtection.PageNone;

        private string _fileName = "";
        public string FileName { get { return _fileName; } }

        private long _maxSize;
        private readonly bool _is64bit;

        public long MaxSize { get { return _maxSize; } }

        #region Constants

        private const int GENERIC_READ = unchecked((int)0x80000000);
        private const int GENERIC_WRITE = unchecked((int)0x40000000);
        private const int OPEN_ALWAYS = 4;
        private static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);
        private static readonly IntPtr NULL_HANDLE = IntPtr.Zero;

        #endregion // Constants

        #region Properties
        public bool IsOpen
        {
            get { return (_hMap != NULL_HANDLE); }
        }

        public bool Is64bit
        {
            get { return _is64bit; }
        }

        private MemoryMappedFile()
        {
            _is64bit = IntPtr.Size == 8;
        }

        ~MemoryMappedFile()
        {
            Dispose(false);
        }

        #region Create Overloads


        public static MemoryMappedFile
            Create(MapProtection protection, long maxSize, string name)
        {
            return Create(null, protection, maxSize, name);
        }


        public static MemoryMappedFile
            Create(MapProtection protection, long maxSize)
        {
            return Create(null, protection, maxSize, null);
        }


        public static MemoryMappedFile
            Create(string fileName, MapProtection protection)
        {
            return Create(fileName, protection, 0, null);
        }


        public static MemoryMappedFile
            Create(string fileName, MapProtection protection,
                              long maxSize)
        {
            return Create(fileName, protection, maxSize, null);
        }


        public static MemoryMappedFile
            Create(string fileName, MapProtection protection,
                              long maxSize, String name)
        {
            MemoryMappedFile map = new MemoryMappedFile();
            if (!map.Is64bit && maxSize > uint.MaxValue)
                throw new ConstraintException("32bit systems support max size of 4gb.");

            // open file first
            IntPtr hFile = INVALID_HANDLE_VALUE;

            if (!string.IsNullOrEmpty(fileName))
            {
                if (maxSize == 0)
                {
                    if (!File.Exists(fileName))
                    {
                        throw new Exception(string.Format("Winterdom.IO.FileMap.MemoryMappedFile.Create - \"{0}\" does not exist ==> Unable to map entire file", fileName));
                    }

                    FileInfo backingFileInfo = new FileInfo(fileName);
                    maxSize = backingFileInfo.Length;

                    if (maxSize == 0)
                    {
                        throw new Exception(string.Format("Winterdom.IO.FileMap.MemoryMappedFile.Create - \"{0}\" is zero bytes ==> Unable to map entire file", fileName));
                    }
                }

                // determine file access needed
                // we'll always need generic read access
                int desiredAccess = GENERIC_READ;
                if ((protection == MapProtection.PageReadWrite) ||
                      (protection == MapProtection.PageWriteCopy))
                {
                    desiredAccess |= GENERIC_WRITE;
                }

                // open or create the file
                // if it doesn't exist, it gets created
                hFile = Win32MapApis.CreateFile(
                            fileName, desiredAccess, 0,
                            IntPtr.Zero, OPEN_ALWAYS, 0, IntPtr.Zero
                          );
                if (hFile == INVALID_HANDLE_VALUE)
                    //throw new FileMapIOException(Marshal.GetHRForLastWin32Error());
                    throw new FileMapIOException("MMF");

                map._fileName = fileName;
            }




            map._hMap = Win32MapApis.CreateFileMapping(
                        hFile, IntPtr.Zero, (int)protection,
                        (int)((maxSize >> 32) & 0xFFFFFFFF),
                        (int)(maxSize & 0xFFFFFFFF), "unique"
                    );
            // close file handle, we don't need it
            if (hFile != INVALID_HANDLE_VALUE) Win32MapApis.CloseHandle(hFile);
            if (map._hMap == NULL_HANDLE)
                //throw new FileMapIOException(Marshal.GetHRForLastWin32Error());
                throw new FileMapIOException("MMF");

            map._protection = protection;
            map._maxSize = maxSize;

            return map;
        }

        #endregion // Create Overloads


        public static MemoryMappedFile Open(MapAccess access, String name)
        {
            MemoryMappedFile map = new MemoryMappedFile
            {
                _hMap = Win32MapApis.OpenFileMapping((uint)access, false, name)
            };

            if (map._hMap == NULL_HANDLE)
                throw new FileMapIOException("MMF");

            //throw new FileMapIOException(Marshal.GetHRForLastWin32Error());
            map._maxSize = -1; // debug unknown
            return map;
        }


        public void Close()
        {
            Dispose(true);
        }

        public IntPtr MapView(MapAccess access, long offset, long size)
        {
            if (!IsOpen)
                throw new ObjectDisposedException("Winterdom.IO.FileMap.MemoryMappedFile.MapView - MMF already closed");

            // Throws OverflowException if (a) this is a 32-bit platform AND (b) size is out of bounds (ie. int bounds) with respect to this platform
            IntPtr mapSize = new IntPtr(size);

            IntPtr baseAddress = Win32MapApis.MapViewOfFile(
              _hMap, (int)access,
              (int)((offset >> 32) & 0xFFFFFFFF),
              (int)(offset & 0xFFFFFFFF), mapSize
              );

            if (baseAddress == IntPtr.Zero)
                throw new FileMapIOException("MMF");

            //throw new FileMapIOException(Marshal.GetHRForLastWin32Error());

            return baseAddress;

        }

        public MapViewStream MapAsStream()
        {
            if (!IsOpen)
                throw new ObjectDisposedException("Winterdom.IO.FileMap.MemoryMappedFile.MapView - MMF already closed");

            // sws should verify against _protection
            // Don't know what to do about FILE_MAP_COPY et al

            bool isWriteable = (_protection & MapProtection.PageReadWrite) == MapProtection.PageReadWrite;
            return new MapViewStream(this, MaxSize, isWriteable);

        }

        public void UnMapView(IntPtr mapBaseAddr)
        {
            Win32MapApis.UnmapViewOfFile(mapBaseAddr);
        }

        public void UnMapView(MapViewStream mappedViewStream)
        {
            UnMapView(mappedViewStream.ViewBaseAddr);
        }

        public void Flush(IntPtr viewBaseAddr)
        {
            // Throws OverflowException if (a) this is a 32-bit platform AND (b) size is out of bounds (ie. int bounds) with respect to this platform
            IntPtr flushLength = new IntPtr(MaxSize);
            Win32MapApis.FlushViewOfFile(viewBaseAddr, flushLength);
        }

        public void Flush(MapViewStream mappedViewStream)
        {
            Flush(mappedViewStream.ViewBaseAddr);
        }

        #region IDisposable implementation

        public void Dispose()
        {
            Dispose(true);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (IsOpen)
                Win32MapApis.CloseHandle(_hMap);
            _hMap = NULL_HANDLE;

            if (disposing)
                GC.SuppressFinalize(this);
        }

        #endregion // IDisposable implementation

    }  // class MemoryMappedFile

    public class MapViewStream : Stream, IDisposable
    {
        #region Map/View Related Fields

        protected MemoryMappedFile _backingFile;
        protected MapAccess _access = MapAccess.FileMapWrite;
        protected bool _isWriteable;
        IntPtr _viewBaseAddr = IntPtr.Zero; // Pointer to the base address of the currently mapped view
        protected long _mapSize;
        protected long _viewStartIdx = -1;
        protected long _viewSize = -1;
        long _position; //! our current position in the stream buffer


        #region Properties
        public IntPtr ViewBaseAddr
        {
            get { return _viewBaseAddr; }
        }
        public bool IsViewMapped
        {
            get { return (_viewStartIdx != -1) && (_viewStartIdx + _viewSize) <= (_mapSize); }
        }

        #endregion

        #endregion // Map/View Related Fields

        #region Map / Unmap View

        #region Unmap View

        protected void UnmapView()
        {
            if (IsViewMapped)
            {
                _backingFile.UnMapView(this);
                _viewStartIdx = -1;
                _viewSize = -1;
            }
        }

        #endregion

        #region Map View

        protected void MapView(ref long viewStartIdx, ref long viewSize)
        {
            // Now map the view
            _viewBaseAddr = _backingFile.MapView(_access, viewStartIdx, viewSize);
            _viewStartIdx = viewStartIdx;
            _viewSize = viewSize;

        }
        #endregion

        #endregion

        #region Constructors


        internal MapViewStream(MemoryMappedFile backingFile, long mapSize, bool isWriteable)
        {
            if (backingFile == null)
            {
                throw new Exception("MapViewStream.MapViewStream - backingFile is null");
            }
            if (!backingFile.IsOpen)
            {
                throw new Exception("MapViewStream.MapViewStream - backingFile is not open");
            }
            if ((mapSize < 1) || (mapSize > backingFile.MaxSize))
            {
                throw new Exception(string.Format("MapViewStream.MapViewStream - mapSize is invalid.  mapSize == {0}, backingFile.MaxSize == {1}", mapSize, backingFile.MaxSize));
            }

            _backingFile = backingFile;
            _isWriteable = isWriteable;
            _access = isWriteable ? MapAccess.FileMapWrite : MapAccess.FileMapRead;
            // Need a backingFile.SupportsAccess function that takes a MapAccess compares it against its stored MapProtection protection and returns bool
            _mapSize = mapSize;

            _isOpen = true;

            // Map the first view

            Seek(0, SeekOrigin.Begin);
        }

        #endregion

        #region Stream Properties

        public override bool CanRead
        {
            get { return true; }
        }
        public override bool CanSeek
        {
            get { return true; }
        }
        public override bool CanWrite
        {
            get { return _isWriteable; }
        }
        public override long Length
        {
            get { return _mapSize; }
        }

        public override long Position
        {
            get { return _position; }
            set { Seek(value, SeekOrigin.Begin); }
        }

        #endregion // Stream Properties

        #region Stream Methods

        public override void Flush()
        {
            if (!IsOpen)
                throw new ObjectDisposedException("Winterdom.IO.FileMap.MapViewStream.Flush - Stream is closed");

            // flush the view but leave the buffer intact
            _backingFile.Flush(this);
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            if (!IsOpen)
                throw new ObjectDisposedException("Stream is closed");

            if (buffer.Length - offset < count)
                throw new ArgumentException("Invalid Offset");

            int bytesToRead = (int)Math.Min(Length - _position, count);
            Marshal.Copy((IntPtr)(_viewBaseAddr.ToInt64() + _position), buffer, offset, bytesToRead);

            _position += bytesToRead;
            return bytesToRead;
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            if (!IsOpen)
                throw new ObjectDisposedException("Stream is closed");
            if (!CanWrite)
                throw new FileMapIOException("Stream cannot be written to");

            if (buffer.Length - offset < count)
                throw new ArgumentException("Invalid Offset");

            int bytesToWrite = (int)Math.Min(Length - _position, count);
            if (bytesToWrite == 0)
                return;

            Marshal.Copy(buffer, offset, (IntPtr)(_viewBaseAddr.ToInt64() + _position), bytesToWrite);

            _position += bytesToWrite;
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            if (!IsOpen)
                throw new ObjectDisposedException("Stream is closed");

            long newpos = 0;
            switch (origin)
            {
                case SeekOrigin.Begin: newpos = offset; break;
                case SeekOrigin.Current: newpos = Position + offset; break;
                case SeekOrigin.End: newpos = Length + offset; break;
            }
            // sanity check
            if (newpos < 0 || newpos > Length)
                throw new FileMapIOException("Invalid Seek Offset");
            _position = newpos;

            if (!IsViewMapped)
            {
                MapView(ref newpos, ref _mapSize); // use _mapsize here??
            }

            return newpos;
        }

        public override void SetLength(long value)
        {
            // not supported!
            throw new NotSupportedException("Winterdom.IO.FileMap.MapViewStream.SetLength - Can't change map size");
        }

        public override void Close()
        {
            Dispose(true);
        }

        #endregion // Stream methods

        #region IDisposable Implementation

        private bool _isOpen;
        public bool IsOpen { get { return _isOpen; } }

        public new void Dispose()
        {
            Dispose(true);
        }

        protected new virtual void Dispose(bool disposing)
        {
            if (IsOpen)
            {
                Flush();
                UnmapView();
                _isOpen = false;
            }

            if (disposing)
                GC.SuppressFinalize(this);
        }

        ~MapViewStream()
        {
            Dispose(false);
        }

        #endregion // IDisposable Implementation

    } // class MapViewStream

    public class GenericMemoryMappedArray<TValue> : IDisposable, IEnumerable<TValue>
        where TValue : struct
    {
        #region Private fields
        private string _path;
        private string _fileName;
        private string _uniqueName = "mmf-" + "12345Test";//Guid.NewGuid();
        private long _fileSize;
        private MemoryMappedFile _map;
        private int _dataSize;
        private bool _deleteFile = true;
        private byte[] _buffer;
        private IntPtr _memPtr;
        private bool _autogrow = true;

        private Dictionary<int, MapViewStream> _inUse = new Dictionary<int, MapViewStream>(10);
        private Dictionary<int, DateTime> _lastUsedThread = new Dictionary<int, DateTime>();
        private readonly object _lockObject = new object();
        //private Timer _pooltimer;
        private bool _isDisposed;
        #endregion

        #region Properties

        public string UniqueName
        {
            get { return _uniqueName; }
            set { _uniqueName = value; }
        }


        public long Length
        {
            get
            {
                return _fileSize / _dataSize;
            }
        }


        public long Position
        {
            set
            {
                int threadId = Thread.CurrentThread.ManagedThreadId;
                _lastUsedThread[threadId] = DateTime.UtcNow;

                Stream s = GetView(threadId);
                s.Position = value * _dataSize;
            }
        }


        public bool AutoGrow
        {
            get { return _autogrow; }
            set { _autogrow = value; }
        }

        public override string ToString()
        {
            return string.Format("Length {0}", Length);
        }
        #endregion

        #region Constructor

        public GenericMemoryMappedArray(long size, string path)
        {
            _path = path;           

            _fileName = Path.Combine(path, _uniqueName + ".bin");
            //_fileName = Path.Combine(path, "mmfTest.bin");

            // Get the size of TValue
            _dataSize = Marshal.SizeOf(typeof(TValue));

            // Allocate a global buffer for this instance
            _buffer = new byte[_dataSize];
            // Allocate a global unmanaged buffer for this instance
            _memPtr = Marshal.AllocHGlobal(_dataSize);
            SetFileSize(size);
        }
        #endregion



        #region Finalizer
        ~GenericMemoryMappedArray()
        {
            Dispose(false);
        }
        #endregion

        #region Private methods

        private Stream GetView(int threadId)
        {
            MapViewStream s;
            if (!_inUse.TryGetValue(threadId, out s))
            {
                // create new view and add to pool
                MapViewStream mvs = _map.MapAsStream();
                lock (_lockObject)
                {
                    _inUse.Add(threadId, mvs);
                }
                return mvs;
            }
            return s;
        }


        private void SetFileSize(long size)
        {
            _fileSize = _dataSize * size;
            _map = MemoryMappedFile.Create(_fileName, MapProtection.PageReadWrite, _fileSize);
        }
        #endregion

        #region Public methods
        public void Write(byte[] buffer)
        {
            int threadId = Thread.CurrentThread.ManagedThreadId;
            _lastUsedThread[threadId] = DateTime.UtcNow;

            Stream s = GetView(threadId);
            s.Write(buffer, 0, buffer.Length);
        }

        public void WriteByte(byte b)
        {
            int threadId = Thread.CurrentThread.ManagedThreadId;
            _lastUsedThread[threadId] = DateTime.UtcNow;

            Stream s = GetView(threadId);
            byte[] buffer = new byte[1] { b };
            s.Write(buffer, 0, 1);
        }

        public int Read()
        {
            int threadId = Thread.CurrentThread.ManagedThreadId;
            _lastUsedThread[threadId] = DateTime.UtcNow;

            Stream s = GetView(threadId);
            int count = s.Read(_buffer, 0, _buffer.Length);
            return count;
        }

        public byte ReadByte()
        {
            int threadId = Thread.CurrentThread.ManagedThreadId;
            Stream s = GetView(threadId);

            return (byte)s.ReadByte();
        }

        public TValue this[long index]
        {
            get
            {
                lock (this)
                {
                    if (index >= Length)
                    {
                        throw new ArgumentOutOfRangeException("index", "Tried to access item outside the array boundaries");
                    }
                    Position = index;
                    Read();
                    TValue value = ConvertToTValue();
                    return value;
                }
            }
            set
            {
                lock (this)
                {
                    if (index >= Length)
                    {
                        if (_autogrow)
                            Grow(index, 10);
                        else
                        {
                            throw new ArgumentOutOfRangeException("index", "Tried to access item outside the array");
                        }
                    }
                    Position = index;
                    ConvertToBytes(value);
                    Write(_buffer);
                }
            }
        }

        private void ConvertToBytes(TValue value)
        {
            // Could set the last parameter to false if TValue only contains value types
            // Safer to leave it to true for all purposes.
            Marshal.StructureToPtr(value, _memPtr, true);
            Marshal.Copy(_memPtr, _buffer, 0, _dataSize);
        }

        private TValue ConvertToTValue()
        {
            Marshal.Copy(_buffer, 0, _memPtr, _dataSize);

            object obj = Marshal.PtrToStructure(_memPtr, typeof(TValue));
            return (TValue)obj;
        }


        private void Grow(long size, int percentage)
        {
            _deleteFile = false;

            lock (_lockObject)
            {
                Dispose(true);
                long oldSize = _fileSize;
                _fileSize = (long)((float)size * _dataSize * ((100F + percentage) / 100F)); //required filesize
                if (_fileSize < (oldSize + _dataSize))
                {
                    _fileSize = oldSize + _dataSize;
                }
                _map = MemoryMappedFile.Create(_fileName, MapProtection.PageReadWrite, _fileSize);
            }
        }
        #endregion

        #region Clone Members

        public GenericMemoryMappedArray<TValue> Clone()
        {
            string copyName = _uniqueName + Guid.NewGuid();
            string currentPath = Path.Combine(_path, copyName + ".bin");

            File.Copy(_fileName, currentPath);
            GenericMemoryMappedArray<TValue> current = new GenericMemoryMappedArray<TValue>(Length, currentPath);
            return current;
        }
        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!_isDisposed && disposing)
            {
                lock (_lockObject)
                {
                    // Clean up all views
                    foreach (KeyValuePair<int, MapViewStream> pair in _inUse)
                    {
                        pair.Value.Dispose();
                        pair.Value.Close();
                    }
                    _inUse.Clear();
                    _lastUsedThread.Clear();
                }

                if (_map != null)
                {
                    _map.Close();
                }
            }

            try
            {
                if (_deleteFile)
                {
                    Marshal.DestroyStructure(_memPtr, typeof(TValue)); // Clear unmanaged buffer data
                    Marshal.FreeHGlobal(_memPtr); // Free unmanaged buffer

                    if (File.Exists(_fileName)) File.Delete(_fileName);
                }
            }
            catch (Exception)
            {
                // TODO: Handle files which for some reason didn't want to be deleted
                throw;
            }
            _deleteFile = true;
        }
        #endregion

        #region IEnumerable<TValue> Members

        public IEnumerator<TValue> GetEnumerator()
        {
            lock (this)
            {
                Position = 0;
                for (int i = 0; i < Length; i++)
                {
                    Read();
                    yield return ConvertToTValue();
                }
            }
        }

        #endregion

        IEnumerator IEnumerable.GetEnumerator()
        {
            throw new NotImplementedException();
        }
    }

}

Application for creating the memory mapped file and writing to it, this works OK.

namespace MMFWriteDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            GenericMemoryMappedArray<byte> mmfTest = new GenericMemoryMappedArray<byte>(1024 * 1024 * 8, @"internal\data");
            int i = 0;
            byte b = 0;
            while (i < 1000)
            {
                mmfTest.WriteByte(b);
                b++;
                if (b == 255)
                {
                    b = 0;
                }
                i++;
            }
            Console.ReadLine();
        }
    }
}

This works good, i can see the file created and I can write to it.

Application for opening the memory mapped file and reading from it, it does not work.

namespace MMFReaderDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            GenericMemoryMappedArray<byte> mmfTest = new GenericMemoryMappedArray<byte>(1024 * 1024 * 8, @"internal\data");
            int i = 0;
            while (i < 100)
            {
                Console.WriteLine(mmfTest.ReadByte());
                            i++;         
            }
        }
    }
}

ErrorCode: 0x80000005

Actually I am trying to share the memory mapped file between driver(written in C++) and user app(written in C#).

c#
c++
compact-framework
asked on Stack Overflow May 9, 2014 by Embedd_0913 • edited May 9, 2014 by Embedd_0913

0 Answers

Nobody has answered this question yet.


User contributions licensed under CC BY-SA 3.0