From e606b23829beeddf50c3fbe403af20823ff4143b Mon Sep 17 00:00:00 2001 From: Chris Friedt Date: Tue, 24 Dec 2024 12:44:46 -0500 Subject: [PATCH 1/5] zvfs: improve libc FILE to integer fd abstraction Previously, there was an implicit assumption that Zephyr's internal struct fd_entry * was synonymous with FILE * from the C library. This is generally not the case and aliasing these two distinct types was preventing a fair bit of functionality from Just Working - namely stdio function calls like fgets() and fopen(). The problem count be seen directly when trying to use a function like zvfs_fdopen(). Instead of aliasing the two types, require that all Zephyr C libraries provide 1. FILE *z_libc_file_alloc(int fd, const char *mode) Allocate and populate the required fields of a FILE object 2. int z_libc_file_get_fd(FILE *fp) Convert a FILE* object to an integer file descriptor. For Picolibc and Newlib-based C libraries, these functions set and get the integer file descriptor from a field of the internal FILE object representation. For the minimal C library, these functions convert between array index and struct fd_entry pointers. Includes changes necessary for tests to still pass. Signed-off-by: Chris Friedt Co-authored-by: Jakub Klimczak Signed-off-by: Jakub Klimczak --- include/zephyr/posix/unistd.h | 2 + include/zephyr/sys/fdtable.h | 32 ++++++---- lib/libc/common/CMakeLists.txt | 1 + lib/libc/common/Kconfig | 8 ++- lib/libc/common/source/stdio/fclose.c | 25 ++++++++ lib/libc/newlib/CMakeLists.txt | 2 + lib/libc/newlib/Kconfig | 8 +++ lib/libc/newlib/libc-hooks.c | 6 ++ lib/libc/newlib/zvfs_libc.c | 82 ++++++++++++++++++++++++++ lib/libc/picolibc/CMakeLists.txt | 2 + lib/libc/picolibc/Kconfig | 8 +++ lib/libc/picolibc/stdio.c | 22 ++++++- lib/libc/picolibc/zvfs_libc.c | 85 +++++++++++++++++++++++++++ lib/os/fdtable.c | 60 +++++++++++++++++-- lib/os/zvfs/CMakeLists.txt | 1 + lib/os/zvfs/Kconfig | 20 +++++++ lib/os/zvfs/zvfs_libc_file.c | 85 +++++++++++++++++++++++++++ lib/posix/options/device_io.c | 2 +- lib/posix/options/posix_clock.h | 12 ++++ tests/lib/c_lib/thrd/prj.conf | 2 +- tests/lib/c_lib/thrd/testcase.yaml | 2 +- 21 files changed, 446 insertions(+), 21 deletions(-) create mode 100644 lib/libc/common/source/stdio/fclose.c create mode 100644 lib/libc/newlib/zvfs_libc.c create mode 100644 lib/libc/picolibc/zvfs_libc.c create mode 100644 lib/os/zvfs/zvfs_libc_file.c diff --git a/include/zephyr/posix/unistd.h b/include/zephyr/posix/unistd.h index 2f07aa636e181..ddf65b97d7720 100644 --- a/include/zephyr/posix/unistd.h +++ b/include/zephyr/posix/unistd.h @@ -25,7 +25,9 @@ extern "C" { /* File related operations */ int close(int file); ssize_t write(int file, const void *buffer, size_t count); +ssize_t pwrite(int file, const void *buffer, size_t count, off_t offset); ssize_t read(int file, void *buffer, size_t count); +ssize_t pread(int fd, void *buf, size_t count, off_t offset); off_t lseek(int file, off_t offset, int whence); int fsync(int fd); int ftruncate(int fd, off_t length); diff --git a/include/zephyr/sys/fdtable.h b/include/zephyr/sys/fdtable.h index d4153828fbb2d..50ca50b50b37f 100644 --- a/include/zephyr/sys/fdtable.h +++ b/include/zephyr/sys/fdtable.h @@ -14,6 +14,20 @@ #include #include +/* FIXME: use k_off_t and k_ssize_t to avoid the POSIX->Zephyr->POSIX dependency cycle */ +#ifdef CONFIG_NEWLIB_LIBC +#ifndef _OFF_T_DECLARED +typedef _off_t off_t; +#define _OFF_T_DECLARED +#endif +#ifndef _SSIZE_T_DECLARED +typedef _ssize_t ssize_t; +#define _SSIZE_T_DECLARED +#endif +#endif + +#include + #ifdef CONFIG_PICOLIBC #define ZVFS_O_APPEND 0x0400 #define ZVFS_O_CREAT 0x0040 @@ -60,18 +74,6 @@ extern "C" { #endif -/* FIXME: use k_off_t and k_ssize_t to avoid the POSIX->Zephyr->POSIX dependency cycle */ -#ifdef CONFIG_NEWLIB_LIBC -#ifndef _OFF_T_DECLARED -typedef __off_t off_t; -#define _OFF_T_DECLARED -#endif -#ifndef _SSIZE_T_DECLARED -typedef _ssize_t ssize_t; -#define _SSIZE_T_DECLARED -#endif -#endif - /** * File descriptor virtual method table. * Currently all operations beyond read/write/close go thru ioctl method. @@ -261,6 +263,12 @@ __syscall int zvfs_select(int nfds, struct zvfs_fd_set *ZRESTRICT readfds, struct zvfs_fd_set *ZRESTRICT errorfds, const struct timespec *ZRESTRICT timeout, const void *ZRESTRICT sigmask); +void zvfs_libc_file_alloc_cb(int fd, const char *mode, FILE *fp); +int zvfs_libc_file_alloc(int fd, const char *mode, FILE **fp, k_timeout_t timeout); +void zvfs_libc_file_free(FILE *fp); +int zvfs_libc_file_get_fd(FILE *fp); +FILE *zvfs_libc_file_from_fd(int fd); + /** * Request codes for fd_op_vtable.ioctl(). * diff --git a/lib/libc/common/CMakeLists.txt b/lib/libc/common/CMakeLists.txt index ed3d28fe5a622..5357c0903da43 100644 --- a/lib/libc/common/CMakeLists.txt +++ b/lib/libc/common/CMakeLists.txt @@ -20,6 +20,7 @@ zephyr_library_sources_ifdef(CONFIG_COMMON_LIBC_THRD source/thrd/tss.c ) zephyr_library_sources_ifdef(CONFIG_COMMON_LIBC_REMOVE source/stdio/remove.c) +zephyr_library_sources_ifdef(CONFIG_COMMON_LIBC_FCLOSE source/stdio/fclose.c) # Prevent compiler from optimizing calloc into an infinite recursive call zephyr_library_compile_options($) diff --git a/lib/libc/common/Kconfig b/lib/libc/common/Kconfig index c4ac88dbab1cd..b98faaef6ac1f 100644 --- a/lib/libc/common/Kconfig +++ b/lib/libc/common/Kconfig @@ -109,7 +109,7 @@ config COMMON_LIBC_THRD depends on DYNAMIC_THREAD # Note: the POSIX_API dependency is only necessary until common elements # of C11 threads and POSIX API can be abstracted out to a common library. - depends on POSIX_API + depends on POSIX_THREADS default y help Common implementation of C11 API. @@ -120,3 +120,9 @@ config COMMON_LIBC_REMOVE default y if FILE_SYSTEM help Common implementation of remove(). + +config COMMON_LIBC_FCLOSE + bool "Common C library fclose" + default y if ZVFS && !MINIMAL_LIBC + help + Enable the common C library implementation of fclose() diff --git a/lib/libc/common/source/stdio/fclose.c b/lib/libc/common/source/stdio/fclose.c new file mode 100644 index 0000000000000..11c8e704f0129 --- /dev/null +++ b/lib/libc/common/source/stdio/fclose.c @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2025 Antmicro + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +int fclose(FILE *stream) +{ + int fd; + + fflush(stream); + fd = zvfs_libc_file_get_fd(stream); + + if (!fd) { + return EOF; + } + if (close(fd)) { + return EOF; + } + + return 0; +} diff --git a/lib/libc/newlib/CMakeLists.txt b/lib/libc/newlib/CMakeLists.txt index d3dc448eccafc..97af247e6d77f 100644 --- a/lib/libc/newlib/CMakeLists.txt +++ b/lib/libc/newlib/CMakeLists.txt @@ -3,6 +3,8 @@ zephyr_library() zephyr_library_sources(libc-hooks.c) +zephyr_library_sources_ifdef(CONFIG_ZVFS zvfs_libc.c) + # Do not allow LTO when compiling libc-hooks.c file set_source_files_properties(libc-hooks.c PROPERTIES COMPILE_OPTIONS $) diff --git a/lib/libc/newlib/Kconfig b/lib/libc/newlib/Kconfig index 24c114f35f9fa..6359d71496b01 100644 --- a/lib/libc/newlib/Kconfig +++ b/lib/libc/newlib/Kconfig @@ -80,4 +80,12 @@ config NEWLIB_LIBC_USE_POSIX_LIMITS_H Disable this option if your toolchain's newlib already includes all mandatory POSIX limits. +if ZVFS + +config ZVFS_LIBC_FILE_SIZE + default 184 if 64BIT + default 104 + +endif # ZVFS + endif # NEWLIB_LIBC diff --git a/lib/libc/newlib/libc-hooks.c b/lib/libc/newlib/libc-hooks.c index 4ec1887f1cb54..e690006db096d 100644 --- a/lib/libc/newlib/libc-hooks.c +++ b/lib/libc/newlib/libc-hooks.c @@ -169,6 +169,9 @@ static int (*_stdout_hook)(int) = _stdout_hook_default; void __stdout_hook_install(int (*hook)(int)) { _stdout_hook = hook; +#ifdef CONFIG_ZVFS + stdout->_file = 1; /* STDOUT_FILENO */ +#endif } static unsigned char _stdin_hook_default(void) @@ -181,6 +184,9 @@ static unsigned char (*_stdin_hook)(void) = _stdin_hook_default; void __stdin_hook_install(unsigned char (*hook)(void)) { _stdin_hook = hook; +#ifdef CONFIG_ZVFS + stdin->_file = 0; /* STDIN_FILENO */ +#endif } int z_impl_zephyr_read_stdin(char *buf, int nbytes) diff --git a/lib/libc/newlib/zvfs_libc.c b/lib/libc/newlib/zvfs_libc.c new file mode 100644 index 0000000000000..b8f697e4842cd --- /dev/null +++ b/lib/libc/newlib/zvfs_libc.c @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2024 Tenstorrent AI ULC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include + +BUILD_ASSERT(CONFIG_ZVFS_LIBC_FILE_SIZE >= sizeof(__FILE), + "CONFIG_ZVFS_LIBC_FILE_SIZE must at least match size of FILE object"); +BUILD_ASSERT(CONFIG_ZVFS_LIBC_FILE_ALIGN == __alignof(__FILE), + "CONFIG_ZVFS_LIBC_FILE_SIZE must match alignment of FILE object"); + +static int z_libc_sflags(const char *mode) +{ + int ret = 0; + + switch (mode[0]) { + case 'r': + ret = ZVFS_O_RDONLY; + break; + + case 'w': + ret = ZVFS_O_WRONLY | ZVFS_O_CREAT | ZVFS_O_TRUNC; + break; + + case 'a': + ret = ZVFS_O_WRONLY | ZVFS_O_CREAT | ZVFS_O_APPEND; + break; + default: + return 0; + } + + while (*++mode) { + switch (*mode) { + case '+': + ret |= ZVFS_O_RDWR; + break; + case 'x': + ret |= ZVFS_O_EXCL; + break; + default: + break; + } + } + + return ret; +} + +void zvfs_libc_file_alloc_cb(int fd, const char *mode, FILE *fp) +{ + /* + * These symbols have conflicting declarations in upstream headers. + * Use uintptr_t and a cast to assign cleanly. + */ + extern uintptr_t _close_r; + extern uintptr_t _lseek_r; + extern uintptr_t _read_r; + extern uintptr_t _write_r; + + __ASSERT_NO_MSG(mode != NULL); + __ASSERT_NO_MSG(fp != NULL); + + fp->_flags = z_libc_sflags(mode); + fp->_file = fd; + fp->_cookie = (void *)fp; + + *(uintptr_t *)&fp->_read = (uintptr_t)&_read_r; + *(uintptr_t *)&fp->_write = (uintptr_t)&_write_r; + *(uintptr_t *)&fp->_seek = (uintptr_t)&_lseek_r; + *(uintptr_t *)&fp->_close = (uintptr_t)&_close_r; +} + +int zvfs_libc_file_get_fd(FILE *fp) +{ + return fp->_file; +} diff --git a/lib/libc/picolibc/CMakeLists.txt b/lib/libc/picolibc/CMakeLists.txt index 752379dd76d44..92bf2852bbfc2 100644 --- a/lib/libc/picolibc/CMakeLists.txt +++ b/lib/libc/picolibc/CMakeLists.txt @@ -11,6 +11,8 @@ zephyr_library_sources( stdio.c ) +zephyr_library_sources_ifdef(CONFIG_ZVFS zvfs_libc.c) + zephyr_library_compile_options($) # define __LINUX_ERRNO_EXTENSIONS__ so we get errno defines like -ESHUTDOWN diff --git a/lib/libc/picolibc/Kconfig b/lib/libc/picolibc/Kconfig index be30e6a8f3a60..e9703f48d1de2 100644 --- a/lib/libc/picolibc/Kconfig +++ b/lib/libc/picolibc/Kconfig @@ -200,4 +200,12 @@ config PICOLIBC_GLOBAL_ERRNO endif # PICOLIBC_USE_MODULE +if ZVFS + +config ZVFS_LIBC_FILE_SIZE + default 144 if 64BIT + default 80 + +endif # ZVFS + endif # PICOLIBC diff --git a/lib/libc/picolibc/stdio.c b/lib/libc/picolibc/stdio.c index 342eedc877101..faa2ef46d05d8 100644 --- a/lib/libc/picolibc/stdio.c +++ b/lib/libc/picolibc/stdio.c @@ -5,6 +5,7 @@ */ #include "picolibc-hooks.h" +#include "stdio-bufio.h" static LIBC_DATA int (*_stdout_hook)(int); @@ -22,6 +23,7 @@ static inline int z_vrfy_zephyr_fputc(int c, FILE *stream) #include #endif +#ifndef CONFIG_ZVFS static int picolibc_put(char a, FILE *f) { zephyr_fputc(a, f); @@ -30,6 +32,7 @@ static int picolibc_put(char a, FILE *f) static LIBC_DATA FILE __stdout = FDEV_SETUP_STREAM(picolibc_put, NULL, NULL, 0); static LIBC_DATA FILE __stdin = FDEV_SETUP_STREAM(NULL, NULL, NULL, 0); +#endif #ifdef __strong_reference #define STDIO_ALIAS(x) __strong_reference(stdout, x); @@ -37,18 +40,35 @@ static LIBC_DATA FILE __stdin = FDEV_SETUP_STREAM(NULL, NULL, NULL, 0); #define STDIO_ALIAS(x) FILE *const x = &__stdout; #endif +#ifndef CONFIG_ZVFS FILE *const stdin = &__stdin; FILE *const stdout = &__stdout; STDIO_ALIAS(stderr); +#endif void __stdout_hook_install(int (*hook)(int)) { - _stdout_hook = hook; +#ifdef CONFIG_ZVFS + struct __file_bufio *bp = (struct __file_bufio *)stdout; + + bp->ptr = INT_TO_POINTER(1 /* STDOUT_FILENO */); + bp->bflags |= _FDEV_SETUP_WRITE; +#else __stdout.flags |= _FDEV_SETUP_WRITE; +#endif + + _stdout_hook = hook; } void __stdin_hook_install(unsigned char (*hook)(void)) { +#ifdef CONFIG_ZVFS + struct __file_bufio *bp = (struct __file_bufio *)stdin; + + bp->bflags |= _FDEV_SETUP_READ; + bp->ptr = INT_TO_POINTER(0 /* STDIN_FILENO */); +#else __stdin.get = (int (*)(FILE *)) hook; __stdin.flags |= _FDEV_SETUP_READ; +#endif } diff --git a/lib/libc/picolibc/zvfs_libc.c b/lib/libc/picolibc/zvfs_libc.c new file mode 100644 index 0000000000000..470aad20c2337 --- /dev/null +++ b/lib/libc/picolibc/zvfs_libc.c @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2024 Tenstorrent AI ULC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "stdio-bufio.h" + +#include +#include +#include + +#include +#include + +BUILD_ASSERT(CONFIG_ZVFS_LIBC_FILE_SIZE >= sizeof(struct __file_bufio), + "CONFIG_ZVFS_LIBC_FILE_SIZE must at least match size of FILE object"); +BUILD_ASSERT(CONFIG_ZVFS_LIBC_FILE_ALIGN == __alignof(struct __file_bufio), + "CONFIG_ZVFS_LIBC_FILE_SIZE must match alignment of FILE object"); + +#define FDEV_SETUP_ZVFS(fd, buf, size, rwflags, bflags) \ + FDEV_SETUP_BUFIO(fd, buf, size, zvfs_read_wrap, zvfs_write_wrap, zvfs_lseek, zvfs_close, \ + rwflags, bflags) + +/* FIXME: do not use ssize_t or off_t */ +ssize_t zvfs_read(int fd, void *buf, size_t sz, const size_t *from_offset); +ssize_t zvfs_write(int fd, const void *buf, size_t sz, const size_t *from_offset); +off_t zvfs_lseek(int fd, off_t offset, int whence); +int zvfs_close(int fd); + +static ssize_t zvfs_read_wrap(int fd, void *buf, size_t sz) +{ + return zvfs_read(fd, buf, sz, NULL); +} + +static ssize_t zvfs_write_wrap(int fd, const void *buf, size_t sz) +{ + return zvfs_write(fd, buf, sz, NULL); +} + +static int sflags(const char *mode) +{ + int flags = 0; + + if (mode == NULL) { + return 0; + } + + switch (mode[0]) { + case 'r': + flags |= __SRD; + break; + case 'w': + flags |= __SWR; + break; + case 'a': + flags |= __SWR; + break; + default: + /* ignore */ + break; + } + if (mode[1] == '+') { + flags |= __SRD | __SWR; + } + + return flags; +} + +void zvfs_libc_file_alloc_cb(int fd, const char *mode, FILE *fp) +{ + struct __file_bufio *const bf = (struct __file_bufio *)fp; + + *bf = (struct __file_bufio)FDEV_SETUP_ZVFS(fd, (char *)(bf + 1), BUFSIZ, sflags(mode), + __BFALL); + + __bufio_lock_init(&(bf->xfile.cfile.file)); +} + +int zvfs_libc_file_get_fd(FILE *fp) +{ + const struct __file_bufio *const bf = (const struct __file_bufio *)fp; + + return POINTER_TO_INT(bf->ptr); +} diff --git a/lib/os/fdtable.c b/lib/os/fdtable.c index be53627efca7b..ad060f333befa 100644 --- a/lib/os/fdtable.c +++ b/lib/os/fdtable.c @@ -75,6 +75,47 @@ static struct fd_entry fdtable[CONFIG_ZVFS_OPEN_MAX] = { static K_MUTEX_DEFINE(fdtable_lock); +#ifdef CONFIG_MINIMAL_LIBC +void zvfs_libc_file_alloc_cb(int fd, const char *mode, FILE *fp) +{ + ARG_UNUSED(fd); + ARG_UNUSED(mode); + ARG_UNUSED(fp); +} + +int zvfs_libc_file_alloc(int fd, const char *mode, FILE **fp, k_timeout_t timeout) +{ + ARG_UNUSED(mode); + ARG_UNUSED(timeout); + + __ASSERT_NO_MSG(mode != NULL); + __ASSERT_NO_MSG(fp != NULL); + + *fp = (FILE *)&fdtable[fd]; + + return 0; +} + +void zvfs_libc_file_free(FILE *fp) +{ + ARG_UNUSED(fp); +} + +int zvfs_libc_file_get_fd(FILE *fp) +{ + return (const struct fd_entry *)fp - fdtable; +} + +FILE *zvfs_libc_file_from_fd(int fd) +{ + if ((fd < 0) || (fd >= ARRAY_SIZE(fdtable))) { + return NULL; + } + + return (FILE *)&fdtable[fd]; +} +#endif + static int z_fd_ref(int fd) { return atomic_inc(&fdtable[fd].refcount) + 1; @@ -406,6 +447,7 @@ int zvfs_close(int fd) } k_mutex_unlock(&fdtable[fd].lock); + zvfs_libc_file_free(zvfs_libc_file_from_fd(fd)); zvfs_free_fd(fd); return res; @@ -413,23 +455,33 @@ int zvfs_close(int fd) FILE *zvfs_fdopen(int fd, const char *mode) { - ARG_UNUSED(mode); + int ret; + FILE *fp = NULL; if (_check_fd(fd) < 0) { return NULL; } - return (FILE *)&fdtable[fd]; + ret = zvfs_libc_file_alloc(fd, mode, &fp, K_NO_WAIT); + if (ret < 0) { + errno = ENOMEM; + } else { + __ASSERT(fp != NULL, "zvfs_libc_file_alloc() returned an invalid file pointer"); + } + + return fp; } int zvfs_fileno(FILE *file) { - if (!IS_ARRAY_ELEMENT(fdtable, file)) { + int fd = zvfs_libc_file_get_fd(file); + + if (fd < 0 || fd >= ARRAY_SIZE(fdtable)) { errno = EBADF; return -1; } - return (struct fd_entry *)file - fdtable; + return fd; } int zvfs_fstat(int fd, struct stat *buf) diff --git a/lib/os/zvfs/CMakeLists.txt b/lib/os/zvfs/CMakeLists.txt index d855d1005efc6..c9120e697a864 100644 --- a/lib/os/zvfs/CMakeLists.txt +++ b/lib/os/zvfs/CMakeLists.txt @@ -1,6 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 zephyr_library() +zephyr_library_sources_ifndef(CONFIG_MINIMAL_LIBC zvfs_libc_file.c) zephyr_library_sources_ifdef(CONFIG_ZVFS_EVENTFD zvfs_eventfd.c) zephyr_library_sources_ifdef(CONFIG_ZVFS_POLL zvfs_poll.c) zephyr_library_sources_ifdef(CONFIG_ZVFS_SELECT zvfs_select.c) diff --git a/lib/os/zvfs/Kconfig b/lib/os/zvfs/Kconfig index 794c15142db26..ec96025f98a21 100644 --- a/lib/os/zvfs/Kconfig +++ b/lib/os/zvfs/Kconfig @@ -7,6 +7,7 @@ menuconfig ZVFS bool "Zephyr virtual filesystem (ZVFS) support [EXPERIMENTAL]" select FDTABLE select EXPERIMENTAL + select NATIVE_LIBC_INCOMPATIBLE help ZVFS is a central, Zephyr-native library that provides a common interoperable API for all types of file descriptors such as those from the non-virtual FS, sockets, eventfds, FILE *'s @@ -14,6 +15,25 @@ menuconfig ZVFS if ZVFS +config ZVFS_LIBC_FILE_SIZE + int + default -1 + help + Size of C library FILE objects, in bytes. All C libraries must specify this value via + Kconfig. + + Beyond that, all C libraries must also provide the following two functions: + + - void zvfs_libc_file_alloc_cb(int fd, const char *mode, FILE *fp); + - void zvfs_libc_file_get_fd(int fd, const char *mode, FILE *fp); + +config ZVFS_LIBC_FILE_ALIGN + int + default 8 if 64BIT + default 4 + help + Alignment of C library FILE objects, in bytes. + config ZVFS_EVENTFD bool "ZVFS event file descriptor support" imply ZVFS_POLL diff --git a/lib/os/zvfs/zvfs_libc_file.c b/lib/os/zvfs/zvfs_libc_file.c new file mode 100644 index 0000000000000..d0117fe67dbf6 --- /dev/null +++ b/lib/os/zvfs/zvfs_libc_file.c @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2024 Zephyr Contributors + * Copyright (c) 2025 Antmicro + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include +#include + +/* in case these are defined as macros */ +#undef stdin +#undef stdout +#undef stderr + +static char __aligned(WB_UP(CONFIG_ZVFS_LIBC_FILE_ALIGN)) _stdin[CONFIG_ZVFS_LIBC_FILE_SIZE]; +static char __aligned(WB_UP(CONFIG_ZVFS_LIBC_FILE_ALIGN)) _stdout[CONFIG_ZVFS_LIBC_FILE_SIZE]; +static char __aligned(WB_UP(CONFIG_ZVFS_LIBC_FILE_ALIGN)) _stderr[CONFIG_ZVFS_LIBC_FILE_SIZE]; +FILE *const stdin = (FILE *)(&_stdin); +FILE *const stdout = (FILE *)(&_stdout); +FILE *const stderr = (FILE *)(&_stderr); + +#define table_stride ROUND_UP(CONFIG_ZVFS_LIBC_FILE_SIZE, CONFIG_ZVFS_LIBC_FILE_ALIGN) +#define table_data ((uint8_t *)_k_mem_slab_buf_zvfs_libc_file_table) + +K_MEM_SLAB_DEFINE_STATIC(zvfs_libc_file_table, CONFIG_ZVFS_LIBC_FILE_SIZE, CONFIG_ZVFS_OPEN_MAX, + CONFIG_ZVFS_LIBC_FILE_ALIGN); + +static int file_to_fd[CONFIG_ZVFS_OPEN_MAX]; +#define ptr_to_idx(fp) (POINTER_TO_INT((uint8_t *)fp - table_data) / table_stride) + +int zvfs_libc_file_alloc(int fd, const char *mode, FILE **fp, k_timeout_t timeout) +{ + int ret; + + __ASSERT_NO_MSG(mode != NULL); + __ASSERT_NO_MSG(fp != NULL); + + ret = k_mem_slab_alloc(&zvfs_libc_file_table, (void **)fp, timeout); + if (ret < 0) { + return ret; + } + file_to_fd[ptr_to_idx(*fp)] = fd; + zvfs_libc_file_alloc_cb(fd, mode, *fp); + + return 0; +} + +void zvfs_libc_file_free(FILE *fp) +{ + if ((fp == NULL) || (fp == stdin) || (fp == stdout) || (fp == stderr)) { + return; + } + + k_mem_slab_free(&zvfs_libc_file_table, (void *)fp); + file_to_fd[ptr_to_idx(fp)] = -1; +} + +FILE *zvfs_libc_file_from_fd(int fd) +{ + if ((fd < 0) || (fd >= ARRAY_SIZE(file_to_fd))) { + return NULL; + } + switch (fd) { + case 0: + return stdin; + case 1: + return stdout; + case 2: + return stderr; + default: + /* fd points at an actual file */ + break; + } + + for (int i = 0; i < ARRAY_SIZE(file_to_fd); i++) { + if (file_to_fd[i] == fd) { + return (FILE *)&_k_mem_slab_buf_zvfs_libc_file_table[table_stride * i]; + } + } + return NULL; +} diff --git a/lib/posix/options/device_io.c b/lib/posix/options/device_io.c index 6fea22d01545d..83c180fa9d208 100644 --- a/lib/posix/options/device_io.c +++ b/lib/posix/options/device_io.c @@ -99,7 +99,7 @@ int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, return zvfs_select(nfds, readfds, writefds, exceptfds, timeout, sigmask); } -ssize_t pwrite(int fd, void *buf, size_t count, off_t offset) +ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset) { size_t off = (size_t)offset; diff --git a/lib/posix/options/posix_clock.h b/lib/posix/options/posix_clock.h index b955d81f914b3..bad12eb99ea5b 100644 --- a/lib/posix/options/posix_clock.h +++ b/lib/posix/options/posix_clock.h @@ -22,6 +22,18 @@ extern "C" { /** @cond INTERNAL_HIDDEN */ +#if !defined(_CLOCK_T_DECLARED) && !defined(__clock_t_defined) +typedef unsigned long clock_t; +#define _CLOCK_T_DECLARED +#define __clock_t_defined +#endif + +#if !defined(_CLOCKID_T_DECLARED) && !defined(__clockid_t_defined) +typedef unsigned long clockid_t; +#define _CLOCKID_T_DECLARED +#define __clockid_t_defined +#endif + static inline int64_t ts_to_ns(const struct timespec *ts) { return ts->tv_sec * NSEC_PER_SEC + ts->tv_nsec; diff --git a/tests/lib/c_lib/thrd/prj.conf b/tests/lib/c_lib/thrd/prj.conf index 2ee6109079b6f..21a8d36f94fae 100644 --- a/tests/lib/c_lib/thrd/prj.conf +++ b/tests/lib/c_lib/thrd/prj.conf @@ -2,7 +2,7 @@ CONFIG_ZTEST=y CONFIG_TEST_USERSPACE=y CONFIG_ZTEST_FATAL_HOOK=y -CONFIG_POSIX_API=y +CONFIG_POSIX_AEP_CHOICE_BASE=y CONFIG_THREAD_STACK_INFO=y CONFIG_DYNAMIC_THREAD=y CONFIG_DYNAMIC_THREAD_POOL_SIZE=2 diff --git a/tests/lib/c_lib/thrd/testcase.yaml b/tests/lib/c_lib/thrd/testcase.yaml index e11360efd2eaf..a126353d6b112 100644 --- a/tests/lib/c_lib/thrd/testcase.yaml +++ b/tests/lib/c_lib/thrd/testcase.yaml @@ -12,7 +12,7 @@ common: tests: libraries.libc.c11_threads.minimal: tags: minimal_libc - filter: CONFIG_MINIMAL_LIBC_SUPPORTED + filter: CONFIG_MINIMAL_LIBC_SUPPORTED and not CONFIG_REQUIRES_FULL_LIBC extra_configs: - CONFIG_MINIMAL_LIBC=y - CONFIG_MINIMAL_LIBC_NON_REENTRANT_FUNCTIONS=y From f156e34dd81af8e08bd865a45fce0d3535e02848 Mon Sep 17 00:00:00 2001 From: Chris Friedt Date: Sat, 31 May 2025 13:17:48 -0400 Subject: [PATCH 2/5] tests: posix: add POSIX_DEVICE_IO testsuite Add a testsuite for the POSIX_DEVICE_IO Option Group. Signed-off-by: Chris Friedt Co-authored-by: Jakub Klimczak Signed-off-by: Jakub Klimczak --- tests/posix/device_io/CMakeLists.txt | 12 ++ tests/posix/device_io/prj.conf | 8 ++ tests/posix/device_io/src/main.c | 185 +++++++++++++++++++++++++++ tests/posix/device_io/testcase.yaml | 35 +++++ 4 files changed, 240 insertions(+) create mode 100644 tests/posix/device_io/CMakeLists.txt create mode 100644 tests/posix/device_io/prj.conf create mode 100644 tests/posix/device_io/src/main.c create mode 100644 tests/posix/device_io/testcase.yaml diff --git a/tests/posix/device_io/CMakeLists.txt b/tests/posix/device_io/CMakeLists.txt new file mode 100644 index 0000000000000..24c7cfd219486 --- /dev/null +++ b/tests/posix/device_io/CMakeLists.txt @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(posix_c_lib_ext) + +FILE(GLOB app_sources src/*.c) + +target_sources(app PRIVATE ${app_sources}) + +target_include_directories(app PRIVATE ${ZEPHYR_BASE}/lib/posix/options/getopt) +target_compile_options(app PRIVATE -U_POSIX_C_SOURCE -D_POSIX_C_SOURCE=200809L) diff --git a/tests/posix/device_io/prj.conf b/tests/posix/device_io/prj.conf new file mode 100644 index 0000000000000..de1f0919083a9 --- /dev/null +++ b/tests/posix/device_io/prj.conf @@ -0,0 +1,8 @@ +CONFIG_POSIX_API=y +CONFIG_ZTEST=y + +CONFIG_POSIX_AEP_CHOICE_BASE=y +CONFIG_POSIX_DEVICE_IO=y + +CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048 +CONFIG_ZTEST_STACK_SIZE=2048 diff --git a/tests/posix/device_io/src/main.c b/tests/posix/device_io/src/main.c new file mode 100644 index 0000000000000..bcf0815b949dd --- /dev/null +++ b/tests/posix/device_io/src/main.c @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2025 Tenstorrent AI ULC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include + +#include +#include +#include +#include + +#ifndef STDIN_FILENO +#define STDIN_FILENO 0 +#endif + +#ifndef STDOUT_FILENO +#define STDOUT_FILENO 1 +#endif + +#ifndef STDERR_FILENO +#define STDERR_FILENO 2 +#endif + +ZTEST(posix_device_io, test_FD_CLR) +{ + fd_set fds; + + FD_CLR(0, &fds); +} + +ZTEST(posix_device_io, test_FD_ISSET) +{ + fd_set fds; + + zassert_false(FD_ISSET(0, &fds)); +} + +ZTEST(posix_device_io, test_FD_SET) +{ + fd_set fds; + + FD_SET(0, &fds); + zassert_true(FD_ISSET(0, &fds)); +} + +ZTEST(posix_device_io, test_FD_ZERO) +{ + fd_set fds; + + FD_ZERO(&fds); + zassert_false(FD_ISSET(0, &fds)); +} + +ZTEST(posix_device_io, test_close) +{ + zassert_not_ok(close(-1)); +} + +ZTEST(posix_device_io, test_fdopen) +{ + zassert_not_null(fdopen(1, "r")); +} + +ZTEST(posix_device_io, test_fileno) +{ +#define DECL_TDATA(_stream, _fd) \ + { \ + .stream_name = #_stream, \ + .stream = _stream, \ + .fd_name = #_fd, \ + .fd = _fd, \ + } + + const struct fileno_test_data { + const char *stream_name; + FILE *stream; + const char *fd_name; + int fd; + } test_data[] = { + DECL_TDATA(stdin, STDIN_FILENO), + DECL_TDATA(stdout, STDOUT_FILENO), + DECL_TDATA(stderr, STDERR_FILENO), + }; + + ARRAY_FOR_EACH(test_data, i) { + FILE *stream = test_data[i].stream; + int expect_fd = test_data[i].fd; + int actual_fd = fileno(stream); + + if ((i == STDERR_FILENO) && + (IS_ENABLED(CONFIG_PICOLIBC) || IS_ENABLED(CONFIG_NEWLIB_LIBC))) { + TC_PRINT("Note: stderr not enabled\n"); + continue; + } + + zexpect_equal(actual_fd, expect_fd, "fileno(%s) (%d) != %s (%d)", + test_data[i].stream_name, actual_fd, test_data[i].fd_name, expect_fd); + } +} + +ZTEST(posix_device_io, test_open) +{ + /* + * Note: open() is already exercised extensively in tests/posix/fs, but we should test it + * here on device nodes as well. + */ + zexpect_equal(open("/dev/null", O_RDONLY), -1); + zexpect_equal(errno, ENOENT); +} + +ZTEST(posix_device_io, test_poll) +{ + struct pollfd fds[1] = {{.fd = STDIN_FILENO, .events = POLLIN}}; + + /* + * Note: poll() is already exercised extensively in tests/posix/eventfd, but we should test + * it here on device nodes as well. + */ + zexpect_equal(poll(fds, ARRAY_SIZE(fds), 0), 1); +} + +ZTEST(posix_device_io, test_pread) +{ + uint8_t buf[8]; + + zexpect_equal(pread(STDIN_FILENO, buf, sizeof(buf), 0), -1); + zexpect_equal(errno, ENOTSUP); +} + +ZTEST(posix_device_io, test_pselect) +{ + fd_set fds; + struct timespec timeout = {.tv_sec = 0, .tv_nsec = 0}; + + FD_ZERO(&fds); + FD_SET(STDIN_FILENO, &fds); + + /* Zephyr does not yet support select or poll on stdin */ + zexpect_equal(pselect(STDIN_FILENO + 1, &fds, NULL, NULL, &timeout, NULL), -1); + zexpect_equal(errno, EBADF); +} + +ZTEST(posix_device_io, test_pwrite) +{ + /* Zephyr does not yet support writing through a file descriptor */ + zexpect_equal(pwrite(STDOUT_FILENO, "x", 1, 0), -1); + zexpect_equal(errno, ENOTSUP, "%d", errno); +} + +ZTEST(posix_device_io, test_read) +{ + uint8_t buf[8]; + + /* reading from stdin does not work in Zephyr */ + zassert_equal(read(STDIN_FILENO, buf, sizeof(buf)), 0); +} + +ZTEST(posix_device_io, test_select) +{ + fd_set fds; + struct timeval timeout = {.tv_sec = 0, .tv_usec = 0}; + + FD_ZERO(&fds); + FD_SET(STDIN_FILENO, &fds); + + /* Zephyr does not yet support select or poll on stdin */ + zassert_equal(select(STDIN_FILENO + 1, &fds, NULL, NULL, &timeout), -1); + zexpect_equal(errno, EBADF, "%d", errno); +} + +ZTEST(posix_device_io, test_write) +{ +/* write is only implemented in newlib and arcmwdt */ +#if defined(CONFIG_NEWLIB_LIBC) || defined(CONFIG_ARCMWDT_LIBC) + zexpect_equal(write(STDOUT_FILENO, "x", 1), 1); +#else + zexpect_equal(write(STDOUT_FILENO, "x", 1), 0); +#endif +} + +ZTEST_SUITE(posix_device_io, NULL, NULL, NULL, NULL, NULL); diff --git a/tests/posix/device_io/testcase.yaml b/tests/posix/device_io/testcase.yaml new file mode 100644 index 0000000000000..546186774ed2b --- /dev/null +++ b/tests/posix/device_io/testcase.yaml @@ -0,0 +1,35 @@ +common: + filter: not CONFIG_NATIVE_LIBC + tags: + - posix + - device_io + # 1 tier0 platform per supported architecture + platform_key: + - arch + - simulation + min_ram: 32 +tests: + portability.posix.device_io: + extra_configs: + - CONFIG_COMMON_LIBC_MALLOC_ARENA_SIZE=256 + portability.posix.device_io.armclang_std_libc: + toolchain_allow: armclang + extra_configs: + - CONFIG_ARMCLANG_STD_LIBC=y + portability.posix.device_io.arcmwdtlib: + toolchain_allow: arcmwdt + extra_configs: + - CONFIG_ARCMWDT_LIBC=y + portability.posix.device_io.minimal: + extra_configs: + - CONFIG_MINIMAL_LIBC=y + - CONFIG_COMMON_LIBC_MALLOC_ARENA_SIZE=256 + portability.posix.device_io.newlib: + filter: TOOLCHAIN_HAS_NEWLIB == 1 + extra_configs: + - CONFIG_NEWLIB_LIBC=y + portability.posix.device_io.picolibc: + tags: picolibc + filter: CONFIG_PICOLIBC_SUPPORTED + extra_configs: + - CONFIG_PICOLIBC=y From 1a32f98139fb11ccf8b3cf279e2b0533f7124f24 Mon Sep 17 00:00:00 2001 From: Jakub Klimczak Date: Thu, 4 Sep 2025 10:19:28 +0200 Subject: [PATCH 3/5] tests: posix: fs: Add test for fdopen Add a test to the POSIX filesystem API suite that checks whether fdopen() returns a valid value. Signed-off-by: Jakub Klimczak --- tests/posix/fs/src/test_fs_file.c | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/posix/fs/src/test_fs_file.c b/tests/posix/fs/src/test_fs_file.c index 23496c92b8c22..be52a3df625bf 100644 --- a/tests/posix/fs/src/test_fs_file.c +++ b/tests/posix/fs/src/test_fs_file.c @@ -28,6 +28,18 @@ static int test_file_open(void) return TC_PASS; } +static int test_file_fdopen(void) +{ + FILE *fp = fdopen(file, "r"); + + if (fp == NULL) { + TC_ERROR("Failed associating file descriptor %d with FILE, errno=%d\n", file, + errno); + return TC_FAIL; + } + return TC_PASS; +} + int test_file_write(void) { ssize_t brw; @@ -197,6 +209,18 @@ ZTEST(posix_fs_file_test, test_fs_open) zassert_true(test_file_open() == TC_PASS); } +/** + * @brief Test for POSIX fdopen API + * + * @details Test converts file descriptor to FILE * through POSIX fdopen API. + */ +ZTEST(posix_fs_file_test, test_fs_fdopen) +{ + /* FIXME: restructure tests as per #46897 */ + zassert_true(test_file_open() == TC_PASS); + zassert_true(test_file_fdopen() == TC_PASS); +} + /** * @brief Test for POSIX write API * From dd819e4631186a9e45ed62acf15c6c0feaa34357 Mon Sep 17 00:00:00 2001 From: Jakub Klimczak Date: Thu, 11 Sep 2025 12:14:39 +0200 Subject: [PATCH 4/5] lib: Implement POSIX read(), write(), poll() etc. on stdio Give the user the ability to interact with stdio using POSIX read() and write() using the tty.h API. This avoids depending on standard C functions and allows poll(), select() et al. to work for stdio thanks to the internal use of semaphores. Alternatively, should there be no UART console device, read() and write() will use stdio hooks (where available) and poll() will simply return POLLNVAL for fd's 0, 1 and 2. Tested using the posix/device_io test suite. Signed-off-by: Jakub Klimczak --- .../minimal/source/stdout/stdout_console.c | 22 +++ lib/libc/picolibc/stdio.c | 56 +++++-- lib/os/fdtable.c | 138 +++++++++++++++++- lib/posix/options/Kconfig.device_io | 29 ++++ subsys/console/CMakeLists.txt | 3 +- 5 files changed, 227 insertions(+), 21 deletions(-) diff --git a/lib/libc/minimal/source/stdout/stdout_console.c b/lib/libc/minimal/source/stdout/stdout_console.c index 9cef4afc873d0..dac63a5e80543 100644 --- a/lib/libc/minimal/source/stdout/stdout_console.c +++ b/lib/libc/minimal/source/stdout/stdout_console.c @@ -107,6 +107,28 @@ static inline size_t z_vrfy_zephyr_fwrite(const void *ZRESTRICT ptr, #include #endif +int z_impl_zephyr_write_stdout(const void *buffer, int nbytes) +{ + const char *buf = buffer; + + for (int i = 0; i < nbytes; i++) { + if (*(buf + i) == '\n') { + _stdout_hook('\r'); + } + _stdout_hook(*(buf + i)); + } + return nbytes; +} + +#ifdef CONFIG_USERSPACE +static inline int z_vrfy_zephyr_write_stdout(const void *buf, int nbytes) +{ + K_OOPS(K_SYSCALL_MEMORY_READ(buf, nbytes)); + return z_impl_zephyr_write_stdout((const void *)buf, nbytes); +} +#include +#endif + size_t fwrite(const void *ZRESTRICT ptr, size_t size, size_t nitems, FILE *ZRESTRICT stream) { diff --git a/lib/libc/picolibc/stdio.c b/lib/libc/picolibc/stdio.c index faa2ef46d05d8..74608ded862af 100644 --- a/lib/libc/picolibc/stdio.c +++ b/lib/libc/picolibc/stdio.c @@ -7,12 +7,18 @@ #include "picolibc-hooks.h" #include "stdio-bufio.h" -static LIBC_DATA int (*_stdout_hook)(int); +static int _stdout_hook_default(int c) +{ + (void)(c); /* Prevent warning about unused argument */ + + return EOF; +} + +static LIBC_DATA int (*_stdout_hook)(int) = _stdout_hook_default; int z_impl_zephyr_fputc(int a, FILE *out) { - (*_stdout_hook)(a); - return 0; + return ((out == stdout) || (out == stderr)) ? _stdout_hook(a) : EOF; } #ifdef CONFIG_USERSPACE @@ -23,15 +29,14 @@ static inline int z_vrfy_zephyr_fputc(int c, FILE *stream) #include #endif -#ifndef CONFIG_ZVFS static int picolibc_put(char a, FILE *f) { - zephyr_fputc(a, f); - return 0; + return zephyr_fputc(a, f); } -static LIBC_DATA FILE __stdout = FDEV_SETUP_STREAM(picolibc_put, NULL, NULL, 0); -static LIBC_DATA FILE __stdin = FDEV_SETUP_STREAM(NULL, NULL, NULL, 0); +#ifndef CONFIG_ZVFS +static LIBC_DATA FILE __stdout = FDEV_SETUP_STREAM(picolibc_put, NULL, NULL, _FDEV_SETUP_WRITE); +static LIBC_DATA FILE __stdin = FDEV_SETUP_STREAM(NULL, NULL, NULL, _FDEV_SETUP_READ); #endif #ifdef __strong_reference @@ -48,27 +53,48 @@ STDIO_ALIAS(stderr); void __stdout_hook_install(int (*hook)(int)) { + _stdout_hook = hook; #ifdef CONFIG_ZVFS + stdout->put = picolibc_put; + stdout->flags |= _FDEV_SETUP_WRITE; + struct __file_bufio *bp = (struct __file_bufio *)stdout; bp->ptr = INT_TO_POINTER(1 /* STDOUT_FILENO */); bp->bflags |= _FDEV_SETUP_WRITE; -#else - __stdout.flags |= _FDEV_SETUP_WRITE; #endif - - _stdout_hook = hook; } void __stdin_hook_install(unsigned char (*hook)(void)) { + stdin->get = (int (*)(FILE *))hook; #ifdef CONFIG_ZVFS + stdin->flags |= _FDEV_SETUP_READ; struct __file_bufio *bp = (struct __file_bufio *)stdin; bp->bflags |= _FDEV_SETUP_READ; bp->ptr = INT_TO_POINTER(0 /* STDIN_FILENO */); -#else - __stdin.get = (int (*)(FILE *)) hook; - __stdin.flags |= _FDEV_SETUP_READ; #endif } + +int z_impl_zephyr_write_stdout(const void *buffer, int nbytes) +{ + const char *buf = buffer; + + for (int i = 0; i < nbytes; i++) { + if (*(buf + i) == '\n') { + _stdout_hook('\r'); + } + _stdout_hook(*(buf + i)); + } + return nbytes; +} + +#ifdef CONFIG_USERSPACE +static inline int z_vrfy_zephyr_write_stdout(const void *buf, int nbytes) +{ + K_OOPS(K_SYSCALL_MEMORY_READ(buf, nbytes)); + return z_impl_zephyr_write_stdout((const void *)buf, nbytes); +} +#include +#endif diff --git a/lib/os/fdtable.c b/lib/os/fdtable.c index ad060f333befa..e7c3fc431ff90 100644 --- a/lib/os/fdtable.c +++ b/lib/os/fdtable.c @@ -23,6 +23,10 @@ #include #include #include +#include +#include +#include +#include struct stat; @@ -42,6 +46,12 @@ static const struct fd_op_vtable stdinout_fd_op_vtable; BUILD_ASSERT(CONFIG_ZVFS_OPEN_MAX >= 3, "CONFIG_ZVFS_OPEN_MAX >= 3 for CONFIG_POSIX_DEVICE_IO"); #endif /* defined(CONFIG_POSIX_DEVICE_IO) */ +#if defined(CONFIG_POSIX_DEVICE_IO_STDIO_CONSOLE) +static struct tty_serial tty; +static uint8_t tty_rxbuf[CONFIG_POSIX_DEVICE_IO_STDIN_BUFSIZE]; +static uint8_t tty_txbuf[CONFIG_POSIX_DEVICE_IO_STDOUT_BUFSIZE]; +#endif + static struct fd_entry fdtable[CONFIG_ZVFS_OPEN_MAX] = { #if defined(CONFIG_POSIX_DEVICE_IO) /* @@ -49,6 +59,9 @@ static struct fd_entry fdtable[CONFIG_ZVFS_OPEN_MAX] = { */ { /* STDIN */ +#if defined(CONFIG_POSIX_DEVICE_IO_STDIO_CONSOLE) + .obj = &tty, +#endif .vtable = &stdinout_fd_op_vtable, .refcount = ATOMIC_INIT(1), .lock = Z_MUTEX_INITIALIZER(fdtable[0].lock), @@ -56,6 +69,9 @@ static struct fd_entry fdtable[CONFIG_ZVFS_OPEN_MAX] = { }, { /* STDOUT */ +#if defined(CONFIG_POSIX_DEVICE_IO_STDIO_CONSOLE) + .obj = &tty, +#endif .vtable = &stdinout_fd_op_vtable, .refcount = ATOMIC_INIT(1), .lock = Z_MUTEX_INITIALIZER(fdtable[1].lock), @@ -63,6 +79,9 @@ static struct fd_entry fdtable[CONFIG_ZVFS_OPEN_MAX] = { }, { /* STDERR */ +#if defined(CONFIG_POSIX_DEVICE_IO_STDIO_CONSOLE) + .obj = &tty, +#endif .vtable = &stdinout_fd_op_vtable, .refcount = ATOMIC_INIT(1), .lock = Z_MUTEX_INITIALIZER(fdtable[2].lock), @@ -378,7 +397,7 @@ static ssize_t zvfs_rw(int fd, void *buf, size_t sz, bool is_write, const size_t * Seekable file types should support pread() / pwrite() and per-fd offset passing. * Otherwise, it's a bug. */ - errno = ENOTSUP; + errno = ESPIPE; res = -1; goto unlock; } @@ -593,16 +612,66 @@ int zvfs_ioctl(int fd, unsigned long request, va_list args) * fd operations for stdio/stdout/stderr */ +int z_impl_zephyr_read_stdin(char *buf, int nbytes); int z_impl_zephyr_write_stdout(const char *buf, int nbytes); +#if defined(CONFIG_POSIX_DEVICE_IO_STDIO_CONSOLE) +static int initialize_tty(struct tty_serial *obj) +{ + static bool initialized; + static int ret; + + if (initialized) { + return ret; + } + ret = tty_init(obj, DEVICE_DT_GET(DT_CHOSEN(zephyr_console))); + initialized = true; + if (ret) { + errno = -ret; + ret = -1; + return ret; + } + tty_set_tx_buf(obj, tty_txbuf, sizeof(tty_txbuf)); + tty_set_rx_buf(obj, tty_rxbuf, sizeof(tty_rxbuf)); + return ret; +} +#endif + static ssize_t stdinout_read_vmeth(void *obj, void *buffer, size_t count) { +#if defined(CONFIG_POSIX_DEVICE_IO_STDIO_CONSOLE) + int ret = initialize_tty(obj); + + if (ret) { + return ret; + } + ret = tty_read(obj, buffer, count); + if (ret < -1) { /* return no less than -1 as per POSIX; errno is already set */ + return -1; + } + return ret; +#elif defined(CONFIG_NEWLIB_LIBC) + return z_impl_zephyr_read_stdin(buffer, count); +#else return 0; +#endif } static ssize_t stdinout_write_vmeth(void *obj, const void *buffer, size_t count) { -#if defined(CONFIG_NEWLIB_LIBC) || defined(CONFIG_ARCMWDT_LIBC) +#if defined(CONFIG_POSIX_DEVICE_IO_STDIO_CONSOLE) + int ret = initialize_tty(obj); + + if (ret) { + return ret; + } + ret = tty_write(obj, buffer, count); + if (ret < -1) { /* return no less than -1 as per POSIX; errno is already set */ + return -1; + } + return ret; +#elif defined(CONFIG_ARCMWDT_LIBC) || defined(CONFIG_MINIMAL_LIBC) || \ + defined(CONFIG_NEWLIB_LIBC) || defined(CONFIG_PICOLIBC) return z_impl_zephyr_write_stdout(buffer, count); #else return 0; @@ -611,10 +680,69 @@ static ssize_t stdinout_write_vmeth(void *obj, const void *buffer, size_t count) static int stdinout_ioctl_vmeth(void *obj, unsigned int request, va_list args) { - errno = EINVAL; - return -1; -} +#if defined(CONFIG_POSIX_DEVICE_IO_STDIO_CONSOLE) + struct tty_serial *cons = obj; + if (initialize_tty(cons)) { + return -1; + } +#else + request = 0xffff; +#endif + switch (request) { + case ZFD_IOCTL_POLL_PREPARE: { + __maybe_unused struct zvfs_pollfd *pfd = va_arg(args, struct zvfs_pollfd *); + struct k_poll_event **pev = va_arg(args, struct k_poll_event **); + struct k_poll_event *pev_end = va_arg(args, struct k_poll_event *); + + if (*pev == pev_end) { + return -ENOMEM; + } +#if CONFIG_POSIX_DEVICE_IO_STDIN_BUFSIZE + CONFIG_POSIX_DEVICE_IO_STDOUT_BUFSIZE > 0 + if ((pfd->fd == 0) && (pfd->events & ZVFS_POLLIN)) { + (*pev)->type = K_POLL_TYPE_SEM_AVAILABLE; + (*pev)->state = K_POLL_STATE_NOT_READY; + (*pev)->sem = &(cons->rx_sem); + } + if (((pfd->fd == 1) || (pfd->fd == 2)) && (pfd->events & ZVFS_POLLOUT)) { + (*pev)->type = K_POLL_TYPE_SEM_AVAILABLE; + (*pev)->state = K_POLL_STATE_NOT_READY; + (*pev)->sem = &(cons->tx_sem); + } +#else + (*pev)->type = K_POLL_TYPE_IGNORE; + (*pev)->state = K_POLL_STATE_NOT_READY; + (*pev)->obj = NULL; +#endif + (*pev)++; + return 0; + } + case ZFD_IOCTL_POLL_UPDATE: { + struct zvfs_pollfd *pfd = va_arg(args, struct zvfs_pollfd *); + struct k_poll_event **pev = va_arg(args, struct k_poll_event **); + +#if CONFIG_POSIX_DEVICE_IO_STDIN_BUFSIZE + CONFIG_POSIX_DEVICE_IO_STDOUT_BUFSIZE > 0 + if ((*pev)->state & K_POLL_STATE_SEM_AVAILABLE) { + if ((pfd->fd == 0) && (pfd->events & ZVFS_POLLIN)) { + pfd->revents |= ZVFS_POLLIN; + } + if (((pfd->fd == 1) || (pfd->fd == 2)) && (pfd->events & ZVFS_POLLOUT)) { + pfd->revents |= ZVFS_POLLOUT; + } + } +#else + if (pfd->events & (ZVFS_POLLIN | ZVFS_POLLPRI | ZVFS_POLLOUT)) { + pfd->revents |= ZVFS_POLLNVAL; + } +#endif + (*pev)++; + return 0; + } + default: + errno = EINVAL; + return -1; + } +} static const struct fd_op_vtable stdinout_fd_op_vtable = { .read = stdinout_read_vmeth, diff --git a/lib/posix/options/Kconfig.device_io b/lib/posix/options/Kconfig.device_io index 416ccf74a6218..ffa7b552b0a15 100644 --- a/lib/posix/options/Kconfig.device_io +++ b/lib/posix/options/Kconfig.device_io @@ -20,6 +20,35 @@ config POSIX_DEVICE_IO if POSIX_DEVICE_IO +config POSIX_DEVICE_IO_STDIO_CONSOLE + bool "read() and write() to/from stdio use console subsystem" + default y + depends on CONSOLE_SUBSYS + depends on !(CONSOLE_GETCHAR || CONSOLE_GETLINE) + help + Enable to use UART console whenever read() or write() is called + on stdin, stdout or stderr. + +if POSIX_DEVICE_IO_STDIO_CONSOLE + +config POSIX_DEVICE_IO_STDIN_BUFSIZE + int "read() buffer size for stdin" + default 16 + help + Size of internal buffer for read()-ing from stdin. Setting both this and + POSIX_DEVICE_IO_STDOUT_BUFSIZE to 0 will replace interrupt-driven operation + with busy-polling and stop poll(), select() and others from working with stdio. + +config POSIX_DEVICE_IO_STDOUT_BUFSIZE + int "write() buffer size for stdout" + default 16 + help + Size of internal buffer for write()-ing to stdout. Setting both this and + POSIX_DEVICE_IO_STDIN_BUFSIZE to 0 will replace interrupt-driven operation + with busy-polling and stop poll(), select() and others from working with stdio. + +endif # POSIX_DEVICE_IO_STDIO_CONSOLE + # These options are intended to be used for compatibility with external POSIX # implementations such as those in Newlib or Picolibc. diff --git a/subsys/console/CMakeLists.txt b/subsys/console/CMakeLists.txt index 37ff969710b77..8c03572443052 100644 --- a/subsys/console/CMakeLists.txt +++ b/subsys/console/CMakeLists.txt @@ -1,4 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -zephyr_sources_ifdef(CONFIG_CONSOLE_GETCHAR tty.c getchar.c) +zephyr_sources_ifdef(CONFIG_CONSOLE_SUBSYS tty.c) +zephyr_sources_ifdef(CONFIG_CONSOLE_GETCHAR getchar.c) zephyr_sources_ifdef(CONFIG_CONSOLE_GETLINE getline.c) From 6d6489c56b5e7d89da761bba3158c56ab51b58a3 Mon Sep 17 00:00:00 2001 From: Jakub Klimczak Date: Thu, 11 Sep 2025 12:15:12 +0200 Subject: [PATCH 5/5] tests: posix: device_io: Update tests for working select(), poll() Update the device_io test suite so that it expects select(), pselect() and poll() to work on stdio. Signed-off-by: Jakub Klimczak --- tests/posix/device_io/src/main.c | 104 ++++++++++++++++++---------- tests/posix/device_io/testcase.yaml | 4 ++ 2 files changed, 70 insertions(+), 38 deletions(-) diff --git a/tests/posix/device_io/src/main.c b/tests/posix/device_io/src/main.c index bcf0815b949dd..b922b17a77af3 100644 --- a/tests/posix/device_io/src/main.c +++ b/tests/posix/device_io/src/main.c @@ -32,13 +32,6 @@ ZTEST(posix_device_io, test_FD_CLR) FD_CLR(0, &fds); } -ZTEST(posix_device_io, test_FD_ISSET) -{ - fd_set fds; - - zassert_false(FD_ISSET(0, &fds)); -} - ZTEST(posix_device_io, test_FD_SET) { fd_set fds; @@ -112,73 +105,108 @@ ZTEST(posix_device_io, test_open) zexpect_equal(errno, ENOENT); } +#if (defined(CONFIG_POSIX_DEVICE_IO_STDIO_CONSOLE) && \ + (CONFIG_POSIX_DEVICE_IO_STDIN_BUFSIZE + CONFIG_POSIX_DEVICE_IO_STDOUT_BUFSIZE > 0)) +#define STDIO_POLL +#endif + ZTEST(posix_device_io, test_poll) { - struct pollfd fds[1] = {{.fd = STDIN_FILENO, .events = POLLIN}}; + struct pollfd fds[] = { + {.fd = STDIN_FILENO, .events = POLLIN}, + {.fd = STDOUT_FILENO, .events = POLLOUT}, + {.fd = STDERR_FILENO, .events = POLLOUT}, + }; /* * Note: poll() is already exercised extensively in tests/posix/eventfd, but we should test * it here on device nodes as well. */ - zexpect_equal(poll(fds, ARRAY_SIZE(fds), 0), 1); +#ifdef STDIO_POLL + zexpect_equal(poll(fds, ARRAY_SIZE(fds), 0), 2); + /* stdin shouldn't have any bytes ready to read */ + zexpect_equal(fds[0].revents, 0); + zexpect_equal(fds[1].revents, POLLOUT); + zexpect_equal(fds[2].revents, POLLOUT); +#else + zexpect_equal(poll(fds, ARRAY_SIZE(fds), 0), 3); + zexpect_equal(fds[0].revents, POLLNVAL); + zexpect_equal(fds[1].revents, POLLNVAL); + zexpect_equal(fds[2].revents, POLLNVAL); +#endif } ZTEST(posix_device_io, test_pread) { uint8_t buf[8]; + /* stdio is non-seekable */ zexpect_equal(pread(STDIN_FILENO, buf, sizeof(buf), 0), -1); - zexpect_equal(errno, ENOTSUP); + zexpect_equal(errno, ESPIPE); } ZTEST(posix_device_io, test_pselect) { - fd_set fds; + fd_set readfds; + fd_set writefds; struct timespec timeout = {.tv_sec = 0, .tv_nsec = 0}; - FD_ZERO(&fds); - FD_SET(STDIN_FILENO, &fds); - - /* Zephyr does not yet support select or poll on stdin */ - zexpect_equal(pselect(STDIN_FILENO + 1, &fds, NULL, NULL, &timeout, NULL), -1); - zexpect_equal(errno, EBADF); + FD_ZERO(&readfds); + FD_SET(STDIN_FILENO, &readfds); + FD_ZERO(&writefds); + FD_SET(STDOUT_FILENO, &writefds); + FD_SET(STDERR_FILENO, &writefds); + +#ifdef STDIO_POLL + zexpect_equal(pselect(STDERR_FILENO + 1, &readfds, &writefds, NULL, &timeout, NULL), 2); + zassert_false(FD_ISSET(STDIN_FILENO, &readfds)); + zassert_true(FD_ISSET(STDOUT_FILENO, &writefds)); + zassert_true(FD_ISSET(STDERR_FILENO, &writefds)); +#else + zexpect_equal(pselect(STDERR_FILENO + 1, &readfds, &writefds, NULL, &timeout, NULL), -1); + zassert_equal(errno, EBADF); +#endif } ZTEST(posix_device_io, test_pwrite) { - /* Zephyr does not yet support writing through a file descriptor */ + /* stdio is non-seekable */ zexpect_equal(pwrite(STDOUT_FILENO, "x", 1, 0), -1); - zexpect_equal(errno, ENOTSUP, "%d", errno); -} - -ZTEST(posix_device_io, test_read) -{ - uint8_t buf[8]; - - /* reading from stdin does not work in Zephyr */ - zassert_equal(read(STDIN_FILENO, buf, sizeof(buf)), 0); + zexpect_equal(errno, ESPIPE, "%d", errno); } ZTEST(posix_device_io, test_select) { - fd_set fds; + fd_set readfds; + fd_set writefds; struct timeval timeout = {.tv_sec = 0, .tv_usec = 0}; - FD_ZERO(&fds); - FD_SET(STDIN_FILENO, &fds); - - /* Zephyr does not yet support select or poll on stdin */ - zassert_equal(select(STDIN_FILENO + 1, &fds, NULL, NULL, &timeout), -1); - zexpect_equal(errno, EBADF, "%d", errno); + FD_ZERO(&readfds); + FD_SET(STDIN_FILENO, &readfds); + FD_ZERO(&writefds); + FD_SET(STDOUT_FILENO, &writefds); + FD_SET(STDERR_FILENO, &writefds); + +#ifdef STDIO_POLL + zassert_equal(select(STDERR_FILENO + 1, &readfds, &writefds, NULL, &timeout), 2); + zassert_false(FD_ISSET(STDIN_FILENO, &readfds)); + zassert_true(FD_ISSET(STDOUT_FILENO, &writefds)); + zassert_true(FD_ISSET(STDERR_FILENO, &writefds)); +#else + zassert_equal(select(STDERR_FILENO + 1, &readfds, &writefds, NULL, &timeout), -1); + zassert_equal(errno, EBADF); +#endif } ZTEST(posix_device_io, test_write) { -/* write is only implemented in newlib and arcmwdt */ -#if defined(CONFIG_NEWLIB_LIBC) || defined(CONFIG_ARCMWDT_LIBC) - zexpect_equal(write(STDOUT_FILENO, "x", 1), 1); + static const char msg[] = "Hello world!\n"; + +#if defined(CONFIG_POSIX_DEVICE_IO_STDIO_CONSOLE) || defined(CONFIG_ARCMWDT_LIBC) || \ + defined(CONFIG_MINIMAL_LIBC) || defined(CONFIG_NEWLIB_LIBC) || defined(CONFIG_PICOLIBC) + zexpect_equal(write(STDOUT_FILENO, msg, ARRAY_SIZE(msg)), ARRAY_SIZE(msg)); #else - zexpect_equal(write(STDOUT_FILENO, "x", 1), 0); + zexpect_equal(write(STDOUT_FILENO, msg, ARRAY_SIZE(msg)), 0); #endif } diff --git a/tests/posix/device_io/testcase.yaml b/tests/posix/device_io/testcase.yaml index 546186774ed2b..4601ba8038421 100644 --- a/tests/posix/device_io/testcase.yaml +++ b/tests/posix/device_io/testcase.yaml @@ -12,6 +12,10 @@ tests: portability.posix.device_io: extra_configs: - CONFIG_COMMON_LIBC_MALLOC_ARENA_SIZE=256 + portability.posix.device_io.tty_stdio: + filter: CONFIG_UART_CONSOLE + extra_configs: + - CONFIG_CONSOLE_SUBSYS=y portability.posix.device_io.armclang_std_libc: toolchain_allow: armclang extra_configs: