Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
#
Expand Down Expand Up @@ -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"
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions mcpp.toml
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -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
Expand Down
100 changes: 92 additions & 8 deletions src/fs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <string.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <sched.h>
#include <unistd.h>

// THE FILE SYSTEM IS THE INTERFACE WHERE THIS PLATFORM'S VOCABULARY AND
Expand Down Expand Up @@ -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<DIR*>(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

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<kal_uintptr>(dir);
auto* s = static_cast<listing*>(::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<kal_uintptr>(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<listing*>(*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;
}
Expand Down Expand Up @@ -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;
}
}
Expand Down
16 changes: 16 additions & 0 deletions src/terminal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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{};
Expand All @@ -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;
}
Expand All @@ -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
Expand All @@ -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
Expand Down
40 changes: 37 additions & 3 deletions src/threads/task.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,22 +87,56 @@ 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.
auto* s = static_cast<Start*>(::malloc(sizeof(Start)));
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<size_t>(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<kal_uintptr>(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<pthread_t>(t.h), nullptr);
Expand Down
Loading