Skip to content

Add console:print_err/1 and route io standard_error to stderr#2346

Open
pguyot wants to merge 1 commit into
atomvm:release-0.7from
pguyot:w25/console-print-err
Open

Add console:print_err/1 and route io standard_error to stderr#2346
pguyot wants to merge 1 commit into
atomvm:release-0.7from
pguyot:w25/console-print-err

Conversation

@pguyot

@pguyot pguyot commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

These changes are made under both the "Apache 2.0" and the "GNU Lesser General
Public License 2.1 or later" license terms (dual license).

SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later

@petermm

petermm commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

pick and choose, number 3 seems interesting..

PR Review — Add console:print_err/1 and route io standard_error to stderr

Commit: f671b6dAdd console:print_err/1 and route io standard_error to stderr
Author: Paul Guyot

Summary

Adds a console:print_err/1 NIF that writes to stderr, and changes
io:put_chars(standard_error, ...) (and therefore io:format/3 /
io:fwrite/3 targeting standard_error) to route to stderr instead of being
aliased to standard_io. The C-level nif_console_print body is refactored
into a shared console_print_to(FILE *stream, ...) helper reused by both the
stdout and stderr NIFs.

Files touched: src/libAtomVM/nifs.c, src/libAtomVM/nifs.gperf,
libs/eavmlib/src/console.erl, libs/estdlib/src/io.erl, CHANGELOG.md.

Verdict

Approve with suggestions — no blockers.

  • The ctx / RAISE_ERROR refactor is safe: the macro references an implicit
    ctx, and console_print_to now takes a local ctx parameter that resolves
    correctly.
  • console:print_err/1 is exported, stubbed with erlang:nif_error(undefined),
    registered in nifs.gperf, and wired to console_print_err_nif.
  • io:format(standard_error, ...) and io:fwrite(standard_error, ...) both
    funnel through put_chars(standard_error, Msg)console:print_err/1, so the
    fix is complete for the documented paths.
  • Bypassing group_leader() for standard_error is the correct OTP-like choice
    (see finding 2).

Findings (ranked)

1. Suggestion — add stdout/stderr separation test coverage

No test was added. This is exactly the kind of change that regresses silently
(standard_error used to land on stdout). Add a black-box test that redirects
stdout and stderr separately and asserts the split, exercising put_chars/2,
format/3, and fwrite/3 in one shot:

start() ->
    io:put_chars("out1\n"),
    io:put_chars(standard_error, "err1\n"),
    io:format(standard_error, "err~p~n", [2]),
    io:fwrite(standard_error, "err~p~n", [3]),
    ok.

Expected:

stdout: out1\n
stderr: err1\nerr2\nerr3\n

2. Suggestion — document the intentional group-leader semantic change

Old behavior routed standard_error through standard_io, which — when the
group leader was not self() — dispatched an {io_request, ...} to the leader:

put_chars(standard_error, Chars) ->
    put_chars(standard_io, Chars).   % went through group leader

New behavior always goes straight to the OS error stream:

put_chars(standard_error, Chars) ->
    console:print_err(Chars).        % bypasses group leader

This matches OTP semantics (standard_io is the group-leader-backed default
device; standard_error is the OS error device). Do not route stderr
through execute_request/2 or the console io_request protocol.

The only regression risk is AtomVM-specific code/tests that previously captured
standard_error by swapping the group leader — that relied on the old aliasing.
Such callers should pass an explicit pid/device instead of standard_error.
Worth a one-line note in UPDATING.md / release notes.

3. Suggestion — prefer fwrite over fprintf("%.*s", ...) in the helper

The refactored helper preserves the pre-existing fprintf(stream, "%.*s", (int) n, data)
pattern. Now that it also backs the new stderr NIF, it is worth fixing two
latent correctness issues that affect both stdout and stderr:

  • "%.*s" stops at embedded NUL bytes (binaries/iodata can legitimately contain
    0x00).
  • the (int) cast on the size truncates very large binaries/iolists.

Adding a size == 0 guard also avoids treating malloc(0) == NULL as OOM on
some embedded libc implementations.

 static term console_print_to(FILE *stream, Context *ctx, term argv[])
 {
     term t = argv[0];
     if (term_is_binary(t)) {
         const char *data = term_binary_data(t);
-        unsigned long n = term_binary_size(t);
-        fprintf(stream, "%.*s", (int) n, data);
-        fflush(stream);
+        size_t n = term_binary_size(t);
+        fwrite(data, 1, n, stream);
+        fflush(stream);
     } else {
         size_t size;
         switch (interop_iolist_size(t, &size)) {
             case InteropOk:
                 break;
             case InteropMemoryAllocFail:
                 RAISE_ERROR(OUT_OF_MEMORY_ATOM);
             case InteropBadArg:
                 RAISE_ERROR(BADARG_ATOM);
         }
+        if (size == 0) {
+            fflush(stream);
+            return OK_ATOM;
+        }
         char *buf = malloc(size);
         if (IS_NULL_PTR(buf)) {
             RAISE_ERROR(OUT_OF_MEMORY_ATOM);
         }
         switch (interop_write_iolist(t, buf)) {
             case InteropOk:
                 break;
             case InteropMemoryAllocFail:
                 free(buf);
                 RAISE_ERROR(OUT_OF_MEMORY_ATOM);
             case InteropBadArg:
                 free(buf);
                 RAISE_ERROR(BADARG_ATOM);
         }
-        fprintf(stream, "%.*s", (int) size, buf);
-        fflush(stream);
+        fwrite(buf, 1, size, stream);
+        fflush(stream);
         free(buf);
     }
     return OK_ATOM;
 }

Note: this is a pre-existing behavior carried over by the refactor, not
introduced by this commit — so it is optional, but a natural place to fix.

4. Nit — don't over-promise a separate physical stderr on embedded targets

fprintf(stderr, ...) is already used elsewhere in the shared nifs.c, so no
new platform/link dependency is introduced. On embedded targets, however,
stderr may be wired to the same UART/backend as stdout. The public doc/promise
should read something like "writes to C/OS standard error where the platform
distinguishes it," not "guaranteed separate physical stream on every target."

The console:print_err/1 docstring also advertises {error, Reason}, but the
NIF only ever returns ok or raises — this mirrors the existing print/1
docstring, so it is consistent, just slightly inaccurate.

@pguyot pguyot force-pushed the w25/console-print-err branch from f671b6d to 996f805 Compare July 3, 2026 05:40
Signed-off-by: Paul Guyot <pguyot@kallisys.net>
@pguyot pguyot force-pushed the w25/console-print-err branch from 996f805 to 57dd570 Compare July 3, 2026 21:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants