1550 字
8 分钟
build-way
2026-05-09
无标签
中文版

Help me evaluate whether it’s feasible to build Windows without the CRT — just roughly how hard it would be. No need to read through everything in detail.

● Based on my analysis of the LuaJIT source code, here is the summary:

Feasibility: theoretically possible, but an enormous amount of work.

ModuleDifficultyNotes
memcpy/memset/memmove/memcmpEasyJust replace with intrinsics or write a loop yourself
strlen/strcmp/strchr string functionsMediumMany call sites, but simple to implement — just grunt work
malloc/realloc/freeEasyOnly used in lib_aux.c; just wrap HeapAlloc
exit/abort/signalsEasyReplace with ExitProcess
clock/time/localtime/setlocaleMediumWin32 APIs can substitute
File I/O (fopen/fread/fwrite/fprintf/fgets/tmpfile etc.)Very hardlib_io.c, lib_os.c, lib_package.c, luajit.c heavily depend on stdio; all of it needs to be rewritten to use CreateFile/ReadFile/WriteFile etc.
Math library (pow/sin/cos/tan/log/exp/sqrt etc.)Very hardlib_math.c + lj_vmmath.c use the full set of math functions; you need to implement the numerical algorithms yourself (or quietly link ucrtbase.dll)

Two tough nuts to crack

  1. Math library — on x64, sqrt can use the SSE sqrtsd instruction, but pow, log, exp, sin, cos, tan, asin, etc. have no CPU instructions and require several thousand lines of numerical algorithm implementation
  2. File I/O — these four files cover all Lua file operations (reading files, writing files, pipes, temp files, error output) and need a comprehensive rewrite
ModuleDifficultyNotes
memcpy/memset/memmove/memcmpEasyJust replace with intrinsics or write a loop yourself
strlen/strcmp/strchr string functionsMediumMany call sites, but simple to implement — just grunt work
malloc/realloc/freeEasyOnly used in lib_aux.c; just wrap HeapAlloc
exit/abort/signalsEasyReplace with ExitProcess
clock/time/localtime/setlocaleMediumWin32 APIs can substitute
File I/O (fopen/fread/fwrite/fprintf/fgets/tmpfile etc.)Very hardlib_io.c, lib_os.c, lib_package.c, luajit.c heavily depend on stdio; all of it needs to be rewritten to use CreateFile/ReadFile/WriteFile etc.
Math library (pow/sin/cos/tan/log/exp/sqrt etc.)Very hardlib_math.c + lj_vmmath.c use the full set of math functions; you need to implement the numerical algorithms yourself (or quietly link ucrtbase.dll)

Two tough nuts to crack

  1. Math library — on x64, sqrt can use the SSE sqrtsd instruction, but pow, log, exp, sin, cos, tan, asin, etc. have no CPU instructions and require several thousand lines of numerical algorithm implementation
  2. File I/O — these four files cover all Lua file operations (reading files, writing files, pipes, temp files, error output) and need a comprehensive rewrite

Recommendation

Your /MT build already statically links the CRT, so the binary no longer depends on the external msvcrt.dll, and both size and compatibility have improved significantly. If you insist on going fully noCRT, the amount of work is equivalent to rewriting LuaJIT’s low-level I/O layer plus embedding a mini math library — and the cost of maintaining upstream upgrades afterwards would be extremely high.

A more pragmatic approach: /MT plus link-time trimming (removing unneeded CRT code) can basically achieve a “zero DLL dependency” result without rewriting half of LuaJIT.

Want me to go into detail about what other optimizations can be done on top of /MT?

✻ Cogitated for 31s

❯ I don’t quite understand something. I’ve already set MT mode, so why does the generated lib still need to link the CRT? Does MT mode only apply to exe?

● You’re right to notice this — it’s an important question. Let me explain clearly:

What /MT actually does The only difference between /MT and /MD is where the CRT code comes from:

  • /MD (dynamic): the .obj records “find these functions in MSVCRT.DLL”, and the exe/dll loads MSVCRT.dll at startup
  • /MT (static): the .obj records “find these functions in libcmt.lib”, and at link time the linker copies the CRT machine code into the exe/dll

In both modes, the CRT symbols referenced by the .obj are exactly the same — memcpy, printf, strlen, etc., not one of them is missing. The only difference is where the implementation code for those functions ultimately comes from.

So what’s the deal with the .lib?

A .lib (static library) is essentially a bundle of .obj files. Each .obj inside it contains:

memcpy → undefined symbol (must be provided externally) printf → undefined symbol (must be provided externally) strlen → undefined symbol (must be provided externally) …

The .lib itself is not a final executable; it doesn’t link anything. Only when you link the .lib into your project does the linker resolve those symbols.

So your luajit_static.lib isn’t “still needing to link the CRT” — the .obj files lying inside it merely record “I need memcpy, printf…”, and those records are there whether you use /MD or /MT.

The key difference is at the executable level

Build modeDoes the EXE/DLL need MSVCRT.dll at runtime?Does the EXE/DLL contain CRT code?
/MDYes (loads MSVCRT.dll at runtime)Yes (but only loaded, not embedded)
/MTNoYes (embedded in the binary)
True noCRTNoNo (but you must implement all CRT functions yourself)

Analogy

  • /MD: you say “I want a hammer” and borrow one from your neighbor (MSVCRT.dll)
  • /MT: you say “I want a hammer”, take one from your toolbox (libcmt.lib) and carry it in your pocket
  • noCRT: you don’t need a hammer, but when you hit a nail you have to push it in with your fingers — that is, you have to implement pow, sin, fopen… yourself

