b0fd5d9b8962bbfbcfefa532774b6fdb94adf6dd
[mono.git] / mono / metadata / file-mmap-windows.c
1 /*
2  * file-mmap-windows.c: MemoryMappedFile internal calls for Windows
3  *
4  * Copyright 2016 Microsoft
5  * Licensed under the MIT license. See LICENSE file in the project root for full license information.
6  */
7
8 /*
9  * The code in this file has been inspired by the CoreFX MemoryMappedFile Windows implementation contained in the files
10  *
11  * https://github.com/dotnet/corefx/blob/master/src/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.Windows.cs
12  * https://github.com/dotnet/corefx/blob/master/src/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedView.Windows.cs
13  */
14
15 #include <config.h>
16
17 #ifdef HOST_WIN32
18
19 #include <glib.h>
20
21 #include <mono/metadata/file-mmap.h>
22
23 // These control the retry behaviour when lock violation errors occur during Flush:
24 #define MAX_FLUSH_WAITS 15  // must be <=30
25 #define MAX_FLUSH_RETIRES_PER_WAIT 20
26
27 typedef struct {
28         void *address;
29         size_t length;
30 } MmapInstance;
31
32 enum {
33         BAD_CAPACITY_FOR_FILE_BACKED = 1,
34         CAPACITY_SMALLER_THAN_FILE_SIZE,
35         FILE_NOT_FOUND,
36         FILE_ALREADY_EXISTS,
37         PATH_TOO_LONG,
38         COULD_NOT_OPEN,
39         CAPACITY_MUST_BE_POSITIVE,
40         INVALID_FILE_MODE,
41         COULD_NOT_MAP_MEMORY,
42         ACCESS_DENIED,
43         CAPACITY_LARGER_THAN_LOGICAL_ADDRESS_SPACE
44 };
45
46 enum {
47         FILE_MODE_CREATE_NEW = 1,
48         FILE_MODE_CREATE = 2,
49         FILE_MODE_OPEN = 3,
50         FILE_MODE_OPEN_OR_CREATE = 4,
51         FILE_MODE_TRUNCATE = 5,
52         FILE_MODE_APPEND = 6,
53 };
54
55 enum {
56         MMAP_FILE_ACCESS_READ_WRITE = 0,
57         MMAP_FILE_ACCESS_READ = 1,
58         MMAP_FILE_ACCESS_WRITE = 2,
59         MMAP_FILE_ACCESS_COPY_ON_WRITE = 3,
60         MMAP_FILE_ACCESS_READ_EXECUTE = 4,
61         MMAP_FILE_ACCESS_READ_WRITE_EXECUTE = 5,
62 };
63
64 static DWORD get_page_access (int access)
65 {
66         switch (access) {
67         case MMAP_FILE_ACCESS_READ:
68                 return PAGE_READONLY;
69         case MMAP_FILE_ACCESS_READ_WRITE:
70                 return PAGE_READWRITE;
71         case MMAP_FILE_ACCESS_COPY_ON_WRITE:
72                 return PAGE_WRITECOPY;
73         case MMAP_FILE_ACCESS_READ_EXECUTE:
74                 return PAGE_EXECUTE_READ;
75         case MMAP_FILE_ACCESS_READ_WRITE_EXECUTE:
76                 return PAGE_EXECUTE_READWRITE;
77         default:
78                 g_error ("unknown MemoryMappedFileAccess %d", access);
79         }
80 }
81
82 static DWORD get_file_access (int access)
83 {
84         switch (access) {
85         case MMAP_FILE_ACCESS_READ:
86         case MMAP_FILE_ACCESS_READ_EXECUTE:
87                 return GENERIC_READ;
88         case MMAP_FILE_ACCESS_READ_WRITE:
89         case MMAP_FILE_ACCESS_COPY_ON_WRITE:
90         case MMAP_FILE_ACCESS_READ_WRITE_EXECUTE:
91                 return GENERIC_READ | GENERIC_WRITE;
92         case MMAP_FILE_ACCESS_WRITE:
93                 return GENERIC_WRITE;
94         default:
95                 g_error ("unknown MemoryMappedFileAccess %d", access);
96         }
97 }
98
99 static int get_file_map_access (int access)
100 {
101         switch (access) {
102         case MMAP_FILE_ACCESS_READ:
103                 return FILE_MAP_READ;
104         case MMAP_FILE_ACCESS_WRITE:
105                 return FILE_MAP_WRITE;
106         case MMAP_FILE_ACCESS_READ_WRITE:
107                 return FILE_MAP_READ | FILE_MAP_WRITE;
108         case MMAP_FILE_ACCESS_COPY_ON_WRITE:
109                 return FILE_MAP_COPY;
110         case MMAP_FILE_ACCESS_READ_EXECUTE:
111                 return FILE_MAP_EXECUTE | FILE_MAP_READ;
112         case MMAP_FILE_ACCESS_READ_WRITE_EXECUTE:
113                 return FILE_MAP_EXECUTE | FILE_MAP_READ | FILE_MAP_WRITE;
114         default:
115                 g_error ("unknown MemoryMappedFileAccess %d", access);
116         }
117 }
118
119 static int convert_win32_error (int error, int def)
120 {
121         switch (error) {
122         case ERROR_FILE_NOT_FOUND:
123                 return FILE_NOT_FOUND;
124         case ERROR_FILE_EXISTS:
125         case ERROR_ALREADY_EXISTS:
126                 return FILE_ALREADY_EXISTS;
127         case ERROR_ACCESS_DENIED:
128                 return ACCESS_DENIED;
129         }
130         return def;
131 }
132
133 static void *open_handle (void *handle, MonoString *mapName, int mode, gint64 *capacity, int access, int options, int *error)
134 {
135         g_assert (handle != NULL);
136
137         wchar_t *w_mapName = NULL;
138         HANDLE result = NULL;
139
140         if (handle == INVALID_HANDLE_VALUE) {
141                 if (*capacity <= 0) {
142                         *error = CAPACITY_MUST_BE_POSITIVE;
143                         return NULL;
144                 }
145 #if SIZEOF_VOID_P == 4
146                 if (*capacity > UINT32_MAX) {
147                         *error = CAPACITY_LARGER_THAN_LOGICAL_ADDRESS_SPACE;
148                         return NULL;
149                 }
150 #endif
151                 if (!(mode == FILE_MODE_CREATE_NEW || mode == FILE_MODE_OPEN_OR_CREATE || mode == FILE_MODE_OPEN)) {
152                         *error = INVALID_FILE_MODE;
153                         return NULL;
154                 }
155         } else {
156                 FILE_STANDARD_INFO info;
157                 if (!GetFileInformationByHandleEx ((HANDLE) handle, FileStandardInfo, &info, sizeof (FILE_STANDARD_INFO))) {
158                         *error = convert_win32_error (GetLastError (), COULD_NOT_OPEN);
159                         return NULL;
160                 }
161                 if (*capacity == 0) {
162                         if (info.EndOfFile.QuadPart == 0) {
163                                 *error = CAPACITY_SMALLER_THAN_FILE_SIZE;
164                                 return NULL;
165                         }
166                 } else if (*capacity < info.EndOfFile.QuadPart) {
167                         *error = CAPACITY_SMALLER_THAN_FILE_SIZE;
168                         return NULL;
169                 }
170         }
171
172         w_mapName = mapName ? mono_string_to_utf16 (mapName) : NULL;
173
174         if (mode == FILE_MODE_CREATE_NEW || handle != INVALID_HANDLE_VALUE) {
175                 result = CreateFileMappingW ((HANDLE)handle, NULL, get_page_access (access) | options, (DWORD)(((guint64)*capacity) >> 32), (DWORD)*capacity, w_mapName);
176                 if (result && GetLastError () == ERROR_ALREADY_EXISTS) {
177                         CloseHandle (result);
178                         result = NULL;
179                         *error = FILE_ALREADY_EXISTS;
180                 } else if (!result && GetLastError () != NO_ERROR) {
181                         *error = convert_win32_error (GetLastError (), COULD_NOT_OPEN);
182                 }
183         } else if (mode == FILE_MODE_OPEN || mode == FILE_MODE_OPEN_OR_CREATE && access == MMAP_FILE_ACCESS_WRITE) {
184                 result = OpenFileMappingW (get_file_map_access (access), FALSE, w_mapName);
185                 if (!result) {
186                         if (mode == FILE_MODE_OPEN_OR_CREATE && GetLastError () == ERROR_FILE_NOT_FOUND) {
187                                 *error = INVALID_FILE_MODE;
188                         } else {
189                                 *error = convert_win32_error (GetLastError (), COULD_NOT_OPEN);
190                         }
191                 }
192         } else if (mode == FILE_MODE_OPEN_OR_CREATE) {
193
194                 // This replicates how CoreFX does MemoryMappedFile.CreateOrOpen ().
195
196                 /// Try to open the file if it exists -- this requires a bit more work. Loop until we can
197                 /// either create or open a memory mapped file up to a timeout. CreateFileMapping may fail
198                 /// if the file exists and we have non-null security attributes, in which case we need to
199                 /// use OpenFileMapping.  But, there exists a race condition because the memory mapped file
200                 /// may have closed between the two calls -- hence the loop. 
201                 /// 
202                 /// The retry/timeout logic increases the wait time each pass through the loop and times 
203                 /// out in approximately 1.4 minutes. If after retrying, a MMF handle still hasn't been opened, 
204                 /// throw an InvalidOperationException.
205
206                 guint32 waitRetries = 14;   //((2^13)-1)*10ms == approximately 1.4mins
207                 guint32 waitSleep = 0;
208
209                 while (waitRetries > 0) {
210                         result = CreateFileMappingW ((HANDLE)handle, NULL, get_page_access (access) | options, (DWORD)(((guint64)*capacity) >> 32), (DWORD)*capacity, w_mapName);
211                         if (result)
212                                 break;
213                         if (GetLastError() != ERROR_ACCESS_DENIED) {
214                                 *error = convert_win32_error (GetLastError (), COULD_NOT_OPEN);
215                                 break;
216                         }
217                         result = OpenFileMappingW (get_file_map_access (access), FALSE, w_mapName);
218                         if (result)
219                                 break;
220                         if (GetLastError () != ERROR_FILE_NOT_FOUND) {
221                                 *error = convert_win32_error (GetLastError (), COULD_NOT_OPEN);
222                                 break;
223                         }
224                         // increase wait time
225                         --waitRetries;
226                         if (waitSleep == 0) {
227                                 waitSleep = 10;
228                         } else {
229                                 mono_thread_info_sleep (waitSleep, NULL);
230                                 waitSleep *= 2;
231                         }
232                 }
233
234                 if (!result) {
235                         *error = COULD_NOT_OPEN;
236                 }
237         }
238
239         if (w_mapName)
240                 g_free (w_mapName);
241         return result;
242 }
243
244 void *mono_mmap_open_file (MonoString *path, int mode, MonoString *mapName, gint64 *capacity, int access, int options, int *error)
245 {
246         g_assert (path != NULL || mapName != NULL);
247
248         wchar_t *w_path = NULL;
249         HANDLE hFile = INVALID_HANDLE_VALUE;
250         HANDLE result = NULL;
251         gboolean delete_on_error = FALSE;
252
253         if (path) {
254                 w_path = mono_string_to_utf16 (path);
255                 WIN32_FILE_ATTRIBUTE_DATA file_attrs;
256                 gboolean existed = GetFileAttributesExW (w_path, GetFileExInfoStandard, &file_attrs);
257                 if (!existed && mode == FILE_MODE_CREATE_NEW && *capacity == 0) {
258                         *error = CAPACITY_SMALLER_THAN_FILE_SIZE;
259                         goto done;
260                 }
261                 hFile = CreateFileW (w_path, get_file_access (access), FILE_SHARE_READ, NULL, mode, FILE_ATTRIBUTE_NORMAL, NULL);
262                 if (hFile == INVALID_HANDLE_VALUE) {
263                         *error = convert_win32_error (GetLastError (), COULD_NOT_OPEN);
264                         goto done;
265                 }
266                 delete_on_error = !existed;
267         }
268
269         result = open_handle (hFile, mapName, mode, capacity, access, options, error);
270
271 done:
272         if (!result && delete_on_error)
273                 DeleteFileW (w_path);
274         if (w_path)
275                 g_free (w_path);
276
277         return result;
278 }
279
280 void *mono_mmap_open_handle (void *handle, MonoString *mapName, gint64 *capacity, int access, int options, int *error)
281 {
282         g_assert (handle != NULL);
283
284         return open_handle (handle, mapName, FILE_MODE_OPEN, capacity, access, options, error);
285 }
286
287 void mono_mmap_close (void *mmap_handle)
288 {
289         g_assert (mmap_handle);
290         CloseHandle ((HANDLE) mmap_handle);
291 }
292
293 void mono_mmap_configure_inheritability (void *mmap_handle, gboolean inheritability)
294 {
295         g_assert (mmap_handle);
296         if (!SetHandleInformation ((HANDLE) mmap_handle, HANDLE_FLAG_INHERIT, inheritability ? HANDLE_FLAG_INHERIT : 0)) {
297                 g_error ("mono_mmap_configure_inheritability: SetHandleInformation failed with error %d!", GetLastError ());
298         }
299 }
300
301 void mono_mmap_flush (void *mmap_handle)
302 {
303         g_assert (mmap_handle);
304         MmapInstance *h = (MmapInstance *)mmap_handle;
305
306         if (FlushViewOfFile (h->address, h->length))
307                 return;
308
309         // This replicates how CoreFX does MemoryMappedView.Flush ().
310
311         // It is a known issue within the NTFS transaction log system that
312         // causes FlushViewOfFile to intermittently fail with ERROR_LOCK_VIOLATION
313         // As a workaround, we catch this particular error and retry the flush operation 
314         // a few milliseconds later. If it does not work, we give it a few more tries with
315         // increasing intervals. Eventually, however, we need to give up. In ad-hoc tests
316         // this strategy successfully flushed the view after no more than 3 retries.
317
318         if (GetLastError () != ERROR_LOCK_VIOLATION)
319                 // TODO: Propagate error to caller
320                 return;
321
322         for (int w = 0; w < MAX_FLUSH_WAITS; w++) {
323                 int pause = (1 << w);  // MaxFlushRetries should never be over 30
324                 mono_thread_info_sleep (pause, NULL);
325
326                 for (int r = 0; r < MAX_FLUSH_RETIRES_PER_WAIT; r++) {
327                         if (FlushViewOfFile (h->address, h->length))
328                                 return;
329
330                         if (GetLastError () != ERROR_LOCK_VIOLATION)
331                                 // TODO: Propagate error to caller
332                                 return;
333
334                         mono_thread_info_yield ();
335                 }
336         }
337
338         // We got to here, so there was no success:
339         // TODO: Propagate error to caller
340 }
341
342 int mono_mmap_map (void *handle, gint64 offset, gint64 *size, int access, void **mmap_handle, void **base_address)
343 {
344         static DWORD allocationGranularity = 0;
345         if (allocationGranularity == 0) {
346                 SYSTEM_INFO info;
347                 GetSystemInfo (&info);
348                 allocationGranularity = info.dwAllocationGranularity;
349         }
350
351         gint64 extraMemNeeded = offset % allocationGranularity;
352         guint64 newOffset = offset - extraMemNeeded;
353         gint64 nativeSize = (*size != 0) ? *size + extraMemNeeded : 0;
354
355 #if SIZEOF_VOID_P == 4
356         if (nativeSize > UINT32_MAX)
357                 return CAPACITY_LARGER_THAN_LOGICAL_ADDRESS_SPACE;
358 #endif
359         
360         void *address = MapViewOfFile ((HANDLE) handle, get_file_map_access (access), (DWORD) (newOffset >> 32), (DWORD) newOffset, (SIZE_T) nativeSize);
361         if (!address)
362                 return convert_win32_error (GetLastError (), COULD_NOT_MAP_MEMORY);
363
364         // Query the view for its size and allocation type
365         MEMORY_BASIC_INFORMATION viewInfo;
366         VirtualQuery (address, &viewInfo, sizeof (MEMORY_BASIC_INFORMATION));
367         guint64 viewSize = (guint64) viewInfo.RegionSize;
368
369         // Allocate the pages if we were using the MemoryMappedFileOptions.DelayAllocatePages option
370         // OR check if the allocated view size is smaller than the expected native size
371         // If multiple overlapping views are created over the file mapping object, the pages in a given region
372         // could have different attributes(MEM_RESERVE OR MEM_COMMIT) as MapViewOfFile preserves coherence between 
373         // views created on a mapping object backed by same file.
374         // In which case, the viewSize will be smaller than nativeSize required and viewState could be MEM_COMMIT 
375         // but more pages may need to be committed in the region.
376         // This is because, VirtualQuery function(that internally invokes VirtualQueryEx function) returns the attributes 
377         // and size of the region of pages with matching attributes starting from base address.
378         // VirtualQueryEx: http://msdn.microsoft.com/en-us/library/windows/desktop/aa366907(v=vs.85).aspx
379         if (((viewInfo.State & MEM_RESERVE) != 0) || viewSize < (guint64) nativeSize) {
380                 void *tempAddress = VirtualAlloc (address, nativeSize != 0 ? nativeSize : viewSize, MEM_COMMIT, get_page_access (access));
381                 if (!tempAddress) {
382                         return convert_win32_error (GetLastError (), COULD_NOT_MAP_MEMORY);
383                 }
384                 // again query the view for its new size
385                 VirtualQuery (address, &viewInfo, sizeof (MEMORY_BASIC_INFORMATION));
386                 viewSize = (guint64) viewInfo.RegionSize;
387         }
388
389         if (*size == 0)
390                 *size = viewSize - extraMemNeeded;
391
392         MmapInstance *h = g_malloc0 (sizeof (MmapInstance));
393         h->address = address;
394         h->length = *size + extraMemNeeded;
395         *mmap_handle = h;
396         *base_address = (char*) address + (offset - newOffset);
397
398         return 0;
399 }
400
401 gboolean mono_mmap_unmap (void *mmap_handle)
402 {
403         g_assert (mmap_handle);
404
405         MmapInstance *h = (MmapInstance *) mmap_handle;
406
407         gboolean result = UnmapViewOfFile (h->address);
408
409         g_free (h);
410         return result;
411 }
412
413 #endif