Skip to content

Commit 8033767

Browse files
authored
wasm-linker: Implement linker tests (#12006)
* test/link: initial wasm support This adds basic parsing and dumping of wasm section so they can be tested using the new linker-test infrastructure. * test/link: all wasm sections parsing and dumping We now parse and dump all sections for the wasm binary format. Currently, this only dumps the name of a custom section. Later this should also dump symbol table, name, linking metadata and relocations. All of those live within the custom sections. * Add wasm linker test This also fixes a parser mistake in reading the flags. * test/link: implement linker tests wasm & fixes Adds several test cases to test the wasm self-hosted linker. This also introduces fixes that were caught during the implementation of those tests. * test-runner: obey omit_stage2 for standalone When a standalone test requires stage2, but stage2 is omit from the compiler, such test case will not be included as part of the test suite that is being ran. This is to support CI's where we omit stage2 to lower the memory usage.
1 parent 7d2e142 commit 8033767

File tree

16 files changed

+519
-9
lines changed

16 files changed

+519
-9
lines changed

build.zig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -481,8 +481,8 @@ pub fn build(b: *Builder) !void {
481481
));
482482

483483
toolchain_step.dependOn(tests.addCompareOutputTests(b, test_filter, modes));
484-
toolchain_step.dependOn(tests.addStandaloneTests(b, test_filter, modes, skip_non_native, enable_macos_sdk, target));
485-
toolchain_step.dependOn(tests.addLinkTests(b, test_filter, modes, enable_macos_sdk));
484+
toolchain_step.dependOn(tests.addStandaloneTests(b, test_filter, modes, skip_non_native, enable_macos_sdk, target, omit_stage2));
485+
toolchain_step.dependOn(tests.addLinkTests(b, test_filter, modes, enable_macos_sdk, omit_stage2));
486486
toolchain_step.dependOn(tests.addStackTraceTests(b, test_filter, modes));
487487
toolchain_step.dependOn(tests.addCliTests(b, test_filter, modes));
488488
toolchain_step.dependOn(tests.addAssembleAndLinkTests(b, test_filter, modes));

ci/azure/macos_script

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ release/bin/zig build test-run-translated-c -Denable-macos-sdk
7676
release/bin/zig build docs -Denable-macos-sdk
7777
release/bin/zig build test-fmt -Denable-macos-sdk
7878
release/bin/zig build test-cases -Denable-macos-sdk -Dsingle-threaded
79-
release/bin/zig build test-link -Denable-macos-sdk
79+
release/bin/zig build test-link -Denable-macos-sdk -Domit-stage2
8080

8181
if [ "${BUILD_REASON}" != "PullRequest" ]; then
8282
mv ../LICENSE release/

lib/std/build.zig

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1618,6 +1618,7 @@ pub const LibExeObjStep = struct {
16181618
want_lto: ?bool = null,
16191619
use_stage1: ?bool = null,
16201620
use_llvm: ?bool = null,
1621+
use_lld: ?bool = null,
16211622
ofmt: ?std.Target.ObjectFormat = null,
16221623

16231624
output_path_source: GeneratedFile,
@@ -2474,6 +2475,14 @@ pub const LibExeObjStep = struct {
24742475
}
24752476
}
24762477

2478+
if (self.use_lld) |use_lld| {
2479+
if (use_lld) {
2480+
try zig_args.append("-fLLD");
2481+
} else {
2482+
try zig_args.append("-fno-LLD");
2483+
}
2484+
}
2485+
24772486
if (self.ofmt) |ofmt| {
24782487
try zig_args.append(try std.fmt.allocPrint(builder.allocator, "-ofmt={s}", .{@tagName(ofmt)}));
24792488
}

lib/std/build/CheckObjectStep.zig

Lines changed: 296 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,10 @@ fn make(step: *Step) !void {
265265
}),
266266
.elf => @panic("TODO elf parser"),
267267
.coff => @panic("TODO coff parser"),
268-
.wasm => @panic("TODO wasm parser"),
268+
.wasm => try WasmDumper.parseAndDump(contents, .{
269+
.gpa = gpa,
270+
.dump_symtab = self.dump_symtab,
271+
}),
269272
else => unreachable,
270273
};
271274

