From 7645f1138eafd4ca2316556037054db7a4c4e240 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 05:41:20 +0900 Subject: [PATCH 1/5] Closing a directory ends its unfinished enumerations, and a closed directory is refused by kal_fs_list_next The walk held a duplicate of the directory's descriptor that only reaching the end released, the shape openkal-linux#34 measured. An iterator is now a listing kept with its directory, the close ends it, a listing another context is reading is released by that context on its way out (clause 6.6), and the word is found among the open listings rather than read through. --- src/fs.cpp | 100 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 92 insertions(+), 8 deletions(-) diff --git a/src/fs.cpp b/src/fs.cpp index 096d04b..f7318f1 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include // THE FILE SYSTEM IS THE INTERFACE WHERE THIS PLATFORM'S VOCABULARY AND @@ -132,7 +133,76 @@ void fill(const struct stat& st, kal_u32 wanted, kal_node_info* out) { // `closedir` that ends the walk -- and it is NOT put through handle.h, because // a DIR* is not a descriptor and packing it would be a second discipline on a // word this file already owns. -DIR* as_dir(kal_uintptr iter) { return reinterpret_cast(iter); } +// AN ITERATOR IS A LISTING, AND THE LISTINGS NOT YET READ TO THE END ARE KEPT +// WITH THEIR DIRECTORY, 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 walk holds a duplicate of the directory's +// descriptor, so closing the directory's descriptor 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 descriptor for every directory it closed. +// +// The lock spins rather than suspends because the build without threads has +// nothing to suspend upon, and the build with them needs nothing more. +struct listing { + DIR* dir; + int of; // the descriptor 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 +}; + +listing* g_listings = nullptr; +volatile int g_listings_held = 0; + +void listings_lock() { + while (__atomic_exchange_n(&g_listings_held, 1, __ATOMIC_ACQUIRE)) ::sched_yield(); +} +void listings_unlock() { __atomic_store_n(&g_listings_held, 0, __ATOMIC_RELEASE); } + +void listing_release(listing* s) { ::closedir(s->dir); ::free(s); } + +// Ends every listing begun upon `of'. A listing another context is reading is +// marked rather than released, and that context releases it on its way out +// (clause 6.6). +void listings_end(int of) { + listing* done = nullptr; + listings_lock(); + for (listing** p = &g_listings; *p != nullptr;) { + listing* s = *p; + if (s->of != of || s->doomed) { p = &s->next; continue; } + if (s->busy) { s->doomed = 1; p = &s->next; continue; } + *p = s->next; s->next = done; done = s; + } + listings_unlock(); + 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 `of'. The word is found among the open listings rather than read through. +bool listing_take(listing* s, int of) { + for (;;) { + listings_lock(); + listing* p = g_listings; + while (p != nullptr && p != s) p = p->next; + if (p == nullptr || p->of != of || p->doomed) { listings_unlock(); return false; } + if (!p->busy) { p->busy = 1; listings_unlock(); return true; } + listings_unlock(); + ::sched_yield(); + } +} + +void listing_give(listing* s, bool ended) { + 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; } + } + listings_unlock(); + if (release) listing_release(s); +} } // namespace @@ -221,7 +291,11 @@ int kal_fs_open(kal_dir base, const char* name, kal_uintptr len, void kal_fs_close_dir(kal_dir d) { const int fd = oke::unpack(d.h); - if (fd >= 0) { ::close(fd); oke::retire(d.h); } + if (fd < 0) return; + // Before the close, because after it the number may already name another + // directory whose enumerations are not this one's. + listings_end(fd); + ::close(fd); oke::retire(d.h); } void kal_fs_close_file(kal_file f) { @@ -470,25 +544,34 @@ int kal_fs_list_begin(kal_dir d, kal_uintptr* iter) { if (::lseek(copy, 0, SEEK_SET) < 0) { /* a directory may refuse; harmless */ } DIR* dir = ::fdopendir(copy); if (dir == nullptr) { const int e = oke::last(); ::close(copy); return e; } - *iter = reinterpret_cast(dir); + auto* s = static_cast(::malloc(sizeof(listing))); + if (s == nullptr) { ::closedir(dir); return kal_err_no_memory; } + s->dir = dir; s->of = fd; s->busy = 0; s->doomed = 0; + listings_lock(); s->next = g_listings; g_listings = s; listings_unlock(); + *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; - DIR* dir = as_dir(*iter); + // A closed directory is a released handle (clause 6.7), and closing it + // released the iterator too, which fs.h states. + const int of = oke::unpack(d.h); + if (of < 0 || iter == nullptr || *iter == 0) return kal_err_invalid; + auto* s = reinterpret_cast(*iter); + if (!listing_take(s, of)) return kal_err_invalid; + DIR* dir = s->dir; for (;;) { errno = 0; struct dirent* e = ::readdir(dir); if (e == nullptr) { - if (errno != 0) return oke::last(); + if (errno != 0) { const int err = oke::last(); listing_give(s, false); return err; } // THE END OF THE WALK RELEASES THE ITERATOR AND SAYS SO ONCE. A // caller that called again would otherwise pass a closed DIR* to // `readdir`; zeroing the word makes the second call // kal_err_invalid rather than undefined. - ::closedir(dir); + listing_give(s, true); *iter = 0; return kal_err_not_found; } @@ -524,6 +607,7 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter, default: *kind = kal_node_other; break; } } + listing_give(s, false); return kal_ok; } } From 63c1e47a0eb9c8e90f93fe637ec485b41c0e21fc Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 05:41:20 +0900 Subject: [PATCH 2/5] KAL_TERM_PASS_INPUT is the four termios flags that alter input, under node as the host has them --- src/terminal.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/terminal.cpp b/src/terminal.cpp index ed07eaf..21f2f03 100644 --- a/src/terminal.cpp +++ b/src/terminal.cpp @@ -22,6 +22,8 @@ // program that prints a password. extern "C" { +constexpr tcflag_t kAlters = ISTRIP | INLCR | IGNCR | ICRNL; + int kal_terminal_get_mode(kal_stream s, kal_uintptr* mode) { if (mode == nullptr) return kal_err_invalid; struct termios t{}; @@ -38,6 +40,12 @@ int kal_terminal_get_mode(kal_stream s, kal_uintptr* mode) { // terminal and the property word says so before any of this is reached. if ((t.c_lflag & (ISIG | IEXTEN)) == 0 && (t.c_iflag & IXON) == 0) m |= KAL_TERM_PASS_CONTROL; + // KAL_TERM_PASS_INPUT IS READ FROM FOUR FLAGS, for the same reason: it + // states that nothing typed is altered, which holds only where every + // mechanism that alters input --- the high bit cleared, a line feed or a + // carriage return rewritten into the other, a carriage return discarded --- + // is off. Under node these are the host's own. + if ((t.c_iflag & kAlters) == 0) m |= KAL_TERM_PASS_INPUT; *mode = m; return kal_ok; } @@ -48,6 +56,7 @@ int kal_terminal_set_mode(kal_stream s, kal_uintptr mode) { if (::tcgetattr(fd, &t) != 0) return oke::last(); const bool reserved_none = (t.c_lflag & (ISIG | IEXTEN)) == 0 && (t.c_iflag & IXON) == 0; + const bool altered_none = (t.c_iflag & kAlters) == 0; // READ, MODIFY, WRITE, AND ONLY THE POSITIONS openkal NAMES. A mode // composed from scratch would silently reset every other attribute of the // terminal -- the special characters, the baud rate -- none of which this @@ -68,6 +77,13 @@ int kal_terminal_set_mode(kal_stream s, kal_uintptr mode) { } } + // The same rule for the position that stands for four flags. Clearing it + // restores the rewriting of a carriage return alone, which is what a + // terminal ordinarily applies. + if (((mode & KAL_TERM_PASS_INPUT) != 0) != altered_none) { + if (mode & KAL_TERM_PASS_INPUT) t.c_iflag &= ~kAlters; + else t.c_iflag |= ICRNL; + } // AND A MODE IS NOT A WAY TO END THE INPUT. With line assembly off, how // long a read waits is decided by VMIN and VTIME, and a terminal left at // VMIN=0 makes `kal_stream_read' report zero --- which clause 7.4 says From 4e357ad685ab48e102b7a43a57a089bd6205bd63 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 05:41:20 +0900 Subject: [PATCH 3/5] kal_task_start_sized asks the thread library for at least the size requested The default stays this platform's own: a stack here is linear memory taken from the heap when the context starts, so raising it would cost every context its whole size at once. --- src/threads/task.cpp | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/src/threads/task.cpp b/src/threads/task.cpp index db66f54..867a7c8 100644 --- a/src/threads/task.cpp +++ b/src/threads/task.cpp @@ -87,8 +87,17 @@ void* trampoline(void* p) { } } // namespace -int kal_task_start(void (*entry)(void*), void* arg, kal_task* out) { +namespace { + +// A least size, version 0.16. The default is this platform's own and is not +// raised to the 8 MiB the three native implementations give: a stack here is +// linear memory taken from the heap when the context starts, not a reservation +// committed as it is touched, so a larger default would cost every context its +// whole size at once. A context that needs more asks, and a request the heap +// cannot meet is the exhaustion kal_task_start_sized states. +int start(void (*entry)(void*), void* arg, kal_uintptr want, kal_task* out) { if (entry == nullptr || out == nullptr) return kal_err_invalid; + if (want > (kal_uintptr{1} << 30)) return kal_err_no_memory; // THE ARGUMENT PAIR OUTLIVES THIS CALL, so it cannot be on this stack. // Freed by the new context rather than by this one, which is the only // arrangement that needs no synchronisation between them. @@ -96,13 +105,38 @@ int kal_task_start(void (*entry)(void*), void* arg, kal_task* out) { if (s == nullptr) return kal_err_no_memory; s->entry = entry; s->arg = arg; + pthread_attr_t attr; + ::pthread_attr_init(&attr); + int rc = 0; + if (want != 0) { + size_t now = 0; + ::pthread_attr_getstacksize(&attr, &now); + const size_t w = (static_cast(want) + 15u) & ~size_t{15}; + if (w > now) rc = ::pthread_attr_setstacksize(&attr, w); + } pthread_t th{}; - const int rc = ::pthread_create(&th, nullptr, &trampoline, s); - if (rc != 0) { ::free(s); errno = rc; return oke::last(); } + if (rc == 0) rc = ::pthread_create(&th, &attr, &trampoline, s); + ::pthread_attr_destroy(&attr); + if (rc != 0) { + ::free(s); + if (want != 0 && (rc == EAGAIN || rc == ENOMEM || rc == EINVAL)) return kal_err_no_memory; + errno = rc; return oke::last(); + } out->h = reinterpret_cast(th); return kal_ok; } +} // namespace + +int kal_task_start(void (*entry)(void*), void* arg, kal_task* out) { + return start(entry, arg, 0, out); +} + +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) { if (t.h == 0) return kal_err_invalid; const int rc = ::pthread_join(reinterpret_cast(t.h), nullptr); From 809d83700a65a15d8eca332a6f921403b77f9c11 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 05:41:24 +0900 Subject: [PATCH 4/5] 0.5.0 (draft) --- written against openkal 0.16 --- README.md | 4 ++-- mcpp.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a05bd68..120536c 100644 --- a/README.md +++ b/README.md @@ -161,10 +161,10 @@ implementation in its source: ```toml [dependencies] -openkal = "0.15.0" +openkal = "0.16.0" [target.'cfg(os = "emscripten")'.dependencies] -openkal-emscripten = "0.4.0" +openkal-emscripten = "0.5.0" ``` ## Licence diff --git a/mcpp.toml b/mcpp.toml index be89fd3..73b64d6 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-emscripten" -version = "0.4.0" +version = "0.5.0" description = "An implementation of openkal for Emscripten, written ABOVE a C library rather than beneath one, because on this platform there is no kernel to issue calls to." license = "Apache-2.0" @@ -18,7 +18,7 @@ authors = ["mcpplibs"] repo = "https://github.com/mcpplibs/openkal-emscripten" [dependencies] -openkal = "0.15.0" +openkal = "0.16.0" [build] # THE DIALECT FLAGS ARE THE SAME AS EVERY OTHER IMPLEMENTATION'S AND THE REASON From d808099b59a8818d3ca4e6201c0db63601e3ecf9 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 06:13:47 +0900 Subject: [PATCH 5/5] The task gate counts ten symbols, kal_task_start_sized being the tenth --- .github/workflows/ci.yml | 8 ++++---- README.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d03a659..eb9054a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ name: CI # does not have fails at LINK naming the symbol, which is # clause 6.2's second time and is the mechanism rather than a # defect -# task gating the nine kal_task_* symbols exist in a `-pthread` link and +# task gating the ten kal_task_* symbols exist in a `-pthread` link and # in no other, and the interfaces word follows the link # # ONE TOOLCHAIN, NOT TWO, AND THAT IS A PROPERTY OF THE TARGET RATHER THAN A @@ -308,7 +308,7 @@ jobs: # 1. without the feature and the switch, no task symbol exists; # 2. the feature without the switch is refused before anything compiles, # naming the feature; - # 3. with both, the nine symbols exist and a program that starts a task, + # 3. with both, the ten symbols exist and a program that starts a task, # joins it, and asks each context for its own stack, exits 0 under # node. # @@ -395,8 +395,8 @@ jobs: n=$("$NM" --defined-only $objs 2>/dev/null \ | awk '$2=="T"||$2=="W"{print $3}' | sed 's/^_//' \ | grep -c '^kal_task' || true) - test "$n" -eq 9 || { echo "::error::expected 9 kal_task symbols with the switch, found $n"; exit 1; } - echo " ok nine task symbols with the switch" + test "$n" -eq 10 || { echo "::error::expected 10 kal_task symbols with the switch, found $n"; exit 1; } + echo " ok ten task symbols with the switch" ( cd "$work" && timeout 300 mcpp run --target wasm32-emscripten ) \ || { echo "::error::the task program did not exit 0 under node"; exit 1; } echo " ok a task starts, runs and joins under node, and both contexts are told their own stack" diff --git a/README.md b/README.md index 120536c..3c148a1 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ So the interface is carried by a feature: openkal-emscripten = { version = "0.4.0", features = ["threads"] } ``` -Without it, `src/threads/task.cpp` compiles to nothing, the nine `kal_task_*` +Without it, `src/threads/task.cpp` compiles to nothing, the ten `kal_task_*` symbols do not exist, and `kal_interfaces()` does not claim the interface -- the same treatment the three absent interfaces get, for the same reason.