If you use TinyCC in its 32-bit mode (-m32
) to compile a sample program that uses any CRT function/symbol from msvcrt.dll (such as the snippet provided later in this question), you'll be met with compilation failures:
tcc.exe -std=c11 -Wall -Werror -Wl,-subsystem=console -m32 .\main.c
"tcc: error: undefined symbol '_iob', missing __declspec(dllimport)?"
(This only happens under -m32
, whereas -m64
works perfectly fine.)
_iob
is not the only "unresolved" symbol, either; printf
, freopen
, freopen_s
, and basically everything from the CRT will fail to link.
Regardless of whether or not you use -lmsvcrt
, #pragma comment(lib, "msvcrt")
, _declspec(dllimport)
, attribute ((dllimport))
, -static
or -shared
, or even -impdef
on C:\Windows\SysWow64\msvcrt.dll (or earlier versions thereof: msvcrt40.dll), TCC *still *complains.
I've verified with DUMPBIN.exe
that both 32- and 64-bit msvcrt.dlls do, in fact, define _iob
and other symbols.
By some arcane logic, the following works perfectly fine: tcc.exe -std=c11 -Wall -Werror -Wl,-subsystem=console -m64 .\main.c
main.c
//#pragma comment(lib, "msvcrt") //__attribute__((dllimport)) extern __declspec(dllimport) FILE _iob[]; #include <windows.h> // _MSVCRT_ being defined will cause MinGW's stdio.h to use _iob as // opposed to _imp___iob; only the former is defined in msvcrt.dll. // However, even though _iob is exported by both the 32- and 64-bit // versions of said dll, TinyCC still fails to find _iob in the former. #define _MSVCRT_ #include <stdio.h> void main() { // AllocConsole() and basically everything from kernel32.dll or // user32.dll work perfectly fine, both in -m32 and -m64; it's // only msvcrt.dll that causes issues with TinyCC. AllocConsole(); // Any CRT function (e.g., freopen, freopen_s, printf, etc.) // fail to get linked properly ONLY in -m32; -m64 is fine. // Even if I change the -I and -L paths to C:/Windows/SysWow64 // and/or use tcc.exe -impdef to create .def files from them, // TCC still fails in finding _iob and other symbols. // Also, using #pragma comment(lib, "msvcrt") or -lmsvcrt // doesn't help at all. Even if you do get TCC to somehow // stop complaining about missing symbols, it'd just include // a blank IAT.printf or IAT.freopen, causing segfaults. freopen("CONOUT$", "w", stdout); printf("This only compiles (and prints) under TCC in 64-bit mode."); }
As mentioned earlier, this error in -m32
happens regardless of other switches like -std
, -shared
, -static
, -lmsvcrt
, -subsyetem
, etc. So, at this point, I'm starting to think this might really be a bug with TinyCC 0.9.27 (Win32 & Win64 builds) itself.
After nearly 16 hours of debugging, I've found the culprit: _iob
and _imp___iob
should have been declared with either __attribute__((dllimport))
or __declspec(dllimport)
.
To be more specific, lines 93 to 106 of <stdio.h> in TCC's ./include directory are currently written as follows:
<stdio.h>:93-106
#ifndef _STDIO_DEFINED # ifdef _WIN64 _CRTIMP FILE *__cdecl __iob_func(void); # else # ifdef _MSVCRT_ extern FILE _iob[]; /* A pointer to an array of FILE */ # define __iob_func() (_iob) # else extern FILE (*_imp___iob)[]; /* A pointer to an array of FILE */ # define __iob_func() (*_imp___iob) # define _iob __iob_func() # endif # endif #endif
To fix them so that it compiles under -m32
just as fine as it does
under -m64
, you need to change them to the following:
<stdio.h>:93-106
#ifndef _STDIO_DEFINED # ifdef _WIN64 _CRTIMP FILE *__cdecl __iob_func(void); # else # ifdef _MSVCRT_ __attribute__((dllimport)) extern FILE _iob[]; # define __iob_func() (_iob) # else __attribute__((dllimport)) extern FILE (*_imp___iob)[]; # define __iob_func() (*_imp___iob) # define _iob __iob_func() # endif # endif #endif
(NOTE: As I said earlier, both __attribute__((dllimport))
and
__declspec(dllimport)
work here. Either of them fix it for both
_MSVCRT_
and non-_MSVCRT_
headers.)
A few additional details on why this fix is even useful in the first place:
freopen()
is optional in -subsystem=console
(i.e., CLI) but
absolutely required if you compile in -subsystem=windows
(i.e., GUI)
mode; if freopen()
isn't called in the latter, printf()
and other stream
outputs don't show up on the allocated console;freopen()
depends on the value of stdout
, and stdout
is
defined based on either _iob
or _imp___iob
in <stdio.h>;_imp___iob
is too old and no longer exists in newer msvcrt.dll or ucrtbase.dll, making _iob
(and therefore defining _MSVCRT_
) the go-to choice; and#define
'ing _MSVCRT_
before <windows.h> is akin to not defining it at all; I think <windows.h> undefines it somewhere, internally.I still find it strange that such a "fix" was required, because TCC works just fine under -m64
anyway. I'm not yet sure if this is a TCC-specific patch or if it actually affects any compiler that uses <stdio.h> from MinGW; I'll follow up the rest in TinyCC's mailing lists.
(Also, I did compile the latest mob branch from 10/24/2024 using build tcc.bat -x -c cl
but this <stdio.h> bug affected it the same way it affected 0.9.27.)