1 /**
2  * Copyright: Copyright Jason White, 2014-2016
3  * License:   $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
4  * Authors:   Jason White
5  *
6  * Synopsis:
7  * ---
8  * // Creates a 1 GiB file containing random data.
9  * import io.file;
10  * import std.parallelism : parallel;
11  * auto f = File("big_random_file.dat", FileFlags.writeNew);
12  * f.length = 1024^^3; // 1 GiB
13  *
14  * auto map = f.memoryMap!size_t(Access.write);
15  * foreach (i, ref e; parallel(map[]))
16  *     e = uniform!"[]"(size_t.min, size_t.max);
17  * ---
18  */
19 module io.file.mmap;
20 
21 public import io.file.stream;
22 
23 version (Posix)
24 {
25     import core.sys.posix.sys.mman;
26 
27     // Converts file access flags to POSIX protection flags.
28     private @property int protectionFlags(Access access) pure nothrow
29     {
30         int flags = PROT_NONE;
31         if (access & Access.read)    flags |= PROT_READ;
32         if (access & Access.write)   flags |= PROT_WRITE;
33         if (access & Access.execute) flags |= PROT_EXEC;
34         return flags;
35     }
36 }
37 else version (Windows)
38 {
39     import core.sys.windows.windows;
40 
41     enum FILE_MAP_EXECUTE = 0x0020;
42 
43     // Converts file access flags to Windows protection flags.
44     private @property DWORD protectionFlags(Access access) pure nothrow
45     {
46         switch (access)
47         {
48             case Access.read: return PAGE_READONLY;
49             case Access.write: return PAGE_READWRITE;
50             case Access.readWrite: return PAGE_READWRITE;
51             case Access.read | Access.execute: return PAGE_EXECUTE_READ;
52             case Access.write | Access.execute: return PAGE_EXECUTE_READWRITE;
53             case Access.readWrite | Access.execute: return PAGE_EXECUTE_READWRITE;
54             default: return PAGE_READONLY;
55         }
56     }
57 
58     // Converts file access flags to Windows MapViewOfFileEx flags
59     private @property DWORD mapViewFlags(Access access) pure nothrow
60     {
61         DWORD flags = 0;
62         if (access & Access.read)    flags |= FILE_MAP_READ;
63         if (access & Access.write)   flags |= FILE_MAP_WRITE;
64         if (access & Access.execute) flags |= FILE_MAP_EXECUTE;
65         return flags;
66     }
67 }
68 else
69 {
70     static assert(false, "Not implemented on this platform.");
71 }
72 
73 /**
74  * A memory mapped file. This essentially allows a file to be used as if it were
75  * a slice of memory. For many use cases, it is a very efficient means of
76  * accessing a file.
77  */
78 private struct MemoryMapImpl(T)
79 {
80     // Memory mapped data.
81     T[] data;
82 
83     alias data this;
84 
85     version (Windows)
86         private HANDLE fileMap = null;
87 
88     /**
89      * Maps the contents of the specified file into memory.
90      *
91      * Params:
92      *   file = Open file to be mapped. The file may be closed after being
93      *          mapped to memory. The file must not be a terminal or a pipe. It
94      *          must have random access capabilities.
95      *   access = Access flags of the memory region. Read-only access by
96      *            default.
97      *   length = Length of the memory map in number of $(D T). If 0, the length
98      *            is taken to be the size of the file. 0 by default.
99      *   start = Offset within the file to start the mapping in bytes. 0 by
100      *           default.
101      *   share = If true, changes are visible to other processes. If false,
102      *           changes are not visible to other processes and are never
103      *           written back to the file. True by default.
104      *   address = The preferred memory address to map the file to. This is just
105      *             a hint, the system is may or may not use this address. If
106      *             null, the system chooses an appropriate address. Null by
107      *             default.
108      *
109      * Throws: SysException if the memory map could not be created.
110      */
111     this(File file, Access access = Access.read, size_t length = 0,
112         long start = 0, bool share = true, void* address = null)
113     {
114         import std.conv : to;
115 
116         if (length == 0)
117             length = (file.length - start).to!size_t / T.sizeof;
118 
119         version (Posix)
120         {
121             int flags = share ? MAP_SHARED : MAP_PRIVATE;
122 
123             auto p = cast(T*)mmap(
124                 address,                // Preferred base address
125                 length * T.sizeof,      // Length of the memory map
126                 access.protectionFlags, // Protection flags
127                 flags,                  // Mapping flags
128                 file.handle,            // File descriptor
129                 cast(off_t)start        // Offset within the file
130                 );
131 
132             sysEnforce(p != MAP_FAILED, "Failed to map file to memory");
133 
134             data = p[0 .. length];
135         }
136         else version (Windows)
137         {
138             immutable ULARGE_INTEGER maxSize =
139                 {QuadPart: cast(ulong)(length * T.sizeof)};
140 
141             // Create the file mapping object
142             fileMap = CreateFileMappingW(
143                 file.handle,            // File handle
144                 null,                   // Security attributes
145                 access.protectionFlags, // Page protection flags
146                 maxSize.HighPart,       // Maximum size (high-order bytes)
147                 maxSize.LowPart,        // Maximum size (low-order bytes)
148                 null                    // Optional name to give the object
149                 );
150 
151             sysEnforce(fileMap, "Failed to create file mapping object");
152 
153             scope(failure) CloseHandle(fileMap);
154 
155             immutable ULARGE_INTEGER offset = {QuadPart: cast(ulong)start};
156 
157             // Create a view into the file mapping
158             auto p = cast(T*)MapViewOfFileEx(
159                 fileMap,             // File mapping object
160                 access.mapViewFlags, // Desired access
161                 offset.HighPart,     // File offset (high-order bytes)
162                 offset.LowPart,      // File offset (low-order bytes)
163                 length * T.sizeof,   // Number of bytes to map
164                 address,             // Preferred base address
165                 );
166 
167             sysEnforce(p, "Failed to map file to memory");
168 
169             data = p[0 .. length];
170         }
171     }
172 
173     /**
174      * Unmaps the file from memory and writes back any changes to the file
175      * system.
176      */
177     ~this()
178     {
179         if (data is null) return;
180 
181         version (Posix)
182         {
183             sysEnforce(
184                 munmap(data.ptr, data.length * T.sizeof) == 0,
185                 "Failed to unmap memory"
186                 );
187         }
188         else version (Windows)
189         {
190             sysEnforce(
191                 UnmapViewOfFile(data.ptr) != 0,
192                 "Failed to unmap memory"
193                 );
194             sysEnforce(
195                 CloseHandle(fileMap),
196                 "Failed to close file map object handle"
197                 );
198         }
199     }
200 
201     /**
202      * Synchronously writes any pending changes to the file on the file system.
203      */
204     void flush()
205     {
206         version (Posix)
207         {
208             sysEnforce(
209                 msync(data.ptr, data.length * T.sizeof, MS_SYNC) == 0,
210                 "Failed to flush memory map"
211                 );
212         }
213         else version (Windows)
214         {
215             // TODO: Make this synchronous
216             sysEnforce(
217                 FlushViewOfFile(data.ptr, data.length * T.sizeof) != 0,
218                 "Failed to flush memory map"
219                 );
220         }
221     }
222 
223     /**
224      * Asynchronously writes any pending changes to the file on the file system.
225      */
226     void flushAsync()
227     {
228         version (Posix)
229         {
230             sysEnforce(
231                 msync(data.ptr, data.length * T.sizeof, MS_ASYNC) == 0,
232                 "Failed to flush memory map"
233                 );
234         }
235         else version (Windows)
236         {
237             sysEnforce(
238                 FlushViewOfFile(data.ptr, data.length * T.sizeof) != 0,
239                 "Failed to flush memory map"
240                 );
241         }
242     }
243 
244     // Disable appends. It is possible to use mremap() on Linux to extend (or
245     // contract) the length of the map. However, this is not a portable feature.
246     @disable void opOpAssign(string op = "~")(const(T)[] rhs);
247 }
248 
249 import std.typecons;
250 alias MemoryMap(T) = RefCounted!(MemoryMapImpl!T, RefCountedAutoInitialize.no);
251 
252 /**
253  * Convenience function for creating a memory map.
254  */
255 auto memoryMap(T)(File file, Access access = Access.read,
256     size_t length = 0, long start = 0, bool share = true,
257     void* address = null)
258 {
259     return MemoryMap!T(file, access, length, start, share, address);
260 }
261 
262 ///
263 unittest
264 {
265     auto tf = testFile();
266 
267     immutable newData = "The quick brown fox jumps over the lazy dog.";
268 
269     // Modify the file
270     {
271         auto f = File(tf.name, FileFlags.readWriteEmpty);
272         f.length = newData.length;
273 
274         auto map = f.memoryMap!char(Access.readWrite);
275         assert(map.length == newData.length);
276 
277         map[] = newData[];
278 
279         assert(map[0 .. newData.length] == newData[]);
280     }
281 
282     // Read the file back in
283     {
284         auto f = File(tf.name, FileFlags.readExisting);
285         auto map = f.memoryMap!char(Access.read);
286         assert(map.length == newData.length);
287         assert(map[0 .. newData.length] == newData[]);
288     }
289 }
290 
291 unittest
292 {
293     import std.range : ElementType;
294     static assert(is(ElementType!(MemoryMap!size_t) == size_t));
295 }
296 
297 unittest
298 {
299     import std.exception;
300 
301     auto tf = testFile();
302 
303     auto f = File(tf.name, FileFlags.readWriteEmpty);
304     assert(f.length == 0);
305     assert(collectException!SysException(f.memoryMap!char(Access.readWrite)));
306 }
307 
308 unittest
309 {
310     import io.file.temp;
311 
312     immutable int[] data = [4, 8, 15, 16, 23, 42];
313 
314     auto f = tempFile.file;
315     f.length = data.length * int.sizeof;
316 
317     auto map = f.memoryMap!int(Access.readWrite);
318     map[] = data;
319     assert(map[] == data);
320     assert(map ~ [100, 200] == data ~ [100, 200]);
321 }
322 
323 unittest
324 {
325     import io.file.temp;
326     import std.parallelism, std.random;
327 
328     immutable N = 1024;
329 
330     auto f = tempFile.file;
331     f.length = size_t.sizeof * N;
332 
333     auto map = f.memoryMap!size_t(Access.readWrite);
334     assert(map.length == N);
335 
336     foreach (i, ref e; parallel(map[]))
337         e = uniform!"[]"(size_t.min, size_t.max);
338 }