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