cosmopolitan icon indicating copy to clipboard operation
cosmopolitan copied to clipboard

Bug: printf() isn't working as expected

Open patricus3 opened this issue 1 year ago • 5 comments

Contact Details

contact me on here

What happened?

hello. found a weird issue with printf, I don't know why this happens, but if I compile my example calculator app, it doesn't print to stdout until you fill out all needed stuff, here's the C code.

#include<stdio.h>
#include<stdlib.h>
int main(){
    printf("welcome in  calculator.");
    char *choice=malloc(sizeof(char));
    printf("type in the operator");
    scanf("%c",choice);
    switch(*choice)
    {
        case '/':
            free(choice);
            printf("first number");
            int64_t *num1=malloc(sizeof(int64_t));
            int64_t *num2=malloc(sizeof(int64_t));
            printf("first number");
            scanf("%lld",num1);
            printf("second number");
            scanf("%lld",num2);
            int64_t *result=malloc(sizeof(int64_t));
            *result =*num1 / *num2;
            free(num1);
            free(num2);
            printf("%lld",*result);
            free(result);
            break;
        case '+':
            free(choice);
            int64_t *Num1=malloc(sizeof(int64_t));
            int64_t *Num2=malloc(sizeof(int64_t));
            printf("type in first number");
            scanf("%lld",Num1);
            printf("second number");
            scanf("%lld",Num2);
            int64_t *Result=malloc(sizeof(int64_t));
            *Result= *Num1+ *Num2;
            free(Num1);
            free(Num2);
            printf("%lld",*Result);
            free(Result);
            break;
        case '*':
            free(choice);
            int64_t *n1=malloc(sizeof(int64_t));
            int64_t *n2=malloc(sizeof(int64_t));
            scanf("%lld",n1);
            scanf("%lld",n2);
            int64_t *res=malloc(sizeof(int64_t));
            *res= *n1 * *n2;
            free(n1);
            free(n2);
            printf("%lld",*res);
            free(res);
            break;
        case '-':
            free(choice);
            int64_t *N1=malloc(sizeof(int64_t));
            int64_t *N2=malloc(sizeof(int64_t));
            printf("type in the first number");
            scanf("%lld",N1);
            printf("type in second number");
            scanf("%lld",N2);
            int64_t *Res=malloc(sizeof(int64_t));
            *Res= *N1- *N2;
            free(N1);
            free(N2);
            printf("%lld",*Res);
            free(Res);
            break;
    }
}

Version

cosmocc (GCC) 14.1.0

What operating system are you seeing the problem on?

Mac

Relevant log output

No response

patricus3 avatar Aug 10 '24 07:08 patricus3

cosmos seems to have stdout line-buffered, just use setbuf(stdout, NULL) or some \n in your strings.

randomtwdude avatar Aug 10 '24 09:08 randomtwdude

Why they do this? Is there any way to fix it so that will not be needed?

patricus3 avatar Aug 10 '24 10:08 patricus3

libc/stdio/stdout.c contains the reason why. You can maybe just change the mode there, but I don't know if it will work. (just adding one setbuf at the start of your code is probably easier, or you could use fflush instead)

randomtwdude avatar Aug 10 '24 16:08 randomtwdude

With glibc in ttys, stdout is also line buffered. glibc appears to flush stdout when stdin is read from. Personally, I wouldn't depend on this behavior as it doesn't appear to be guaranteed by C, but this may be something we can improve.

https://stackoverflow.com/questions/66815208/how-is-printf-getting-flushed-before-scanf-is-executed

G4Vi avatar Aug 15 '24 02:08 G4Vi

Upon further consideration, I'm not convinced we should change this. musl has good reasoning why they don't:

Rich Felker:

There's nothing detectable here because there's nothing wrong with the program; the bug is in the programmer's expectation that the output be visible. It's possible to implement the behavior the programmer here desired, the optional flushing of line-buffered output streams before reading input. This would not help detect the bug in expectaions though; it would just help mask it. The reason this behavior is not present in musl is because it does not scale with significant numbers of stdio streams open, and can even produce deadlock conditions in multithreaded programs where there is no semantic deadlock but the additional flushing produces an extraneous operation on a stream in a way that causes deadlock.

https://inbox.vuxu.org/musl/CAAMnvke+NhDbGahX5mbzOpsWEPzPf4K-0dYE-ZMHJTr5xSEGhQ@mail.gmail.com/T/

To reproduce the deadlock condition I made a demo program, save to wr_stdio.c

#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main(){
    const unsigned pipesize = fcntl(1, F_GETPIPE_SZ);
    fprintf(stderr, "pipesize %u\n", pipesize);

    // fill up the stdout pipe
    static char buf[4096];
    for(unsigned i = 0; i < (pipesize / sizeof(buf)); i++) {
        write(1, buf, sizeof(buf));
    }
    // print without flushing
    printf("write without flushing");
    fprintf(stderr, "the printf itself is harmless\n");
    getchar(); // reading should be
    fprintf(stderr, "glibc never gets here as we are deadlocked writing to stdout\n");
}

Compile and run with cosmocc.

cosmocc -o wr_stdio_cosmo wr_stdio.c && ./wr_stdio_cosmo | sleep 10000

After you press ENTER, the final output should look something like:

pipesize 65536
the printf itself is harmless

glibc never gets here as we are deadlocked writing to stdout

Now if we do the same with gcc with glibc:

gcc -o wr_stdio_gcc wr_stdio.c && stdbuf --output=L ./wr_stdio_gcc | sleep 10000
pipesize 65536
the printf itself is harmless


To easily reproduce, stdbuf --output=L forces the stdout buffering to line mode as glibc doesn't use line mode with pipes.

G4Vi avatar Aug 16 '24 03:08 G4Vi

POSIX defines stdout as buffered. You need to use fflush() if you want to print a prompt to your terminal before reading.

jart avatar Dec 27 '24 23:12 jart