[RFC] Implementation of stdio on baremetal

Thanks everyone for your input. I’d like to progress the discussion.

For purposes of this discussion, let’s call each level of support a tier. Based on your comments, I categorise each tier as follows:

  • Tier 1: the way it is now. Baremetal only provides functions such as printf and scanf.
  • Tier 2: provides all functionality in stdio.h, but only for stdin, stdout and stderr.
  • Tier 3: provides all functionality in stdio.h for general FILE*.

I had discussions with some colleagues at Arm (thanks in particular to @smithp35 and @statham-arm) and we gathered some thoughts about the option of handling it all via linker semantics:

  1. lldwill search libraries in the specified search path order, picking the first object that defines an unresolved non-weak reference. The order of symbol references is determined by the order of input objects so it’s not really under control from the library authors’ perspective.
  2. Having the linker choose a tier based on just symbol references is difficult. If the tiers had no intersection of symbol names between them, it would be doable, as the linker could pull in more than one tier with no risk of a multiple definition error. However, as soon as alternative implementations of the same symbol can exist, this doesn’t work anymore.
  3. Assuming that each tier is a superset of the one above, we could have one separate stdio library for each tier, then delegate the choice to the user by the use of -lstdio-{tier_identifier}. This can be prone to user error if they specify a tier that isn’t enough to satisfy all symbol references used.
  4. One might enable alternative implementations by the use of weak definitions. By that, the most basic tier could have all its public symbols defined as weak, and leave the most complete tier to define its own symbols as strong. Then, the two tiers would be fed to the linker, and in case that advanced functionality is required, both tiers would be pulled in by the linker and the symbols of the most complete one would override the weak ones. Unfortunately this approach has a couple of shortcomings: (a) it can’t be done with more than two tiers, since symbols can only be either weak or strong (that is, there are only two states); (b) the mixture of object files from different tiers can happen inadvertently and might lead to undesired behaviour (some functions will come from weak definitions and others from strong definitions).
  5. Finally, we could also only support the building of a single stdio tier at a time, having as default perhaps the current implementation (called tier 1 in this made up nomenclature). Toolchain providers can switch to another tier at library build time if so they wish. One obvious weakness is that a configuration flag is required in the libc’s build system to select which tier to build.

Expanding on (3), for toolchain providers, it’s possible to leverage clang’s multilib selection mechanism. We already have the multilib custom flags feature I’ve implemented some time ago ( Multilib — Clang 22.0.0git documentation ). We could have each stdio variant in its own static library, and clang’s driver can choose which one to pick up based on the command-line:

  • clang .. -fmultilib-flag=stdio-minimal
  • clang .. -fmultilib-flag=stdio-stdstreams
  • clang .. -fmultilib-flag=stdio-full

In my opinion, the best solution is either (3) or (5). I’d be happy to hear your thoughts in order to make progress.

1 Like