cgitgitlabexternunresolved-external

Multiple definitions errors showed up in project history in 2020. Now no commit before that point can be built without adding "extern"


A project I've been working on since 2000 compiled fine with brief periods of problems, maybe two or three commits at a time and very rare at that. Back in February 2020, builds failed like this:

/usr/bin/ld: src/dumb/frotz_dumb.a(dinit.o):/home/foobar/src/dumb/dinit.c:26: multiple definition of `f_setup'; src/common/frotz_common.a(object.o):/home/foobar/src/common/object.c:23: first defined here

It was easily fixed by changing instances of f_setup_t f_setup; to extern f_setup_t f_setup; in the source files where the f_setup structure was accessed. Simple, right? But then I go back to 2011, when I finally started using Git for this project. The compile fails pretty much the same way:

gcc -O2 -DCONFIG_DIR="\"/usr/local/etc\""  -DVERSION="\"2.43\"" -DSOUND_DEV="\"\"" -DCOLOR_SUPPORT      -o src/dumb/dumb_input.o -c src/dumb/dumb_input.c
src/dumb/dumb_input.c:82:13: error: conflicting types for ‘getline’
   82 | static void getline(char *s)

I set aside my frustrations with this in the past two years because, well, it builds and works fine now. But now I'm working on an issue someone submitted along with two build logs: one from the latest commit and one from a commit before the easy fix I described above was committed. So I tried a build on these older commits using a new user, then a fresh Debian machine, then a fresh FreeBSD machine. I don't have a macOS machine, which is what the issue submitter is using. This problem severely impacts my ability to do a bisect to root out trouble.

I really really REALLY don't want to insert fixes here and there starting in 2011 thereby making a mess of existing commit hashes. My only guess as to what happened is some sort of default changed in GCC around 2020 for Debian. So... what the heck is going on?

The project I'm talking about is here: https://gitlab.com/DavidGriffith/frotz/. The simple fix happened at https://gitlab.com/DavidGriffith/frotz/-/commit/b001e3f64a0f223136babb228f30fcac7fe09804


Solution

  • The C standard does not define the behavior of using f_setup_t f_setup; in multiple translation units. This form of declaration is called a tentative definition, although it is not actually a definition. However, it causes a definition to be created if no definition is seen before the end of a translation unit.

    Due to the history of C development, this form of declaration was handled differently by different C implementations. It was a documented Unix behavior that definitions created by tentative definitions created “common” symbols were coalesced during linking (multiple instances of the same “common” symbol would be linked into references to a single entity). So uses of these declarations in header files would work without causing linker errors. In other C implementations, tentative definitions would create hard references that would cause linker errors about multiple definitions. Because different C implementations treated them differently, the C standard did not require one behavior or another; it left it undefined.

    Prior to GCC version 10, GCC marked tentative definitions as “common” symbols by default. The default changed in GCC version 10.

    To remedy the issue, you can use a GCC version earlier than version 10, you can add -fcommon to your compile commands to select the old behavior, or you can add extern to the declarations to make them into ordinary declarations that are not definitions instead of tentative definitions. In the latter case, you will need to ensure there is exactly one definition of the identifier somewhere in the program.