1 /// Constructs for allocating and managing memory without the GC
2 module mecca.lib.memory;
3 
4 // Licensed under the Boost license. Full copyright information in the AUTHORS file
5 
6 import std.exception;
7 import std..string;
8 import std.conv;
9 import std.uuid;
10 
11 import core.atomic;
12 import core.stdc.errno;
13 import core.sys.posix.fcntl;
14 import core.sys.posix.sys.mman;
15 import core.sys.posix.unistd;
16 import core.sys.posix.sys.mman: MAP_ANON;
17 
18 import mecca.lib.exception;
19 import mecca.lib.reflection: setToInit, abiSignatureOf, as;
20 import mecca.platform.os : MAP_POPULATE, MREMAP_MAYMOVE, mremap, MmapArguments;
21 
22 import mecca.log;
23 
24 enum SYS_PAGE_SIZE = 4096;
25 
26 shared static this() {
27     auto actual = sysconf(_SC_PAGESIZE);
28     enforce(SYS_PAGE_SIZE == actual, "PAGE_SIZE = %s".format(actual));
29 }
30 
31 public import mecca.platform.x86: prefetch;
32 
33 /**
34  * A variable size array backed by mmap-allocated memory.
35  *
36  * Behaves just like a native dynamic array, but all methods are `@nogc`.
37  *
38  * Params:
39  *      shrink = Determines whether the capacity of the array can be reduced by setting .length.
40  *      Allocated memory can always be freed by calling free().
41  */
42 public struct MmapArray(T, bool shrink = false) {
43 private:
44     void *_ptr = MAP_FAILED;
45     size_t _capacity = 0;
46     T[] _arr;
47     bool registerWithGC = false;
48 
49 public:
50 
51     /// Returns `true` if the array currently has no allocated memory.
52     @property bool closed() const pure nothrow @nogc { return (_ptr == MAP_FAILED); }
53 
54     /// Returns the array as a standard D slice.
55     @property inout(T[]) arr() inout pure nothrow @nogc { return _arr; }
56 
57     /// Returns the number of elements the array can grow to with no further allocations
58     @property size_t capacity() const pure nothrow @nogc { return _capacity / T.sizeof; }
59 
60     /// Get/set the number of elements in the array.
61     @property size_t length() const pure nothrow @nogc { return _arr.length; }
62     /// ditto
63     @property size_t length(size_t numElements) @trusted @nogc {
64         return this.lengthImpl!true = numElements;
65     }
66 
67     @disable this(this);
68     ~this() nothrow @safe @nogc {
69         free();
70     }
71 
72     alias arr this;
73 
74     /// Pre-allocate enough memory for at least numElements to be appended to the array.
75     @notrace void reserve(size_t numElements) @trusted @nogc {
76         if (this.capacity >= numElements) {
77             return;
78         }
79         reserveImpl(numElements);
80     }
81 
82     /// Initial allocation of the array memory.
83     ///
84     /// Params:
85     ///     numElements = The initial number of elements of the array.
86     ///     registerWithGC = Whether the array's memory should be scanned by the GC. This is required for arrays holding
87     ///         pointers to GC-allocated memory.
88     ///
89     /// Notes:
90     ///     Should be called on a closed array.
91     ///
92     ///     This method is added for symmetry with `free()`. Its use is optional. If `registerWithGC` is `false`, this
93     ///     call has the same effect as setting `length` on a closed array.
94     @notrace void allocate(size_t numElements, bool registerWithGC = false) @trusted @nogc {
95         assert (closed, "Already opened");
96         assert (numElements > 0);
97         this.registerWithGC = registerWithGC;
98         this.lengthImpl!false = numElements;
99     }
100 
101     /// Free all allocated memory and set length to 0.
102     @notrace void free() nothrow @trusted @nogc {
103         if (closed) {
104             return;
105         }
106 
107         this.gcUnregister();
108         munmap(_ptr, _capacity);
109         this._ptr = MAP_FAILED;
110         this._capacity = 0;
111         this._arr = null;
112     }
113 
114 private:
115 
116     @notrace void reserveImpl(size_t numElements) @trusted @nogc {
117         immutable size_t newCapacity = ((((numElements * T.sizeof) + SYS_PAGE_SIZE - 1) / SYS_PAGE_SIZE) * SYS_PAGE_SIZE);
118         static if (shrink) {
119             if (newCapacity == _capacity) {
120                 return;
121             }
122         } else {
123             if (newCapacity <= _capacity) {
124                 return;
125             }
126         }
127 
128         if (numElements == 0) {
129             free();
130             return;
131         }
132 
133         this.gcUnregister();
134         void* ptr = MAP_FAILED;
135         enum MmapArguments mmapArguments = {
136             prot: PROT_READ | PROT_WRITE,
137             flags: MAP_PRIVATE | MAP_ANON | MAP_POPULATE,
138             fd: -1,
139             offset: 0
140         };
141 
142         // initial allocation - mmap
143         if (closed) {
144             ptr = mmap(null, newCapacity, mmapArguments.tupleof);
145             enforceFmt!ErrnoException(ptr != MAP_FAILED, "mmap(%s bytes) failed", newCapacity);
146         }
147 
148         // resize existing allocation - mremap
149         else {
150             ptr = as!"@nogc"(() => mremap(mmapArguments, _ptr, _capacity, newCapacity, MREMAP_MAYMOVE));
151             enforceFmt!ErrnoException(ptr != MAP_FAILED, "mremap(%s bytes -> %s bytes) failed", _capacity, newCapacity);
152         }
153 
154         immutable size_t currLength = this.length;
155         this._ptr = ptr;
156         this._capacity = newCapacity;
157         this._arr = (cast(T*)_ptr)[0 .. currLength];
158         this.gcRegister();
159     }
160 
161     @property size_t lengthImpl(bool forceSetToInit)(size_t numElements) @trusted @nogc {
162         import std.algorithm : min;
163         import std.traits : hasElaborateDestructor;
164         immutable size_t currLength = this.length;
165 
166         static if (hasElaborateDestructor!T) {
167             foreach (ref a; this._arr[min($, numElements) .. $]) {
168                 a.__xdtor();
169             }
170         }
171 
172         this.reserveImpl(numElements);
173         this._arr = this._arr.ptr[0 .. numElements];
174 
175         // XXX DBUG change this runtime if() to static if with __traits(isZeroInit, T) once it's implemented
176         if (forceSetToInit || typeid(T).initializer.ptr !is null) {
177             foreach (ref a; this._arr[min($, currLength) .. $]) {
178                 setToInit!true(a);
179             }
180         }
181 
182         return numElements;
183     }
184 
185     @notrace gcRegister() const nothrow @trusted @nogc {
186         import core.memory;
187         if (registerWithGC) {
188             GC.addRange(_ptr, _capacity);
189         }
190     }
191     @notrace gcUnregister() const nothrow @trusted @nogc {
192         import core.memory;
193         if (registerWithGC && !closed) {
194             GC.removeRange(_ptr);
195         }
196     }
197 }
198 
199 alias MmapBuffer = MmapArray!ubyte;
200 
201 @nogc unittest {
202     MmapArray!ubyte arr;
203     assert (arr is null);
204     assert (arr.length == 0);
205     arr.allocate(1024);
206     assert(arr.length == 1024);
207     arr[4] = 199;
208     arr[$-1] = 200;
209     arr.free();
210     assert (arr is null);
211 
212     arr.length = SYS_PAGE_SIZE / 4;
213     assert (arr.capacity == SYS_PAGE_SIZE);
214     arr[$-1] = 0x13;
215     arr.length = SYS_PAGE_SIZE / 2;
216     arr[$-1] = 0x37;
217     arr.length = SYS_PAGE_SIZE / 4;
218     assert (arr[$-1] == 0x13);
219     arr.length = SYS_PAGE_SIZE / 2;
220     assert (arr[$-1] == 0);
221     assert (arr.capacity == SYS_PAGE_SIZE);
222 
223     arr.length = 4*SYS_PAGE_SIZE - 100;
224     assert (arr.capacity == 4*SYS_PAGE_SIZE);
225     assert (arr[SYS_PAGE_SIZE/4 - 1] == 0x13);
226     arr.length = SYS_PAGE_SIZE / 4;
227     assert (arr.capacity == 4*SYS_PAGE_SIZE);
228     assert (arr[$ - 1] == 0x13);
229     arr.length = 0;
230     assert (arr.empty);
231     assert (arr.capacity == 4*SYS_PAGE_SIZE);
232 
233     arr.free();
234     assert (arr is null);
235     assert (arr.empty);
236     assert (arr.capacity == 0);
237 }
238 
239 @nogc unittest {
240     static ulong count;
241     count = 0;
242 
243     struct S {
244         int x;
245         ~this() @nogc {
246             // count is global (static) to avoid allocating a closure.
247             count++;
248         }
249     }
250     MmapArray!S arr;
251 
252     arr.reserve(100);
253     assert (arr.empty);
254     assert (arr.capacity >= 100);
255 
256     arr.length = 10;
257     assert (!arr.empty);
258     assert (count == 0);
259 
260     arr[9].x = 9;
261     arr.length = 5;
262     assert (count == 5);
263     arr.length = 20;
264     assert (count == 5);
265     assert (arr[9].x == 0);
266     arr.length = 5;
267     assert (count == 20);
268 }
269 
270 struct SharedFile {
271     string filename;
272     int fd = -1;
273     ubyte[] data;
274 
275     void open(string filename, size_t size, bool readWrite = true) {
276         assert (data is null, "Already open");
277         assert (fd < 0, "Already open (fd)");
278 
279         //fd = .open(filename.toStringz, O_EXCL | O_CREAT | (readWrite ? O_RDWR : O_RDONLY), octal!644);
280         //bool created = (fd >= 0);
281         //if (fd < 0 && errno == EEXIST) {
282         //    // this time without O_EXCL
283         //    fd = .open(filename.toStringz, O_CREAT | (readWrite ? O_RDWR : O_RDONLY), octal!644);
284         //}
285         //errnoEnforce(fd >= 0, "open(%s) failed".format(filename));
286 
287         //fd = errnoCall!(.open)(filename.toStringz, O_CREAT | (readWrite ? O_RDWR : O_RDONLY), octal!644);
288         fd = .open(filename.toStringz, O_CREAT | (readWrite ? O_RDWR : O_RDONLY), octal!644);
289         scope(failure) {
290             .close(fd);
291             fd = -1;
292         }
293 
294         auto roundedSize = ((size + SYS_PAGE_SIZE - 1) / SYS_PAGE_SIZE) * SYS_PAGE_SIZE;
295         errnoCall!ftruncate(fd, roundedSize);
296 
297         auto ptr = mmap(null, size, PROT_READ | (readWrite ? PROT_WRITE : 0), MAP_SHARED, fd, 0);
298         errnoEnforce(ptr != MAP_FAILED, "mmap(%s) failed".format(size));
299 
300         data = (cast(ubyte*)ptr)[0 .. size];
301         this.filename = filename;
302     }
303     void close() @nogc {
304         if (fd >= 0) {
305             .close(fd);
306         }
307         if (data) {
308             munmap(data.ptr, data.length);
309             data = null;
310         }
311     }
312     @property bool closed() @nogc @safe pure nothrow{
313         return data is null;
314     }
315 
316     void unlink() {
317         if (filename) {
318             errnoCall!(.unlink)(filename.toStringz);
319             filename = null;
320         }
321     }
322     void lock() {
323         errnoCall!lockf(fd, F_LOCK, 0);
324     }
325     void unlock() {
326         errnoCall!lockf(fd, F_ULOCK, 0);
327     }
328 
329     alias data this;
330 }
331 
332 unittest {
333     enum fn = "/tmp/mapped_file_ut";
334     SharedFile sf;
335     scope(exit) {
336         sf.unlink();
337         sf.close();
338     }
339     sf.open(fn, 7891);
340     sf[7890] = 8;
341     assert(sf[$-1] == 8);
342 }
343 
344 struct SharedFileStruct(T) {
345     import std.traits;
346     static assert (is(T == struct));
347     static assert (!hasIndirections!T, "Shared struct cannot hold pointers");
348 
349     struct Wrapper {
350         shared size_t inited;
351         ulong abiSignature;  // make sure it's the same `T` in all users
352         UUID uuid;           // make sure it belongs to the same object in all users
353         align(64) T data;
354     }
355 
356     SharedFile sharedFile;
357 
358     void open(string filename, UUID uuid = UUID.init) {
359         sharedFile.open(filename, Wrapper.sizeof);
360         sharedFile.lock();
361         scope(exit) sharedFile.unlock();
362 
363         if (atomicLoad(wrapper.inited) == 0) {
364             // we have the guarantee that the first time the file is opened, it's all zeros
365             // so let's init it here
366             setToInit(wrapper.data);
367             static if (__traits(hasMember, T, "sharedInit")) {
368                 wrapper.data.sharedInit();
369             }
370             wrapper.abiSignature = abiSignatureOf!T;
371             wrapper.uuid = uuid;
372             atomicStore(wrapper.inited, 1UL);
373         }
374         else {
375             // already inited
376             assert (wrapper.uuid == uuid);
377             assert (wrapper.abiSignature == abiSignatureOf!T);
378         }
379     }
380     void close() @nogc {
381         sharedFile.close();
382     }
383     @property bool closed() @nogc {
384         return sharedFile.closed();
385     }
386     void unlink() {
387         sharedFile.unlink();
388     }
389 
390     private @property ref Wrapper wrapper() @nogc {pragma(inline, true);
391         return *cast(Wrapper*)sharedFile.data.ptr;
392     }
393     @property ref T data() @nogc {pragma(inline, true);
394         return (cast(Wrapper*)sharedFile.data.ptr).data;
395     }
396 
397     void lock() {
398         //import core.sys.posix.sched: sched_yield;
399         //while (!cas(&wrapper.locked, 0UL, 1UL)) {
400         //    sched_yield();
401         //}
402         sharedFile.lock();
403     }
404     void unlock() {
405         //assert (wrapper.locked);
406         //atomicStore(wrapper.locked, 0UL);
407         sharedFile.unlock();
408     }
409 }
410 
411 unittest {
412     struct S {
413         ulong x = 17;
414         ulong y = 18;
415     }
416 
417     SharedFileStruct!S sfs;
418     sfs.open("/tmp/mapped_file_ut");
419     scope(exit) {
420         sfs.unlink();
421         sfs.close();
422     }
423 
424     sfs.data.x++;
425     sfs.data.y++;
426     assert (sfs.data.x == 18);
427     assert (sfs.data.y == 19);
428 }
429 
430 
431 // Adapted from https://github.com/D-Programming-Language/druntime/blob/master/src/gc/stats.d
432 struct GCStats {
433     size_t poolSizeBytes;   // total size of pool (in bytes)
434     size_t usedSizeBytes;   // bytes allocated
435     size_t freeBlocks;      // number of blocks marked FREE
436     size_t freeListSize;    // total of memory on free lists
437     size_t pageBlocks;      // number of blocks marked PAGE
438 }
439 
440 struct DRuntimeStackDescriptor {
441     private import core.sync.mutex: Mutex;
442     private import core.thread: Thread;
443 
444     import std.conv: to;
445 
446     void*                       bstack; /// Stack bottom
447     void*                       tstack; /// Stack top
448     void*                       ehContext;
449     DRuntimeStackDescriptor*    within;
450     DRuntimeStackDescriptor*    next;
451     DRuntimeStackDescriptor*    prev;
452 
453     private enum mutextInstanceSize = __traits(classInstanceSize, Mutex);
454     private enum mangleSuffix = mutextInstanceSize.to!string ~ "v";
455 
456     static if (__traits(hasMember, Thread, "_locks")) {
457         pragma(mangle, "_D4core6thread6Thread6_locksG2G" ~ mangleSuffix) extern __gshared static
458             void[__traits(classInstanceSize, Mutex)][2] _locks;
459         @notrace private Mutex _slock() nothrow @nogc {
460             return cast(Mutex)_locks[0].ptr;
461         }
462     } else {
463         pragma(mangle,"_D4core6thread6Thread6_slockG72" ~ mangleSuffix) extern __gshared static
464             void[__traits(classInstanceSize, Mutex)] _slock;
465         @notrace private Mutex _slock() nothrow @nogc {
466             return cast(Mutex)_slock.ptr;
467         }
468     }
469 
470     static if (__VERSION__ < 2077) {
471         pragma(mangle, "_D4core6thread6Thread7sm_cbegPS4core6thread6Thread7Context") extern __gshared static
472                 DRuntimeStackDescriptor* sm_cbeg;
473     } else {
474         pragma(mangle, "_D4core6thread6Thread7sm_cbegPSQBdQBbQx7Context") extern __gshared static
475                 DRuntimeStackDescriptor* sm_cbeg;
476     }
477 
478     @notrace void add() nothrow @nogc {
479         auto slock = _slock();
480         slock.lock_nothrow();
481         scope(exit) slock.unlock_nothrow();
482 
483         if (sm_cbeg) {
484             this.next = sm_cbeg;
485             sm_cbeg.prev = &this;
486         }
487         sm_cbeg = &this;
488     }
489 
490     @notrace void remove() nothrow @nogc {
491         auto slock = _slock();
492         slock.lock_nothrow();
493         scope(exit) slock.unlock_nothrow();
494 
495         if (this.prev) {
496             this.prev.next = this.next;
497         }
498         if (this.next) {
499             this.next.prev = this.prev;
500         }
501         if (sm_cbeg == &this) {
502             sm_cbeg = this.next;
503         }
504     }
505 }