1 /++
2 	$(PITFALL
3 		Please note: the api and behavior of this module is not externally stable at this time. See the documentation on specific functions for details.
4 	)
5 
6 	Shared core functionality including exception helpers, library loader, event loop, and possibly more. Maybe command line processor and uda helper and some basic shared annotation types.
7 
8 	I'll probably move the url, websocket, and ssl stuff in here too as they are often shared. Maybe a small internationalization helper type (a hook for external implementation) and COM helpers too. I might move the process helpers out to their own module - even things in here are not considered stable to library users at this time!
9 
10 	If you use this directly outside the arsd library despite its current instability caveats, you might consider using `static import` since names in here are likely to clash with Phobos if you use them together. `static import` will let you easily disambiguate and avoid name conflict errors if I add more here. Some names even clash deliberately to remind me to avoid some antipatterns inside the arsd modules!
11 
12 	## Contributor notes
13 
14 	arsd.core should be focused on things that enable interoperability primarily and secondarily increased code quality between other, otherwise independent arsd modules. As a foundational library, it is not permitted to import anything outside the druntime `core` namespace, except in templates and examples not normally compiled in. This keeps it independent and avoids transitive dependency spillover to end users while also keeping compile speeds fast. To help keep builds snappy, also avoid significant use of ctfe inside this module.
15 
16 	On my linux computer, `dmd -unittest -main core.d` takes about a quarter second to run. We do not want this to grow.
17 
18 	`@safe` compatibility is ok when it isn't too big of a hassle. `@nogc` is a non-goal. I might accept it on some of the trivial functions but if it means changing the logic in any way to support, you will need a compelling argument to justify it. The arsd libs are supposed to be reliable and easy to use. That said, of course, don't be unnecessarily wasteful - if you can easily provide a reliable and easy to use way to let advanced users do their thing without hurting the other cases, let's discuss it.
19 
20 	If functionality is not needed by multiple existing arsd modules, consider adding a new module instead of adding it to the core.
21 
22 	Unittests should generally be hidden behind a special version guard so they don't interfere with end user tests.
23 
24 	History:
25 		Added March 2023 (dub v11.0). Several functions were migrated in here at that time, noted individually. Members without a note were added with the module.
26 +/
27 module arsd.core;
28 
29 
30 static if(__traits(compiles, () { import core.interpolation; })) {
31 	import core.interpolation;
32 
33 	alias InterpolationHeader    = core.interpolation.InterpolationHeader;
34 	alias InterpolationFooter    = core.interpolation.InterpolationFooter;
35 	alias InterpolatedLiteral    = core.interpolation.InterpolatedLiteral;
36 	alias InterpolatedExpression = core.interpolation.InterpolatedExpression;
37 } else {
38 	// polyfill for old versions
39 	struct InterpolationHeader {}
40 	struct InterpolationFooter {}
41 	struct InterpolatedLiteral(string literal) {}
42 	struct InterpolatedExpression(string code) {}
43 }
44 
45 version(use_arsd_core)
46 	enum use_arsd_core = true;
47 else
48 	enum use_arsd_core = false;
49 
50 import core.attribute;
51 static if(__traits(hasMember, core.attribute, "implicit"))
52 	alias implicit = core.attribute.implicit;
53 else
54 	enum implicit;
55 
56 static if(__traits(hasMember, core.attribute, "standalone"))
57 	alias standalone = core.attribute.standalone;
58 else
59 	enum standalone;
60 
61 
62 
63 // FIXME: add callbacks on file open for tracing dependencies dynamically
64 
65 // see for useful info: https://devblogs.microsoft.com/dotnet/how-async-await-really-works/
66 
67 // see: https://wiki.openssl.org/index.php/Simple_TLS_Server
68 
69 // see: When you only want to track changes on a file or directory, be sure to open it using the O_EVTONLY flag.
70 
71 ///ArsdUseCustomRuntime is used since other derived work from WebAssembly may be used and thus specified in the CLI
72 version(Emscripten) {
73 	version = EmptyEventLoop;
74 	version = EmptyCoreEvent;
75 	version = HasTimer;
76 } else version(WebAssembly) version = ArsdUseCustomRuntime;
77 else
78 
79 // note that kqueue might run an i/o loop on mac, ios, etc. but then NSApp.run on the io thread
80 // but on bsd, you want the kqueue loop in i/o too....
81 
82 version(ArsdUseCustomRuntime)
83 {
84 	version = UseStdioWriteln;
85 }
86 else
87 {
88 	version(D_OpenD) {
89 		version(OSX)
90 			version=OSXCocoa;
91 		version(iOS)
92 			version=OSXCocoa;
93 	}
94 
95 	version = HasFile;
96 	version = HasSocket;
97 	version = HasThread;
98 	import core.stdc.errno;
99 
100 	version(Windows)
101 		version = HasTimer;
102 	version(linux)
103 		version = HasTimer;
104 	version(OSXCocoa)
105 		version = HasTimer;
106 }
107 
108 version(HasThread)
109 {
110 	import core.thread;
111 	import core..volatile;
112 	import core.atomic;
113 }
114 else
115 {
116 	// polyfill for missing core.time
117 	/*
118 	struct Duration {
119 		static Duration max() { return Duration(); }
120 	}
121 	struct MonoTime {}
122 	*/
123 }
124 
125 import core.time;
126 
127 version(OSXCocoa) {
128 	version(ArsdNoCocoa)
129 		enum bool UseCocoa = false;
130 	else
131 		enum bool UseCocoa = true;
132 }
133 
134 import core.attribute;
135 static if(!__traits(hasMember, core.attribute, "mustuse"))
136 	enum mustuse;
137 
138 // FIXME: add an arena allocator? can do task local destruction maybe.
139 
140 // the three implementations are windows, epoll, and kqueue
141 
142 version(Emscripten)  {
143 	import core.stdc.errno;
144 	import core.atomic;
145 	import core..volatile;
146 
147 } else version(Windows) {
148 	version=Arsd_core_windows;
149 
150 	// import core.sys.windows.windows;
151 	import core.sys.windows.winbase;
152 	import core.sys.windows.windef;
153 	import core.sys.windows.winnls;
154 	import core.sys.windows.winuser;
155 	import core.sys.windows.winsock2;
156 
157 	pragma(lib, "user32");
158 	pragma(lib, "ws2_32");
159 } else version(linux) {
160 	version=Arsd_core_epoll;
161 
162 	static if(__VERSION__ >= 2098) {
163 		version=Arsd_core_has_cloexec;
164 	}
165 } else version(FreeBSD) {
166 	version=Arsd_core_kqueue;
167 
168 	import core.sys.freebsd.sys.event;
169 
170 	// the version in druntime doesn't have the default arg making it a pain to use when the freebsd
171 	// version adds a new field
172 	extern(D) void EV_SET(kevent_t* kevp, typeof(kevent_t.tupleof) args = kevent_t.tupleof.init)
173 	{
174 	    *kevp = kevent_t(args);
175 	}
176 } else version(DragonFlyBSD) {
177 	// NOT ACTUALLY TESTED
178 	version=Arsd_core_kqueue;
179 
180 	import core.sys.dragonflybsd.sys.event;
181 } else version(NetBSD) {
182 	// NOT ACTUALLY TESTED
183 	version=Arsd_core_kqueue;
184 
185 	import core.sys.netbsd.sys.event;
186 } else version(OpenBSD) {
187 	version=Arsd_core_kqueue;
188 
189 	// THIS FILE DOESN'T ACTUALLY EXIST, WE NEED TO MAKE IT
190 	import core.sys.openbsd.sys.event;
191 } else version(OSX) {
192 	version=Arsd_core_kqueue;
193 
194 	import core.sys.darwin.sys.event;
195 } else version(iOS) {
196 	version=Arsd_core_kqueue;
197 
198 	import core.sys.darwin.sys.event;
199 }
200 
201 // FIXME: pragma(linkerDirective, "-framework", "Cocoa") works in ldc
202 version(OSXCocoa)
203 	enum CocoaAvailable = true;
204 else
205 	enum CocoaAvailable = false;
206 
207 version(D_OpenD) {
208 	version(OSXCocoa)
209 		pragma(linkerDirective, "-framework", "Cocoa");
210 } else {
211 	version(OSXCocoa)
212 	version(LDC)
213 		pragma(linkerDirective, "-framework", "Cocoa");
214 }
215 
216 version(Posix) {
217 	import core.sys.posix.signal;
218 	import core.sys.posix.unistd;
219 
220 	version(Emscripten) {} else {
221 	import core.sys.posix.sys.un;
222 	import core.sys.posix.sys.socket;
223 	import core.sys.posix.netinet.in_;
224 	}
225 }
226 
227 // FIXME: the exceptions should actually give some explanatory text too (at least sometimes)
228 
229 /+
230 	=========================
231 	GENERAL UTILITY FUNCTIONS
232 	=========================
233 +/
234 
235 /++
236 	Casts value `v` to type `T`.
237 
238 	$(TIP
239 		This is a helper function for readability purposes.
240 		The idea is to make type-casting as accessible as `to()` from `std.conv`.
241 	)
242 
243 	---
244 	int i =  cast(int)(foo * bar);
245 	int i = castTo!int(foo * bar);
246 
247 	int j = cast(int) round(floatValue);
248 	int j = round(floatValue).castTo!int;
249 
250 	int k = cast(int) floatValue  + foobar;
251 	int k = floatValue.castTo!int + foobar;
252 
253 	auto m = Point(
254 		cast(int) calc(a.x, b.x),
255 		cast(int) calc(a.y, b.y),
256 	);
257 	auto m = Point(
258 		calc(a.x, b.x).castTo!int,
259 		calc(a.y, b.y).castTo!int,
260 	);
261 	---
262 
263 	History:
264 		Added on April 24, 2024.
265 		Renamed from `typeCast` to `castTo` on May 24, 2024.
266  +/
267 auto ref T castTo(T, S)(auto ref S v) {
268 	return cast(T) v;
269 }
270 
271 ///
272 alias typeCast = castTo;
273 
274 /++
275 	Does math as a 64 bit number, but saturates at int.min and int.max when converting back to a 32 bit int.
276 
277 	History:
278 		Added January 1, 2025
279 +/
280 alias NonOverflowingInt = NonOverflowingIntBase!(int.min, int.max);
281 
282 /// ditto
283 alias NonOverflowingUint = NonOverflowingIntBase!(0, int.max);
284 
285 /// ditto
286 struct NonOverflowingIntBase(int min, int max) {
287 	this(long v) {
288 		this.value = v;
289 	}
290 
291 	private long value;
292 
293 	NonOverflowingInt opBinary(string op)(long rhs) {
294 		return NonOverflowingInt(mixin("this.value", op, "rhs"));
295 	}
296 	NonOverflowingInt opBinary(string op)(NonOverflowingInt rhs) {
297 		return this.opBinary!op(rhs.value);
298 	}
299 	NonOverflowingInt opUnary(string op)() {
300 		return NonOverflowingInt(mixin(op, "this.value"));
301 	}
302 	NonOverflowingInt opOpAssign(string op)(long rhs) {
303 		return this = this.opBinary!(op)(rhs);
304 	}
305 	NonOverflowingInt opOpAssign(string op)(NonOverflowingInt rhs) {
306 		return this = this.opBinary!(op)(rhs.value);
307 	}
308 
309 	int getValue() const {
310 		if(value < min)
311 			return min;
312 		else if(value > max)
313 			return max;
314 		return cast(int) value;
315 	}
316 
317 	alias getValue this;
318 }
319 
320 unittest {
321 	assert(-5.NonOverflowingInt - int.max == int.min);
322 	assert(-5.NonOverflowingInt + 5 == 0);
323 
324 	assert(NonOverflowingInt(5) + int.max - 5 == int.max);
325 	assert(NonOverflowingInt(5) + int.max - int.max - 5 == 0); // it truncates at the end of the op chain, not at intermediates
326 	assert(NonOverflowingInt(0) + int.max * 2L == int.max); // note the L there is required to pass since the order of operations means mul done before it gets to the NonOverflowingInt controls
327 }
328 
329 // enum stringz : const(char)* { init = null }
330 
331 /++
332 	A wrapper around a `const(char)*` to indicate that it is a zero-terminated C string.
333 +/
334 struct stringz {
335 	private const(char)* raw;
336 
337 	/++
338 		Wraps the given pointer in the struct. Note that it retains a copy of the pointer.
339 	+/
340 	this(const(char)* raw) {
341 		this.raw = raw;
342 	}
343 
344 	/++
345 		Returns the original raw pointer back out.
346 	+/
347 	const(char)* ptr() const {
348 		return raw;
349 	}
350 
351 	/++
352 		Borrows a slice of the pointer up to (but not including) the zero terminator.
353 	+/
354 	const(char)[] borrow() const @system {
355 		if(raw is null)
356 			return null;
357 
358 		const(char)* p = raw;
359 		int length;
360 		while(*p++) length++;
361 
362 		return raw[0 .. length];
363 	}
364 }
365 
366 /+
367 	DateTime
368 		year: 16 bits (-32k to +32k)
369 		month: 4 bits
370 		day: 5 bits
371 
372 		hour: 5 bits
373 		minute: 6 bits
374 		second: 6 bits
375 
376 		total: 25 bits + 17 bits = 42 bits
377 
378 		fractional seconds: 10 bits
379 
380 		accuracy flags: date_valid | time_valid = 2 bits
381 
382 		54 bits used, 8 bits remain. reserve 1 for signed.
383 
384 		would need 11 bits for minute-precise dt offset but meh.
385 +/
386 
387 /++
388 	A packed date/time/datetime representation added for use with LimitedVariant.
389 
390 	You should probably not use this much directly, it is mostly an internal storage representation.
391 +/
392 struct PackedDateTime {
393 	private ulong packedData;
394 
395 	string toString() const {
396 		char[64] buffer;
397 		size_t pos;
398 
399 		if(hasDate) {
400 			pos += intToString(year, buffer[pos .. $], IntToStringArgs().withPadding(4)).length;
401 			buffer[pos++] = '-';
402 			pos += intToString(month, buffer[pos .. $], IntToStringArgs().withPadding(2)).length;
403 			buffer[pos++] = '-';
404 			pos += intToString(day, buffer[pos .. $], IntToStringArgs().withPadding(2)).length;
405 		}
406 
407 		if(hasTime) {
408 			if(pos)
409 				buffer[pos++] = 'T';
410 
411 			pos += intToString(hours, buffer[pos .. $], IntToStringArgs().withPadding(2)).length;
412 			buffer[pos++] = ':';
413 			pos += intToString(minutes, buffer[pos .. $], IntToStringArgs().withPadding(2)).length;
414 			buffer[pos++] = ':';
415 			pos += intToString(seconds, buffer[pos .. $], IntToStringArgs().withPadding(2)).length;
416 			if(fractionalSeconds) {
417 				buffer[pos++] = '.';
418 				pos += intToString(fractionalSeconds, buffer[pos .. $], IntToStringArgs().withPadding(4)).length;
419 			}
420 		}
421 
422 		return buffer[0 .. pos].idup;
423 	}
424 
425 	/++
426 	+/
427 	int fractionalSeconds() const { return getFromMask(00, 10); }
428 	/// ditto
429 	void fractionalSeconds(int a) {     setWithMask(a, 00, 10); }
430 
431 	/// ditto
432 	int  seconds() const          { return getFromMask(10,  6); }
433 	/// ditto
434 	void seconds(int a)           {     setWithMask(a, 10,  6); }
435 	/// ditto
436 	int  minutes() const          { return getFromMask(16,  6); }
437 	/// ditto
438 	void minutes(int a)           {     setWithMask(a, 16,  6); }
439 	/// ditto
440 	int  hours() const            { return getFromMask(22,  5); }
441 	/// ditto
442 	void hours(int a)             {     setWithMask(a, 22,  5); }
443 
444 	/// ditto
445 	int  day() const              { return getFromMask(27,  5); }
446 	/// ditto
447 	void day(int a)               {     setWithMask(a, 27,  5); }
448 	/// ditto
449 	int  month() const            { return getFromMask(32,  4); }
450 	/// ditto
451 	void month(int a)             {     setWithMask(a, 32,  4); }
452 	/// ditto
453 	int  year() const             { return getFromMask(36, 16); }
454 	/// ditto
455 	void year(int a)              {     setWithMask(a, 36, 16); }
456 
457 	/// ditto
458 	bool hasTime() const          { return cast(bool) getFromMask(52,  1); }
459 	/// ditto
460 	void hasTime(bool a)          {     setWithMask(a, 52,  1); }
461 	/// ditto
462 	bool hasDate() const          { return cast(bool) getFromMask(53,  1); }
463 	/// ditto
464 	void hasDate(bool a)          {     setWithMask(a, 53,  1); }
465 
466 	private void setWithMask(int a, int bitOffset, int bitCount) {
467 		auto mask = (1UL << bitCount) - 1;
468 
469 		packedData &= ~(mask << bitOffset);
470 		packedData |= (a & mask) << bitOffset;
471 	}
472 
473 	private int getFromMask(int bitOffset, int bitCount) const {
474 		ulong packedData = this.packedData;
475 		packedData >>= bitOffset;
476 
477 		ulong mask = (1UL << bitCount) - 1;
478 
479 		return cast(int) (packedData & mask);
480 	}
481 }
482 
483 unittest {
484 	PackedDateTime dt;
485 	dt.hours = 14;
486 	dt.minutes = 30;
487 	dt.seconds = 25;
488 	dt.hasTime = true;
489 
490 	assert(dt.toString() == "14:30:25", dt.toString());
491 
492 	dt.hasTime = false;
493 	dt.year = 2024;
494 	dt.month = 5;
495 	dt.day = 31;
496 	dt.hasDate = true;
497 
498 	assert(dt.toString() == "2024-05-31", dt.toString());
499 	dt.hasTime = true;
500 	assert(dt.toString() == "2024-05-31T14:30:25", dt.toString());
501 }
502 
503 /++
504 	Basically a Phobos SysTime but standing alone as a simple 6 4 bit integer (but wrapped) for compatibility with LimitedVariant.
505 +/
506 struct SimplifiedUtcTimestamp {
507 	long timestamp;
508 
509 	string toString() const {
510 		import core.stdc.time;
511 		char[128] buffer;
512 		auto ut = toUnixTime();
513 		tm* t = gmtime(&ut);
514 		if(t is null)
515 			return "null time";
516 
517 		return buffer[0 .. strftime(buffer.ptr, buffer.length, "%Y-%m-%dT%H:%M:%SZ", t)].idup;
518 	}
519 
520 	version(Windows)
521 		alias time_t = int;
522 
523 	static SimplifiedUtcTimestamp fromUnixTime(time_t t) {
524 		return SimplifiedUtcTimestamp(621_355_968_000_000_000L + t * 1_000_000_000L / 100);
525 	}
526 
527 	time_t toUnixTime() const {
528 		return cast(time_t) ((timestamp - 621_355_968_000_000_000L) / 1_000_000_0); // hnsec = 7 digits
529 	}
530 }
531 
532 unittest {
533 	SimplifiedUtcTimestamp sut = SimplifiedUtcTimestamp.fromUnixTime(86_400);
534 	assert(sut.toString() == "1970-01-02T00:00:00Z");
535 }
536 
537 /++
538 	A limited variant to hold just a few types. It is made for the use of packing a small amount of extra data into error messages and some transit across virtual function boundaries.
539 +/
540 /+
541 	ALL OF THESE ARE SUBJECT TO CHANGE
542 
543 	* if length and ptr are both 0, it is null
544 	* if ptr == 1, length is an integer
545 	* if ptr == 2, length is an unsigned integer (suggest printing in hex)
546 	* if ptr == 3, length is a combination of flags (suggest printing in binary)
547 	* if ptr == 4, length is a unix permission thing (suggest printing in octal)
548 	* if ptr == 5, length is a double float
549 	* if ptr == 6, length is an Object ref (reinterpret casted to void*)
550 
551 	* if ptr == 7, length is a ticks count (from MonoTime)
552 	* if ptr == 8, length is a utc timestamp (hnsecs)
553 	* if ptr == 9, length is a duration (signed hnsecs)
554 	* if ptr == 10, length is a date or date time (bit packed, see flags in data to determine if it is a Date, Time, or DateTime)
555 	* if ptr == 11, length is a dchar
556 	* if ptr == 12, length is a bool (redundant to int?)
557 
558 	13, 14 reserved. prolly decimals. (4, 8 digits after decimal)
559 
560 	* if ptr == 15, length must be 0. this holds an empty, non-null, SSO string.
561 	* if ptr >= 16 && < 24, length is reinterpret-casted a small string of length of (ptr & 0x7) + 1
562 
563 	* if length == size_t.max, ptr is interpreted as a stringz
564 	* if ptr >= 1024, it is a non-null D string or byte array. It is a string if the length high bit is clear, a byte array if it is set. the length is what is left after you mask that out.
565 
566 	All other ptr values are reserved for future expansion.
567 
568 	It basically can store:
569 		null
570 			type details = must be 0
571 		int (actually long)
572 			type details = formatting hints
573 		float (actually double)
574 			type details = formatting hints
575 		dchar (actually enum - upper half is the type tag, lower half is the member tag)
576 			type details = ???
577 		decimal
578 			type details = precision specifier
579 		object
580 			type details = ???
581 		timestamp
582 			type details: ticks, utc timestamp, relative duration
583 
584 		sso
585 		stringz
586 
587 		or it is bytes or a string; a normal D array (just bytes has a high bit set on length).
588 
589 	But there are subtypes of some of those; ints can just have formatting hints attached.
590 		Could reserve 0-7 as low level type flag (null, int, float, pointer, object)
591 		15-24 still can be the sso thing
592 
593 		We have 10 bits really.
594 
595 		00000 00000
596 		????? OOLLL
597 
598 		The ????? are type details bits.
599 
600 	64 bits decmial to 4 points of precision needs... 14 bits for the small part (so max of 4 digits)? so 50 bits for the big part (max of about 1 quadrillion)
601 		...actually it can just be a dollars * 10000 + cents * 100.
602 
603 +/
604 struct LimitedVariant {
605 
606 	/++
607 
608 	+/
609 	enum Contains {
610 		null_,
611 		intDecimal,
612 		intHex,
613 		intBinary,
614 		intOctal,
615 		double_,
616 		object,
617 
618 		monoTime,
619 		utcTimestamp,
620 		duration,
621 		dateTime,
622 
623 		// FIXME boolean? char? decimal?
624 		// could do enums by way of a pointer but kinda iffy
625 
626 		// maybe some kind of prefixed string too for stuff like xml and json or enums etc.
627 
628 		// fyi can also use stringzs or length-prefixed string pointers
629 		emptySso,
630 		stringSso,
631 		stringz,
632 		string,
633 		bytes,
634 
635 		invalid,
636 	}
637 
638 	/++
639 		Each datum stored in the LimitedVariant has a tag associated with it.
640 
641 		Each tag belongs to one or more data families.
642 	+/
643 	Contains contains() const {
644 		auto tag = cast(size_t) ptr;
645 		if(ptr is null && length is null)
646 			return Contains.null_;
647 		else switch(tag) {
648 			case 1: return Contains.intDecimal;
649 			case 2: return Contains.intHex;
650 			case 3: return Contains.intBinary;
651 			case 4: return Contains.intOctal;
652 			case 5: return Contains.double_;
653 			case 6: return Contains.object;
654 
655 			case 7: return Contains.monoTime;
656 			case 8: return Contains.utcTimestamp;
657 			case 9: return Contains.duration;
658 			case 10: return Contains.dateTime;
659 
660 			case 15: return length is null ? Contains.emptySso : Contains.invalid;
661 			default:
662 				if(tag >= 16 && tag < 24) {
663 					return Contains.stringSso;
664 				} else if(tag >= 1024) {
665 					if(cast(size_t) length == size_t.max)
666 						return Contains.stringz;
667 					else
668 						return isHighBitSet ? Contains.bytes : Contains..string;
669 				} else {
670 					return Contains.invalid;
671 				}
672 		}
673 	}
674 
675 	/// ditto
676 	bool containsNull() const {
677 		return contains() == Contains.null_;
678 	}
679 
680 	/// ditto
681 	bool containsInt() const {
682 		with(Contains)
683 		switch(contains) {
684 			case intDecimal, intHex, intBinary, intOctal:
685 				return true;
686 			default:
687 				return false;
688 		}
689 	}
690 
691 	// all specializations of int...
692 
693 	/// ditto
694 	bool containsMonoTime() const {
695 		return contains() == Contains.monoTime;
696 	}
697 	/// ditto
698 	bool containsUtcTimestamp() const {
699 		return contains() == Contains.utcTimestamp;
700 	}
701 	/// ditto
702 	bool containsDuration() const {
703 		return contains() == Contains.duration;
704 	}
705 	/// ditto
706 	bool containsDateTime() const {
707 		return contains() == Contains.dateTime;
708 	}
709 
710 	// done int specializations
711 
712 	/// ditto
713 	bool containsString() const {
714 		with(Contains)
715 		switch(contains) {
716 			case null_, emptySso, stringSso, string:
717 			case stringz:
718 				return true;
719 			default:
720 				return false;
721 		}
722 	}
723 
724 	/// ditto
725 	bool containsDouble() const {
726 		with(Contains)
727 		switch(contains) {
728 			case double_:
729 				return true;
730 			default:
731 				return false;
732 		}
733 	}
734 
735 	/// ditto
736 	bool containsBytes() const {
737 		with(Contains)
738 		switch(contains) {
739 			case bytes, null_:
740 				return true;
741 			default:
742 				return false;
743 		}
744 	}
745 
746 	private const(void)* length;
747 	private const(ubyte)* ptr;
748 
749 	private void Throw() const {
750 		throw ArsdException!"LimitedVariant"(cast(size_t) length, cast(size_t) ptr);
751 	}
752 
753 	private bool isHighBitSet() const {
754 		return (cast(size_t) length >> (size_t.sizeof * 8 - 1) & 0x1) != 0;
755 	}
756 
757 	/++
758 		getString gets a reference to the string stored internally, see [toString] to get a string representation or whatever is inside.
759 
760 	+/
761 	const(char)[] getString() const return {
762 		with(Contains)
763 		switch(contains()) {
764 			case null_:
765 				return null;
766 			case emptySso:
767 				return (cast(const(char)*) ptr)[0 .. 0]; // zero length, non-null
768 			case stringSso:
769 				auto len = ((cast(size_t) ptr) & 0x7) + 1;
770 				return (cast(char*) &length)[0 .. len];
771 			case string:
772 				return (cast(const(char)*) ptr)[0 .. cast(size_t) length];
773 			case stringz:
774 				return arsd.core.stringz(cast(char*) ptr).borrow;
775 			default:
776 				Throw(); assert(0);
777 		}
778 	}
779 
780 	/// ditto
781 	long getInt() const {
782 		if(containsInt)
783 			return cast(long) length;
784 		else
785 			Throw();
786 		assert(0);
787 	}
788 
789 	/// ditto
790 	double getDouble() const {
791 		if(containsDouble) {
792 			floathack hack;
793 			hack.e = cast(void*) length; // casting away const
794 			return hack.d;
795 		} else
796 			Throw();
797 		assert(0);
798 	}
799 
800 	/// ditto
801 	const(ubyte)[] getBytes() const {
802 		with(Contains)
803 		switch(contains()) {
804 			case null_:
805 				return null;
806 			case bytes:
807 				return ptr[0 .. (cast(size_t) length) & ((1UL << (size_t.sizeof * 8 - 1)) - 1)];
808 			default:
809 				Throw(); assert(0);
810 		}
811 	}
812 
813 	/// ditto
814 	Object getObject() const {
815 		with(Contains)
816 		switch(contains()) {
817 			case null_:
818 				return null;
819 			case object:
820 				return cast(Object) length; // FIXME const correctness sigh
821 			default:
822 				Throw(); assert(0);
823 		}
824 	}
825 
826 	/// ditto
827 	MonoTime getMonoTime() const {
828 		if(containsMonoTime) {
829 			MonoTime time;
830 			__traits(getMember, time, "_ticks") = cast(long) length;
831 			return time;
832 		} else
833 			Throw();
834 		assert(0);
835 	}
836 	/// ditto
837 	SimplifiedUtcTimestamp getUtcTimestamp() const {
838 		if(containsUtcTimestamp)
839 			return SimplifiedUtcTimestamp(cast(long) length);
840 		else
841 			Throw();
842 		assert(0);
843 	}
844 	/// ditto
845 	Duration getDuration() const {
846 		if(containsDuration)
847 			return hnsecs(cast(long) length);
848 		else
849 			Throw();
850 		assert(0);
851 	}
852 	/// ditto
853 	PackedDateTime getDateTime() const {
854 		if(containsDateTime)
855 			return PackedDateTime(cast(long) length);
856 		else
857 			Throw();
858 		assert(0);
859 	}
860 
861 
862 	/++
863 
864 	+/
865 	string toString() const {
866 
867 		string intHelper(string prefix, int radix) {
868 			char[128] buffer;
869 			buffer[0 .. prefix.length] = prefix[];
870 			char[] toUse = buffer[prefix.length .. $];
871 
872 			auto got = intToString(getInt(), toUse[], IntToStringArgs().withRadix(radix));
873 
874 			return buffer[0 .. prefix.length + got.length].idup;
875 		}
876 
877 		with(Contains)
878 		final switch(contains()) {
879 			case null_:
880 				return "<null>";
881 			case intDecimal:
882 				return intHelper("", 10);
883 			case intHex:
884 				return intHelper("0x", 16);
885 			case intBinary:
886 				return intHelper("0b", 2);
887 			case intOctal:
888 				return intHelper("0o", 8);
889 			case emptySso, stringSso, string, stringz:
890 				return getString().idup;
891 			case bytes:
892 				auto b = getBytes();
893 
894 				return "<bytes>"; // FIXME
895 			case object:
896 				auto o = getObject();
897 				return o is null ? "null" : o.toString();
898 			case monoTime:
899 				return getMonoTime.toString();
900 			case utcTimestamp:
901 				return getUtcTimestamp().toString();
902 			case duration:
903 				return getDuration().toString();
904 			case dateTime:
905 				return getDateTime().toString();
906 			case double_:
907 				auto d = getDouble();
908 
909 				import core.stdc.stdio;
910 				char[128] buffer;
911 				auto count = snprintf(buffer.ptr, buffer.length, "%.17lf", d);
912 				return buffer[0 .. count].idup;
913 			case invalid:
914 				return "<invalid>";
915 		}
916 	}
917 
918 	/++
919 		Note for integral types that are not `int` and `long` (for example, `short` or `ubyte`), you might want to explicitly convert them to `int`.
920 	+/
921 	this(string s) {
922 		ptr = cast(const(ubyte)*) s.ptr;
923 		length = cast(void*) s.length;
924 	}
925 
926 	/// ditto
927 	this(const(char)* stringz) {
928 		if(stringz !is null) {
929 			ptr = cast(const(ubyte)*) stringz;
930 			length = cast(void*) size_t.max;
931 		} else {
932 			ptr = null;
933 			length = null;
934 		}
935 	}
936 
937 	/// ditto
938 	this(const(ubyte)[] b) {
939 		ptr = cast(const(ubyte)*) b.ptr;
940 		length = cast(void*) (b.length | (1UL << (size_t.sizeof * 8 - 1)));
941 	}
942 
943 	/// ditto
944 	this(long l, int base = 10) {
945 		int tag;
946 		switch(base) {
947 			case 10: tag = 1; break;
948 			case 16: tag = 2; break;
949 			case  2: tag = 3; break;
950 			case  8: tag = 4; break;
951 			default: assert(0, "You passed an invalid base to LimitedVariant");
952 		}
953 		ptr = cast(ubyte*) tag;
954 		length = cast(void*) l;
955 	}
956 
957 	/// ditto
958 	this(int i, int base = 10) {
959 		this(cast(long) i, base);
960 	}
961 
962 	/// ditto
963 	this(bool i) {
964 		// FIXME?
965 		this(cast(long) i);
966 	}
967 
968 	/// ditto
969 	this(double d) {
970 		// the reinterpret cast hack crashes dmd! omg
971 		ptr = cast(ubyte*) 5;
972 
973 		floathack h;
974 		h.d = d;
975 
976 		this.length = h.e;
977 	}
978 
979 	/// ditto
980 	this(Object o) {
981 		this.ptr = cast(ubyte*) 6;
982 		this.length = cast(void*) o;
983 	}
984 
985 	/// ditto
986 	this(MonoTime a) {
987 		this.ptr = cast(ubyte*) 7;
988 		this.length = cast(void*) a.ticks;
989 	}
990 
991 	/// ditto
992 	this(SimplifiedUtcTimestamp a) {
993 		this.ptr = cast(ubyte*) 8;
994 		this.length = cast(void*) a.timestamp;
995 	}
996 
997 	/// ditto
998 	this(Duration a) {
999 		this.ptr = cast(ubyte*) 9;
1000 		this.length = cast(void*) a.total!"hnsecs";
1001 	}
1002 
1003 	/// ditto
1004 	this(PackedDateTime a) {
1005 		this.ptr = cast(ubyte*) 10;
1006 		this.length = cast(void*) a.packedData;
1007 	}
1008 }
1009 
1010 unittest {
1011 	LimitedVariant v = LimitedVariant("foo");
1012 	assert(v.containsString());
1013 	assert(!v.containsInt());
1014 	assert(v.getString() == "foo");
1015 
1016 	LimitedVariant v2 = LimitedVariant(4);
1017 	assert(v2.containsInt());
1018 	assert(!v2.containsString());
1019 	assert(v2.getInt() == 4);
1020 
1021 	LimitedVariant v3 = LimitedVariant(cast(ubyte[]) [1, 2, 3]);
1022 	assert(v3.containsBytes());
1023 	assert(!v3.containsString());
1024 	assert(v3.getBytes() == [1, 2, 3]);
1025 }
1026 
1027 private union floathack {
1028 	// in 32 bit we'll use float instead since it at least fits in the void*
1029 	static if(double.sizeof == (void*).sizeof) {
1030 		double d;
1031 	} else {
1032 		float d;
1033 	}
1034 	void* e;
1035 }
1036 
1037 /++
1038 	This is a dummy type to indicate the end of normal arguments and the beginning of the file/line inferred args.  It is meant to ensure you don't accidentally send a string that is interpreted as a filename when it was meant to be a normal argument to the function and trigger the wrong overload.
1039 +/
1040 struct ArgSentinel {}
1041 
1042 /++
1043 	A trivial wrapper around C's malloc that creates a D slice. It multiples n by T.sizeof and returns the slice of the pointer from 0 to n.
1044 
1045 	Please note that the ptr might be null - it is your responsibility to check that, same as normal malloc. Check `ret is null` specifically, since `ret.length` will always be `n`, even if the `malloc` failed.
1046 
1047 	Remember to `free` the returned pointer with `core.stdc.stdlib.free(ret.ptr);`
1048 
1049 	$(TIP
1050 		I strongly recommend you simply use the normal garbage collector unless you have a very specific reason not to.
1051 	)
1052 
1053 	See_Also:
1054 		[mallocedStringz]
1055 +/
1056 T[] mallocSlice(T)(size_t n) {
1057 	import c = core.stdc.stdlib;
1058 
1059 	return (cast(T*) c.malloc(n * T.sizeof))[0 .. n];
1060 }
1061 
1062 /++
1063 	Uses C's malloc to allocate a copy of `original` with an attached zero terminator. It may return a slice with a `null` pointer (but non-zero length!) if `malloc` fails and you are responsible for freeing the returned pointer with `core.stdc.stdlib.free(ret.ptr)`.
1064 
1065 	$(TIP
1066 		I strongly recommend you use [CharzBuffer] or Phobos' [std.string.toStringz] instead unless there's a special reason not to.
1067 	)
1068 
1069 	See_Also:
1070 		[CharzBuffer] for a generally better alternative. You should only use `mallocedStringz` where `CharzBuffer` cannot be used (e.g. when druntime is not usable or you have no stack space for the temporary buffer).
1071 
1072 		[mallocSlice] is the function this function calls, so the notes in its documentation applies here too.
1073 +/
1074 char[] mallocedStringz(in char[] original) {
1075 	auto slice = mallocSlice!char(original.length + 1);
1076 	if(slice is null)
1077 		return null;
1078 	slice[0 .. original.length] = original[];
1079 	slice[original.length] = 0;
1080 	return slice;
1081 }
1082 
1083 /++
1084 	Basically a `scope class` you can return from a function or embed in another aggregate.
1085 +/
1086 struct OwnedClass(Class) {
1087 	ubyte[__traits(classInstanceSize, Class)] rawData;
1088 
1089 	static OwnedClass!Class defaultConstructed() {
1090 		OwnedClass!Class i = OwnedClass!Class.init;
1091 		i.initializeRawData();
1092 		return i;
1093 	}
1094 
1095 	private void initializeRawData() @trusted {
1096 		if(!this)
1097 			rawData[] = cast(ubyte[]) typeid(Class).initializer[];
1098 	}
1099 
1100 	this(T...)(T t) {
1101 		initializeRawData();
1102 		rawInstance.__ctor(t);
1103 	}
1104 
1105 	bool opCast(T : bool)() @trusted {
1106 		return !(*(cast(void**) rawData.ptr) is null);
1107 	}
1108 
1109 	@disable this();
1110 	@disable this(this);
1111 
1112 	Class rawInstance() return @trusted {
1113 		if(!this)
1114 			throw new Exception("null");
1115 		return cast(Class) rawData.ptr;
1116 	}
1117 
1118 	alias rawInstance this;
1119 
1120 	~this() @trusted {
1121 		if(this)
1122 			.destroy(rawInstance());
1123 	}
1124 }
1125 
1126 // might move RecyclableMemory here
1127 
1128 version(Posix)
1129 package(arsd) void makeNonBlocking(int fd) {
1130 	import core.sys.posix.fcntl;
1131 	auto flags = fcntl(fd, F_GETFL, 0);
1132 	if(flags == -1)
1133 		throw new ErrnoApiException("fcntl get", errno);
1134 	flags |= O_NONBLOCK;
1135 	auto s = fcntl(fd, F_SETFL, flags);
1136 	if(s == -1)
1137 		throw new ErrnoApiException("fcntl set", errno);
1138 }
1139 
1140 version(Posix)
1141 package(arsd) void setCloExec(int fd) {
1142 	import core.sys.posix.fcntl;
1143 	auto flags = fcntl(fd, F_GETFD, 0);
1144 	if(flags == -1)
1145 		throw new ErrnoApiException("fcntl get", errno);
1146 	flags |= FD_CLOEXEC;
1147 	auto s = fcntl(fd, F_SETFD, flags);
1148 	if(s == -1)
1149 		throw new ErrnoApiException("fcntl set", errno);
1150 }
1151 
1152 
1153 /++
1154 	A helper object for temporarily constructing a string appropriate for the Windows API from a D UTF-8 string.
1155 
1156 
1157 	It will use a small internal static buffer is possible, and allocate a new buffer if the string is too big.
1158 
1159 	History:
1160 		Moved from simpledisplay.d to core.d in March 2023 (dub v11.0).
1161 +/
1162 version(Windows)
1163 struct WCharzBuffer {
1164 	private wchar[] buffer;
1165 	private wchar[128] staticBuffer = void;
1166 
1167 	/// Length of the string, excluding the zero terminator.
1168 	size_t length() {
1169 		return buffer.length;
1170 	}
1171 
1172 	// Returns the pointer to the internal buffer. You must assume its lifetime is less than that of the WCharzBuffer. It is zero-terminated.
1173 	wchar* ptr() {
1174 		return buffer.ptr;
1175 	}
1176 
1177 	/// Returns the slice of the internal buffer, excluding the zero terminator (though there is one present right off the end of the slice). You must assume its lifetime is less than that of the WCharzBuffer.
1178 	wchar[] slice() {
1179 		return buffer;
1180 	}
1181 
1182 	/// Copies it into a static array of wchars
1183 	void copyInto(R)(ref R r) {
1184 		static if(is(R == wchar[N], size_t N)) {
1185 			r[0 .. this.length] = slice[];
1186 			r[this.length] = 0;
1187 		} else static assert(0, "can only copy into wchar[n], not " ~ R.stringof);
1188 	}
1189 
1190 	/++
1191 		conversionFlags = [WindowsStringConversionFlags]
1192 	+/
1193 	this(in char[] data, int conversionFlags = 0) {
1194 		conversionFlags |= WindowsStringConversionFlags.zeroTerminate; // this ALWAYS zero terminates cuz of its name
1195 		auto sz = sizeOfConvertedWstring(data, conversionFlags);
1196 		if(sz > staticBuffer.length)
1197 			buffer = new wchar[](sz);
1198 		else
1199 			buffer = staticBuffer[];
1200 
1201 		buffer = makeWindowsString(data, buffer, conversionFlags);
1202 	}
1203 }
1204 
1205 /++
1206 	Alternative for toStringz
1207 
1208 	History:
1209 		Added March 18, 2023 (dub v11.0)
1210 +/
1211 struct CharzBuffer {
1212 	private char[] buffer;
1213 	private char[128] staticBuffer = void;
1214 
1215 	/// Length of the string, excluding the zero terminator.
1216 	size_t length() {
1217 		assert(buffer.length > 0);
1218 		return buffer.length - 1;
1219 	}
1220 
1221 	// Returns the pointer to the internal buffer. You must assume its lifetime is less than that of the CharzBuffer. It is zero-terminated.
1222 	char* ptr() {
1223 		return buffer.ptr;
1224 	}
1225 
1226 	/// Returns the slice of the internal buffer, excluding the zero terminator (though there is one present right off the end of the slice). You must assume its lifetime is less than that of the CharzBuffer.
1227 	char[] slice() {
1228 		assert(buffer.length > 0);
1229 		return buffer[0 .. $-1];
1230 	}
1231 
1232 	/// Copies it into a static array of chars
1233 	void copyInto(R)(ref R r) {
1234 		static if(is(R == char[N], size_t N)) {
1235 			r[0 .. this.length] = slice[];
1236 			r[this.length] = 0;
1237 		} else static assert(0, "can only copy into char[n], not " ~ R.stringof);
1238 	}
1239 
1240 	@disable this();
1241 	@disable this(this);
1242 
1243 	/++
1244 		Copies `data` into the CharzBuffer, allocating a new one if needed, and zero-terminates it.
1245 	+/
1246 	this(in char[] data) {
1247 		if(data.length + 1 > staticBuffer.length)
1248 			buffer = new char[](data.length + 1);
1249 		else
1250 			buffer = staticBuffer[];
1251 
1252 		buffer[0 .. data.length] = data[];
1253 		buffer[data.length] = 0;
1254 	}
1255 }
1256 
1257 /++
1258 	Given the string `str`, converts it to a string compatible with the Windows API and puts the result in `buffer`, returning the slice of `buffer` actually used. `buffer` must be at least [sizeOfConvertedWstring] elements long.
1259 
1260 	History:
1261 		Moved from simpledisplay.d to core.d in March 2023 (dub v11.0).
1262 +/
1263 version(Windows)
1264 wchar[] makeWindowsString(in char[] str, wchar[] buffer, int conversionFlags = WindowsStringConversionFlags.zeroTerminate) {
1265 	if(str.length == 0)
1266 		return null;
1267 
1268 	int pos = 0;
1269 	dchar last;
1270 	foreach(dchar c; str) {
1271 		if(c <= 0xFFFF) {
1272 			if((conversionFlags & WindowsStringConversionFlags.convertNewLines) && c == 10 && last != 13)
1273 				buffer[pos++] = 13;
1274 			buffer[pos++] = cast(wchar) c;
1275 		} else if(c <= 0x10FFFF) {
1276 			buffer[pos++] = cast(wchar)((((c - 0x10000) >> 10) & 0x3FF) + 0xD800);
1277 			buffer[pos++] = cast(wchar)(((c - 0x10000) & 0x3FF) + 0xDC00);
1278 		}
1279 
1280 		last = c;
1281 	}
1282 
1283 	if(conversionFlags & WindowsStringConversionFlags.zeroTerminate) {
1284 		buffer[pos] = 0;
1285 	}
1286 
1287 	return buffer[0 .. pos];
1288 }
1289 
1290 /++
1291 	Converts the Windows API string `str` to a D UTF-8 string, storing it in `buffer`. Returns the slice of `buffer` actually used.
1292 
1293 	History:
1294 		Moved from simpledisplay.d to core.d in March 2023 (dub v11.0).
1295 +/
1296 version(Windows)
1297 char[] makeUtf8StringFromWindowsString(in wchar[] str, char[] buffer) {
1298 	if(str.length == 0)
1299 		return null;
1300 
1301 	auto got = WideCharToMultiByte(CP_UTF8, 0, str.ptr, cast(int) str.length, buffer.ptr, cast(int) buffer.length, null, null);
1302 	if(got == 0) {
1303 		if(GetLastError() == ERROR_INSUFFICIENT_BUFFER)
1304 			throw new object.Exception("not enough buffer");
1305 		else
1306 			throw new object.Exception("conversion"); // FIXME: GetLastError
1307 	}
1308 	return buffer[0 .. got];
1309 }
1310 
1311 /++
1312 	Converts the Windows API string `str` to a newly-allocated D UTF-8 string.
1313 
1314 	History:
1315 		Moved from simpledisplay.d to core.d in March 2023 (dub v11.0).
1316 +/
1317 version(Windows)
1318 string makeUtf8StringFromWindowsString(in wchar[] str) {
1319 	char[] buffer;
1320 	auto got = WideCharToMultiByte(CP_UTF8, 0, str.ptr, cast(int) str.length, null, 0, null, null);
1321 	buffer.length = got;
1322 
1323 	// it is unique because we just allocated it above!
1324 	return cast(string) makeUtf8StringFromWindowsString(str, buffer);
1325 }
1326 
1327 /// ditto
1328 version(Windows)
1329 string makeUtf8StringFromWindowsString(wchar* str) {
1330 	char[] buffer;
1331 	auto got = WideCharToMultiByte(CP_UTF8, 0, str, -1, null, 0, null, null);
1332 	buffer.length = got;
1333 
1334 	got = WideCharToMultiByte(CP_UTF8, 0, str, -1, buffer.ptr, cast(int) buffer.length, null, null);
1335 	if(got == 0) {
1336 		if(GetLastError() == ERROR_INSUFFICIENT_BUFFER)
1337 			throw new object.Exception("not enough buffer");
1338 		else
1339 			throw new object.Exception("conversion"); // FIXME: GetLastError
1340 	}
1341 	return cast(string) buffer[0 .. got];
1342 }
1343 
1344 // only used from minigui rn
1345 package int findIndexOfZero(in wchar[] str) {
1346 	foreach(idx, wchar ch; str)
1347 		if(ch == 0)
1348 			return cast(int) idx;
1349 	return cast(int) str.length;
1350 }
1351 package int findIndexOfZero(in char[] str) {
1352 	foreach(idx, char ch; str)
1353 		if(ch == 0)
1354 			return cast(int) idx;
1355 	return cast(int) str.length;
1356 }
1357 
1358 /++
1359 	Returns a minimum buffer length to hold the string `s` with the given conversions. It might be slightly larger than necessary, but is guaranteed to be big enough to hold it.
1360 
1361 	History:
1362 		Moved from simpledisplay.d to core.d in March 2023 (dub v11.0).
1363 +/
1364 version(Windows)
1365 int sizeOfConvertedWstring(in char[] s, int conversionFlags) {
1366 	int size = 0;
1367 
1368 	if(conversionFlags & WindowsStringConversionFlags.convertNewLines) {
1369 		// need to convert line endings, which means the length will get bigger.
1370 
1371 		// BTW I betcha this could be faster with some simd stuff.
1372 		char last;
1373 		foreach(char ch; s) {
1374 			if(ch == 10 && last != 13)
1375 				size++; // will add a 13 before it...
1376 			size++;
1377 			last = ch;
1378 		}
1379 	} else {
1380 		// no conversion necessary, just estimate based on length
1381 		/*
1382 			I don't think there's any string with a longer length
1383 			in code units when encoded in UTF-16 than it has in UTF-8.
1384 			This will probably over allocate, but that's OK.
1385 		*/
1386 		size = cast(int) s.length;
1387 	}
1388 
1389 	if(conversionFlags & WindowsStringConversionFlags.zeroTerminate)
1390 		size++;
1391 
1392 	return size;
1393 }
1394 
1395 /++
1396 	Used by [makeWindowsString] and [WCharzBuffer]
1397 
1398 	History:
1399 		Moved from simpledisplay.d to core.d in March 2023 (dub v11.0).
1400 +/
1401 version(Windows)
1402 enum WindowsStringConversionFlags : int {
1403 	/++
1404 		Append a zero terminator to the string.
1405 	+/
1406 	zeroTerminate = 1,
1407 	/++
1408 		Converts newlines from \n to \r\n.
1409 	+/
1410 	convertNewLines = 2,
1411 }
1412 
1413 /++
1414 	An int printing function that doesn't need to import Phobos. Can do some of the things std.conv.to and std.format.format do.
1415 
1416 	The buffer must be sized to hold the converted number. 32 chars is enough for most anything.
1417 
1418 	Returns: the slice of `buffer` containing the converted number.
1419 +/
1420 char[] intToString(long value, char[] buffer, IntToStringArgs args = IntToStringArgs.init) {
1421 	const int radix = args.radix ? args.radix : 10;
1422 	const int digitsPad = args.padTo;
1423 	const int groupSize = args.groupSize;
1424 
1425 	int pos;
1426 
1427 	if(value < 0) {
1428 		buffer[pos++] = '-';
1429 		value = -value;
1430 	}
1431 
1432 	int start = pos;
1433 	int digitCount;
1434 
1435 	do {
1436 		auto remainder = value % radix;
1437 		value = value / radix;
1438 
1439 		buffer[pos++] = cast(char) (remainder < 10 ? (remainder + '0') : (remainder - 10 + args.ten));
1440 		digitCount++;
1441 	} while(value);
1442 
1443 	if(digitsPad > 0) {
1444 		while(digitCount < digitsPad) {
1445 			buffer[pos++] = args.padWith;
1446 			digitCount++;
1447 		}
1448 	}
1449 
1450 	assert(pos >= 1);
1451 	assert(pos - start > 0);
1452 
1453 	auto reverseSlice = buffer[start .. pos];
1454 	for(int i = 0; i < reverseSlice.length / 2; i++) {
1455 		auto paired = cast(int) reverseSlice.length - i - 1;
1456 		char tmp = reverseSlice[i];
1457 		reverseSlice[i] = reverseSlice[paired];
1458 		reverseSlice[paired] = tmp;
1459 	}
1460 
1461 	return buffer[0 .. pos];
1462 }
1463 
1464 /// ditto
1465 struct IntToStringArgs {
1466 	private {
1467 		ubyte padTo;
1468 		char padWith;
1469 		ubyte radix;
1470 		char ten;
1471 		ubyte groupSize;
1472 		char separator;
1473 	}
1474 
1475 	IntToStringArgs withPadding(int padTo, char padWith = '0') {
1476 		IntToStringArgs args = this;
1477 		args.padTo = cast(ubyte) padTo;
1478 		args.padWith = padWith;
1479 		return args;
1480 	}
1481 
1482 	IntToStringArgs withRadix(int radix, char ten = 'a') {
1483 		IntToStringArgs args = this;
1484 		args.radix = cast(ubyte) radix;
1485 		args.ten = ten;
1486 		return args;
1487 	}
1488 
1489 	IntToStringArgs withGroupSeparator(int groupSize, char separator = '_') {
1490 		IntToStringArgs args = this;
1491 		args.groupSize = cast(ubyte) groupSize;
1492 		args.separator = separator;
1493 		return args;
1494 	}
1495 }
1496 
1497 unittest {
1498 	char[32] buffer;
1499 	assert(intToString(0, buffer[]) == "0");
1500 	assert(intToString(-1, buffer[]) == "-1");
1501 	assert(intToString(-132, buffer[]) == "-132");
1502 	assert(intToString(-1932, buffer[]) == "-1932");
1503 	assert(intToString(1, buffer[]) == "1");
1504 	assert(intToString(132, buffer[]) == "132");
1505 	assert(intToString(1932, buffer[]) == "1932");
1506 
1507 	assert(intToString(0x1, buffer[], IntToStringArgs().withRadix(16)) == "1");
1508 	assert(intToString(0x1b, buffer[], IntToStringArgs().withRadix(16)) == "1b");
1509 	assert(intToString(0xef1, buffer[], IntToStringArgs().withRadix(16)) == "ef1");
1510 
1511 	assert(intToString(0xef1, buffer[], IntToStringArgs().withRadix(16).withPadding(8)) == "00000ef1");
1512 	assert(intToString(-0xef1, buffer[], IntToStringArgs().withRadix(16).withPadding(8)) == "-00000ef1");
1513 	assert(intToString(-0xef1, buffer[], IntToStringArgs().withRadix(16, 'A').withPadding(8, ' ')) == "-     EF1");
1514 }
1515 
1516 /++
1517 	History:
1518 		Moved from color.d to core.d in March 2023 (dub v11.0).
1519 +/
1520 nothrow @safe @nogc pure
1521 inout(char)[] stripInternal(return inout(char)[] s) {
1522 	bool isAllWhitespace = true;
1523 	foreach(i, char c; s)
1524 		if(c != ' ' && c != '\t' && c != '\n' && c != '\r') {
1525 			s = s[i .. $];
1526 			isAllWhitespace = false;
1527 			break;
1528 		}
1529 
1530 	if(isAllWhitespace)
1531 		return s[$..$];
1532 
1533 	for(int a = cast(int)(s.length - 1); a > 0; a--) {
1534 		char c = s[a];
1535 		if(c != ' ' && c != '\t' && c != '\n' && c != '\r') {
1536 			s = s[0 .. a + 1];
1537 			break;
1538 		}
1539 	}
1540 
1541 	return s;
1542 }
1543 
1544 /// ditto
1545 nothrow @safe @nogc pure
1546 inout(char)[] stripRightInternal(return inout(char)[] s) {
1547 	bool isAllWhitespace = true;
1548 	foreach_reverse(a, c; s) {
1549 		if(c != ' ' && c != '\t' && c != '\n' && c != '\r') {
1550 			s = s[0 .. a + 1];
1551 			isAllWhitespace = false;
1552 			break;
1553 		}
1554 	}
1555 	if(isAllWhitespace)
1556 		s = s[0..0];
1557 
1558 	return s;
1559 
1560 }
1561 
1562 /++
1563 	Shortcut for converting some types to string without invoking Phobos (but it may as a last resort).
1564 
1565 	History:
1566 		Moved from color.d to core.d in March 2023 (dub v11.0).
1567 +/
1568 string toStringInternal(T)(T t) {
1569 	char[32] buffer;
1570 	static if(is(T : string))
1571 		return t;
1572 	else static if(is(T : long))
1573 		return intToString(t, buffer[]).idup;
1574 	else static if(is(T == enum)) {
1575 		switch(t) {
1576 			foreach(memberName; __traits(allMembers, T)) {
1577 				case __traits(getMember, T, memberName):
1578 					return memberName;
1579 			}
1580 			default:
1581 				return "<unknown>";
1582 		}
1583 	} else static if(is(T : const E[], E)) {
1584 		string ret = "[";
1585 		foreach(idx, e; t) {
1586 			if(idx)
1587 				ret ~= ", ";
1588 			ret ~= toStringInternal(e);
1589 		}
1590 		ret ~= "]";
1591 		return ret;
1592 	} else {
1593 		static assert(0, T.stringof ~ " makes compile too slow");
1594 		// import std.conv; return to!string(t);
1595 	}
1596 }
1597 
1598 /++
1599 
1600 +/
1601 string flagsToString(Flags)(ulong value) {
1602 	string r;
1603 
1604 	void add(string memberName) {
1605 		if(r.length)
1606 			r ~= " | ";
1607 		r ~= memberName;
1608 	}
1609 
1610 	string none = "<none>";
1611 
1612 	foreach(memberName; __traits(allMembers, Flags)) {
1613 		auto flag = cast(ulong) __traits(getMember, Flags, memberName);
1614 		if(flag) {
1615 			if((value & flag) == flag)
1616 				add(memberName);
1617 		} else {
1618 			none = memberName;
1619 		}
1620 	}
1621 
1622 	if(r.length == 0)
1623 		r = none;
1624 
1625 	return r;
1626 }
1627 
1628 unittest {
1629 	enum MyFlags {
1630 		none = 0,
1631 		a = 1,
1632 		b = 2
1633 	}
1634 
1635 	assert(flagsToString!MyFlags(3) == "a | b");
1636 	assert(flagsToString!MyFlags(0) == "none");
1637 	assert(flagsToString!MyFlags(2) == "b");
1638 }
1639 
1640 private enum dchar replacementDchar = '\uFFFD';
1641 
1642 package size_t encodeUtf8(out char[4] buf, dchar c) @safe pure {
1643     if (c <= 0x7F)
1644     {
1645         assert(isValidDchar(c));
1646         buf[0] = cast(char) c;
1647         return 1;
1648     }
1649     if (c <= 0x7FF)
1650     {
1651         assert(isValidDchar(c));
1652         buf[0] = cast(char)(0xC0 | (c >> 6));
1653         buf[1] = cast(char)(0x80 | (c & 0x3F));
1654         return 2;
1655     }
1656     if (c <= 0xFFFF)
1657     {
1658         if (0xD800 <= c && c <= 0xDFFF)
1659             c = replacementDchar;
1660 
1661         assert(isValidDchar(c));
1662     L3:
1663         buf[0] = cast(char)(0xE0 | (c >> 12));
1664         buf[1] = cast(char)(0x80 | ((c >> 6) & 0x3F));
1665         buf[2] = cast(char)(0x80 | (c & 0x3F));
1666         return 3;
1667     }
1668     if (c <= 0x10FFFF)
1669     {
1670         assert(isValidDchar(c));
1671         buf[0] = cast(char)(0xF0 | (c >> 18));
1672         buf[1] = cast(char)(0x80 | ((c >> 12) & 0x3F));
1673         buf[2] = cast(char)(0x80 | ((c >> 6) & 0x3F));
1674         buf[3] = cast(char)(0x80 | (c & 0x3F));
1675         return 4;
1676     }
1677 
1678     assert(!isValidDchar(c));
1679     c = replacementDchar;
1680     goto L3;
1681 }
1682 
1683 
1684 
1685 private bool isValidDchar(dchar c) pure nothrow @safe @nogc
1686 {
1687     return c < 0xD800 || (c > 0xDFFF && c <= 0x10FFFF);
1688 }
1689 
1690 // technically s is octets but meh
1691 package string encodeUriComponent(string s) {
1692 	char[3] encodeChar(char c) {
1693 		char[3] buffer;
1694 		buffer[0] = '%';
1695 
1696 		enum hexchars = "0123456789ABCDEF";
1697 		buffer[1] = hexchars[c >> 4];
1698 		buffer[2] = hexchars[c & 0x0f];
1699 
1700 		return buffer;
1701 	}
1702 
1703 	string n;
1704 	size_t previous = 0;
1705 	foreach(idx, char ch; s) {
1706 		if(
1707 			(ch >= 'A' && ch <= 'Z')
1708 			||
1709 			(ch >= 'a' && ch <= 'z')
1710 			||
1711 			(ch >= '0' && ch <= '9')
1712 			|| ch == '-' || ch == '_' || ch == '.' || ch == '~' // unreserved set
1713 			|| ch == '!' || ch == '*' || ch == '\''|| ch == '(' || ch == ')' // subdelims but allowed in uri component (phobos also no encode them)
1714 		) {
1715 			// does not need encoding
1716 		} else {
1717 			n ~= s[previous .. idx];
1718 			n ~= encodeChar(ch);
1719 			previous = idx + 1;
1720 		}
1721 	}
1722 
1723 	if(n.length) {
1724 		n ~= s[previous .. $];
1725 		return n;
1726 	} else {
1727 		return s; // nothing needed encoding
1728 	}
1729 }
1730 unittest {
1731 	assert(encodeUriComponent("foo") == "foo");
1732 	assert(encodeUriComponent("f33Ao") == "f33Ao");
1733 	assert(encodeUriComponent("/") == "%2F");
1734 	assert(encodeUriComponent("/foo") == "%2Ffoo");
1735 	assert(encodeUriComponent("foo/") == "foo%2F");
1736 	assert(encodeUriComponent("foo/bar") == "foo%2Fbar");
1737 	assert(encodeUriComponent("foo/bar/") == "foo%2Fbar%2F");
1738 }
1739 
1740 // FIXME: I think if translatePlusToSpace we're supposed to do newline normalization too
1741 package string decodeUriComponent(string s, bool translatePlusToSpace = false) {
1742 	int skipping = 0;
1743 	size_t previous = 0;
1744 	string n = null;
1745 	foreach(idx, char ch; s) {
1746 		if(skipping) {
1747 			skipping--;
1748 			continue;
1749 		}
1750 
1751 		if(ch == '%') {
1752 			int hexDecode(char c) {
1753 				if(c >= 'A' && c <= 'F')
1754 					return c - 'A' + 10;
1755 				else if(c >= 'a' && c <= 'f')
1756 					return c - 'a' + 10;
1757 				else if(c >= '0' && c <= '9')
1758 					return c - '0' + 0;
1759 				else
1760 					throw ArsdException!"Invalid percent-encoding"("Invalid char encountered", idx, s);
1761 			}
1762 
1763 			skipping = 2;
1764 			n ~= s[previous .. idx];
1765 
1766 			if(idx + 2 >= s.length)
1767 				throw ArsdException!"Invalid percent-encoding"("End of string reached", idx, s);
1768 
1769 			n ~= (hexDecode(s[idx + 1]) << 4) | hexDecode(s[idx + 2]);
1770 
1771 			previous = idx + 3;
1772 		} else if(translatePlusToSpace && ch == '+') {
1773 			n ~= s[previous .. idx];
1774 			n ~= " ";
1775 			previous = idx + 1;
1776 		}
1777 	}
1778 
1779 	if(n.length) {
1780 		n ~= s[previous .. $];
1781 		return n;
1782 	} else {
1783 		return s; // nothing needed decoding
1784 	}
1785 }
1786 
1787 unittest {
1788 	assert(decodeUriComponent("foo") == "foo");
1789 	assert(decodeUriComponent("%2F") == "/");
1790 	assert(decodeUriComponent("%2f") == "/");
1791 	assert(decodeUriComponent("%2Ffoo") == "/foo");
1792 	assert(decodeUriComponent("foo%2F") == "foo/");
1793 	assert(decodeUriComponent("foo%2Fbar") == "foo/bar");
1794 	assert(decodeUriComponent("foo%2Fbar%2F") == "foo/bar/");
1795 	assert(decodeUriComponent("%2F%2F%2F") == "///");
1796 
1797 	assert(decodeUriComponent("+") == "+");
1798 	assert(decodeUriComponent("+", true) == " ");
1799 }
1800 
1801 private auto toDelegate(T)(T t) {
1802 	// static assert(is(T == function)); // lol idk how to do what i actually want here
1803 
1804 	static if(is(T Return == return))
1805 	static if(is(typeof(*T) Params == __parameters)) {
1806 		static struct Wrapper {
1807 			Return call(Params params) {
1808 				return (cast(T) &this)(params);
1809 			}
1810 		}
1811 		return &((cast(Wrapper*) t).call);
1812 	} else static assert(0, "could not get params");
1813 	else static assert(0, "could not get return value");
1814 }
1815 
1816 @system unittest {
1817 	int function(int) fn;
1818 	fn = (a) { return a; };
1819 
1820 	int delegate(int) dg = toDelegate(fn);
1821 
1822 	assert(dg.ptr is fn); // it stores the original function as the context pointer
1823 	assert(dg.funcptr !is fn); // which is called through a lil trampoline
1824 	assert(dg(5) == 5); // and forwards the args correctly
1825 }
1826 
1827 /++
1828 	This populates a struct from a list of values (or other expressions, but it only looks at the values) based on types of the members, with one exception: `bool` members.. maybe.
1829 
1830 	It is intended for collecting a record of relevant UDAs off a symbol in a single call like this:
1831 
1832 	---
1833 		struct Name {
1834 			string n;
1835 		}
1836 
1837 		struct Validator {
1838 			string regex;
1839 		}
1840 
1841 		struct FormInfo {
1842 			Name name;
1843 			Validator validator;
1844 		}
1845 
1846 		@Name("foo") @Validator(".*")
1847 		void foo() {}
1848 
1849 		auto info = populateFromUdas!(FormInfo, __traits(getAttributes, foo));
1850 		assert(info.name == Name("foo"));
1851 		assert(info.validator == Validator(".*"));
1852 	---
1853 
1854 	Note that instead of UDAs, you can also pass a variadic argument list and get the same result, but the function is `populateFromArgs` and you pass them as the runtime list to bypass "args cannot be evaluated at compile time" errors:
1855 
1856 	---
1857 		void foo(T...)(T t) {
1858 			auto info = populateFromArgs!(FormInfo)(t);
1859 			// assuming the call below
1860 			assert(info.name == Name("foo"));
1861 			assert(info.validator == Validator(".*"));
1862 		}
1863 
1864 		foo(Name("foo"), Validator(".*"));
1865 	---
1866 
1867 	The benefit of this over constructing the struct directly is that the arguments can be reordered or missing. Its value is diminished with named arguments in the language.
1868 +/
1869 template populateFromUdas(Struct, UDAs...) {
1870 	enum Struct populateFromUdas = () {
1871 		Struct ret;
1872 		foreach(memberName; __traits(allMembers, Struct)) {
1873 			alias memberType = typeof(__traits(getMember, Struct, memberName));
1874 			foreach(uda; UDAs) {
1875 				static if(is(memberType == PresenceOf!a, a)) {
1876 					static if(__traits(isSame, a, uda))
1877 						__traits(getMember, ret, memberName) = true;
1878 				}
1879 				else
1880 				static if(is(typeof(uda) : memberType)) {
1881 					__traits(getMember, ret, memberName) = uda;
1882 				}
1883 			}
1884 		}
1885 
1886 		return ret;
1887 	}();
1888 }
1889 
1890 /// ditto
1891 Struct populateFromArgs(Struct, Args...)(Args args) {
1892 	Struct ret;
1893 	foreach(memberName; __traits(allMembers, Struct)) {
1894 		alias memberType = typeof(__traits(getMember, Struct, memberName));
1895 		foreach(arg; args) {
1896 			static if(is(typeof(arg == memberType))) {
1897 				__traits(getMember, ret, memberName) = arg;
1898 			}
1899 		}
1900 	}
1901 
1902 	return ret;
1903 }
1904 
1905 /// ditto
1906 struct PresenceOf(alias a) {
1907 	bool there;
1908 	alias there this;
1909 }
1910 
1911 ///
1912 unittest {
1913 	enum a;
1914 	enum b;
1915 	struct Name { string name; }
1916 	struct Info {
1917 		Name n;
1918 		PresenceOf!a athere;
1919 		PresenceOf!b bthere;
1920 		int c;
1921 	}
1922 
1923 	void test() @a @Name("test") {}
1924 
1925 	auto info = populateFromUdas!(Info, __traits(getAttributes, test));
1926 	assert(info.n == Name("test")); // but present ones are in there
1927 	assert(info.athere == true); // non-values can be tested with PresenceOf!it, which works like a bool
1928 	assert(info.bthere == false);
1929 	assert(info.c == 0); // absent thing will keep the default value
1930 }
1931 
1932 /++
1933 	Declares a delegate property with several setters to allow for handlers that don't care about the arguments.
1934 
1935 	Throughout the arsd library, you will often see types of these to indicate that you can set listeners with or without arguments. If you care about the details of the callback event, you can set a delegate that declares them. And if you don't, you can set one that doesn't even declare them and it will be ignored.
1936 +/
1937 struct FlexibleDelegate(DelegateType) {
1938 	// please note that Parameters and ReturnType are public now!
1939 	static if(is(DelegateType FunctionType == delegate))
1940 	static if(is(FunctionType Parameters == __parameters))
1941 	static if(is(DelegateType ReturnType == return)) {
1942 
1943 		/++
1944 			Calls the currently set delegate.
1945 
1946 			Diagnostics:
1947 				If the callback delegate has not been set, this may cause a null pointer dereference.
1948 		+/
1949 		ReturnType opCall(Parameters args) {
1950 			return dg(args);
1951 		}
1952 
1953 		/++
1954 			Use `if(thing)` to check if the delegate is null or not.
1955 		+/
1956 		bool opCast(T : bool)() {
1957 			return dg !is null;
1958 		}
1959 
1960 		/++
1961 			These opAssign overloads are what puts the flexibility in the flexible delegate.
1962 
1963 			Bugs:
1964 				The other overloads do not keep attributes like `nothrow` on the `dg` parameter, making them unusable if `DelegateType` requires them. I consider the attributes more trouble than they're worth anyway, and the language's poor support for composing them doesn't help any. I have no need for them and thus no plans to add them in the overloads at this time.
1965 		+/
1966 		void opAssign(DelegateType dg) {
1967 			this.dg = dg;
1968 		}
1969 
1970 		/// ditto
1971 		void opAssign(ReturnType delegate() dg) {
1972 			this.dg = (Parameters ignored) => dg();
1973 		}
1974 
1975 		/// ditto
1976 		void opAssign(ReturnType function(Parameters params) dg) {
1977 			this.dg = (Parameters params) => dg(params);
1978 		}
1979 
1980 		/// ditto
1981 		void opAssign(ReturnType function() dg) {
1982 			this.dg = (Parameters ignored) => dg();
1983 		}
1984 
1985 		/// ditto
1986 		void opAssign(typeof(null) explicitNull) {
1987 			this.dg = null;
1988 		}
1989 
1990 		private DelegateType dg;
1991 	}
1992 	else static assert(0, DelegateType.stringof ~ " failed return value check");
1993 	else static assert(0, DelegateType.stringof ~ " failed parameters check");
1994 	else static assert(0, DelegateType.stringof ~ " failed delegate check");
1995 }
1996 
1997 /++
1998 
1999 +/
2000 unittest {
2001 	// you don't have to put the arguments in a struct, but i recommend
2002 	// you do as it is more future proof - you can add more info to the
2003 	// struct without breaking user code that consumes it.
2004 	struct MyEventArguments {
2005 
2006 	}
2007 
2008 	// then you declare it just adding FlexibleDelegate!() around the
2009 	// plain delegate type you'd normally use
2010 	FlexibleDelegate!(void delegate(MyEventArguments args)) callback;
2011 
2012 	// until you set it, it will be null and thus be false in any boolean check
2013 	assert(!callback);
2014 
2015 	// can set it to the properly typed thing
2016 	callback = delegate(MyEventArguments args) {};
2017 
2018 	// and now it is no longer null
2019 	assert(callback);
2020 
2021 	// or if you don't care about the args, you can leave them off
2022 	callback = () {};
2023 
2024 	// and it works if the compiler types you as a function instead of delegate too
2025 	// (which happens automatically if you don't access any local state or if you
2026 	// explicitly define it as a function)
2027 
2028 	callback = function(MyEventArguments args) { };
2029 
2030 	// can set it back to null explicitly if you ever wanted
2031 	callback = null;
2032 
2033 	// the reflection info used internally also happens to be exposed publicly
2034 	// which can actually sometimes be nice so if the language changes, i'll change
2035 	// the code to keep this working.
2036 	static assert(is(callback.ReturnType == void));
2037 
2038 	// which can be convenient if the params is an annoying type since you can
2039 	// consistently use something like this too
2040 	callback = (callback.Parameters params) {};
2041 
2042 	// check for null and call it pretty normally
2043 	if(callback)
2044 		callback(MyEventArguments());
2045 }
2046 
2047 /+
2048 	======================
2049 	ERROR HANDLING HELPERS
2050 	======================
2051 +/
2052 
2053 /+ +
2054 	arsd code shouldn't be using Exception. Really, I don't think any code should be - instead, construct an appropriate object with structured information.
2055 
2056 	If you want to catch someone else's Exception, use `catch(object.Exception e)`.
2057 +/
2058 //package deprecated struct Exception {}
2059 
2060 
2061 /++
2062 	Base class representing my exceptions. You should almost never work with this directly, but you might catch it as a generic thing. Catch it before generic `object.Exception` or `object.Throwable` in any catch chains.
2063 
2064 
2065 	$(H3 General guidelines for exceptions)
2066 
2067 	The purpose of an exception is to cancel a task that has proven to be impossible and give the programmer enough information to use at a higher level to decide what to do about it.
2068 
2069 	Cancelling a task is accomplished with the `throw` keyword. The transmission of information to a higher level is done by the language runtime. The decision point is marked by the `catch` keyword. The part missing - the job of the `Exception` class you construct and throw - is to gather the information that will be useful at a later decision point.
2070 
2071 	It is thus important that you gather as much useful information as possible and keep it in a way that the code catching the exception can still interpret it when constructing an exception. Other concerns are secondary to this to this primary goal.
2072 
2073 	With this in mind, here's some guidelines for exception handling in arsd code.
2074 
2075 	$(H4 Allocations and lifetimes)
2076 
2077 	Don't get clever with exception allocations. You don't know what the catcher is going to do with an exception and you don't want the error handling scheme to introduce its own tricky bugs. Remember, an exception object's first job is to deliver useful information up the call chain in a way this code can use it. You don't know what this code is or what it is going to do.
2078 
2079 	Keep your memory management schemes simple and let the garbage collector do its job.
2080 
2081 	$(LIST
2082 		* All thrown exceptions should be allocated with the `new` keyword.
2083 
2084 		* Members inside the exception should be value types or have infinite lifetime (that is, be GC managed).
2085 
2086 		* While this document is concerned with throwing, you might want to add additional information to an in-flight exception, and this is done by catching, so you need to know how that works too, and there is a global compiler switch that can change things, so even inside arsd we can't completely avoid its implications.
2087 
2088 		DIP1008's presence complicates things a bit on the catch side - if you catch an exception and return it from a function, remember to `ex.refcount = ex.refcount + 1;` so you don't introduce more use-after-free woes for those unfortunate souls.
2089 	)
2090 
2091 	$(H4 Error strings)
2092 
2093 	Strings can deliver useful information to people reading the message, but are often suboptimal for delivering useful information to other chunks of code. Remember, an exception's first job is to be caught by another block of code. Printing to users is a last resort; even if you want a user-readable error message, an exception is not the ideal way to deliver one since it is constructed in the guts of a failed task, without the higher level context of what the user was actually trying to do. User error messages ought to be made from information in the exception, combined with higher level knowledge. This is best done in a `catch` block, not a `throw` statement.
2094 
2095 	As such, I recommend that you:
2096 
2097 	$(LIST
2098 		* Don't concatenate error strings at the throw site. Instead, pass the data you would have used to build the string as actual data to the constructor. This lets catchers see the original data without having to try to extract it from a string. For unique data, you will likely need a unique exception type. More on this in the next section.
2099 
2100 		* Don't construct error strings in a constructor either, for the same reason. Pass the useful data up the call chain, as exception members, to the maximum extent possible. Exception: if you are passed some data with a temporary lifetime that is important enough to pass up the chain. You may `.idup` or `to!string` to preserve as much data as you can before it is lost, but still store it in a separate member of the Exception subclass object.
2101 
2102 		* $(I Do) construct strings out of public members in [getAdditionalPrintableInformation]. When this is called, the user has requested as much relevant information as reasonable in string format. Still, avoid concatenation - it lets you pass as many key/value pairs as you like to the caller. They can concatenate as needed. However, note the words "public members" - everything you do in `getAdditionalPrintableInformation` ought to also be possible for code that caught your exception via your public methods and properties.
2103 	)
2104 
2105 	$(H4 Subclasses)
2106 
2107 	Any exception with unique data types should be a unique class. Whenever practical, this should be one you write and document at the top-level of a module. But I know we get lazy - me too - and this is why in standard D we'd often fall back to `throw new Exception("some string " ~ some info)`. To help resist these urges, I offer some helper functions to use instead that better achieve the key goal of exceptions - passing structured data up a call chain - while still being convenient to write.
2108 
2109 	See: [ArsdException], [Win32Enforce]
2110 
2111 +/
2112 class ArsdExceptionBase : object.Exception {
2113 	/++
2114 		Don't call this except from other exceptions; this is essentially an abstract class.
2115 
2116 		Params:
2117 			operation = the specific operation that failed, throwing the exception
2118 	+/
2119 	package this(string operation, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
2120 		super(operation, file, line, next);
2121 	}
2122 
2123 	/++
2124 		The toString method will print out several components:
2125 
2126 		$(LIST
2127 			* The file, line, static message, and object class name from the constructor. You can access these independently with the members `file`, `line`, `msg`, and [printableExceptionName].
2128 			* The generic category codes stored with this exception
2129 			* Additional members stored with the exception child classes (e.g. platform error codes, associated function arguments)
2130 			* The stack trace associated with the exception. You can access these lines independently with `foreach` over the `info` member.
2131 		)
2132 
2133 		This is meant to be read by the developer, not end users. You should wrap your user-relevant tasks in a try/catch block and construct more appropriate error messages from context available there, using the individual properties of the exception to add richness.
2134 	+/
2135 	final override void toString(scope void delegate(in char[]) sink) const {
2136 		// class name and info from constructor
2137 		sink(printableExceptionName);
2138 		sink("@");
2139 		sink(file);
2140 		sink("(");
2141 		char[16] buffer;
2142 		sink(intToString(line, buffer[]));
2143 		sink("): ");
2144 		sink(message);
2145 
2146 		getAdditionalPrintableInformation((string name, in char[] value) {
2147 			sink("\n");
2148 			sink(name);
2149 			sink(": ");
2150 			sink(value);
2151 		});
2152 
2153 		// full stack trace
2154 		sink("\n----------------\n");
2155 		foreach(str; info) {
2156 			sink(str);
2157 			sink("\n");
2158 		}
2159 	}
2160 	/// ditto
2161 	final override string toString() {
2162 		string s;
2163 		toString((in char[] chunk) { s ~= chunk; });
2164 		return s;
2165 	}
2166 
2167 	/++
2168 		Users might like to see additional information with the exception. API consumers should pull this out of properties on your child class, but the parent class might not be able to deal with the arbitrary types at runtime the children can introduce, so bringing them all down to strings simplifies that.
2169 
2170 		Overrides should always call `super.getAdditionalPrintableInformation(sink);` before adding additional information by calling the sink with other arguments afterward.
2171 
2172 		You should spare no expense in preparing this information - translate error codes, build rich strings, whatever it takes - to make the information here useful to the reader.
2173 	+/
2174 	void getAdditionalPrintableInformation(scope void delegate(string name, in char[] value) sink) const {
2175 
2176 	}
2177 
2178 	/++
2179 		This is the name of the exception class, suitable for printing. This should be static data (e.g. a string literal). Override it in subclasses.
2180 	+/
2181 	string printableExceptionName() const {
2182 		return typeid(this).name;
2183 	}
2184 
2185 	/// deliberately hiding `Throwable.msg`. Use [message] and [toString] instead.
2186 	@disable final void msg() {}
2187 
2188 	override const(char)[] message() const {
2189 		return super.msg;
2190 	}
2191 }
2192 
2193 /++
2194 
2195 +/
2196 class InvalidArgumentsException : ArsdExceptionBase {
2197 	static struct InvalidArgument {
2198 		string name;
2199 		string description;
2200 		LimitedVariant givenValue;
2201 	}
2202 
2203 	InvalidArgument[] invalidArguments;
2204 
2205 	this(InvalidArgument[] invalidArguments, string functionName = __PRETTY_FUNCTION__, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
2206 		this.invalidArguments = invalidArguments;
2207 		super(functionName, file, line, next);
2208 	}
2209 
2210 	this(string argumentName, string argumentDescription, LimitedVariant givenArgumentValue = LimitedVariant.init, string functionName = __PRETTY_FUNCTION__, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
2211 		this([
2212 			InvalidArgument(argumentName, argumentDescription, givenArgumentValue)
2213 		], functionName, file, line, next);
2214 	}
2215 
2216 	this(string argumentName, string argumentDescription, string functionName = __PRETTY_FUNCTION__, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
2217 		this(argumentName, argumentDescription, LimitedVariant.init, functionName, file, line, next);
2218 	}
2219 
2220 	override void getAdditionalPrintableInformation(scope void delegate(string name, in char[] value) sink) const {
2221 		// FIXME: print the details better
2222 		foreach(arg; invalidArguments)
2223 			sink(arg.name, arg.givenValue.toString ~ " - " ~ arg.description);
2224 	}
2225 }
2226 
2227 /++
2228 	Base class for when you've requested a feature that is not available. It may not be available because it is possible, but not yet implemented, or it might be because it is impossible on your operating system.
2229 +/
2230 class FeatureUnavailableException : ArsdExceptionBase {
2231 	this(string featureName = __PRETTY_FUNCTION__, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
2232 		super(featureName, file, line, next);
2233 	}
2234 }
2235 
2236 /++
2237 	This means the feature could be done, but I haven't gotten around to implementing it yet. If you email me, I might be able to add it somewhat quickly and get back to you.
2238 +/
2239 class NotYetImplementedException : FeatureUnavailableException {
2240 	this(string featureName = __PRETTY_FUNCTION__, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
2241 		super(featureName, file, line, next);
2242 	}
2243 
2244 }
2245 
2246 /++
2247 	This means the feature is not supported by your current operating system. You might be able to get it in an update, but you might just have to find an alternate way of doing things.
2248 +/
2249 class NotSupportedException : FeatureUnavailableException {
2250 	this(string featureName, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
2251 		super(featureName, file, line, next);
2252 	}
2253 }
2254 
2255 /++
2256 	This is a generic exception with attached arguments. It is used when I had to throw something but didn't want to write a new class.
2257 
2258 	You can catch an ArsdException to get its passed arguments out.
2259 
2260 	You can pass either a base class or a string as `Type`.
2261 
2262 	See the examples for how to use it.
2263 +/
2264 template ArsdException(alias Type, DataTuple...) {
2265 	static if(DataTuple.length)
2266 		alias Parent = ArsdException!(Type, DataTuple[0 .. $-1]);
2267 	else
2268 		alias Parent = ArsdExceptionBase;
2269 
2270 	class ArsdException : Parent {
2271 		DataTuple data;
2272 
2273 		this(DataTuple data, string file = __FILE__, size_t line = __LINE__) {
2274 			this.data = data;
2275 			static if(is(Parent == ArsdExceptionBase))
2276 				super(null, file, line);
2277 			else
2278 				super(data[0 .. $-1], file, line);
2279 		}
2280 
2281 		static opCall(R...)(R r, string file = __FILE__, size_t line = __LINE__) {
2282 			return new ArsdException!(Type, DataTuple, R)(r, file, line);
2283 		}
2284 
2285 		override string printableExceptionName() const {
2286 			static if(DataTuple.length)
2287 				enum str = "ArsdException!(" ~ Type.stringof ~ ", " ~ DataTuple.stringof[1 .. $-1] ~ ")";
2288 			else
2289 				enum str = "ArsdException!" ~ Type.stringof;
2290 			return str;
2291 		}
2292 
2293 		override void getAdditionalPrintableInformation(scope void delegate(string name, in char[] value) sink) const {
2294 			ArsdExceptionBase.getAdditionalPrintableInformation(sink);
2295 
2296 			foreach(idx, datum; data) {
2297 				enum int lol = cast(int) idx;
2298 				enum key = "[" ~ lol.stringof ~ "] " ~ DataTuple[idx].stringof;
2299 				sink(key, toStringInternal(datum));
2300 			}
2301 		}
2302 	}
2303 }
2304 
2305 /// This example shows how you can throw and catch the ad-hoc exception types.
2306 unittest {
2307 	// you can throw and catch by matching the string and argument types
2308 	try {
2309 		// throw it with parenthesis after the template args (it uses opCall to construct)
2310 		throw ArsdException!"Test"();
2311 		// you could also `throw new ArsdException!"test";`, but that gets harder with args
2312 		// as we'll see in the following example
2313 		assert(0); // remove from docs
2314 	} catch(ArsdException!"Test" e) { // catch it without them
2315 		// this has no useful information except for the type
2316 		// but you can catch it like this and it is still more than generic Exception
2317 	}
2318 
2319 	// an exception's job is to deliver useful information up the chain
2320 	// and you can do that easily by passing arguments:
2321 
2322 	try {
2323 		throw ArsdException!"Test"(4, "four");
2324 		// you could also `throw new ArsdException!("Test", int, string)(4, "four")`
2325 		// but now you start to see how the opCall convenience constructor simplifies things
2326 		assert(0); // remove from docs
2327 	} catch(ArsdException!("Test", int, string) e) { // catch it and use info by specifying types
2328 		assert(e.data[0] == 4); // and extract arguments like this
2329 		assert(e.data[1] == "four");
2330 	}
2331 
2332 	// a throw site can add additional information without breaking code that catches just some
2333 	// generally speaking, each additional argument creates a new subclass on top of the previous args
2334 	// so you can cast
2335 
2336 	try {
2337 		throw ArsdException!"Test"(4, "four", 9);
2338 		assert(0); // remove from docs
2339 	} catch(ArsdException!("Test", int, string) e) { // this catch still works
2340 		assert(e.data[0] == 4);
2341 		assert(e.data[1] == "four");
2342 		// but if you were to print it, all the members would be there
2343 		// import std.stdio; writeln(e); // would show something like:
2344 		/+
2345 			ArsdException!("Test", int, string, int)@file.d(line):
2346 			[0] int: 4
2347 			[1] string: four
2348 			[2] int: 9
2349 		+/
2350 		// indicating that there's additional information available if you wanted to process it
2351 
2352 		// and meanwhile:
2353 		ArsdException!("Test", int) e2 = e; // this implicit cast works thanks to the parent-child relationship
2354 		ArsdException!"Test" e3 = e; // this works too, the base type/string still matches
2355 
2356 		// so catching those types would work too
2357 	}
2358 }
2359 
2360 /++
2361 	A tagged union that holds an error code from system apis, meaning one from Windows GetLastError() or C's errno.
2362 
2363 	You construct it with `SystemErrorCode(thing)` and the overloaded constructor tags and stores it.
2364 +/
2365 struct SystemErrorCode {
2366 	///
2367 	enum Type {
2368 		errno, ///
2369 		win32 ///
2370 	}
2371 
2372 	const Type type; ///
2373 	const int code; /// You should technically cast it back to DWORD if it is a win32 code
2374 
2375 	/++
2376 		C/unix error are typed as signed ints...
2377 		Windows' errors are typed DWORD, aka unsigned...
2378 
2379 		so just passing them straight up will pick the right overload here to set the tag.
2380 	+/
2381 	this(int errno) {
2382 		this.type = Type.errno;
2383 		this.code = errno;
2384 	}
2385 
2386 	/// ditto
2387 	this(uint win32) {
2388 		this.type = Type.win32;
2389 		this.code = win32;
2390 	}
2391 
2392 	/++
2393 		Returns if the code indicated success.
2394 
2395 		Please note that many calls do not actually set a code to success, but rather just don't touch it. Thus this may only be true on `init`.
2396 	+/
2397 	bool wasSuccessful() const {
2398 		final switch(type) {
2399 			case Type.errno:
2400 				return this.code == 0;
2401 			case Type.win32:
2402 				return this.code == 0;
2403 		}
2404 	}
2405 
2406 	/++
2407 		Constructs a string containing both the code and the explanation string.
2408 	+/
2409 	string toString() const {
2410 		return "[" ~ codeAsString ~ "] " ~ errorString;
2411 	}
2412 
2413 	/++
2414 		The numeric code itself as a string.
2415 
2416 		See [errorString] for a text explanation of the code.
2417 	+/
2418 	string codeAsString() const {
2419 		char[16] buffer;
2420 		final switch(type) {
2421 			case Type.errno:
2422 				return intToString(code, buffer[]).idup;
2423 			case Type.win32:
2424 				buffer[0 .. 2] = "0x";
2425 				return buffer[0 .. 2 + intToString(cast(uint) code, buffer[2 .. $], IntToStringArgs().withRadix(16).withPadding(8)).length].idup;
2426 		}
2427 	}
2428 
2429 	/++
2430 		A text explanation of the code. See [codeAsString] for a string representation of the numeric representation.
2431 	+/
2432 	string errorString() const @trusted {
2433 		final switch(type) {
2434 			case Type.errno:
2435 				import core.stdc.string;
2436 				auto strptr = strerror(code);
2437 				auto orig = strptr;
2438 				int len;
2439 				while(*strptr++) {
2440 					len++;
2441 				}
2442 
2443 				return orig[0 .. len].idup;
2444 			case Type.win32:
2445 				version(Windows) {
2446 					wchar[256] buffer;
2447 					auto size = FormatMessageW(
2448 						FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
2449 						null,
2450 						code,
2451 						MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
2452 						buffer.ptr,
2453 						buffer.length,
2454 						null
2455 					);
2456 
2457 					return makeUtf8StringFromWindowsString(buffer[0 .. size]).stripInternal;
2458 				} else {
2459 					return null;
2460 				}
2461 		}
2462 	}
2463 }
2464 
2465 /++
2466 
2467 +/
2468 struct SavedArgument {
2469 	string name;
2470 	LimitedVariant value;
2471 }
2472 
2473 /++
2474 
2475 +/
2476 class SystemApiException : ArsdExceptionBase {
2477 	this(string msg, int originalErrorNo, scope SavedArgument[] args = null, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
2478 		this(msg, SystemErrorCode(originalErrorNo), args, file, line, next);
2479 	}
2480 
2481 	version(Windows)
2482 	this(string msg, DWORD windowsError, scope SavedArgument[] args = null, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
2483 		this(msg, SystemErrorCode(windowsError), args, file, line, next);
2484 	}
2485 
2486 	this(string msg, SystemErrorCode code, SavedArgument[] args = null, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
2487 		this.errorCode = code;
2488 
2489 		// discard stuff that won't fit
2490 		if(args.length > this.args.length)
2491 			args = args[0 .. this.args.length];
2492 
2493 		this.args[0 .. args.length] = args[];
2494 
2495 		super(msg, file, line, next);
2496 	}
2497 
2498 	/++
2499 
2500 	+/
2501 	const SystemErrorCode errorCode;
2502 
2503 	/++
2504 
2505 	+/
2506 	const SavedArgument[8] args;
2507 
2508 	override void getAdditionalPrintableInformation(scope void delegate(string name, in char[] value) sink) const {
2509 		super.getAdditionalPrintableInformation(sink);
2510 		sink("Error code", errorCode.toString());
2511 
2512 		foreach(arg; args)
2513 			if(arg.name !is null)
2514 				sink(arg.name, arg.value.toString());
2515 	}
2516 
2517 }
2518 
2519 /++
2520 	The low level use of this would look like `throw new WindowsApiException("MsgWaitForMultipleObjectsEx", GetLastError())` but it is meant to be used from higher level things like [Win32Enforce].
2521 
2522 	History:
2523 		Moved from simpledisplay.d to core.d in March 2023 (dub v11.0).
2524 +/
2525 alias WindowsApiException = SystemApiException;
2526 
2527 /++
2528 	History:
2529 		Moved from simpledisplay.d to core.d in March 2023 (dub v11.0).
2530 +/
2531 alias ErrnoApiException = SystemApiException;
2532 
2533 /++
2534 	Calls the C API function `fn`. If it returns an error value, it throws an [ErrnoApiException] (or subclass) after getting `errno`.
2535 +/
2536 template ErrnoEnforce(alias fn, alias errorValue = void) {
2537 	static if(is(typeof(fn) Return == return))
2538 	static if(is(typeof(fn) Params == __parameters)) {
2539 		static if(is(errorValue == void)) {
2540 			static if(is(typeof(null) : Return))
2541 				enum errorValueToUse = null;
2542 			else static if(is(Return : long))
2543 				enum errorValueToUse = -1;
2544 			else
2545 				static assert(0, "Please pass the error value");
2546 		} else {
2547 			enum errorValueToUse = errorValue;
2548 		}
2549 
2550 		Return ErrnoEnforce(Params params, ArgSentinel sentinel = ArgSentinel.init, string file = __FILE__, size_t line = __LINE__) {
2551 			import core.stdc.errno;
2552 
2553 			Return value = fn(params);
2554 
2555 			if(value == errorValueToUse) {
2556 				SavedArgument[] args; // FIXME
2557 				/+
2558 				static foreach(idx; 0 .. Params.length)
2559 					args ~= SavedArgument(
2560 						__traits(identifier, Params[idx .. idx + 1]),
2561 						params[idx]
2562 					);
2563 				+/
2564 				throw new ErrnoApiException(__traits(identifier, fn), errno, args, file, line);
2565 			}
2566 
2567 			return value;
2568 		}
2569 	}
2570 }
2571 
2572 version(Windows) {
2573 	/++
2574 		Calls the Windows API function `fn`. If it returns an error value, it throws a [WindowsApiException] (or subclass) after calling `GetLastError()`.
2575 	+/
2576 	template Win32Enforce(alias fn, alias errorValue = void) {
2577 		static if(is(typeof(fn) Return == return))
2578 		static if(is(typeof(fn) Params == __parameters)) {
2579 			static if(is(errorValue == void)) {
2580 				static if(is(Return == BOOL))
2581 					enum errorValueToUse = false;
2582 				else static if(is(Return : HANDLE))
2583 					enum errorValueToUse = NULL;
2584 				else static if(is(Return == DWORD))
2585 					enum errorValueToUse = cast(DWORD) 0xffffffff;
2586 				else
2587 					static assert(0, "Please pass the error value");
2588 			} else {
2589 				enum errorValueToUse = errorValue;
2590 			}
2591 
2592 			Return Win32Enforce(Params params, ArgSentinel sentinel = ArgSentinel.init, string file = __FILE__, size_t line = __LINE__) {
2593 				Return value = fn(params);
2594 
2595 				if(value == errorValueToUse) {
2596 					auto error = GetLastError();
2597 					SavedArgument[] args; // FIXME
2598 					throw new WindowsApiException(__traits(identifier, fn), error, args, file, line);
2599 				}
2600 
2601 				return value;
2602 			}
2603 		}
2604 	}
2605 
2606 }
2607 
2608 /+
2609 	===============
2610 	EVENT LOOP CORE
2611 	===============
2612 +/
2613 
2614 /+
2615 	UI threads
2616 		need to get window messages in addition to all the other jobs
2617 	I/O Worker threads
2618 		need to get commands for read/writes, run them, and send the reply back. not necessary on Windows
2619 		if interrupted, check cancel flags.
2620 	CPU Worker threads
2621 		gets functions, runs them, send reply back. should send a cancel flag to periodically check
2622 	Task worker threads
2623 		runs fibers and multiplexes them
2624 
2625 
2626 	General procedure:
2627 		issue the read/write command
2628 		if it would block on linux, epoll associate it. otherwise do the callback immediately
2629 
2630 		callbacks have default affinity to the current thread, meaning their callbacks always run here
2631 		accepts can usually be dispatched to any available thread tho
2632 
2633 	//  In other words, a single thread can be associated with, at most, one I/O completion port.
2634 
2635 	Realistically, IOCP only used if there is no thread affinity. If there is, just do overlapped w/ sleepex.
2636 
2637 
2638 	case study: http server
2639 
2640 	1) main thread starts the server. it does an accept loop with no thread affinity. the main thread does NOT check the global queue (the iocp/global epoll)
2641 	2) connections come in and are assigned to first available thread via the iocp/global epoll
2642 	3) these run local event loops until the connection task is finished
2643 
2644 	EVENT LOOP TYPES:
2645 		1) main ui thread - MsgWaitForMultipleObjectsEx / epoll on the local ui. it does NOT check the any worker thread thing!
2646 			The main ui thread should never terminate until the program is ready to close.
2647 			You can have additional ui threads in theory but im not really gonna support that in full; most things will assume there is just the one. simpledisplay's gui thread is the primary if it exists. (and sdpy will prolly continue to be threaded the way it is now)
2648 
2649 			The biggest complication is the TerminalDirectToEmulator, where the primary ui thread is NOT the thread that runs `main`
2650 		2) worker thread GetQueuedCompletionStatusEx / epoll on the local thread fd and the global epoll fd
2651 		3) local event loop - check local things only. SleepEx / epoll on local thread fd. This more of a compatibility hack for `waitForCompletion` outside a fiber.
2652 
2653 		i'll use:
2654 			* QueueUserAPC to send interruptions to a worker thread
2655 			* PostQueuedCompletionStatus is to send interruptions to any available thread.
2656 			* PostMessage to a window
2657 			* ??? to a fiber task
2658 
2659 		I also need a way to de-duplicate events in the queue so if you try to push the same thing it won't trigger multiple times.... I might want to keep a duplicate of the thing... really, what I'd do is post the "event wake up" message and keep the queue in my own thing. (WM_PAINT auto-coalesces)
2660 
2661 		Destructors need to be able to post messages back to a specific task to queue thread-affinity cleanup. This must be GC safe.
2662 
2663 		A task might want to wait on certain events. If the task is a fiber, it yields and gets called upon the event. If the task is a thread, it really has to call the event loop... which can be a loop of loops we want to avoid. `waitForCompletion` is more often gonna be used just to run the loop at top level tho... it might not even check for the global info availability so it'd run the local thing only.
2664 
2665 		APCs should not themselves enter an alterable wait cuz it can stack overflow. So generally speaking, they should avoid calling fibers or other event loops.
2666 +/
2667 
2668 /++
2669 	You can also pass a handle to a specific thread, if you have one.
2670 +/
2671 enum ThreadToRunIn {
2672 	/++
2673 		The callback should be only run by the same thread that set it.
2674 	+/
2675 	CurrentThread,
2676 	/++
2677 		The UI thread is a special one - it is the supervisor of the workers and the controller of gui and console handles. It is the first thread to call [arsd_core_init] actively running an event loop unless there is a thread that has actively asserted the ui supervisor role. FIXME is this true after i implemen it?
2678 
2679 		A ui thread should be always quickly responsive to new events.
2680 
2681 		There should only be one main ui thread, in which simpledisplay and minigui can be used.
2682 
2683 		Other threads can run like ui threads, but are considered temporary and only concerned with their own needs (it is the default style of loop
2684 		for an undeclared thread but will not receive messages from other threads unless there is no other option)
2685 
2686 
2687 		Ad-Hoc thread - something running an event loop that isn't another thing
2688 		Controller thread - running an explicit event loop instance set as not a task runner or blocking worker
2689 		UI thread - simpledisplay's event loop, which it will require remain live for the duration of the program (running two .eventLoops without a parent EventLoop instance will become illegal, throwing at runtime if it happens telling people to change their code)
2690 
2691 		Windows HANDLES will always be listened on the thread itself that is requesting, UNLESS it is a worker/helper thread, in which case it goes to a coordinator thread. since it prolly can't rely on the parent per se this will have to be one created by arsd core init, UNLESS the parent is inside an explicit EventLoop structure.
2692 
2693 		All use the MsgWaitForMultipleObjectsEx pattern
2694 
2695 
2696 	+/
2697 	UiThread,
2698 	/++
2699 		The callback can be called from any available worker thread. It will be added to a global queue and the first thread to see it will run it.
2700 
2701 		These will not run on the UI thread unless there is no other option on the platform (and all platforms this lib supports have other options).
2702 
2703 		These are expected to run cooperatively multitasked things; functions that frequently yield as they wait on other tasks. Think a fiber.
2704 
2705 		A task runner should be generally responsive to new events.
2706 	+/
2707 	AnyAvailableTaskRunnerThread,
2708 	/++
2709 		These are expected to run longer blocking, but independent operations. Think an individual function with no context.
2710 
2711 		A blocking worker can wait hundreds of milliseconds between checking for new events.
2712 	+/
2713 	AnyAvailableBlockingWorkerThread,
2714 	/++
2715 		The callback will be duplicated across all threads known to the arsd.core event loop.
2716 
2717 		It adds it to an immutable queue that each thread will go through... might just replace with an exit() function.
2718 
2719 
2720 		so to cancel all associated tasks for like a web server, it could just have the tasks atomicAdd to a counter and subtract when they are finished. Then you have a single semaphore you signal the number of times you have an active thing and wait for them to acknowledge it.
2721 
2722 		threads should report when they start running the loop and they really should report when they terminate but that isn't reliable
2723 
2724 
2725 		hmmm what if: all user-created threads (the public api) count as ui threads. only ones created in here are task runners or helpers. ui threads can wait on a global event to exit.
2726 
2727 		there's still prolly be one "the" ui thread, which does the handle listening on windows and is the one sdpy wants.
2728 	+/
2729 	BroadcastToAllThreads,
2730 }
2731 
2732 /++
2733 	Initializes the arsd core event loop and creates its worker threads. You don't actually have to call this, since the first use of an arsd.core function that requires it will call it implicitly, but calling it yourself gives you a chance to control the configuration more explicitly if you want to.
2734 +/
2735 void arsd_core_init(int numberOfWorkers = 0) {
2736 
2737 }
2738 
2739 version(Windows)
2740 class WindowsHandleReader_ex {
2741 	// Windows handles are always dispatched to the main ui thread, which can then send a command back to a worker thread to run the callback if needed
2742 	this(HANDLE handle) {}
2743 }
2744 
2745 version(Posix)
2746 class PosixFdReader_ex {
2747 	// posix readers can just register with whatever instance we want to handle the callback
2748 }
2749 
2750 /++
2751 
2752 +/
2753 interface ICoreEventLoop {
2754 	/++
2755 		Runs the event loop for this thread until the `until` delegate returns `true`.
2756 	+/
2757 	final void run(scope bool delegate() until) {
2758 		while(!exitApplicationRequested && !until()) {
2759 			runOnce();
2760 		}
2761 	}
2762 
2763 	private __gshared bool exitApplicationRequested;
2764 
2765 	final static void exitApplication() {
2766 		exitApplicationRequested = true;
2767 		// FIXME: wake up all the threads
2768 	}
2769 
2770 	/++
2771 		Returns details from a call to [runOnce]. Use the named methods here for details, or it can be used in a `while` loop directly thanks to its `opCast` automatic conversion to `bool`.
2772 
2773 		History:
2774 			Added December 28, 2023
2775 	+/
2776 	static struct RunOnceResult {
2777 		enum Possibilities {
2778 			CarryOn,
2779 			LocalExit,
2780 			GlobalExit,
2781 			Interrupted
2782 
2783 		}
2784 		Possibilities result;
2785 
2786 		/++
2787 			Returns `true` if the event loop should generally continue.
2788 
2789 			Might be false if the local loop was exited or if the application is supposed to exit. If this is `false`, check [applicationExitRequested] to determine if you should move on to other work or start your final cleanup process.
2790 		+/
2791 		bool shouldContinue() const {
2792 			return result == Possibilities.CarryOn;
2793 		}
2794 
2795 		/++
2796 			Returns `true` if [ICoreEventLoop.exitApplication] was called during this event, or if the user or operating system has requested the application exit.
2797 
2798 			Details might be available through other means.
2799 		+/
2800 		bool applicationExitRequested() const {
2801 			return result == Possibilities.GlobalExit;
2802 		}
2803 
2804 		/++
2805 			Returns [shouldContinue] when used in a context for an implicit bool (e.g. `if` statements).
2806 		+/
2807 		bool opCast(T : bool)() const {
2808 			reutrn shouldContinue();
2809 		}
2810 	}
2811 
2812 	/++
2813 		Runs a single iteration of the event loop for this thread. It will return when the first thing happens, but that thing might be totally uninteresting to anyone, or it might trigger significant work you'll wait on.
2814 
2815 		Note that running this externally instead of `run` gives only the $(I illusion) of control. You're actually better off setting a recurring timer if you need things to run on a clock tick, or a single-shot timer for a one time event. They're more likely to be called on schedule inside this function than outside it.
2816 
2817 		Parameters:
2818 			timeout = a timeout value for an idle loop. There is no guarantee you won't return earlier or later than this; the function might run longer than the timeout if it has work to do. Pass `Duration.max` (the default) for an infinite duration timeout (but remember, once it finds work to do, including a false-positive wakeup or interruption by the operating system, it will return early anyway).
2819 
2820 		History:
2821 			Prior to December 28, 2023, it returned `void` and took no arguments. This change is breaking, but since the entire module is documented as unstable, it was permitted to happen as that document provided prior notice.
2822 	+/
2823 	RunOnceResult runOnce(Duration timeout = Duration.max);
2824 
2825 	/++
2826 		Adds a delegate to be called on each loop iteration, called based on the `timingFlags`.
2827 
2828 
2829 		The order in which the delegates are called is undefined and may change with each iteration of the loop. Additionally, when and how many times a loop iterates is undefined; multiple events might be handled by each iteration, or sometimes, nothing will be handled and it woke up spuriously. Your delegates need to be ok with all of this.
2830 
2831 		Parameters:
2832 			dg = the delegate to call
2833 			timingFlags =
2834 				0: never actually run the function; it can assert error if you pass this
2835 				1: run before each loop OS wait call
2836 				2: run after each loop OS wait call
2837 				3: run both before and after each OS wait call
2838 				4: single shot?
2839 				8: no-coalesce? (if after was just run, it will skip the before loops unless this flag is set)
2840 
2841 	+/
2842 	void addDelegateOnLoopIteration(void delegate() dg, uint timingFlags);
2843 
2844 	final void addDelegateOnLoopIteration(void function() dg, uint timingFlags) {
2845 		addDelegateOnLoopIteration(toDelegate(dg), timingFlags);
2846 	}
2847 
2848 	// to send messages between threads, i'll queue up a function that just call dispatchMessage. can embed the arg inside the callback helper prolly.
2849 	// tho i might prefer to actually do messages w/ run payloads so it is easier to deduplicate i can still dedupe by insepcting the call args so idk
2850 
2851 	version(Posix) {
2852 		@mustuse
2853 		static struct UnregisterToken {
2854 			private CoreEventLoopImplementation impl;
2855 			private int fd;
2856 			private CallbackHelper cb;
2857 
2858 			/++
2859 				Unregisters the file descriptor from the event loop and releases the reference to the callback held by the event loop (which will probably free it).
2860 
2861 				You must call this when you're done. Normally, this will be right before you close the fd (Which is often after the other side closes it, meaning you got a 0 length read).
2862 			+/
2863 			void unregister() {
2864 				assert(impl !is null, "Cannot reuse unregister token");
2865 
2866 				version(Arsd_core_epoll) {
2867 					impl.unregisterFd(fd);
2868 				} else version(Arsd_core_kqueue) {
2869 					// intentionally blank - all registrations are one-shot there
2870 					// FIXME: actually it might not have gone off yet, in that case we do need to delete the filter
2871 				} else version(EmptyCoreEvent) {
2872 
2873 				}
2874 				else static assert(0);
2875 
2876 				cb.release();
2877 				this = typeof(this).init;
2878 			}
2879 		}
2880 
2881 		@mustuse
2882 		static struct RearmToken {
2883 			private bool readable;
2884 			private CoreEventLoopImplementation impl;
2885 			private int fd;
2886 			private CallbackHelper cb;
2887 			private uint flags;
2888 
2889 			/++
2890 				Calls [UnregisterToken.unregister]
2891 			+/
2892 			void unregister() {
2893 				assert(impl !is null, "cannot reuse rearm token after unregistering it");
2894 
2895 				version(Arsd_core_epoll) {
2896 					impl.unregisterFd(fd);
2897 				} else version(Arsd_core_kqueue) {
2898 					// intentionally blank - all registrations are one-shot there
2899 					// FIXME: actually it might not have gone off yet, in that case we do need to delete the filter
2900 				} else version(EmptyCoreEvent) {
2901 
2902 				} else static assert(0);
2903 
2904 				cb.release();
2905 				this = typeof(this).init;
2906 			}
2907 
2908 			/++
2909 				Rearms the event so you will get another callback next time it is ready.
2910 			+/
2911 			void rearm() {
2912 				assert(impl !is null, "cannot reuse rearm token after unregistering it");
2913 				impl.rearmFd(this);
2914 			}
2915 		}
2916 
2917 		UnregisterToken addCallbackOnFdReadable(int fd, CallbackHelper cb);
2918 		RearmToken addCallbackOnFdReadableOneShot(int fd, CallbackHelper cb);
2919 		RearmToken addCallbackOnFdWritableOneShot(int fd, CallbackHelper cb);
2920 	}
2921 
2922 	version(Windows) {
2923 		@mustuse
2924 		static struct UnregisterToken {
2925 			private CoreEventLoopImplementation impl;
2926 			private HANDLE handle;
2927 			private CallbackHelper cb;
2928 
2929 			/++
2930 				Unregisters the handle from the event loop and releases the reference to the callback held by the event loop (which will probably free it).
2931 
2932 				You must call this when you're done. Normally, this will be right before you close the handle.
2933 			+/
2934 			void unregister() {
2935 				assert(impl !is null, "Cannot reuse unregister token");
2936 
2937 				impl.unregisterHandle(handle, cb);
2938 
2939 				cb.release();
2940 				this = typeof(this).init;
2941 			}
2942 		}
2943 
2944 		UnregisterToken addCallbackOnHandleReady(HANDLE handle, CallbackHelper cb);
2945 	}
2946 }
2947 
2948 /++
2949 	Get the event loop associated with this thread
2950 +/
2951 ICoreEventLoop getThisThreadEventLoop(EventLoopType type = EventLoopType.AdHoc) {
2952 	static ICoreEventLoop loop;
2953 	if(loop is null)
2954 		loop = new CoreEventLoopImplementation();
2955 	return loop;
2956 }
2957 
2958 /++
2959 	The internal types that will be exposed through other api things.
2960 +/
2961 package(arsd) enum EventLoopType {
2962 	/++
2963 		The event loop is being run temporarily and the thread doesn't promise to keep running it.
2964 	+/
2965 	AdHoc,
2966 	/++
2967 		The event loop struct has been instantiated at top level. Its destructor will run when the
2968 		function exits, which is only at the end of the entire block of work it is responsible for.
2969 
2970 		It must be in scope for the whole time the arsd event loop functions are expected to be used
2971 		(meaning it should generally be top-level in `main`)
2972 	+/
2973 	Explicit,
2974 	/++
2975 		A specialization of `Explicit`, so all the same rules apply there, but this is specifically the event loop coming from simpledisplay or minigui. It will run for the duration of the UI's existence.
2976 	+/
2977 	Ui,
2978 	/++
2979 		A special event loop specifically for threads that listen to the task runner queue and handle I/O events from running tasks. Typically, a task runner runs cooperatively multitasked coroutines (so they prefer not to block the whole thread).
2980 	+/
2981 	TaskRunner,
2982 	/++
2983 		A special event loop specifically for threads that listen to the helper function request queue. Helper functions are expected to run independently for a somewhat long time (them blocking the thread for some time is normal) and send a reply message back to the requester.
2984 	+/
2985 	HelperWorker
2986 }
2987 
2988 /+
2989 	Tasks are given an object to talk to their parent... can be a dialog where it is like
2990 
2991 	sendBuffer
2992 	waitForWordToProceed
2993 
2994 	in a loop
2995 
2996 
2997 	Tasks are assigned to a worker thread and may share it with other tasks.
2998 +/
2999 
3000 /+
3001 private ThreadLocalGcRoots gcRoots;
3002 
3003 private struct ThreadLocalGcRoots {
3004 	// it actually would be kinda cool if i could tell the GC
3005 	// that only part of this array is actually used so it can skip
3006 	// scanning the rest. but meh.
3007 	const(void)*[] roots;
3008 
3009 	void* add(const(void)* what) {
3010 		roots ~= what;
3011 		return &roots[$-1];
3012 	}
3013 }
3014 +/
3015 
3016 // the GC may not be able to see this! remember, it can be hidden inside kernel buffers
3017 package(arsd) class CallbackHelper {
3018 	import core.memory;
3019 
3020 	void call() {
3021 		if(callback)
3022 			callback();
3023 	}
3024 
3025 	void delegate() callback;
3026 	void*[3] argsStore;
3027 
3028 	void addref() {
3029 		version(HasThread)
3030 		atomicOp!"+="(refcount, 1);
3031 	}
3032 
3033 	void release() {
3034 		version(HasThread)
3035 		if(atomicOp!"-="(refcount, 1) <= 0) {
3036 			if(flags & 1)
3037 				GC.removeRoot(cast(void*) this);
3038 		}
3039 	}
3040 
3041 	private shared(int) refcount;
3042 	private uint flags;
3043 
3044 	this(void function() callback) {
3045 		this( () { callback(); } );
3046 	}
3047 
3048 	this(void delegate() callback, bool addRoot = true) {
3049 		version(HasThread)
3050 		if(addRoot) {
3051 			GC.addRoot(cast(void*) this);
3052 			this.flags |= 1;
3053 		}
3054 
3055 		this.addref();
3056 		this.callback = callback;
3057 	}
3058 }
3059 
3060 inout(char)[] trimSlashesRight(inout(char)[] txt) {
3061 	//if(txt.length && (txt[0] == '/' || txt[0] == '\\'))
3062 		//txt = txt[1 .. $];
3063 
3064 	if(txt.length && (txt[$-1] == '/' || txt[$-1] == '\\'))
3065 		txt = txt[0 .. $-1];
3066 
3067 	return txt;
3068 }
3069 
3070 enum TreatAsWindowsPath {
3071 	guess,
3072 	ifVersionWindows,
3073 	yes,
3074 	no,
3075 }
3076 
3077 // FIXME add uri from cgi/http2 and make sure the relative methods are reasonable compatible
3078 
3079 /++
3080 	This represents a file. Technically, file paths aren't actually strings (for example, on Linux, they need not be valid utf-8, while a D string is supposed to be), even though we almost always use them like that.
3081 
3082 	This type is meant to represent a filename / path. I might not keep it around.
3083 +/
3084 struct FilePath {
3085 	private string path;
3086 
3087 	this(string path) {
3088 		this.path = path;
3089 	}
3090 
3091 	bool isNull() const {
3092 		return path is null;
3093 	}
3094 
3095 	bool opCast(T:bool)() const {
3096 		return !isNull;
3097 	}
3098 
3099 	string toString() const {
3100 		return path;
3101 	}
3102 
3103 	//alias toString this;
3104 
3105 	/+ +++++++++++++++++ +/
3106 	/+  String analysis  +/
3107 	/+ +++++++++++++++++ +/
3108 
3109 	FilePath makeAbsolute(FilePath base, TreatAsWindowsPath treatAsWindowsPath = TreatAsWindowsPath.guess) const {
3110 		if(base.path.length == 0)
3111 			return this.removeExtraParts();
3112 		if(base.path[$-1] != '/' && base.path[$-1] != '\\')
3113 			base.path ~= '/';
3114 
3115 		bool isWindowsPath;
3116 		final switch(treatAsWindowsPath) {
3117 			case TreatAsWindowsPath.guess:
3118 			case TreatAsWindowsPath.yes:
3119 				isWindowsPath = true;
3120 			break;
3121 			case TreatAsWindowsPath.no:
3122 				isWindowsPath = false;
3123 			break;
3124 			case TreatAsWindowsPath.ifVersionWindows:
3125 				version(Windows)
3126 					isWindowsPath = true;
3127 				else
3128 					isWindowsPath = false;
3129 			break;
3130 		}
3131 		if(isWindowsPath) {
3132 			if(this.isUNC)
3133 				return this.removeExtraParts();
3134 			if(this.driveName)
3135 				return this.removeExtraParts();
3136 			if(this.path.length >= 1 && (this.path[0] == '/' || this.path[0] == '\\')) {
3137 				// drive-relative path, take the drive from the base
3138 				return FilePath(base.driveName ~ this.path).removeExtraParts();
3139 			}
3140 			// otherwise, take the dir name from the base and add us onto it
3141 			return FilePath(base.directoryName ~ this.path).removeExtraParts();
3142 		} else {
3143 			if(this.path.length >= 1 && this.path[0] == '/')
3144 				return this.removeExtraParts();
3145 			else
3146 				return FilePath(base.directoryName ~ this.path).removeExtraParts();
3147 		}
3148 	}
3149 
3150 	// dg returns true to continue, false to break
3151 	void foreachPathComponent(scope bool delegate(size_t index, in char[] component) dg) const {
3152 		size_t start;
3153 		size_t skip;
3154 		if(isUNC()) {
3155 			dg(start, this.path[start .. 2]);
3156 			start = 2;
3157 			skip = 2;
3158 		}
3159 		foreach(idx, ch; this.path) {
3160 			if(skip) { skip--; continue; }
3161 			if(ch == '/' || ch == '\\') {
3162 				if(!dg(start, this.path[start .. idx + 1]))
3163 					return;
3164 				start = idx + 1;
3165 			}
3166 		}
3167 		if(start != path.length)
3168 			dg(start, this.path[start .. $]);
3169 	}
3170 
3171 	// remove cases of // or /. or /.. Only valid to call this on an absolute path.
3172 	private FilePath removeExtraParts() const {
3173 		bool changeNeeded;
3174 		foreachPathComponent((idx, component) {
3175 			auto name = component.trimSlashesRight;
3176 			if(name.length == 0 && idx != 0)
3177 				changeNeeded = true;
3178 			if(name == "." || name == "..")
3179 				changeNeeded = true;
3180 			return !changeNeeded;
3181 		});
3182 
3183 		if(!changeNeeded)
3184 			return this;
3185 
3186 		string newPath;
3187 		foreachPathComponent((idx, component) {
3188 			auto name = component.trimSlashesRight;
3189 			if(component == `\\`) // must preserve unc paths
3190 				newPath ~= component;
3191 			else if(name.length == 0 && idx != 0)
3192 				{}
3193 			else if(name == ".")
3194 				{}
3195 			else if(name == "..") {
3196 				// remove the previous component, unless it is the first component
3197 				auto sofar = FilePath(newPath);
3198 				size_t previousComponentIndex;
3199 				sofar.foreachPathComponent((idx2, component2) {
3200 					if(idx2 != newPath.length)
3201 						previousComponentIndex = idx2;
3202 					return true;
3203 				});
3204 
3205 				if(previousComponentIndex && previousComponentIndex != newPath.length) {
3206 					newPath = newPath[0 .. previousComponentIndex];
3207 					//newPath.assumeSafeAppend();
3208 				}
3209 			} else {
3210 				newPath ~= component;
3211 			}
3212 
3213 			return true;
3214 		});
3215 
3216 		return FilePath(newPath);
3217 	}
3218 
3219 	// assuming we're looking at a Windows path...
3220 	bool isUNC() const {
3221 		return (path.length > 2 && path[0 .. 2] == `\\`);
3222 	}
3223 
3224 	// assuming we're looking at a Windows path...
3225 	string driveName() const {
3226 		if(path.length < 2)
3227 			return null;
3228 		if((path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z')) {
3229 			if(path[1] == ':') {
3230 				if(path.length == 2 || path[2] == '\\' || path[2] == '/')
3231 					return path[0 .. 2];
3232 			}
3233 		}
3234 		return null;
3235 	}
3236 
3237 	/+
3238 	bool isAbsolute() {
3239 		if(path.length && path[0] == '/')
3240 			return true;
3241 
3242 	}
3243 
3244 	FilePath relativeTo() {
3245 
3246 	}
3247 
3248 	bool matchesGlobPattern(string globPattern) {
3249 
3250 	}
3251 
3252 	this(string directoryName, string filename) {}
3253 	this(string directoryName, string basename, string extension) {}
3254 
3255 	// remove ./, ../, stuff like that
3256 	FilePath normalize(FilePath relativeTo) {}
3257 	+/
3258 
3259 	/++
3260 		Returns the path with the directory cut off.
3261 	+/
3262 	string filename() {
3263 		foreach_reverse(idx, ch; path) {
3264 			if(ch == '\\' || ch == '/')
3265 				return path[idx + 1 .. $];
3266 		}
3267 		return path;
3268 	}
3269 
3270 	/++
3271 		Returns the path with the filename cut off.
3272 	+/
3273 	string directoryName() {
3274 		auto fn = this.filename();
3275 		if(fn is path)
3276 			return null;
3277 		return path[0 .. $ - fn.length];
3278 	}
3279 
3280 	/++
3281 		Returns the file extension, if present, including the last dot.
3282 	+/
3283 	string extension() {
3284 		foreach_reverse(idx, ch; path) {
3285 			if(ch == '.')
3286 				return path[idx .. $];
3287 		}
3288 		return null;
3289 	}
3290 
3291 	/++
3292 		Guesses the media (aka mime) content type from the file extension for this path.
3293 
3294 		Only has a few things supported. Returns null if it doesn't know.
3295 
3296 		History:
3297 			Moved from arsd.cgi to arsd.core.FilePath on October 28, 2024
3298 	+/
3299 	string contentTypeFromFileExtension() {
3300 		switch(this.extension) {
3301 			// images
3302 			case ".png":
3303 				return "image/png";
3304 			case ".apng":
3305 				return "image/apng";
3306 			case ".svg":
3307 				return "image/svg+xml";
3308 			case ".jpg":
3309 			case ".jpeg":
3310 				return "image/jpeg";
3311 
3312 			case ".txt":
3313 				return "text/plain";
3314 
3315 			case ".html":
3316 				return "text/html";
3317 			case ".css":
3318 				return "text/css";
3319 			case ".js":
3320 				return "application/javascript";
3321 			case ".wasm":
3322 				return "application/wasm";
3323 
3324 			case ".mp3":
3325 				return "audio/mpeg";
3326 
3327 			case ".pdf":
3328 				return "application/pdf";
3329 
3330 			default:
3331 				return null;
3332 		}
3333 	}
3334 }
3335 
3336 unittest {
3337 	FilePath fn;
3338 
3339 	fn = FilePath("dir/name.ext");
3340 	assert(fn.directoryName == "dir/");
3341 	assert(fn.filename == "name.ext");
3342 	assert(fn.extension == ".ext");
3343 
3344 	fn = FilePath(null);
3345 	assert(fn.directoryName is null);
3346 	assert(fn.filename is null);
3347 	assert(fn.extension is null);
3348 
3349 	fn = FilePath("file.txt");
3350 	assert(fn.directoryName is null);
3351 	assert(fn.filename == "file.txt");
3352 	assert(fn.extension == ".txt");
3353 
3354 	fn = FilePath("dir/");
3355 	assert(fn.directoryName == "dir/");
3356 	assert(fn.filename == "");
3357 	assert(fn.extension is null);
3358 
3359 	assert(fn.makeAbsolute(FilePath("/")).path == "/dir/");
3360 	assert(fn.makeAbsolute(FilePath("file.txt")).path == "file.txt/dir/"); // FilePaths as a base are ALWAYS treated as a directory
3361 	assert(FilePath("file.txt").makeAbsolute(fn).path == "dir/file.txt");
3362 
3363 	assert(FilePath("c:/file.txt").makeAbsolute(FilePath("d:/")).path == "c:/file.txt");
3364 	assert(FilePath("../file.txt").makeAbsolute(FilePath("d:/")).path == "d:/file.txt");
3365 
3366 	assert(FilePath("../file.txt").makeAbsolute(FilePath("d:/foo")).path == "d:/file.txt");
3367 	assert(FilePath("../file.txt").makeAbsolute(FilePath("d:/")).path == "d:/file.txt");
3368 	assert(FilePath("../file.txt").makeAbsolute(FilePath("/home/me")).path == "/home/file.txt");
3369 	assert(FilePath("../file.txt").makeAbsolute(FilePath(`\\arsd\me`)).path == `\\arsd\file.txt`);
3370 	assert(FilePath("../../file.txt").makeAbsolute(FilePath("/home/me")).path == "/file.txt");
3371 	assert(FilePath("../../../file.txt").makeAbsolute(FilePath("/home/me")).path == "/file.txt");
3372 
3373 	assert(FilePath("test/").makeAbsolute(FilePath("/home/me/")).path == "/home/me/test/");
3374 	assert(FilePath("/home/me/test/").makeAbsolute(FilePath("/home/me/test/")).path == "/home/me/test/");
3375 }
3376 
3377 version(HasFile)
3378 /++
3379 	History:
3380 		Added January 2, 2024
3381 +/
3382 FilePath getCurrentWorkingDirectory() {
3383 	version(Windows) {
3384 		wchar[256] staticBuffer;
3385 		wchar[] buffer = staticBuffer[];
3386 
3387 		try_again:
3388 		auto ret = GetCurrentDirectoryW(cast(DWORD) buffer.length, buffer.ptr);
3389 		if(ret == 0)
3390 			throw new WindowsApiException("GetCurrentDirectoryW", GetLastError());
3391 		if(ret < buffer.length) {
3392 			return FilePath(makeUtf8StringFromWindowsString(buffer[0 .. ret]));
3393 		} else {
3394 			buffer.length = ret;
3395 			goto try_again;
3396 		}
3397 	} else version(Posix) {
3398 		char[128] staticBuffer;
3399 		char[] buffer = staticBuffer[];
3400 
3401 		try_again:
3402 		auto ret = getcwd(buffer.ptr, buffer.length);
3403 		if(ret is null && errno == ERANGE && buffer.length < 4096 / 2) {
3404 			buffer.length = buffer.length * 2;
3405 			goto try_again;
3406 		} else if(ret is null) {
3407 			throw new ErrnoApiException("getcwd", errno);
3408 		}
3409 		return FilePath(stringz(ret).borrow.idup);
3410 	} else
3411 		assert(0, "Not implemented");
3412 }
3413 
3414 /+
3415 struct FilePathGeneric {
3416 
3417 }
3418 
3419 struct FilePathWin32 {
3420 
3421 }
3422 
3423 struct FilePathPosix {
3424 
3425 }
3426 
3427 struct FilePathWindowsUnc {
3428 
3429 }
3430 
3431 version(Windows)
3432 	alias FilePath = FilePathWin32;
3433 else
3434 	alias FilePath = FilePathPosix;
3435 +/
3436 
3437 
3438 /++
3439 	Represents a generic async, waitable request.
3440 +/
3441 class AsyncOperationRequest {
3442 	/++
3443 		Actually issues the request, starting the operation.
3444 	+/
3445 	abstract void start();
3446 	/++
3447 		Cancels the request. This will cause `isComplete` to return true once the cancellation has been processed, but [AsyncOperationResponse.wasSuccessful] will return `false` (unless it completed before the cancellation was processed, in which case it is still allowed to finish successfully).
3448 
3449 		After cancelling a request, you should still wait for it to complete to ensure that the task has actually released its resources before doing anything else on it.
3450 
3451 		Once a cancellation request has been sent, it cannot be undone.
3452 	+/
3453 	abstract void cancel();
3454 
3455 	/++
3456 		Returns `true` if the operation has been completed. It may be completed successfully, cancelled, or have errored out - to check this, call [waitForCompletion] and check the members on the response object.
3457 	+/
3458 	abstract bool isComplete();
3459 	/++
3460 		Waits until the request has completed - successfully or otherwise - and returns the response object. It will run an ad-hoc event loop that may call other callbacks while waiting.
3461 
3462 		The response object may be embedded in the request object - do not reuse the request until you are finished with the response and do not keep the response around longer than you keep the request.
3463 
3464 
3465 		Note to implementers: all subclasses should override this and return their specific response object. You can use the top-level `waitForFirstToCompleteByIndex` function with a single-element static array to help with the implementation.
3466 	+/
3467 	abstract AsyncOperationResponse waitForCompletion();
3468 
3469 	/++
3470 
3471 	+/
3472 	// abstract void repeat();
3473 }
3474 
3475 /++
3476 
3477 +/
3478 interface AsyncOperationResponse {
3479 	/++
3480 		Returns true if the request completed successfully, finishing what it was supposed to.
3481 
3482 		Should be set to `false` if the request was cancelled before completing or encountered an error.
3483 	+/
3484 	bool wasSuccessful();
3485 }
3486 
3487 /++
3488 	It returns the $(I request) so you can identify it more easily. `request.waitForCompletion()` is guaranteed to return the response without any actual wait, since it is already complete when this function returns.
3489 
3490 	Please note that "completion" is not necessary successful completion; a request being cancelled or encountering an error also counts as it being completed.
3491 
3492 	The `waitForFirstToCompleteByIndex` version instead returns the index of the array entry that completed first.
3493 
3494 	It is your responsibility to remove the completed request from the array before calling the function again, since any request already completed will always be immediately returned.
3495 
3496 	You might prefer using [asTheyComplete], which will give each request as it completes and loop over until all of them are complete.
3497 
3498 	Returns:
3499 		`null` or `requests.length` if none completed before returning.
3500 +/
3501 AsyncOperationRequest waitForFirstToComplete(AsyncOperationRequest[] requests...) {
3502 	auto idx = waitForFirstToCompleteByIndex(requests);
3503 	if(idx == requests.length)
3504 		return null;
3505 	return requests[idx];
3506 }
3507 /// ditto
3508 size_t waitForFirstToCompleteByIndex(AsyncOperationRequest[] requests...) {
3509 	size_t helper() {
3510 		foreach(idx, request; requests)
3511 			if(request.isComplete())
3512 				return idx;
3513 		return requests.length;
3514 	}
3515 
3516 	auto idx = helper();
3517 	// if one is already done, return it
3518 	if(idx != requests.length)
3519 		return idx;
3520 
3521 	// otherwise, run the ad-hoc event loop until one is
3522 	// FIXME: what if we are inside a fiber?
3523 	auto el = getThisThreadEventLoop();
3524 	el.run(() => (idx = helper()) != requests.length);
3525 
3526 	return idx;
3527 }
3528 
3529 /++
3530 	Waits for all the `requests` to complete, giving each one through the range interface as it completes.
3531 
3532 	This meant to be used in a foreach loop.
3533 
3534 	The `requests` array and its contents must remain valid for the lifetime of the returned range. Its contents may be shuffled as the requests complete (the implementation works through an unstable sort+remove).
3535 +/
3536 AsTheyCompleteRange asTheyComplete(AsyncOperationRequest[] requests...) {
3537 	return AsTheyCompleteRange(requests);
3538 }
3539 /// ditto
3540 struct AsTheyCompleteRange {
3541 	AsyncOperationRequest[] requests;
3542 
3543 	this(AsyncOperationRequest[] requests) {
3544 		this.requests = requests;
3545 
3546 		if(requests.length == 0)
3547 			return;
3548 
3549 		// wait for first one to complete, then move it to the front of the array
3550 		moveFirstCompleteToFront();
3551 	}
3552 
3553 	private void moveFirstCompleteToFront() {
3554 		auto idx = waitForFirstToCompleteByIndex(requests);
3555 
3556 		auto tmp = requests[0];
3557 		requests[0] = requests[idx];
3558 		requests[idx] = tmp;
3559 	}
3560 
3561 	bool empty() {
3562 		return requests.length == 0;
3563 	}
3564 
3565 	void popFront() {
3566 		assert(!empty);
3567 		/+
3568 			this needs to
3569 			1) remove the front of the array as being already processed (unless it is the initial priming call)
3570 			2) wait for one of them to complete
3571 			3) move the complete one to the front of the array
3572 		+/
3573 
3574 		requests[0] = requests[$-1];
3575 		requests = requests[0 .. $-1];
3576 
3577 		if(requests.length)
3578 			moveFirstCompleteToFront();
3579 	}
3580 
3581 	AsyncOperationRequest front() {
3582 		return requests[0];
3583 	}
3584 }
3585 
3586 version(Windows) {
3587 	alias NativeFileHandle = HANDLE; ///
3588 	alias NativeSocketHandle = SOCKET; ///
3589 	alias NativePipeHandle = HANDLE; ///
3590 } else version(Posix) {
3591 	alias NativeFileHandle = int; ///
3592 	alias NativeSocketHandle = int; ///
3593 	alias NativePipeHandle = int; ///
3594 }
3595 
3596 /++
3597 	An `AbstractFile` represents a file handle on the operating system level. You cannot do much with it.
3598 +/
3599 version(HasFile) class AbstractFile {
3600 	private {
3601 		NativeFileHandle handle;
3602 	}
3603 
3604 	/++
3605 	+/
3606 	enum OpenMode {
3607 		readOnly, /// C's "r", the file is read
3608 		writeWithTruncation, /// C's "w", the file is blanked upon opening so it only holds what you write
3609 		appendOnly, /// C's "a", writes will always be appended to the file
3610 		readAndWrite /// C's "r+", writes will overwrite existing parts of the file based on where you seek (default is at the beginning)
3611 	}
3612 
3613 	/++
3614 	+/
3615 	enum RequirePreexisting {
3616 		no,
3617 		yes
3618 	}
3619 
3620 	/+
3621 	enum SpecialFlags {
3622 		randomAccessExpected, /// FILE_FLAG_SEQUENTIAL_SCAN is turned off and posix_fadvise(POSIX_FADV_SEQUENTIAL)
3623 		skipCache, /// O_DSYNC, FILE_FLAG_NO_BUFFERING and maybe WRITE_THROUGH. note that metadata still goes through the cache, FlushFileBuffers and fsync can still do those
3624 		temporary, /// FILE_ATTRIBUTE_TEMPORARY on Windows, idk how to specify on linux. also FILE_FLAG_DELETE_ON_CLOSE can be combined to make a (almost) all memory file. kinda like a private anonymous mmap i believe.
3625 		deleteWhenClosed, /// Windows has a flag for this but idk if it is of any real use
3626 		async, /// open it in overlapped mode, all reads and writes must then provide an offset. Only implemented on Windows
3627 	}
3628 	+/
3629 
3630 	/++
3631 
3632 	+/
3633 	protected this(bool async, FilePath filename, OpenMode mode = OpenMode.readOnly, RequirePreexisting require = RequirePreexisting.no, uint specialFlags = 0) {
3634 		version(Windows) {
3635 			DWORD access;
3636 			DWORD creation;
3637 
3638 			final switch(mode) {
3639 				case OpenMode.readOnly:
3640 					access = GENERIC_READ;
3641 					creation = OPEN_EXISTING;
3642 				break;
3643 				case OpenMode.writeWithTruncation:
3644 					access = GENERIC_WRITE;
3645 
3646 					final switch(require) {
3647 						case RequirePreexisting.no:
3648 							creation = CREATE_ALWAYS;
3649 						break;
3650 						case RequirePreexisting.yes:
3651 							creation = TRUNCATE_EXISTING;
3652 						break;
3653 					}
3654 				break;
3655 				case OpenMode.appendOnly:
3656 					access = FILE_APPEND_DATA;
3657 
3658 					final switch(require) {
3659 						case RequirePreexisting.no:
3660 							creation = CREATE_ALWAYS;
3661 						break;
3662 						case RequirePreexisting.yes:
3663 							creation = OPEN_EXISTING;
3664 						break;
3665 					}
3666 				break;
3667 				case OpenMode.readAndWrite:
3668 					access = GENERIC_READ | GENERIC_WRITE;
3669 
3670 					final switch(require) {
3671 						case RequirePreexisting.no:
3672 							creation = CREATE_NEW;
3673 						break;
3674 						case RequirePreexisting.yes:
3675 							creation = OPEN_EXISTING;
3676 						break;
3677 					}
3678 				break;
3679 			}
3680 
3681 			WCharzBuffer wname = WCharzBuffer(filename.path);
3682 
3683 			auto handle = CreateFileW(
3684 				wname.ptr,
3685 				access,
3686 				FILE_SHARE_READ,
3687 				null,
3688 				creation,
3689 				FILE_ATTRIBUTE_NORMAL | (async ? FILE_FLAG_OVERLAPPED : 0),
3690 				null
3691 			);
3692 
3693 			if(handle == INVALID_HANDLE_VALUE) {
3694 				// FIXME: throw the filename and other params here too
3695 				SavedArgument[3] args;
3696 				args[0] = SavedArgument("filename", LimitedVariant(filename.path));
3697 				args[1] = SavedArgument("access", LimitedVariant(access, 2));
3698 				args[2] = SavedArgument("requirePreexisting", LimitedVariant(require == RequirePreexisting.yes));
3699 				throw new WindowsApiException("CreateFileW", GetLastError(), args[]);
3700 			}
3701 
3702 			this.handle = handle;
3703 		} else version(Posix) {
3704 			import core.sys.posix.unistd;
3705 			import core.sys.posix.fcntl;
3706 
3707 			CharzBuffer namez = CharzBuffer(filename.path);
3708 			int flags;
3709 
3710 			// FIXME does mac not have cloexec for real or is this just a druntime problem?????
3711 			version(Arsd_core_has_cloexec) {
3712 				flags = O_CLOEXEC;
3713 			} else {
3714 				scope(success)
3715 					setCloExec(this.handle);
3716 			}
3717 
3718 			if(async)
3719 				flags |= O_NONBLOCK;
3720 
3721 			final switch(mode) {
3722 				case OpenMode.readOnly:
3723 					flags |= O_RDONLY;
3724 				break;
3725 				case OpenMode.writeWithTruncation:
3726 					flags |= O_WRONLY | O_TRUNC;
3727 
3728 					final switch(require) {
3729 						case RequirePreexisting.no:
3730 							flags |= O_CREAT;
3731 						break;
3732 						case RequirePreexisting.yes:
3733 						break;
3734 					}
3735 				break;
3736 				case OpenMode.appendOnly:
3737 					flags |= O_APPEND;
3738 
3739 					final switch(require) {
3740 						case RequirePreexisting.no:
3741 							flags |= O_CREAT;
3742 						break;
3743 						case RequirePreexisting.yes:
3744 						break;
3745 					}
3746 				break;
3747 				case OpenMode.readAndWrite:
3748 					flags |= O_RDWR;
3749 
3750 					final switch(require) {
3751 						case RequirePreexisting.no:
3752 							flags |= O_CREAT;
3753 						break;
3754 						case RequirePreexisting.yes:
3755 						break;
3756 					}
3757 				break;
3758 			}
3759 
3760 			auto perms = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
3761 			int fd = open(namez.ptr, flags, perms);
3762 			if(fd == -1) {
3763 				SavedArgument[3] args;
3764 				args[0] = SavedArgument("filename", LimitedVariant(filename.path));
3765 				args[1] = SavedArgument("flags", LimitedVariant(flags, 2));
3766 				args[2] = SavedArgument("perms", LimitedVariant(perms, 8));
3767 				throw new ErrnoApiException("open", errno, args[]);
3768 			}
3769 
3770 			this.handle = fd;
3771 		}
3772 	}
3773 
3774 	/++
3775 
3776 	+/
3777 	private this(NativeFileHandle handleToWrap) {
3778 		this.handle = handleToWrap;
3779 	}
3780 
3781 	// only available on some types of file
3782 	long size() { return 0; }
3783 
3784 	// note that there is no fsync thing, instead use the special flag.
3785 
3786 	/++
3787 
3788 	+/
3789 	void close() {
3790 		version(Windows) {
3791 			Win32Enforce!CloseHandle(handle);
3792 			handle = null;
3793 		} else version(Posix) {
3794 			import unix = core.sys.posix.unistd;
3795 			import core.sys.posix.fcntl;
3796 
3797 			ErrnoEnforce!(unix.close)(handle);
3798 			handle = -1;
3799 		}
3800 	}
3801 }
3802 
3803 /++
3804 
3805 +/
3806 version(HasFile) class File : AbstractFile {
3807 
3808 	/++
3809 		Opens a file in synchronous access mode.
3810 
3811 		The permission mask is on used on posix systems FIXME: implement it
3812 	+/
3813 	this(FilePath filename, OpenMode mode = OpenMode.readOnly, RequirePreexisting require = RequirePreexisting.no, uint specialFlags = 0, uint permMask = 0) {
3814 		super(false, filename, mode, require, specialFlags);
3815 	}
3816 
3817 	/++
3818 
3819 	+/
3820 	ubyte[] read(scope ubyte[] buffer) {
3821 		return null;
3822 	}
3823 
3824 	/++
3825 
3826 	+/
3827 	void write(in void[] buffer) {
3828 	}
3829 
3830 	enum Seek {
3831 		current,
3832 		fromBeginning,
3833 		fromEnd
3834 	}
3835 
3836 	// Seeking/telling/sizing is not permitted when appending and some files don't support it
3837 	// also not permitted in async mode
3838 	void seek(long where, Seek fromWhence) {}
3839 	long tell() { return 0; }
3840 }
3841 
3842 /++
3843 	 Only one operation can be pending at any time in the current implementation.
3844 +/
3845 version(HasFile) class AsyncFile : AbstractFile {
3846 	/++
3847 		Opens a file in asynchronous access mode.
3848 	+/
3849 	this(FilePath filename, OpenMode mode = OpenMode.readOnly, RequirePreexisting require = RequirePreexisting.no, uint specialFlags = 0, uint permissionMask = 0) {
3850 		// FIXME: implement permissionMask
3851 		super(true, filename, mode, require, specialFlags);
3852 	}
3853 
3854 	package(arsd) this(NativeFileHandle adoptPreSetup) {
3855 		super(adoptPreSetup);
3856 	}
3857 
3858 	///
3859 	AsyncReadRequest read(ubyte[] buffer, long offset = 0) {
3860 		return new AsyncReadRequest(this, buffer, offset);
3861 	}
3862 
3863 	///
3864 	AsyncWriteRequest write(const(void)[] buffer, long offset = 0) {
3865 		return new AsyncWriteRequest(this, cast(ubyte[]) buffer, offset);
3866 	}
3867 
3868 }
3869 else class AsyncFile {
3870 	package(arsd) this(NativeFileHandle adoptPreSetup) {}
3871 }
3872 
3873 /++
3874 	Reads or writes a file in one call. It might internally yield, but is generally blocking if it returns values. The callback ones depend on the implementation.
3875 
3876 	Tip: prefer the callback ones. If settings where async is possible, it will do async, and if not, it will sync.
3877 
3878 	NOT IMPLEMENTED
3879 +/
3880 void writeFile(string filename, const(void)[] contents) {
3881 
3882 }
3883 
3884 /// ditto
3885 string readTextFile(string filename, string fileEncoding = null) {
3886 	return null;
3887 }
3888 
3889 /// ditto
3890 const(ubyte[]) readBinaryFile(string filename) {
3891 	return null;
3892 }
3893 
3894 /+
3895 private Class recycleObject(Class, Args...)(Class objectToRecycle, Args args) {
3896 	if(objectToRecycle is null)
3897 		return new Class(args);
3898 	// destroy nulls out the vtable which is the first thing in the object
3899 	// so if it hasn't already been destroyed, we'll do it here
3900 	if((*cast(void**) objectToRecycle) !is null) {
3901 		assert(typeid(objectToRecycle) is typeid(Class)); // to make sure we're actually recycling the right kind of object
3902 		.destroy(objectToRecycle);
3903 	}
3904 
3905 	// then go ahead and reinitialize it
3906 	ubyte[] rawData = (cast(ubyte*) cast(void*) objectToRecycle)[0 .. __traits(classInstanceSize, Class)];
3907 	rawData[] = (cast(ubyte[]) typeid(Class).initializer)[];
3908 
3909 	objectToRecycle.__ctor(args);
3910 
3911 	return objectToRecycle;
3912 }
3913 +/
3914 
3915 /+
3916 /++
3917 	Preallocates a class object without initializing it.
3918 
3919 	This is suitable *only* for passing to one of the functions in here that takes a preallocated object for recycling.
3920 +/
3921 Class preallocate(Class)() {
3922 	import core.memory;
3923 	// FIXME: can i pass NO_SCAN here?
3924 	return cast(Class) GC.calloc(__traits(classInstanceSize, Class), 0, typeid(Class));
3925 }
3926 
3927 OwnedClass!Class preallocateOnStack(Class)() {
3928 
3929 }
3930 +/
3931 
3932 // thanks for a random person on stack overflow for this function
3933 version(Windows)
3934 BOOL MyCreatePipeEx(
3935 	PHANDLE lpReadPipe,
3936 	PHANDLE lpWritePipe,
3937 	LPSECURITY_ATTRIBUTES lpPipeAttributes,
3938 	DWORD nSize,
3939 	DWORD dwReadMode,
3940 	DWORD dwWriteMode
3941 )
3942 {
3943 	HANDLE ReadPipeHandle, WritePipeHandle;
3944 	DWORD dwError;
3945 	CHAR[MAX_PATH] PipeNameBuffer;
3946 
3947 	if (nSize == 0) {
3948 		nSize = 4096;
3949 	}
3950 
3951 	// FIXME: should be atomic op and gshared
3952 	static shared(int) PipeSerialNumber = 0;
3953 
3954 	import core.stdc.string;
3955 	import core.stdc.stdio;
3956 
3957 	sprintf(PipeNameBuffer.ptr,
3958 		"\\\\.\\Pipe\\ArsdCoreAnonymousPipe.%08x.%08x".ptr,
3959 		GetCurrentProcessId(),
3960 		atomicOp!"+="(PipeSerialNumber, 1)
3961 	);
3962 
3963 	ReadPipeHandle = CreateNamedPipeA(
3964 		PipeNameBuffer.ptr,
3965 		1/*PIPE_ACCESS_INBOUND*/ | dwReadMode,
3966 		0/*PIPE_TYPE_BYTE*/ | 0/*PIPE_WAIT*/,
3967 		1,             // Number of pipes
3968 		nSize,         // Out buffer size
3969 		nSize,         // In buffer size
3970 		120 * 1000,    // Timeout in ms
3971 		lpPipeAttributes
3972 	);
3973 
3974 	if (! ReadPipeHandle) {
3975 		return FALSE;
3976 	}
3977 
3978 	WritePipeHandle = CreateFileA(
3979 		PipeNameBuffer.ptr,
3980 		GENERIC_WRITE,
3981 		0,                         // No sharing
3982 		lpPipeAttributes,
3983 		OPEN_EXISTING,
3984 		FILE_ATTRIBUTE_NORMAL | dwWriteMode,
3985 		null                       // Template file
3986 	);
3987 
3988 	if (INVALID_HANDLE_VALUE == WritePipeHandle) {
3989 		dwError = GetLastError();
3990 		CloseHandle( ReadPipeHandle );
3991 		SetLastError(dwError);
3992 		return FALSE;
3993 	}
3994 
3995 	*lpReadPipe = ReadPipeHandle;
3996 	*lpWritePipe = WritePipeHandle;
3997 	return( TRUE );
3998 }
3999 
4000 
4001 
4002 /+
4003 
4004 	// this is probably useless.
4005 
4006 /++
4007 	Creates a pair of anonymous pipes ready for async operations.
4008 
4009 	You can pass some preallocated objects to recycle if you like.
4010 +/
4011 AsyncAnonymousPipe[2] anonymousPipePair(AsyncAnonymousPipe[2] preallocatedObjects = [null, null], bool inheritable = false) {
4012 	version(Posix) {
4013 		int[2] fds;
4014 		auto ret = pipe(fds);
4015 
4016 		if(ret == -1)
4017 			throw new SystemApiException("pipe", errno);
4018 
4019 		// FIXME: do we want them inheritable? and do we want both sides to be async?
4020 		if(!inheritable) {
4021 			setCloExec(fds[0]);
4022 			setCloExec(fds[1]);
4023 		}
4024 		// if it is inherited, do we actually want it non-blocking?
4025 		makeNonBlocking(fds[0]);
4026 		makeNonBlocking(fds[1]);
4027 
4028 		return [
4029 			recycleObject(preallocatedObjects[0], fds[0]),
4030 			recycleObject(preallocatedObjects[1], fds[1]),
4031 		];
4032 	} else version(Windows) {
4033 		HANDLE rp, wp;
4034 		// FIXME: do we want them inheritable? and do we want both sides to be async?
4035 		if(!MyCreatePipeEx(&rp, &wp, null, 0, FILE_FLAG_OVERLAPPED, FILE_FLAG_OVERLAPPED))
4036 			throw new SystemApiException("MyCreatePipeEx", GetLastError());
4037 		return [
4038 			recycleObject(preallocatedObjects[0], rp),
4039 			recycleObject(preallocatedObjects[1], wp),
4040 		];
4041 	} else throw ArsdException!"NotYetImplemented"();
4042 }
4043 	// on posix, just do pipe() w/ non block
4044 	// on windows, do an overlapped named pipe server, connect, stop listening, return pair.
4045 +/
4046 
4047 /+
4048 class NamedPipe : AsyncFile {
4049 
4050 }
4051 +/
4052 
4053 /++
4054 	A named pipe ready to accept connections.
4055 
4056 	A Windows named pipe is an IPC mechanism usable on local machines or across a Windows network.
4057 +/
4058 version(Windows)
4059 class NamedPipeServer {
4060 	// unix domain socket or windows named pipe
4061 
4062 	// Promise!AsyncAnonymousPipe connect;
4063 	// Promise!AsyncAnonymousPipe accept;
4064 
4065 	// when a new connection arrives, it calls your callback
4066 	// can be on a specific thread or on any thread
4067 }
4068 
4069 private version(Windows) extern(Windows) {
4070 	const(char)* inet_ntop(int, const void*, char*, socklen_t);
4071 }
4072 
4073 /++
4074 	Some functions that return arrays allow you to provide your own buffer. These are indicated in the type system as `UserProvidedBuffer!Type`, and you get to decide what you want to happen if the buffer is too small via the [OnOutOfSpace] parameter.
4075 
4076 	These are usually optional, since an empty user provided buffer with the default policy of reallocate will also work fine for whatever needs to be returned, thanks to the garbage collector taking care of it for you.
4077 
4078 	The API inside `UserProvidedBuffer` is all private to the arsd library implementation; your job is just to provide the buffer to it with [provideBuffer] or a constructor call and decide on your on-out-of-space policy.
4079 
4080 	$(TIP
4081 		To properly size a buffer, I suggest looking at what covers about 80% of cases. Trying to cover everything often leads to wasted buffer space, and if you use a reallocate policy it can cover the rest. You might be surprised how far just two elements can go!
4082 	)
4083 
4084 	History:
4085 		Added August 4, 2023 (dub v11.0)
4086 +/
4087 struct UserProvidedBuffer(T) {
4088 	private T[] buffer;
4089 	private int actualLength;
4090 	private OnOutOfSpace policy;
4091 
4092 	/++
4093 
4094 	+/
4095 	public this(scope T[] buffer, OnOutOfSpace policy = OnOutOfSpace.reallocate) {
4096 		this.buffer = buffer;
4097 		this.policy = policy;
4098 	}
4099 
4100 	package(arsd) bool append(T item) {
4101 		if(actualLength < buffer.length) {
4102 			buffer[actualLength++] = item;
4103 			return true;
4104 		} else final switch(policy) {
4105 			case OnOutOfSpace.discard:
4106 				return false;
4107 			case OnOutOfSpace.exception:
4108 				throw ArsdException!"Buffer out of space"(buffer.length, actualLength);
4109 			case OnOutOfSpace.reallocate:
4110 				buffer ~= item;
4111 				actualLength++;
4112 				return true;
4113 		}
4114 	}
4115 
4116 	package(arsd) T[] slice() {
4117 		return buffer[0 .. actualLength];
4118 	}
4119 }
4120 
4121 /// ditto
4122 UserProvidedBuffer!T provideBuffer(T)(scope T[] buffer, OnOutOfSpace policy = OnOutOfSpace.reallocate) {
4123 	return UserProvidedBuffer!T(buffer, policy);
4124 }
4125 
4126 /++
4127 	Possible policies for [UserProvidedBuffer]s that run out of space.
4128 +/
4129 enum OnOutOfSpace {
4130 	reallocate, /// reallocate the buffer with the GC to make room
4131 	discard, /// discard all contents that do not fit in your provided buffer
4132 	exception, /// throw an exception if there is data that would not fit in your provided buffer
4133 }
4134 
4135 
4136 
4137 /+
4138 	The GC can be called from any thread, and a lot of cleanup must be done
4139 	on the gui thread. Since the GC can interrupt any locks - including being
4140 	triggered inside a critical section - it is vital to avoid deadlocks to get
4141 	these functions called from the right place.
4142 
4143 	If the buffer overflows, things are going to get leaked. I'm kinda ok with that
4144 	right now.
4145 
4146 	The cleanup function is run when the event loop gets around to it, which is just
4147 	whenever there's something there after it has been woken up for other work. It does
4148 	NOT wake up the loop itself - can't risk doing that from inside the GC in another thread.
4149 	(Well actually it might be ok but i don't wanna mess with it right now.)
4150 +/
4151 package(arsd) struct CleanupQueue {
4152 	import core.stdc.stdlib;
4153 
4154 	void queue(alias func, T...)(T args) {
4155 		static struct Args {
4156 			T args;
4157 		}
4158 		static struct RealJob {
4159 			Job j;
4160 			Args a;
4161 		}
4162 		static void call(Job* data) {
4163 			auto rj = cast(RealJob*) data;
4164 			func(rj.a.args);
4165 		}
4166 
4167 		RealJob* thing = cast(RealJob*) malloc(RealJob.sizeof);
4168 		thing.j.call = &call;
4169 		thing.a.args = args;
4170 
4171 		buffer[tail++] = cast(Job*) thing;
4172 
4173 		// FIXME: set overflowed
4174 	}
4175 
4176 	void process() {
4177 		const tail = this.tail;
4178 
4179 		while(tail != head) {
4180 			Job* job = cast(Job*) buffer[head++];
4181 			job.call(job);
4182 			free(job);
4183 		}
4184 
4185 		if(overflowed)
4186 			throw new object.Exception("cleanup overflowed");
4187 	}
4188 
4189 	private:
4190 
4191 	ubyte tail; // must ONLY be written by queue
4192 	ubyte head; // must ONLY be written by process
4193 	bool overflowed;
4194 
4195 	static struct Job {
4196 		void function(Job*) call;
4197 	}
4198 
4199 	void*[256] buffer;
4200 }
4201 package(arsd) __gshared CleanupQueue cleanupQueue;
4202 
4203 
4204 
4205 
4206 /++
4207 	A timer that will trigger your function on a given interval.
4208 
4209 
4210 	You create a timer with an interval and a callback. It will continue
4211 	to fire on the interval until it is destroyed.
4212 
4213 	---
4214 	auto timer = new Timer(50, { it happened!; });
4215 	timer.destroy();
4216 	---
4217 
4218 	Timers can only be expected to fire when the event loop is running and only
4219 	once per iteration through the event loop.
4220 
4221 	History:
4222 		Prior to December 9, 2020, a timer pulse set too high with a handler too
4223 		slow could lock up the event loop. It now guarantees other things will
4224 		get a chance to run between timer calls, even if that means not keeping up
4225 		with the requested interval.
4226 
4227 		Originally part of arsd.simpledisplay, this code was integrated into
4228 		arsd.core on May 26, 2024 (committed on June 10).
4229 +/
4230 version(HasTimer)
4231 class Timer {
4232 	// FIXME: absolute time vs relative time
4233 	// FIXME: real time?
4234 
4235 	// FIXME: I might add overloads for ones that take a count of
4236 	// how many elapsed since last time (on Windows, it will divide
4237 	// the ticks thing given, on Linux it is just available) and
4238 	// maybe one that takes an instance of the Timer itself too
4239 
4240 
4241 	/++
4242 		Creates an initialized, but unarmed timer. You must call other methods later.
4243 	+/
4244 	this(bool actuallyInitialize = true) {
4245 		if(actuallyInitialize)
4246 			initialize();
4247 	}
4248 
4249 	private void initialize() {
4250 		version(Windows) {
4251 			handle = CreateWaitableTimer(null, false, null);
4252 			if(handle is null)
4253 				throw new WindowsApiException("CreateWaitableTimer", GetLastError());
4254 			cbh = new CallbackHelper(&trigger);
4255 		} else version(Emscripten) {
4256 			assert(0);
4257 		} else version(linux) {
4258 			import core.sys.linux.timerfd;
4259 
4260 			fd = timerfd_create(CLOCK_MONOTONIC, 0);
4261 			if(fd == -1)
4262 				throw new Exception("timer create failed");
4263 
4264 			auto el = getThisThreadEventLoop(EventLoopType.Ui);
4265 			unregisterToken = el.addCallbackOnFdReadable(fd, new CallbackHelper(&trigger));
4266 		} else throw new NotYetImplementedException();
4267 		// FIXME: freebsd 12 has timer_fd and netbsd 10 too
4268 	}
4269 
4270 	/++
4271 	+/
4272 	void setPulseCallback(void delegate() onPulse) {
4273 		assert(onPulse !is null);
4274 		this.onPulse = onPulse;
4275 	}
4276 
4277 	/++
4278 	+/
4279 	void changeTime(int intervalInMilliseconds, bool repeats) {
4280 		this.intervalInMilliseconds = intervalInMilliseconds;
4281 		this.repeats = repeats;
4282 		changeTimeInternal(intervalInMilliseconds, repeats);
4283 	}
4284 
4285 	private void changeTimeInternal(int intervalInMilliseconds, bool repeats) {
4286 		version(Windows)
4287 		{
4288 			LARGE_INTEGER initialTime;
4289 			initialTime.QuadPart = -intervalInMilliseconds * 10000000L / 1000; // Windows wants hnsecs, we have msecs
4290 			if(!SetWaitableTimer(handle, &initialTime, repeats ? intervalInMilliseconds : 0, &timerCallback, cast(void*) cbh, false))
4291 				throw new WindowsApiException("SetWaitableTimer", GetLastError());
4292 		} else version(Emscripten) {
4293 			assert(0);
4294 		} else version(linux) {
4295 			import core.sys.linux.timerfd;
4296 
4297 			itimerspec value = makeItimerspec(intervalInMilliseconds, repeats);
4298 			if(timerfd_settime(fd, 0, &value, null) == -1) {
4299 				throw new ErrnoApiException("couldn't change pulse timer", errno);
4300 			}
4301 		} else {
4302 			throw new NotYetImplementedException();
4303 		}
4304 		// FIXME: freebsd 12 has timer_fd and netbsd 10 too
4305 	}
4306 
4307 	/++
4308 	+/
4309 	void pause() {
4310 		// FIXME this kinda makes little sense tbh
4311 		// when it restarts, it won't be on the same rhythm as it was at first...
4312 		changeTimeInternal(0, false);
4313 	}
4314 
4315 	/++
4316 	+/
4317 	void unpause() {
4318 		changeTimeInternal(this.intervalInMilliseconds, this.repeats);
4319 	}
4320 
4321 	/++
4322 	+/
4323 	void cancel() {
4324 		version(Windows)
4325 			CancelWaitableTimer(handle);
4326 		else
4327 			changeTime(0, false);
4328 	}
4329 
4330 
4331 	/++
4332 		Create a timer with a callback when it triggers.
4333 	+/
4334 	this(int intervalInMilliseconds, void delegate() onPulse, bool repeats = true) @trusted {
4335 		assert(onPulse !is null);
4336 
4337 		initialize();
4338 		setPulseCallback(onPulse);
4339 		changeTime(intervalInMilliseconds, repeats);
4340 	}
4341 
4342 	/++
4343 		Sets a one-of timer that happens some time after the given timestamp, then destroys itself
4344 	+/
4345 	this(SimplifiedUtcTimestamp when, void delegate() onTimeArrived) {
4346 		import core.stdc.time;
4347 		auto ts = when.toUnixTime;
4348 		auto now = time(null);
4349 		if(ts <= now) {
4350 			this(false);
4351 			onTimeArrived();
4352 		} else {
4353 			// FIXME: should use the OS facilities to set the actual time on the real time clock
4354 			auto dis = this;
4355 			this(cast(int)(ts - now) * 1000, () {
4356 				onTimeArrived();
4357 				dis.cancel();
4358 				dis.dispose();
4359 			}, false);
4360 		}
4361 	}
4362 
4363 	version(Windows) {} else {
4364 		ICoreEventLoop.UnregisterToken unregisterToken;
4365 	}
4366 
4367 	// just cuz I sometimes call it this.
4368 	alias dispose = destroy;
4369 
4370 	/++
4371 		Stop and destroy the timer object.
4372 
4373 		You should not use it again after destroying it.
4374 	+/
4375 	void destroy() {
4376 		version(Windows) {
4377 			cbh.release();
4378 		} else {
4379 			unregisterToken.unregister();
4380 		}
4381 
4382 		version(Windows) {
4383 			staticDestroy(handle);
4384 			handle = null;
4385 		} else version(linux) {
4386 			staticDestroy(fd);
4387 			fd = -1;
4388 		} else throw new NotYetImplementedException();
4389 	}
4390 
4391 	~this() {
4392 		version(Windows) {} else
4393 			cleanupQueue.queue!unregister(unregisterToken);
4394 		version(Windows) { if(handle)
4395 			cleanupQueue.queue!staticDestroy(handle);
4396 		} else version(linux) { if(fd != -1)
4397 			cleanupQueue.queue!staticDestroy(fd);
4398 		}
4399 	}
4400 
4401 
4402 	private:
4403 
4404 	version(Windows)
4405 	static void staticDestroy(HANDLE handle) {
4406 		if(handle) {
4407 			// KillTimer(null, handle);
4408 			CancelWaitableTimer(cast(void*)handle);
4409 			CloseHandle(handle);
4410 		}
4411 	}
4412 	else version(linux)
4413 	static void staticDestroy(int fd) @system {
4414 		if(fd != -1) {
4415 			import unix = core.sys.posix.unistd;
4416 
4417 			unix.close(fd);
4418 		}
4419 	}
4420 
4421 	version(Windows) {} else
4422 	static void unregister(arsd.core.ICoreEventLoop.UnregisterToken urt) {
4423 		urt.unregister();
4424 	}
4425 
4426 
4427 	void delegate() onPulse;
4428 	int intervalInMilliseconds;
4429 	bool repeats;
4430 
4431 	int lastEventLoopRoundTriggered;
4432 
4433 	version(linux) {
4434 		static auto makeItimerspec(int intervalInMilliseconds, bool repeats) {
4435 			import core.sys.linux.timerfd;
4436 
4437 			itimerspec value;
4438 			value.it_value.tv_sec = cast(int) (intervalInMilliseconds / 1000);
4439 			value.it_value.tv_nsec = (intervalInMilliseconds % 1000) * 1000_000;
4440 
4441 			if(repeats) {
4442 				value.it_interval.tv_sec = cast(int) (intervalInMilliseconds / 1000);
4443 				value.it_interval.tv_nsec = (intervalInMilliseconds % 1000) * 1000_000;
4444 			}
4445 
4446 			return value;
4447 		}
4448 	}
4449 
4450 	void trigger() {
4451 		version(linux) {
4452 			import unix = core.sys.posix.unistd;
4453 			long val;
4454 			unix.read(fd, &val, val.sizeof); // gotta clear the pipe
4455 		} else version(Windows) {
4456 			if(this.lastEventLoopRoundTriggered == eventLoopRound)
4457 				return; // never try to actually run faster than the event loop
4458 			lastEventLoopRoundTriggered = eventLoopRound;
4459 		} else throw new NotYetImplementedException();
4460 
4461 		if(onPulse)
4462 			onPulse();
4463 	}
4464 
4465 	version(Windows)
4466 		extern(Windows)
4467 		//static void timerCallback(HWND, UINT, UINT_PTR timer, DWORD dwTime) nothrow {
4468 		static void timerCallback(void* timer, DWORD lowTime, DWORD hiTime) nothrow {
4469 			auto cbh = cast(CallbackHelper) timer;
4470 			try
4471 				cbh.call();
4472 			catch(Throwable e) { sdpy_abort(e); assert(0); }
4473 		}
4474 
4475 	version(Windows) {
4476 		HANDLE handle;
4477 		CallbackHelper cbh;
4478 	} else version(linux) {
4479 		int fd = -1;
4480 	} else version(OSXCocoa) {
4481 	} else static assert(0, "timer not supported");
4482 }
4483 
4484 version(Windows)
4485 	private void sdpy_abort(Throwable e) nothrow {
4486 		try
4487 			MessageBoxA(null, (e.toString() ~ "\0").ptr, "Exception caught in WndProc", 0);
4488 		catch(Exception e)
4489 			MessageBoxA(null, "Exception.toString threw too!", "Exception caught in WndProc", 0);
4490 		ExitProcess(1);
4491 	}
4492 
4493 
4494 private int eventLoopRound = -1; // so things that assume 0 still work eg lastEventLoopRoundTriggered
4495 
4496 
4497 
4498 /++
4499 	For functions that give you an unknown address, you can use this to hold it.
4500 
4501 	Can get:
4502 		ip4
4503 		ip6
4504 		unix
4505 		abstract_
4506 
4507 		name lookup for connect (stream or dgram)
4508 			request canonical name?
4509 
4510 		interface lookup for bind (stream or dgram)
4511 +/
4512 version(HasSocket) struct SocketAddress {
4513 	import core.sys.posix.netdb;
4514 
4515 	/++
4516 		Provides the set of addresses to listen on all supported protocols on the machine for the given interfaces. `localhost` only listens on the loopback interface, whereas `allInterfaces` will listen on loopback as well as the others on the system (meaning it may be publicly exposed to the internet).
4517 
4518 		If you provide a buffer, I recommend using one of length two, so `SocketAddress[2]`, since this usually provides one address for ipv4 and one for ipv6.
4519 	+/
4520 	static SocketAddress[] localhost(ushort port, return UserProvidedBuffer!SocketAddress buffer = null) {
4521 		buffer.append(ip6("::1", port));
4522 		buffer.append(ip4("127.0.0.1", port));
4523 		return buffer.slice;
4524 	}
4525 
4526 	/// ditto
4527 	static SocketAddress[] allInterfaces(ushort port, return UserProvidedBuffer!SocketAddress buffer = null) {
4528 		char[16] str;
4529 		return allInterfaces(intToString(port, str[]), buffer);
4530 	}
4531 
4532 	/// ditto
4533 	static SocketAddress[] allInterfaces(scope const char[] serviceOrPort, return UserProvidedBuffer!SocketAddress buffer = null) {
4534 		addrinfo hints;
4535 		hints.ai_flags = AI_PASSIVE;
4536 		hints.ai_socktype = SOCK_STREAM; // just to filter it down a little tbh
4537 		return get(null, serviceOrPort, &hints, buffer);
4538 	}
4539 
4540 	/++
4541 		Returns a single address object for the given protocol and parameters.
4542 
4543 		You probably should generally prefer [get], [localhost], or [allInterfaces] to have more flexible code.
4544 	+/
4545 	static SocketAddress ip4(scope const char[] address, ushort port, bool forListening = false) {
4546 		return getSingleAddress(AF_INET, AI_NUMERICHOST | (forListening ? AI_PASSIVE : 0), address, port);
4547 	}
4548 
4549 	/// ditto
4550 	static SocketAddress ip4(ushort port) {
4551 		return ip4(null, port, true);
4552 	}
4553 
4554 	/// ditto
4555 	static SocketAddress ip6(scope const char[] address, ushort port, bool forListening = false) {
4556 		return getSingleAddress(AF_INET6, AI_NUMERICHOST | (forListening ? AI_PASSIVE : 0), address, port);
4557 	}
4558 
4559 	/// ditto
4560 	static SocketAddress ip6(ushort port) {
4561 		return ip6(null, port, true);
4562 	}
4563 
4564 	/// ditto
4565 	static SocketAddress unix(scope const char[] path) {
4566 		// FIXME
4567 		SocketAddress addr;
4568 		return addr;
4569 	}
4570 
4571 	/// ditto
4572 	static SocketAddress abstract_(scope const char[] path) {
4573 		char[190] buffer = void;
4574 		buffer[0] = 0;
4575 		buffer[1 .. path.length] = path[];
4576 		return unix(buffer[0 .. 1 + path.length]);
4577 	}
4578 
4579 	private static SocketAddress getSingleAddress(int family, int flags, scope const char[] address, ushort port) {
4580 		addrinfo hints;
4581 		hints.ai_family = family;
4582 		hints.ai_flags = flags;
4583 
4584 		char[16] portBuffer;
4585 		char[] portString = intToString(port, portBuffer[]);
4586 
4587 		SocketAddress[1] addr;
4588 		auto res = get(address, portString, &hints, provideBuffer(addr[]));
4589 		if(res.length == 0)
4590 			throw ArsdException!"bad address"(address.idup, port);
4591 		return res[0];
4592 	}
4593 
4594 	/++
4595 		Calls `getaddrinfo` and returns the array of results. It will populate the data into the buffer you provide, if you provide one, otherwise it will allocate its own.
4596 	+/
4597 	static SocketAddress[] get(scope const char[] nodeName, scope const char[] serviceOrPort, addrinfo* hints = null, return UserProvidedBuffer!SocketAddress buffer = null, scope bool delegate(scope addrinfo* ai) filter = null) @trusted {
4598 		addrinfo* res;
4599 		CharzBuffer node = nodeName;
4600 		CharzBuffer service = serviceOrPort;
4601 		auto ret = getaddrinfo(nodeName is null ? null : node.ptr, serviceOrPort is null ? null : service.ptr, hints, &res);
4602 		if(ret == 0) {
4603 			auto current = res;
4604 			while(current) {
4605 				if(filter is null || filter(current)) {
4606 					SocketAddress addr;
4607 					addr.addrlen = cast(socklen_t) current.ai_addrlen;
4608 					switch(current.ai_family) {
4609 						case AF_INET:
4610 							addr.in4 = * cast(sockaddr_in*) current.ai_addr;
4611 							break;
4612 						case AF_INET6:
4613 							addr.in6 = * cast(sockaddr_in6*) current.ai_addr;
4614 							break;
4615 						case AF_UNIX:
4616 							addr.unix_address = * cast(sockaddr_un*) current.ai_addr;
4617 							break;
4618 						default:
4619 							// skip
4620 					}
4621 
4622 					if(!buffer.append(addr))
4623 						break;
4624 				}
4625 
4626 				current = current.ai_next;
4627 			}
4628 
4629 			freeaddrinfo(res);
4630 		} else {
4631 			version(Windows) {
4632 				throw new WindowsApiException("getaddrinfo", ret);
4633 			} else {
4634 				const char* error = gai_strerror(ret);
4635 			}
4636 		}
4637 
4638 		return buffer.slice;
4639 	}
4640 
4641 	/++
4642 		Returns a string representation of the address that identifies it in a custom format.
4643 
4644 		$(LIST
4645 			* Unix domain socket addresses are their path prefixed with "unix:", unless they are in the abstract namespace, in which case it is prefixed with "abstract:" and the zero is trimmed out. For example, "unix:/tmp/pipe".
4646 
4647 			* IPv4 addresses are written in dotted decimal followed by a colon and the port number. For example, "127.0.0.1:8080".
4648 
4649 			* IPv6 addresses are written in colon separated hex format, but enclosed in brackets, then followed by the colon and port number. For example, "[::1]:8080".
4650 		)
4651 	+/
4652 	string toString() const @trusted {
4653 		char[200] buffer;
4654 		switch(address.sa_family) {
4655 			case AF_INET:
4656 				auto writable = stringz(inet_ntop(address.sa_family, &in4.sin_addr, buffer.ptr, buffer.length));
4657 				auto it = writable.borrow;
4658 				buffer[it.length] = ':';
4659 				auto numbers = intToString(port, buffer[it.length + 1 .. $]);
4660 				return buffer[0 .. it.length + 1 + numbers.length].idup;
4661 			case AF_INET6:
4662 				buffer[0] = '[';
4663 				auto writable = stringz(inet_ntop(address.sa_family, &in6.sin6_addr, buffer.ptr + 1, buffer.length - 1));
4664 				auto it = writable.borrow;
4665 				buffer[it.length + 1] = ']';
4666 				buffer[it.length + 2] = ':';
4667 				auto numbers = intToString(port, buffer[it.length + 3 .. $]);
4668 				return buffer[0 .. it.length + 3 + numbers.length].idup;
4669 			case AF_UNIX:
4670 				// FIXME: it might be abstract in which case stringz is wrong!!!!!
4671 				auto writable = stringz(cast(char*) unix_address.sun_path.ptr).borrow;
4672 				if(writable.length == 0)
4673 					return "unix:";
4674 				string prefix = writable[0] == 0 ? "abstract:" : "unix:";
4675 				buffer[0 .. prefix.length] = prefix[];
4676 				buffer[prefix.length .. prefix.length + writable.length] = writable[writable[0] == 0 ? 1 : 0 .. $];
4677 				return buffer.idup;
4678 			case AF_UNSPEC:
4679 				return "<unspecified address>";
4680 			default:
4681 				return "<unsupported address>"; // FIXME
4682 		}
4683 	}
4684 
4685 	ushort port() const @trusted {
4686 		switch(address.sa_family) {
4687 			case AF_INET:
4688 				return ntohs(in4.sin_port);
4689 			case AF_INET6:
4690 				return ntohs(in6.sin6_port);
4691 			default:
4692 				return 0;
4693 		}
4694 	}
4695 
4696 	/+
4697 	@safe unittest {
4698 		SocketAddress[4] buffer;
4699 		foreach(addr; SocketAddress.get("arsdnet.net", "http", null, provideBuffer(buffer[])))
4700 			writeln(addr.toString());
4701 	}
4702 	+/
4703 
4704 	/+
4705 	unittest {
4706 		// writeln(SocketAddress.ip4(null, 4444, true));
4707 		// writeln(SocketAddress.ip4("400.3.2.1", 4444));
4708 		// writeln(SocketAddress.ip4("bar", 4444));
4709 		foreach(addr; localhost(4444))
4710 			writeln(addr.toString());
4711 	}
4712 	+/
4713 
4714 	socklen_t addrlen = typeof(this).sizeof - socklen_t.sizeof; // the size of the union below
4715 
4716 	union {
4717 		sockaddr address;
4718 
4719 		sockaddr_storage storage;
4720 
4721 		sockaddr_in in4;
4722 		sockaddr_in6 in6;
4723 
4724 		sockaddr_un unix_address;
4725 	}
4726 
4727 	/+
4728 	this(string node, string serviceOrPort, int family = 0) {
4729 		// need to populate the approrpiate address and the length and make sure you set sa_family
4730 	}
4731 	+/
4732 
4733 	int domain() {
4734 		return address.sa_family;
4735 	}
4736 	sockaddr* rawAddr() return {
4737 		return &address;
4738 	}
4739 	socklen_t rawAddrLength() {
4740 		return addrlen;
4741 	}
4742 
4743 	// FIXME it is AF_BLUETOOTH
4744 	// see: https://people.csail.mit.edu/albert/bluez-intro/x79.html
4745 	// see: https://learn.microsoft.com/en-us/windows/win32/Bluetooth/bluetooth-programming-with-windows-sockets
4746 }
4747 
4748 private version(Windows) {
4749 	struct sockaddr_un {
4750 		ushort sun_family;
4751 		char[108] sun_path;
4752 	}
4753 }
4754 
4755 version(HasFile) class AsyncSocket : AsyncFile {
4756 	// otherwise: accept, bind, connect, shutdown, close.
4757 
4758 	static auto lastError() {
4759 		version(Windows)
4760 			return WSAGetLastError();
4761 		else
4762 			return errno;
4763 	}
4764 
4765 	static bool wouldHaveBlocked() {
4766 		auto error = lastError;
4767 		version(Windows) {
4768 			return error == WSAEWOULDBLOCK || error == WSAETIMEDOUT;
4769 		} else {
4770 			return error == EAGAIN || error == EWOULDBLOCK;
4771 		}
4772 	}
4773 
4774 	version(Windows)
4775 		enum INVALID = INVALID_SOCKET;
4776 	else
4777 		enum INVALID = -1;
4778 
4779 	// type is mostly SOCK_STREAM or SOCK_DGRAM
4780 	/++
4781 		Creates a socket compatible with the given address. It does not actually connect or bind, nor store the address. You will want to pass it again to those functions:
4782 
4783 		---
4784 		auto socket = new Socket(address, Socket.Type.Stream);
4785 		socket.connect(address).waitForCompletion();
4786 		---
4787 	+/
4788 	this(SocketAddress address, int type, int protocol = 0) {
4789 		// need to look up these values for linux
4790 		// type |= SOCK_NONBLOCK | SOCK_CLOEXEC;
4791 
4792 		handle_ = socket(address.domain(), type, protocol);
4793 		if(handle == INVALID)
4794 			throw new SystemApiException("socket", lastError());
4795 
4796 		super(cast(NativeFileHandle) handle); // I think that cast is ok on Windows... i think
4797 
4798 		version(Posix) {
4799 			makeNonBlocking(handle);
4800 			setCloExec(handle);
4801 		}
4802 
4803 		if(address.domain == AF_INET6) {
4804 			int opt = 1;
4805 			setsockopt(handle, IPPROTO_IPV6 /*SOL_IPV6*/, IPV6_V6ONLY, &opt, opt.sizeof);
4806 		}
4807 
4808 		// FIXME: chekc for broadcast
4809 
4810 		// FIXME: REUSEADDR ?
4811 
4812 		// FIXME: also set NO_DELAY prolly
4813 		// int opt = 1;
4814 		// setsockopt(handle, IPPROTO_TCP, TCP_NODELAY, &opt, opt.sizeof);
4815 	}
4816 
4817 	/++
4818 		Enabling NODELAY can give latency improvements if you are managing buffers on your end
4819 	+/
4820 	void setNoDelay(bool enabled) {
4821 
4822 	}
4823 
4824 	/++
4825 
4826 		`allowQuickRestart` will set the SO_REUSEADDR on unix and SO_DONTLINGER on Windows,
4827 		allowing the application to be quickly restarted despite there still potentially being
4828 		pending data in the tcp stack.
4829 
4830 		See https://stackoverflow.com/questions/3229860/what-is-the-meaning-of-so-reuseaddr-setsockopt-option-linux for more information.
4831 
4832 		If you already set your appropriate socket options or value correctness and reliability of the network stream over restart speed, leave this at the default `false`.
4833 	+/
4834 	void bind(SocketAddress address, bool allowQuickRestart = false) {
4835 		if(allowQuickRestart) {
4836 			// FIXME
4837 		}
4838 
4839 		auto ret = .bind(handle, address.rawAddr, address.rawAddrLength);
4840 		if(ret == -1)
4841 			throw new SystemApiException("bind", lastError);
4842 	}
4843 
4844 	/++
4845 		You must call [bind] before this.
4846 
4847 		The backlog should be set to a value where your application can reliably catch up on the backlog in a reasonable amount of time under average load. It is meant to smooth over short duration bursts and making it too big will leave clients hanging - which might cause them to try to reconnect, thinking things got lost in transit, adding to your impossible backlog.
4848 
4849 		I personally tend to set this to be two per worker thread unless I have actual real world measurements saying to do something else. It is a bit arbitrary and not based on legitimate reasoning, it just seems to work for me (perhaps just because it has never really been put to the test).
4850 	+/
4851 	void listen(int backlog) {
4852 		auto ret = .listen(handle, backlog);
4853 		if(ret == -1)
4854 			throw new SystemApiException("listen", lastError);
4855 	}
4856 
4857 	/++
4858 	+/
4859 	void shutdown(int how) {
4860 		auto ret = .shutdown(handle, how);
4861 		if(ret == -1)
4862 			throw new SystemApiException("shutdown", lastError);
4863 	}
4864 
4865 	/++
4866 	+/
4867 	override void close() {
4868 		version(Windows)
4869 			closesocket(handle);
4870 		else
4871 			.close(handle);
4872 		handle_ = -1;
4873 	}
4874 
4875 	/++
4876 		You can also construct your own request externally to control the memory more.
4877 	+/
4878 	AsyncConnectRequest connect(SocketAddress address, ubyte[] bufferToSend = null) {
4879 		return new AsyncConnectRequest(this, address, bufferToSend);
4880 	}
4881 
4882 	/++
4883 		You can also construct your own request externally to control the memory more.
4884 	+/
4885 	AsyncAcceptRequest accept() {
4886 		return new AsyncAcceptRequest(this);
4887 	}
4888 
4889 	// note that send is just sendto w/ a null address
4890 	// and receive is just receivefrom w/ a null address
4891 	/++
4892 		You can also construct your own request externally to control the memory more.
4893 	+/
4894 	AsyncSendRequest send(const(ubyte)[] buffer, int flags = 0) {
4895 		return new AsyncSendRequest(this, buffer, null, flags);
4896 	}
4897 
4898 	/++
4899 		You can also construct your own request externally to control the memory more.
4900 	+/
4901 	AsyncReceiveRequest receive(ubyte[] buffer, int flags = 0) {
4902 		return new AsyncReceiveRequest(this, buffer, null, flags);
4903 	}
4904 
4905 	/++
4906 		You can also construct your own request externally to control the memory more.
4907 	+/
4908 	AsyncSendRequest sendTo(const(ubyte)[] buffer, SocketAddress* address, int flags = 0) {
4909 		return new AsyncSendRequest(this, buffer, address, flags);
4910 	}
4911 	/++
4912 		You can also construct your own request externally to control the memory more.
4913 	+/
4914 	AsyncReceiveRequest receiveFrom(ubyte[] buffer, SocketAddress* address, int flags = 0) {
4915 		return new AsyncReceiveRequest(this, buffer, address, flags);
4916 	}
4917 
4918 	/++
4919 	+/
4920 	SocketAddress localAddress() {
4921 		SocketAddress addr;
4922 		getsockname(handle, &addr.address, &addr.addrlen);
4923 		return addr;
4924 	}
4925 	/++
4926 	+/
4927 	SocketAddress peerAddress() {
4928 		SocketAddress addr;
4929 		getpeername(handle, &addr.address, &addr.addrlen);
4930 		return addr;
4931 	}
4932 
4933 	// for unix sockets on unix only: send/receive fd, get peer creds
4934 
4935 	/++
4936 
4937 	+/
4938 	final NativeSocketHandle handle() {
4939 		return handle_;
4940 	}
4941 
4942 	private NativeSocketHandle handle_;
4943 }
4944 
4945 /++
4946 	Initiates a connection request and optionally sends initial data as soon as possible.
4947 
4948 	Calls `ConnectEx` on Windows and emulates it on other systems.
4949 
4950 	The entire buffer is sent before the operation is considered complete.
4951 
4952 	NOT IMPLEMENTED / NOT STABLE
4953 +/
4954 version(HasSocket) class AsyncConnectRequest : AsyncOperationRequest {
4955 	// FIXME: i should take a list of addresses and take the first one that succeeds, so a getaddrinfo can be sent straight in.
4956 	this(AsyncSocket socket, SocketAddress address, ubyte[] dataToWrite) {
4957 
4958 	}
4959 
4960 	override void start() {}
4961 	override void cancel() {}
4962 	override bool isComplete() { return true; }
4963 	override AsyncConnectResponse waitForCompletion() { assert(0); }
4964 }
4965 /++
4966 +/
4967 version(HasSocket) class AsyncConnectResponse : AsyncOperationResponse {
4968 	const SystemErrorCode errorCode;
4969 
4970 	this(SystemErrorCode errorCode) {
4971 		this.errorCode = errorCode;
4972 	}
4973 
4974 	override bool wasSuccessful() {
4975 		return errorCode.wasSuccessful;
4976 	}
4977 
4978 }
4979 
4980 // FIXME: TransmitFile/sendfile support
4981 
4982 /++
4983 	Calls `AcceptEx` on Windows and emulates it on other systems.
4984 
4985 	NOT IMPLEMENTED / NOT STABLE
4986 +/
4987 version(HasSocket) class AsyncAcceptRequest : AsyncOperationRequest {
4988 	AsyncSocket socket;
4989 
4990 	override void start() {}
4991 	override void cancel() {}
4992 	override bool isComplete() { return true; }
4993 	override AsyncConnectResponse waitForCompletion() { assert(0); }
4994 
4995 
4996 	struct LowLevelOperation {
4997 		AsyncSocket file;
4998 		ubyte[] buffer;
4999 		SocketAddress* address;
5000 
5001 		this(typeof(this.tupleof) args) {
5002 			this.tupleof = args;
5003 		}
5004 
5005 		version(Windows) {
5006 			auto opCall(OVERLAPPED* overlapped, LPOVERLAPPED_COMPLETION_ROUTINE ocr) {
5007 				WSABUF buf;
5008 				buf.len = cast(int) buffer.length;
5009 				buf.buf = cast(typeof(buf.buf)) buffer.ptr;
5010 
5011 				uint flags;
5012 
5013 				if(address is null)
5014 					return WSARecv(file.handle, &buf, 1, null, &flags, overlapped, ocr);
5015 				else {
5016 					return WSARecvFrom(file.handle, &buf, 1, null, &flags, &(address.address), &(address.addrlen), overlapped, ocr);
5017 				}
5018 			}
5019 		} else {
5020 			auto opCall() {
5021 				int flags;
5022 				if(address is null)
5023 					return core.sys.posix.sys.socket.recv(file.handle, buffer.ptr, buffer.length, flags);
5024 				else
5025 					return core.sys.posix.sys.socket.recvfrom(file.handle, buffer.ptr, buffer.length, flags, &(address.address), &(address.addrlen));
5026 			}
5027 		}
5028 
5029 		string errorString() {
5030 			return "Receive";
5031 		}
5032 	}
5033 	mixin OverlappedIoRequest!(AsyncAcceptResponse, LowLevelOperation);
5034 
5035 	this(AsyncSocket socket, ubyte[] buffer = null, SocketAddress* address = null) {
5036 		llo = LowLevelOperation(socket, buffer, address);
5037 		this.response = typeof(this.response).defaultConstructed;
5038 	}
5039 
5040 	// can also look up the local address
5041 }
5042 /++
5043 +/
5044 version(HasSocket) class AsyncAcceptResponse : AsyncOperationResponse {
5045 	AsyncSocket newSocket;
5046 	const SystemErrorCode errorCode;
5047 
5048 	this(SystemErrorCode errorCode, ubyte[] buffer) {
5049 		this.errorCode = errorCode;
5050 	}
5051 
5052 	this(AsyncSocket newSocket, SystemErrorCode errorCode) {
5053 		this.newSocket = newSocket;
5054 		this.errorCode = errorCode;
5055 	}
5056 
5057 	override bool wasSuccessful() {
5058 		return errorCode.wasSuccessful;
5059 	}
5060 }
5061 
5062 /++
5063 +/
5064 version(HasSocket) class AsyncReceiveRequest : AsyncOperationRequest {
5065 	struct LowLevelOperation {
5066 		AsyncSocket file;
5067 		ubyte[] buffer;
5068 		int flags;
5069 		SocketAddress* address;
5070 
5071 		this(typeof(this.tupleof) args) {
5072 			this.tupleof = args;
5073 		}
5074 
5075 		version(Windows) {
5076 			auto opCall(OVERLAPPED* overlapped, LPOVERLAPPED_COMPLETION_ROUTINE ocr) {
5077 				WSABUF buf;
5078 				buf.len = cast(int) buffer.length;
5079 				buf.buf = cast(typeof(buf.buf)) buffer.ptr;
5080 
5081 				uint flags = this.flags;
5082 
5083 				if(address is null)
5084 					return WSARecv(file.handle, &buf, 1, null, &flags, overlapped, ocr);
5085 				else {
5086 					return WSARecvFrom(file.handle, &buf, 1, null, &flags, &(address.address), &(address.addrlen), overlapped, ocr);
5087 				}
5088 			}
5089 		} else {
5090 			auto opCall() {
5091 				if(address is null)
5092 					return core.sys.posix.sys.socket.recv(file.handle, buffer.ptr, buffer.length, flags);
5093 				else
5094 					return core.sys.posix.sys.socket.recvfrom(file.handle, buffer.ptr, buffer.length, flags, &(address.address), &(address.addrlen));
5095 			}
5096 		}
5097 
5098 		string errorString() {
5099 			return "Receive";
5100 		}
5101 	}
5102 	mixin OverlappedIoRequest!(AsyncReceiveResponse, LowLevelOperation);
5103 
5104 	this(AsyncSocket socket, ubyte[] buffer, SocketAddress* address, int flags) {
5105 		llo = LowLevelOperation(socket, buffer, flags, address);
5106 		this.response = typeof(this.response).defaultConstructed;
5107 	}
5108 
5109 }
5110 /++
5111 +/
5112 version(HasSocket) class AsyncReceiveResponse : AsyncOperationResponse {
5113 	const ubyte[] bufferWritten;
5114 	const SystemErrorCode errorCode;
5115 
5116 	this(SystemErrorCode errorCode, const(ubyte)[] bufferWritten) {
5117 		this.errorCode = errorCode;
5118 		this.bufferWritten = bufferWritten;
5119 	}
5120 
5121 	override bool wasSuccessful() {
5122 		return errorCode.wasSuccessful;
5123 	}
5124 }
5125 
5126 /++
5127 +/
5128 version(HasSocket) class AsyncSendRequest : AsyncOperationRequest {
5129 	struct LowLevelOperation {
5130 		AsyncSocket file;
5131 		const(ubyte)[] buffer;
5132 		int flags;
5133 		SocketAddress* address;
5134 
5135 		this(typeof(this.tupleof) args) {
5136 			this.tupleof = args;
5137 		}
5138 
5139 		version(Windows) {
5140 			auto opCall(OVERLAPPED* overlapped, LPOVERLAPPED_COMPLETION_ROUTINE ocr) {
5141 				WSABUF buf;
5142 				buf.len = cast(int) buffer.length;
5143 				buf.buf = cast(typeof(buf.buf)) buffer.ptr;
5144 
5145 				if(address is null)
5146 					return WSASend(file.handle, &buf, 1, null, flags, overlapped, ocr);
5147 				else {
5148 					return WSASendTo(file.handle, &buf, 1, null, flags, address.rawAddr, address.rawAddrLength, overlapped, ocr);
5149 				}
5150 			}
5151 		} else {
5152 			auto opCall() {
5153 				if(address is null)
5154 					return core.sys.posix.sys.socket.send(file.handle, buffer.ptr, buffer.length, flags);
5155 				else
5156 					return core.sys.posix.sys.socket.sendto(file.handle, buffer.ptr, buffer.length, flags, address.rawAddr, address.rawAddrLength);
5157 			}
5158 		}
5159 
5160 		string errorString() {
5161 			return "Send";
5162 		}
5163 	}
5164 	mixin OverlappedIoRequest!(AsyncSendResponse, LowLevelOperation);
5165 
5166 	this(AsyncSocket socket, const(ubyte)[] buffer, SocketAddress* address, int flags) {
5167 		llo = LowLevelOperation(socket, buffer, flags, address);
5168 		this.response = typeof(this.response).defaultConstructed;
5169 	}
5170 }
5171 
5172 /++
5173 +/
5174 version(HasSocket) class AsyncSendResponse : AsyncOperationResponse {
5175 	const ubyte[] bufferWritten;
5176 	const SystemErrorCode errorCode;
5177 
5178 	this(SystemErrorCode errorCode, const(ubyte)[] bufferWritten) {
5179 		this.errorCode = errorCode;
5180 		this.bufferWritten = bufferWritten;
5181 	}
5182 
5183 	override bool wasSuccessful() {
5184 		return errorCode.wasSuccessful;
5185 	}
5186 
5187 }
5188 
5189 /++
5190 	A set of sockets bound and ready to accept connections on worker threads.
5191 
5192 	Depending on the specified address, it can be tcp, tcpv6, unix domain, or all of the above.
5193 
5194 	NOT IMPLEMENTED / NOT STABLE
5195 +/
5196 version(HasSocket) class StreamServer {
5197 	AsyncSocket[] sockets;
5198 
5199 	this(SocketAddress[] listenTo, int backlog = 8) {
5200 		foreach(listen; listenTo) {
5201 			auto socket = new AsyncSocket(listen, SOCK_STREAM);
5202 
5203 			// FIXME: allInterfaces for ipv6 also covers ipv4 so the bind can fail...
5204 			// so we have to permit it to fail w/ address in use if we know we already
5205 			// are listening to ipv6
5206 
5207 			// or there is a setsockopt ipv6 only thing i could set.
5208 
5209 			socket.bind(listen);
5210 			socket.listen(backlog);
5211 			sockets ~= socket;
5212 
5213 			// writeln(socket.localAddress.port);
5214 		}
5215 
5216 		// i have to start accepting on each thread for each socket...
5217 	}
5218 	// when a new connection arrives, it calls your callback
5219 	// can be on a specific thread or on any thread
5220 
5221 
5222 	void start() {
5223 		foreach(socket; sockets) {
5224 			auto request = socket.accept();
5225 			request.start();
5226 		}
5227 	}
5228 }
5229 
5230 /+
5231 unittest {
5232 	auto ss = new StreamServer(SocketAddress.localhost(0));
5233 }
5234 +/
5235 
5236 /++
5237 	A socket bound and ready to use receiveFrom
5238 
5239 	Depending on the address, it can be udp or unix domain.
5240 
5241 	NOT IMPLEMENTED / NOT STABLE
5242 +/
5243 version(HasSocket) class DatagramListener {
5244 	// whenever a udp message arrives, it calls your callback
5245 	// can be on a specific thread or on any thread
5246 
5247 	// UDP is realistically just an async read on the bound socket
5248 	// just it can get the "from" data out and might need the "more in packet" flag
5249 }
5250 
5251 /++
5252 	Just in case I decide to change the implementation some day.
5253 +/
5254 version(HasFile) alias AsyncAnonymousPipe = AsyncFile;
5255 
5256 
5257 // AsyncAnonymousPipe connectNamedPipe(AsyncAnonymousPipe preallocated, string name)
5258 
5259 // unix fifos are considered just non-seekable files and have no special support in the lib; open them as a regular file w/ the async flag.
5260 
5261 // DIRECTORY LISTINGS
5262 	// not async, so if you want that, do it in a helper thread
5263 	// just a convenient function to have (tho phobos has a decent one too, importing it expensive af)
5264 
5265 /++
5266 	Note that the order of items called for your delegate is undefined; if you want it sorted, you'll have to collect and sort yourself. But it *might* be sorted by the OS (on Windows, it almost always is), so consider that when choosing a sorting algorithm.
5267 
5268 	History:
5269 		previously in minigui as a private function. Moved to arsd.core on April 3, 2023
5270 +/
5271 version(HasFile) GetFilesResult getFiles(string directory, scope void delegate(string name, bool isDirectory) dg) {
5272 	// FIXME: my buffers here aren't great lol
5273 
5274 	SavedArgument[1] argsForException() {
5275 		return [
5276 			SavedArgument("directory", LimitedVariant(directory)),
5277 		];
5278 	}
5279 
5280 	version(Windows) {
5281 		WIN32_FIND_DATA data;
5282 		// FIXME: if directory ends with / or \\ ?
5283 		WCharzBuffer search = WCharzBuffer(directory ~ "/*");
5284 		auto handle = FindFirstFileW(search.ptr, &data);
5285 		scope(exit) if(handle !is INVALID_HANDLE_VALUE) FindClose(handle);
5286 		if(handle is INVALID_HANDLE_VALUE) {
5287 			if(GetLastError() == ERROR_FILE_NOT_FOUND)
5288 				return GetFilesResult.fileNotFound;
5289 			throw new WindowsApiException("FindFirstFileW", GetLastError(), argsForException()[]);
5290 		}
5291 
5292 		try_more:
5293 
5294 		string name = makeUtf8StringFromWindowsString(data.cFileName[0 .. findIndexOfZero(data.cFileName[])]);
5295 
5296 		/+
5297   FILETIME ftLastWriteTime;
5298   DWORD    nFileSizeHigh;
5299   DWORD    nFileSizeLow;
5300 
5301   but these not available on linux w/o statting each file!
5302 		+/
5303 
5304 		dg(name, (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? true : false);
5305 
5306 		auto ret = FindNextFileW(handle, &data);
5307 		if(ret == 0) {
5308 			if(GetLastError() == ERROR_NO_MORE_FILES)
5309 				return GetFilesResult.success;
5310 			throw new WindowsApiException("FindNextFileW", GetLastError(), argsForException()[]);
5311 		}
5312 
5313 		goto try_more;
5314 
5315 	} else version(Posix) {
5316 		import core.sys.posix.dirent;
5317 		import core.stdc.errno;
5318 		auto dir = opendir((directory ~ "\0").ptr);
5319 		scope(exit)
5320 			if(dir) closedir(dir);
5321 		if(dir is null)
5322 			throw new ErrnoApiException("opendir", errno, argsForException());
5323 
5324 		auto dirent = readdir(dir);
5325 		if(dirent is null)
5326 			return GetFilesResult.fileNotFound;
5327 
5328 		try_more:
5329 
5330 		string name = dirent.d_name[0 .. findIndexOfZero(dirent.d_name[])].idup;
5331 
5332 		dg(name, dirent.d_type == DT_DIR);
5333 
5334 		dirent = readdir(dir);
5335 		if(dirent is null)
5336 			return GetFilesResult.success;
5337 
5338 		goto try_more;
5339 	} else static assert(0);
5340 }
5341 
5342 /// ditto
5343 enum GetFilesResult {
5344 	success,
5345 	fileNotFound
5346 }
5347 
5348 /++
5349 	This is currently a simplified glob where only the * wildcard in the first or last position gets special treatment or a single * in the middle.
5350 
5351 	More things may be added later to be more like what Phobos supports.
5352 +/
5353 bool matchesFilePattern(scope const(char)[] name, scope const(char)[] pattern) {
5354 	if(pattern.length == 0)
5355 		return false;
5356 	if(pattern == "*")
5357 		return true;
5358 	if(pattern.length > 2 && pattern[0] == '*' && pattern[$-1] == '*') {
5359 		// if the rest of pattern appears in name, it is good
5360 		return name.indexOf(pattern[1 .. $-1]) != -1;
5361 	} else if(pattern[0] == '*') {
5362 		// if the rest of pattern is at end of name, it is good
5363 		return name.endsWith(pattern[1 .. $]);
5364 	} else if(pattern[$-1] == '*') {
5365 		// if the rest of pattern is at start of name, it is good
5366 		return name.startsWith(pattern[0 .. $-1]);
5367 	} else if(pattern.length >= 3) {
5368 		auto idx = pattern.indexOf("*");
5369 		if(idx != -1) {
5370 			auto lhs = pattern[0 .. idx];
5371 			auto rhs = pattern[idx + 1 .. $];
5372 			if(name.length >= lhs.length + rhs.length) {
5373 				return name.startsWith(lhs) && name.endsWith(rhs);
5374 			} else {
5375 				return false;
5376 			}
5377 		}
5378 	}
5379 
5380 	return name == pattern;
5381 }
5382 
5383 unittest {
5384 	assert("test.html".matchesFilePattern("*"));
5385 	assert("test.html".matchesFilePattern("*.html"));
5386 	assert("test.html".matchesFilePattern("*.*"));
5387 	assert("test.html".matchesFilePattern("test.*"));
5388 	assert(!"test.html".matchesFilePattern("pest.*"));
5389 	assert(!"test.html".matchesFilePattern("*.dhtml"));
5390 
5391 	assert("test.html".matchesFilePattern("t*.html"));
5392 	assert(!"test.html".matchesFilePattern("e*.html"));
5393 }
5394 
5395 package(arsd) int indexOf(scope const(char)[] haystack, scope const(char)[] needle) {
5396 	if(haystack.length < needle.length)
5397 		return -1;
5398 	if(haystack == needle)
5399 		return 0;
5400 	foreach(i; 0 .. haystack.length - needle.length + 1)
5401 		if(haystack[i .. i + needle.length] == needle)
5402 			return cast(int) i;
5403 	return -1;
5404 }
5405 
5406 package(arsd) int indexOf(scope const(ubyte)[] haystack, scope const(char)[] needle) {
5407 	return indexOf(cast(const(char)[]) haystack, needle);
5408 }
5409 
5410 unittest {
5411 	assert("foo".indexOf("f") == 0);
5412 	assert("foo".indexOf("o") == 1);
5413 	assert("foo".indexOf("foo") == 0);
5414 	assert("foo".indexOf("oo") == 1);
5415 	assert("foo".indexOf("fo") == 0);
5416 	assert("foo".indexOf("boo") == -1);
5417 	assert("foo".indexOf("food") == -1);
5418 }
5419 
5420 package(arsd) bool endsWith(scope const(char)[] haystack, scope const(char)[] needle) {
5421 	if(needle.length > haystack.length)
5422 		return false;
5423 	return haystack[$ - needle.length .. $] == needle;
5424 }
5425 
5426 unittest {
5427 	assert("foo".endsWith("o"));
5428 	assert("foo".endsWith("oo"));
5429 	assert("foo".endsWith("foo"));
5430 	assert(!"foo".endsWith("food"));
5431 	assert(!"foo".endsWith("d"));
5432 }
5433 
5434 package(arsd) bool startsWith(scope const(char)[] haystack, scope const(char)[] needle) {
5435 	if(needle.length > haystack.length)
5436 		return false;
5437 	return haystack[0 .. needle.length] == needle;
5438 }
5439 
5440 unittest {
5441 	assert("foo".startsWith("f"));
5442 	assert("foo".startsWith("fo"));
5443 	assert("foo".startsWith("foo"));
5444 	assert(!"foo".startsWith("food"));
5445 	assert(!"foo".startsWith("d"));
5446 }
5447 
5448 
5449 // FILE/DIR WATCHES
5450 	// linux does it by name, windows and bsd do it by handle/descriptor
5451 	// dispatches change event to either your thread or maybe the any task` queue.
5452 
5453 /++
5454 	PARTIALLY IMPLEMENTED / NOT STABLE
5455 
5456 +/
5457 class DirectoryWatcher {
5458 	private {
5459 		version(Arsd_core_windows) {
5460 			OVERLAPPED overlapped;
5461 			HANDLE hDirectory;
5462 			ubyte[] buffer;
5463 
5464 			extern(Windows)
5465 			static void overlappedCompletionRoutine(DWORD dwErrorCode, DWORD dwNumberOfBytesTransferred, LPOVERLAPPED lpOverlapped) @system {
5466 				typeof(this) rr = cast(typeof(this)) (cast(void*) lpOverlapped - typeof(this).overlapped.offsetof);
5467 
5468 				// dwErrorCode
5469 				auto response = rr.buffer[0 .. dwNumberOfBytesTransferred];
5470 
5471 				while(response.length) {
5472 					auto fni = cast(FILE_NOTIFY_INFORMATION*) response.ptr;
5473 					auto filename = fni.FileName[0 .. fni.FileNameLength];
5474 
5475 					if(fni.NextEntryOffset)
5476 						response = response[fni.NextEntryOffset .. $];
5477 					else
5478 						response = response[$..$];
5479 
5480 					// FIXME: I think I need to pin every overlapped op while it is pending
5481 					// and unpin it when it is returned. GC.addRoot... but i don't wanna do that
5482 					// every op so i guess i should do a refcount scheme similar to the other callback helper.
5483 
5484 					rr.changeHandler(
5485 						FilePath(makeUtf8StringFromWindowsString(filename)), // FIXME: this is a relative path
5486 						ChangeOperation.unknown // FIXME this is fni.Action
5487 					);
5488 				}
5489 
5490 				rr.requestRead();
5491 			}
5492 
5493 			void requestRead() {
5494 				DWORD ignored;
5495 				if(!ReadDirectoryChangesW(
5496 					hDirectory,
5497 					buffer.ptr,
5498 					cast(int) buffer.length,
5499 					recursive,
5500 					FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_FILE_NAME,
5501 					&ignored,
5502 					&overlapped,
5503 					&overlappedCompletionRoutine
5504 				)) {
5505 					auto error = GetLastError();
5506 					/+
5507 					if(error == ERROR_IO_PENDING) {
5508 						// not expected here, the docs say it returns true when queued
5509 					}
5510 					+/
5511 
5512 					throw new SystemApiException("ReadDirectoryChangesW", error);
5513 				}
5514 			}
5515 		} else version(Arsd_core_epoll) {
5516 			static int inotifyfd = -1; // this is TLS since it is associated with the thread's event loop
5517 			static ICoreEventLoop.UnregisterToken inotifyToken;
5518 			static CallbackHelper inotifycb;
5519 			static DirectoryWatcher[int] watchMappings;
5520 
5521 			static ~this() {
5522 				if(inotifyfd != -1) {
5523 					close(inotifyfd);
5524 					inotifyfd = -1;
5525 				}
5526 			}
5527 
5528 			import core.sys.linux.sys.inotify;
5529 
5530 			int watchId = -1;
5531 
5532 			static void inotifyReady() {
5533 				// read from it
5534 				ubyte[256 /* NAME_MAX + 1 */ + inotify_event.sizeof] sbuffer;
5535 
5536 				auto ret = read(inotifyfd, sbuffer.ptr, sbuffer.length);
5537 				if(ret == -1) {
5538 					auto errno = errno;
5539 					if(errno == EAGAIN || errno == EWOULDBLOCK)
5540 						return;
5541 					throw new SystemApiException("read inotify", errno);
5542 				} else if(ret == 0) {
5543 					assert(0, "I don't think this is ever supposed to happen");
5544 				}
5545 
5546 				auto buffer = sbuffer[0 .. ret];
5547 
5548 				while(buffer.length > 0) {
5549 					inotify_event* event = cast(inotify_event*) buffer.ptr;
5550 					buffer = buffer[inotify_event.sizeof .. $];
5551 					char[] filename = cast(char[]) buffer[0 .. event.len];
5552 					buffer = buffer[event.len .. $];
5553 
5554 					// note that filename is padded with zeroes, so it is actually a stringz
5555 
5556 					if(auto obj = event.wd in watchMappings) {
5557 						(*obj).changeHandler(
5558 							FilePath(stringz(filename.ptr).borrow.idup), // FIXME: this is a relative path
5559 							ChangeOperation.unknown // FIXME
5560 						);
5561 					} else {
5562 						// it has probably already been removed
5563 					}
5564 				}
5565 			}
5566 		} else version(Arsd_core_kqueue) {
5567 			int fd;
5568 			CallbackHelper cb;
5569 		}
5570 
5571 		FilePath path;
5572 		string globPattern;
5573 		bool recursive;
5574 		void delegate(FilePath filename, ChangeOperation op) changeHandler;
5575 	}
5576 
5577 	enum ChangeOperation {
5578 		unknown,
5579 		deleted, // NOTE_DELETE, IN_DELETE, FILE_NOTIFY_CHANGE_FILE_NAME
5580 		written, // NOTE_WRITE / NOTE_EXTEND / NOTE_TRUNCATE, IN_MODIFY, FILE_NOTIFY_CHANGE_LAST_WRITE / FILE_NOTIFY_CHANGE_SIZE
5581 		renamed, // NOTE_RENAME, the moved from/to in linux, FILE_NOTIFY_CHANGE_FILE_NAME
5582 		metadataChanged // NOTE_ATTRIB, IN_ATTRIB, FILE_NOTIFY_CHANGE_ATTRIBUTES
5583 
5584 		// there is a NOTE_OPEN on freebsd 13, and the access change on Windows. and an open thing on linux. so maybe i can do note open/note_read too.
5585 	}
5586 
5587 	/+
5588 		Windows and Linux work best when you watch directories. The operating system tells you the name of files as they change.
5589 
5590 		BSD doesn't support this. You can only get names and reports when a file is modified by watching specific files. AS such, when you watch a directory on those systems, your delegate will be called with a null path. Cross-platform applications should check for this and not assume the name is always usable.
5591 
5592 		inotify is kinda clearly the best of the bunch, with Windows in second place, and kqueue dead last.
5593 
5594 
5595 		If path to watch is a directory, it signals when a file inside the directory (only one layer deep) is created or modified. This is the most efficient on Windows and Linux.
5596 
5597 		If a path is a file, it only signals when that specific file is written. This is most efficient on BSD.
5598 
5599 
5600 		The delegate is called when something happens. Note that the path modified may not be accurate on all systems when you are watching a directory.
5601 	+/
5602 
5603 	/++
5604 		Watches a directory and its contents. If the `globPattern` is `null`, it will not attempt to add child items but also will not filter it, meaning you will be left with platform-specific behavior.
5605 
5606 		On Windows, the globPattern is just used to filter events.
5607 
5608 		On Linux, the `recursive` flag, if set, will cause it to add additional OS-level watches for each subdirectory.
5609 
5610 		On BSD, anything other than a null pattern will cause a directory scan to add files to the watch list.
5611 
5612 		For best results, use the most limited thing you need, as watches can get quite involved on the bsd systems.
5613 
5614 		Newly added files and subdirectories may not be automatically added in all cases, meaning if it is added and then subsequently modified, you might miss a notification.
5615 
5616 		If the event queue is too busy, the OS may skip a notification.
5617 
5618 		You should always offer some way for the user to force a refresh and not rely on notifications being present; they are a convenience when they work, not an always reliable method.
5619 	+/
5620 	this(FilePath directoryToWatch, string globPattern, bool recursive, void delegate(FilePath pathModified, ChangeOperation op) dg) {
5621 		this.path = directoryToWatch;
5622 		this.globPattern = globPattern;
5623 		this.recursive = recursive;
5624 		this.changeHandler = dg;
5625 
5626 		version(Arsd_core_windows) {
5627 			WCharzBuffer wname = directoryToWatch.path;
5628 			buffer = new ubyte[](1024);
5629 			hDirectory = CreateFileW(
5630 				wname.ptr,
5631 				GENERIC_READ,
5632 				FILE_SHARE_READ,
5633 				null,
5634 				OPEN_EXISTING,
5635 				FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED | FILE_FLAG_BACKUP_SEMANTICS,
5636 				null
5637 			);
5638 			if(hDirectory == INVALID_HANDLE_VALUE)
5639 				throw new SystemApiException("CreateFileW", GetLastError());
5640 
5641 			requestRead();
5642 		} else version(Arsd_core_epoll) {
5643 			auto el = getThisThreadEventLoop();
5644 
5645 			// no need for sync because it is thread-local
5646 			if(inotifyfd == -1) {
5647 				inotifyfd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
5648 				if(inotifyfd == -1)
5649 					throw new SystemApiException("inotify_init1", errno);
5650 
5651 				inotifycb = new CallbackHelper(&inotifyReady);
5652 				inotifyToken = el.addCallbackOnFdReadable(inotifyfd, inotifycb);
5653 			}
5654 
5655 			uint event_mask = IN_CREATE | IN_MODIFY  | IN_DELETE; // FIXME
5656 			CharzBuffer dtw = directoryToWatch.path;
5657 			auto watchId = inotify_add_watch(inotifyfd, dtw.ptr, event_mask);
5658 			if(watchId < -1)
5659 				throw new SystemApiException("inotify_add_watch", errno, [SavedArgument("path", LimitedVariant(directoryToWatch.path))]);
5660 
5661 			watchMappings[watchId] = this;
5662 
5663 			// FIXME: recursive needs to add child things individually
5664 
5665 		} else version(Arsd_core_kqueue) {
5666 			auto el = cast(CoreEventLoopImplementation) getThisThreadEventLoop();
5667 
5668 			// FIXME: need to scan for globPattern
5669 			// when a new file is added, i'll have to diff my list to detect it and open it too
5670 			// and recursive might need to scan down too.
5671 
5672 			kevent_t ev;
5673 
5674 			import core.sys.posix.fcntl;
5675 			CharzBuffer buffer = CharzBuffer(directoryToWatch.path);
5676 			fd = ErrnoEnforce!open(buffer.ptr, O_RDONLY);
5677 			setCloExec(fd);
5678 
5679 			cb = new CallbackHelper(&triggered);
5680 
5681 			EV_SET(&ev, fd, EVFILT_VNODE, EV_ADD | EV_ENABLE | EV_CLEAR, NOTE_WRITE, 0, cast(void*) cb);
5682 			ErrnoEnforce!kevent(el.kqueuefd, &ev, 1, null, 0, null);
5683 		} else assert(0, "Not yet implemented for this platform");
5684 	}
5685 
5686 	private void triggered() {
5687 		writeln("triggered");
5688 	}
5689 
5690 	void dispose() {
5691 		version(Arsd_core_windows) {
5692 			CloseHandle(hDirectory);
5693 		} else version(Arsd_core_epoll) {
5694 			watchMappings.remove(watchId); // I could also do this on the IN_IGNORE notification but idk
5695 			inotify_rm_watch(inotifyfd, watchId);
5696 		} else version(Arsd_core_kqueue) {
5697 			ErrnoEnforce!close(fd);
5698 			fd = -1;
5699 		}
5700 	}
5701 }
5702 
5703 version(none)
5704 void main() {
5705 
5706 	// auto file = new AsyncFile(FilePath("test.txt"), AsyncFile.OpenMode.writeWithTruncation, AsyncFile.RequirePreexisting.yes);
5707 
5708 	/+
5709 	getFiles("c:/windows\\", (string filename, bool isDirectory) {
5710 		writeln(filename, " ", isDirectory ? "[dir]": "[file]");
5711 	});
5712 	+/
5713 
5714 	auto w = new DirectoryWatcher(FilePath("."), "*", false, (path, op) {
5715 		writeln(path.path);
5716 	});
5717 	getThisThreadEventLoop().run(() => false);
5718 }
5719 
5720 /++
5721 	This starts up a local pipe. If it is already claimed, it just communicates with the existing one through the interface.
5722 +/
5723 class SingleInstanceApplication {
5724 	// FIXME
5725 }
5726 
5727 version(none)
5728 void main() {
5729 
5730 	auto file = new AsyncFile(FilePath("test.txt"), AsyncFile.OpenMode.writeWithTruncation, AsyncFile.RequirePreexisting.yes);
5731 
5732 	auto buffer = cast(ubyte[]) "hello";
5733 	auto wr = new AsyncWriteRequest(file, buffer, 0);
5734 	wr.start();
5735 
5736 	wr.waitForCompletion();
5737 
5738 	file.close();
5739 }
5740 
5741 /++
5742 	Implementation details of some requests. You shouldn't need to know any of this, the interface is all public.
5743 +/
5744 mixin template OverlappedIoRequest(Response, LowLevelOperation) {
5745 	private {
5746 		LowLevelOperation llo;
5747 
5748 		OwnedClass!Response response;
5749 
5750 		version(Windows) {
5751 			OVERLAPPED overlapped;
5752 
5753 			extern(Windows)
5754 			static void overlappedCompletionRoutine(DWORD dwErrorCode, DWORD dwNumberOfBytesTransferred, LPOVERLAPPED lpOverlapped) @system {
5755 				typeof(this) rr = cast(typeof(this)) (cast(void*) lpOverlapped - typeof(this).overlapped.offsetof);
5756 
5757 				rr.response = typeof(rr.response)(SystemErrorCode(dwErrorCode), rr.llo.buffer[0 .. dwNumberOfBytesTransferred]);
5758 				rr.state_ = State.complete;
5759 
5760 				if(rr.oncomplete)
5761 					rr.oncomplete(rr);
5762 
5763 				// FIXME: on complete?
5764 				// this will queue our CallbackHelper and that should be run at the end of the event loop after it is woken up by the APC run
5765 			}
5766 		}
5767 
5768 		version(Posix) {
5769 			ICoreEventLoop.RearmToken eventRegistration;
5770 			CallbackHelper cb;
5771 
5772 			final CallbackHelper getCb() {
5773 				if(cb is null)
5774 					cb = new CallbackHelper(&cbImpl);
5775 				return cb;
5776 			}
5777 
5778 			final void cbImpl() {
5779 				// it is ready to complete, time to do it
5780 				auto ret = llo();
5781 				markCompleted(ret, errno);
5782 			}
5783 
5784 			void markCompleted(long ret, int errno) {
5785 				// maybe i should queue an apc to actually do it, to ensure the event loop has cycled... FIXME
5786 				if(ret == -1)
5787 					response = typeof(response)(SystemErrorCode(errno), null);
5788 				else
5789 					response = typeof(response)(SystemErrorCode(0), llo.buffer[0 .. cast(size_t) ret]);
5790 				state_ = State.complete;
5791 
5792 				if(oncomplete)
5793 					oncomplete(this);
5794 			}
5795 		}
5796 	}
5797 
5798 	enum State {
5799 		unused,
5800 		started,
5801 		inProgress,
5802 		complete
5803 	}
5804 	private State state_;
5805 
5806 	override void start() {
5807 		assert(state_ == State.unused);
5808 
5809 		state_ = State.started;
5810 
5811 		version(Windows) {
5812 			if(llo(&overlapped, &overlappedCompletionRoutine)) {
5813 				// all good, though GetLastError() might have some informative info
5814 				//writeln(GetLastError());
5815 			} else {
5816 				// operation failed, the operation is always ReadFileEx or WriteFileEx so it won't give the io pending thing here
5817 				// should i issue error async? idk
5818 				state_ = State.complete;
5819 				throw new SystemApiException(llo.errorString(), GetLastError());
5820 			}
5821 
5822 			// ReadFileEx always queues, even if it completed synchronously. I *could* check the get overlapped result and sleepex here but i'm prolly better off just letting the event loop do its thing anyway.
5823 		} else version(Posix) {
5824 
5825 			// first try to just do it
5826 			auto ret = llo();
5827 
5828 			auto errno = errno;
5829 			if(ret == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { // unable to complete right now, register and try when it is ready
5830 				if(eventRegistration is typeof(eventRegistration).init)
5831 					eventRegistration = getThisThreadEventLoop().addCallbackOnFdReadableOneShot(this.llo.file.handle, this.getCb);
5832 				else
5833 					eventRegistration.rearm();
5834 			} else {
5835 				// i could set errors sync or async and since it couldn't even start, i think a sync exception is the right way
5836 				if(ret == -1)
5837 					throw new SystemApiException(llo.errorString(), errno);
5838 				markCompleted(ret, errno); // it completed synchronously (if it is an error nor not is handled by the completion handler)
5839 			}
5840 		}
5841 	}
5842 
5843 	override void cancel() {
5844 		if(state_ == State.complete)
5845 			return; // it has already finished, just leave it alone, no point discarding what is already done
5846 		version(Windows) {
5847 			if(state_ != State.unused)
5848 				Win32Enforce!CancelIoEx(llo.file.AbstractFile.handle, &overlapped);
5849 			// Windows will notify us when the cancellation is complete, so we need to wait for that before updating the state
5850 		} else version(Posix) {
5851 			if(state_ != State.unused)
5852 				eventRegistration.unregister();
5853 			markCompleted(-1, ECANCELED);
5854 		}
5855 	}
5856 
5857 	override bool isComplete() {
5858 		// just always let the event loop do it instead
5859 		return state_ == State.complete;
5860 
5861 		/+
5862 		version(Windows) {
5863 			return HasOverlappedIoCompleted(&overlapped);
5864 		} else version(Posix) {
5865 			return state_ == State.complete;
5866 
5867 		}
5868 		+/
5869 	}
5870 
5871 	override Response waitForCompletion() {
5872 		if(state_ == State.unused)
5873 			start();
5874 
5875 		// FIXME: if we are inside a fiber, we can set a oncomplete callback and then yield instead...
5876 		if(state_ != State.complete)
5877 			getThisThreadEventLoop().run(&isComplete);
5878 
5879 		/+
5880 		version(Windows) {
5881 			SleepEx(INFINITE, true);
5882 
5883 			//DWORD numberTransferred;
5884 			//Win32Enforce!GetOverlappedResult(file.handle, &overlapped, &numberTransferred, true);
5885 		} else version(Posix) {
5886 			getThisThreadEventLoop().run(&isComplete);
5887 		}
5888 		+/
5889 
5890 		return response;
5891 	}
5892 
5893 	/++
5894 		Repeats the operation, restarting the request.
5895 
5896 		This must only be called when the operation has already completed.
5897 	+/
5898 	void repeat() {
5899 		if(state_ != State.complete)
5900 			throw new Exception("wrong use, cannot repeat if not complete");
5901 		state_ = State.unused;
5902 		start();
5903 	}
5904 
5905 	void delegate(typeof(this) t) oncomplete;
5906 }
5907 
5908 /++
5909 	You can write to a file asynchronously by creating one of these.
5910 +/
5911 version(HasSocket) final class AsyncWriteRequest : AsyncOperationRequest {
5912 	struct LowLevelOperation {
5913 		AsyncFile file;
5914 		ubyte[] buffer;
5915 		long offset;
5916 
5917 		this(typeof(this.tupleof) args) {
5918 			this.tupleof = args;
5919 		}
5920 
5921 		version(Windows) {
5922 			auto opCall(OVERLAPPED* overlapped, LPOVERLAPPED_COMPLETION_ROUTINE ocr) {
5923 				overlapped.Offset = (cast(ulong) offset) & 0xffff_ffff;
5924 				overlapped.OffsetHigh = ((cast(ulong) offset) >> 32) & 0xffff_ffff;
5925 				return WriteFileEx(file.handle, buffer.ptr, cast(int) buffer.length, overlapped, ocr);
5926 			}
5927 		} else {
5928 			auto opCall() {
5929 				return core.sys.posix.unistd.write(file.handle, buffer.ptr, buffer.length);
5930 			}
5931 		}
5932 
5933 		string errorString() {
5934 			return "Write";
5935 		}
5936 	}
5937 	mixin OverlappedIoRequest!(AsyncWriteResponse, LowLevelOperation);
5938 
5939 	this(AsyncFile file, ubyte[] buffer, long offset) {
5940 		this.llo = LowLevelOperation(file, buffer, offset);
5941 		response = typeof(response).defaultConstructed;
5942 	}
5943 }
5944 
5945 /++
5946 
5947 +/
5948 class AsyncWriteResponse : AsyncOperationResponse {
5949 	const ubyte[] bufferWritten;
5950 	const SystemErrorCode errorCode;
5951 
5952 	this(SystemErrorCode errorCode, const(ubyte)[] bufferWritten) {
5953 		this.errorCode = errorCode;
5954 		this.bufferWritten = bufferWritten;
5955 	}
5956 
5957 	override bool wasSuccessful() {
5958 		return errorCode.wasSuccessful;
5959 	}
5960 }
5961 
5962 // FIXME: on Windows, you may want two operations outstanding at once
5963 // so there's no delay between sequential ops. this system currently makes that
5964 // impossible since epoll won't let you register twice...
5965 
5966 // FIXME: if an op completes synchronously, and oncomplete calls repeat
5967 // you can get infinite recursion into the stack...
5968 
5969 /++
5970 
5971 +/
5972 version(HasSocket) final class AsyncReadRequest : AsyncOperationRequest {
5973 	struct LowLevelOperation {
5974 		AsyncFile file;
5975 		ubyte[] buffer;
5976 		long offset;
5977 
5978 		this(typeof(this.tupleof) args) {
5979 			this.tupleof = args;
5980 		}
5981 
5982 		version(Windows) {
5983 			auto opCall(OVERLAPPED* overlapped, LPOVERLAPPED_COMPLETION_ROUTINE ocr) {
5984 				overlapped.Offset = (cast(ulong) offset) & 0xffff_ffff;
5985 				overlapped.OffsetHigh = ((cast(ulong) offset) >> 32) & 0xffff_ffff;
5986 				return ReadFileEx(file.handle, buffer.ptr, cast(int) buffer.length, overlapped, ocr);
5987 			}
5988 		} else {
5989 			auto opCall() {
5990 				return core.sys.posix.unistd.read(file.handle, buffer.ptr, buffer.length);
5991 			}
5992 		}
5993 
5994 		string errorString() {
5995 			return "Read";
5996 		}
5997 	}
5998 	mixin OverlappedIoRequest!(AsyncReadResponse, LowLevelOperation);
5999 
6000 	/++
6001 		The file must have the overlapped flag enabled on Windows and the nonblock flag set on Posix.
6002 
6003 		The buffer MUST NOT be touched by you - not used by another request, modified, read, or freed, including letting a static array going out of scope - until this request's `isComplete` returns `true`.
6004 
6005 		The offset is where to start reading a disk file. For all other types of files, pass 0.
6006 	+/
6007 	this(AsyncFile file, ubyte[] buffer, long offset) {
6008 		this.llo = LowLevelOperation(file, buffer, offset);
6009 		response = typeof(response).defaultConstructed;
6010 	}
6011 
6012 	/++
6013 
6014 	+/
6015 	// abstract void repeat();
6016 }
6017 
6018 /++
6019 
6020 +/
6021 class AsyncReadResponse : AsyncOperationResponse {
6022 	const ubyte[] bufferRead;
6023 	const SystemErrorCode errorCode;
6024 
6025 	this(SystemErrorCode errorCode, const(ubyte)[] bufferRead) {
6026 		this.errorCode = errorCode;
6027 		this.bufferRead = bufferRead;
6028 	}
6029 
6030 	override bool wasSuccessful() {
6031 		return errorCode.wasSuccessful;
6032 	}
6033 }
6034 
6035 /+
6036 	Tasks:
6037 		startTask()
6038 		startSubTask() - what if it just did this when it knows it is being run from inside a task?
6039 		runHelperFunction() - whomever it reports to is the parent
6040 +/
6041 
6042 version(HasThread) class SchedulableTask : Fiber {
6043 	private void delegate() dg;
6044 
6045 	// linked list stuff
6046 	private static SchedulableTask taskRoot;
6047 	private SchedulableTask previous;
6048 	private SchedulableTask next;
6049 
6050 	// need the controlling thread to know how to wake it up if it receives a message
6051 	private Thread controllingThread;
6052 
6053 	// the api
6054 
6055 	this(void delegate() dg) {
6056 		assert(dg !is null);
6057 
6058 		this.dg = dg;
6059 		super(&taskRunner);
6060 
6061 		if(taskRoot !is null) {
6062 			this.next = taskRoot;
6063 			taskRoot.previous = this;
6064 		}
6065 		taskRoot = this;
6066 	}
6067 
6068 	/+
6069 	enum BehaviorOnCtrlC {
6070 		ignore,
6071 		cancel,
6072 		deliverMessage
6073 	}
6074 	+/
6075 
6076 	private bool cancelled;
6077 
6078 	public void cancel() {
6079 		this.cancelled = true;
6080 		// if this is running, we can throw immediately
6081 		// otherwise if we're calling from an appropriate thread, we can call it immediately
6082 		// otherwise we need to queue a wakeup to its own thread.
6083 		// tbh we should prolly just queue it every time
6084 	}
6085 
6086 	private void taskRunner() {
6087 		try {
6088 			dg();
6089 		} catch(TaskCancelledException tce) {
6090 			// this space intentionally left blank;
6091 			// the purpose of this exception is to just
6092 			// let the fiber's destructors run before we
6093 			// let it die.
6094 		} catch(Throwable t) {
6095 			if(taskUncaughtException is null) {
6096 				throw t;
6097 			} else {
6098 				taskUncaughtException(t);
6099 			}
6100 		} finally {
6101 			if(this is taskRoot) {
6102 				taskRoot = taskRoot.next;
6103 				if(taskRoot !is null)
6104 					taskRoot.previous = null;
6105 			} else {
6106 				assert(this.previous !is null);
6107 				assert(this.previous.next is this);
6108 				this.previous.next = this.next;
6109 				if(this.next !is null)
6110 					this.next.previous = this.previous;
6111 			}
6112 		}
6113 	}
6114 }
6115 
6116 /++
6117 
6118 +/
6119 void delegate(Throwable t) taskUncaughtException;
6120 
6121 /++
6122 	Gets an object that lets you control a schedulable task (which is a specialization of a fiber) and can be used in an `if` statement.
6123 
6124 	---
6125 		if(auto controller = inSchedulableTask()) {
6126 			controller.yieldUntilReadable(...);
6127 		}
6128 	---
6129 
6130 	History:
6131 		Added August 11, 2023 (dub v11.1)
6132 +/
6133 version(HasThread) SchedulableTaskController inSchedulableTask() {
6134 	import core.thread.fiber;
6135 
6136 	if(auto fiber = Fiber.getThis) {
6137 		return SchedulableTaskController(cast(SchedulableTask) fiber);
6138 	}
6139 
6140 	return SchedulableTaskController(null);
6141 }
6142 
6143 /// ditto
6144 version(HasThread) struct SchedulableTaskController {
6145 	private this(SchedulableTask fiber) {
6146 		this.fiber = fiber;
6147 	}
6148 
6149 	private SchedulableTask fiber;
6150 
6151 	/++
6152 
6153 	+/
6154 	bool opCast(T : bool)() {
6155 		return fiber !is null;
6156 	}
6157 
6158 	/++
6159 
6160 	+/
6161 	version(Posix)
6162 	void yieldUntilReadable(NativeFileHandle handle) {
6163 		assert(fiber !is null);
6164 
6165 		auto cb = new CallbackHelper(() { fiber.call(); });
6166 
6167 		// FIXME: if the fd is already registered in this thread it can throw...
6168 		version(Windows)
6169 			auto rearmToken = getThisThreadEventLoop().addCallbackOnFdReadableOneShot(handle, cb);
6170 		else
6171 			auto rearmToken = getThisThreadEventLoop().addCallbackOnFdReadableOneShot(handle, cb);
6172 
6173 		// FIXME: this is only valid if the fiber is only ever going to run in this thread!
6174 		fiber.yield();
6175 
6176 		rearmToken.unregister();
6177 
6178 		// what if there are other messages, like a ctrl+c?
6179 		if(fiber.cancelled)
6180 			throw new TaskCancelledException();
6181 	}
6182 
6183 	version(Windows)
6184 	void yieldUntilSignaled(NativeFileHandle handle) {
6185 		// add it to the WaitForMultipleObjects thing w/ a cb
6186 	}
6187 }
6188 
6189 class TaskCancelledException : object.Exception {
6190 	this() {
6191 		super("Task cancelled");
6192 	}
6193 }
6194 
6195 version(HasThread) private class CoreWorkerThread : Thread {
6196 	this(EventLoopType type) {
6197 		this.type = type;
6198 
6199 		// task runners are supposed to have smallish stacks since they either just run a single callback or call into fibers
6200 		// the helper runners might be a bit bigger tho
6201 		super(&run);
6202 	}
6203 	void run() {
6204 		eventLoop = getThisThreadEventLoop(this.type);
6205 		atomicOp!"+="(startedCount, 1);
6206 		atomicOp!"+="(runningCount, 1);
6207 		scope(exit) {
6208 			atomicOp!"-="(runningCount, 1);
6209 		}
6210 
6211 		eventLoop.run(() => cancelled);
6212 	}
6213 
6214 	private bool cancelled;
6215 
6216 	void cancel() {
6217 		cancelled = true;
6218 	}
6219 
6220 	EventLoopType type;
6221 	ICoreEventLoop eventLoop;
6222 
6223 	__gshared static {
6224 		CoreWorkerThread[] taskRunners;
6225 		CoreWorkerThread[] helperRunners;
6226 		ICoreEventLoop mainThreadLoop;
6227 
6228 		// for the helper function thing on the bsds i could have my own little circular buffer of availability
6229 
6230 		shared(int) startedCount;
6231 		shared(int) runningCount;
6232 
6233 		bool started;
6234 
6235 		void setup(int numberOfTaskRunners, int numberOfHelpers) {
6236 			assert(!started);
6237 			synchronized {
6238 				mainThreadLoop = getThisThreadEventLoop();
6239 
6240 				foreach(i; 0 .. numberOfTaskRunners) {
6241 					auto nt = new CoreWorkerThread(EventLoopType.TaskRunner);
6242 					taskRunners ~= nt;
6243 					nt.start();
6244 				}
6245 				foreach(i; 0 .. numberOfHelpers) {
6246 					auto nt = new CoreWorkerThread(EventLoopType.HelperWorker);
6247 					helperRunners ~= nt;
6248 					nt.start();
6249 				}
6250 
6251 				const expectedCount = numberOfHelpers + numberOfTaskRunners;
6252 
6253 				while(startedCount < expectedCount) {
6254 					Thread.yield();
6255 				}
6256 
6257 				started = true;
6258 			}
6259 		}
6260 
6261 		void cancelAll() {
6262 			foreach(runner; taskRunners)
6263 				runner.cancel();
6264 			foreach(runner; helperRunners)
6265 				runner.cancel();
6266 
6267 		}
6268 	}
6269 }
6270 
6271 private int numberOfCpus() {
6272 	return 4; // FIXME
6273 }
6274 
6275 /++
6276 	To opt in to the full functionality of this module with customization opportunity, create one and only one of these objects that is valid for exactly the lifetime of the application.
6277 
6278 	Normally, this means writing a main like this:
6279 
6280 	---
6281 	import arsd.core;
6282 	void main() {
6283 		ArsdCoreApplication app = ArsdCoreApplication("Your app name");
6284 
6285 		// do your setup here
6286 
6287 		// the rest of your code here
6288 	}
6289 	---
6290 
6291 	Its destructor runs the event loop then waits to for the workers to finish to clean them up.
6292 +/
6293 // FIXME: single instance?
6294 version(HasThread) struct ArsdCoreApplication {
6295 	private ICoreEventLoop impl;
6296 
6297 	/++
6298 		default number of threads is to split your cpus between blocking function runners and task runners
6299 	+/
6300 	this(string applicationName) {
6301 		auto num = numberOfCpus();
6302 		num /= 2;
6303 		if(num <= 0)
6304 			num = 1;
6305 		this(applicationName, num, num);
6306 	}
6307 
6308 	/++
6309 
6310 	+/
6311 	this(string applicationName, int numberOfTaskRunners, int numberOfHelpers) {
6312 		impl = getThisThreadEventLoop(EventLoopType.Explicit);
6313 		CoreWorkerThread.setup(numberOfTaskRunners, numberOfHelpers);
6314 	}
6315 
6316 	@disable this();
6317 	@disable this(this);
6318 	/++
6319 		This must be deterministically destroyed.
6320 	+/
6321 	@disable new();
6322 
6323 	~this() {
6324 		if(!alreadyRun)
6325 			run();
6326 		exitApplication();
6327 		waitForWorkersToExit(3000);
6328 	}
6329 
6330 	void exitApplication() {
6331 		CoreWorkerThread.cancelAll();
6332 	}
6333 
6334 	void waitForWorkersToExit(int timeoutMilliseconds) {
6335 
6336 	}
6337 
6338 	private bool alreadyRun;
6339 
6340 	void run() {
6341 		impl.run(() => false);
6342 		alreadyRun = true;
6343 	}
6344 }
6345 
6346 
6347 private class CoreEventLoopImplementation : ICoreEventLoop {
6348 	version(EmptyEventLoop) RunOnceResult runOnce(Duration timeout = Duration.max) { return RunOnceResult(RunOnceResult.Possibilities.LocalExit); }
6349 	version(EmptyCoreEvent)
6350 	{
6351 		UnregisterToken addCallbackOnFdReadable(int fd, CallbackHelper cb){return typeof(return).init;}
6352 		RearmToken addCallbackOnFdReadableOneShot(int fd, CallbackHelper cb){return typeof(return).init;}
6353 		RearmToken addCallbackOnFdWritableOneShot(int fd, CallbackHelper cb){return typeof(return).init;}
6354 		private void rearmFd(RearmToken token) {}
6355 	}
6356 
6357 
6358 	private {
6359 		static struct LoopIterationDelegate {
6360 			void delegate() dg;
6361 			uint flags;
6362 		}
6363 		LoopIterationDelegate[] loopIterationDelegates;
6364 
6365 		void runLoopIterationDelegates() {
6366 			foreach(lid; loopIterationDelegates)
6367 				lid.dg();
6368 		}
6369 	}
6370 
6371 	void addDelegateOnLoopIteration(void delegate() dg, uint timingFlags) {
6372 		loopIterationDelegates ~= LoopIterationDelegate(dg, timingFlags);
6373 	}
6374 
6375 	version(Arsd_core_kqueue) {
6376 		// this thread apc dispatches go as a custom event to the queue
6377 		// the other queues go through one byte at a time pipes (barf). freebsd 13 and newest nbsd have eventfd too tho so maybe i can use them but the other kqueue systems don't.
6378 
6379 		RunOnceResult runOnce(Duration timeout = Duration.max) {
6380 			scope(exit) eventLoopRound++;
6381 			kevent_t[16] ev;
6382 			//timespec tout = timespec(1, 0);
6383 			auto nev = kevent(kqueuefd, null, 0, ev.ptr, ev.length, null/*&tout*/);
6384 			if(nev == -1) {
6385 				// FIXME: EINTR
6386 				throw new SystemApiException("kevent", errno);
6387 			} else if(nev == 0) {
6388 				// timeout
6389 			} else {
6390 				foreach(event; ev[0 .. nev]) {
6391 					if(event.filter == EVFILT_SIGNAL) {
6392 						// FIXME: I could prolly do this better tbh
6393 						markSignalOccurred(cast(int) event.ident);
6394 						signalChecker();
6395 					} else {
6396 						// FIXME: event.filter more specific?
6397 						CallbackHelper cb = cast(CallbackHelper) event.udata;
6398 						cb.call();
6399 					}
6400 				}
6401 			}
6402 
6403 			runLoopIterationDelegates();
6404 
6405 			return RunOnceResult(RunOnceResult.Possibilities.CarryOn);
6406 		}
6407 
6408 		// FIXME: idk how to make one event that multiple kqueues can listen to w/o being shared
6409 		// maybe a shared kqueue could work that the thread kqueue listen to (which i rejected for
6410 		// epoll cuz it caused thundering herd problems but maybe it'd work here)
6411 
6412 		UnregisterToken addCallbackOnFdReadable(int fd, CallbackHelper cb) {
6413 			kevent_t ev;
6414 
6415 			EV_SET(&ev, fd, EVFILT_READ, EV_ADD | EV_ENABLE/* | EV_ONESHOT*/, 0, 0, cast(void*) cb);
6416 
6417 			ErrnoEnforce!kevent(kqueuefd, &ev, 1, null, 0, null);
6418 
6419 			return UnregisterToken(this, fd, cb);
6420 		}
6421 
6422 		RearmToken addCallbackOnFdReadableOneShot(int fd, CallbackHelper cb) {
6423 			kevent_t ev;
6424 
6425 			EV_SET(&ev, fd, EVFILT_READ, EV_ADD | EV_ENABLE/* | EV_ONESHOT*/, 0, 0, cast(void*) cb);
6426 
6427 			ErrnoEnforce!kevent(kqueuefd, &ev, 1, null, 0, null);
6428 
6429 			return RearmToken(true, this, fd, cb, 0);
6430 		}
6431 
6432 		RearmToken addCallbackOnFdWritableOneShot(int fd, CallbackHelper cb) {
6433 			kevent_t ev;
6434 
6435 			EV_SET(&ev, fd, EVFILT_WRITE, EV_ADD | EV_ENABLE/* | EV_ONESHOT*/, 0, 0, cast(void*) cb);
6436 
6437 			ErrnoEnforce!kevent(kqueuefd, &ev, 1, null, 0, null);
6438 
6439 			return RearmToken(false, this, fd, cb, 0);
6440 		}
6441 
6442 		private void rearmFd(RearmToken token) {
6443 			if(token.readable)
6444 				cast(void) addCallbackOnFdReadableOneShot(token.fd, token.cb);
6445 			else
6446 				cast(void) addCallbackOnFdWritableOneShot(token.fd, token.cb);
6447 		}
6448 
6449 		private void triggerGlobalEvent() {
6450 			ubyte a;
6451 			import core.sys.posix.unistd;
6452 			write(kqueueGlobalFd[1], &a, 1);
6453 		}
6454 
6455 		private this() {
6456 			kqueuefd = ErrnoEnforce!kqueue();
6457 			setCloExec(kqueuefd); // FIXME O_CLOEXEC
6458 
6459 			if(kqueueGlobalFd[0] == 0) {
6460 				import core.sys.posix.unistd;
6461 				pipe(kqueueGlobalFd);
6462 				setCloExec(kqueueGlobalFd[0]);
6463 				setCloExec(kqueueGlobalFd[1]);
6464 
6465 				signal(SIGINT, SIG_IGN); // FIXME
6466 			}
6467 
6468 			kevent_t ev;
6469 
6470 			EV_SET(&ev, SIGCHLD, EVFILT_SIGNAL, EV_ADD | EV_ENABLE, 0, 0, null);
6471 			ErrnoEnforce!kevent(kqueuefd, &ev, 1, null, 0, null);
6472 			EV_SET(&ev, SIGINT, EVFILT_SIGNAL, EV_ADD | EV_ENABLE, 0, 0, null);
6473 			ErrnoEnforce!kevent(kqueuefd, &ev, 1, null, 0, null);
6474 
6475 			globalEventSent = new CallbackHelper(&readGlobalEvent);
6476 			EV_SET(&ev, kqueueGlobalFd[0], EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, cast(void*) globalEventSent);
6477 			ErrnoEnforce!kevent(kqueuefd, &ev, 1, null, 0, null);
6478 		}
6479 
6480 		private int kqueuefd = -1;
6481 
6482 		private CallbackHelper globalEventSent;
6483 		void readGlobalEvent() {
6484 			kevent_t event;
6485 
6486 			import core.sys.posix.unistd;
6487 			ubyte a;
6488 			read(kqueueGlobalFd[0], &a, 1);
6489 
6490 			// FIXME: the thread is woken up, now we need to check the circualr buffer queue
6491 		}
6492 
6493 		private __gshared int[2] kqueueGlobalFd;
6494 	}
6495 
6496 	/+
6497 		// this setup  needs no extra allocation
6498 		auto op = read(file, buffer);
6499 		op.oncomplete = &thisfiber.call;
6500 		op.start();
6501 		thisfiber.yield();
6502 		auto result = op.waitForCompletion(); // guaranteed to return instantly thanks to previous setup
6503 
6504 		can generically abstract that into:
6505 
6506 		auto result = thisTask.await(read(file, buffer));
6507 
6508 
6509 		You MUST NOT use buffer in any way - not read, modify, deallocate, reuse, anything - until the PendingOperation is complete.
6510 
6511 		Note that PendingOperation may just be a wrapper around an internally allocated object reference... but then if you do a waitForFirstToComplete what happens?
6512 
6513 		those could of course just take the value type things
6514 	+/
6515 
6516 
6517 	version(Arsd_core_windows) {
6518 		// all event loops share the one iocp, Windows
6519 		// manages how to do it
6520 		__gshared HANDLE iocpTaskRunners;
6521 		__gshared HANDLE iocpWorkers;
6522 
6523 		HANDLE[] handles;
6524 		CallbackHelper[] handlesCbs;
6525 
6526 		void unregisterHandle(HANDLE handle, CallbackHelper cb) {
6527 			foreach(idx, h; handles)
6528 				if(h is handle && handlesCbs[idx] is cb) {
6529 					handles[idx] = handles[$-1];
6530 					handles = handles[0 .. $-1].assumeSafeAppend;
6531 
6532 					handlesCbs[idx] = handlesCbs[$-1];
6533 					handlesCbs = handlesCbs[0 .. $-1].assumeSafeAppend;
6534 				}
6535 		}
6536 
6537 		UnregisterToken addCallbackOnHandleReady(HANDLE handle, CallbackHelper cb) {
6538 			handles ~= handle;
6539 			handlesCbs ~= cb;
6540 
6541 			return UnregisterToken(this, handle, cb);
6542 		}
6543 
6544 		// i think to terminate i just have to post the message at least once for every thread i know about, maybe a few more times for threads i don't know about.
6545 
6546 		bool isWorker; // if it is a worker we wait on the iocp, if not we wait on msg
6547 
6548 		RunOnceResult runOnce(Duration timeout = Duration.max) {
6549 			scope(exit) eventLoopRound++;
6550 			if(isWorker) {
6551 				// this function is only supported on Windows Vista and up, so using this
6552 				// means dropping support for XP.
6553 				//GetQueuedCompletionStatusEx();
6554 				assert(0); // FIXME
6555 			} else {
6556 				auto wto = 0;
6557 
6558 				auto waitResult = MsgWaitForMultipleObjectsEx(
6559 					cast(int) handles.length, handles.ptr,
6560 					(wto == 0 ? INFINITE : wto), /* timeout */
6561 					0x04FF, /* QS_ALLINPUT */
6562 					0x0002 /* MWMO_ALERTABLE */ | 0x0004 /* MWMO_INPUTAVAILABLE */);
6563 
6564 				enum WAIT_OBJECT_0 = 0;
6565 				if(waitResult >= WAIT_OBJECT_0 && waitResult < handles.length + WAIT_OBJECT_0) {
6566 					auto h = handles[waitResult - WAIT_OBJECT_0];
6567 					auto cb = handlesCbs[waitResult - WAIT_OBJECT_0];
6568 					cb.call();
6569 				} else if(waitResult == handles.length + WAIT_OBJECT_0) {
6570 					// message ready
6571 					int count;
6572 					MSG message;
6573 					while(PeekMessage(&message, null, 0, 0, PM_NOREMOVE)) { // need to peek since sometimes MsgWaitForMultipleObjectsEx returns even though GetMessage can block. tbh i don't fully understand it but the docs say it is foreground activation
6574 						auto ret = GetMessage(&message, null, 0, 0);
6575 						if(ret == -1)
6576 							throw new WindowsApiException("GetMessage", GetLastError());
6577 						TranslateMessage(&message);
6578 						DispatchMessage(&message);
6579 
6580 						count++;
6581 						if(count > 10)
6582 							break; // take the opportunity to catch up on other events
6583 
6584 						if(ret == 0) { // WM_QUIT
6585 							exitApplication();
6586 						}
6587 					}
6588 				} else if(waitResult == 0x000000C0L /* WAIT_IO_COMPLETION */) {
6589 					SleepEx(0, true); // I call this to give it a chance to do stuff like async io
6590 				} else if(waitResult == 258L /* WAIT_TIMEOUT */) {
6591 					// timeout, should never happen since we aren't using it
6592 				} else if(waitResult == 0xFFFFFFFF) {
6593 						// failed
6594 						throw new WindowsApiException("MsgWaitForMultipleObjectsEx", GetLastError());
6595 				} else {
6596 					// idk....
6597 				}
6598 			}
6599 
6600 			runLoopIterationDelegates();
6601 
6602 			return RunOnceResult(RunOnceResult.Possibilities.CarryOn);
6603 		}
6604 	}
6605 
6606 	version(Posix) {
6607 		private __gshared uint sigChildHappened = 0;
6608 		private __gshared uint sigIntrHappened = 0;
6609 
6610 		static void signalChecker() {
6611 			if(cas(&sigChildHappened, 1, 0)) {
6612 				while(true) { // multiple children could have exited before we processed the notification
6613 
6614 					import core.sys.posix.sys.wait;
6615 
6616 					int status;
6617 					auto pid = waitpid(-1, &status, WNOHANG);
6618 					if(pid == -1) {
6619 						import core.stdc.errno;
6620 						auto errno = errno;
6621 						if(errno == ECHILD)
6622 							break; // also all done, there are no children left
6623 						// no need to check EINTR since we set WNOHANG
6624 						throw new ErrnoApiException("waitpid", errno);
6625 					}
6626 					if(pid == 0)
6627 						break; // all done, all children are still running
6628 
6629 					// look up the pid for one of our objects
6630 					// if it is found, inform it of its status
6631 					// and then inform its controlling thread
6632 					// to wake up so it can check its waitForCompletion,
6633 					// trigger its callbacks, etc.
6634 
6635 					ExternalProcess.recordChildTerminated(pid, status);
6636 				}
6637 
6638 			}
6639 			if(cas(&sigIntrHappened, 1, 0)) {
6640 				// FIXME
6641 				import core.stdc.stdlib;
6642 				exit(0);
6643 			}
6644 		}
6645 
6646 		/++
6647 			Informs the arsd.core system that the given signal happened. You can call this from inside a signal handler.
6648 		+/
6649 		public static void markSignalOccurred(int sigNumber) nothrow {
6650 			import core.sys.posix.unistd;
6651 
6652 			if(sigNumber == SIGCHLD)
6653 				volatileStore(&sigChildHappened, 1);
6654 			if(sigNumber == SIGINT)
6655 				volatileStore(&sigIntrHappened, 1);
6656 
6657 			version(Arsd_core_epoll) {
6658 				ulong writeValue = 1;
6659 				write(signalPipeFd, &writeValue, writeValue.sizeof);
6660 			}
6661 		}
6662 	}
6663 
6664 	version(Arsd_core_epoll) {
6665 
6666 		import core.sys.linux.epoll;
6667 		import core.sys.linux.sys.eventfd;
6668 
6669 		private this() {
6670 
6671 			if(!globalsInitialized) {
6672 				synchronized {
6673 					if(!globalsInitialized) {
6674 						// blocking signals is problematic because it is inherited by child processes
6675 						// and that can be problematic for general purpose stuff so i use a self pipe
6676 						// here. though since it is linux, im using an eventfd instead just to notify
6677 						signalPipeFd = ErrnoEnforce!eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
6678 						signalReaderCallback = new CallbackHelper(&signalReader);
6679 
6680 						runInTaskRunnerQueue = new CallbackQueue("task runners", true);
6681 						runInHelperThreadQueue = new CallbackQueue("helper threads", true);
6682 
6683 						setSignalHandlers();
6684 
6685 						globalsInitialized = true;
6686 					}
6687 				}
6688 			}
6689 
6690 			epollfd = epoll_create1(EPOLL_CLOEXEC);
6691 
6692 			// FIXME: ensure UI events get top priority
6693 
6694 			// global listeners
6695 
6696 			// FIXME: i should prolly keep the tokens and release them when tearing down.
6697 
6698 			cast(void) addCallbackOnFdReadable(signalPipeFd, signalReaderCallback);
6699 			if(true) { // FIXME: if this is a task runner vs helper thread vs ui thread
6700 				cast(void) addCallbackOnFdReadable(runInTaskRunnerQueue.fd, runInTaskRunnerQueue.callback);
6701 				runInTaskRunnerQueue.callback.addref();
6702 			} else {
6703 				cast(void) addCallbackOnFdReadable(runInHelperThreadQueue.fd, runInHelperThreadQueue.callback);
6704 				runInHelperThreadQueue.callback.addref();
6705 			}
6706 
6707 			// local listener
6708 			thisThreadQueue = new CallbackQueue("this thread", false);
6709 			cast(void) addCallbackOnFdReadable(thisThreadQueue.fd, thisThreadQueue.callback);
6710 
6711 			// what are we going to do about timers?
6712 		}
6713 
6714 		void teardown() {
6715 			import core.sys.posix.fcntl;
6716 			import core.sys.posix.unistd;
6717 
6718 			close(epollfd);
6719 			epollfd = -1;
6720 
6721 			thisThreadQueue.teardown();
6722 
6723 			// FIXME: should prolly free anything left in the callback queue, tho those could also be GC managed tbh.
6724 		}
6725 
6726 		/+ // i call it explicitly at the thread exit instead, but worker threads aren't really supposed to exit generally speaking till process done anyway
6727 		static ~this() {
6728 			teardown();
6729 		}
6730 		+/
6731 
6732 		static void teardownGlobals() {
6733 			import core.sys.posix.fcntl;
6734 			import core.sys.posix.unistd;
6735 
6736 			synchronized {
6737 				restoreSignalHandlers();
6738 				close(signalPipeFd);
6739 				signalReaderCallback.release();
6740 
6741 				runInTaskRunnerQueue.teardown();
6742 				runInHelperThreadQueue.teardown();
6743 
6744 				globalsInitialized = false;
6745 			}
6746 
6747 		}
6748 
6749 
6750 		private static final class CallbackQueue {
6751 			int fd = -1;
6752 			string name;
6753 			CallbackHelper callback;
6754 			SynchronizedCircularBuffer!CallbackHelper queue;
6755 
6756 			this(string name, bool dequeueIsShared) {
6757 				this.name = name;
6758 				queue = typeof(queue)(this);
6759 
6760 				fd = ErrnoEnforce!eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK | (dequeueIsShared ? EFD_SEMAPHORE : 0));
6761 
6762 				callback = new CallbackHelper(dequeueIsShared ? &sharedDequeueCb : &threadLocalDequeueCb);
6763 			}
6764 
6765 			bool resetEvent() {
6766 				import core.sys.posix.unistd;
6767 				ulong count;
6768 				return read(fd, &count, count.sizeof) == count.sizeof;
6769 			}
6770 
6771 			void sharedDequeueCb() {
6772 				if(resetEvent()) {
6773 					auto cb = queue.dequeue();
6774 					cb.call();
6775 					cb.release();
6776 				}
6777 			}
6778 
6779 			void threadLocalDequeueCb() {
6780 				CallbackHelper[16] buffer;
6781 				foreach(cb; queue.dequeueSeveral(buffer[], () { resetEvent(); })) {
6782 					cb.call();
6783 					cb.release();
6784 				}
6785 			}
6786 
6787 			void enqueue(CallbackHelper cb) {
6788 				if(queue.enqueue(cb)) {
6789 					import core.sys.posix.unistd;
6790 					ulong count = 1;
6791 					ErrnoEnforce!write(fd, &count, count.sizeof);
6792 				} else {
6793 					throw new ArsdException!"queue is full"(name);
6794 				}
6795 			}
6796 
6797 			void teardown() {
6798 				import core.sys.posix.fcntl;
6799 				import core.sys.posix.unistd;
6800 
6801 				close(fd);
6802 				fd = -1;
6803 
6804 				callback.release();
6805 			}
6806 		}
6807 
6808 		// there's a global instance of this we refer back to
6809 		private __gshared {
6810 			bool globalsInitialized;
6811 
6812 			CallbackHelper signalReaderCallback;
6813 
6814 			CallbackQueue runInTaskRunnerQueue;
6815 			CallbackQueue runInHelperThreadQueue;
6816 
6817 			int exitEventFd = -1; // FIXME: implement
6818 		}
6819 
6820 		// and then the local loop
6821 		private {
6822 			int epollfd = -1;
6823 
6824 			CallbackQueue thisThreadQueue;
6825 		}
6826 
6827 		// signal stuff {
6828 		import core.sys.posix.signal;
6829 
6830 		private __gshared sigaction_t oldSigIntr;
6831 		private __gshared sigaction_t oldSigChld;
6832 		private __gshared sigaction_t oldSigPipe;
6833 
6834 		private __gshared int signalPipeFd = -1;
6835 		// sigpipe not important, i handle errors on the writes
6836 
6837 		public static void setSignalHandlers() {
6838 			static extern(C) void interruptHandler(int sigNumber) nothrow {
6839 				markSignalOccurred(sigNumber);
6840 
6841 				/+
6842 				// calling the old handler is non-trivial since there can be ignore
6843 				// or default or a plain handler or a sigaction 3 arg handler and i
6844 				// i don't think it is worth teh complication
6845 				sigaction_t* oldHandler;
6846 				if(sigNumber == SIGCHLD)
6847 					oldHandler = &oldSigChld;
6848 				else if(sigNumber == SIGINT)
6849 					oldHandler = &oldSigIntr;
6850 				if(oldHandler && oldHandler.sa_handler)
6851 					oldHandler
6852 				+/
6853 			}
6854 
6855 			sigaction_t n;
6856 			n.sa_handler = &interruptHandler;
6857 			n.sa_mask = cast(sigset_t) 0;
6858 			n.sa_flags = 0;
6859 			sigaction(SIGINT, &n, &oldSigIntr);
6860 			sigaction(SIGCHLD, &n, &oldSigChld);
6861 
6862 			n.sa_handler = SIG_IGN;
6863 			sigaction(SIGPIPE, &n, &oldSigPipe);
6864 		}
6865 
6866 		public static void restoreSignalHandlers() {
6867 			sigaction(SIGINT, &oldSigIntr, null);
6868 			sigaction(SIGCHLD, &oldSigChld, null);
6869 			sigaction(SIGPIPE, &oldSigPipe, null);
6870 		}
6871 
6872 		private static void signalReader() {
6873 			import core.sys.posix.unistd;
6874 			ulong number;
6875 			read(signalPipeFd, &number, number.sizeof);
6876 
6877 			signalChecker();
6878 		}
6879 		// signal stuff done }
6880 
6881 		// the any thread poll is just registered in the this thread poll w/ exclusive. nobody actaully epoll_waits
6882 		// on the global one directly.
6883 
6884 		RunOnceResult runOnce(Duration timeout = Duration.max) {
6885 			scope(exit) eventLoopRound++;
6886 			epoll_event[16] events;
6887 			auto ret = epoll_wait(epollfd, events.ptr, cast(int) events.length, -1); // FIXME: timeout
6888 			if(ret == -1) {
6889 				import core.stdc.errno;
6890 				if(errno == EINTR) {
6891 					return RunOnceResult(RunOnceResult.Possibilities.Interrupted);
6892 				}
6893 				throw new ErrnoApiException("epoll_wait", errno);
6894 			} else if(ret == 0) {
6895 				// timeout
6896 			} else {
6897 				// loop events and call associated callbacks
6898 				foreach(event; events[0 .. ret]) {
6899 					auto flags = event.events;
6900 					auto cbObject = cast(CallbackHelper) event.data.ptr;
6901 
6902 					// FIXME: or if it is an error...
6903 					// EPOLLERR - write end of pipe when read end closed or other error. and EPOLLHUP - terminal hangup or read end when write end close (but it will give 0 reading after that soon anyway)
6904 
6905 					cbObject.call();
6906 				}
6907 			}
6908 
6909 			runLoopIterationDelegates();
6910 
6911 			return RunOnceResult(RunOnceResult.Possibilities.CarryOn);
6912 		}
6913 
6914 		// building blocks for low-level integration with the loop
6915 
6916 		UnregisterToken addCallbackOnFdReadable(int fd, CallbackHelper cb) {
6917 			epoll_event event;
6918 			event.data.ptr = cast(void*) cb;
6919 			event.events = EPOLLIN | EPOLLEXCLUSIVE;
6920 			if(epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event) == -1)
6921 				throw new ErrnoApiException("epoll_ctl", errno);
6922 
6923 			return UnregisterToken(this, fd, cb);
6924 		}
6925 
6926 		/++
6927 			Adds a one-off callback that you can optionally rearm when it happens.
6928 		+/
6929 		RearmToken addCallbackOnFdReadableOneShot(int fd, CallbackHelper cb) {
6930 			epoll_event event;
6931 			event.data.ptr = cast(void*) cb;
6932 			event.events = EPOLLIN | EPOLLONESHOT;
6933 			if(epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event) == -1)
6934 				throw new ErrnoApiException("epoll_ctl", errno);
6935 
6936 			return RearmToken(true, this, fd, cb, EPOLLIN | EPOLLONESHOT);
6937 		}
6938 
6939 		/++
6940 			Adds a one-off callback that you can optionally rearm when it happens.
6941 		+/
6942 		RearmToken addCallbackOnFdWritableOneShot(int fd, CallbackHelper cb) {
6943 			epoll_event event;
6944 			event.data.ptr = cast(void*) cb;
6945 			event.events = EPOLLOUT | EPOLLONESHOT;
6946 			if(epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event) == -1)
6947 				throw new ErrnoApiException("epoll_ctl", errno);
6948 
6949 			return RearmToken(false, this, fd, cb, EPOLLOUT | EPOLLONESHOT);
6950 		}
6951 
6952 		private void unregisterFd(int fd) {
6953 			epoll_event event;
6954 			if(epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, &event) == -1)
6955 				throw new ErrnoApiException("epoll_ctl", errno);
6956 		}
6957 
6958 		private void rearmFd(RearmToken token) {
6959 			epoll_event event;
6960 			event.data.ptr = cast(void*) token.cb;
6961 			event.events = token.flags;
6962 			if(epoll_ctl(epollfd, EPOLL_CTL_MOD, token.fd, &event) == -1)
6963 				throw new ErrnoApiException("epoll_ctl", errno);
6964 		}
6965 
6966 		// Disk files will have to be sent as messages to a worker to do the read and report back a completion packet.
6967 	}
6968 
6969 	version(Arsd_core_kqueue) {
6970 		// FIXME
6971 	}
6972 
6973 	// cross platform adapters
6974 	void setTimeout() {}
6975 	void addFileOrDirectoryChangeListener(FilePath name, uint flags, bool recursive = false) {}
6976 }
6977 
6978 // deduplication???????//
6979 bool postMessage(ThreadToRunIn destination, void delegate() code) {
6980 	return false;
6981 }
6982 bool postMessage(ThreadToRunIn destination, Object message) {
6983 	return false;
6984 }
6985 
6986 /+
6987 void main() {
6988 	// FIXME: the offset doesn't seem to be done right
6989 	auto file = new AsyncFile(FilePath("test.txt"), AsyncFile.OpenMode.writeWithTruncation);
6990 	file.write("hello", 10).waitForCompletion();
6991 }
6992 +/
6993 
6994 // to test the mailboxes
6995 /+
6996 void main() {
6997 	/+
6998 	import std.stdio;
6999 	Thread[4] pool;
7000 
7001 	bool shouldExit;
7002 
7003 	static int received;
7004 
7005 	static void tester() {
7006 		received++;
7007 		//writeln(cast(void*) Thread.getThis, " ", received);
7008 	}
7009 
7010 	foreach(ref thread; pool) {
7011 		thread = new Thread(() {
7012 			getThisThreadEventLoop().run(() {
7013 				return shouldExit;
7014 			});
7015 		});
7016 		thread.start();
7017 	}
7018 
7019 	getThisThreadEventLoop(); // ensure it is all initialized before proceeding. FIXME: i should have an ensure initialized function i do on most the public apis.
7020 
7021 	int lol;
7022 
7023 	try
7024 	foreach(i; 0 .. 6000) {
7025 		CoreEventLoopImplementation.runInTaskRunnerQueue.enqueue(new CallbackHelper(&tester));
7026 		lol = cast(int) i;
7027 	}
7028 	catch(ArsdExceptionBase e)  {
7029 		Thread.sleep(50.msecs);
7030 		writeln(e);
7031 		writeln(lol);
7032 	}
7033 
7034 	import core.stdc.stdlib;
7035 	exit(0);
7036 
7037 	version(none)
7038 	foreach(i; 0 .. 100)
7039 		CoreEventLoopImplementation.runInTaskRunnerQueue.enqueue(new CallbackHelper(&tester));
7040 
7041 
7042 	foreach(ref thread; pool) {
7043 		thread.join();
7044 	}
7045 	+/
7046 
7047 
7048 	static int received;
7049 
7050 	static void tester() {
7051 		received++;
7052 		//writeln(cast(void*) Thread.getThis, " ", received);
7053 	}
7054 
7055 
7056 
7057 	auto ev = cast(CoreEventLoopImplementation) getThisThreadEventLoop();
7058 	foreach(i; 0 .. 100)
7059 		ev.thisThreadQueue.enqueue(new CallbackHelper(&tester));
7060 	foreach(i; 0 .. 100 / 16 + 1)
7061 	ev.runOnce();
7062 	import std.conv;
7063 	assert(received == 100, to!string(received));
7064 
7065 }
7066 +/
7067 
7068 /++
7069 	This is primarily a helper for the event queues. It is public in the hope it might be useful,
7070 	but subject to change without notice; I will treat breaking it the same as if it is private.
7071 	(That said, it is a simple little utility that does its job, so it is unlikely to change much.
7072 	The biggest change would probably be letting it grow and changing from inline to dynamic array.)
7073 
7074 	It is a fixed-size ring buffer that synchronizes on a given object you give it in the constructor.
7075 
7076 	After enqueuing something, you should probably set an event to notify the other threads. This is left
7077 	as an exercise to you (or another wrapper).
7078 +/
7079 struct SynchronizedCircularBuffer(T, size_t maxSize = 128) {
7080 	private T[maxSize] ring;
7081 	private int front;
7082 	private int back;
7083 
7084 	private Object synchronizedOn;
7085 
7086 	@disable this();
7087 
7088 	/++
7089 		The Object's monitor is used to synchronize the methods in here.
7090 	+/
7091 	this(Object synchronizedOn) {
7092 		this.synchronizedOn = synchronizedOn;
7093 	}
7094 
7095 	/++
7096 		Note the potential race condition between calling this and actually dequeuing something. You might
7097 		want to acquire the lock on the object before calling this (nested synchronized things are allowed
7098 		as long as the same thread is the one doing it).
7099 	+/
7100 	bool isEmpty() {
7101 		synchronized(this.synchronizedOn) {
7102 			return front == back;
7103 		}
7104 	}
7105 
7106 	/++
7107 		Note the potential race condition between calling this and actually queuing something.
7108 	+/
7109 	bool isFull() {
7110 		synchronized(this.synchronizedOn) {
7111 			return isFullUnsynchronized();
7112 		}
7113 	}
7114 
7115 	private bool isFullUnsynchronized() nothrow const {
7116 		return ((back + 1) % ring.length) == front;
7117 
7118 	}
7119 
7120 	/++
7121 		If this returns true, you should signal listening threads (with an event or a semaphore,
7122 		depending on how you dequeue it). If it returns false, the queue was full and your thing
7123 		was NOT added. You might wait and retry later (you could set up another event to signal it
7124 		has been read and wait for that, or maybe try on a timer), or just fail and throw an exception
7125 		or to abandon the message.
7126 	+/
7127 	bool enqueue(T what) {
7128 		synchronized(this.synchronizedOn) {
7129 			if(isFullUnsynchronized())
7130 				return false;
7131 			ring[(back++) % ring.length] = what;
7132 			return true;
7133 		}
7134 	}
7135 
7136 	private T dequeueUnsynchronized() nothrow {
7137 		assert(front != back);
7138 		return ring[(front++) % ring.length];
7139 	}
7140 
7141 	/++
7142 		If you are using a semaphore to signal, you can call this once for each count of it
7143 		and you can do that separately from this call (though they should be paired).
7144 
7145 		If you are using an event, you should use [dequeueSeveral] instead to drain it.
7146 	+/
7147 	T dequeue() {
7148 		synchronized(this.synchronizedOn) {
7149 			return dequeueUnsynchronized();
7150 		}
7151 	}
7152 
7153 	/++
7154 		Note that if you use a semaphore to signal waiting threads, you should probably not call this.
7155 
7156 		If you use a set/reset event, there's a potential race condition between the dequeue and event
7157 		reset. This is why the `runInsideLockIfEmpty` delegate is there - when it is empty, before it
7158 		unlocks, it will give you a chance to reset the event. Otherwise, it can remain set to indicate
7159 		that there's still pending data in the queue.
7160 	+/
7161 	T[] dequeueSeveral(return T[] buffer, scope void delegate() runInsideLockIfEmpty = null) {
7162 		int pos;
7163 		synchronized(this.synchronizedOn) {
7164 			while(pos < buffer.length && front != back) {
7165 				buffer[pos++] = dequeueUnsynchronized();
7166 			}
7167 			if(front == back && runInsideLockIfEmpty !is null)
7168 				runInsideLockIfEmpty();
7169 		}
7170 		return buffer[0 .. pos];
7171 	}
7172 }
7173 
7174 unittest {
7175 	Object object = new Object();
7176 	auto queue = SynchronizedCircularBuffer!CallbackHelper(object);
7177 	assert(queue.isEmpty);
7178 	foreach(i; 0 .. queue.ring.length - 1)
7179 		queue.enqueue(cast(CallbackHelper) cast(void*) i);
7180 	assert(queue.isFull);
7181 
7182 	foreach(i; 0 .. queue.ring.length - 1)
7183 		assert(queue.dequeue() is (cast(CallbackHelper) cast(void*) i));
7184 	assert(queue.isEmpty);
7185 
7186 	foreach(i; 0 .. queue.ring.length - 1)
7187 		queue.enqueue(cast(CallbackHelper) cast(void*) i);
7188 	assert(queue.isFull);
7189 
7190 	CallbackHelper[] buffer = new CallbackHelper[](300);
7191 	auto got = queue.dequeueSeveral(buffer);
7192 	assert(got.length == queue.ring.length - 1);
7193 	assert(queue.isEmpty);
7194 	foreach(i, item; got)
7195 		assert(item is (cast(CallbackHelper) cast(void*) i));
7196 
7197 	foreach(i; 0 .. 8)
7198 		queue.enqueue(cast(CallbackHelper) cast(void*) i);
7199 	buffer = new CallbackHelper[](4);
7200 	got = queue.dequeueSeveral(buffer);
7201 	assert(got.length == 4);
7202 	foreach(i, item; got)
7203 		assert(item is (cast(CallbackHelper) cast(void*) i));
7204 	got = queue.dequeueSeveral(buffer);
7205 	assert(got.length == 4);
7206 	foreach(i, item; got)
7207 		assert(item is (cast(CallbackHelper) cast(void*) (i+4)));
7208 	got = queue.dequeueSeveral(buffer);
7209 	assert(got.length == 0);
7210 	assert(queue.isEmpty);
7211 }
7212 
7213 /++
7214 
7215 +/
7216 enum ByteOrder {
7217 	irrelevant,
7218 	littleEndian,
7219 	bigEndian,
7220 }
7221 
7222 /++
7223 	A class to help write a stream of binary data to some target.
7224 
7225 	NOT YET FUNCTIONAL
7226 +/
7227 class WritableStream {
7228 	/++
7229 
7230 	+/
7231 	this(size_t bufferSize) {
7232 		this(new ubyte[](bufferSize));
7233 	}
7234 
7235 	/// ditto
7236 	this(ubyte[] buffer) {
7237 		this.buffer = buffer;
7238 	}
7239 
7240 	/++
7241 
7242 	+/
7243 	final void put(T)(T value, ByteOrder byteOrder = ByteOrder.irrelevant, string file = __FILE__, size_t line = __LINE__) {
7244 		static if(T.sizeof == 8)
7245 			ulong b;
7246 		else static if(T.sizeof == 4)
7247 			uint b;
7248 		else static if(T.sizeof == 2)
7249 			ushort b;
7250 		else static if(T.sizeof == 1)
7251 			ubyte b;
7252 		else static assert(0, "unimplemented type, try using just the basic types");
7253 
7254 		if(byteOrder == ByteOrder.irrelevant && T.sizeof > 1)
7255 			throw new InvalidArgumentsException("byteOrder", "byte order must be specified for type " ~ T.stringof ~ " because it is bigger than one byte", "WritableStream.put", file, line);
7256 
7257 		final switch(byteOrder) {
7258 			case ByteOrder.irrelevant:
7259 				writeOneByte(b);
7260 			break;
7261 			case ByteOrder.littleEndian:
7262 				foreach(i; 0 .. T.sizeof) {
7263 					writeOneByte(b & 0xff);
7264 					b >>= 8;
7265 				}
7266 			break;
7267 			case ByteOrder.bigEndian:
7268 				int amount = T.sizeof * 8 - 8;
7269 				foreach(i; 0 .. T.sizeof) {
7270 					writeOneByte((b >> amount) & 0xff);
7271 					amount -= 8;
7272 				}
7273 			break;
7274 		}
7275 	}
7276 
7277 	/// ditto
7278 	final void put(T : E[], E)(T value, ByteOrder elementByteOrder = ByteOrder.irrelevant, string file = __FILE__, size_t line = __LINE__) {
7279 		foreach(item; value)
7280 			put(item, elementByteOrder, file, line);
7281 	}
7282 
7283 	/++
7284 		Performs a final flush() call, then marks the stream as closed, meaning no further data will be written to it.
7285 	+/
7286 	void close() {
7287 		isClosed_ = true;
7288 	}
7289 
7290 	/++
7291 		Writes what is currently in the buffer to the target and waits for the target to accept it.
7292 		Please note: if you are subclassing this to go to a different target
7293 	+/
7294 	void flush() {}
7295 
7296 	/++
7297 		Returns true if either you closed it or if the receiving end closed their side, indicating they
7298 		don't want any more data.
7299 	+/
7300 	bool isClosed() {
7301 		return isClosed_;
7302 	}
7303 
7304 	// hasRoomInBuffer
7305 	// canFlush
7306 	// waitUntilCanFlush
7307 
7308 	// flushImpl
7309 	// markFinished / close - tells the other end you're done
7310 
7311 	private final writeOneByte(ubyte value) {
7312 		if(bufferPosition == buffer.length)
7313 			flush();
7314 
7315 		buffer[bufferPosition++] = value;
7316 	}
7317 
7318 
7319 	private {
7320 		ubyte[] buffer;
7321 		int bufferPosition;
7322 		bool isClosed_;
7323 	}
7324 }
7325 
7326 /++
7327 	A stream can be used by just one task at a time, but one task can consume multiple streams.
7328 
7329 	Streams may be populated by async sources (in which case they must be called from a fiber task),
7330 	from a function generating the data on demand (including an input range), from memory, or from a synchronous file.
7331 
7332 	A stream of heterogeneous types is compatible with input ranges.
7333 
7334 	It reads binary data.
7335 +/
7336 version(HasThread) class ReadableStream {
7337 
7338 	this() {
7339 
7340 	}
7341 
7342 	/++
7343 		Gets data of the specified type `T` off the stream. The byte order of the T on the stream must be specified unless it is irrelevant (e.g. single byte entries).
7344 
7345 		---
7346 		// get an int out of a big endian stream
7347 		int i = stream.get!int(ByteOrder.bigEndian);
7348 
7349 		// get i bytes off the stream
7350 		ubyte[] data = stream.get!(ubyte[])(i);
7351 		---
7352 	+/
7353 	final T get(T)(ByteOrder byteOrder = ByteOrder.irrelevant, string file = __FILE__, size_t line = __LINE__) {
7354 		if(byteOrder == ByteOrder.irrelevant && T.sizeof > 1)
7355 			throw new InvalidArgumentsException("byteOrder", "byte order must be specified for type " ~ T.stringof ~ " because it is bigger than one byte", "ReadableStream.get", file, line);
7356 
7357 		// FIXME: what if it is a struct?
7358 
7359 		while(bufferedLength() < T.sizeof)
7360 			waitForAdditionalData();
7361 
7362 		static if(T.sizeof == 1) {
7363 			ubyte ret = consumeOneByte();
7364 			return *cast(T*) &ret;
7365 		} else {
7366 			static if(T.sizeof == 8)
7367 				ulong ret;
7368 			else static if(T.sizeof == 4)
7369 				uint ret;
7370 			else static if(T.sizeof == 2)
7371 				ushort ret;
7372 			else static assert(0, "unimplemented type, try using just the basic types");
7373 
7374 			if(byteOrder == ByteOrder.littleEndian) {
7375 				typeof(ret) buffer;
7376 				foreach(b; 0 .. T.sizeof) {
7377 					buffer = consumeOneByte();
7378 					buffer <<= T.sizeof * 8 - 8;
7379 
7380 					ret >>= 8;
7381 					ret |= buffer;
7382 				}
7383 			} else {
7384 				foreach(b; 0 .. T.sizeof) {
7385 					ret <<= 8;
7386 					ret |= consumeOneByte();
7387 				}
7388 			}
7389 
7390 			return *cast(T*) &ret;
7391 		}
7392 	}
7393 
7394 	/// ditto
7395 	final T get(T : E[], E)(size_t length, ByteOrder elementByteOrder = ByteOrder.irrelevant, string file = __FILE__, size_t line = __LINE__) {
7396 		if(elementByteOrder == ByteOrder.irrelevant && E.sizeof > 1)
7397 			throw new InvalidArgumentsException("elementByteOrder", "byte order must be specified for type " ~ E.stringof ~ " because it is bigger than one byte", "ReadableStream.get", file, line);
7398 
7399 		// if the stream is closed before getting the length or the terminator, should we send partial stuff
7400 		// or just throw?
7401 
7402 		while(bufferedLength() < length * E.sizeof)
7403 			waitForAdditionalData();
7404 
7405 		T ret;
7406 
7407 		ret.length = length;
7408 
7409 		if(false && elementByteOrder == ByteOrder.irrelevant) {
7410 			// ret[] =
7411 			// FIXME: can prolly optimize
7412 		} else {
7413 			foreach(i; 0 .. length)
7414 				ret[i] = get!E(elementByteOrder);
7415 		}
7416 
7417 		return ret;
7418 
7419 	}
7420 
7421 	/// ditto
7422 	final T get(T : E[], E)(scope bool delegate(E e) isTerminatingSentinel, ByteOrder elementByteOrder = ByteOrder.irrelevant, string file = __FILE__, size_t line = __LINE__) {
7423 		if(elementByteOrder == ByteOrder.irrelevant && E.sizeof > 1)
7424 			throw new InvalidArgumentsException("elementByteOrder", "byte order must be specified for type " ~ E.stringof ~ " because it is bigger than one byte", "ReadableStream.get", file, line);
7425 
7426 		T ret;
7427 
7428 		do {
7429 			try
7430 				ret ~= get!E(elementByteOrder);
7431 			catch(ArsdException!"is already closed" ae)
7432 				return ret;
7433 		} while(!isTerminatingSentinel(ret[$-1]));
7434 
7435 		return ret[0 .. $-1]; // cut off the terminating sentinel
7436 	}
7437 
7438 	/++
7439 
7440 	+/
7441 	bool isClosed() {
7442 		return isClosed_ && currentBuffer.length == 0 && leftoverBuffer.length == 0;
7443 	}
7444 
7445 	// Control side of things
7446 
7447 	private bool isClosed_;
7448 
7449 	/++
7450 		Feeds data into the stream, which can be consumed by `get`. If a task is waiting for more
7451 		data to satisfy its get requests, this will trigger those tasks to resume.
7452 
7453 		If you feed it empty data, it will mark the stream as closed.
7454 	+/
7455 	void feedData(ubyte[] data) {
7456 		if(data.length == 0)
7457 			isClosed_ = true;
7458 
7459 		currentBuffer = data;
7460 		// this is a borrowed buffer, so we won't keep the reference long term
7461 		scope(exit)
7462 			currentBuffer = null;
7463 
7464 		if(waitingTask !is null) {
7465 			waitingTask.call();
7466 		}
7467 	}
7468 
7469 	/++
7470 		You basically have to use this thing from a task
7471 	+/
7472 	protected void waitForAdditionalData() {
7473 		if(isClosed_)
7474 			throw ArsdException!("is already closed")();
7475 
7476 		Fiber task = Fiber.getThis;
7477 
7478 		assert(task !is null);
7479 
7480 		if(waitingTask !is null && waitingTask !is task)
7481 			throw new ArsdException!"streams can only have one waiting task";
7482 
7483 		// copy any pending data in our buffer to the longer-term buffer
7484 		if(currentBuffer.length)
7485 			leftoverBuffer ~= currentBuffer;
7486 
7487 		waitingTask = task;
7488 		task.yield();
7489 	}
7490 
7491 	private Fiber waitingTask;
7492 	private ubyte[] leftoverBuffer;
7493 	private ubyte[] currentBuffer;
7494 
7495 	private size_t bufferedLength() {
7496 		return leftoverBuffer.length + currentBuffer.length;
7497 	}
7498 
7499 	private ubyte consumeOneByte() {
7500 		ubyte b;
7501 		if(leftoverBuffer.length) {
7502 			b = leftoverBuffer[0];
7503 			leftoverBuffer = leftoverBuffer[1 .. $];
7504 		} else if(currentBuffer.length) {
7505 			b = currentBuffer[0];
7506 			currentBuffer = currentBuffer[1 .. $];
7507 		} else {
7508 			assert(0, "consuming off an empty buffer is impossible");
7509 		}
7510 
7511 		return b;
7512 	}
7513 }
7514 
7515 // FIXME: do a stringstream too
7516 
7517 unittest {
7518 	auto stream = new ReadableStream();
7519 
7520 	int position;
7521 	char[16] errorBuffer;
7522 
7523 	auto fiber = new Fiber(() {
7524 		position = 1;
7525 		int a = stream.get!int(ByteOrder.littleEndian);
7526 		assert(a == 10, intToString(a, errorBuffer[]));
7527 		position = 2;
7528 		ubyte b = stream.get!ubyte;
7529 		assert(b == 33);
7530 		position = 3;
7531 
7532 		// ubyte[] c = stream.get!(ubyte[])(3);
7533 		// int[] d = stream.get!(int[])(3);
7534 	});
7535 
7536 	fiber.call();
7537 	assert(position == 1);
7538 	stream.feedData([10, 0, 0, 0]);
7539 	assert(position == 2);
7540 	stream.feedData([33]);
7541 	assert(position == 3);
7542 
7543 	// stream.feedData([1,2,3]);
7544 	// stream.feedData([1,2,3,4,1,2,3,4,1,2,3,4]);
7545 }
7546 
7547 /++
7548 	UNSTABLE, NOT FULLY IMPLEMENTED. DO NOT USE YET.
7549 
7550 	You might use this like:
7551 
7552 	---
7553 	auto proc = new ExternalProcess();
7554 	auto stdoutStream = new ReadableStream();
7555 
7556 	// to use a stream you can make one and have a task consume it
7557 	runTask({
7558 		while(!stdoutStream.isClosed) {
7559 			auto line = stdoutStream.get!string(e => e == '\n');
7560 		}
7561 	});
7562 
7563 	// then make the process feed into the stream
7564 	proc.onStdoutAvailable = (got) {
7565 		stdoutStream.feedData(got); // send it to the stream for processing
7566 		stdout.rawWrite(got); // forward it through to our own thing
7567 		// could also append it to a buffer to return it on complete
7568 	};
7569 	proc.start();
7570 	---
7571 
7572 	Please note that this does not currently and I have no plans as of this writing to add support for any kind of direct file descriptor passing. It always pipes them back to the parent for processing. If you don't want this, call the lower level functions yourself; the reason this class is here is to aid integration in the arsd.core event loop. Of course, I might change my mind on this.
7573 +/
7574 class ExternalProcess /*: AsyncOperationRequest*/ {
7575 
7576 	private static version(Posix) {
7577 		__gshared ExternalProcess[pid_t] activeChildren;
7578 
7579 		void recordChildCreated(pid_t pid, ExternalProcess proc) {
7580 			synchronized(typeid(ExternalProcess)) {
7581 				activeChildren[pid] = proc;
7582 			}
7583 		}
7584 
7585 		void recordChildTerminated(pid_t pid, int status) {
7586 			synchronized(typeid(ExternalProcess)) {
7587 				if(pid in activeChildren) {
7588 					auto ac = activeChildren[pid];
7589 					ac.markComplete(status);
7590 					activeChildren.remove(pid);
7591 				}
7592 			}
7593 		}
7594 	}
7595 
7596 	// FIXME: config to pass through a shell or not
7597 
7598 	/++
7599 		This is the native version for Windows.
7600 	+/
7601 	version(Windows)
7602 	this(FilePath program, string commandLine) {
7603 		version(Posix) {
7604 			assert(0, "not implemented command line to posix args yet");
7605 		} else version(Windows) {
7606 			this.program = program;
7607 			this.commandLine = commandLine;
7608 		}
7609 		else throw new NotYetImplementedException();
7610 	}
7611 
7612 	/+
7613 	this(string commandLine) {
7614 		version(Posix) {
7615 			assert(0, "not implemented command line to posix args yet");
7616 		}
7617 		else throw new NotYetImplementedException();
7618 	}
7619 
7620 	this(string[] args) {
7621 		version(Posix) {
7622 			this.program = FilePath(args[0]);
7623 			this.args = args;
7624 		}
7625 		else throw new NotYetImplementedException();
7626 	}
7627 	+/
7628 
7629 	/++
7630 		This is the native version for Posix.
7631 	+/
7632 	version(Posix)
7633 	this(FilePath program, string[] args) {
7634 		version(Posix) {
7635 			this.program = program;
7636 			this.args = args;
7637 		}
7638 		else throw new NotYetImplementedException();
7639 	}
7640 
7641 	/++
7642 
7643 	+/
7644 	void start() {
7645 		version(Posix) {
7646 			getThisThreadEventLoop(); // ensure it is initialized
7647 
7648 			int ret;
7649 
7650 			int[2] stdinPipes;
7651 			ret = pipe(stdinPipes);
7652 			if(ret == -1)
7653 				throw new ErrnoApiException("stdin pipe", errno);
7654 
7655 			scope(failure) {
7656 				close(stdinPipes[0]);
7657 				close(stdinPipes[1]);
7658 			}
7659 
7660 			auto stdinFd = stdinPipes[1];
7661 
7662 			int[2] stdoutPipes;
7663 			ret = pipe(stdoutPipes);
7664 			if(ret == -1)
7665 				throw new ErrnoApiException("stdout pipe", errno);
7666 
7667 			scope(failure) {
7668 				close(stdoutPipes[0]);
7669 				close(stdoutPipes[1]);
7670 			}
7671 
7672 			auto stdoutFd = stdoutPipes[0];
7673 
7674 			int[2] stderrPipes;
7675 			ret = pipe(stderrPipes);
7676 			if(ret == -1)
7677 				throw new ErrnoApiException("stderr pipe", errno);
7678 
7679 			scope(failure) {
7680 				close(stderrPipes[0]);
7681 				close(stderrPipes[1]);
7682 			}
7683 
7684 			auto stderrFd = stderrPipes[0];
7685 
7686 
7687 			int[2] errorReportPipes;
7688 			ret = pipe(errorReportPipes);
7689 			if(ret == -1)
7690 				throw new ErrnoApiException("error reporting pipe", errno);
7691 
7692 			scope(failure) {
7693 				close(errorReportPipes[0]);
7694 				close(errorReportPipes[1]);
7695 			}
7696 
7697 			setCloExec(errorReportPipes[0]);
7698 			setCloExec(errorReportPipes[1]);
7699 
7700 			auto forkRet = fork();
7701 			if(forkRet == -1)
7702 				throw new ErrnoApiException("fork", errno);
7703 
7704 			if(forkRet == 0) {
7705 				// child side
7706 
7707 				// FIXME can we do more error checking that is actually useful here?
7708 				// these operations are virtually guaranteed to succeed given the setup anyway.
7709 
7710 				// FIXME pty too
7711 
7712 				void fail(int step) {
7713 					import core.stdc.errno;
7714 					auto code = errno;
7715 
7716 					// report the info back to the parent then exit
7717 
7718 					int[2] msg = [step, code];
7719 					auto ret = write(errorReportPipes[1], msg.ptr, msg.sizeof);
7720 
7721 					// but if this fails there's not much we can do...
7722 
7723 					import core.stdc.stdlib;
7724 					exit(1);
7725 				}
7726 
7727 				// dup2 closes the fd it is replacing automatically
7728 				dup2(stdinPipes[0], 0);
7729 				dup2(stdoutPipes[1], 1);
7730 				dup2(stderrPipes[1], 2);
7731 
7732 				// don't need either of the original pipe fds anymore
7733 				close(stdinPipes[0]);
7734 				close(stdinPipes[1]);
7735 				close(stdoutPipes[0]);
7736 				close(stdoutPipes[1]);
7737 				close(stderrPipes[0]);
7738 				close(stderrPipes[1]);
7739 
7740 				// the error reporting pipe will be closed upon exec since we set cloexec before fork
7741 				// and everything else should have cloexec set too hopefully.
7742 
7743 				if(beforeExec)
7744 					beforeExec();
7745 
7746 				// i'm not sure that a fully-initialized druntime is still usable
7747 				// after a fork(), so i'm gonna stick to the C lib in here.
7748 
7749 				const(char)* file = mallocedStringz(program.path).ptr;
7750 				if(file is null)
7751 					fail(1);
7752 				const(char)*[] argv = mallocSlice!(const(char)*)(args.length + 1);
7753 				if(argv is null)
7754 					fail(2);
7755 				foreach(idx, arg; args) {
7756 					argv[idx] = mallocedStringz(args[idx]).ptr;
7757 					if(argv[idx] is null)
7758 						fail(3);
7759 				}
7760 				argv[args.length] = null;
7761 
7762 				auto rete = execvp/*e*/(file, argv.ptr/*, envp*/);
7763 				if(rete == -1) {
7764 					fail(4);
7765 				} else {
7766 					// unreachable code, exec never returns if it succeeds
7767 					assert(0);
7768 				}
7769 			} else {
7770 				pid = forkRet;
7771 
7772 				recordChildCreated(pid, this);
7773 
7774 				// close our copy of the write side of the error reporting pipe
7775 				// so the read will immediately give eof when the fork closes it too
7776 				ErrnoEnforce!close(errorReportPipes[1]);
7777 
7778 				int[2] msg;
7779 				// this will block to wait for it to actually either start up or fail to exec (which should be near instant)
7780 				auto val = read(errorReportPipes[0], msg.ptr, msg.sizeof);
7781 
7782 				if(val == -1)
7783 					throw new ErrnoApiException("read error report", errno);
7784 
7785 				if(val == msg.sizeof) {
7786 					// error happened
7787 					// FIXME: keep the step part of the error report too
7788 					throw new ErrnoApiException("exec", msg[1]);
7789 				} else if(val == 0) {
7790 					// pipe closed, meaning exec succeeded
7791 				} else {
7792 					assert(0); // never supposed to happen
7793 				}
7794 
7795 				// set the ones we keep to close upon future execs
7796 				// FIXME should i set NOBLOCK at this time too? prolly should
7797 				setCloExec(stdinPipes[1]);
7798 				setCloExec(stdoutPipes[0]);
7799 				setCloExec(stderrPipes[0]);
7800 
7801 				// and close the others
7802 				ErrnoEnforce!close(stdinPipes[0]);
7803 				ErrnoEnforce!close(stdoutPipes[1]);
7804 				ErrnoEnforce!close(stderrPipes[1]);
7805 
7806 				ErrnoEnforce!close(errorReportPipes[0]);
7807 
7808 				makeNonBlocking(stdinFd);
7809 				makeNonBlocking(stdoutFd);
7810 				makeNonBlocking(stderrFd);
7811 
7812 				_stdin = new AsyncFile(stdinFd);
7813 				_stdout = new AsyncFile(stdoutFd);
7814 				_stderr = new AsyncFile(stderrFd);
7815 			}
7816 		} else version(Windows) {
7817 			WCharzBuffer program = this.program.path;
7818 			WCharzBuffer cmdLine = this.commandLine;
7819 
7820 			PROCESS_INFORMATION pi;
7821 			STARTUPINFOW startupInfo;
7822 
7823 			SECURITY_ATTRIBUTES saAttr;
7824 			saAttr.nLength = SECURITY_ATTRIBUTES.sizeof;
7825 			saAttr.bInheritHandle = true;
7826 			saAttr.lpSecurityDescriptor = null;
7827 
7828 			HANDLE inreadPipe;
7829 			HANDLE inwritePipe;
7830 			if(MyCreatePipeEx(&inreadPipe, &inwritePipe, &saAttr, 0, 0, FILE_FLAG_OVERLAPPED) == 0)
7831 				throw new WindowsApiException("CreatePipe", GetLastError());
7832 			if(!SetHandleInformation(inwritePipe, 1/*HANDLE_FLAG_INHERIT*/, 0))
7833 				throw new WindowsApiException("SetHandleInformation", GetLastError());
7834 
7835 			HANDLE outreadPipe;
7836 			HANDLE outwritePipe;
7837 			if(MyCreatePipeEx(&outreadPipe, &outwritePipe, &saAttr, 0, FILE_FLAG_OVERLAPPED, 0) == 0)
7838 				throw new WindowsApiException("CreatePipe", GetLastError());
7839 			if(!SetHandleInformation(outreadPipe, 1/*HANDLE_FLAG_INHERIT*/, 0))
7840 				throw new WindowsApiException("SetHandleInformation", GetLastError());
7841 
7842 			HANDLE errreadPipe;
7843 			HANDLE errwritePipe;
7844 			if(MyCreatePipeEx(&errreadPipe, &errwritePipe, &saAttr, 0, FILE_FLAG_OVERLAPPED, 0) == 0)
7845 				throw new WindowsApiException("CreatePipe", GetLastError());
7846 			if(!SetHandleInformation(errreadPipe, 1/*HANDLE_FLAG_INHERIT*/, 0))
7847 				throw new WindowsApiException("SetHandleInformation", GetLastError());
7848 
7849 			startupInfo.cb = startupInfo.sizeof;
7850 			startupInfo.dwFlags = STARTF_USESTDHANDLES;
7851 			startupInfo.hStdInput = inreadPipe;
7852 			startupInfo.hStdOutput = outwritePipe;
7853 			startupInfo.hStdError = errwritePipe;
7854 
7855 			auto result = CreateProcessW(
7856 				program.ptr,
7857 				cmdLine.ptr,
7858 				null, // process attributes
7859 				null, // thread attributes
7860 				true, // inherit handles; necessary for the std in/out/err ones to work
7861 				0, // dwCreationFlags FIXME might be useful to change
7862 				null, // environment, might be worth changing
7863 				null, // current directory
7864 				&startupInfo,
7865 				&pi
7866 			);
7867 
7868 			if(!result)
7869 				throw new WindowsApiException("CreateProcessW", GetLastError());
7870 
7871 			_stdin = new AsyncFile(inwritePipe);
7872 			_stdout = new AsyncFile(outreadPipe);
7873 			_stderr = new AsyncFile(errreadPipe);
7874 
7875 			Win32Enforce!CloseHandle(inreadPipe);
7876 			Win32Enforce!CloseHandle(outwritePipe);
7877 			Win32Enforce!CloseHandle(errwritePipe);
7878 
7879 			Win32Enforce!CloseHandle(pi.hThread);
7880 
7881 			handle = pi.hProcess;
7882 
7883 			procRegistration = getThisThreadEventLoop.addCallbackOnHandleReady(handle, new CallbackHelper(&almostComplete));
7884 		}
7885 	}
7886 
7887 	version(Windows) {
7888 		private HANDLE handle;
7889 		private FilePath program;
7890 		private string commandLine;
7891 		private ICoreEventLoop.UnregisterToken procRegistration;
7892 
7893 		private final void almostComplete() {
7894 			// GetProcessTimes lol
7895 			Win32Enforce!GetExitCodeProcess(handle, cast(uint*) &_status);
7896 
7897 			markComplete(_status);
7898 
7899 			procRegistration.unregister();
7900 			CloseHandle(handle);
7901 			this.completed = true;
7902 		}
7903 	} else version(Posix) {
7904 		import core.sys.posix.unistd;
7905 		import core.sys.posix.fcntl;
7906 
7907 		private pid_t pid = -1;
7908 
7909 		public void delegate() beforeExec;
7910 
7911 		private FilePath program;
7912 		private string[] args;
7913 	}
7914 
7915 	private final void markComplete(int status) {
7916 		completed = true;
7917 		_status = status;
7918 
7919 		if(oncomplete)
7920 			oncomplete(this);
7921 	}
7922 
7923 
7924 	private AsyncFile _stdin;
7925 	private AsyncFile _stdout;
7926 	private AsyncFile _stderr;
7927 
7928 	/++
7929 
7930 	+/
7931 	AsyncFile stdin() {
7932 		return _stdin;
7933 	}
7934 	/// ditto
7935 	AsyncFile stdout() {
7936 		return _stdout;
7937 	}
7938 	/// ditto
7939 	AsyncFile stderr() {
7940 		return _stderr;
7941 	}
7942 
7943 	/++
7944 	+/
7945 	void waitForCompletion() {
7946 		getThisThreadEventLoop().run(&this.isComplete);
7947 	}
7948 
7949 	/++
7950 	+/
7951 	bool isComplete() {
7952 		return completed;
7953 	}
7954 
7955 	private bool completed;
7956 	private int _status = int.min;
7957 
7958 	/++
7959 	+/
7960 	int status() {
7961 		return _status;
7962 	}
7963 
7964 	// void delegate(int code) onTermination;
7965 
7966 	void delegate(ExternalProcess) oncomplete;
7967 
7968 	// pty?
7969 }
7970 
7971 // FIXME: comment this out
7972 /+
7973 unittest {
7974 	auto proc = new ExternalProcess(FilePath("/bin/cat"), ["/bin/cat"]);
7975 
7976 	getThisThreadEventLoop(); // initialize it
7977 
7978 	int c = 0;
7979 	proc.onStdoutAvailable = delegate(ubyte[] got) {
7980 		if(c == 0)
7981 			assert(cast(string) got == "hello!");
7982 		else
7983 			assert(got.length == 0);
7984 			// import std.stdio; writeln(got);
7985 		c++;
7986 	};
7987 
7988 	proc.start();
7989 
7990 	assert(proc.pid != -1);
7991 
7992 
7993 	import std.stdio;
7994 	Thread[4] pool;
7995 
7996 	bool shouldExit;
7997 
7998 	static int received;
7999 
8000 	proc.writeToStdin("hello!");
8001 	proc.writeToStdin(null); // closes the pipe
8002 
8003 	proc.waitForCompletion();
8004 
8005 	assert(proc.status == 0);
8006 
8007 	assert(c == 2);
8008 
8009 	// writeln("here");
8010 }
8011 +/
8012 
8013 // to test the thundering herd on signal handling
8014 version(none)
8015 unittest {
8016 	Thread[4] pool;
8017 	foreach(ref thread; pool) {
8018 		thread = new class Thread {
8019 			this() {
8020 				super({
8021 					int count;
8022 					getThisThreadEventLoop().run(() {
8023 						if(count > 4) return true;
8024 						count++;
8025 						return false;
8026 					});
8027 				});
8028 			}
8029 		};
8030 		thread.start();
8031 	}
8032 	foreach(ref thread; pool) {
8033 		thread.join();
8034 	}
8035 }
8036 
8037 /+
8038 	================
8039 	LOGGER FRAMEWORK
8040 	================
8041 +/
8042 /++
8043 	The arsd.core logger works differently than many in that it works as a ring buffer of objects that are consumed (or missed; buffer overruns are possible) by a different thread instead of as strings written to some file.
8044 
8045 	A library (or an application) defines a log source. They write to this source.
8046 
8047 	Applications then define log sinks, zero or more, which reads from various sources and does something with them.
8048 
8049 	Log calls, in this sense, are quite similar to asynchronous events that can be subscribed to by event handlers. The difference is events are generally not dropped - they might coalesce but are usually not just plain dropped in a buffer overrun - whereas logs can be. If the log consumer can't keep up, the details are just lost. The log producer will not wait for the consumer to catch up.
8050 
8051 
8052 	An application can also set a default subscriber which applies to all log objects throughout.
8053 
8054 	All log message objects must be capable of being converted to strings and to json.
8055 
8056 	Ad-hoc messages can be done with interpolated sequences.
8057 
8058 	Messages automatically get a timestamp. They can also have file/line and maybe even a call stack.
8059 
8060 	Examples:
8061 	---
8062 		mixin LoggerOf!X mylogger;
8063 
8064 		mylogger.log(i"$this heartbeat"); // creates an ad-hoc log message
8065 	---
8066 
8067 	History:
8068 		Added May 27, 2024
8069 +/
8070 mixin template LoggerOf(T) {
8071 	void log(LogLevel l, T message) {
8072 
8073 	}
8074 }
8075 
8076 enum LogLevel {
8077 	Info
8078 }
8079 
8080 /+
8081 	=====================
8082 	TRANSLATION FRAMEWORK
8083 	=====================
8084 +/
8085 
8086 /++
8087 	Represents a translatable string.
8088 
8089 
8090 	This depends on interpolated expression sequences to be ergonomic to use and in most cases, a function that uses this should take it as `tstring name...`; a typesafe variadic (this is also why it is a class rather than a struct - D only supports this particular feature on classes).
8091 
8092 	You can use `null` as a tstring. You can also construct it with UFCS: `i"foo".tstring`.
8093 
8094 	The actual translation engine should be set on the application level.
8095 
8096 	It is possible to get all translatable string templates compiled into the application at runtime.
8097 
8098 	History:
8099 		Added June 23, 2024
8100 +/
8101 class tstring {
8102 	private GenericEmbeddableInterpolatedSequence geis;
8103 
8104 	/++
8105 		For a case where there is no plural specialization.
8106 	+/
8107 	this(Args...)(InterpolationHeader hdr, Args args, InterpolationFooter ftr) {
8108 		geis = GenericEmbeddableInterpolatedSequence(hdr, args, ftr);
8109 		tstringTemplateProcessor!(Args.length, Args) tp;
8110 	}
8111 
8112 	/+
8113 	/++
8114 		When here is a plural specialization this passes the default one.
8115 	+/
8116 	this(SArgs..., Pargs...)(
8117 		InterpolationHeader shdr, SArgs singularArgs, InterpolationFooter sftr,
8118 		InterpolationHeader phdr, PArgs pluralArgs, InterpolationFooter pftr
8119 	)
8120 	{
8121 		geis = GenericEmbeddableInterpolatedSequence(shdr, singularArgs, sftr);
8122 		//geis = GenericEmbeddableInterpolatedSequence(phdr, pluralArgs, pftr);
8123 
8124 		tstringTemplateProcessor!(Args.length, Args) tp;
8125 	}
8126 	+/
8127 
8128 	final override string toString() {
8129 		if(this is null)
8130 			return null;
8131 		if(translationEngine !is null)
8132 			return translationEngine.translate(geis);
8133 		else
8134 			return geis.toString();
8135 	}
8136 
8137 	static tstring opCall(Args...)(InterpolationHeader hdr, Args args, InterpolationFooter ftr) {
8138 		return new tstring(hdr, args, ftr);
8139 	}
8140 
8141 	/+ +++ +/
8142 
8143 	private static shared(TranslationEngine) translationEngine_ = null;
8144 
8145 	static shared(TranslationEngine) translationEngine() {
8146 		return translationEngine_;
8147 	}
8148 
8149 	static void translationEngine(shared TranslationEngine e) {
8150 		translationEngine_ = e;
8151 		if(e !is null) {
8152 			auto item = first;
8153 			while(item) {
8154 				e.handleTemplate(*item);
8155 				item = item.next;
8156 			}
8157 		}
8158 	}
8159 
8160 	public struct TranslatableElement {
8161 		string templ;
8162 		string pluralTempl;
8163 
8164 		TranslatableElement* next;
8165 	}
8166 
8167 	static __gshared TranslatableElement* first;
8168 
8169 	// FIXME: the template should be identified to the engine somehow
8170 
8171 	private static enum templateStringFor(Args...) = () {
8172 		int count;
8173 		string templ;
8174 		foreach(arg; Args) {
8175 			static if(is(arg == InterpolatedLiteral!str, string str))
8176 				templ ~= str;
8177 			else static if(is(arg == InterpolatedExpression!code, string code))
8178 				templ ~= "{" ~ cast(char)(++count + '0') ~ "}";
8179 		}
8180 		return templ;
8181 	}();
8182 
8183 	// this is here to inject static ctors so we can build up a runtime list from ct data
8184 	private static struct tstringTemplateProcessor(size_t pluralBegins, Args...) {
8185 		static __gshared TranslatableElement e = TranslatableElement(
8186 			templateStringFor!(Args[0 .. pluralBegins]),
8187 			templateStringFor!(Args[pluralBegins .. $]),
8188 			null /* next, filled in by the static ctor below */);
8189 
8190 		@standalone @system
8191 		shared static this() {
8192 			e.next = first;
8193 			first = &e;
8194 		}
8195 	}
8196 }
8197 
8198 /// ditto
8199 class TranslationEngine {
8200 	string translate(GenericEmbeddableInterpolatedSequence geis) shared {
8201 		return geis.toString();
8202 	}
8203 
8204 	/++
8205 		If the translation engine has been set early in the module
8206 		construction process (which it should be!)
8207 	+/
8208 	void handleTemplate(tstring.TranslatableElement t) shared {
8209 	}
8210 }
8211 
8212 private static template WillFitInGeis(Args...) {
8213 	static int lengthRequired() {
8214 		int place;
8215 		foreach(arg; Args) {
8216 			static if(is(arg == InterpolatedLiteral!str, string str)) {
8217 				if(place & 1) // can't put string in the data slot
8218 					place++;
8219 				place++;
8220 			} else static if(is(arg == InterpolationHeader) || is(arg == InterpolationFooter) || is(arg == InterpolatedExpression!code, string code)) {
8221 				// no storage required
8222 			} else {
8223 				if((place & 1) == 0) // can't put data in the string slot
8224 					place++;
8225 				place++;
8226 			}
8227 		}
8228 
8229 		if(place & 1)
8230 			place++;
8231 		return place / 2;
8232 	}
8233 
8234 	enum WillFitInGeis = lengthRequired() <= GenericEmbeddableInterpolatedSequence.seq.length;
8235 }
8236 
8237 
8238 /+
8239 	For making an array of istrings basically; it moves their CT magic to RT dynamic type.
8240 +/
8241 struct GenericEmbeddableInterpolatedSequence {
8242 	static struct Element {
8243 		string str; // these are pointers to string literals every time
8244 		LimitedVariant lv;
8245 	}
8246 
8247 	Element[8] seq;
8248 
8249 	this(Args...)(InterpolationHeader, Args args, InterpolationFooter) {
8250 		int place;
8251 		bool stringUsedInPlace;
8252 		bool overflowed;
8253 
8254 		static assert(WillFitInGeis!(Args), "Your interpolated elements will not fit in the generic buffer.");
8255 
8256 		foreach(arg; args) {
8257 			static if(is(typeof(arg) == InterpolatedLiteral!str, string str)) {
8258 				if(stringUsedInPlace) {
8259 					place++;
8260 					stringUsedInPlace = false;
8261 				}
8262 
8263 				if(place == seq.length) {
8264 					overflowed = true;
8265 					break;
8266 				}
8267 				seq[place].str = str;
8268 				stringUsedInPlace = true;
8269 			} else static if(is(typeof(arg) == InterpolationHeader) || is(typeof(arg) == InterpolationFooter)) {
8270 				static assert(0, "Cannot embed interpolated sequences");
8271 			} else static if(is(typeof(arg) == InterpolatedExpression!code, string code)) {
8272 				// irrelevant
8273 			} else {
8274 				if(place == seq.length) {
8275 					overflowed = true;
8276 					break;
8277 				}
8278 				seq[place].lv = LimitedVariant(arg);
8279 				place++;
8280 				stringUsedInPlace = false;
8281 			}
8282 		}
8283 	}
8284 
8285 	string toString() {
8286 		string s;
8287 		foreach(item; seq) {
8288 			if(item.str !is null)
8289 				s ~= item.str;
8290 			if(!item.lv.containsNull())
8291 				s ~= item.lv.toString();
8292 		}
8293 		return s;
8294 	}
8295 }
8296 
8297 private struct LoggedElement(T) {
8298 	LogLevel level; // ?
8299 	MonoTime timestamp;
8300 	void*[16] stack; // ?
8301 	string originComponent;
8302 	string originFile;
8303 	size_t originLine;
8304 
8305 	T message;
8306 }
8307 
8308 private class TypeErasedLogger {
8309 	ubyte[] buffer;
8310 
8311 	void*[] messagePointers;
8312 	size_t position;
8313 }
8314 
8315 
8316 
8317 
8318 /+
8319 	=================
8320 	STDIO REPLACEMENT
8321 	=================
8322 +/
8323 
8324 private void appendToBuffer(ref char[] buffer, ref int pos, scope const(char)[] what) {
8325 	auto required = pos + what.length;
8326 	if(buffer.length < required)
8327 		buffer.length = required;
8328 	buffer[pos .. pos + what.length] = what[];
8329 	pos += what.length;
8330 }
8331 
8332 private void appendToBuffer(ref char[] buffer, ref int pos, long what) {
8333 	if(buffer.length < pos + 16)
8334 		buffer.length = pos + 16;
8335 	auto sliced = intToString(what, buffer[pos .. $]);
8336 	pos += sliced.length;
8337 }
8338 
8339 /++
8340 	A `writeln` that actually works, at least for some basic types.
8341 
8342 	It works correctly on Windows, using the correct functions to write unicode to the console.  even allocating a console if needed. If the output has been redirected to a file or pipe, it writes UTF-8.
8343 
8344 	This always does text. See also WritableStream and WritableTextStream when they are implemented.
8345 +/
8346 void writeln(T...)(T t) {
8347 	char[256] bufferBacking;
8348 	char[] buffer = bufferBacking[];
8349 	int pos;
8350 
8351 	foreach(arg; t) {
8352 		static if(is(typeof(arg) : const char[])) {
8353 			appendToBuffer(buffer, pos, arg);
8354 		} else static if(is(typeof(arg) : stringz)) {
8355 			appendToBuffer(buffer, pos, arg.borrow);
8356 		} else static if(is(typeof(arg) : long)) {
8357 			appendToBuffer(buffer, pos, arg);
8358 		} else static if(is(typeof(arg.toString()) : const char[])) {
8359 			appendToBuffer(buffer, pos, arg.toString());
8360 		} else {
8361 			appendToBuffer(buffer, pos, "<" ~ typeof(arg).stringof ~ ">");
8362 		}
8363 	}
8364 
8365 	appendToBuffer(buffer, pos, "\n");
8366 
8367 	actuallyWriteToStdout(buffer[0 .. pos]);
8368 }
8369 
8370 private void actuallyWriteToStdout(scope char[] buffer) @trusted {
8371 
8372 	version(UseStdioWriteln)
8373 	{
8374 		import std.stdio;
8375 		writeln(buffer);
8376 	}
8377 	else version(Windows) {
8378 		import core.sys.windows.wincon;
8379 
8380 		auto hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
8381 		if(hStdOut == null || hStdOut == INVALID_HANDLE_VALUE) {
8382 			AllocConsole();
8383 			hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
8384 		}
8385 
8386 		if(GetFileType(hStdOut) == FILE_TYPE_CHAR) {
8387 			wchar[256] wbuffer;
8388 			auto toWrite = makeWindowsString(buffer, wbuffer, WindowsStringConversionFlags.convertNewLines);
8389 
8390 			DWORD written;
8391 			WriteConsoleW(hStdOut, toWrite.ptr, cast(DWORD) toWrite.length, &written, null);
8392 		} else {
8393 			DWORD written;
8394 			WriteFile(hStdOut, buffer.ptr, cast(DWORD) buffer.length, &written, null);
8395 		}
8396 	} else {
8397 		import unix = core.sys.posix.unistd;
8398 		unix.write(1, buffer.ptr, buffer.length);
8399 	}
8400 }
8401 
8402 /+
8403 
8404 STDIO
8405 
8406 	/++
8407 		Please note using this will create a compile-time dependency on [arsd.terminal]
8408 
8409 
8410 
8411 so my writeln replacement:
8412 
8413 1) if the std output handle is null, alloc one
8414 2) if it is a character device, write out the proper Unicode text.
8415 3) otherwise write out UTF-8.... maybe with a BOM but maybe not. it is tricky to know what the other end of a pipe expects...
8416 [8:15 AM]
8417 im actually tempted to make the write binary to stdout functions throw an exception if it is a character console / interactive terminal instead of letting you spam it right out
8418 [8:16 AM]
8419 of course you can still cheat by casting binary data to string and using the write string function (and this might be appropriate sometimes) but there kinda is a legit difference between a text output and a binary output device
8420 
8421 Stdout can represent either
8422 
8423 	+/
8424 	void writeln(){} {
8425 
8426 	}
8427 
8428 	stderr?
8429 
8430 	/++
8431 		Please note using this will create a compile-time dependency on [arsd.terminal]
8432 
8433 		It can be called from a task.
8434 
8435 		It works correctly on Windows and is user friendly on Linux (using arsd.terminal.getline)
8436 		while also working if stdin has been redirected (where arsd.terminal itself would throw)
8437 
8438 
8439 so say you run a program on an interactive terminal. the program tries to open the stdin binary stream
8440 
8441 instead of throwing, the prompt could change to indicate the binary data is expected and you can feed it in either by typing it up,,,,  or running some command like maybe <file.bin to have the library do what the shell would have done and feed that to the rest of the program
8442 
8443 	+/
8444 	string readln()() {
8445 
8446 	}
8447 
8448 
8449 	// if using stdio as a binary output thing you can pretend it is a file w/ stream capability
8450 	struct File {
8451 		WritableStream ostream;
8452 		ReadableStream istream;
8453 
8454 		ulong tell;
8455 		void seek(ulong to) {}
8456 
8457 		void sync();
8458 		void close();
8459 	}
8460 
8461 	// these are a bit special because if it actually is an interactive character device, it might be different than other files and even different than other pipes.
8462 	WritableStream stdoutStream() { return null; }
8463 	WritableStream stderrStream() { return null; }
8464 	ReadableStream stdinStream() { return null; }
8465 
8466 +/
8467 
8468 
8469 /+
8470 
8471 
8472 /+
8473 	Druntime appears to have stuff for darwin, freebsd. I might have to add some for openbsd here and maybe netbsd if i care to test it.
8474 +/
8475 
8476 /+
8477 
8478 	arsd_core_init(number_of_worker_threads)
8479 
8480 	Building-block things wanted for the event loop integration:
8481 		* ui
8482 			* windows
8483 			* terminal / console
8484 		* generic
8485 			* adopt fd
8486 			* adopt windows handle
8487 		* shared lib
8488 			* load
8489 		* timers (relative and real time)
8490 			* create
8491 			* update
8492 			* cancel
8493 		* file/directory watches
8494 			* file created
8495 			* file deleted
8496 			* file modified
8497 		* file ops
8498 			* open
8499 			* close
8500 			* read
8501 			* write
8502 			* seek
8503 			* sendfile on linux, TransmitFile on Windows
8504 			* let completion handlers run in the io worker thread instead of signaling back
8505 		* pipe ops (anonymous or named)
8506 			* create
8507 			* read
8508 			* write
8509 			* get info about other side of the pipe
8510 		* network ops (stream + datagram, ip, ipv6, unix)
8511 			* address look up
8512 			* connect
8513 			* start tls
8514 			* listen
8515 			* send
8516 			* receive
8517 			* get peer info
8518 		* process ops
8519 			* spawn
8520 			* notifications when it is terminated or fork or execs
8521 			* send signal
8522 			* i/o pipes
8523 		* thread ops (isDaemon?)
8524 			* spawn
8525 			* talk to its event loop
8526 			* termination notification
8527 		* signals
8528 			* ctrl+c is the only one i really care about but the others might be made available too. sigchld needs to be done as an impl detail of process ops.
8529 		* custom messages
8530 			* should be able to send messages from finalizers...
8531 
8532 		* want to make sure i can stream stuff on top of it all too.
8533 
8534 		========
8535 
8536 		These things all refer back to a task-local thing that queues the tasks. If it is a fiber, it uses that
8537 		and if it is a thread it uses that...
8538 
8539 		tls IArsdCoreEventLoop curentTaskInterface; // this yields on the wait for calls. the fiber swapper will swap this too.
8540 		tls IArsdCoreEventLoop currentThreadInterface; // this blocks on the event loop
8541 
8542 		shared IArsdCoreEventLoop currentProcessInterface; // this dispatches to any available thread
8543 +/
8544 
8545 
8546 /+
8547 	You might have configurable tasks that do not auto-start, e.g. httprequest. maybe @mustUse on those
8548 
8549 	then some that do auto-start, e.g. setTimeout
8550 
8551 
8552 	timeouts: duration, MonoTime, or SysTime? duration is just a timer monotime auto-adjusts the when, systime sets a real time timerfd
8553 
8554 	tasks can be set to:
8555 		thread affinity - this, any, specific reference
8556 		reports to - defaults to this, can also pass down a parent reference. if reports to dies, all its subordinates are cancelled.
8557 
8558 
8559 	you can send a message to a task... maybe maybe just to a task runner (which is itself a task?)
8560 
8561 	auto file = readFile(x);
8562 	auto timeout = setTimeout(y);
8563 	auto completed = waitForFirstToCompleteThenCancelOthers(file, timeout);
8564 	if(completed == 0) {
8565 		file....
8566 	} else {
8567 		timeout....
8568 	}
8569 
8570 	/+
8571 		A task will run on a thread (with possible migration), and report to a task.
8572 	+/
8573 
8574 	// a compute task is run on a helper thread
8575 	auto task = computeTask((shared(bool)* cancellationRequested) {
8576 		// or pass in a yield thing... prolly a TaskController which has cancellationRequested and yield controls as well as send message to parent (sync or async)
8577 
8578 		// you'd periodically send messages back to the parent
8579 	}, RunOn.AnyAvailable, Affinity.CanMigrate);
8580 
8581 	auto task = Task((TaskController controller) {
8582 		foreach(x, 0 .. 1000) {
8583 			if(x % 10 == 0)
8584 				controller.yield(); // periodically yield control, which also checks for cancellation for us
8585 			// do some work
8586 
8587 			controller.sendMessage(...);
8588 			controller.sendProgress(x); // yields it for a foreach stream kind of thing
8589 		}
8590 
8591 		return something; // automatically sends the something as the result in a TaskFinished message
8592 	});
8593 
8594 	foreach(item; task) // waitsForProgress, sendProgress sends an item and the final return sends an item
8595 		{}
8596 
8597 
8598 		see ~/test/task.d
8599 
8600 	// an io task is run locally via the event loops
8601 	auto task2 = ioTask(() {
8602 
8603 	});
8604 
8605 
8606 
8607 	waitForEvent
8608 +/
8609 
8610 /+
8611 	Most functions should prolly take a thread arg too, which defaults
8612 	to this thread, but you can also pass it a reference, or a "any available" thing.
8613 
8614 	This can be a ufcs overload
8615 +/
8616 
8617 interface SemiSynchronousTask {
8618 
8619 }
8620 
8621 struct TimeoutCompletionResult {
8622 	bool completed;
8623 
8624 	bool opCast(T : bool)() {
8625 		return completed;
8626 	}
8627 }
8628 
8629 struct Timeout {
8630 	void reschedule(Duration when) {
8631 
8632 	}
8633 
8634 	void cancel() {
8635 
8636 	}
8637 
8638 	TimeoutCompletionResult waitForCompletion() {
8639 		return TimeoutCompletionResult(false);
8640 	}
8641 }
8642 
8643 Timeout setTimeout(void delegate() dg, int msecs, int permittedJitter = 20) {
8644 static assert(0);
8645 	return Timeout.init;
8646 }
8647 
8648 void clearTimeout(Timeout timeout) {
8649 	timeout.cancel();
8650 }
8651 
8652 void createInterval() {}
8653 void clearInterval() {}
8654 
8655 /++
8656 	Schedules a task at the given wall clock time.
8657 +/
8658 void scheduleTask() {}
8659 
8660 struct IoOperationCompletionResult {
8661 	enum Status {
8662 		cancelled,
8663 		completed
8664 	}
8665 
8666 	Status status;
8667 
8668 	int error;
8669 	int bytesWritten;
8670 
8671 	bool opCast(T : bool)() {
8672 		return status == Status.completed;
8673 	}
8674 }
8675 
8676 struct IoOperation {
8677 	void cancel() {}
8678 
8679 	IoOperationCompletionResult waitForCompletion() {
8680 		return IoOperationCompletionResult.init;
8681 	}
8682 
8683 	// could contain a scoped class in here too so it stack allocated
8684 }
8685 
8686 // Should return both the object and the index in the array!
8687 Result waitForFirstToComplete(Operation[]...) {}
8688 
8689 IoOperation read(IoHandle handle, ubyte[] buffer
8690 
8691 /+
8692 	class IoOperation {}
8693 
8694 	// an io operation and its buffer must not be modified or freed
8695 	// in between a call to enqueue and a call to waitForCompletion
8696 	// if you used the whenComplete callback, make sure it is NOT gc'd or scope thing goes out of scope in the mean time
8697 	// if its dtor runs, it'd be forced to be cancelled...
8698 
8699 	scope IoOperation op = new IoOperation(buffer_size);
8700 	op.start();
8701 	op.waitForCompletion();
8702 +/
8703 
8704 /+
8705 	will want:
8706 		read, write
8707 		send, recv
8708 
8709 		cancel
8710 
8711 		open file, open (named or anonymous) pipe, open process
8712 		connect, accept
8713 		SSL
8714 		close
8715 
8716 		postEvent
8717 		postAPC? like run in gui thread / async
8718 		waitForEvent ? needs to handle a timeout and a cancellation. would only work in the fiber task api.
8719 
8720 		waitForSuccess
8721 
8722 		interrupt handler
8723 
8724 		onPosixReadReadiness
8725 		onPosixWriteReadiness
8726 
8727 		onWindowsHandleReadiness
8728 			- but they're one-offs so you gotta reregister for each event
8729 +/
8730 
8731 
8732 
8733 /+
8734 arsd.core.uda
8735 
8736 you define a model struct with the types you want to extract
8737 
8738 you get it with like Model extract(Model, UDAs...)(Model default)
8739 
8740 defaultModel!alias > defaultModel!Type(defaultModel("identifier"))
8741 
8742 
8743 
8744 
8745 
8746 
8747 
8748 
8749 
8750 
8751 so while i laid there sleep deprived i did think a lil more on some uda stuff. it isn't especially novel but a combination of a few other techniques
8752 
8753 you might be like
8754 
8755 struct MyUdas {
8756     DbName name;
8757     DbIgnore ignore;
8758 }
8759 
8760 elsewhere
8761 
8762 foreach(alias; allMembers) {
8763      auto udas = getUdas!(MyUdas, __traits(getAttributes, alias))(MyUdas(DbName(__traits(identifier, alias))));
8764 }
8765 
8766 
8767 so you pass the expected type and the attributes as the template params, then the runtime params are the default values for the given types
8768 
8769 so what the thing does essentially is just sets the values of the given thing to the udas based on type then returns the modified instance
8770 
8771 so the end result is you keep the last ones. it wouldn't report errors if multiple things added but it p simple to understand, simple to document (even though the default values are not in the struct itself, you can put ddocs in them), and uses the tricks to minimize generated code size
8772 +/
8773 
8774 +/
8775 
8776 package(arsd) version(Windows) extern(Windows) {
8777 	BOOL CancelIoEx(HANDLE, LPOVERLAPPED);
8778 
8779 	struct WSABUF {
8780 		ULONG len;
8781 		ubyte* buf;
8782 	}
8783 	alias LPWSABUF = WSABUF*;
8784 
8785 	// https://learn.microsoft.com/en-us/windows/win32/api/winsock2/ns-winsock2-wsaoverlapped
8786 	// "The WSAOVERLAPPED structure is compatible with the Windows OVERLAPPED structure."
8787 	// so ima lie here in the bindings.
8788 
8789 	int WSASend(SOCKET, LPWSABUF, DWORD, LPDWORD, DWORD, LPOVERLAPPED, LPOVERLAPPED_COMPLETION_ROUTINE);
8790 	int WSASendTo(SOCKET, LPWSABUF, DWORD, LPDWORD, DWORD, const sockaddr*, int, LPOVERLAPPED, LPOVERLAPPED_COMPLETION_ROUTINE);
8791 
8792 	int WSARecv(SOCKET, LPWSABUF, DWORD, LPDWORD, LPDWORD, LPOVERLAPPED, LPOVERLAPPED_COMPLETION_ROUTINE);
8793 	int WSARecvFrom(SOCKET, LPWSABUF, DWORD, LPDWORD, LPDWORD, sockaddr*, LPINT, LPOVERLAPPED, LPOVERLAPPED_COMPLETION_ROUTINE);
8794 }
8795 
8796 package(arsd) version(OSXCocoa) {
8797 
8798 /* Copy/paste chunk from Jacob Carlborg { */
8799 // from https://raw.githubusercontent.com/jacob-carlborg/druntime/550edd0a64f0eb2c4f35d3ec3d88e26b40ac779e/src/core/stdc/clang_block.d
8800 // with comments stripped (see docs in the original link), code reformatted, and some names changed to avoid potential conflicts
8801 
8802 import core.stdc.config;
8803 struct ObjCBlock(R = void, Params...) {
8804 private:
8805 	alias extern(C) R function(ObjCBlock*, Params) Invoke;
8806 
8807 	void* isa;
8808 	int flags;
8809 	int reserved = 0;
8810 	Invoke invoke;
8811 	Descriptor* descriptor;
8812 
8813 	// Imported variables go here
8814 	R delegate(Params) dg;
8815 
8816 	this(void* isa, int flags, Invoke invoke, R delegate(Params) dg) {
8817 		this.isa = isa;
8818 		this.flags = flags;
8819 		this.invoke = invoke;
8820 		this.dg = dg;
8821 		this.descriptor = &.objcblock_descriptor;
8822 	}
8823 }
8824 ObjCBlock!(R, Params) block(R, Params...)(R delegate(Params) dg) {
8825 	static if (Params.length == 0)
8826 	    enum flags = 0x50000000;
8827 	else
8828 		enum flags = 0x40000000;
8829 
8830 	return ObjCBlock!(R, Params)(&_NSConcreteStackBlock, flags, &objcblock_invoke!(R, Params), dg);
8831 }
8832 
8833 private struct Descriptor {
8834     c_ulong reserved;
8835     c_ulong size;
8836     const(char)* signature;
8837 }
8838 private extern(C) extern __gshared void*[32] _NSConcreteStackBlock;
8839 private __gshared auto objcblock_descriptor = Descriptor(0, ObjCBlock!().sizeof);
8840 private extern(C) R objcblock_invoke(R, Args...)(ObjCBlock!(R, Args)* block, Args args) {
8841     return block.dg(args);
8842 }
8843 
8844 
8845 /* End copy/paste chunk from Jacob Carlborg } */
8846 
8847 
8848 /+
8849 To let Cocoa know that you intend to use multiple threads, all you have to do is spawn a single thread using the NSThread class and let that thread immediately exit. Your thread entry point need not do anything. Just the act of spawning a thread using NSThread is enough to ensure that the locks needed by the Cocoa frameworks are put in place.
8850 
8851 If you are not sure if Cocoa thinks your application is multithreaded or not, you can use the isMultiThreaded method of NSThread to check.
8852 +/
8853 
8854 
8855 	struct DeifiedNSString {
8856 		char[16] sso;
8857 		const(char)[] str;
8858 
8859 		this(NSString s) {
8860 			auto len = s.length;
8861 			if(len <= sso.length / 4)
8862 				str = sso[];
8863 			else
8864 				str = new char[](len * 4);
8865 
8866 			NSUInteger count;
8867 			NSRange leftover;
8868 			auto ret = s.getBytes(cast(char*) str.ptr, str.length, &count, NSStringEncoding.NSUTF8StringEncoding, NSStringEncodingConversionOptions.none, NSRange(0, len), &leftover);
8869 			if(ret)
8870 				str = str[0 .. count];
8871 			else
8872 				throw new Exception("uh oh");
8873 		}
8874 	}
8875 
8876 	extern (Objective-C) {
8877 		import core.attribute; // : selector, optional;
8878 
8879 		alias NSUInteger = size_t;
8880 		alias NSInteger = ptrdiff_t;
8881 		alias unichar = wchar;
8882 		struct SEL_;
8883 		alias SEL_* SEL;
8884 		// this is called plain `id` in objective C but i fear mistakes with that in D. like sure it is a type instead of a variable like most things called id but i still think it is weird. i might change my mind later.
8885 		alias void* NSid; // FIXME? the docs say this is a pointer to an instance of a class, but that is not necessary a child of NSObject
8886 
8887 		extern class NSObject {
8888 			static NSObject alloc() @selector("alloc");
8889 			NSObject init() @selector("init");
8890 
8891 			void retain() @selector("retain");
8892 			void release() @selector("release");
8893 			void autorelease() @selector("autorelease");
8894 
8895 			void performSelectorOnMainThread(SEL aSelector, NSid arg, bool waitUntilDone) @selector("performSelectorOnMainThread:withObject:waitUntilDone:");
8896 		}
8897 
8898 		// this is some kind of generic in objc...
8899 		extern class NSArray : NSObject {
8900 			static NSArray arrayWithObjects(NSid* objects, NSUInteger count) @selector("arrayWithObjects:count:");
8901 		}
8902 
8903 		extern class NSString : NSObject {
8904 			override static NSString alloc() @selector("alloc");
8905 			override NSString init() @selector("init");
8906 
8907 			NSString initWithUTF8String(const scope char* str) @selector("initWithUTF8String:");
8908 
8909 			NSString initWithBytes(
8910 				const(ubyte)* bytes,
8911 				NSUInteger length,
8912 				NSStringEncoding encoding
8913 			) @selector("initWithBytes:length:encoding:");
8914 
8915 			unichar characterAtIndex(NSUInteger index) @selector("characterAtIndex:");
8916 			NSUInteger length() @selector("length");
8917 			const char* UTF8String() @selector("UTF8String");
8918 
8919 			void getCharacters(wchar* buffer, NSRange range) @selector("getCharacters:range:");
8920 
8921 			bool getBytes(void* buffer, NSUInteger maxBufferCount, NSUInteger* usedBufferCount, NSStringEncoding encoding, NSStringEncodingConversionOptions options, NSRange range, NSRange* leftover) @selector("getBytes:maxLength:usedLength:encoding:options:range:remainingRange:");
8922 		}
8923 
8924 		struct NSRange {
8925 			NSUInteger loc;
8926 			NSUInteger len;
8927 		}
8928 
8929 		enum NSStringEncodingConversionOptions : NSInteger {
8930 			none = 0,
8931 			NSAllowLossyEncodingConversion = 1,
8932 			NSExternalRepresentationEncodingConversion = 2
8933 		}
8934 
8935 		enum NSEventType {
8936 			idk
8937 
8938 		}
8939 
8940 		enum NSEventModifierFlags : NSUInteger {
8941 			NSEventModifierFlagCapsLock = 1 << 16,
8942 			NSEventModifierFlagShift = 1 << 17,
8943 			NSEventModifierFlagControl = 1 << 18,
8944 			NSEventModifierFlagOption = 1 << 19, // aka Alt
8945 			NSEventModifierFlagCommand = 1 << 20, // aka super
8946 			NSEventModifierFlagNumericPad = 1 << 21,
8947 			NSEventModifierFlagHelp = 1 << 22,
8948 			NSEventModifierFlagFunction = 1 << 23,
8949 			NSEventModifierFlagDeviceIndependentFlagsMask = 0xffff0000UL
8950 		}
8951 
8952 		version(OSX)
8953 		extern class NSEvent : NSObject {
8954 			NSEventType type() @selector("type");
8955 
8956 			NSPoint locationInWindow() @selector("locationInWindow");
8957 			NSTimeInterval timestamp() @selector("timestamp");
8958 			NSWindow window() @selector("window"); // note: nullable
8959 			NSEventModifierFlags modifierFlags() @selector("modifierFlags");
8960 
8961 			NSString characters() @selector("characters");
8962 			NSString charactersIgnoringModifiers() @selector("charactersIgnoringModifiers");
8963 			ushort keyCode() @selector("keyCode");
8964 			ushort specialKey() @selector("specialKey");
8965 
8966 			static NSUInteger pressedMouseButtons() @selector("pressedMouseButtons");
8967 			NSPoint locationInWindow() @selector("locationInWindow"); // in screen coordinates
8968 			static NSPoint mouseLocation() @selector("mouseLocation"); // in screen coordinates
8969 			NSInteger buttonNumber() @selector("buttonNumber");
8970 
8971 			CGFloat deltaX() @selector("deltaX");
8972 			CGFloat deltaY() @selector("deltaY");
8973 			CGFloat deltaZ() @selector("deltaZ");
8974 
8975 			bool hasPreciseScrollingDeltas() @selector("hasPreciseScrollingDeltas");
8976 
8977 			CGFloat scrollingDeltaX() @selector("scrollingDeltaX");
8978 			CGFloat scrollingDeltaY() @selector("scrollingDeltaY");
8979 
8980 			// @property(getter=isDirectionInvertedFromDevice, readonly) BOOL directionInvertedFromDevice;
8981 		}
8982 
8983 		extern /* final */ class NSTimer : NSObject { // the docs say don't subclass this, but making it final breaks the bridge
8984 			override static NSTimer alloc() @selector("alloc");
8985 			override NSTimer init() @selector("init");
8986 
8987 			static NSTimer schedule(NSTimeInterval timeIntervalInSeconds, NSid target, SEL selector, NSid userInfo, bool repeats) @selector("scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:");
8988 
8989 			void fire() @selector("fire");
8990 			void invalidate() @selector("invalidate");
8991 
8992 			bool valid() @selector("isValid");
8993 			// @property(copy) NSDate *fireDate;
8994 			NSTimeInterval timeInterval() @selector("timeInterval");
8995 			NSid userInfo() @selector("userInfo");
8996 
8997 			NSTimeInterval tolerance() @selector("tolerance");
8998 			NSTimeInterval tolerance(NSTimeInterval) @selector("setTolerance:");
8999 		}
9000 
9001 		alias NSTimeInterval = double;
9002 
9003 		version(OSX)
9004 		extern class NSResponder : NSObject {
9005 			NSMenu menu() @selector("menu");
9006 			void menu(NSMenu menu) @selector("setMenu:");
9007 
9008 			void keyDown(NSEvent event) @selector("keyDown:");
9009 			void keyUp(NSEvent event) @selector("keyUp:");
9010 
9011 			// - (void)interpretKeyEvents:(NSArray<NSEvent *> *)eventArray;
9012 
9013 			void mouseDown(NSEvent event) @selector("mouseDown:");
9014 			void mouseDragged(NSEvent event) @selector("mouseDragged:");
9015 			void mouseUp(NSEvent event) @selector("mouseUp:");
9016 			void mouseMoved(NSEvent event) @selector("mouseMoved:");
9017 			void mouseEntered(NSEvent event) @selector("mouseEntered:");
9018 			void mouseExited(NSEvent event) @selector("mouseExited:");
9019 
9020 			void rightMouseDown(NSEvent event) @selector("rightMouseDown:");
9021 			void rightMouseDragged(NSEvent event) @selector("rightMouseDragged:");
9022 			void rightMouseUp(NSEvent event) @selector("rightMouseUp:");
9023 
9024 			void otherMouseDown(NSEvent event) @selector("otherMouseDown:");
9025 			void otherMouseDragged(NSEvent event) @selector("otherMouseDragged:");
9026 			void otherMouseUp(NSEvent event) @selector("otherMouseUp:");
9027 
9028 			void scrollWheel(NSEvent event) @selector("scrollWheel:");
9029 
9030 			// touch events should also be here btw among others
9031 		}
9032 
9033 		version(OSX)
9034 		extern class NSApplication : NSResponder {
9035 			static NSApplication shared_() @selector("sharedApplication");
9036 
9037 			NSApplicationDelegate delegate_() @selector("delegate");
9038 			void delegate_(NSApplicationDelegate) @selector("setDelegate:");
9039 
9040 			bool setActivationPolicy(NSApplicationActivationPolicy activationPolicy) @selector("setActivationPolicy:");
9041 
9042 			void activateIgnoringOtherApps(bool flag) @selector("activateIgnoringOtherApps:");
9043 
9044 			@property NSMenu mainMenu() @selector("mainMenu");
9045 			@property NSMenu mainMenu(NSMenu) @selector("setMainMenu:");
9046 
9047 			void run() @selector("run");
9048 
9049 			void terminate(void*) @selector("terminate:");
9050 		}
9051 
9052 		version(OSX)
9053 		extern interface NSApplicationDelegate {
9054 			void applicationWillFinishLaunching(NSNotification notification) @selector("applicationWillFinishLaunching:");
9055 			void applicationDidFinishLaunching(NSNotification notification) @selector("applicationDidFinishLaunching:");
9056 			bool applicationShouldTerminateAfterLastWindowClosed(NSNotification notification) @selector("applicationShouldTerminateAfterLastWindowClosed:");
9057 		}
9058 
9059 		extern class NSNotification : NSObject {
9060 			@property NSid object() @selector("object");
9061 		}
9062 
9063 		enum NSApplicationActivationPolicy : ptrdiff_t {
9064 			/* The application is an ordinary app that appears in the Dock and may have a user interface.  This is the default for bundled apps, unless overridden in the Info.plist. */
9065 			regular,
9066 
9067 			/* The application does not appear in the Dock and does not have a menu bar, but it may be activated programmatically or by clicking on one of its windows.  This corresponds to LSUIElement=1 in the Info.plist. */
9068 			accessory,
9069 
9070 			/* The application does not appear in the Dock and may not create windows or be activated.  This corresponds to LSBackgroundOnly=1 in the Info.plist.  This is also the default for unbundled executables that do not have Info.plists. */
9071 			prohibited
9072 		}
9073 
9074 		extern class NSGraphicsContext : NSObject {
9075 			static NSGraphicsContext currentContext() @selector("currentContext");
9076 			NSGraphicsContext graphicsPort() @selector("graphicsPort");
9077 		}
9078 
9079 		version(OSX)
9080 		extern class NSMenu : NSObject {
9081 			override static NSMenu alloc() @selector("alloc");
9082 
9083 			override NSMenu init() @selector("init");
9084 			NSMenu init(NSString title) @selector("initWithTitle:");
9085 
9086 			void setSubmenu(NSMenu menu, NSMenuItem item) @selector("setSubmenu:forItem:");
9087 			void addItem(NSMenuItem newItem) @selector("addItem:");
9088 
9089 			NSMenuItem addItem(
9090 				NSString title,
9091 				SEL selector,
9092 				NSString charCode
9093 			) @selector("addItemWithTitle:action:keyEquivalent:");
9094 		}
9095 
9096 		version(OSX)
9097 		extern class NSMenuItem : NSObject {
9098 			override static NSMenuItem alloc() @selector("alloc");
9099 			override NSMenuItem init() @selector("init");
9100 
9101 			NSMenuItem init(
9102 				NSString title,
9103 				SEL selector,
9104 				NSString charCode
9105 			) @selector("initWithTitle:action:keyEquivalent:");
9106 
9107 			void enabled(bool) @selector("setEnabled:");
9108 
9109 			NSResponder target(NSResponder) @selector("setTarget:");
9110 		}
9111 
9112 		enum NSWindowStyleMask : size_t {
9113 			borderless = 0,
9114 			titled = 1 << 0,
9115 			closable = 1 << 1,
9116 			miniaturizable = 1 << 2,
9117 			resizable	= 1 << 3,
9118 
9119 			/* Specifies a window with textured background. Textured windows generally don't draw a top border line under the titlebar/toolbar. To get that line, use the NSUnifiedTitleAndToolbarWindowMask mask.
9120 			 */
9121 			texturedBackground = 1 << 8,
9122 
9123 			/* Specifies a window whose titlebar and toolbar have a unified look - that is, a continuous background. Under the titlebar and toolbar a horizontal separator line will appear.
9124 			 */
9125 			unifiedTitleAndToolbar = 1 << 12,
9126 
9127 			/* When set, the window will appear full screen. This mask is automatically toggled when toggleFullScreen: is called.
9128 			 */
9129 			fullScreen = 1 << 14,
9130 
9131 			/* If set, the contentView will consume the full size of the window; it can be combined with other window style masks, but is only respected for windows with a titlebar.
9132 			 Utilizing this mask opts-in to layer-backing. Utilize the contentLayoutRect or auto-layout contentLayoutGuide to layout views underneath the titlebar/toolbar area.
9133 			 */
9134 			fullSizeContentView = 1 << 15,
9135 
9136 			/* The following are only applicable for NSPanel (or a subclass thereof)
9137 			 */
9138 			utilityWindow			= 1 << 4,
9139 			docModalWindow		 = 1 << 6,
9140 			nonactivatingPanel		= 1 << 7, // Specifies that a panel that does not activate the owning application
9141 			hUDWindow = 1 << 13 // Specifies a heads up display panel
9142 		}
9143 
9144 		version(OSX)
9145 		extern class NSWindow : NSObject {
9146 			override static NSWindow alloc() @selector("alloc");
9147 
9148 			override NSWindow init() @selector("init");
9149 
9150 			NSWindow initWithContentRect(
9151 				NSRect contentRect,
9152 				NSWindowStyleMask style,
9153 				NSBackingStoreType bufferingType,
9154 				bool flag
9155 			) @selector("initWithContentRect:styleMask:backing:defer:");
9156 
9157 			void makeKeyAndOrderFront(NSid sender) @selector("makeKeyAndOrderFront:");
9158 			NSView contentView() @selector("contentView");
9159 			void contentView(NSView view) @selector("setContentView:");
9160 			void orderFrontRegardless() @selector("orderFrontRegardless");
9161 			void center() @selector("center");
9162 
9163 			NSRect frame() @selector("frame");
9164 
9165 			NSRect contentRectForFrameRect(NSRect frameRect) @selector("contentRectForFrameRect:");
9166 
9167 			NSString title() @selector("title");
9168 			void title(NSString value) @selector("setTitle:");
9169 
9170 			void close() @selector("close");
9171 
9172 			NSWindowDelegate delegate_() @selector("delegate");
9173 			void delegate_(NSWindowDelegate) @selector("setDelegate:");
9174 
9175 			void setBackgroundColor(NSColor color) @selector("setBackgroundColor:");
9176 		}
9177 
9178 		version(OSX)
9179 		extern interface NSWindowDelegate {
9180 			@optional:
9181 			void windowDidResize(NSNotification notification) @selector("windowDidResize:");
9182 
9183 			NSSize windowWillResize(NSWindow sender, NSSize frameSize) @selector("windowWillResize:toSize:");
9184 
9185 			void windowWillClose(NSNotification notification) @selector("windowWillClose:");
9186 		}
9187 
9188 		version(OSX)
9189 		extern class NSView : NSResponder {
9190 			//override NSView init() @selector("init");
9191 			NSView initWithFrame(NSRect frameRect) @selector("initWithFrame:");
9192 
9193 			void addSubview(NSView view) @selector("addSubview:");
9194 
9195 			bool wantsLayer() @selector("wantsLayer");
9196 			void wantsLayer(bool value) @selector("setWantsLayer:");
9197 
9198 			CALayer layer() @selector("layer");
9199 			void uiDelegate(NSObject) @selector("setUIDelegate:");
9200 
9201 			void drawRect(NSRect rect) @selector("drawRect:");
9202 			bool isFlipped() @selector("isFlipped");
9203 			bool acceptsFirstResponder() @selector("acceptsFirstResponder");
9204 			bool setNeedsDisplay(bool) @selector("setNeedsDisplay:");
9205 
9206 			// DO NOT USE: https://issues.dlang.org/show_bug.cgi?id=19017
9207 			// an asm { pop RAX; } after getting the struct can kinda hack around this but still
9208 			@property NSRect frame() @selector("frame");
9209 			@property NSRect frame(NSRect rect) @selector("setFrame:");
9210 
9211 			void setFrameSize(NSSize newSize) @selector("setFrameSize:");
9212 			void setFrameOrigin(NSPoint newOrigin) @selector("setFrameOrigin:");
9213 
9214 			void addSubview(NSView what) @selector("addSubview:");
9215 			void removeFromSuperview() @selector("removeFromSuperview");
9216 		}
9217 
9218 		extern class NSFont : NSObject {
9219 			void set() @selector("set"); // sets it into the current graphics context
9220 			void setInContext(NSGraphicsContext context) @selector("setInContext:");
9221 
9222 			static NSFont fontWithName(NSString fontName, CGFloat fontSize) @selector("fontWithName:size:");
9223 			// fontWithDescriptor too
9224 			// fontWithName and matrix too
9225 			static NSFont systemFontOfSize(CGFloat fontSize) @selector("systemFontOfSize:");
9226 			// among others
9227 
9228 			@property CGFloat pointSize() @selector("pointSize");
9229 			@property bool isFixedPitch() @selector("isFixedPitch");
9230 			// fontDescriptor
9231 			@property NSString displayName() @selector("displayName");
9232 
9233 			@property CGFloat ascender() @selector("ascender");
9234 			@property CGFloat descender() @selector("descender"); // note it is negative
9235 			@property CGFloat capHeight() @selector("capHeight");
9236 			@property CGFloat leading() @selector("leading");
9237 			@property CGFloat xHeight() @selector("xHeight");
9238 			// among many more
9239 		}
9240 
9241 		extern class NSColor : NSObject {
9242 			override static NSColor alloc() @selector("alloc");
9243 			static NSColor redColor() @selector("redColor");
9244 			static NSColor whiteColor() @selector("whiteColor");
9245 
9246 			CGColorRef CGColor() @selector("CGColor");
9247 		}
9248 
9249 		extern class CALayer : NSObject {
9250 			CGFloat borderWidth() @selector("borderWidth");
9251 			void borderWidth(CGFloat value) @selector("setBorderWidth:");
9252 
9253 			CGColorRef borderColor() @selector("borderColor");
9254 			void borderColor(CGColorRef) @selector("setBorderColor:");
9255 		}
9256 
9257 
9258 		version(OSX)
9259 		extern class NSViewController : NSObject {
9260 			NSView view() @selector("view");
9261 			void view(NSView view) @selector("setView:");
9262 		}
9263 
9264 		enum NSBackingStoreType : size_t {
9265 			retained = 0,
9266 			nonretained = 1,
9267 			buffered = 2
9268 		}
9269 
9270 		enum NSStringEncoding : NSUInteger {
9271 			NSASCIIStringEncoding = 1,		/* 0..127 only */
9272 			NSUTF8StringEncoding = 4,
9273 			NSUnicodeStringEncoding = 10,
9274 
9275 			NSUTF16StringEncoding = NSUnicodeStringEncoding,
9276 			NSUTF16BigEndianStringEncoding = 0x90000100,
9277 			NSUTF16LittleEndianStringEncoding = 0x94000100,
9278 			NSUTF32StringEncoding = 0x8c000100,
9279 			NSUTF32BigEndianStringEncoding = 0x98000100,
9280 			NSUTF32LittleEndianStringEncoding = 0x9c000100
9281 		}
9282 
9283 
9284 		struct CGColor;
9285 		alias CGColorRef = CGColor*;
9286 
9287 		// note on the watch os it is float, not double
9288 		alias CGFloat = double;
9289 
9290 		struct NSPoint {
9291 			CGFloat x;
9292 			CGFloat y;
9293 		}
9294 
9295 		struct NSSize {
9296 			CGFloat width;
9297 			CGFloat height;
9298 		}
9299 
9300 		struct NSRect {
9301 			NSPoint origin;
9302 			NSSize size;
9303 		}
9304 
9305 		alias NSPoint CGPoint;
9306 		alias NSSize CGSize;
9307 		alias NSRect CGRect;
9308 
9309 		pragma(inline, true) NSPoint NSMakePoint(CGFloat x, CGFloat y) {
9310 			NSPoint p;
9311 			p.x = x;
9312 			p.y = y;
9313 			return p;
9314 		}
9315 
9316 		pragma(inline, true) NSSize NSMakeSize(CGFloat w, CGFloat h) {
9317 			NSSize s;
9318 			s.width = w;
9319 			s.height = h;
9320 			return s;
9321 		}
9322 
9323 		pragma(inline, true) NSRect NSMakeRect(CGFloat x, CGFloat y, CGFloat w, CGFloat h) {
9324 			NSRect r;
9325 			r.origin.x = x;
9326 			r.origin.y = y;
9327 			r.size.width = w;
9328 			r.size.height = h;
9329 			return r;
9330 		}
9331 
9332 
9333 	}
9334 
9335 	// helper raii refcount object
9336 	static if(UseCocoa)
9337 	struct MacString {
9338 		union {
9339 			// must be wrapped cuz of bug in dmd
9340 			// referencing an init symbol when it should
9341 			// just be null. but the union makes it work
9342 			NSString s;
9343 		}
9344 
9345 		// FIXME: if a string literal it would be kinda nice to use
9346 		// the other function. but meh
9347 
9348 		this(scope const char[] str) {
9349 			this.s = NSString.alloc.initWithBytes(
9350 				cast(const(ubyte)*) str.ptr,
9351 				str.length,
9352 				NSStringEncoding.NSUTF8StringEncoding
9353 			);
9354 		}
9355 
9356 		NSString borrow() {
9357 			return s;
9358 		}
9359 
9360 		this(this) {
9361 			if(s !is null)
9362 				s.retain();
9363 		}
9364 
9365 		~this() {
9366 			if(s !is null) {
9367 				s.release();
9368 				s = null;
9369 			}
9370 		}
9371 	}
9372 
9373 	extern(C) void NSLog(NSString, ...);
9374 	extern(C) SEL sel_registerName(const(char)* str);
9375 
9376 	version(OSX)
9377 	extern (Objective-C) __gshared NSApplication NSApp_;
9378 
9379 	version(OSX)
9380 	NSApplication NSApp() {
9381 		if(NSApp_ is null)
9382 			NSApp_ = NSApplication.shared_;
9383 		return NSApp_;
9384 	}
9385 
9386 	version(DigitalMars) {
9387 	// hacks to work around compiler bug
9388 	extern(C) __gshared void* _D4arsd4core17NSGraphicsContext7__ClassZ = null;
9389 	extern(C) __gshared void* _D4arsd4core6NSView7__ClassZ = null;
9390 	extern(C) __gshared void* _D4arsd4core8NSWindow7__ClassZ = null;
9391 	}
9392 }