Skip to content

Commit 7552f89

Browse files
committed
std.Build: add precompiled C header support
- add build-obj syntax to emit pch: `zig build-obj -femit-pch -lc++ -x c++-header test.h` `zig run -lc++ -cflags -include-pch test.pch -- main.cpp` - add a `.pch` kind Build.Step.Compile and Builder.addPrecompiledCHeader() - augment addCSourceFiles() to include an optional precompiled_header
1 parent bf7ebfa commit 7552f89

File tree

11 files changed

+247
-18
lines changed

11 files changed

+247
-18
lines changed

lib/std/Build.zig

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,35 @@ pub fn addObject(b: *Build, options: ObjectOptions) *Step.Compile {
714714
});
715715
}
716716

717+
pub const PchOptions = struct {
718+
name: []const u8,
719+
target: ResolvedTarget,
720+
optimize: std.builtin.OptimizeMode,
721+
max_rss: usize = 0,
722+
error_tracing: ?bool = null,
723+
cpp_header: bool = false,
724+
};
725+
726+
pub fn addPrecompiledCHeader(b: *Build, options: PchOptions, source: Module.CSourceFile) *Step.Compile {
727+
const pch = Step.Compile.create(b, .{
728+
.name = options.name,
729+
.root_module = .{
730+
.target = options.target,
731+
.optimize = options.optimize,
732+
.link_libc = !options.cpp_header,
733+
.link_libcpp = options.cpp_header,
734+
.error_tracing = options.error_tracing,
735+
},
736+
.kind = .pch,
737+
.max_rss = options.max_rss,
738+
.use_llvm = true,
739+
});
740+
pch.is_linking_libc = !options.cpp_header;
741+
pch.is_linking_libcpp = options.cpp_header;
742+
pch.addCSourceFile(source);
743+
return pch;
744+
}
745+
717746
pub const SharedLibraryOptions = struct {
718747
name: []const u8,
719748
/// To choose the same computer as the one building the package, pass the

lib/std/Build/Module.zig

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,16 +84,19 @@ pub const CSourceFiles = struct {
8484
/// else relative to the build root.
8585
files: []const []const u8,
8686
flags: []const []const u8,
87+
precompiled_header: ?*Step.Compile = null,
8788
};
8889

8990
pub const CSourceFile = struct {
9091
file: LazyPath,
9192
flags: []const []const u8 = &.{},
93+
precompiled_header: ?*Step.Compile = null,
9294

9395
pub fn dupe(self: CSourceFile, b: *std.Build) CSourceFile {
9496
return .{
9597
.file = self.file.dupe(b),
9698
.flags = b.dupeStrings(self.flags),
99+
.precompiled_header = self.precompiled_header,
97100
};
98101
}
99102
};
@@ -458,6 +461,7 @@ pub const AddCSourceFilesOptions = struct {
458461
dependency: ?*std.Build.Dependency = null,
459462
files: []const []const u8,
460463
flags: []const []const u8 = &.{},
464+
precompiled_header: ?*Step.Compile = null,
461465
};
462466

463467
/// Handy when you have many C/C++ source files and want them all to have the same flags.
@@ -469,8 +473,15 @@ pub fn addCSourceFiles(m: *Module, options: AddCSourceFilesOptions) void {
469473
.dependency = options.dependency,
470474
.files = b.dupeStrings(options.files),
471475
.flags = b.dupeStrings(options.flags),
476+
.precompiled_header = options.precompiled_header,
472477
};
473478
m.link_objects.append(allocator, .{ .c_source_files = c_source_files }) catch @panic("OOM");
479+
480+
if (options.precompiled_header) |pch| {
481+
assert(pch.kind == .pch);
482+
_ = pch.getEmittedBin(); // Indicate there is a dependency on the outputted pch binary.
483+
m.addStepDependenciesOnly(&pch.step);
484+
}
474485
}
475486

