1 /++
2  + D interface to the Boehm-Demers-Weiser Garbage Collector (BDWGC).
3  + Provides a structured allocator API for GC-managed memory with thread support.
4  +
5  + Note: All allocations, including aligned ones, are GC-managed via BDWGC.
6  + Thread registration is required for multi-threaded applications when GCThreads is enabled.
7  +/
8 module bdwgc;
9 
10 version (D_BetterC)
11 {
12     version (LDC)
13     {
14         pragma(LDC_no_moduleinfo);
15         pragma(LDC_no_typeinfo);
16     }
17 }
18 
19 /// BDWGC C bindings
20 public import c.gc; // @system
21 import std.algorithm.comparison : max;
22 import std.experimental.allocator : IAllocator;
23 
24 // Declare missing BDWGC thread functions
25 version (GCThreads)
26 {
27 extern (C) @nogc nothrow:
28     int GC_thread_is_registered(); // Returns non-zero if thread is registered
29     void GC_register_my_thread(); // Registers the current thread
30     void GC_unregister_my_thread(); // Unregisters the current thread
31     version (Posix) void GC_allow_register_threads(); // Enables dynamic thread registration
32 }
33 
34 /++
35  + Checks if alignment is valid: a power of 2 and at least pointer size.
36  +/
37 bool isGoodDynamicAlignment(uint x) @nogc nothrow pure
38 {
39     return x >= (void*).sizeof && (x & (x - 1)) == 0;
40 }
41 
42 version (Windows)
43 {
44     private import core.stdc.stdio : printf;
45 
46     alias GC_printf = printf; /// Alias for Windows printf
47 }
48 else
49 {
50     /// Formatted output for GC logging
51     pragma(printf)
52     extern (C) void GC_printf(const(char)* format, ...) @trusted @nogc nothrow;
53 }
54 
55 /++
56  + Manage BDWGC thread registration.
57  + Use ThreadGuard.create() to instantiate and register the current thread.
58  + Unregisters the thread on destruction. No-op if GCThreads is disabled.
59  +/
60 struct ThreadGuard
61 {
62 @nogc nothrow:
63     this(this) @disable; // Prevent copying
64     private bool isRegistered; // Track registration state
65 
66     /// Factory function to create and register a ThreadGuard
67     @trusted static ThreadGuard create()
68     {
69         ThreadGuard guard;
70         version (GCThreads)
71         {
72             if (!GC_thread_is_registered())
73             {
74                 debug
75                     GC_printf("Registering thread\n");
76                 GC_register_my_thread();
77                 guard.isRegistered = true;
78             }
79         }
80         return guard;
81     }
82 
83     /// Unregisters the thread if registered
84     @trusted ~this()
85     {
86         version (GCThreads)
87         {
88             if (isRegistered && GC_thread_is_registered())
89             {
90                 debug
91                     GC_printf("Unregistering thread\n");
92                 GC_unregister_my_thread();
93             }
94         }
95     }
96 }
97 
98 /++
99  + Allocator for BDWGC-managed memory, implementing IAllocator.
100  + Thread-safe and compatible with `-betterC`.
101  + Requires thread registration for multi-threaded use when GCThreads is enabled.
102  +/
103 struct BoehmAllocator
104 {
105     version (StdUnittest) @system unittest
106     {
107         extern (C) void testAllocator(alias alloc)(); // Declare testAllocator
108         testAllocator!(() => BoehmAllocator.instance)();
109     }
110 
111     /// Alignment ensures proper alignment for D data types
112     enum uint alignment = max(double.alignof, real.alignof);
113 
114     /// One-time initialization of BDWGC with thread support
115     shared static this() @nogc nothrow
116     {
117         debug
118             GC_printf("Initializing BDWGC\n");
119         GC_init();
120         version (GCThreads)
121         {
122             // Enable thread support
123             version (Posix)
124                 GC_allow_register_threads();
125         }
126     }
127 
128     /// Allocates memory of specified size, returns null if allocation fails
129     @trusted @nogc nothrow
130     void[] allocate(size_t bytes) shared
131     {
132         if (!bytes)
133             return null;
134         auto p = GC_MALLOC(bytes);
135         return p ? p[0 .. bytes] : null;
136     }
137 
138     /// Allocates aligned memory using GC_memalign, returns null if allocation fails
139     @trusted @nogc nothrow
140     void[] alignedAllocate(size_t bytes, uint a) shared
141     {
142         if (!bytes || !a.isGoodDynamicAlignment)
143             return null;
144         auto p = GC_memalign(a, bytes);
145         return p ? p[0 .. bytes] : null;
146     }
147 
148     /// Deallocates memory, safe for null buffers
149     @system @nogc nothrow
150     bool deallocate(void[] b) shared
151     {
152         if (b.ptr)
153             GC_FREE(b.ptr);
154         return true;
155     }
156 
157     /// Reallocates memory to new size, handles zero-size deallocation
158     @system @nogc nothrow
159     bool reallocate(ref void[] b, size_t newSize) shared
160     {
161         if (!newSize)
162         {
163             deallocate(b);
164             b = null;
165             return true;
166         }
167         auto p = GC_REALLOC(b.ptr, newSize);
168         if (!p)
169             return false;
170         b = p[0 .. newSize];
171         return true;
172     }
173 
174     /// Allocates zero-initialized memory
175     @trusted @nogc nothrow
176     void[] allocateZeroed(size_t bytes) shared
177     {
178         if (!bytes)
179             return null;
180         auto p = GC_MALLOC_ATOMIC(bytes);
181         if (!p)
182             return null;
183         import core.stdc.string : memset;
184 
185         memset(p, 0, bytes);
186         return p[0 .. bytes];
187     }
188 
189     /// Enables incremental garbage collection
190     @trusted @nogc nothrow
191     void enableIncremental() shared
192     {
193         GC_enable_incremental();
194     }
195 
196     /// Disables garbage collection
197     @trusted @nogc nothrow
198     void disable() shared
199     {
200         GC_disable();
201     }
202 
203     /// Triggers garbage collection
204     @trusted @nogc nothrow
205     void collect() shared
206     {
207         GC_gcollect();
208     }
209 
210     /// Checks if pointer is GC-managed
211     @trusted @nogc nothrow
212     bool isHeapPtr(const void* ptr) shared const
213     {
214         return GC_is_heap_ptr(cast(void*) ptr) != 0;
215     }
216 
217     /// Checks if the allocator owns the memory block
218     @trusted @nogc nothrow
219     bool owns(void[] b) shared const
220     {
221         return b.ptr && isHeapPtr(b.ptr);
222     }
223 
224     /// Suggests a good allocation size
225     @trusted @nogc nothrow
226     size_t goodAllocSize(size_t n) shared const
227     {
228         if (n == 0)
229             return 0;
230         // Round up to the next multiple of alignment
231         return ((n + alignment - 1) / alignment) * alignment;
232     }
233 
234     /// Global thread-safe instance
235     static shared BoehmAllocator instance;
236 
237     // IAllocator interface compliance
238     alias allocate this;
239 }
240 
241 /**
242  * Unit tests
243  */
244 version (unittest)
245 {
246     import std.experimental.allocator : makeArray;
247 
248     @("Basic allocation and deallocation")
249     @nogc @system nothrow unittest
250     {
251         auto guard = ThreadGuard.create();
252         auto buffer = BoehmAllocator.instance.allocate(1024 * 1024 * 4);
253         scope (exit)
254             BoehmAllocator.instance.deallocate(buffer);
255         assert(buffer !is null);
256         assert(BoehmAllocator.instance.isHeapPtr(buffer.ptr));
257         assert(BoehmAllocator.instance.owns(buffer));
258     }
259 
260     @("Aligned allocation")
261     @nogc @system nothrow unittest
262     {
263         auto guard = ThreadGuard.create();
264         auto buffer = BoehmAllocator.instance.alignedAllocate(1024, 128);
265         scope (exit)
266             BoehmAllocator.instance.deallocate(buffer);
267         assert(buffer !is null);
268         assert((cast(size_t) buffer.ptr) % 128 == 0);
269     }
270 
271     @("Reallocation and zeroed allocation")
272     @nogc @system nothrow unittest
273     {
274         auto guard = ThreadGuard.create();
275         void[] b = BoehmAllocator.instance.allocate(16);
276         assert(b !is null, "Allocation failed");
277         (cast(ubyte[]) b)[] = ubyte(1);
278         // Debug: Print buffer contents before reallocation
279         debug
280         {
281             GC_printf("Before realloc: ");
282             foreach (i; 0 .. 16)
283                 GC_printf("%02x ", (cast(ubyte[]) b)[i]);
284             GC_printf("\n");
285         }
286         assert(BoehmAllocator.instance.reallocate(b, 32), "Reallocation failed");
287         // Debug: Print buffer contents after reallocation
288         debug
289         {
290             GC_printf("After realloc: ");
291             foreach (i; 0 .. 16)
292                 GC_printf("%02x ", (cast(ubyte[]) b)[i]);
293             GC_printf("\n");
294         }
295         ubyte[16] expected = 1;
296         // Manual comparison to avoid issues
297         bool isEqual = true;
298         for (size_t i = 0; i < 16; i++)
299             if ((cast(ubyte[]) b)[i] != 1)
300             {
301                 isEqual = false;
302                 break;
303             }
304         assert(isEqual, "Reallocated buffer contents incorrect");
305         BoehmAllocator.instance.deallocate(b);
306 
307         auto zeroed = BoehmAllocator.instance.allocateZeroed(16);
308         assert(zeroed !is null, "Zeroed allocation failed");
309         ubyte[16] zeroExpected = 0;
310         assert((cast(ubyte[]) zeroed)[] == zeroExpected, "Zeroed buffer not zero");
311         BoehmAllocator.instance.deallocate(zeroed);
312     }
313 
314     @("Incremental GC and collection")
315     @nogc @system nothrow unittest
316     {
317         auto guard = ThreadGuard.create();
318         BoehmAllocator.instance.enableIncremental();
319         auto b = BoehmAllocator.instance.allocate(1024);
320         assert(b !is null);
321         BoehmAllocator.instance.collect();
322         BoehmAllocator.instance.disable();
323         BoehmAllocator.instance.deallocate(b);
324     }
325 
326     @("Allocator interface compliance")
327     @nogc @system nothrow unittest
328     {
329         auto guard = ThreadGuard.create();
330         static void test(A)()
331         {
332             int* p = cast(int*) A.instance.allocate(int.sizeof);
333             scope (exit)
334                 A.instance.deallocate(p[0 .. int.sizeof]);
335             *p = 42;
336             assert(*p == 42);
337         }
338 
339         test!BoehmAllocator();
340     }
341 
342     @("Thread registration")
343     @nogc @system nothrow unittest
344     {
345         version (GCThreads)
346         {
347             assert(!GC_thread_is_registered());
348             {
349                 auto guard = ThreadGuard.create();
350                 assert(GC_thread_is_registered());
351                 auto buffer = BoehmAllocator.instance.allocate(1024);
352                 assert(buffer !is null);
353                 BoehmAllocator.instance.deallocate(buffer);
354             }
355             assert(!GC_thread_is_registered());
356         }
357         else
358         {
359             auto guard = ThreadGuard.create();
360             auto buffer = BoehmAllocator.instance.allocate(1024);
361             assert(buffer !is null);
362             BoehmAllocator.instance.deallocate(buffer);
363         }
364     }
365 
366     @("Aligned allocation (cross-platform)")
367     @nogc @system nothrow unittest
368     {
369         auto guard = ThreadGuard.create();
370         void[] b = BoehmAllocator.instance.alignedAllocate(16, 32);
371         (cast(ubyte[]) b)[] = ubyte(1);
372         ubyte[16] expected = 1;
373         assert((cast(ubyte[]) b)[] == expected);
374         BoehmAllocator.instance.deallocate(b);
375     }
376 
377     @("IAllocator compliance")
378     @nogc @system nothrow unittest
379     {
380         auto guard = ThreadGuard.create();
381         char*[] names = makeArray!(char*)(BoehmAllocator.instance, 3);
382         assert(names.length == 3);
383         assert(names.ptr);
384         BoehmAllocator.instance.deallocate(names);
385     }
386 }