@@ -522,3 +525,295 @@ const MachODumper = struct {
522525
}
523526
}
524527
};
528+
529+
const WasmDumper = struct {
530+
const symtab_label = "symbols";
531+
532+
fn parseAndDump(bytes: []const u8, opts: Opts) ![]const u8 {
533+
const gpa = opts.gpa orelse unreachable; // Wasm dumper requires an allocator
534+
if (opts.dump_symtab) {
535+
@panic("TODO: Implement symbol table parsing and dumping");
536+
}
537+
538+
var fbs = std.io.fixedBufferStream(bytes);
539+
const reader = fbs.reader();
540+
541+
const buf = try reader.readBytesNoEof(8);
542+
if (!mem.eql(u8, buf[0..4], &std.wasm.magic)) {
543+
return error.InvalidMagicByte;
544+
}
545+
if (!mem.eql(u8, buf[4..], &std.wasm.version)) {
546+
return error.UnsupportedWasmVersion;
547+
}
548+
549+
var output = std.ArrayList(u8).init(gpa);
550+
errdefer output.deinit();
551+
const writer = output.writer();
552+
553+
while (reader.readByte()) |current_byte| {
554+
const section = std.meta.intToEnum(std.wasm.Section, current_byte) catch |err| {
555+
std.debug.print("Found invalid section id '{d}'\n", .{current_byte});
556+
return err;
557+
};
558+
559+
const section_length = try std.leb.readULEB128(u32, reader);
560+
try parseAndDumpSection(section, bytes[fbs.pos..][0..section_length], writer);
561+
fbs.pos += section_length;
562+
} else |_| {} // reached end of stream
563+
564+
return output.toOwnedSlice();
565+
}
566+
567+
fn parseAndDumpSection(section: std.wasm.Section, data: []const u8, writer: anytype) !void {
568+
var fbs = std.io.fixedBufferStream(data);
569+
const reader = fbs.reader();
570+
571+
try writer.print(
572+
\\Section {s}
573+
\\size {d}
574+
, .{ @tagName(section), data.len });
575+
576+
switch (section) {
577+
.type,
578+
.import,
579+
.function,
580+
.table,
581+
.memory,
582+
.global,
583+
.@"export",
584+
.element,
585+
.code,
586+
.data,
587+
=> {
588+
const entries = try std.leb.readULEB128(u32, reader);
589+
try writer.print("\nentries {d}\n", .{entries});
590+
try dumpSection(section, data[fbs.pos..], entries, writer);
591+
},
592+
.custom => {
593+
const name_length = try std.leb.readULEB128(u32, reader);
594+
const name = data[fbs.pos..][0..name_length];
595+
fbs.pos += name_length;
596+
try writer.print("\nname {s}\n", .{name});
597+
598+
if (mem.eql(u8, name, "name")) {
599+
try parseDumpNames(reader, writer, data);
600+
}
601+
// TODO: Implement parsing and dumping other custom sections (such as relocations)
602+
},
603+
.start => {
604+
const start = try std.leb.readULEB128(u32, reader);
605+
try writer.print("\nstart {d}\n", .{start});
606+
},
607+
else => {}, // skip unknown sections
608+
}
609+
}
610+
611+
fn dumpSection(section: std.wasm.Section, data: []const u8, entries: u32, writer: anytype) !void {
612+
var fbs = std.io.fixedBufferStream(data);
613+
const reader = fbs.reader();
614+
615+
switch (section) {
616+
.type => {
617+
var i: u32 = 0;
618+
while (i < entries) : (i += 1) {
619+
const func_type = try reader.readByte();
620+
if (func_type != std.wasm.function_type) {
621+
std.debug.print("Expected function type, found byte '{d}'\n", .{func_type});
622+
return error.UnexpectedByte;
623+
}
624+
const params = try std.leb.readULEB128(u32, reader);
625+
try writer.print("params {d}\n", .{params});
626+
var index: u32 = 0;
627+
while (index < params) : (index += 1) {
628+
try parseDumpType(std.wasm.Valtype, reader, writer);
629+
} else index = 0;
630+
const returns = try std.leb.readULEB128(u32, reader);
631+
try writer.print("returns {d}\n", .{returns});
632+
while (index < returns) : (index += 1) {
633+
try parseDumpType(std.wasm.Valtype, reader, writer);
634+
}
635+
}
636+
},
637+
.import => {
638+
var i: u32 = 0;
639+
while (i < entries) : (i += 1) {
640+
const module_name_len = try std.leb.readULEB128(u32, reader);
641+
const module_name = data[fbs.pos..][0..module_name_len];
642+
fbs.pos += module_name_len;
643+
const name_len = try std.leb.readULEB128(u32, reader);
644+
const name = data[fbs.pos..][0..name_len];
645+
fbs.pos += name_len;
646+
647+
const kind = std.meta.intToEnum(std.wasm.ExternalKind, try reader.readByte()) catch |err| {
648+
std.debug.print("Invalid import kind\n", .{});
649+
return err;
650+
};
651+
652+
try writer.print(
653+
\\module {s}
654+
\\name {s}
655+
\\kind {s}
656+
, .{ module_name, name, @tagName(kind) });
657+
try writer.writeByte('\n');
658+
switch (kind) {
659+
.function => {
660+
try writer.print("index {d}\n", .{try std.leb.readULEB128(u32, reader)});
661+
},
662+
.memory => {
663+
try parseDumpLimits(reader, writer);
664+
},
665+
.global => {
666+
try parseDumpType(std.wasm.Valtype, reader, writer);
667+
try writer.print("mutable {}\n", .{0x01 == try std.leb.readULEB128(u32, reader)});
668+
},
669+
.table => {
670+
try parseDumpType(std.wasm.RefType, reader, writer);
671+
try parseDumpLimits(reader, writer);
672+
},
673+
}
674+
}
675+
},
676+
.function => {
677+
var i: u32 = 0;
678+
while (i < entries) : (i += 1) {
679+
try writer.print("index {d}\n", .{try std.leb.readULEB128(u32, reader)});
680+
}
681+
},
682+
.table => {
683+
var i: u32 = 0;
684+
while (i < entries) : (i += 1) {
685+
try parseDumpType(std.wasm.RefType, reader, writer);
686+
try parseDumpLimits(reader, writer);
687+
}
688+
},
689+
.memory => {
690+
var i: u32 = 0;
691+
while (i < entries) : (i += 1) {
692+
try parseDumpLimits(reader, writer);
693+
}
694+
},
695+
.global => {
696+
var i: u32 = 0;
697+
while (i < entries) : (i += 1) {
698+
try parseDumpType(std.wasm.Valtype, reader, writer);
699+
try writer.print("mutable {}\n", .{0x01 == try std.leb.readULEB128(u1, reader)});
700+
try parseDumpInit(reader, writer);
701+
}
702+
},
703+
.@"export" => {
704+
var i: u32 = 0;
705+
while (i < entries) : (i += 1) {
706+
const name_len = try std.leb.readULEB128(u32, reader);
707+
const name = data[fbs.pos..][0..name_len];
708+
fbs.pos += name_len;
709+
const kind_byte = try std.leb.readULEB128(u8, reader);
710+
const kind = std.meta.intToEnum(std.wasm.ExternalKind, kind_byte) catch |err| {
711+
std.debug.print("invalid export kind value '{d}'\n", .{kind_byte});
712+
return err;
713+
};
714+
const index = try std.leb.readULEB128(u32, reader);
715+
try writer.print(
716+
\\name {s}
717+
\\kind {s}
718+
\\index {d}
719+
, .{ name, @tagName(kind), index });
720+
try writer.writeByte('\n');
721+
}
722+
},
723+
.element => {
724+
var i: u32 = 0;
725+
while (i < entries) : (i += 1) {
726+
try writer.print("table index {d}\n", .{try std.leb.readULEB128(u32, reader)});
727+
try parseDumpInit(reader, writer);
728+
729+
const function_indexes = try std.leb.readULEB128(u32, reader);
730+
var function_index: u32 = 0;
731+
try writer.print("indexes {d}\n", .{function_indexes});
732+
while (function_index < function_indexes) : (function_index += 1) {
733+
try writer.print("index {d}\n", .{try std.leb.readULEB128(u32, reader)});
734+
}
735+
}
736+
},
737+
.code => {}, // code section is considered opaque to linker
738+
.data => {
739+
var i: u32 = 0;
740+
while (i < entries) : (i += 1) {
741+
const index = try std.leb.readULEB128(u32, reader);
742+
try writer.print("memory index 0x{x}\n", .{index});
743+
try parseDumpInit(reader, writer);
744+
const size = try std.leb.readULEB128(u32, reader);
745+
try writer.print("size {d}\n", .{size});
746+
try reader.skipBytes(size, .{}); // we do not care about the content of the segments
747+
}
748+
},
749+
else => unreachable,
750+
}
751+
}
752+
753+
fn parseDumpType(comptime WasmType: type, reader: anytype, writer: anytype) !void {
754+
const type_byte = try reader.readByte();
755+
const valtype = std.meta.intToEnum(WasmType, type_byte) catch |err| {
756+
std.debug.print("Invalid wasm type value '{d}'\n", .{type_byte});
757+
return err;
758+
};
759+
try writer.print("type {s}\n", .{@tagName(valtype)});
760+
}
761+
762+
fn parseDumpLimits(reader: anytype, writer: anytype) !void {
763+
const flags = try std.leb.readULEB128(u8, reader);
764+
const min = try std.leb.readULEB128(u32, reader);
765+
766+
try writer.print("min {x}\n", .{min});
767+
if (flags != 0) {
768+
try writer.print("max {x}\n", .{try std.leb.readULEB128(u32, reader)});
769+
}
770+
}
771+
772+
fn parseDumpInit(reader: anytype, writer: anytype) !void {
773+
const byte = try std.leb.readULEB128(u8, reader);
774+
const opcode = std.meta.intToEnum(std.wasm.Opcode, byte) catch |err| {
775+
std.debug.print("invalid wasm opcode '{d}'\n", .{byte});
776+
return err;
777+
};
778+
switch (opcode) {
779+
.i32_const => try writer.print("i32.const {x}\n", .{try std.leb.readILEB128(i32, reader)}),
780+
.i64_const => try writer.print("i64.const {x}\n", .{try std.leb.readILEB128(i64, reader)}),
781+
.f32_const => try writer.print("f32.const {x}\n", .{@bitCast(f32, try reader.readIntLittle(u32))}),
782+
.f64_const => try writer.print("f64.const {x}\n", .{@bitCast(f64, try reader.readIntLittle(u64))}),
783+
.global_get => try writer.print("global.get {x}\n", .{try std.leb.readULEB128(u32, reader)}),
784+
else => unreachable,
785+
}
786+
const end_opcode = try std.leb.readULEB128(u8, reader);
787+
if (end_opcode != std.wasm.opcode(.end)) {
788+
std.debug.print("expected 'end' opcode in init expression\n", .{});
789+
return error.MissingEndOpcode;
790+
}
791+
}
792+
793+
fn parseDumpNames(reader: anytype, writer: anytype, data: []const u8) !void {
794+
while (reader.context.pos < data.len) {
795+
try parseDumpType(std.wasm.NameSubsection, reader, writer);
796+
const size = try std.leb.readULEB128(u32, reader);
797+
const entries = try std.leb.readULEB128(u32, reader);
798+
try writer.print(
799+
\\size {d}
800+
\\names {d}
801+
, .{ size, entries });
802+
try writer.writeByte('\n');
803+
var i: u32 = 0;
804+
while (i < entries) : (i += 1) {
805+
const index = try std.leb.readULEB128(u32, reader);
806+
const name_len = try std.leb.readULEB128(u32, reader);
807+
const pos = reader.context.pos;
808+
const name = data[pos..][0..name_len];
809+
reader.context.pos += name_len;
810+
811+
try writer.print(
812+
\\index {d}
813+
\\name {s}
814+
, .{ index, name });
815+
try writer.writeByte('\n');
816+
}
817+
}
818+
}
819+
};

