diff --git a/README.md b/README.md
index 6a241d8..8e35bc0 100644
--- a/README.md
+++ b/README.md
@@ -4,10 +4,10 @@ An implementation of [openkal](https://github.com/mcpplibs/openkal) for Windows.
```toml
[dependencies]
-openkal = "0.15.0"
+openkal = "0.16.0"
[target.'cfg(windows)'.dependencies]
-openkal-windows = "0.11.0"
+openkal-windows = "0.12.0"
```
Its purpose is as much to test the specification as to be used. openkal was
diff --git a/mcpp.toml b/mcpp.toml
index 7cea71d..ebb0674 100644
--- a/mcpp.toml
+++ b/mcpp.toml
@@ -1,7 +1,7 @@
[package]
namespace = "mcpplibs"
name = "openkal-windows"
-version = "0.11.0"
+version = "0.12.0"
description = "An implementation of openkal for Windows, written on the Win32 interfaces and the object manager beneath them, using no C runtime symbol."
license = "Apache-2.0"
@@ -62,7 +62,7 @@ provides-interfaces = [
]
[dependencies]
-openkal = "0.15.0"
+openkal = "0.16.0"
# The package contributes definitions and no modules. The interface it
# implements is declared by the specification package, which this package
diff --git a/src/fs.cpp b/src/fs.cpp
index 33e07ac..3a0fdf1 100644
--- a/src/fs.cpp
+++ b/src/fs.cpp
@@ -230,6 +230,10 @@ void fill_absent(kal_node_info* out) {
// each other's entries.
struct listing {
void* handle;
+ void* dir; // the handle of the directory it was begun upon
+ listing* next;
+ int busy; // a context is inside kal_fs_list_next with it
+ int doomed; // its directory was closed while it was busy
okw_uptr used;
okw_uptr at;
bool first;
@@ -248,6 +252,72 @@ struct listing {
char reported[512];
};
+// THE ENUMERATIONS NOT YET READ TO THE END, BECAUSE CLOSING THE DIRECTORY MUST
+// END THEM. fs.h releases an iterator "by reading past the end or by closing
+// the directory that produced it", and the handle a listing holds is its own,
+// so closing the directory's handle does not reach it. Measured on the Linux
+// implementation, which had the same shape (mcpplibs/openkal-linux#34): a C
+// library above that stopped reading a directory early kept a handle for every
+// directory it closed.
+//
+// A slim lock, because it is one pointer, all-zero is unlocked, and it needs
+// nothing from openkal.task.
+listing* g_listings = nullptr;
+SRWLOCK_ g_listings_lock{};
+
+void listing_release(listing* s) {
+ okw::NtClose(s->handle);
+ kal_free(s, sizeof(listing), alignof(listing));
+}
+
+// Ends every listing begun upon `dir'. A listing another context is reading is
+// marked rather than released, and that context releases it on its way out:
+// clause 6.6 requires that concurrent operations upon one handle not damage
+// this implementation's state, and freeing a buffer the system is writing into
+// would.
+void listings_end(void* dir) {
+ listing* done = nullptr;
+ AcquireSRWLockExclusive(&g_listings_lock);
+ for (listing** p = &g_listings; *p != nullptr;) {
+ listing* s = *p;
+ if (s->dir != dir || s->doomed) { p = &s->next; continue; }
+ if (s->busy) { s->doomed = 1; p = &s->next; continue; }
+ *p = s->next; s->next = done; done = s;
+ }
+ ReleaseSRWLockExclusive(&g_listings_lock);
+ while (done != nullptr) { listing* s = done; done = s->next; listing_release(s); }
+}
+
+// Takes `s' for the calling context, or reports that it is not an open listing
+// of `dir'. The word is found among the open listings rather than read through,
+// so a stale iterator is refused rather than followed.
+bool listing_take(listing* s, void* dir) {
+ for (;;) {
+ AcquireSRWLockExclusive(&g_listings_lock);
+ listing* p = g_listings;
+ while (p != nullptr && p != s) p = p->next;
+ if (p == nullptr || p->dir != dir || p->doomed) {
+ ReleaseSRWLockExclusive(&g_listings_lock);
+ return false;
+ }
+ if (!p->busy) { p->busy = 1; ReleaseSRWLockExclusive(&g_listings_lock); return true; }
+ ReleaseSRWLockExclusive(&g_listings_lock);
+ SwitchToThread();
+ }
+}
+
+void listing_give(listing* s, bool ended) {
+ AcquireSRWLockExclusive(&g_listings_lock);
+ s->busy = 0;
+ const bool release = ended || s->doomed;
+ if (release) {
+ for (listing** p = &g_listings; *p != nullptr; p = &(*p)->next)
+ if (*p == s) { *p = s->next; break; }
+ }
+ ReleaseSRWLockExclusive(&g_listings_lock);
+ if (release) listing_release(s);
+}
+
} // namespace
extern "C" {
@@ -348,7 +418,11 @@ int kal_fs_open(kal_dir base, const char* name, kal_uintptr len,
void kal_fs_close_dir(kal_dir d) {
void* h = dir_handle(d);
- if (h) { okw::retire(d.h); okw::NtClose(h); }
+ if (!h) return;
+ // Before the close, because after it the same value may already name
+ // another directory whose enumerations are not this one's.
+ listings_end(h);
+ okw::retire(d.h); okw::NtClose(h);
}
void kal_fs_close_file(kal_file f) {
@@ -647,16 +721,24 @@ int kal_fs_list_begin(kal_dir d, kal_uintptr* iter) {
auto* s = static_cast
(kal_alloc(sizeof(listing), alignof(listing)));
if (!s) { okw::NtClose(own); return kal_err_no_memory; }
- s->handle = own; s->used = 0; s->at = 0; s->first = true;
+ s->handle = own; s->dir = root; s->busy = 0; s->doomed = 0;
+ s->used = 0; s->at = 0; s->first = true;
+ AcquireSRWLockExclusive(&g_listings_lock);
+ s->next = g_listings; g_listings = s;
+ ReleaseSRWLockExclusive(&g_listings_lock);
*iter = reinterpret_cast(s);
return kal_ok;
}
-int kal_fs_list_next(kal_dir, kal_uintptr* iter,
+int kal_fs_list_next(kal_dir d, kal_uintptr* iter,
char* name_out, kal_uintptr name_cap,
kal_uintptr* name_len, int* kind) {
- if (iter == nullptr || *iter == 0) return kal_err_invalid;
+ // A closed directory is a released handle (clause 6.7), and closing it
+ // released the iterator too, which fs.h states.
+ void* root = dir_handle(d);
+ if (!root || iter == nullptr || *iter == 0) return kal_err_invalid;
auto* s = reinterpret_cast(*iter);
+ if (!listing_take(s, root)) return kal_err_invalid;
for (;;) {
if (s->at >= s->used) {
@@ -667,8 +749,7 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter,
okw::file_directory_information_class, 0, nullptr, s->first ? 1 : 0);
s->first = false;
if (!okw::ok(r)) {
- okw::NtClose(s->handle);
- kal_free(s, sizeof(listing), alignof(listing));
+ listing_give(s, true);
*iter = 0;
if (name_len) *name_len = 0;
// The end of a directory is reported here as a distinct status
@@ -697,6 +778,7 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter,
if (kind) *kind = (e->file_attributes & FILE_ATTRIBUTE_REPARSE_POINT) ? kal_node_link
: (e->file_attributes & FILE_ATTRIBUTE_DIRECTORY) ? kal_node_directory
: kal_node_file;
+ listing_give(s, false);
return kal_ok;
}
}
diff --git a/src/task.cpp b/src/task.cpp
index 203e383..957a590 100644
--- a/src/task.cpp
+++ b/src/task.cpp
@@ -28,19 +28,41 @@ DWORD __stdcall run(void* p) {
return 0;
}
-} // namespace
+// THE SIZE A CONTEXT IS GIVEN WHEN IT ASKS FOR NONE. This environment would
+// reserve what the program's header states, 1 MiB by default, and the two other
+// implementations give a context the 8 MiB the host's C library on their systems
+// gives a thread; a program that asks for nothing --- C++'s `std::thread' cannot
+// ask --- is given the same room on all three. It is a reservation, committed as
+// the stack grows, so the size costs address space and not memory.
+//
+// No guard is added. This environment places its own below the committed part
+// of the stack and moves it as the stack grows, and the compilers that target it
+// probe a frame larger than a page one page at a time, so a large frame cannot
+// step over it.
+constexpr unsigned long long kDefaultStack = 8ull * 1024u * 1024u;
+
+// What a request for `want' bytes is reserved as. The bounds this environment
+// reports to the context include the pages it keeps for its guard, so the
+// reservation exceeds the request by more than those; what the context is told
+// by kal_task_stack is then at least what it asked for.
+constexpr unsigned long long kSlack = 256u * 1024u;
+
+unsigned long long stack_for(unsigned long long want) {
+ if (want > (1ull << 46)) return 0;
+ const unsigned long long w = (want + 0xffffu) & ~0xffffull; // the reservation granularity
+ return (w > kDefaultStack ? w : kDefaultStack) + kSlack;
+}
-extern "C" {
+constexpr DWORD stack_size_param_is_a_reservation = 0x00010000;
-int kal_task_start(void (*entry)(void*), void* arg, kal_task* out) {
+int start(void (*entry)(void*), void* arg, unsigned long long want, kal_task* out) {
if (entry == nullptr || out == nullptr) return kal_err_invalid;
+ const unsigned long long bytes = stack_for(want);
+ if (bytes == 0) return kal_err_no_memory;
auto* c = static_cast(kal_alloc(sizeof(context), alignof(context)));
if (!c) return kal_err_no_memory;
c->entry = entry; c->arg = arg;
- // The stack is the implementation's, which is what the specification says
- // of it: an environment that does not allocate stacks separately cannot
- // honour a request for one, so there is no parameter for it.
- void* h = CreateThread(nullptr, 0, run, c, 0, nullptr);
+ void* h = CreateThread(nullptr, bytes, run, c, stack_size_param_is_a_reservation, nullptr);
if (!h) {
const int e = okw::translate_win32(GetLastError());
kal_free(c, sizeof(context), alignof(context));
@@ -51,6 +73,21 @@ int kal_task_start(void (*entry)(void*), void* arg, kal_task* out) {
return kal_ok;
}
+} // namespace
+
+extern "C" {
+
+int kal_task_start(void (*entry)(void*), void* arg, kal_task* out) {
+ return start(entry, arg, 0, out);
+}
+
+// A least size, version 0.16: at least the size asked for, and never less than
+// the default.
+int kal_task_start_sized(void (*entry)(void*), void* arg, kal_uintptr stack_size,
+ kal_task* out) {
+ return start(entry, arg, stack_size, out);
+}
+
int kal_task_join(kal_task t) {
auto* c = reinterpret_cast(t.h);
if (!c) return kal_err_invalid;
diff --git a/src/terminal.cpp b/src/terminal.cpp
index b68fb17..c2791d7 100644
--- a/src/terminal.cpp
+++ b/src/terminal.cpp
@@ -21,6 +21,7 @@ bool valid(void* h) { return h != nullptr && h != INVALID_HANDLE_VALUE; }
constexpr DWORD enable_processed_input = 0x0001;
constexpr DWORD enable_line_input = 0x0002;
constexpr DWORD enable_echo_input = 0x0004;
+constexpr DWORD enable_virtual_terminal_input = 0x0200;
// KAL_TERM_PASS_CONTROL IS ENABLE_PROCESSED_INPUT, INVERTED. This environment
// keeps the keystrokes it reserves --- the interrupt, and the pair that stops
@@ -50,6 +51,19 @@ int kal_terminal_get_mode(kal_stream s, kal_uintptr* mode) {
if ((m & enable_line_input) != 0) out |= KAL_TERM_LINE_EDIT;
if ((m & enable_echo_input) != 0) out |= KAL_TERM_ECHO;
if ((m & enable_processed_input) == 0) out |= KAL_TERM_PASS_CONTROL;
+ // KAL_TERM_PASS_INPUT IS VIRTUAL TERMINAL INPUT WITH LINE INPUT OFF.
+ //
+ // Measured on a console this implementation's tests allocate, with the keys
+ // written into its input as the key events a keyboard produces. With line
+ // input off and virtual terminal input off, the return key arrives as 0x0d
+ // and Ctrl+J arrives as NOTHING: the console consumes the key and delivers
+ // no byte, which is an alteration of what was typed. With virtual terminal
+ // input on, the return key is 0x0d and Ctrl+J is 0x0a --- the two bytes the
+ // other environments deliver with the position set. With line input on,
+ // the return key ends a line as 0x0d 0x0a and no switch changes that, so the
+ // position reads clear there.
+ if ((m & enable_line_input) == 0 && (m & enable_virtual_terminal_input) != 0)
+ out |= KAL_TERM_PASS_INPUT;
*mode = out;
return kal_ok;
}
@@ -65,6 +79,7 @@ int kal_terminal_set_mode(kal_stream s, kal_uintptr mode) {
// would find its console changed in ways it did not ask for.
DWORD m = 0;
if (!GetConsoleMode(h, &m)) return kal_err_not_supported;
+ const bool unaltered = (m & enable_line_input) == 0 && (m & enable_virtual_terminal_input) != 0;
if ((mode & KAL_TERM_LINE_EDIT) != 0) m |= enable_line_input;
else m &= ~enable_line_input;
@@ -80,9 +95,29 @@ int kal_terminal_set_mode(kal_stream s, kal_uintptr mode) {
// several mechanisms; this one stands for exactly one, and assigning it
// from the mode word is the same act as leaving it alone.
+ // KAL_TERM_PASS_INPUT is virtual terminal input, and only while line input
+ // is off: with lines assembled this environment alters input whatever the
+ // switch says, so a request for the position there leaves it as it stands,
+ // which is what terminal.h states of such an environment. A position whose
+ // requested value is the one in effect is not written.
+ bool asked_vt = false;
+ if (((mode & KAL_TERM_PASS_INPUT) != 0) != unaltered) {
+ if ((mode & KAL_TERM_PASS_INPUT) != 0) {
+ if ((mode & KAL_TERM_LINE_EDIT) == 0) { m |= enable_virtual_terminal_input; asked_vt = true; }
+ } else {
+ m &= ~enable_virtual_terminal_input;
+ }
+ }
+
// A position this implementation does not distinguish is ignored rather than
- // refused, which clause 6.2 requires of a word.
- if (!SetConsoleMode(h, m)) return kal_err_not_supported;
+ // refused, which clause 6.2 requires of a word. A console that predates
+ // virtual terminal input refuses the whole word when it is asked for, so
+ // the word is written again without it: the other positions are
+ // established, and the read-back says the fourth was not.
+ if (!SetConsoleMode(h, m)) {
+ if (!asked_vt || !SetConsoleMode(h, m & ~enable_virtual_terminal_input))
+ return kal_err_not_supported;
+ }
return kal_ok;
}
diff --git a/tests/console_input.cpp b/tests/console_input.cpp
new file mode 100644
index 0000000..0926d3a
--- /dev/null
+++ b/tests/console_input.cpp
@@ -0,0 +1,197 @@
+// What a console delivers for the return key and for Ctrl+J, measured, and what
+// KAL_TERM_PASS_INPUT (openkal 0.16) reports for it.
+//
+// mcpplibs/openkal#48: above the two other implementations the return key
+// reached a program in raw mode as a line feed, the same byte Ctrl+J produces.
+// The position that answers it makes the bytes a program reads the same on all
+// three when it is set. This test writes the keys into a console of its own as
+// the key events a keyboard produces, reads them back through kal_stream_read,
+// prints what arrived under each mode, and checks:
+//
+// with line input off and nothing else asked, the position reads clear, the
+// return key is 0x0d, and what Ctrl+J produces is printed (measured: nothing);
+// with the position set, the return key is 0x0d and Ctrl+J is 0x0a, which is
+// what the other two deliver with it set;
+// with line input on, the position reads clear whatever was asked.
+//
+// A console is allocated rather than inherited, because a runner's own may be
+// absent or redirected. Every measurement ends with a key that always produces
+// a byte, so a key that produces none is measured rather than waited for; a
+// watchdog ends the test if even that does not arrive.
+#include
+#include
+#include
+#include
+#include
+
+namespace {
+
+int failures = 0;
+
+void check(bool held, const char* what) {
+ std::printf(" %s %s\n", held ? "held" : "FAIL", what);
+ std::fflush(stdout);
+ if (!held) ++failures;
+}
+
+DWORD WINAPI watchdog(void*) {
+ Sleep(20000);
+ std::printf(" FAIL a read waited for a key that never arrived\n");
+ std::fflush(stdout);
+ ExitProcess(3);
+}
+
+void key(INPUT_RECORD* r, WORD vk, WORD scan, wchar_t ch, DWORD ctrl, BOOL down) {
+ ZeroMemory(r, sizeof *r);
+ r->EventType = KEY_EVENT;
+ r->Event.KeyEvent.bKeyDown = down;
+ r->Event.KeyEvent.wRepeatCount = 1;
+ r->Event.KeyEvent.wVirtualKeyCode = vk;
+ r->Event.KeyEvent.wVirtualScanCode = scan;
+ r->Event.KeyEvent.uChar.UnicodeChar = ch;
+ r->Event.KeyEvent.dwControlKeyState = ctrl;
+}
+
+// How Ctrl+J is spelled as a key event. A keyboard produces the virtual key
+// 'J', its scan code, the control state and the character the layout gives it;
+// which of those the console reads is what is being measured, so each spelling
+// is tried and printed.
+int g_spelling = 0;
+
+// Writes the keys `keys' as presses and releases, then a `q', and reads until
+// the `q' arrives. What came before it is what the keys produced --- a key the
+// console turns into no byte produces nothing, and the `q' still ends the read,
+// which is what lets nothing be measured rather than waited for.
+int type_and_read(HANDLE in, kal_stream s, const char* keys, unsigned char* out, int cap) {
+ FlushConsoleInputBuffer(in);
+ INPUT_RECORD rec[34];
+ int n = 0;
+ char all[16]; int m = 0;
+ for (const char* k = keys; *k && m < 14; ++k) all[m++] = *k;
+ all[m++] = 'q'; all[m] = 0;
+ for (const char* k = all; *k; ++k) {
+ WORD vk; WORD scan = 0; wchar_t ch; DWORD ctrl = 0;
+ if (*k == 'E') { vk = VK_RETURN; scan = 0x1c; ch = L'\r'; }
+ else if (*k == 'J') {
+ vk = 'J'; ch = L'\n';
+ switch (g_spelling) {
+ case 0: scan = 0x24; ctrl = LEFT_CTRL_PRESSED; break; // as a keyboard sends it
+ case 1: scan = 0; ctrl = LEFT_CTRL_PRESSED; break;
+ case 2: scan = 0x24; ctrl = 0; break; // the character alone
+ }
+ }
+ else { vk = static_cast(*k - 'a' + 'A'); scan = 0; ch = static_cast(*k); }
+ key(&rec[n++], vk, scan, ch, ctrl, TRUE);
+ key(&rec[n++], vk, scan, ch, ctrl, FALSE);
+ }
+ DWORD written = 0;
+ if (!WriteConsoleInputW(in, rec, static_cast(n), &written)) {
+ std::printf(" WriteConsoleInputW failed: %lu\n", GetLastError());
+ return -1;
+ }
+ int got = 0;
+ unsigned char buf[64];
+ int have = 0;
+ for (;;) {
+ const kal_intptr r = kal_stream_read(s, buf + have, sizeof buf - have);
+ if (r <= 0) break;
+ have += static_cast(r);
+ bool end = false;
+ for (int i = 0; i < have; ++i) if (buf[i] == 'q') { end = true; have = i; break; }
+ if (end || have >= static_cast(sizeof buf)) break;
+ }
+ for (int i = 0; i < have && got < cap; ++i) out[got++] = buf[i];
+ return got;
+}
+
+void show(const char* mode, const unsigned char* b, int n) {
+ std::printf(" measured %-34s", mode);
+ for (int i = 0; i < n; ++i) std::printf(" %02x", b[i]);
+ std::printf("\n");
+}
+
+} // namespace
+
+int main() {
+ FreeConsole();
+ if (!AllocConsole()) {
+ std::printf("openkal-windows: no console could be allocated; not observed\n");
+ return 0;
+ }
+ HANDLE in = CreateFileW(L"CONIN$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE,
+ nullptr, OPEN_EXISTING, 0, nullptr);
+ if (in == INVALID_HANDLE_VALUE) {
+ std::printf("openkal-windows: the console's input could not be opened; not observed\n");
+ return 0;
+ }
+ CreateThread(nullptr, 0, watchdog, nullptr, 0, nullptr);
+ const kal_stream s{ reinterpret_cast(in) };
+ unsigned char b[16] = {};
+ kal_uintptr m = 0;
+
+ // Raw, nothing else asked: what this console does with the keys by itself.
+ check(kal_terminal_set_mode(s, 0) == kal_ok, "line input and echo are turned off");
+ check(kal_terminal_get_mode(s, &m) == kal_ok && (m & KAL_TERM_PASS_INPUT) == 0,
+ "with virtual terminal input off the position reads clear");
+ int got = type_and_read(in, s, "E", b, sizeof b);
+ show("raw: Enter", b, got);
+ check(got == 1 && b[0] == 0x0d, "the return key is 0x0d");
+ const char* spelled[] = { "raw: Ctrl+J, keyboard scan code", "raw: Ctrl+J, no scan code",
+ "raw: line feed, no control" };
+ for (g_spelling = 0; g_spelling < 3; ++g_spelling) {
+ got = type_and_read(in, s, "J", b, sizeof b);
+ show(spelled[g_spelling], b, got);
+ }
+ g_spelling = 0;
+
+ // Raw, and nothing altered: what a program that tells keys apart asks for.
+ // These are the bytes the other two environments deliver with the position
+ // set, which is what the position is for.
+ check(kal_terminal_set_mode(s, KAL_TERM_PASS_INPUT) == kal_ok, "unaltered input is requested");
+ check(kal_terminal_get_mode(s, &m) == kal_ok && (m & KAL_TERM_PASS_INPUT) != 0,
+ "the position reads set once established");
+ got = type_and_read(in, s, "EJ", b, sizeof b);
+ show("unaltered: Enter Ctrl+J", b, got);
+ check(got == 2 && b[0] == 0x0d && b[1] == 0x0a, "the return key is 0x0d and Ctrl+J is 0x0a");
+
+ check(kal_terminal_set_mode(s, KAL_TERM_PASS_CONTROL | KAL_TERM_PASS_INPUT) == kal_ok,
+ "every keystroke, unaltered, is requested");
+ got = type_and_read(in, s, "EJ", b, sizeof b);
+ show("unaltered, nothing reserved: Enter Ctrl+J", b, got);
+ check(got == 2 && b[0] == 0x0d && b[1] == 0x0a, "the same two bytes with nothing reserved");
+
+ // Cleared again, and written only because it changed.
+ check(kal_terminal_set_mode(s, KAL_TERM_PASS_CONTROL) == kal_ok, "altered input is requested again");
+ check(kal_terminal_get_mode(s, &m) == kal_ok && (m & KAL_TERM_PASS_INPUT) == 0,
+ "the position reads clear once cleared");
+
+ // Lines assembled, with the position asked for: this console alters input
+ // while it assembles lines whatever its switches say, so the request leaves
+ // the position clear and the read-back says so.
+ check(kal_terminal_set_mode(s, KAL_TERM_LINE_EDIT | KAL_TERM_PASS_INPUT) == kal_ok,
+ "unaltered input is requested with line input on");
+ check(kal_terminal_get_mode(s, &m) == kal_ok && (m & KAL_TERM_PASS_INPUT) == 0,
+ "the read-back reports what is in effect rather than what was asked");
+
+ // Lines assembled: the position reads clear, and what arrives is measured.
+ check(kal_terminal_set_mode(s, KAL_TERM_LINE_EDIT) == kal_ok, "line input is turned on");
+ check(kal_terminal_get_mode(s, &m) == kal_ok && (m & KAL_TERM_PASS_INPUT) == 0,
+ "with line input on the position reads clear");
+ // With lines assembled the `q' is part of the next line, so the line is
+ // read on its own: the read returns once the return key ends it.
+ FlushConsoleInputBuffer(in);
+ {
+ INPUT_RECORD rec[4];
+ key(&rec[0], 'A', 0, L'a', 0, TRUE); key(&rec[1], 'A', 0, L'a', 0, FALSE);
+ key(&rec[2], VK_RETURN, 0x1c, L'\r', 0, TRUE); key(&rec[3], VK_RETURN, 0x1c, L'\r', 0, FALSE);
+ DWORD w = 0;
+ WriteConsoleInputW(in, rec, 4, &w);
+ got = static_cast(kal_stream_read(s, b, sizeof b));
+ }
+ show("line on: a Enter", b, got);
+ check(got >= 2 && b[0] == 'a', "a line is delivered");
+
+ std::printf("openkal-windows: what a console delivers, %s\n", failures ? "FAILED" : "held");
+ std::fflush(stdout);
+ return failures == 0 ? 0 : 1;
+}
diff --git a/tests/enumeration_ends_at_close.cpp b/tests/enumeration_ends_at_close.cpp
new file mode 100644
index 0000000..96aaa4e
--- /dev/null
+++ b/tests/enumeration_ends_at_close.cpp
@@ -0,0 +1,106 @@
+// What closing a directory does to its enumerations (fs.h, openkal 0.16).
+//
+// The Linux implementation kept a descriptor for every directory a C library
+// above it had stopped reading early (mcpplibs/openkal-linux#34), and this one
+// had the same shape: only reaching the end closed the handle an enumeration
+// opened. Counted in this environment's own table of the process's handles,
+// because a count of open handles is not portable and the conformance suite
+// cannot make it (SPEC clause 11, entry 24).
+#include
+#include "../src/win32.h"
+#include
+#include
+#include
+
+extern "C" OKW_IMPORT BOOL OKW_API GetProcessHandleCount(HANDLE, DWORD*);
+extern "C" OKW_IMPORT HANDLE OKW_API GetCurrentProcess(void);
+
+namespace {
+
+int failures = 0;
+
+void check(bool held, const char* what) {
+ std::printf(" %s %s\n", held ? "held" : "FAIL", what);
+ if (!held) ++failures;
+}
+
+DWORD handles() { DWORD n = 0; return GetProcessHandleCount(GetCurrentProcess(), &n) ? n : 0; }
+
+struct race { kal_dir d; volatile int told; };
+
+void reader(void* p) {
+ auto* r = static_cast(p);
+ kal_uintptr it = 0;
+ if (kal_fs_list_begin(r->d, &it) != kal_ok) { r->told = 1; return; }
+ for (;;) {
+ char name[512]; kal_uintptr len = 0; int kind = 0;
+ if (kal_fs_list_next(r->d, &it, name, sizeof name, &len, &kind) != kal_ok) { r->told = 1; return; }
+ if (it == 0 && kal_fs_list_begin(r->d, &it) != kal_ok) { r->told = 1; return; }
+ }
+}
+
+} // namespace
+
+int main() {
+ kal_dir cwd{};
+ if (kal_fs_preopen_count() == 0 || kal_fs_preopen(0, &cwd, nullptr, 0, nullptr) != kal_ok) {
+ std::printf("openkal-windows: no preopened directory to test in\n");
+ return 1;
+ }
+ kal_fs_mkdir(cwd, "okw-enum.d", 10);
+ kal_file f{};
+ if (kal_fs_open(cwd, "okw-enum.d/inner", 16, KAL_OPEN_WRITE | KAL_OPEN_CREATE, &f) == kal_ok)
+ kal_fs_close_file(f);
+
+ const DWORD before = handles();
+ for (int i = 0; i < 64; ++i) {
+ kal_dir d{}; kal_uintptr it = 0;
+ if (kal_fs_open_dir(cwd, "okw-enum.d", 10, &d) != kal_ok) break;
+ if (kal_fs_list_begin(d, &it) == kal_ok) {
+ char name[512]; kal_uintptr len = 0; int kind = 0;
+ kal_fs_list_next(d, &it, name, sizeof name, &len, &kind);
+ }
+ kal_fs_close_dir(d);
+ }
+ const DWORD after = handles();
+ std::printf(" handles before=%lu after=%lu\n", (unsigned long)before, (unsigned long)after);
+ if (before == 0) std::printf(" not observed the environment did not count the process's handles\n");
+ else check(after == before, "closing a directory ends an enumeration left part-way");
+
+ {
+ kal_dir once{}; kal_uintptr it = 0;
+ if (kal_fs_open_dir(cwd, "okw-enum.d", 10, &once) == kal_ok && kal_fs_list_begin(once, &it) == kal_ok) {
+ kal_fs_close_dir(once);
+ char name[512]; kal_uintptr len = 0; int kind = 0;
+ check(kal_fs_list_next(once, &it, name, sizeof name, &len, &kind) == kal_err_invalid,
+ "an enumeration of a closed directory is refused");
+ }
+ kal_dir a{}, b{}; kal_uintptr ia = 0;
+ if (kal_fs_open_dir(cwd, "okw-enum.d", 10, &a) == kal_ok
+ && kal_fs_open_dir(cwd, "okw-enum.d", 10, &b) == kal_ok
+ && kal_fs_list_begin(a, &ia) == kal_ok) {
+ char name[512]; kal_uintptr len = 0; int kind = 0;
+ check(kal_fs_list_next(b, &ia, name, sizeof name, &len, &kind) == kal_err_invalid,
+ "an iterator is refused with a directory that did not produce it");
+ }
+ kal_fs_close_dir(a); kal_fs_close_dir(b);
+ }
+
+ int ended = 0;
+ for (int i = 0; i < 200; ++i) {
+ race r{};
+ if (kal_fs_open_dir(cwd, "okw-enum.d", 10, &r.d) != kal_ok) break;
+ kal_task t{};
+ if (kal_task_start(reader, &r, &t) != kal_ok) { kal_fs_close_dir(r.d); break; }
+ for (int spin = 0; spin < i * 10; ++spin) kal_task_yield();
+ kal_fs_close_dir(r.d);
+ kal_task_join(t);
+ ended += r.told;
+ }
+ check(ended == 200, "an enumeration racing the close of its directory ends and is told so");
+
+ kal_fs_remove(cwd, "okw-enum.d/inner", 16);
+ kal_fs_remove(cwd, "okw-enum.d", 10);
+ std::printf("openkal-windows: what closing a directory ends, %s\n", failures ? "FAILED" : "held");
+ return failures == 0 ? 0 : 1;
+}
diff --git a/tests/stack_size.cpp b/tests/stack_size.cpp
new file mode 100644
index 0000000..392b04a
--- /dev/null
+++ b/tests/stack_size.cpp
@@ -0,0 +1,63 @@
+// The least stack size a started context is given (openkal 0.16).
+//
+// WHAT IS OBSERVED IS ROOM: a recursion of three quarters of the request
+// completes, and the context is told at least the size it asked for. Asking for
+// nothing gives the 8 MiB the other two implementations give.
+#include
+#include
+#include
+
+namespace {
+
+int failures = 0;
+
+void check(bool held, const char* what) {
+ std::printf(" %s %s\n", held ? "held" : "FAIL", what);
+ if (!held) ++failures;
+}
+
+long recurse(long n);
+long (*volatile g_recurse)(long) = recurse;
+long recurse(long n) {
+ volatile char pad[1000];
+ pad[0] = static_cast(n);
+ if (n == 0) return 0;
+ return g_recurse(n - 1) + pad[0];
+}
+
+struct probe { kal_uintptr ask; kal_uintptr told; volatile int done; };
+
+void room(void* p) {
+ auto* q = static_cast(p);
+ void* base = nullptr; kal_uintptr size = 0;
+ if (kal_task_stack(&base, &size) == kal_ok) q->told = size;
+ g_recurse(static_cast(q->ask / 1024 * 3 / 4));
+ q->done = 1;
+}
+
+} // namespace
+
+int main() {
+ const kal_uintptr asks[] = { 1u << 20, 16u << 20, 64u << 20 };
+ for (kal_uintptr ask : asks) {
+ probe q{ ask, 0, 0 };
+ kal_task t{};
+ check(kal_task_start_sized(room, &q, ask, &t) == kal_ok, "a context with a least stack size starts");
+ kal_task_join(t);
+ std::printf(" asked %llu, told %llu\n", (unsigned long long)ask, (unsigned long long)q.told);
+ check(q.told >= ask, "the context is told at least the size asked for");
+ check(q.done == 1, "the context has the room it asked for");
+ }
+ probe d{ 0, 0, 0 };
+ kal_task t{};
+ check(kal_task_start(room, &d, &t) == kal_ok, "a context starts");
+ kal_task_join(t);
+ std::printf(" asked nothing, told %llu\n", (unsigned long long)d.told);
+ check(d.told >= (8u << 20), "a context that asks for nothing is given 8 MiB");
+
+ kal_task never{};
+ check(kal_task_start_sized(room, nullptr, ~kal_uintptr{0} - 4095, &never) == kal_err_no_memory,
+ "a stack larger than the address space is reported as exhaustion");
+ std::printf("openkal-windows: a least stack size, %s\n", failures ? "FAILED" : "held");
+ return failures == 0 ? 0 : 1;
+}