authorgravatar for luuk@degram.devLuuk de Gram <luuk@degram.dev> 2022-10-26 14:04:16+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2022-10-26 14:04:16+02:00
log875e98a57d99c1b6ce8a4a2f9f103b8fe417b8be
treeb78fd03f9ce225de9c1b88cf0aef59aa1cf6a052
parentd42a719e8f7ba31a9e18d6be9d58691b0b38c69a
parentc0710b0c42716bb7173b9fcc2785f9bf5175ae0f
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

Merge pull request #13287 from Luukdegram/wasm-features

wasm-linker: feature compatibility validation

10 files changed, 257 insertions(+), 13 deletions(-)

lib/std/build/CheckObjectStep.zig+17
...@@ -649,6 +649,8 @@ const WasmDumper = struct {...@@ -649,6 +649,8 @@ const WasmDumper = struct {
649 try parseDumpNames(reader, writer, data);649 try parseDumpNames(reader, writer, data);
650 } else if (mem.eql(u8, name, "producers")) {650 } else if (mem.eql(u8, name, "producers")) {
651 try parseDumpProducers(reader, writer, data);651 try parseDumpProducers(reader, writer, data);
652 } else if (mem.eql(u8, name, "target_features")) {
653 try parseDumpFeatures(reader, writer, data);
652 }654 }
653 // TODO: Implement parsing and dumping other custom sections (such as relocations)655 // TODO: Implement parsing and dumping other custom sections (such as relocations)
654 },656 },
...@@ -902,4 +904,19 @@ const WasmDumper = struct {...@@ -902,4 +904,19 @@ const WasmDumper = struct {
902 }904 }
903 }905 }
904 }906 }
907
908 fn parseDumpFeatures(reader: anytype, writer: anytype, data: []const u8) !void {
909 const feature_count = try std.leb.readULEB128(u32, reader);
910 try writer.print("features {d}\n", .{feature_count});
911
912 var index: u32 = 0;
913 while (index < feature_count) : (index += 1) {
914 const prefix_byte = try std.leb.readULEB128(u8, reader);
915 const name_length = try std.leb.readULEB128(u32, reader);
916 const feature_name = data[reader.context.pos..][0..name_length];
917 reader.context.pos += name_length;
918
919 try writer.print("{c} {s}\n", .{ prefix_byte, feature_name });
920 }
921 }
905};922};
src/link.zig+1
...@@ -696,6 +696,7 @@ pub const File = struct {...@@ -696,6 +696,7 @@ pub const File = struct {
696 GlobalTypeMismatch,696 GlobalTypeMismatch,
697 InvalidCharacter,697 InvalidCharacter,
698 InvalidEntryKind,698 InvalidEntryKind,
699 InvalidFeatureSet,
699 InvalidFormat,700 InvalidFormat,
700 InvalidIndex,701 InvalidIndex,
701 InvalidMagicByte,702 InvalidMagicByte,
src/link/Wasm.zig+135
...@@ -651,6 +651,109 @@ fn resolveSymbolsInArchives(wasm: *Wasm) !void {...@@ -651,6 +651,109 @@ fn resolveSymbolsInArchives(wasm: *Wasm) !void {
651 }651 }
652}652}
653653
654fn validateFeatures(
655 wasm: *const Wasm,
656 to_emit: *[@typeInfo(types.Feature.Tag).Enum.fields.len]bool,
657 emit_features_count: *u32,
658) !void {
659 const cpu_features = wasm.base.options.target.cpu.features;
660 const infer = cpu_features.isEmpty(); // when the user did not define any features, we infer them from linked objects.
661 const known_features_count = @typeInfo(types.Feature.Tag).Enum.fields.len;
662
663 var allowed = [_]bool{false} ** known_features_count;
664 var used = [_]u17{0} ** known_features_count;
665 var disallowed = [_]u17{0} ** known_features_count;
666 var required = [_]u17{0} ** known_features_count;
667
668 // when false, we fail linking. We only verify this after a loop to catch all invalid features.
669 var valid_feature_set = true;
670
671 // When the user has given an explicit list of features to enable,
672 // we extract them and insert each into the 'allowed' list.
673 if (!infer) {
674 inline for (@typeInfo(std.Target.wasm.Feature).Enum.fields) |feature_field| {
675 if (cpu_features.isEnabled(feature_field.value)) {
676 allowed[feature_field.value] = true;
677 emit_features_count.* += 1;
678 }
679 }
680 }
681
682 // extract all the used, disallowed and required features from each
683 // linked object file so we can test them.
684 for (wasm.objects.items) |object, object_index| {
685 for (object.features) |feature| {
686 const value = @intCast(u16, object_index) << 1 | @as(u1, 1);
687 switch (feature.prefix) {
688 .used => {
689 used[@enumToInt(feature.tag)] = value;
690 },
691 .disallowed => {
692 disallowed[@enumToInt(feature.tag)] = value;
693 },
694 .required => {
695 required[@enumToInt(feature.tag)] = value;
696 used[@enumToInt(feature.tag)] = value;
697 },
698 }
699 }
700 }
701
702 // when we infer the features, we allow each feature found in the 'used' set
703 // and insert it into the 'allowed' set. When features are not inferred,
704 // we validate that a used feature is allowed.
705 for (used) |used_set, used_index| {
706 const is_enabled = @truncate(u1, used_set) != 0;
707 if (infer) {
708 allowed[used_index] = is_enabled;
709 emit_features_count.* += @boolToInt(is_enabled);
710 } else if (is_enabled and !allowed[used_index]) {
711 log.err("feature '{s}' not allowed, but used by linked object", .{(@intToEnum(types.Feature.Tag, used_index)).toString()});
712 log.err(" defined in '{s}'", .{wasm.objects.items[used_set >> 1].name});
713 valid_feature_set = false;
714 }
715 }
716
717 if (!valid_feature_set) {
718 return error.InvalidFeatureSet;
719 }
720
721 // For each linked object, validate the required and disallowed features
722 for (wasm.objects.items) |object| {
723 var object_used_features = [_]bool{false} ** known_features_count;
724 for (object.features) |feature| {
725 if (feature.prefix == .disallowed) continue; // already defined in 'disallowed' set.
726 // from here a feature is always used
727 const disallowed_feature = disallowed[@enumToInt(feature.tag)];
728 if (@truncate(u1, disallowed_feature) != 0) {
729 log.err("feature '{s}' is disallowed, but used by linked object", .{feature.tag.toString()});
730 log.err(" disallowed by '{s}'", .{wasm.objects.items[disallowed_feature >> 1].name});
731 log.err(" used in '{s}'", .{object.name});
732 valid_feature_set = false;
733 }
734
735 object_used_features[@enumToInt(feature.tag)] = true;
736 }
737
738 // validate the linked object file has each required feature
739 for (required) |required_feature, feature_index| {
740 const is_required = @truncate(u1, required_feature) != 0;
741 if (is_required and !object_used_features[feature_index]) {
742 log.err("feature '{s}' is required but not used in linked object", .{(@intToEnum(types.Feature.Tag, feature_index)).toString()});
743 log.err(" required by '{s}'", .{wasm.objects.items[required_feature >> 1].name});
744 log.err(" missing in '{s}'", .{object.name});
745 valid_feature_set = false;
746 }
747 }
748 }
749
750 if (!valid_feature_set) {
751 return error.InvalidFeatureSet;
752 }
753
754 to_emit.* = allowed;
755}
756
654fn checkUndefinedSymbols(wasm: *const Wasm) !void {757fn checkUndefinedSymbols(wasm: *const Wasm) !void {
655 if (wasm.base.options.output_mode == .Obj) return;758 if (wasm.base.options.output_mode == .Obj) return;
656759
...@@ -2158,6 +2261,9 @@ pub fn flushModule(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Nod...@@ -2158,6 +2261,9 @@ pub fn flushModule(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Nod
2158 try wasm.resolveSymbolsInObject(@intCast(u16, object_index));2261 try wasm.resolveSymbolsInObject(@intCast(u16, object_index));
2159 }2262 }
21602263
2264 var emit_features_count: u32 = 0;
2265 var enabled_features: [@typeInfo(types.Feature.Tag).Enum.fields.len]bool = undefined;
2266 try wasm.validateFeatures(&enabled_features, &emit_features_count);
2161 try wasm.resolveSymbolsInArchives();2267 try wasm.resolveSymbolsInArchives();
2162 try wasm.checkUndefinedSymbols();2268 try wasm.checkUndefinedSymbols();
21632269
...@@ -2603,6 +2709,9 @@ pub fn flushModule(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Nod...@@ -2603,6 +2709,9 @@ pub fn flushModule(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Nod
2603 }2709 }
26042710
2605 try emitProducerSection(&binary_bytes);2711 try emitProducerSection(&binary_bytes);
2712 if (emit_features_count > 0) {
2713 try emitFeaturesSection(&binary_bytes, &enabled_features, emit_features_count);
2714 }
2606 }2715 }
26072716
2608 // Only when writing all sections executed properly we write the magic2717 // Only when writing all sections executed properly we write the magic
...@@ -2695,6 +2804,32 @@ fn emitProducerSection(binary_bytes: *std.ArrayList(u8)) !void {...@@ -2695,6 +2804,32 @@ fn emitProducerSection(binary_bytes: *std.ArrayList(u8)) !void {
2695 );2804 );
2696}2805}
26972806
2807fn emitFeaturesSection(binary_bytes: *std.ArrayList(u8), enabled_features: []const bool, features_count: u32) !void {
2808 const header_offset = try reserveCustomSectionHeader(binary_bytes);
2809
2810 const writer = binary_bytes.writer();
2811 const target_features = "target_features";
2812 try leb.writeULEB128(writer, @intCast(u32, target_features.len));
2813 try writer.writeAll(target_features);
2814
2815 try leb.writeULEB128(writer, features_count);
2816 for (enabled_features) |enabled, feature_index| {
2817 if (enabled) {
2818 const feature: types.Feature = .{ .prefix = .used, .tag = @intToEnum(types.Feature.Tag, feature_index) };
2819 try leb.writeULEB128(writer, @enumToInt(feature.prefix));
2820 const string = feature.tag.toString();
2821 try leb.writeULEB128(writer, @intCast(u32, string.len));
2822 try writer.writeAll(string);
2823 }
2824 }
2825
2826 try writeCustomSectionHeader(
2827 binary_bytes.items,
2828 header_offset,
2829 @intCast(u32, binary_bytes.items.len - header_offset - 6),
2830 );
2831}
2832
2698fn emitNameSection(wasm: *Wasm, binary_bytes: *std.ArrayList(u8), arena: std.mem.Allocator) !void {2833fn emitNameSection(wasm: *Wasm, binary_bytes: *std.ArrayList(u8), arena: std.mem.Allocator) !void {
2699 const Name = struct {2834 const Name = struct {
2700 index: u32,2835 index: u32,
src/link/Wasm/types.zig+31-13
...@@ -183,17 +183,44 @@ pub const Feature = struct {...@@ -183,17 +183,44 @@ pub const Feature = struct {
183 /// Type of the feature, must be unique in the sequence of features.183 /// Type of the feature, must be unique in the sequence of features.
184 tag: Tag,184 tag: Tag,
185185
186 /// Unlike `std.Target.wasm.Feature` this also contains linker-features such as shared-mem
186 pub const Tag = enum {187 pub const Tag = enum {
187 atomics,188 atomics,
188 bulk_memory,189 bulk_memory,
189 exception_handling,190 exception_handling,
191 extended_const,
190 multivalue,192 multivalue,
191 mutable_globals,193 mutable_globals,
192 nontrapping_fptoint,194 nontrapping_fptoint,
195 reference_types,
196 relaxed_simd,
193 sign_ext,197 sign_ext,
194 simd128,198 simd128,
195 tail_call,199 tail_call,
196 shared_mem,200 shared_mem,
201
202 /// From a given cpu feature, returns its linker feature
203 pub fn fromCpuFeature(feature: std.Target.wasm.Feature) Tag {
204 return @intToEnum(Tag, @enumToInt(feature));
205 }
206
207 pub fn toString(tag: Tag) []const u8 {
208 return switch (tag) {
209 .atomics => "atomics",
210 .bulk_memory => "bulk-memory",
211 .exception_handling => "exception-handling",
212 .extended_const => "extended-const",
213 .multivalue => "multivalue",
214 .mutable_globals => "mutable-globals",
215 .nontrapping_fptoint => "nontrapping-fptoint",
216 .reference_types => "reference-types",
217 .relaxed_simd => "relaxed-simd",
218 .sign_ext => "sign-ext",
219 .simd128 => "simd128",
220 .tail_call => "tail-call",
221 .shared_mem => "shared-mem",
222 };
223 }
197 };224 };
198225
199 pub const Prefix = enum(u8) {226 pub const Prefix = enum(u8) {
...@@ -202,22 +229,10 @@ pub const Feature = struct {...@@ -202,22 +229,10 @@ pub const Feature = struct {
202 required = '=',229 required = '=',
203 };230 };
204231
205 pub fn toString(feature: Feature) []const u8 {
206 return switch (feature.tag) {
207 .bulk_memory => "bulk-memory",
208 .exception_handling => "exception-handling",
209 .mutable_globals => "mutable-globals",
210 .nontrapping_fptoint => "nontrapping-fptoint",
211 .sign_ext => "sign-ext",
212 .tail_call => "tail-call",
213 else => @tagName(feature),
214 };
215 }
216
217 pub fn format(feature: Feature, comptime fmt: []const u8, opt: std.fmt.FormatOptions, writer: anytype) !void {232 pub fn format(feature: Feature, comptime fmt: []const u8, opt: std.fmt.FormatOptions, writer: anytype) !void {
218 _ = opt;233 _ = opt;
219 _ = fmt;234 _ = fmt;
220 try writer.print("{c} {s}", .{ feature.prefix, feature.toString() });235 try writer.print("{c} {s}", .{ feature.prefix, feature.tag.toString() });
221 }236 }
222};237};
223238
...@@ -225,9 +240,12 @@ pub const known_features = std.ComptimeStringMap(Feature.Tag, .{...@@ -225,9 +240,12 @@ pub const known_features = std.ComptimeStringMap(Feature.Tag, .{
225 .{ "atomics", .atomics },240 .{ "atomics", .atomics },
226 .{ "bulk-memory", .bulk_memory },241 .{ "bulk-memory", .bulk_memory },
227 .{ "exception-handling", .exception_handling },242 .{ "exception-handling", .exception_handling },
243 .{ "extended-const", .extended_const },
228 .{ "multivalue", .multivalue },244 .{ "multivalue", .multivalue },
229 .{ "mutable-globals", .mutable_globals },245 .{ "mutable-globals", .mutable_globals },
230 .{ "nontrapping-fptoint", .nontrapping_fptoint },246 .{ "nontrapping-fptoint", .nontrapping_fptoint },
247 .{ "reference-types", .reference_types },
248 .{ "relaxed-simd", .relaxed_simd },
231 .{ "sign-ext", .sign_ext },249 .{ "sign-ext", .sign_ext },
232 .{ "simd128", .simd128 },250 .{ "simd128", .simd128 },
233 .{ "tail-call", .tail_call },251 .{ "tail-call", .tail_call },
test/link.zig+8
...@@ -33,6 +33,10 @@ fn addWasmCases(cases: *tests.StandaloneContext) void {...@@ -33,6 +33,10 @@ fn addWasmCases(cases: *tests.StandaloneContext) void {
33 .requires_stage2 = true,33 .requires_stage2 = true,
34 });34 });
3535
36 cases.addBuildFile("test/link/wasm/basic-features/build.zig", .{
37 .requires_stage2 = true,
38 });
39
36 cases.addBuildFile("test/link/wasm/bss/build.zig", .{40 cases.addBuildFile("test/link/wasm/bss/build.zig", .{
37 .build_modes = false,41 .build_modes = false,
38 .requires_stage2 = true,42 .requires_stage2 = true,
...@@ -44,6 +48,10 @@ fn addWasmCases(cases: *tests.StandaloneContext) void {...@@ -44,6 +48,10 @@ fn addWasmCases(cases: *tests.StandaloneContext) void {
44 .use_emulation = true,48 .use_emulation = true,
45 });49 });
4650
51 cases.addBuildFile("test/link/wasm/infer-features/build.zig", .{
52 .requires_stage2 = true,
53 });
54
47 cases.addBuildFile("test/link/wasm/producers/build.zig", .{55 cases.addBuildFile("test/link/wasm/producers/build.zig", .{
48 .build_modes = true,56 .build_modes = true,
49 .requires_stage2 = true,57 .requires_stage2 = true,
test/link/wasm/basic-features/build.zig created+23
...@@ -0,0 +1,23 @@
1const std = @import("std");
2
3pub fn build(b: *std.build.Builder) void {
4 const mode = b.standardReleaseOptions();
5
6 // Library with explicitly set cpu features
7 const lib = b.addSharedLibrary("lib", "main.zig", .unversioned);
8 lib.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding });
9 lib.target.cpu_model = .{ .explicit = &std.Target.wasm.cpu.mvp };
10 lib.target.cpu_features_add.addFeature(0); // index 0 == atomics (see std.Target.wasm.Features)
11 lib.setBuildMode(mode);
12 lib.use_llvm = false;
13 lib.use_lld = false;
14
15 // Verify the result contains the features explicitly set on the target for the library.
16 const check = lib.checkObject(.wasm);
17 check.checkStart("name target_features");
18 check.checkNext("features 1");
19 check.checkNext("+ atomics");
20
21 const test_step = b.step("test", "Run linker test");
22 test_step.dependOn(&check.step);
23}
test/link/wasm/basic-features/main.zig created+1
...@@ -0,0 +1 @@
1export fn foo() void {}
test/link/wasm/infer-features/build.zig created+37
...@@ -0,0 +1,37 @@
1const std = @import("std");
2
3pub fn build(b: *std.build.Builder) void {
4 const mode = b.standardReleaseOptions();
5
6 // Wasm Object file which we will use to infer the features from
7 const c_obj = b.addObject("c_obj", null);
8 c_obj.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding });
9 c_obj.target.cpu_model = .{ .explicit = &std.Target.wasm.cpu.bleeding_edge };
10 c_obj.addCSourceFile("foo.c", &.{});
11 c_obj.setBuildMode(mode);
12
13 // Wasm library that doesn't have any features specified. This will
14 // infer its featureset from other linked object files.
15 const lib = b.addSharedLibrary("lib", "main.zig", .unversioned);
16 lib.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding });
17 lib.target.cpu_model = .{ .explicit = &std.Target.wasm.cpu.mvp };
18 lib.setBuildMode(mode);
19 lib.use_llvm = false;
20 lib.use_lld = false;
21 lib.addObject(c_obj);
22
23 // Verify the result contains the features from the C Object file.
24 const check = lib.checkObject(.wasm);
25 check.checkStart("name target_features");
26 check.checkNext("features 7");
27 check.checkNext("+ atomics");
28 check.checkNext("+ bulk-memory");
29 check.checkNext("+ mutable-globals");
30 check.checkNext("+ nontrapping-fptoint");
31 check.checkNext("+ sign-ext");
32 check.checkNext("+ simd128");
33 check.checkNext("+ tail-call");
34
35 const test_step = b.step("test", "Run linker test");
36 test_step.dependOn(&check.step);
37}
test/link/wasm/infer-features/foo.c created+3
...@@ -0,0 +1,3 @@
1int foo() {
2 return 5;
3}
test/link/wasm/infer-features/main.zig created+1
...@@ -0,0 +1 @@
1extern fn foo() c_int;