src/link.zig

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ pub const File = struct {
351351

352352
pub fn makeWritable(base: *File) !void {
353353
switch (base.tag) {
354-
.coff, .elf, .macho, .plan9 => {
354+
.coff, .elf, .macho, .plan9, .wasm => {
355355
if (base.file != null) return;
356356
const emit = base.options.emit orelse return;
357357
base.file = try emit.directory.handle.createFile(emit.sub_path, .{
@@ -360,7 +360,7 @@ pub const File = struct {
360360
.mode = determineMode(base.options),
361361
});
362362
},
363-
.c, .wasm, .spirv, .nvptx => {},
363+
.c, .spirv, .nvptx => {},
364364
}
365365
}
366366

@@ -394,7 +394,7 @@ pub const File = struct {
394394
base.file = null;
395395
}
396396
},
397-
.coff, .elf, .plan9 => if (base.file) |f| {
397+
.coff, .elf, .plan9, .wasm => if (base.file) |f| {
398398
if (base.intermediary_basename != null) {
399399
// The file we have open is not the final file that we want to
400400
// make executable, so we don't have to close it.
@@ -403,7 +403,7 @@ pub const File = struct {
403403
f.close();
404404
base.file = null;
405405
},
406-
.c, .wasm, .spirv, .nvptx => {},
406+
.c, .spirv, .nvptx => {},
407407
}
408408
}
409409

0 commit comments

Comments
 (0)