upvote
File descriptors are part of the linux syscall API, not libc. Are you thinking of FILE?
reply
The "syscall API" is part of libc too. The read syscall is a trap, you put arguments in the right registers and issue the correct instruction[1] to enter the kernel. That's not something that can be expressed in C. The read() function that your C code actually uses is a C function provided by the C library.

[1] "svc 0" on ARM, "int 0x80" on i386, etc...

reply
> That's not something that can be expressed in C.

I've often made the argument that compilers should add builtins for Linux system calls. Just emit code in the right calling convention and the system call instruction, and return the result. Even high level dynamic languages could have their JIT compilers generate this code.

I actually tried to hack a linux_system_call builtin into GCC at some point. Lost that work in a hard drive crash, sadly. The maintainers didn't seem too convinced in the mailing list so I didn't bother rewriting it.

> The read() function that your C code actually uses is a C function provided by the C library.

These are just magic wrapper functions. The actual Linux system call entry point is language agnostic, specified at the instruction architecture level, and is considered stable.

https://www.matheusmoreira.com/articles/linux-system-calls

This is different from other systems which force people to use the C library to interface with the kernel.

One of the most annoying things in the Linux manuals is they conflate the glibc wrappers with the actual system calls in Linux. The C library does a lot more than just wrap these things, they dynamically choose the best variants and even implement cancellation/interruption mechanisms. Separating the Linux behavior from libc behavior can be difficult, and in my experience requires reading kernel source code.

reply
> I've often made the argument that compilers should add builtins for Linux system calls. Just emit code in the right calling convention and the system call instruction, and return the result. Even high level dynamic languages could have their JIT compilers generate this code.

You can only do that, when you compile for a specific machine. In general you are compiling for some abstract notion of an OS. JITs always compile for the machine they are running on, so they don't have that problem. There is code, that is compiled directly to your syscalls specific to your machine, so that abstract code can use this. It's called libc for the C language.

> One of the most annoying things in the Linux manuals is they conflate the glibc wrappers with the actual system calls in Linux. The C library does a lot more than just wrap these things, they dynamically choose the best variants and even implement cancellation/interruption mechanisms. Separating the Linux behavior from libc behavior can be difficult, and in my experience requires reading kernel source code.

In my experience there are often detailed explanation in the notes section. From readv(2):

  NOTES
       POSIX.1  allows  an  implementation  to  place a limit on the number of
       items that can be passed in iov.  An implementation can  advertise  its
       limit  by  defining IOV_MAX in <limits.h> or at run time via the return
       value from sysconf(_SC_IOV_MAX).  On modern Linux systems, the limit is
       1024.  Back in Linux 2.0 days, this limit was 16.

   C library/kernel differences
       The  raw  preadv() and pwritev() system calls have call signatures that
       differ slightly from that of the corresponding GNU  C  library  wrapper
       functions  shown  in  the SYNOPSIS.  The final argument, offset, is un‐
       packed by the wrapper functions into two arguments in the system calls:

           unsigned long pos_l, unsigned long pos

       These arguments contain, respectively, the low order and high order  32
       bits of offset.

   Historical C library/kernel differences
       To  deal  with  the  fact  that IOV_MAX was so low on early versions of
       Linux, the glibc wrapper functions for readv() and  writev()  did  some
       extra  work  if  they  detected  that the underlying kernel system call
       failed because this limit was exceeded.  In the case  of  readv(),  the
       wrapper  function  allocated a temporary buffer large enough for all of
       the items specified by iov, passed that buffer in a  call  to  read(2),
       copied  data from the buffer to the locations specified by the iov_base
       fields of the elements of iov, and then freed the buffer.  The  wrapper
       function  for  writev()  performed the analogous task using a temporary
       buffer and a call to write(2).

       The need for this extra effort in the glibc wrapper functions went away
       with Linux 2.2 and later.  However, glibc continued to provide this be‐
       havior until version 2.10.  Starting with glibc version 2.9, the  wrap‐
       per  functions  provide  this behavior only if the library detects that
       the system is running a Linux kernel older than version 2.6.18 (an  ar‐
       bitrarily  selected  kernel  version).  And since glibc 2.20 (which re‐
       quires a minimum Linux kernel version of  2.6.32),  the  glibc  wrapper
       functions always just directly invoke the system calls.
reply
> You can only do that, when you compile for a specific machine.

You always compile for a specific machine. There is always a target instruction set architecture. It decides the calling convention used for Linux system calls. Compiler can even produce an error in case the target is not supported by Linux.

> In general you are compiling for some abstract notion of an OS.

This "abstract notion of an OS" boils down to the libc. Freestanding C gets rid of most of it. Making system calls is also perfectly valid in hosted C. Modern languages like Rust also have freestanding modes.

> In my experience there are often detailed explanation in the notes section.

