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