Skip to content

libc: minimal: stdin: Add getc() implementation and unit tests #93062

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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 include/zephyr/sys/libc-hooks.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ __syscall int zephyr_write_stdout(const void *buf, int nbytes);

__syscall int zephyr_fputc(int c, FILE * stream);

__syscall int zephyr_fgetc(FILE *stream);

#ifdef CONFIG_MINIMAL_LIBC
/* Minimal libc only */

Expand Down
1 change: 1 addition & 0 deletions lib/libc/minimal/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ zephyr_library_sources(
source/string/strstr.c
source/string/string.c
source/string/strspn.c
source/stdin/stdin_console.c
source/stdout/stdout_console.c
source/stdout/sprintf.c
source/stdout/fprintf.c
Expand Down
3 changes: 3 additions & 0 deletions lib/libc/minimal/include/stdio.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ int remove(const char *path);
#define putc(c, stream) fputc(c, stream)
#define putchar(c) putc(c, stdout)

int fgetc(FILE *stream);
#define getc(stream) fgetc(stream)

#ifdef __cplusplus
}
#endif
Expand Down
151 changes: 151 additions & 0 deletions lib/libc/minimal/source/stdin/stdin_console.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/* stdin_console.c */

/*
* Copyright (c) 2025 The Zephyr Project Contributors
*
* SPDX-License-Identifier: Apache-2.0
*/

#include <stdio.h>
#include <zephyr/sys/libc-hooks.h>
#include <zephyr/internal/syscall_handler.h>
#include <string.h>

static unsigned char _stdin_hook_default(void)
{
return 0;
}

static unsigned char (*_stdin_hook)(void) = _stdin_hook_default;

void __stdin_hook_install(unsigned char (*hook)(void))
{
_stdin_hook = hook;
}

int z_impl_zephyr_fgetc(FILE *stream)
{
if (stream == stdin && _stdin_hook) {
return _stdin_hook();
}
return EOF;
}

#ifdef CONFIG_USERSPACE
static inline int z_vrfy_zephyr_fgetc(FILE *stream)
{
return z_impl_zephyr_fgetc(stream);
}
#include <zephyr/syscalls/zephyr_fgetc_mrsh.c>
#endif

int fgetc(FILE *stream)
{
return zephyr_fgetc(stream);
}

char *fgets(char *s, int size, FILE *stream)
{
if (s == NULL || size <= 0) {
return NULL;
}

int i = 0;
int c;

while (i < size - 1) {
c = fgetc(stream);
if (c == EOF) {
if (i == 0) {
return NULL;
}
break;
}
s[i++] = (char)c;
if (c == '\n') {
break;
}
}
s[i] = '\0';
return s;
}

#undef getc
int getc(FILE *stream)
{
return zephyr_fgetc(stream);
}

#undef getchar
int getchar(void)
{
return zephyr_fgetc(stdin);
}
Comment on lines +42 to +83
Copy link
Member

Choose a reason for hiding this comment

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

These should probably be moved to separate files


size_t z_impl_zephyr_fread(void *ZRESTRICT ptr, size_t size, size_t nitems, FILE *ZRESTRICT stream)
{
size_t i, j;
unsigned char *p = ptr;

if ((stream != stdin) || (nitems == 0) || (size == 0)) {
return 0;
}

i = nitems;
do {
j = size;
do {
int c = fgetc(stream);
if (c == EOF) {

Check warning on line 99 in lib/libc/minimal/source/stdin/stdin_console.c

View workflow job for this annotation

GitHub Actions / Run compliance checks on patch series (PR)

LINE_SPACING

lib/libc/minimal/source/stdin/stdin_console.c:99 Missing a blank line after declarations
goto done;
}
*p++ = (unsigned char)c;
j--;
} while (j > 0);

i--;
} while (i > 0);

done:
return (nitems - i);
}