476487
pub fn addCSourceFile(m: *Module, source: CSourceFile) void {
@@ -480,6 +491,12 @@ pub fn addCSourceFile(m: *Module, source: CSourceFile) void {
480491
c_source_file.* = source.dupe(b);
481492
m.link_objects.append(allocator, .{ .c_source_file = c_source_file }) catch @panic("OOM");
482493
addLazyPathDependenciesOnly(m, source.file);
494+
495+
if (source.precompiled_header) |pch| {
496+
assert(pch.kind == .pch);
497+
_ = pch.getEmittedBin(); // Indicate there is a dependency on the outputted pch binary.
498+
m.addStepDependenciesOnly(&pch.step);
499+
}
483500
}
484501

485502
/// Resource files must have the extension `.rc`.

lib/std/Build/Step/Compile.zig

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ pub const Kind = enum {
238238
exe,
239239
lib,
240240
obj,
241+
pch,
241242
@"test",
242243
};
243244

@@ -263,27 +264,32 @@ pub fn create(owner: *std.Build, options: Options) *Compile {
263264
.exe => "zig build-exe",
264265
.lib => "zig build-lib",
265266
.obj => "zig build-obj",
267+
.pch => "zig build-pch",
266268
.@"test" => "zig test",
267269
},
268270
name_adjusted,
269271
@tagName(options.root_module.optimize orelse .Debug),
270272
resolved_target.query.zigTriple(owner.allocator) catch @panic("OOM"),
271273
});
272274

273-
const out_filename = std.zig.binNameAlloc(owner.allocator, .{
274-
.root_name = name,
275-
.target = target,
276-
.output_mode = switch (options.kind) {
277-
.lib => .Lib,
278-
.obj => .Obj,
279-
.exe, .@"test" => .Exe,
280-
},
281-
.link_mode = if (options.linkage) |some| @as(std.builtin.LinkMode, switch (some) {
282-
.dynamic => .Dynamic,
283-
.static => .Static,
284-
}) else null,
285-
.version = options.version,
286-
}) catch @panic("OOM");
275+
const out_filename = if (options.kind == .pch)
276+
std.fmt.allocPrint(owner.allocator, "{s}.pch", .{name}) catch @panic("OOM")
277+
else
278+
std.zig.binNameAlloc(owner.allocator, .{
279+
.root_name = name,
280+
.target = target,
281+
.output_mode = switch (options.kind) {
282+
.lib => .Lib,
283+
.obj => .Obj,
284+
.exe, .@"test" => .Exe,
285+
.pch => unreachable,
286+
},
287+
.link_mode = if (options.linkage) |some| @as(std.builtin.LinkMode, switch (some) {
288+
.dynamic => .Dynamic,
289+
.static => .Static,
290+
}) else null,
291+
.version = options.version,
292+
}) catch @panic("OOM");
287293

