Skip to content
Open
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
2 changes: 2 additions & 0 deletions expected/wasm32-wasip3/defined-symbols.txt
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ __wasilibc_open_nomode
__wasilibc_populate_preopens
__wasilibc_pthread_self
__wasilibc_random
__wasilibc_read3
__wasilibc_rename_newat
__wasilibc_rename_oldat
__wasilibc_reset_preopens
Expand All @@ -344,6 +345,7 @@ __wasilibc_stat
__wasilibc_tell
__wasilibc_unlinkat
__wasilibc_utimens
__wasilibc_write3
__wasm_call_dtors
__wcscoll_l
__wcsftime_l
Expand Down
1 change: 1 addition & 0 deletions libc-bottom-half/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ if (WASI STREQUAL "p3")
list(APPEND bottom_half_sources
sources/wasip3.c
sources/wasip3_file.c
sources/wasip3_file_utils.c
sources/wasip3_stdio.c
sources/wasip3_subtask.c
sources/wasip3_tcp.c
Expand Down
4 changes: 1 addition & 3 deletions libc-bottom-half/cloudlibc/src/libc/unistd/read.c
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,7 @@ ssize_t read(int fildes, void *buf, size_t nbyte) {
*off += contents.len;
return contents.len;
#elif defined(__wasip3__)
// TODO(wasip3)
errno = ENOTSUP;
return -1;
return __wasilibc_read3(fildes, buf, nbyte);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently the general style in wasi-libc is to have the implementation of this function inlined here (since I think this will forever be the only caller, right?)

Some helpers I could see being reasonable to put elsewhere, but for the main body of the implementation that seems like it'll be read-specific so I think it's reasonable to inline here (and in write below)

Although I'll be honest in terms of style I'm just winging it I don't have a strong rhyme or reason one way or the other. Keeping things as "one symbol per file and as few deps on other files as possible" seems to be the general ethos.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hoping to factorize out common parts between read and write and having both in a single file (file_util) helps with that, previously I had considerable code inlined here and in write.c.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah if there's a large shared body of code between read/write definitely worth splitting out. I'd imagine though that ABI-wise there's details of read/write that aren't shared which would make sense to go here (vs having this function effectively just tail-call another function), but I'm also ok waiting to see how the dust settles in wasip3 to see what the best organization is

#else
# error "Unsupported WASI version"
#endif
Expand Down
4 changes: 1 addition & 3 deletions libc-bottom-half/cloudlibc/src/libc/unistd/write.c
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,7 @@ ssize_t write(int fildes, const void *buf, size_t nbyte) {
*off += contents.len;
return contents.len;
#elif defined(__wasip3__)
// TODO(wasip3)
errno = ENOTSUP;
return -1;
return __wasilibc_write3(fildes, buf, nbyte);
#else
# error "Unknown WASI version"
#endif
Expand Down
11 changes: 11 additions & 0 deletions libc-bottom-half/headers/private/wasi/descriptor_table.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
#include <sys/stat.h>
#include <netinet/in.h>

#ifdef __wasip3__
// create an alias to distinguish the handle type in the API
typedef uint32_t waitable_t;
#endif

/**
* Operations that are required of all descriptors registered as file
* descriptors.
Expand Down Expand Up @@ -37,6 +42,12 @@ typedef struct descriptor_vtable_t {
/// Same as `get_read_stream`, but for output streams.
int (*get_write_stream)(void*, streams_borrow_output_stream_t*, off_t**, poll_own_pollable_t**);
#endif
#ifdef __wasip3__
/// Start an asynchronous read or write, returns zero on success.
/// Stores the waitable, status and offset location.
int (*read3)(void*, void *buf, size_t nbyte, waitable_t *waitable, wasip3_waitable_status_t *out, off_t**);
int (*write3)(void*, void const *buf, size_t nbyte, waitable_t *waitable, wasip3_waitable_status_t *out, off_t**);
Comment on lines +46 to +49
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to make this look more like get_{read,write}_stream above? Basically where these are in theory simple accessors for the underlying stream and that's it. That way the implementation of read isn't split up across this callback and the wasip3_file_utils.c file (and in the future I think would help deduplicate for, e.g., pipes and tcp streams

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously I just returned a stream_u8_t, then I found that each subsystem has its own name for stream_write (filesystem_, sockets_, stdin_, ...) and moved the write call into the descriptor specific code as well. I don't think it makes a difference, but it just feels wrong to call filesystem_stream_u8_read on a tcp socket.

And as this interface works for read, write and poll I thought that it might be the right level of abstraction.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok that makes sense. Given that those are wit-bindgen-isms I think it'd be reasonable to have something like #define filesystem_... stream_u8... with a comment explaining why and that should be sufficient to have a shared stream utility which doesn't look like it's tied to any one interface

#endif

/// Sets the nonblocking flag for this object to the specified value.
int (*set_blocking)(void*, bool);
Expand Down
7 changes: 7 additions & 0 deletions libc-bottom-half/headers/private/wasi/file_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,11 @@ static unsigned dir_entry_type_to_d_type(filesystem_descriptor_type_t ty) {

#endif

#ifdef __wasip3__
#include <wasi/descriptor_table.h>

ssize_t __wasilibc_write3(int fildes, void const *buf, size_t nbyte);
ssize_t __wasilibc_read3(int fildes, void *buf, size_t nbyte);
#endif

#endif
86 changes: 86 additions & 0 deletions libc-bottom-half/sources/wasip3_file_utils.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#include <assert.h>
#include <common/errors.h>
#include <errno.h>
#include <stddef.h>
#include <stdlib.h>
#include <wasi/file_utils.h>

#ifdef __wasip3__
ssize_t __wasilibc_write3(int fildes, void const *buf, size_t nbyte) {
descriptor_table_entry_t *entry = descriptor_table_get_ref(fildes);
if (!entry)
return -1;
if (!entry->vtable->write3) {
errno = EOPNOTSUPP;
return -1;
}
off_t *off;
waitable_t output_stream;
wasip3_waitable_status_t status;
if ((*entry->vtable->write3)(entry->data, buf, nbyte, &output_stream, &status,
&off) < 0)
return -1;
if (status == WASIP3_WAITABLE_STATUS_BLOCKED) {
wasip3_waitable_set_t set = wasip3_waitable_set_new();
wasip3_waitable_join(output_stream, set);
wasip3_event_t event;
wasip3_waitable_set_wait(set, &event);
assert(event.event == WASIP3_EVENT_STREAM_WRITE);
assert(event.waitable == output_stream);
// remove from set
wasip3_waitable_join(output_stream, 0);
wasip3_waitable_set_drop(set);
ssize_t bytes_written = event.code;
if (off)
*off += bytes_written;
return bytes_written;
} else if (WASIP3_WAITABLE_STATE(status) == WASIP3_WAITABLE_COMPLETED) {
ssize_t bytes_written = WASIP3_WAITABLE_COUNT(status);
if (off)
*off += bytes_written;
return bytes_written;
} else {
abort();
}
}

ssize_t __wasilibc_read3(int fildes, void *buf, size_t nbyte) {
descriptor_table_entry_t *entry = descriptor_table_get_ref(fildes);
if (!entry)
return -1;
if (!entry->vtable->read3) {
errno = EOPNOTSUPP;
return -1;
}
off_t *off;
waitable_t waitable;
wasip3_waitable_status_t status;
if ((*entry->vtable->read3)(entry->data, buf, nbyte, &waitable, &status,
&off) < 0)
return -1;
if (status == WASIP3_WAITABLE_STATUS_BLOCKED) {
wasip3_waitable_set_t set = wasip3_waitable_set_new();
wasip3_waitable_join(waitable, set);
wasip3_event_t event;
wasip3_waitable_set_wait(set, &event);
assert(event.event == WASIP3_EVENT_STREAM_READ);
assert(event.waitable == waitable);
// remove from set
wasip3_waitable_join(waitable, 0);
wasip3_waitable_set_drop(set);
ssize_t bytes_read = event.code;
if (off)
*off += bytes_read;
return bytes_read;
} else if (WASIP3_WAITABLE_STATE(status) == WASIP3_WAITABLE_COMPLETED) {
ssize_t bytes_read = WASIP3_WAITABLE_COUNT(status);
if (off)
*off += bytes_read;
return bytes_read;
} else if (WASIP3_WAITABLE_STATE(status) == WASIP3_WAITABLE_DROPPED) {
return 0;
} else {
abort();
}
}
#endif // __wasip3__
151 changes: 148 additions & 3 deletions libc-bottom-half/sources/wasip3_stdio.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,155 @@
#include <wasi/version.h>

#ifdef __wasip3__
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <wasi/descriptor_table.h>
#include <wasi/wasip3.h>

typedef struct {
stdin_stream_u8_t input;
stdin_future_result_void_error_code_t future;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps input_future as a name to clearly connect it to the input variable? Also mind adding some //-style comments explaining what these fields are and how the futures/streams/tasks all relate?

terminal_input_own_terminal_input_t terminal_in;

stdin_stream_u8_writer_t stdout;
// contents will be filled by host (once stdout has an error)
stdout_result_void_error_code_t stdout_result;
wasip3_subtask_t stdout_task;
terminal_output_own_terminal_output_t terminal_out;
} stdio3_t;

static void stdio3_free(void *data) {
stdio3_t *stdio = (stdio3_t *)data;
if (stdio->terminal_in.__handle)
terminal_input_terminal_input_drop_own(stdio->terminal_in);
if (stdio->future)
stdin_future_result_void_error_code_drop_readable(stdio->future);
if (stdio->input)
stdin_stream_u8_drop_readable(stdio->input);

if (stdio->terminal_out.__handle)
terminal_output_terminal_output_drop_own(stdio->terminal_out);
if (stdio->stdout_task)
wasip3_subtask_cancel(stdio->stdout_task);
if (stdio->stdout)
stdin_stream_u8_drop_writable(stdio->stdout);
free(stdio);
}

static int stdio3_write(void *data, void const *buf, size_t nbyte,
waitable_t *waitable, wasip3_waitable_status_t *out,
off_t **offs) {
stdio3_t *stdio = (stdio3_t *)data;
if (!stdio->stdout) {
errno = EBADF;
return -1;
}
*waitable = stdio->stdout;
*out = stdin_stream_u8_write(stdio->stdout, buf, nbyte);
*offs = NULL;
return 0;
}

static int stdio3_read(void *data, void *buf, size_t nbyte,
waitable_t *waitable, wasip3_waitable_status_t *out,
off_t **offs) {
stdio3_t *stdio = (stdio3_t *)data;
if (!stdio->input) {
errno = EBADF;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For wasip2 this returns errno = EOPNOTSUPP; which might be more appropriate here? The fd is valid just of the wrong kind

Copy link
Contributor Author

@cpetig cpetig Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I just checked with the Linux manual page for write which documented EBADF. EOPNOTSUPP makes sense to me as well.

return -1;
}
*waitable = stdio->input;
*out = stdin_stream_u8_read(stdio->input, buf, nbyte);
*offs = NULL;
return 0;
}

static int stdio3_fstat(void *data, struct stat *buf) {
memset(buf, 0, sizeof(*buf));
return 0;
}

static int stdio3_fcntl_getfl(void *data) {
stdio3_t *stdio = (stdio3_t *)data;
if (stdio->stdout == 0) {
return O_RDONLY;
} else {
return O_WRONLY;
}
}

static int stdio3_isatty(void *data) {
stdio3_t *stdio = (stdio3_t *)data;
return stdio->terminal_in.__handle != 0 || stdio->terminal_out.__handle != 0;
}

static descriptor_vtable_t stdio3_vtable = {
.free = stdio3_free,
.read3 = stdio3_read,
.write3 = stdio3_write,
.fstat = stdio3_fstat,
.fcntl_getfl = stdio3_fcntl_getfl,
.isatty = stdio3_isatty,
};

static int stdio_add_input() {
stdio3_t *stdio = calloc(1, sizeof(stdio3_t));
if (!stdio) {
errno = ENOMEM;
return -1;
}
stdin_tuple2_stream_u8_future_result_void_error_code_t stdin;
stdin_read_via_stream(&stdin);

if (!terminal_stdin_get_terminal_stdin(&stdio->terminal_in))
stdio->terminal_in.__handle = 0;
Comment on lines +103 to +107
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this perhaps be more of a lazy initialization similar to how wasip2 works? I felt that worked out pretty well and helps reduce runtime dependencies to a bare minimum where possible


stdio->input = stdin.f0;
stdio->future = stdin.f1;

descriptor_table_entry_t entry;
entry.vtable = &stdio3_vtable;
entry.data = stdio;
return descriptor_table_insert(entry);
}

static int stdio3_add_output(
// int fd,
wasip3_subtask_status_t (*func)(stdin_stream_u8_t data,
stdout_result_void_error_code_t *result),
bool (*terminal)(terminal_stdout_own_terminal_output_t *ret)) {
stdio3_t *stdio = calloc(1, sizeof(stdio3_t));
if (!stdio) {
errno = ENOMEM;
return -1;
}
stdin_stream_u8_t read_side = stdin_stream_u8_new(&stdio->stdout);
wasip3_subtask_status_t res = (*func)(read_side, &stdio->stdout_result);
stdio->stdout_task = WASIP3_SUBTASK_HANDLE(res);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This'll want to handle the case that res wasn't actually "pending" to mean that the stdout stream is closed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The next write will pick up the error anyway, do you think the open should fail immediately? Wouldn't init returning -1 render the file descriptor infrastructure broken (at least the first open will fail in descriptor_table_insert, IIRC).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that returning -1 shouldn't happen, but handling it in this case (IMO) should effectively move stdout into a state of "it's closed, don't try again". I'd imagine that this would trigger writes to start returning EPIPE (or maybe 0) or something like that, but basically the case that the function returns immediately should be used to update the internal structure vs returning an error


if (!(*terminal)(&stdio->terminal_out))
stdio->terminal_out.__handle = 0;

descriptor_table_entry_t entry;
entry.vtable = &stdio3_vtable;
entry.data = stdio;
return descriptor_table_insert(entry);
}

int __wasilibc_init_stdio() {
// TODO(wasip3)
errno = EOPNOTSUPP;
return -1;
if (stdio_add_input() < 0)
return -1;
if (stdio3_add_output(stdout_write_via_stream,
terminal_stdout_get_terminal_stdout) < 0)
return -1;
// assuming that stdout and stderr functions are compatible
if (stdio3_add_output(
(wasip3_subtask_status_t (*)(
stdin_stream_u8_t,
stdout_result_void_error_code_t *))stderr_write_via_stream,
terminal_stderr_get_terminal_stderr) < 0)
return -1;
return 0;
}
#endif // __wasip3__
4 changes: 2 additions & 2 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ add_wasilibc_test(ftruncate.c FS FAILP3)
add_wasilibc_test(fts.c FS FAILP3)
add_wasilibc_test(fwscanf.c FS FAILP3)
add_wasilibc_test(getentropy.c)
add_wasilibc_test(hello.c PASS_REGULAR_EXPRESSION "Hello, World!" FAILP3)
add_wasilibc_test(hello.c PASS_REGULAR_EXPRESSION "Hello, World!")
add_wasilibc_test(ioctl.c FS FAILP3)
add_wasilibc_test(isatty.c FS FAILP3)
add_wasilibc_test(link.c FS FAILP3)
Expand All @@ -277,7 +277,7 @@ add_wasilibc_test(rename.c FS FAILP3)
add_wasilibc_test(rmdir.c FS FAILP3)
add_wasilibc_test(scandir.c FS FAILP3)
add_wasilibc_test(stat.c FS FAILP3)
add_wasilibc_test(stdio.c FS FAILP3)
add_wasilibc_test(stdio.c FS)
add_wasilibc_test(strchrnul.c LDFLAGS -Wl,--stack-first -Wl,--initial-memory=327680)
add_wasilibc_test(strlen.c LDFLAGS -Wl,--stack-first -Wl,--initial-memory=327680)
add_wasilibc_test(strptime.c)
Expand Down