This is what I said last round — /MT already solves the “zero DLL dependency” problem, but to truly have “no CRT functions in the code” (noCRT), you need to rewrite a lot of code. /MT delivers about 80% of the benefit (no external dependencies) for 20% of the work; true noCRT delivers the remaining 20% of the benefit for 80% of the work.

Question 1: The impact of /MT vs /MD on static libraries

Your intuition is wrong. /MT vs /MD definitely has an impact on .lib.

The difference is in how symbol references are recorded in the .obj file:

Compile optionHow printf is calledUnresolved symbol in .obj
/MTcall printfprintf (ordinary symbol)
/MDcall __imp_printf__imp_printf (DLL import symbol)

Both are unresolved — a static library doesn’t resolve symbols; it just bundles .obj files. The difference appears at final link time:

  • /MT’s printf → the linker finds the implementation in libcmt.lib (static CRT) → embedded in the binary
  • /MD’s __imp_printf → the linker finds the thunk in msvcrt.lib (import library) → points to msvcrt.dll

Your lua51.lib is compiled with /MT

Evidence from build.zig lines 281-283:

_ = msvc_stubs.add("libLIBCMT.a", "!<arch>\n"); // 空壳,消音 /DEFAULTLIB:LIBCMT
_ = msvc_stubs.add("libOLDNAMES.a", "!<arch>\n"); // 空壳,消音 /DEFAULTLIB:OLDNAMES

The /DEFAULTLIB directive is a feature unique to .obj files compiled with /MT (/MD would produce /DEFAULTLIB). So lua51.lib is compiled with /MT.


So, was Zig’s no-CRT effort in vain?

Not in vain, but not a complete success either. Let me trace the entire symbol chain:

lua51.lib (/MT 编译)
├─ 提供: luaL_newstate, vma_pcall, vma_close, ...
├─ 未解析: malloc, free, printf, ... ← CRT 函数
├─ 未解析: __security_cookie, ... ← /GS 栈保护
└─ /DEFAULTLIB:LIBCMT 指令 ← 被空壳 libLIBCMT.a 消音
↓ addObjectFile
zafkiel-client.lib (link_libc = false, Windows)
├─ Zig 代码: HeapAlloc/HeapFree(无 CRT) ← ✓ 你的 no-CRT 成果
├─ 提供: __security_cookie, __security_check_cookie, ... ← Zig stubs
├─ 未提供: malloc, free, printf, ... ← 这些仍悬空!
└─ lua51.lib 的 .obj 原样传播,未解析符号带着走
↓ MSVC 链接器 (C DLL 项目)
最终 C DLL
├─ __security_cookie → Zig stub 解决 ✓
├─ malloc/free/printf → 谁来提供???
└─ 取决于 C DLL 项目的编译选项

The key is the last step. Your Zig no-CRT work removed the CRT references generated by Zig itself (std.c.malloc → HeapAlloc), but lua51.lib’s CRT references pass through the Zig static library untouched, ending up on the linker of the final C DLL.


Three options for the final C DLL

Option A: compile the C DLL with /MT

malloc/free/printf → libcmt.lib → embedded in the DLL

  • No dependency on an external CRT DLL (the CRT code lives inside the DLL)
  • Adds ~100KB to the size
  • This is the “compromised no-CRT” — no external dependency, but the CRT code exists statically

Option B: compile the C DLL with /MD

malloc/free/printf → msvcrt.lib → imports vcruntime140.dll / ucrtbase.dll

  • Crashes at startup (when the Windows loader resolves the import table during LoadLibrary, it fails and returns ERROR_MOD_NOT_FOUND)
  • No code ever executes, not even DllMain

Option C: compile the C DLL with /NODEFAULTLIB and link no CRT at all

malloc/free/printf → no provider → the linker errors with unresolved external symbol

  • It won’t even build; the link phase fails outright

The actual situation

The fact that you can currently build and run means the C DLL project uses Option A or B:

  • If A: the target machine doesn’t need the CRT DLL, but static CRT code is inside the DLL → you’ve essentially achieved “no external CRT dependency”
  • If B: the target machine needs the CRT DLL, but your test machine happens to have VS Redist installed → the dependency is masked

You can verify the final DLL’s CRT dependency with this command:

dumpbin /imports your_final.dll | grep -i "vcruntime\|msvcrt\|ucrtbase"

If the output is empty → Option A, truly no CRT dependency. If there are entries → Option B, switch to /MT.


Summary

  1. /MT vs /MD definitely has an impact on static libraries — it affects the symbol naming in the .obj (printf vs __imp_printf), not “whether code is embedded or not”.
  2. The no-CRT work on the Zig side is not wasted — it ensures Zig’s own code doesn’t introduce new CRT dependencies. But lua51.lib’s CRT dependencies are a separate problem that must be absorbed by compiling the C DLL with /MT.
  3. If you want truly zero CRT code (not even statically embedded), you need to recompile LuaJIT and replace its calls to malloc/free/printf — just like you did for the Zig code. LuaJIT’s CRT usage is actually quite limited (only initial allocation and error output), so the workload isn’t large.

Compiling lib with MT/MD#

When compiling a lib, the choice of MT/MD still matters.

build-way
https://tski.uk/blog/en/build-way/
作者
Tokisaki Galaxy
发布于
2026-05-09
许可协议
CC BY