How to link against static msvc libc on Windows?

Hello Zig Language Community,

I hope this message finds you well. I am reaching out to seek your expertise regarding a couple of issues I’ve encountered while working with Zig to implement a Lua C module on a Windows environment.

My project aims to create a Lua C module without linking against libc, as the Lua binary itself (lua54.dll) requires libc, and my module only needs to link to lua54.dll and use its provided interfaces. However, I’ve run into a compilation error due to the inclusion of the stdio.h header file within lua.h. The error message is as follows:

src\main.zig:2:11: error: C import failed
const c = @cImport({
          ^~~~~~~~
src\main.zig:2:11: note: libc headers not available; compilation does not link against libc
referenced by:
    zig_fx: src\main.zig:8:36
    remaining reference traces hidden; use '-freference-trace' to see all reference traces
C:\Devel\MinGW\include/lauxlib.h:13:10: error: 'stdio.h' file not found
#include <stdio.h>
         ^

To address this, I added lib.linkLibC() to build.zig, which is necessary only on the Windows platform. However, this resulted in the final DLL depending on the dynamic-link libraries provided by Windows. Typically, when working with C, I would link against the static MSVC libc library using the /MT flag, but I have not found a way to pass this flag or an equivalent to the MSVC linker through Zig.

Therefore, I am seeking advice on two fronts:

  1. Is there a way to compile Zig code that depends only on libc headers without actually linking against libc?
  2. If that’s not feasible, is there a way to link against a static version of the libc library on Windows, or perhaps use the VC6-style msvcrt.dll library? Additionally, is it possible to specify the use of MinGW’s runtime with Zig?

Any guidance or suggestions you could provide would be greatly appreciated. I am eager to learn and find a solution that allows my module to work seamlessly.

Thank you for your time and assistance.

Best regards,
Xavier Wang.

3 Likes

Hello @xavier83. Welcome to @ziggit :slight_smile:

I am on linux and tried to generate a windows executable using the latest zig from master branch (0.12.0-dev).
I found two ways:

  1. zig build -Dtarget=x86_64-windows-gnu
    This method uses the zig bundled libc (mingw) but I cannot find a way to make it link statically. It requires dynamic libraries (api-ms-win-crt-*-l1-1-0.dll) that I don’t know where to find them. (On the first run It takes some time to build because it compiles the libc from sources). This must be a bug, because the resulting binary believes that is static (@import("builtin").link_mode == std.builtin.LinkMode.static)
  2. zig build -Dtarget=x86_64-windows-msvc
    This method does not use a zig bundled libc and expects windows headers and libraries to be installed.
    I successfully created a static binary after installing crt and sdk using xwin. The linker used libmt.dll (/mt).
const std = @import("std");

pub fn build(b: *std.Build) void {
    const exe = b.addExecutable(.{
        .name = "foo",
        .root_source_file = b.path("src/main.zig"),
        .target = b.standardTargetOptions(.{}),
        .optimize = b.standardOptimizeOption(.{}),
        .linkage = .static,
        .link_libc = true,
    });
//    required only for msvc when cross compiling, after installing crt & sdk using xwin
//    exe.addIncludePath(.{ .cwd_relative = "xwin/sdk/include/ucrt" });
//    exe.addIncludePath(.{ .cwd_relative = "xwin/crt/include" });
//    exe.addLibraryPath(.{ .cwd_relative = "xwin/crt/lib/x86_64" });
//    exe.addLibraryPath(.{ .cwd_relative = "xwin/sdk/lib/ucrt/x86_64" });
//    exe.addLibraryPath(.{ .cwd_relative = "xwin/sdk/lib/um/x86_64" });
    b.installArtifact(exe);
}

Note the .linkage and the .link_libc options.

3 Likes

Thanks for the quick reply! I’m build a shared library (it’s the Lua C module, loaded by Lua), and it seems lack .linkage in options:

error: no field named 'linkage' in struct 'Build.SharedLibraryOptions'
        .linkage = .static,
         ^~~~~~~

After some research I found the the .linkage value indeed exists in *Compile struct, but it indicate whether the output file is a .lib file or .dll file, but not affect with the linkage of .dll itself. On Windows, .dll file has no different with .exe file, just .dll file can not run standalone, but need some other .exe file to load them. So it’s possible to make a static linked shared library on Windows.

(on the other hand, on Linux or macOS the .so could use the symbols from host executable, makes the needn’t of static link of libc)

1 Like

That’s correct.

Your best option seems to be -windows-msvc.
I tried dynamic linking calling puts and got a dependency on ucrtbase[d].dll and VCRUNTIME140[D].dll
Your build is going to have similar dependencies that are redistributable as VC runtime and universal C runtime.

But calling ‘puts’ needn’t have to link with msvc redist librarians. You could just inline use static version of msvc libc (the LIBCMT.lib). I’m wondering if zig has the same thing as it. It helps me to send my tools to friends that has no msvc redist librarians installed. So it’s important for windows users.

I was building an executable that have a lot of calls for initialization and error handling.

You can build the dll and then check the dependencies using depends.exe or dumpbin.exe /imports

this is a small C test program:

#define LUA_LIB
#include <lua.h>
#include <lauxlib.h>

static int hello(lua_State *L) {
    lua_pushstring(L, "hello from C");
    return 1;
}

LUALIB_API int luaopen_test(lua_State *L) {
    lua_createtable(L, 0, 1);
    lua_pushcfunction(L, hello);
    lua_setfield(L, -2, "hello");
    return 1;
}

using msvc to build:

cl /LD /MT test.c /Fetest.dll /IC:\Devel\Lua54\include C:\Devel\Lua54\lib\lua54.lib

this is depends:

  Section contains the following imports:

    lua54.dll
    KERNEL32.dll

Using zig cc:

zig cc -shared -o test.dll test.c C:\Devel\lua54\lib\lua54.lib

the result of dumpbin /imports:

Dump of file test.dll

File Type: DLL

  Section contains the following imports:

    lua54.dll
    api-ms-win-crt-runtime-l1-1-0.dll
    KERNEL32.dll
    api-ms-win-crt-stdio-l1-1-0.dll
    api-ms-win-crt-environment-l1-1-0.dll
    api-ms-win-crt-heap-l1-1-0.dll
    api-ms-win-crt-time-l1-1-0.dll
    api-ms-win-crt-string-l1-1-0.dll

however, I found a flag -fms-runtime-lib when I search for how to use zig cc:

-fms-runtime-lib=<arg>¶
Specify Visual Studio C runtime library. “static” and “static_dbg” correspond to the cl flags /MT and /MTd which use the multithread, static version. “dll” and “dll_dbg” correspond to the cl flags /MD and /MDd which use the multithread, dll version. <arg> must be ‘static’, ‘static_dbg’, ‘dll’ or ‘dll_dbg’.

but it seems not work:

zig cc -shared -fms-runtime-lib=static -o test.dll test.c C:\Devel\lua54\lib\lua54.lib
zig: warning: argument unused during compilation: '-fms-runtime-lib=static' [-Wunused-command-line-argument]

Update: add --target x86_64-windows-msvc makes depends changes to this:

> zig cc -shared -target x86_64-windows-msvc -fms-runtime-lib=static -o test.dll test.c C:\Devel\lu
a54\lib\lua54.lib

> dumpbin /imports test.dll

Dump of file test.dll

File Type: DLL

  Section contains the following imports:

    lua54.dll
    ucrtbased.dll
    KERNEL32.dll
    VCRUNTIME140D.dll

-fms-runtime-lib still ignored.

Given the number of functions involved is small, I wonder if it wouldn’t be simpler if you just deal with them manually. Basically, create your own copy of stdio.h with the needed declarations in question. Something like this:

#ifndef _STDIO_H_
#define _STDIO_H_
#include <stddef.h>
#include <stdarg.h>

#define stdin  (&__iob_func()[0])
#define stdout (&__iob_func()[1])
#define stderr (&__iob_func()[2])

typedef struct { int unused } FILE;

FILE* __iob_func(void);
size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream);
int fprintf(FILE * stream, const char * format, ...);
int fflush(FILE * stream);

#endif

Then a .def file with these symbols:

NAME msvcrt.dll
EXPORTS
__iob_func
fwrite
fprintf
fflush

And from that an import library using zig dlltool.

add a mock stdio.h did the trick!

#ifndef _STDIO_H_
#define _STDIO_H_

typedef struct FILE FILE;

#endif /* _STDIO_H_ */

my build.zig:

    if (target.result.os.tag == .windows) {
        lib.root_module.addIncludePath(.{ .cwd_relative = "mock_stdlib" });
        lib.addLibraryPath(.{ .path = "C:/Devel/Lua54/lib" });
        lib.linkSystemLibrary("lua54");
    } else {
        lib.out_lib_filename = "zigluamod.so";
    }

Thanks for the workaround! But a proper solution is still expected :-/