#ifdef CONFIG_USERSPACE
static inline size_t z_vrfy_zephyr_fread(void *ZRESTRICT ptr, size_t size, size_t nitems,
FILE *ZRESTRICT stream)
{
K_OOPS(K_SYSCALL_MEMORY_ARRAY_WRITE(ptr, nitems, size));
return z_impl_zephyr_fread(ptr, size, nitems, stream);
}
#include <zephyr/syscalls/zephyr_fread_mrsh.c>
#endif

size_t fread(void *ZRESTRICT ptr, size_t size, size_t nitems, FILE *ZRESTRICT stream)
{
return zephyr_fread(ptr, size, nitems, stream);
}

char *gets(char *s)
Copy link
Member

Choose a reason for hiding this comment

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

This should probably be moved to a separate file.

{
if (s == NULL) {
return NULL;
}

int c;
char *p = s;

while (1) {
c = getchar();
if (c == EOF || c == '\n') {
break;
}
*p++ = (char)c;
}
*p = '\0';

// If nothing was read and EOF, return NULL

Check failure on line 146 in lib/libc/minimal/source/stdin/stdin_console.c

View workflow job for this annotation

GitHub Actions / Run compliance checks on patch series (PR)

C99_COMMENTS

lib/libc/minimal/source/stdin/stdin_console.c:146 do not use C99 // comments
if (p == s && c == EOF) {
return NULL;
}
return s;
}
8 changes: 8 additions & 0 deletions tests/lib/sscanf/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# SPDX-License-Identifier: Apache-2.0

cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(sscanf)