That's the problem. Why is the Linux stuff just a bunch of footnotes in the Linux manual? It should be in the main section. The glibc specifics should be footnotes.

reply
Specific machine meaning defined set of installed software, versions in install locations.

Abstract notion of OS meaning Debian 12. Not Linux kernel commit ####, GNU libc commit ####, dpkg commit ####, apt commit ####, Apache httpd commit #### with patch ### to ### from Debian 4 version ### and Ubuntu 21 version ###, SQLite3 with special patches ### installed in /opt/bin/foo, ... (you get the idea).

> That's the problem. Why is the Linux stuff just a bunch of footnotes in the Linux manual? It should be in the main section. The glibc specifics should be footnotes.

Because you look at the OS manual, not at the documentation of the kernel. Notes and Bugs are also not footnotes in man pages. They are pretty important and are basically the first free-form section where you can tell about the ideas, ideals and history. The first part a pretty strict, formal description of the calling semantics.

reply
Let's systematize this.

Compilers build for target triples such as x86_64-linux-gnu. It is of the form isa-kernel-userspace. If kernel is linux, the builtin can be used. The isa determines the code generated by the compiler, both in general and for the builtin. The userspace can be anything at all, including none. Sometimes compilers build for target quadruples which also include a vendor, and that information is also irrelevant.

reply
I am not sure you understand my point. Inlining libc definitions for syscalls is fine when you only care about Debian 12 commit hash ####. It will break as soon as you think your machine is running Debian 12 and you updated it, so surely it includes the latest userspace-patches. It will also break when a user uses the OS configuration to change the behaviour of some OS functionality, but your code is oblivious to that matter, because your code bypasses the OS version of libc.

Modifying the OS is fine, if this is what you want to do, but it comes with tradeoffs.

----

You wrote earlier:

> actually tried to hack a linux_system_call builtin into GCC at some point. [...] The maintainers didn't seem too convinced in the mailing list so I didn't bother rewriting it.

I am not sure what exactly this means. There is syscall(2) in the libc, if you want to do this. If you want to inline the wrappers you can pass -static to the compiler invocation.

reply
> It will break

If it ever breaks, it's a bug in the Linux kernel.

> It will also break when a user uses the OS configuration to change the behaviour of some OS functionality

Can you give concrete examples of this?

> There is syscall(2) in the libc, if you want to do this.

I know. I've written my own syscall(), as well. The idea is to put it in the compiler as a builtin so there's no need to even write it.

reply
> If it ever breaks, it's a bug in the Linux kernel.

No, your program will still instruct the kernel to do the same. It will just cause conflicts with the other OS internals.

> Can you give concrete examples of this?

Adding another encoding as a gconv module. The DNS issues everyone is talking about.

I don't know what that gets you compared to using syscall(2) and -static. When you want your program to depend on the kernel API instead of the OS API, then you should really link libc statically.

reply
> It will just cause conflicts with the other OS internals.

But not with the kernel.

"Other OS internals" are just replaceable components. The idea is to depend on Linux only, not on Linux+glibc.

> Adding another encoding as a gconv module. The DNS issues everyone is talking about.

Those are glibc problems, not Linux problems. Linux does not perform name resolution or character encoding conversion.

reply
The libc syscall wrappers are part of the libc API, but on Linux, syscalls are part of the stable ABI and so you can freely do __asm__(...) to write your own version of syscall(2) and it is fully supported. Yeah, __asm__ is probably not in the C spec, but every compiler implements it...

For instance, Go directly calls Linux system calls without going through libc (which has lead to lots of workarounds to emulate some glibc-specific behaviour -- swings and roundabouts I guess...).

Other operating systems do not provide this kind of compatibility guarantee and instead require you to always go through libc as the syscall ABI is not stable (though ultimately, you can still use __asm__ if you so choose).

In any case, file descriptors are definitely not a libc construct on Linux.

reply
Yes, you can. Then you don't write against the OS, but against the kernel. It sometimes works, because the kernel is a separate project, it sometimes doesn't, you gave an example yourself.

> In any case, file descriptors are definitely not a libc construct on Linux.

File descriptors come definitely from the kernel, but they do also exist as a concept in libc, and I was referring to them as such. I was saying that I depend on non-portable libc functions, even though I value portability, because the API is just so nice. I did not want to indicate, that I am doing syscalls directly.

reply
syscalls are an implementation detail of some libc impls on some platforms, but the C spec does not mention syscalls.
reply
I did mean file descriptors.
reply
Then I'm confused by what you meant, because you can use fds with or without libc.
reply
I don't want to bypass libc in general, because I care about portability, but fds are just a nice interface, so I still use them instead of FILE, which would be the portable choice. My calls are still subject to OS choices, that differ from the kernel, since I don't bypass libc.
reply