288294
const self = owner.allocator.create(Compile) catch @panic("OOM");
289295
self.* = .{
@@ -691,17 +697,20 @@ pub fn linkFrameworkWeak(c: *Compile, name: []const u8) void {
691697

692698
/// Handy when you have many C/C++ source files and want them all to have the same flags.
693699
pub fn addCSourceFiles(self: *Compile, options: Module.AddCSourceFilesOptions) void {
700+
assert(self.kind != .pch); // pch can only be generated from a single C header file
694701
self.root_module.addCSourceFiles(options);
695702
}
696703

697704
pub fn addCSourceFile(self: *Compile, source: Module.CSourceFile) void {
705+
assert(self.kind != .pch or self.root_module.link_objects.items.len == 0); // pch can only be generated from a single C header file
698706
self.root_module.addCSourceFile(source);
699707
}
700708

701709
/// Resource files must have the extension `.rc`.
702710
/// Can be called regardless of target. The .rc file will be ignored
703711
/// if the target object format does not support embedded resources.
704712
pub fn addWin32ResourceFile(self: *Compile, source: Module.RcSourceFile) void {
713+
assert(self.kind != .pch); // pch can only be generated from a single C header file
705714
self.root_module.addWin32ResourceFile(source);
706715
}
707716

@@ -784,14 +793,17 @@ pub fn getEmittedLlvmBc(self: *Compile) LazyPath {
784793
}
785794

786795
pub fn addAssemblyFile(self: *Compile, source: LazyPath) void {
796+
assert(self.kind != .pch); // pch can only be generated from a single C header file
787797
self.root_module.addAssemblyFile(source);
788798
}
789799

790800
pub fn addObjectFile(self: *Compile, source: LazyPath) void {
801+
assert(self.kind != .pch); // pch can only be generated from a single C header file
791802
self.root_module.addObjectFile(source);
792803
}
793804

794805
pub fn addObject(self: *Compile, object: *Compile) void {
806+
assert(self.kind != .pch); // pch can only be generated from a single C header file
795807
self.root_module.addObject(object);
796808
}
797809

@@ -915,7 +927,7 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void {
915927
const cmd = switch (self.kind) {
916928
.lib => "build-lib",
917929
.exe => "build-exe",
918-
.obj => "build-obj",
930+
.obj, .pch => "build-obj",
919931
.@"test" => "test",
920932
};
921933
try zig_args.append(cmd);
@@ -975,6 +987,14 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void {
975987
}
976988
}
977989

990+
if (self.kind == .pch) {
991+
// precompiled headers must have a single input header file.
992+
var it = self.root_module.iterateDependencies(self, false);
993+
const link_objects = it.next().?.module.link_objects;
994+
assert(link_objects.items.len == 1 and link_objects.items[0] == .c_source_file);
995+
assert(it.next() == null);
996+
}
997+
978998
var cli_named_modules = try CliNamedModules.init(arena, &self.root_module);
979999

9801000
// For this loop, don't chase dynamic libraries because their link
@@ -1076,6 +1096,7 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void {
10761096
switch (other.kind) {
10771097
.exe => return step.fail("cannot link with an executable build artifact", .{}),
10781098
.@"test" => return step.fail("cannot link with a test", .{}),
1099+
.pch => @panic("Cannot link with a precompiled header file"),
10791100
.obj => {
10801101
const included_in_lib_or_obj = !my_responsibility and (compile.kind == .lib or compile.kind == .obj);
10811102
if (!already_linked and !included_in_lib_or_obj) {
@@ -1129,7 +1150,7 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void {
11291150
.c_source_file => |c_source_file| l: {
11301151
if (!my_responsibility) break :l;
11311152

1132-
if (c_source_file.flags.len == 0) {
1153+
if (c_source_file.flags.len == 0 and c_source_file.precompiled_header == null) {
11331154
if (prev_has_cflags) {
11341155
try zig_args.append("-cflags");
11351156
try zig_args.append("--");
@@ -1140,17 +1161,28 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void {
11401161
for (c_source_file.flags) |arg| {
11411162
try zig_args.append(arg);
11421163
}
1164+
if (c_source_file.precompiled_header) |pch| {
1165+
try zig_args.append("-include-pch");
1166+
try zig_args.append(pch.getEmittedBin().getPath(b));
1167+
try zig_args.append("-fpch-validate-input-files-content");
1168+
}
11431169
try zig_args.append("--");
11441170
prev_has_cflags = true;
11451171
}
1172+
1173+
if (self.kind == .pch) {
1174+
try zig_args.append("-x");
1175+
try zig_args.append(if (self.is_linking_libcpp) "c++-header" else "c-header");
1176+
}
1177+
11461178
try zig_args.append(c_source_file.file.getPath2(module.owner, step));
11471179
total_linker_objects += 1;
11481180
},
11491181

11501182
.c_source_files => |c_source_files| l: {
11511183
if (!my_responsibility) break :l;
11521184

1153-
if (c_source_files.flags.len == 0) {
1185+
if (c_source_files.flags.len == 0 and c_source_files.precompiled_header == null) {
11541186
if (prev_has_cflags) {
11551187
try zig_args.append("-cflags");
11561188
try zig_args.append("--");
@@ -1161,6 +1193,13 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void {
11611193
for (c_source_files.flags) |flag| {
11621194
try zig_args.append(flag);
11631195
}
1196+
1197+
if (c_source_files.precompiled_header) |pch| {
1198+
try zig_args.append("-include-pch");
1199+
try zig_args.append(pch.getEmittedBin().getPath(b));
1200+
try zig_args.append("-fpch-validate-input-files-content");
1201+
}
1202+
11641203
try zig_args.append("--");
11651204
prev_has_cflags = true;
11661205
}
@@ -1307,6 +1346,7 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void {
13071346
if (self.generated_llvm_bc != null) try zig_args.append("-femit-llvm-bc");
13081347
if (self.generated_llvm_ir != null) try zig_args.append("-femit-llvm-ir");
13091348
if (self.generated_h != null) try zig_args.append("-femit-h");
1349+
if (self.kind == .pch) try zig_args.append("-femit-pch");
13101350

13111351
try addFlag(&zig_args, "formatted-panics", self.formatted_panics);
13121352

lib/std/Build/Step/InstallArtifact.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ pub fn create(owner: *std.Build, artifact: *Step.Compile, options: Options) *Ins
5656
const dest_dir: ?InstallDir = switch (options.dest_dir) {
5757
.disabled => null,
5858
.default => switch (artifact.kind) {
59-
.obj => @panic("object files have no standard installation procedure"),
59+
.obj, .pch => @panic("object files have no standard installation procedure"),
6060
.exe, .@"test" => InstallDir{ .bin = {} },
6161
.lib => InstallDir{ .lib = {} },
6262
},

src/main.zig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1417,6 +1417,12 @@ fn buildOutputType(
14171417
emit_bin = .{ .yes = arg["-femit-bin=".len..] };
14181418
} else if (mem.eql(u8, arg, "-fno-emit-bin")) {
14191419
emit_bin = .no;
1420+
} else if (mem.eql(u8, arg, "-femit-pch")) {
1421+
clang_preprocessor_mode = .pch;
1422+
emit_bin = .yes_default_path;
1423+
} else if (mem.startsWith(u8, arg, "-femit-pch=")) {
1424+
clang_preprocessor_mode = .pch;
1425+
emit_bin = .{ .yes = arg["-femit-pch=".len..] };
14201426
} else if (mem.eql(u8, arg, "-femit-h")) {
14211427
emit_h = .yes_default_path;
14221428
} else if (mem.startsWith(u8, arg, "-femit-h=")) {

test/standalone.zig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,10 @@ pub const build_cases = [_]BuildCase{
265265
.build_root = "test/standalone/depend_on_main_mod",
266266
.import = @import("standalone/depend_on_main_mod/build.zig"),
267267
},
268+
.{
269+
.build_root = "test/standalone/pch",
270+
.import = @import("standalone/pch/build.zig"),
271+
},
268272
};
269273

270274
const std = @import("std");

test/standalone/pch/build.zig

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
const std = @import("std");
2+
3+
pub fn build(b: *std.Build) void {
4+
const test_step = b.step("test", "Test it");
5+
b.default_step = test_step;
6+
7+
const target = b.standardTargetOptions(.{});
8+
const optimize = b.standardOptimizeOption(.{});
9+
10+
// c-header
11+
{
12+
const exe = b.addExecutable(.{
13+
.name = "pchtest",
14+
.target = target,
15+
.optimize = optimize,
16+
.link_libc = true,
17+
});
18+
19+
const pch = b.addPrecompiledCHeader(.{
20+
.name = "pch_c",
21+
.target = target,
22+
.optimize = optimize,
23+
.cpp_header = false,
24+
}, .{
25+
.file = .{ .path = "include_a.h" },
26+
.flags = &[_][]const u8{},
27+
});
28+
29+
exe.addCSourceFile(.{
30+
.file = .{ .path = "test.c" },
31+
.flags = &[_][]const u8{},
32+
.precompiled_header = pch,
33+
});
34+
35+
test_step.dependOn(&b.addRunArtifact(exe).step);
36+
}
37+
38+
// c++-header
39+
{
40+
const exe = b.addExecutable(.{
41+
.name = "pchtest++",
42+
.target = target,
43+
.optimize = optimize,
44+
.link_libc = true,
45+
});
46+
exe.linkLibCpp();
47+
48+
const pch = b.addPrecompiledCHeader(.{
49+
.name = "pch_c++",
50+
.target = target,
51+
.optimize = optimize,
52+
.cpp_header = true,
53+
}, .{
54+
.file = .{ .path = "include_a.h" },
55+
.flags = &[_][]const u8{},
56+
});
57+
58+
exe.addCSourceFile(.{
59+
.file = .{ .path = "test.cpp" },
60+
.flags = &[_][]const u8{},
61+
.precompiled_header = pch,
62+
});
63+
64+
test_step.dependOn(&b.addRunArtifact(exe).step);
65+
}
66+
}

test/standalone/pch/include_a.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#pragma once
2+
3+
#include "include_b.h"
4+
5+
#include <string.h>
6+
#include <stdlib.h>
7+
8+
#if defined(__cplusplus)
9+
#include <iostream>
10+
#else
11+
#include <stdio.h>
12+
#endif
13+
14+
#define A_INCLUDED 1
15+

0 commit comments

Comments
 (0)