| author | |
| committer | |
| log | 875e98a57d99c1b6ce8a4a2f9f103b8fe417b8be |
| tree | b78fd03f9ce225de9c1b88cf0aef59aa1cf6a052 |
| parent | d42a719e8f7ba31a9e18d6be9d58691b0b38c69a |
| parent | c0710b0c42716bb7173b9fcc2785f9bf5175ae0f |
| signature |
wasm-linker: feature compatibility validation10 files changed, 257 insertions(+), 13 deletions(-)
lib/std/build/CheckObjectStep.zig+17| ... | ... | @@ -649,6 +649,8 @@ const WasmDumper = struct { |
| 649 | 649 | try parseDumpNames(reader, writer, data); |
| 650 | 650 | } else if (mem.eql(u8, name, "producers")) { |
| 651 | 651 | try parseDumpProducers(reader, writer, data); |
| 652 | } else if (mem.eql(u8, name, "target_features")) { | |
| 653 | try parseDumpFeatures(reader, writer, data); | |
| 652 | 654 | } |
| 653 | 655 | // TODO: Implement parsing and dumping other custom sections (such as relocations) |
| 654 | 656 | }, |
| ... | ... | @@ -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 | 696 | GlobalTypeMismatch, |
| 697 | 697 | InvalidCharacter, |
| 698 | 698 | InvalidEntryKind, |
| 699 | InvalidFeatureSet, | |
| 699 | 700 | InvalidFormat, |
| 700 | 701 | InvalidIndex, |
| 701 | 702 | InvalidMagicByte, |
src/link/Wasm.zig+135| ... | ... | @@ -651,6 +651,109 @@ fn resolveSymbolsInArchives(wasm: *Wasm) !void { |
| 651 | 651 | } |
| 652 | 652 | } |
| 653 | 653 | |
| 654 | fn 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 | ||
| 654 | 757 | fn checkUndefinedSymbols(wasm: *const Wasm) !void { |
| 655 | 758 | if (wasm.base.options.output_mode == .Obj) return; |
| 656 | 759 | |
| ... | ... | @@ -2158,6 +2261,9 @@ pub fn flushModule(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Nod |
| 2158 | 2261 | try wasm.resolveSymbolsInObject(@intCast(u16, object_index)); |
| 2159 | 2262 | } |
| 2160 | 2263 | |
| 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 | 2267 | try wasm.resolveSymbolsInArchives(); |
| 2162 | 2268 | try wasm.checkUndefinedSymbols(); |
| 2163 | 2269 | |
| ... | ... | @@ -2603,6 +2709,9 @@ pub fn flushModule(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Nod |
| 2603 | 2709 | } |
| 2604 | 2710 | |
| 2605 | 2711 | try emitProducerSection(&binary_bytes); |
| 2712 | if (emit_features_count > 0) { | |
| 2713 | try emitFeaturesSection(&binary_bytes, &enabled_features, emit_features_count); | |
| 2714 | } | |
| 2606 | 2715 | } |
| 2607 | 2716 | |
| 2608 | 2717 | // Only when writing all sections executed properly we write the magic |
| ... | ... | @@ -2695,6 +2804,32 @@ fn emitProducerSection(binary_bytes: *std.ArrayList(u8)) !void { |
| 2695 | 2804 | ); |
| 2696 | 2805 | } |
| 2697 | 2806 | |
| 2807 | fn 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 | ||
| 2698 | 2833 | fn emitNameSection(wasm: *Wasm, binary_bytes: *std.ArrayList(u8), arena: std.mem.Allocator) !void { |
| 2699 | 2834 | const Name = struct { |
| 2700 | 2835 | index: u32, |
src/link/Wasm/types.zig+31-13| ... | ... | @@ -183,17 +183,44 @@ pub const Feature = struct { |
| 183 | 183 | /// Type of the feature, must be unique in the sequence of features. |
| 184 | 184 | tag: Tag, |
| 185 | 185 | |
| 186 | /// Unlike `std.Target.wasm.Feature` this also contains linker-features such as shared-mem | |
| 186 | 187 | pub const Tag = enum { |
| 187 | 188 | atomics, |
| 188 | 189 | bulk_memory, |
| 189 | 190 | exception_handling, |
| 191 | extended_const, | |
| 190 | 192 | multivalue, |
| 191 | 193 | mutable_globals, |
| 192 | 194 | nontrapping_fptoint, |
| 195 | reference_types, | |
| 196 | relaxed_simd, | |
| 193 | 197 | sign_ext, |
| 194 | 198 | simd128, |
| 195 | 199 | tail_call, |
| 196 | 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 | }; |
| 198 | 225 | |
| 199 | 226 | pub const Prefix = enum(u8) { |
| ... | ... | @@ -202,22 +229,10 @@ pub const Feature = struct { |
| 202 | 229 | required = '=', |
| 203 | 230 | }; |
| 204 | 231 | |
| 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 | 232 | pub fn format(feature: Feature, comptime fmt: []const u8, opt: std.fmt.FormatOptions, writer: anytype) !void { |
| 218 | 233 | _ = opt; |
| 219 | 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 | }; |
| 223 | 238 | |
| ... | ... | @@ -225,9 +240,12 @@ pub const known_features = std.ComptimeStringMap(Feature.Tag, .{ |
| 225 | 240 | .{ "atomics", .atomics }, |
| 226 | 241 | .{ "bulk-memory", .bulk_memory }, |
| 227 | 242 | .{ "exception-handling", .exception_handling }, |
| 243 | .{ "extended-const", .extended_const }, | |
| 228 | 244 | .{ "multivalue", .multivalue }, |
| 229 | 245 | .{ "mutable-globals", .mutable_globals }, |
| 230 | 246 | .{ "nontrapping-fptoint", .nontrapping_fptoint }, |
| 247 | .{ "reference-types", .reference_types }, | |
| 248 | .{ "relaxed-simd", .relaxed_simd }, | |
| 231 | 249 | .{ "sign-ext", .sign_ext }, |
| 232 | 250 | .{ "simd128", .simd128 }, |
| 233 | 251 | .{ "tail-call", .tail_call }, |
test/link.zig+8| ... | ... | @@ -33,6 +33,10 @@ fn addWasmCases(cases: *tests.StandaloneContext) void { |
| 33 | 33 | .requires_stage2 = true, |
| 34 | 34 | }); |
| 35 | 35 | |
| 36 | cases.addBuildFile("test/link/wasm/basic-features/build.zig", .{ | |
| 37 | .requires_stage2 = true, | |
| 38 | }); | |
| 39 | ||
| 36 | 40 | cases.addBuildFile("test/link/wasm/bss/build.zig", .{ |
| 37 | 41 | .build_modes = false, |
| 38 | 42 | .requires_stage2 = true, |
| ... | ... | @@ -44,6 +48,10 @@ fn addWasmCases(cases: *tests.StandaloneContext) void { |
| 44 | 48 | .use_emulation = true, |
| 45 | 49 | }); |
| 46 | 50 | |
| 51 | cases.addBuildFile("test/link/wasm/infer-features/build.zig", .{ | |
| 52 | .requires_stage2 = true, | |
| 53 | }); | |
| 54 | ||
| 47 | 55 | cases.addBuildFile("test/link/wasm/producers/build.zig", .{ |
| 48 | 56 | .build_modes = true, |
| 49 | 57 | .requires_stage2 = true, |
test/link/wasm/basic-features/build.zig created+23| ... | ... | @@ -0,0 +1,23 @@ |
| 1 | const std = @import("std"); | |
| 2 | ||
| 3 | pub 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 @@ |
| 1 | export fn foo() void {} |
test/link/wasm/infer-features/build.zig created+37| ... | ... | @@ -0,0 +1,37 @@ |
| 1 | const std = @import("std"); | |
| 2 | ||
| 3 | pub 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 @@ |
| 1 | int foo() { | |
| 2 | return 5; | |
| 3 | } |
test/link/wasm/infer-features/main.zig created+1| ... | ... | @@ -0,0 +1 @@ |
| 1 | extern fn foo() c_int; |