FILE(GLOB app_sources src/*.c)
target_sources(app PRIVATE ${app_sources})
6 changes: 6 additions & 0 deletions tests/lib/sscanf/prj.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CONFIG_ZTEST=y
CONFIG_FPU=y
CONFIG_TEST_USERSPACE=y
CONFIG_ZTEST_FATAL_HOOK=y
CONFIG_PICOLIBC_IO_FLOAT=y
CONFIG_ZTEST_STACK_SIZE=2048
72 changes: 72 additions & 0 deletions tests/lib/sscanf/src/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright (c) 2025 The Zephyr Project Contributors
*
* SPDX-License-Identifier: Apache-2.0
*
* DESCRIPTION
* This module contains the code for testing input functionality in minimal libc,
* including getc(), fgetc(), fgets(), and getchar().
*/

#include <zephyr/ztest.h>
#include <stdio.h>
#include <string.h>

static const char *test_input = "Hello\nWorld";
static int input_pos;

static int mock_stdin_hook(void)
{
if (test_input[input_pos] == '\0') {
Copy link
Member

Choose a reason for hiding this comment

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

With the suggested change, EOF is returned only after there is no input, which is maybe a bit more accurate.

It also allows you to test how the implementation reacts to processing \0 (which should be possible, afaik, since standard input or any other file can contain 0x00).

Maybe even put some additional text after the \0.

Suggested change
if (test_input[input_pos] == '\0') {
if (input_pos == ARRAY_SIZE(test_input)) {

return EOF;
}
return test_input[input_pos++];
}

void setup_stdin_hook(void)
{
Comment on lines +26 to +27
Copy link
Member

Choose a reason for hiding this comment

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

This can be a "before" callback provided to ZTEST_SUITE() and then it does not need to be called manually in each test.

Suggested change
void setup_stdin_hook(void)
{
static void before(void * arg)
{
ARG_UNUSED(arg);

input_pos = 0;
extern void __stdin_hook_install(int (*hook)(void));
__stdin_hook_install(mock_stdin_hook);
}

ZTEST(sscanf, test_getc)

Check warning on line 33 in tests/lib/sscanf/src/main.c

View workflow job for this annotation

GitHub Actions / Run compliance checks on patch series (PR)

NAKED_SSCANF

tests/lib/sscanf/src/main.c:33 unchecked sscanf return value
{
setup_stdin_hook();
int c = getc(stdin);
zassert_equal(c, 'H', "getc(stdin) did not return 'H'");

Check warning on line 37 in tests/lib/sscanf/src/main.c

View workflow job for this annotation

GitHub Actions / Run compliance checks on patch series (PR)

LINE_SPACING

tests/lib/sscanf/src/main.c:37 Missing a blank line after declarations
c = getc(stdin);
zassert_equal(c, 'e', "getc(stdin) did not return 'e'");
Comment on lines +36 to +39
Copy link
Member

Choose a reason for hiding this comment

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

Why not add a loop and check the entirety of the input?

}

ZTEST(sscanf, test_fgetc)

Check warning on line 42 in tests/lib/sscanf/src/main.c

View workflow job for this annotation

GitHub Actions / Run compliance checks on patch series (PR)

NAKED_SSCANF

tests/lib/sscanf/src/main.c:42 unchecked sscanf return value
{
setup_stdin_hook();
int c = fgetc(stdin);
zassert_equal(c, 'H', "fgetc(stdin) did not return 'H'");

Check warning on line 46 in tests/lib/sscanf/src/main.c

View workflow job for this annotation

GitHub Actions / Run compliance checks on patch series (PR)

LINE_SPACING

tests/lib/sscanf/src/main.c:46 Missing a blank line after declarations
c = fgetc(stdin);
zassert_equal(c, 'e', "fgetc(stdin) did not return 'e'");
Comment on lines +45 to +48
Copy link
Member

Choose a reason for hiding this comment

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

Why not add a loop and check the entirety of the input?

}

ZTEST(sscanf, test_getchar)

Check warning on line 51 in tests/lib/sscanf/src/main.c

View workflow job for this annotation

GitHub Actions / Run compliance checks on patch series (PR)

NAKED_SSCANF

tests/lib/sscanf/src/main.c:51 unchecked sscanf return value
{
setup_stdin_hook();
int c = getchar();
zassert_equal(c, 'H', "getchar() did not return 'H'");

Check warning on line 55 in tests/lib/sscanf/src/main.c

View workflow job for this annotation

GitHub Actions / Run compliance checks on patch series (PR)

LINE_SPACING

tests/lib/sscanf/src/main.c:55 Missing a blank line after declarations
c = getchar();
zassert_equal(c, 'e', "getchar() did not return 'e'");
Comment on lines +54 to +57
Copy link
Member

Choose a reason for hiding this comment

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

Why not add a loop and check the entirety of the input?

}

ZTEST(sscanf, test_fgets)

Check warning on line 60 in tests/lib/sscanf/src/main.c

View workflow job for this annotation

GitHub Actions / Run compliance checks on patch series (PR)

NAKED_SSCANF

tests/lib/sscanf/src/main.c:60 unchecked sscanf return value
{
setup_stdin_hook();
char buf[16];
char *ret = fgets(buf, sizeof(buf), stdin);
zassert_not_null(ret, "fgets returned NULL");

Check warning on line 65 in tests/lib/sscanf/src/main.c

View workflow job for this annotation

GitHub Actions / Run compliance checks on patch series (PR)

LINE_SPACING

tests/lib/sscanf/src/main.c:65 Missing a blank line after declarations
zassert_true(strcmp(buf, "Hello\n") == 0, "fgets did not read 'Hello\\n'");
ret = fgets(buf, sizeof(buf), stdin);
zassert_not_null(ret, "fgets returned NULL on second call");
zassert_true(strcmp(buf, "World") == 0, "fgets did not read 'World'");
}

ZTEST_SUITE(sscanf, NULL, NULL, NULL, NULL, NULL);
Copy link
Member

Choose a reason for hiding this comment

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

sscanf seems like it isn't a particularly descriptive name for a testsuite. I would maybe consider calling this standard_input or something.

Additionally, pass in the "before" function to be called automatically by the test suite like so.

Suggested change
ZTEST_SUITE(sscanf, NULL, NULL, NULL, NULL, NULL);
ZTEST_SUITE(sscanf, NULL, NULL, before, NULL, NULL);

The one thing I would consider potentially is also creating a way to back up the previous stdin hook if one does not exist, and to restore it in a "after" callback.

Loading