From 9932372fae88a6dabdb3945d55556e373bd4bb45 Mon Sep 17 00:00:00 2001 From: Luuk de Gram Date: Sat, 31 Dec 2022 15:27:24 +0100 Subject: [PATCH 1/8] wasm-linker: support export flags Adds support for both the `-rdynamic` and the `--export=` flags. Support is added to both the incremental linker as well as the traditional linker (zld). --- src/link/Wasm.zig | 29 ++++++++++++++++++++++++++++- src/link/Wasm/Symbol.zig | 8 +++----- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index 9beb40e418..d0a3e83aa7 100644 --- a/src/link/Wasm.zig +++ b/src/link/Wasm.zig @@ -1800,9 +1800,36 @@ fn setupExports(wasm: *Wasm) !void { if (wasm.base.options.output_mode == .Obj) return; log.debug("Building exports from symbols", .{}); + const force_exp_names = wasm.base.options.export_symbol_names; + if (force_exp_names.len > 0) { + var failed_exports = try std.ArrayList([]const u8).initCapacity(wasm.base.allocator, force_exp_names.len); + defer failed_exports.deinit(); + + for (force_exp_names) |exp_name| { + const name_index = wasm.string_table.getOffset(exp_name) orelse { + failed_exports.appendAssumeCapacity(exp_name); + continue; + }; + const loc = wasm.globals.get(name_index) orelse { + failed_exports.appendAssumeCapacity(exp_name); + continue; + }; + + const symbol = loc.getSymbol(wasm); + symbol.setFlag(.WASM_SYM_EXPORTED); + } + + if (failed_exports.items.len > 0) { + for (failed_exports.items) |exp_name| { + log.err("could not export '{s}', symbol not found", .{exp_name}); + } + return error.MissingSymbol; + } + } + for (wasm.resolved_symbols.keys()) |sym_loc| { const symbol = sym_loc.getSymbol(wasm); - if (!symbol.isExported()) continue; + if (!symbol.isExported(wasm.base.options.rdynamic)) continue; const sym_name = sym_loc.getName(wasm); const export_name = if (wasm.export_names.get(sym_loc)) |name| name else blk: { diff --git a/src/link/Wasm/Symbol.zig b/src/link/Wasm/Symbol.zig index 365f4a2c0b..089eee289e 100644 --- a/src/link/Wasm/Symbol.zig +++ b/src/link/Wasm/Symbol.zig @@ -139,12 +139,10 @@ pub fn isNoStrip(symbol: Symbol) bool { return symbol.flags & @enumToInt(Flag.WASM_SYM_NO_STRIP) != 0; } -pub fn isExported(symbol: Symbol) bool { +pub fn isExported(symbol: Symbol, is_dynamic: bool) bool { if (symbol.isUndefined() or symbol.isLocal()) return false; - if (symbol.isHidden()) return false; - if (symbol.hasFlag(.WASM_SYM_EXPORTED)) return true; - if (symbol.hasFlag(.WASM_SYM_BINDING_WEAK)) return false; - return true; + if (is_dynamic and symbol.isVisible()) return true; + return symbol.hasFlag(.WASM_SYM_EXPORTED); } pub fn isWeak(symbol: Symbol) bool { From e4869eeac11eccdbb6cf4d8a4e60a272ca179d55 Mon Sep 17 00:00:00 2001 From: Luuk de Gram Date: Sat, 31 Dec 2022 15:29:02 +0100 Subject: [PATCH 2/8] test/link: linker tests for all export cases Adds a linker test case for each possible export case. This means one where no exports are done (i.e. no flags set), when the -dynamic flag is set, and finally when --export= flag(s) are set. --- test/link.zig | 5 ++++ test/link/wasm/bss/build.zig | 3 +-- test/link/wasm/export/build.zig | 48 +++++++++++++++++++++++++++++++++ test/link/wasm/export/main.zig | 1 + 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 test/link/wasm/export/build.zig create mode 100644 test/link/wasm/export/main.zig diff --git a/test/link.zig b/test/link.zig index ff18d65be5..e30e0cf0ae 100644 --- a/test/link.zig +++ b/test/link.zig @@ -42,6 +42,11 @@ fn addWasmCases(cases: *tests.StandaloneContext) void { .requires_stage2 = true, }); + cases.addBuildFile("test/link/wasm/export/build.zig", .{ + .build_modes = true, + .requires_stage2 = true, + }); + cases.addBuildFile("test/link/wasm/extern/build.zig", .{ .build_modes = true, .requires_stage2 = true, diff --git a/test/link/wasm/bss/build.zig b/test/link/wasm/bss/build.zig index c9bc1aa106..e234a3f402 100644 --- a/test/link/wasm/bss/build.zig +++ b/test/link/wasm/bss/build.zig @@ -26,8 +26,7 @@ pub fn build(b: *Builder) void { check_lib.checkNext("name memory"); // as per linker specification // since we are importing memory, ensure it's not exported - check_lib.checkStart("Section export"); - check_lib.checkNext("entries 1"); // we're exporting function 'foo' so only 1 entry + check_lib.checkNotPresent("Section export"); // validate the name of the stack pointer check_lib.checkStart("Section custom"); diff --git a/test/link/wasm/export/build.zig b/test/link/wasm/export/build.zig new file mode 100644 index 0000000000..181e77e296 --- /dev/null +++ b/test/link/wasm/export/build.zig @@ -0,0 +1,48 @@ +const std = @import("std"); + +pub fn build(b: *std.build.Builder) void { + const mode = b.standardReleaseOptions(); + + const no_export = b.addSharedLibrary("no-export", "main.zig", .unversioned); + no_export.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding }); + no_export.setBuildMode(mode); + no_export.use_llvm = false; + no_export.use_lld = false; + + const dynamic_export = b.addSharedLibrary("dynamic", "main.zig", .unversioned); + dynamic_export.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding }); + dynamic_export.setBuildMode(mode); + dynamic_export.rdynamic = true; + dynamic_export.use_llvm = false; + dynamic_export.use_lld = false; + + const force_export = b.addSharedLibrary("force", "main.zig", .unversioned); + force_export.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding }); + force_export.setBuildMode(mode); + force_export.export_symbol_names = &.{"foo"}; + force_export.use_llvm = false; + force_export.use_lld = false; + + const check_no_export = no_export.checkObject(.wasm); + check_no_export.checkStart("Section export"); + check_no_export.checkNext("entries 1"); + check_no_export.checkNext("name memory"); + check_no_export.checkNext("kind memory"); + + const check_dynamic_export = dynamic_export.checkObject(.wasm); + check_dynamic_export.checkStart("Section export"); + check_dynamic_export.checkNext("entries 2"); + check_dynamic_export.checkNext("name foo"); + check_dynamic_export.checkNext("kind function"); + + const check_force_export = force_export.checkObject(.wasm); + check_force_export.checkStart("Section export"); + check_force_export.checkNext("entries 2"); + check_force_export.checkNext("name foo"); + check_force_export.checkNext("kind function"); + + const test_step = b.step("test", "Run linker test"); + test_step.dependOn(&check_no_export.step); + test_step.dependOn(&check_dynamic_export.step); + test_step.dependOn(&check_force_export.step); +} diff --git a/test/link/wasm/export/main.zig b/test/link/wasm/export/main.zig new file mode 100644 index 0000000000..0e416dbf18 --- /dev/null +++ b/test/link/wasm/export/main.zig @@ -0,0 +1 @@ +export fn foo() void {} From 3ca3fe94f454688f05415cf0a39954a0379b4c84 Mon Sep 17 00:00:00 2001 From: Luuk de Gram Date: Sat, 31 Dec 2022 16:58:49 +0100 Subject: [PATCH 3/8] wasm-linker: improve indirect function table Rather than checking for function pointers during the writing phase, we now create a synethtic symbol when a new link job has started. This means the symbol can correctly be resolved during link time with the indirect function table from other object files, ensuring we are properly performing relocations and our binary writer is now unaware of any of its logic and simply emits the table according to the symbol such as any other symbols. --- src/link/Wasm.zig | 192 +++++++++++++++++++++++++--------------------- 1 file changed, 106 insertions(+), 86 deletions(-) diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index d0a3e83aa7..d3b80a350c 100644 --- a/src/link/Wasm.zig +++ b/src/link/Wasm.zig @@ -335,41 +335,64 @@ pub fn openPath(allocator: Allocator, sub_path: []const u8, options: link.Option wasm_bin.base.file = file; wasm_bin.name = sub_path; - // As sym_index '0' is reserved, we use it for our stack pointer symbol - const sym_name = try wasm_bin.string_table.put(allocator, "__stack_pointer"); - const symbol = try wasm_bin.symbols.addOne(allocator); - symbol.* = .{ - .name = sym_name, - .tag = .global, - .flags = 0, - .index = 0, - }; - const loc: SymbolLoc = .{ .file = null, .index = 0 }; - try wasm_bin.resolved_symbols.putNoClobber(allocator, loc, {}); - try wasm_bin.globals.putNoClobber(allocator, sym_name, loc); + // create stack pointer symbol + { + const loc = try wasm_bin.createSyntheticSymbol("__stack_pointer", .global); + const symbol = loc.getSymbol(wasm_bin); + // For object files we will import the stack pointer symbol + if (options.output_mode == .Obj) { + symbol.setUndefined(true); + symbol.index = @intCast(u32, wasm_bin.imported_globals_count); + wasm_bin.imported_globals_count += 1; + try wasm_bin.imports.putNoClobber( + allocator, + loc, + .{ + .module_name = try wasm_bin.string_table.put(allocator, wasm_bin.host_name), + .name = symbol.name, + .kind = .{ .global = .{ .valtype = .i32, .mutable = true } }, + }, + ); + } else { + symbol.index = @intCast(u32, wasm_bin.imported_globals_count + wasm_bin.wasm_globals.items.len); + symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN); + const global = try wasm_bin.wasm_globals.addOne(allocator); + global.* = .{ + .global_type = .{ + .valtype = .i32, + .mutable = true, + }, + .init = .{ .i32_const = 0 }, + }; + } + } - // For object files we will import the stack pointer symbol - if (options.output_mode == .Obj) { - symbol.setUndefined(true); - try wasm_bin.imports.putNoClobber( - allocator, - .{ .file = null, .index = 0 }, - .{ - .module_name = try wasm_bin.string_table.put(allocator, wasm_bin.host_name), - .name = sym_name, - .kind = .{ .global = .{ .valtype = .i32, .mutable = true } }, - }, - ); - } else { - symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN); - const global = try wasm_bin.wasm_globals.addOne(allocator); - global.* = .{ - .global_type = .{ - .valtype = .i32, - .mutable = true, - }, - .init = .{ .i32_const = 0 }, + // create indirect function pointer symbol + { + const loc = try wasm_bin.createSyntheticSymbol("__indirect_function_table", .table); + const symbol = loc.getSymbol(wasm_bin); + const table: std.wasm.Table = .{ + .limits = .{ .min = 0, .max = null }, // will be overwritten during `mapFunctionTable` + .reftype = .funcref, }; + if (options.output_mode == .Obj or options.import_table) { + symbol.setUndefined(true); + symbol.index = @intCast(u32, wasm_bin.imported_tables_count); + wasm_bin.imported_tables_count += 1; + try wasm_bin.imports.put(allocator, loc, .{ + .module_name = try wasm_bin.string_table.put(allocator, wasm_bin.host_name), + .name = symbol.name, + .kind = .{ .table = table }, + }); + } else { + symbol.index = @intCast(u32, wasm_bin.imported_tables_count + wasm_bin.tables.items.len); + try wasm_bin.tables.append(allocator, table); + if (options.export_table) { + symbol.setFlag(.WASM_SYM_EXPORTED); + } else { + symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN); + } + } } if (!options.strip and options.module != null) { @@ -400,6 +423,22 @@ pub fn createEmpty(gpa: Allocator, options: link.Options) !*Wasm { return wasm; } +/// For a given name, creates a new global synthetic symbol. +/// Leaves index undefined and the default flags (0). +fn createSyntheticSymbol(wasm: *Wasm, name: []const u8, tag: Symbol.Tag) !SymbolLoc { + const name_offset = try wasm.string_table.put(wasm.base.allocator, name); + const sym_index = @intCast(u32, wasm.symbols.items.len); + const loc: SymbolLoc = .{ .index = sym_index, .file = null }; + try wasm.symbols.append(wasm.base.allocator, .{ + .name = name_offset, + .flags = 0, + .tag = tag, + .index = undefined, + }); + try wasm.resolved_symbols.putNoClobber(wasm.base.allocator, loc, {}); + try wasm.globals.putNoClobber(wasm.base.allocator, name_offset, loc); + return loc; +} /// Initializes symbols and atoms for the debug sections /// Initialization is only done when compiling Zig code. /// When Zig is invoked as a linker instead, the atoms @@ -1249,7 +1288,7 @@ pub fn updateDeclExports( const existing_sym: Symbol = existing_loc.getSymbol(wasm).*; const exp_is_weak = exp.options.linkage == .Internal or exp.options.linkage == .Weak; - // When both the to-bo-exported symbol and the already existing symbol + // When both the to-be-exported symbol and the already existing symbol // are strong symbols, we have a linker error. // In the other case we replace one with the other. if (!exp_is_weak and !existing_sym.isWeak()) { @@ -1361,6 +1400,19 @@ fn mapFunctionTable(wasm: *Wasm) void { while (it.next()) |value_ptr| : (index += 1) { value_ptr.* = index; } + + if (wasm.base.options.import_table or wasm.base.options.output_mode == .Obj) { + const sym_loc = wasm.globals.get(wasm.string_table.getOffset("__indirect_function_table").?).?; + const import = wasm.imports.getPtr(sym_loc).?; + import.kind.table.limits.min = index - 1; // we start at index 1. + } else if (index > 1) { + log.debug("Appending indirect function table", .{}); + const offset = wasm.string_table.getOffset("__indirect_function_table").?; + const sym_with_loc = wasm.globals.get(offset).?; + const symbol = sym_with_loc.getSymbol(wasm); + const table = &wasm.tables.items[symbol.index - wasm.imported_tables_count]; + table.limits = .{ .min = index, .max = index }; + } } /// Either creates a new import, or updates one if existing. @@ -1700,17 +1752,6 @@ fn setupImports(wasm: *Wasm) !void { /// Takes the global, function and table section from each linked object file /// and merges it into a single section for each. fn mergeSections(wasm: *Wasm) !void { - // append the indirect function table if initialized - if (wasm.string_table.getOffset("__indirect_function_table")) |offset| { - const sym_loc = wasm.globals.get(offset).?; - const table: std.wasm.Table = .{ - .limits = .{ .min = @intCast(u32, wasm.function_table.count()), .max = null }, - .reftype = .funcref, - }; - sym_loc.getSymbol(wasm).index = @intCast(u32, wasm.tables.items.len) + wasm.imported_tables_count; - try wasm.tables.append(wasm.base.allocator, table); - } - for (wasm.resolved_symbols.keys()) |sym_loc| { if (sym_loc.file == null) { // Zig code-generated symbols are already within the sections and do not @@ -2613,28 +2654,9 @@ fn writeToFile( // Import section const import_memory = wasm.base.options.import_memory or is_obj; - const import_table = wasm.base.options.import_table or is_obj; - if (wasm.imports.count() != 0 or import_memory or import_table) { + if (wasm.imports.count() != 0 or import_memory) { const header_offset = try reserveVecSectionHeader(&binary_bytes); - // import table is always first table so emit that first - if (import_table) { - const table_imp: types.Import = .{ - .module_name = try wasm.string_table.put(wasm.base.allocator, wasm.host_name), - .name = try wasm.string_table.put(wasm.base.allocator, "__indirect_function_table"), - .kind = .{ - .table = .{ - .limits = .{ - .min = @intCast(u32, wasm.function_table.count()), - .max = null, - }, - .reftype = .funcref, - }, - }, - }; - try wasm.emitImport(binary_writer, table_imp); - } - var it = wasm.imports.iterator(); while (it.next()) |entry| { assert(entry.key_ptr.*.getSymbol(wasm).isUndefined()); @@ -2657,7 +2679,7 @@ fn writeToFile( header_offset, .import, @intCast(u32, binary_bytes.items.len - header_offset - header_size), - @intCast(u32, wasm.imports.count() + @boolToInt(import_memory) + @boolToInt(import_table)), + @intCast(u32, wasm.imports.count() + @boolToInt(import_memory)), ); section_count += 1; } @@ -2680,22 +2702,20 @@ fn writeToFile( } // Table section - const export_table = wasm.base.options.export_table; - if (!import_table and wasm.function_table.count() != 0) { + if (wasm.tables.items.len > 0) { const header_offset = try reserveVecSectionHeader(&binary_bytes); - try leb.writeULEB128(binary_writer, std.wasm.reftype(.funcref)); - try emitLimits(binary_writer, .{ - .min = @intCast(u32, wasm.function_table.count()) + 1, - .max = null, - }); + for (wasm.tables.items) |table| { + try leb.writeULEB128(binary_writer, std.wasm.reftype(table.reftype)); + try emitLimits(binary_writer, table.limits); + } try writeVecSectionHeader( binary_bytes.items, header_offset, .table, @intCast(u32, binary_bytes.items.len - header_offset - header_size), - @as(u32, 1), + @intCast(u32, wasm.tables.items.len), ); section_count += 1; } @@ -2748,7 +2768,7 @@ fn writeToFile( } // Export section - if (wasm.exports.items.len != 0 or export_table or !import_memory) { + if (wasm.exports.items.len != 0 or !import_memory) { const header_offset = try reserveVecSectionHeader(&binary_bytes); for (wasm.exports.items) |exp| { @@ -2759,13 +2779,6 @@ fn writeToFile( try leb.writeULEB128(binary_writer, exp.index); } - if (export_table) { - try leb.writeULEB128(binary_writer, @intCast(u32, "__indirect_function_table".len)); - try binary_writer.writeAll("__indirect_function_table"); - try binary_writer.writeByte(std.wasm.externalKind(.table)); - try leb.writeULEB128(binary_writer, @as(u32, 0)); // function table is always the first table - } - if (!import_memory) { try leb.writeULEB128(binary_writer, @intCast(u32, "memory".len)); try binary_writer.writeAll("memory"); @@ -2778,7 +2791,7 @@ fn writeToFile( header_offset, .@"export", @intCast(u32, binary_bytes.items.len - header_offset - header_size), - @intCast(u32, wasm.exports.items.len) + @boolToInt(export_table) + @boolToInt(!import_memory), + @intCast(u32, wasm.exports.items.len) + @boolToInt(!import_memory), ); section_count += 1; } @@ -2787,11 +2800,18 @@ fn writeToFile( if (wasm.function_table.count() > 0) { const header_offset = try reserveVecSectionHeader(&binary_bytes); - var flags: u32 = 0x2; // Yes we have a table + const table_loc = wasm.globals.get(wasm.string_table.getOffset("__indirect_function_table").?).?; + const table_sym = table_loc.getSymbol(wasm); + + var flags: u32 = if (table_sym.index == 0) 0x0 else 0x02; // passive with implicit 0-index table or set table index manually try leb.writeULEB128(binary_writer, flags); - try leb.writeULEB128(binary_writer, @as(u32, 0)); // index of that table. TODO: Store synthetic symbols + if (flags == 0x02) { + try leb.writeULEB128(binary_writer, table_sym.index); + } try emitInit(binary_writer, .{ .i32_const = 1 }); // We start at index 1, so unresolved function pointers are invalid - try leb.writeULEB128(binary_writer, @as(u8, 0)); + if (flags == 0x02) { + try leb.writeULEB128(binary_writer, @as(u8, 0)); // represents funcref + } try leb.writeULEB128(binary_writer, @intCast(u32, wasm.function_table.count())); var symbol_it = wasm.function_table.keyIterator(); while (symbol_it.next()) |symbol_loc_ptr| { From 3e32a189566b191f1cfe01e911fed941e3e38603 Mon Sep 17 00:00:00 2001 From: Luuk de Gram Date: Sun, 1 Jan 2023 14:50:19 +0100 Subject: [PATCH 4/8] test/link: add test case for function table Adds 3 linker tests to verify the indirect function table functionality for importing, exporting and its regular definitions. --- test/link.zig | 5 ++ test/link/wasm/function-table/build.zig | 63 +++++++++++++++++++++++++ test/link/wasm/function-table/lib.zig | 7 +++ 3 files changed, 75 insertions(+) create mode 100644 test/link/wasm/function-table/build.zig create mode 100644 test/link/wasm/function-table/lib.zig diff --git a/test/link.zig b/test/link.zig index e30e0cf0ae..124a30a3b6 100644 --- a/test/link.zig +++ b/test/link.zig @@ -58,6 +58,11 @@ fn addWasmCases(cases: *tests.StandaloneContext) void { .requires_stage2 = true, }); + cases.addBuildFile("test/link/wasm/function-table/build.zig", .{ + .build_modes = true, + .requires_stage2 = true, + }); + cases.addBuildFile("test/link/wasm/infer-features/build.zig", .{ .requires_stage2 = true, }); diff --git a/test/link/wasm/function-table/build.zig b/test/link/wasm/function-table/build.zig new file mode 100644 index 0000000000..f7572bd6b1 --- /dev/null +++ b/test/link/wasm/function-table/build.zig @@ -0,0 +1,63 @@ +const std = @import("std"); +const Builder = std.build.Builder; + +pub fn build(b: *Builder) void { + const mode = b.standardReleaseOptions(); + + const test_step = b.step("test", "Test"); + test_step.dependOn(b.getInstallStep()); + + const import_table = b.addSharedLibrary("lib", "lib.zig", .unversioned); + import_table.setBuildMode(mode); + import_table.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding }); + import_table.use_llvm = false; + import_table.use_lld = false; + import_table.import_table = true; + + const export_table = b.addSharedLibrary("lib", "lib.zig", .unversioned); + export_table.setBuildMode(mode); + export_table.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding }); + export_table.use_llvm = false; + export_table.use_lld = false; + export_table.export_table = true; + + const regular_table = b.addSharedLibrary("lib", "lib.zig", .unversioned); + regular_table.setBuildMode(mode); + regular_table.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding }); + regular_table.use_llvm = false; + regular_table.use_lld = false; + + const check_import = import_table.checkObject(.wasm); + const check_export = export_table.checkObject(.wasm); + const check_regular = regular_table.checkObject(.wasm); + + check_import.checkStart("Section import"); + check_import.checkNext("entries 1"); + check_import.checkNext("module env"); + check_import.checkNext("name __indirect_function_table"); + check_import.checkNext("kind table"); + check_import.checkNext("type funcref"); + check_import.checkNext("min 1"); // 1 function pointer + check_import.checkNotPresent("max"); // when importing, we do not provide a max + check_import.checkNotPresent("Section table"); // we're importing it + + check_export.checkStart("Section export"); + check_export.checkNext("entries 2"); + check_export.checkNext("name __indirect_function_table"); // as per linker specification + check_export.checkNext("kind table"); + + check_regular.checkStart("Section table"); + check_regular.checkNext("entries 1"); + check_regular.checkNext("type funcref"); + check_regular.checkNext("min 2"); // index starts at 1 & 1 function pointer = 2. + check_regular.checkNext("max 2"); + check_regular.checkStart("Section element"); + check_regular.checkNext("entries 1"); + check_regular.checkNext("table index 0"); + check_regular.checkNext("i32.const 1"); // we want to start function indexes at 1 + check_regular.checkNext("indexes 1"); // 1 function pointer + + test_step.dependOn(&check_import.step); + test_step.dependOn(&check_export.step); + test_step.dependOn(&check_regular.step); +} diff --git a/test/link/wasm/function-table/lib.zig b/test/link/wasm/function-table/lib.zig new file mode 100644 index 0000000000..ed7a85b2db --- /dev/null +++ b/test/link/wasm/function-table/lib.zig @@ -0,0 +1,7 @@ +var func: *const fn () void = &bar; + +export fn foo() void { + func(); +} + +fn bar() void {} From 86ed96d9336c06668a727893e41856b2e4fe3d23 Mon Sep 17 00:00:00 2001 From: Luuk de Gram Date: Sun, 1 Jan 2023 15:48:00 +0100 Subject: [PATCH 5/8] wasm-linker: check for undefined symbols Unless the `--import-symbols` flag is set, in which case we don't check for any undefined data symbols. --- src/link/Wasm.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index d3b80a350c..42cc2883e2 100644 --- a/src/link/Wasm.zig +++ b/src/link/Wasm.zig @@ -805,6 +805,7 @@ fn validateFeatures( fn checkUndefinedSymbols(wasm: *const Wasm) !void { if (wasm.base.options.output_mode == .Obj) return; + if (wasm.base.options.import_symbols) return; var found_undefined_symbols = false; for (wasm.undefs.values()) |undef| { @@ -2467,6 +2468,7 @@ fn linkWithZld(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Node) l var enabled_features: [@typeInfo(types.Feature.Tag).Enum.fields.len]bool = undefined; try wasm.validateFeatures(&enabled_features, &emit_features_count); try wasm.resolveSymbolsInArchives(); + try wasm.checkUndefinedSymbols(); try wasm.setupStart(); try wasm.setupImports(); From e475ddb08e4bb3bacbc50bdd26fa83f579783dbb Mon Sep 17 00:00:00 2001 From: Luuk de Gram Date: Sun, 1 Jan 2023 17:10:51 +0100 Subject: [PATCH 6/8] wasm-linker: export symbols by virtual address When exporting a data symbol, generate a regular global and use the data symbol's virtual addres as the value (init) of the global. --- src/link/Wasm.zig | 26 ++++++++------------------ src/link/Wasm/Atom.zig | 42 ++++++++++++++++++------------------------ 2 files changed, 26 insertions(+), 42 deletions(-) diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index 42cc2883e2..e03c8b1ba0 100644 --- a/src/link/Wasm.zig +++ b/src/link/Wasm.zig @@ -112,8 +112,6 @@ func_types: std.ArrayListUnmanaged(std.wasm.Type) = .{}, functions: std.AutoArrayHashMapUnmanaged(struct { file: ?u16, index: u32 }, std.wasm.Func) = .{}, /// Output global section wasm_globals: std.ArrayListUnmanaged(std.wasm.Global) = .{}, -/// Global symbols for exported data symbols -address_globals: std.ArrayListUnmanaged(SymbolLoc) = .{}, /// Memory section memories: std.wasm.Memory = .{ .limits = .{ .min = 0, .max = null } }, /// Output table section @@ -880,7 +878,6 @@ pub fn deinit(wasm: *Wasm) void { wasm.func_types.deinit(gpa); wasm.functions.deinit(gpa); wasm.wasm_globals.deinit(gpa); - wasm.address_globals.deinit(gpa); wasm.function_table.deinit(gpa); wasm.tables.deinit(gpa); wasm.exports.deinit(gpa); @@ -1879,8 +1876,13 @@ fn setupExports(wasm: *Wasm) !void { break :blk try wasm.string_table.put(wasm.base.allocator, sym_name); }; const exp: types.Export = if (symbol.tag == .data) exp: { - const global_index = @intCast(u32, wasm.wasm_globals.items.len + wasm.address_globals.items.len); - try wasm.address_globals.append(wasm.base.allocator, sym_loc); + const atom = wasm.symbol_atom.get(sym_loc).?; + const va = atom.getVA(wasm, symbol); + const global_index = @intCast(u32, wasm.imported_globals_count + wasm.wasm_globals.items.len); + try wasm.wasm_globals.append(wasm.base.allocator, .{ + .global_type = .{ .valtype = .i32, .mutable = false }, + .init = .{ .i32_const = @intCast(i32, va) }, + }); break :exp .{ .name = export_name, .kind = .global, @@ -2741,22 +2743,10 @@ fn writeToFile( if (wasm.wasm_globals.items.len > 0) { const header_offset = try reserveVecSectionHeader(&binary_bytes); - var global_count: u32 = 0; for (wasm.wasm_globals.items) |global| { try binary_writer.writeByte(std.wasm.valtype(global.global_type.valtype)); try binary_writer.writeByte(@boolToInt(global.global_type.mutable)); try emitInit(binary_writer, global.init); - global_count += 1; - } - - for (wasm.address_globals.items) |sym_loc| { - const atom = wasm.symbol_atom.get(sym_loc).?; - try binary_writer.writeByte(std.wasm.valtype(.i32)); - try binary_writer.writeByte(0); // immutable - try emitInit(binary_writer, .{ - .i32_const = @bitCast(i32, atom.offset), - }); - global_count += 1; } try writeVecSectionHeader( @@ -2764,7 +2754,7 @@ fn writeToFile( header_offset, .global, @intCast(u32, binary_bytes.items.len - header_offset - header_size), - @intCast(u32, global_count), + @intCast(u32, wasm.wasm_globals.items.len), ); section_count += 1; } diff --git a/src/link/Wasm/Atom.zig b/src/link/Wasm/Atom.zig index eb3a31b8a0..de9cefebdc 100644 --- a/src/link/Wasm/Atom.zig +++ b/src/link/Wasm/Atom.zig @@ -90,24 +90,26 @@ pub fn getFirst(atom: *Atom) *Atom { return tmp; } -/// Unlike `getFirst` this returns the first `*Atom` that was -/// produced from Zig code, rather than an object file. -/// This is useful for debug sections where we want to extend -/// the bytes, and don't want to overwrite existing Atoms. -pub fn getFirstZigAtom(atom: *Atom) *Atom { - if (atom.file == null) return atom; - var tmp = atom; - return while (tmp.prev) |prev| { - if (prev.file == null) break prev; - tmp = prev; - } else unreachable; // must allocate an Atom first! -} - /// Returns the location of the symbol that represents this `Atom` pub fn symbolLoc(atom: Atom) Wasm.SymbolLoc { return .{ .file = atom.file, .index = atom.sym_index }; } +/// Returns the virtual address of the `Atom`. This is the address starting +/// from the first entry within a section. +pub fn getVA(atom: Atom, wasm: *const Wasm, symbol: *const Symbol) u32 { + if (symbol.tag == .function) return atom.offset; + std.debug.assert(symbol.tag == .data); + const merge_segment = wasm.base.options.output_mode != .Obj; + const segment_info = if (atom.file) |object_index| blk: { + break :blk wasm.objects.items[object_index].segment_info; + } else wasm.segment_info.values(); + const segment_name = segment_info[symbol.index].outputName(merge_segment); + const segment_index = wasm.data_segments.get(segment_name).?; + const segment = wasm.segments.items[segment_index]; + return segment.offset + atom.offset; +} + /// Resolves the relocations within the atom, writing the new value /// at the calculated offset. pub fn resolveRelocs(atom: *Atom, wasm_bin: *const Wasm) void { @@ -159,7 +161,7 @@ pub fn resolveRelocs(atom: *Atom, wasm_bin: *const Wasm) void { /// The final value must be casted to the correct size. fn relocationValue(atom: Atom, relocation: types.Relocation, wasm_bin: *const Wasm) u64 { const target_loc = (Wasm.SymbolLoc{ .file = atom.file, .index = relocation.index }).finalLoc(wasm_bin); - const symbol = target_loc.getSymbol(wasm_bin).*; + const symbol = target_loc.getSymbol(wasm_bin); switch (relocation.relocation_type) { .R_WASM_FUNCTION_INDEX_LEB => return symbol.index, .R_WASM_TABLE_NUMBER_LEB => return symbol.index, @@ -190,17 +192,9 @@ fn relocationValue(atom: Atom, relocation: types.Relocation, wasm_bin: *const Wa if (symbol.isUndefined()) { return 0; } - - const merge_segment = wasm_bin.base.options.output_mode != .Obj; const target_atom = wasm_bin.symbol_atom.get(target_loc).?; - const segment_info = if (target_atom.file) |object_index| blk: { - break :blk wasm_bin.objects.items[object_index].segment_info; - } else wasm_bin.segment_info.values(); - const segment_name = segment_info[symbol.index].outputName(merge_segment); - const segment_index = wasm_bin.data_segments.get(segment_name).?; - const segment = wasm_bin.segments.items[segment_index]; - const rel_value = @intCast(i32, target_atom.offset + segment.offset) + relocation.addend; - return @intCast(u32, rel_value); + const va = @intCast(i32, target_atom.getVA(wasm_bin, symbol)); + return @intCast(u32, va + relocation.addend); }, .R_WASM_EVENT_INDEX_LEB => return symbol.index, .R_WASM_SECTION_OFFSET_I32 => { From f9b3e8c762450c20a13c3f51ab91398f409781fb Mon Sep 17 00:00:00 2001 From: Luuk de Gram Date: Sun, 1 Jan 2023 17:13:12 +0100 Subject: [PATCH 7/8] test/link: add test case for exporting data syms --- test/link.zig | 4 +++ test/link/wasm/export-data/build.zig | 41 ++++++++++++++++++++++++++++ test/link/wasm/export-data/lib.zig | 2 ++ 3 files changed, 47 insertions(+) create mode 100644 test/link/wasm/export-data/build.zig create mode 100644 test/link/wasm/export-data/lib.zig diff --git a/test/link.zig b/test/link.zig index 124a30a3b6..2fe62b8068 100644 --- a/test/link.zig +++ b/test/link.zig @@ -47,6 +47,10 @@ fn addWasmCases(cases: *tests.StandaloneContext) void { .requires_stage2 = true, }); + cases.addBuildFile("test/link/wasm/export-data/build.zig", .{ + .build_modes = true, + }); + cases.addBuildFile("test/link/wasm/extern/build.zig", .{ .build_modes = true, .requires_stage2 = true, diff --git a/test/link/wasm/export-data/build.zig b/test/link/wasm/export-data/build.zig new file mode 100644 index 0000000000..3df5c1790c --- /dev/null +++ b/test/link/wasm/export-data/build.zig @@ -0,0 +1,41 @@ +const std = @import("std"); +const Builder = std.build.Builder; + +pub fn build(b: *Builder) void { + const mode = b.standardReleaseOptions(); + + const test_step = b.step("test", "Test"); + test_step.dependOn(b.getInstallStep()); + + const lib = b.addSharedLibrary("lib", "lib.zig", .unversioned); + lib.setBuildMode(mode); + lib.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding }); + lib.use_lld = false; + lib.export_symbol_names = &.{ "foo", "bar" }; + lib.global_base = 0; // put data section at address 0 to make data symbols easier to parse + + const check_lib = lib.checkObject(.wasm); + + check_lib.checkStart("Section global"); + check_lib.checkNext("entries 3"); + check_lib.checkNext("type i32"); // stack pointer so skip other fields + check_lib.checkNext("type i32"); + check_lib.checkNext("mutable false"); + check_lib.checkNext("i32.const {foo_address}"); + check_lib.checkNext("type i32"); + check_lib.checkNext("mutable false"); + check_lib.checkNext("i32.const {bar_address}"); + check_lib.checkComputeCompare("foo_address", .{ .op = .eq, .value = .{ .literal = 0x0c } }); + check_lib.checkComputeCompare("bar_address", .{ .op = .eq, .value = .{ .literal = 0x10 } }); + + check_lib.checkStart("Section export"); + check_lib.checkNext("entries 3"); + check_lib.checkNext("name foo"); + check_lib.checkNext("kind global"); + check_lib.checkNext("index 1"); + check_lib.checkNext("name bar"); + check_lib.checkNext("kind global"); + check_lib.checkNext("index 2"); + + test_step.dependOn(&check_lib.step); +} diff --git a/test/link/wasm/export-data/lib.zig b/test/link/wasm/export-data/lib.zig new file mode 100644 index 0000000000..dffce185fa --- /dev/null +++ b/test/link/wasm/export-data/lib.zig @@ -0,0 +1,2 @@ +export const foo: u32 = 0xbbbbbbbb; +export const bar: u32 = 0xbbbbbbbb; From b9224c172fea2399623bd707a10e021e776329bc Mon Sep 17 00:00:00 2001 From: Luuk de Gram Date: Mon, 2 Jan 2023 15:45:35 +0100 Subject: [PATCH 8/8] wasm-linker: Fix & mangle symbol name of imports When outputting the names section, we should output the actual symbol name rather than the import name. This makes sure that symbols with an explicit name set have the correct name but retain the import name too. We also now correctly mangle the name of an extern function with an explicit library name. This ensures that functions that have a different library name, but the same import/function name, can be resolved correctly with other modules and don't resolve to the same symbol. --- src/link/Wasm.zig | 34 ++++++++++++++++++++-------- test/link.zig | 7 +++--- test/link/wasm/export-data/build.zig | 8 +++---- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index e03c8b1ba0..d62d5adb25 100644 --- a/src/link/Wasm.zig +++ b/src/link/Wasm.zig @@ -813,7 +813,12 @@ fn checkUndefinedSymbols(wasm: *const Wasm) !void { const file_name = if (undef.file) |file_index| name: { break :name wasm.objects.items[file_index].name; } else wasm.name; - log.err("could not resolve undefined symbol '{s}'", .{undef.getName(wasm)}); + const import_name = if (undef.file) |file_index| name: { + const obj = wasm.objects.items[file_index]; + const name_index = obj.findImport(symbol.tag.externalType(), symbol.index).name; + break :name obj.string_table.get(name_index); + } else wasm.string_table.get(wasm.imports.get(undef).?.name); + log.err("could not resolve undefined symbol '{s}'", .{import_name}); log.err(" defined in '{s}'", .{file_name}); } } @@ -1430,18 +1435,31 @@ pub fn addOrUpdateImport( type_index: ?u32, ) !void { assert(symbol_index != 0); - // For the import name itwasm, we use the decl's name, rather than the fully qualified name - const decl_name_index = try wasm.string_table.put(wasm.base.allocator, name); + // For the import name, we use the decl's name, rather than the fully qualified name + // Also mangle the name when the lib name is set and not equal to "C" so imports with the same + // name but different module can be resolved correctly. + const mangle_name = lib_name != null and + !std.mem.eql(u8, std.mem.sliceTo(lib_name.?, 0), "c"); + const full_name = if (mangle_name) full_name: { + break :full_name try std.fmt.allocPrint(wasm.base.allocator, "{s}|{s}", .{ name, lib_name.? }); + } else name; + defer if (mangle_name) wasm.base.allocator.free(full_name); + + const decl_name_index = try wasm.string_table.put(wasm.base.allocator, full_name); const symbol: *Symbol = &wasm.symbols.items[symbol_index]; symbol.setUndefined(true); symbol.setGlobal(true); symbol.name = decl_name_index; + if (mangle_name) { + // we specified a specific name for the symbol that does not match the import name + symbol.setFlag(.WASM_SYM_EXPLICIT_NAME); + } const global_gop = try wasm.globals.getOrPut(wasm.base.allocator, decl_name_index); if (!global_gop.found_existing) { const loc: SymbolLoc = .{ .file = null, .index = symbol_index }; global_gop.value_ptr.* = loc; try wasm.resolved_symbols.put(wasm.base.allocator, loc, {}); - try wasm.undefs.putNoClobber(wasm.base.allocator, name, loc); + try wasm.undefs.putNoClobber(wasm.base.allocator, full_name, loc); } if (type_index) |ty_index| { @@ -1452,7 +1470,7 @@ pub fn addOrUpdateImport( if (!gop.found_existing) { gop.value_ptr.* = .{ .module_name = try wasm.string_table.put(wasm.base.allocator, module_name), - .name = decl_name_index, + .name = try wasm.string_table.put(wasm.base.allocator, name), .kind = .{ .function = ty_index }, }; } @@ -3130,11 +3148,7 @@ fn emitNameSection(wasm: *Wasm, binary_bytes: *std.ArrayList(u8), arena: std.mem for (wasm.resolved_symbols.keys()) |sym_loc| { const symbol = sym_loc.getSymbol(wasm).*; - const name = if (symbol.isUndefined()) blk: { - if (symbol.tag == .data) continue; - const imp = wasm.imports.get(sym_loc) orelse continue; - break :blk wasm.string_table.get(imp.name); - } else sym_loc.getName(wasm); + const name = sym_loc.getName(wasm); switch (symbol.tag) { .function => { const gop = funcs.getOrPutAssumeCapacity(symbol.index); diff --git a/test/link.zig b/test/link.zig index 2fe62b8068..5e26ae728d 100644 --- a/test/link.zig +++ b/test/link.zig @@ -47,9 +47,10 @@ fn addWasmCases(cases: *tests.StandaloneContext) void { .requires_stage2 = true, }); - cases.addBuildFile("test/link/wasm/export-data/build.zig", .{ - .build_modes = true, - }); + // TODO: Fix open handle in wasm-linker refraining rename from working on Windows. + if (builtin.os.tag != .windows) { + cases.addBuildFile("test/link/wasm/export-data/build.zig", .{}); + } cases.addBuildFile("test/link/wasm/extern/build.zig", .{ .build_modes = true, diff --git a/test/link/wasm/export-data/build.zig b/test/link/wasm/export-data/build.zig index 3df5c1790c..283566dab3 100644 --- a/test/link/wasm/export-data/build.zig +++ b/test/link/wasm/export-data/build.zig @@ -2,13 +2,11 @@ const std = @import("std"); const Builder = std.build.Builder; pub fn build(b: *Builder) void { - const mode = b.standardReleaseOptions(); - const test_step = b.step("test", "Test"); test_step.dependOn(b.getInstallStep()); const lib = b.addSharedLibrary("lib", "lib.zig", .unversioned); - lib.setBuildMode(mode); + lib.setBuildMode(.ReleaseSafe); // to make the output deterministic in address positions lib.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding }); lib.use_lld = false; lib.export_symbol_names = &.{ "foo", "bar" }; @@ -25,8 +23,8 @@ pub fn build(b: *Builder) void { check_lib.checkNext("type i32"); check_lib.checkNext("mutable false"); check_lib.checkNext("i32.const {bar_address}"); - check_lib.checkComputeCompare("foo_address", .{ .op = .eq, .value = .{ .literal = 0x0c } }); - check_lib.checkComputeCompare("bar_address", .{ .op = .eq, .value = .{ .literal = 0x10 } }); + check_lib.checkComputeCompare("foo_address", .{ .op = .eq, .value = .{ .literal = 0 } }); + check_lib.checkComputeCompare("bar_address", .{ .op = .eq, .value = .{ .literal = 4 } }); check_lib.checkStart("Section export"); check_lib.checkNext("entries 3");