authorgravatar for luuk@degram.devLuuk de Gram <luuk@degram.dev> 2022-10-25 21:16:51+02:00
committergravatar for luuk@degram.devLuuk de Gram <luuk@degram.dev> 2022-10-25 21:16:51+02:00
logc0710b0c42716bb7173b9fcc2785f9bf5175ae0f
tree33144c17bd3606709342edde8db3b2549e1bccf8
parent3d1d19f3877190db42544acf9e0ed26784ba82ba
signaturelock-open Commit is signed but in an unrecognized format.

use fixed-size arrays for feature lists

Considering all possible features are known by the linker during compile-time, we can create arrays on the stack instead of dynamically allocating hash maps. We use a simple bitset to determine whether a feature is enabled or not, and from which object file it originates. This allows us to make feature validation slightly faster and use less runtime memory. In the future this could be enhanced further by having a single array instead with a more sophisticated bitset.

3 files changed, 55 insertions(+), 69 deletions(-)

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+35-50
...@@ -653,16 +653,17 @@ fn resolveSymbolsInArchives(wasm: *Wasm) !void {...@@ -653,16 +653,17 @@ fn resolveSymbolsInArchives(wasm: *Wasm) !void {
653653
654fn validateFeatures(654fn validateFeatures(
655 wasm: *const Wasm,655 wasm: *const Wasm,
656 arena: Allocator,
657 to_emit: *[@typeInfo(types.Feature.Tag).Enum.fields.len]bool,656 to_emit: *[@typeInfo(types.Feature.Tag).Enum.fields.len]bool,
658 emit_features_count: *u32,657 emit_features_count: *u32,
659) !void {658) !void {
660 const cpu_features = wasm.base.options.target.cpu.features;659 const cpu_features = wasm.base.options.target.cpu.features;
661 const infer = cpu_features.isEmpty(); // when the user did not define any features, we infer them from linked objects.660 const infer = cpu_features.isEmpty(); // when the user did not define any features, we infer them from linked objects.
662 var allowed = std.AutoHashMap(types.Feature.Tag, void).init(arena);661 const known_features_count = @typeInfo(types.Feature.Tag).Enum.fields.len;
663 var used = std.AutoArrayHashMap(types.Feature.Tag, []const u8).init(arena);662
664 var disallowed = std.AutoHashMap(types.Feature.Tag, []const u8).init(arena);663 var allowed = [_]bool{false} ** known_features_count;
665 var required = std.AutoHashMap(types.Feature.Tag, []const u8).init(arena);664 var used = [_]u17{0} ** known_features_count;
665 var disallowed = [_]u17{0} ** known_features_count;
666 var required = [_]u17{0} ** known_features_count;
666667
667 // when false, we fail linking. We only verify this after a loop to catch all invalid features.668 // when false, we fail linking. We only verify this after a loop to catch all invalid features.
668 var valid_feature_set = true;669 var valid_feature_set = true;
...@@ -670,41 +671,29 @@ fn validateFeatures(...@@ -670,41 +671,29 @@ fn validateFeatures(
670 // When the user has given an explicit list of features to enable,671 // When the user has given an explicit list of features to enable,
671 // we extract them and insert each into the 'allowed' list.672 // we extract them and insert each into the 'allowed' list.
672 if (!infer) {673 if (!infer) {
673 try allowed.ensureUnusedCapacity(std.Target.wasm.all_features.len);
674 // std.builtin.Type.EnumField
675 inline for (@typeInfo(std.Target.wasm.Feature).Enum.fields) |feature_field| {674 inline for (@typeInfo(std.Target.wasm.Feature).Enum.fields) |feature_field| {
676 if (cpu_features.isEnabled(feature_field.value)) {675 if (cpu_features.isEnabled(feature_field.value)) {
677 allowed.putAssumeCapacityNoClobber(@intToEnum(types.Feature.Tag, feature_field.value), {});676 allowed[feature_field.value] = true;
677 emit_features_count.* += 1;
678 }678 }
679 }679 }
680 }680 }
681681
682 // extract all the used, disallowed and required features from each682 // extract all the used, disallowed and required features from each
683 // linked object file so we can test them.683 // linked object file so we can test them.
684 for (wasm.objects.items) |object| {684 for (wasm.objects.items) |object, object_index| {
685 for (object.features) |feature| {685 for (object.features) |feature| {
686 const value = @intCast(u16, object_index) << 1 | @as(u1, 1);
686 switch (feature.prefix) {687 switch (feature.prefix) {
687 .used => {688 .used => {
688 const gop = try used.getOrPut(feature.tag);689 used[@enumToInt(feature.tag)] = value;
689 if (!gop.found_existing) {
690 gop.value_ptr.* = object.name;
691 }
692 },690 },
693 .disallowed => {691 .disallowed => {
694 const gop = try disallowed.getOrPut(feature.tag);692 disallowed[@enumToInt(feature.tag)] = value;
695 if (!gop.found_existing) {
696 gop.value_ptr.* = object.name;
697 }
698 },693 },
699 .required => {694 .required => {
700 const gop = try required.getOrPut(feature.tag);695 required[@enumToInt(feature.tag)] = value;
701 if (!gop.found_existing) {696 used[@enumToInt(feature.tag)] = value;
702 gop.value_ptr.* = object.name;
703 }
704 const used_gop = try used.getOrPut(feature.tag);
705 if (!used_gop.found_existing) {
706 used_gop.value_ptr.* = object.name;
707 }
708 },697 },
709 }698 }
710 }699 }
...@@ -713,13 +702,14 @@ fn validateFeatures(...@@ -713,13 +702,14 @@ fn validateFeatures(
713 // when we infer the features, we allow each feature found in the 'used' set702 // when we infer the features, we allow each feature found in the 'used' set
714 // and insert it into the 'allowed' set. When features are not inferred,703 // and insert it into the 'allowed' set. When features are not inferred,
715 // we validate that a used feature is allowed.704 // we validate that a used feature is allowed.
716 if (infer) try allowed.ensureUnusedCapacity(@intCast(u32, used.count()));705 for (used) |used_set, used_index| {
717 for (used.keys()) |used_feature, used_index| {706 const is_enabled = @truncate(u1, used_set) != 0;
718 if (infer) {707 if (infer) {
719 allowed.putAssumeCapacityNoClobber(used_feature, {});708 allowed[used_index] = is_enabled;
720 } else if (!allowed.contains(used_feature)) {709 emit_features_count.* += @boolToInt(is_enabled);
721 log.err("feature '{s}' not allowed, but used by linked object", .{@tagName(used_feature)});710 } else if (is_enabled and !allowed[used_index]) {
722 log.err(" defined in '{s}'", .{used.values()[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});
723 valid_feature_set = false;713 valid_feature_set = false;
724 }714 }
725 }715 }
...@@ -730,27 +720,27 @@ fn validateFeatures(...@@ -730,27 +720,27 @@ fn validateFeatures(
730720
731 // For each linked object, validate the required and disallowed features721 // For each linked object, validate the required and disallowed features
732 for (wasm.objects.items) |object| {722 for (wasm.objects.items) |object| {
733 var object_used_features = std.AutoHashMap(types.Feature.Tag, void).init(arena);723 var object_used_features = [_]bool{false} ** known_features_count;
734 try object_used_features.ensureTotalCapacity(@intCast(u32, object.features.len));
735 for (object.features) |feature| {724 for (object.features) |feature| {
736 if (feature.prefix == .disallowed) continue; // already defined in 'disallowed' set.725 if (feature.prefix == .disallowed) continue; // already defined in 'disallowed' set.
737 // from here a feature is always used726 // from here a feature is always used
738 if (disallowed.get(feature.tag)) |disallowed_object_name| {727 const disallowed_feature = disallowed[@enumToInt(feature.tag)];
739 log.err("feature '{s}' is disallowed, but used by linked object", .{@tagName(feature.tag)});728 if (@truncate(u1, disallowed_feature) != 0) {
740 log.err(" disallowed by '{s}'", .{disallowed_object_name});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});
741 log.err(" used in '{s}'", .{object.name});731 log.err(" used in '{s}'", .{object.name});
742 valid_feature_set = false;732 valid_feature_set = false;
743 }733 }
744734
745 object_used_features.putAssumeCapacity(feature.tag, {});735 object_used_features[@enumToInt(feature.tag)] = true;
746 }736 }
747737
748 // validate the linked object file has each required feature738 // validate the linked object file has each required feature
749 var required_it = required.iterator();739 for (required) |required_feature, feature_index| {
750 while (required_it.next()) |required_feature| {740 const is_required = @truncate(u1, required_feature) != 0;
751 if (!object_used_features.contains(required_feature.key_ptr.*)) {741 if (is_required and !object_used_features[feature_index]) {
752 log.err("feature '{s}' is required but not used in linked object", .{@tagName(required_feature.key_ptr.*)});742 log.err("feature '{s}' is required but not used in linked object", .{(@intToEnum(types.Feature.Tag, feature_index)).toString()});
753 log.err(" required by '{s}'", .{required_feature.value_ptr.*});743 log.err(" required by '{s}'", .{wasm.objects.items[required_feature >> 1].name});
754 log.err(" missing in '{s}'", .{object.name});744 log.err(" missing in '{s}'", .{object.name});
755 valid_feature_set = false;745 valid_feature_set = false;
756 }746 }
...@@ -761,12 +751,7 @@ fn validateFeatures(...@@ -761,12 +751,7 @@ fn validateFeatures(
761 return error.InvalidFeatureSet;751 return error.InvalidFeatureSet;
762 }752 }
763753
764 if (allowed.count() > 0) {754 to_emit.* = allowed;
765 emit_features_count.* = allowed.count();
766 for (to_emit) |*feature_enabled, feature_index| {
767 feature_enabled.* = allowed.contains(@intToEnum(types.Feature.Tag, feature_index));
768 }
769 }
770}755}
771756
772fn checkUndefinedSymbols(wasm: *const Wasm) !void {757fn checkUndefinedSymbols(wasm: *const Wasm) !void {
...@@ -2278,7 +2263,7 @@ pub fn flushModule(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Nod...@@ -2278,7 +2263,7 @@ pub fn flushModule(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Nod
22782263
2279 var emit_features_count: u32 = 0;2264 var emit_features_count: u32 = 0;
2280 var enabled_features: [@typeInfo(types.Feature.Tag).Enum.fields.len]bool = undefined;2265 var enabled_features: [@typeInfo(types.Feature.Tag).Enum.fields.len]bool = undefined;
2281 try wasm.validateFeatures(arena, &enabled_features, &emit_features_count);2266 try wasm.validateFeatures(&enabled_features, &emit_features_count);
2282 try wasm.resolveSymbolsInArchives();2267 try wasm.resolveSymbolsInArchives();
2283 try wasm.checkUndefinedSymbols();2268 try wasm.checkUndefinedSymbols();
22842269
...@@ -2832,7 +2817,7 @@ fn emitFeaturesSection(binary_bytes: *std.ArrayList(u8), enabled_features: []con...@@ -2832,7 +2817,7 @@ fn emitFeaturesSection(binary_bytes: *std.ArrayList(u8), enabled_features: []con
2832 if (enabled) {2817 if (enabled) {
2833 const feature: types.Feature = .{ .prefix = .used, .tag = @intToEnum(types.Feature.Tag, feature_index) };2818 const feature: types.Feature = .{ .prefix = .used, .tag = @intToEnum(types.Feature.Tag, feature_index) };
2834 try leb.writeULEB128(writer, @enumToInt(feature.prefix));2819 try leb.writeULEB128(writer, @enumToInt(feature.prefix));
2835 const string = feature.toString();2820 const string = feature.tag.toString();
2836 try leb.writeULEB128(writer, @intCast(u32, string.len));2821 try leb.writeULEB128(writer, @intCast(u32, string.len));
2837 try writer.writeAll(string);2822 try writer.writeAll(string);
2838 }2823 }
src/link/Wasm/types.zig+19-19
...@@ -203,6 +203,24 @@ pub const Feature = struct {...@@ -203,6 +203,24 @@ pub const Feature = struct {
203 pub fn fromCpuFeature(feature: std.Target.wasm.Feature) Tag {203 pub fn fromCpuFeature(feature: std.Target.wasm.Feature) Tag {
204 return @intToEnum(Tag, @enumToInt(feature));204 return @intToEnum(Tag, @enumToInt(feature));
205 }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 }
206 };224 };
207225
208 pub const Prefix = enum(u8) {226 pub const Prefix = enum(u8) {
...@@ -211,28 +229,10 @@ pub const Feature = struct {...@@ -211,28 +229,10 @@ pub const Feature = struct {
211 required = '=',229 required = '=',
212 };230 };
213231
214 pub fn toString(feature: Feature) []const u8 {
215 return switch (feature.tag) {
216 .atomics => "atomics",
217 .bulk_memory => "bulk-memory",
218 .exception_handling => "exception-handling",
219 .extended_const => "extended-const",
220 .multivalue => "multivalue",
221 .mutable_globals => "mutable-globals",
222 .nontrapping_fptoint => "nontrapping-fptoint",
223 .reference_types => "reference-types",
224 .relaxed_simd => "relaxed-simd",
225 .sign_ext => "sign-ext",
226 .simd128 => "simd128",
227 .tail_call => "tail-call",
228 .shared_mem => "shared-mem",
229 };
230 }
231
232 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 {
233 _ = opt;233 _ = opt;
234 _ = fmt;234 _ = fmt;
235 try writer.print("{c} {s}", .{ feature.prefix, feature.toString() });235 try writer.print("{c} {s}", .{ feature.prefix, feature.tag.toString() });
236 }236 }
237};237};
238238