Commit 143e9599d6

Luuk de Gram <luuk@degram.dev>
2024-01-17 17:21:59
wasm: use `File` abstraction instead of object
When merging sections we now make use of the `File` abstraction so all objects such as globals, functions, imports, etc are also merged from the `ZigObject` module. This allows us to use a singular way to perform each link action without having to check the kind of the file. The logic is mostly handled in the abstract file module, unless its complexity warrants the handling within the corresponding module itself.
1 parent 12505c6
Changed files (3)
src/link/Wasm/Object.zig
@@ -9,6 +9,7 @@ const std = @import("std");
 const Wasm = @import("../Wasm.zig");
 const Symbol = @import("Symbol.zig");
 const Alignment = types.Alignment;
+const File = @import("file.zig").File;
 
 const Allocator = std.mem.Allocator;
 const leb = std.leb;
@@ -16,12 +17,14 @@ const meta = std.meta;
 
 const log = std.log.scoped(.link);
 
+/// Index into the list of relocatable object files within the linker driver.
+index: File.Index = .null,
 /// Wasm spec version used for this `Object`
 version: u32 = 0,
 /// The file descriptor that represents the wasm object file.
 file: ?std.fs.File = null,
 /// Name (read path) of the object file.
-name: []const u8,
+path: []const u8,
 /// Parsed type section
 func_types: []const std.wasm.Type = &.{},
 /// A list of all imports for this module
@@ -64,6 +67,12 @@ relocatable_data: std.AutoHashMapUnmanaged(RelocatableData.Tag, []RelocatableDat
 /// import name, module name and export names. Each string will be deduplicated
 /// and returns an offset into the table.
 string_table: Wasm.StringTable = .{},
+/// Amount of functions in the `import` sections.
+imported_functions_count: u32 = 0,
+/// Amount of globals in the `import` section.
+imported_globals_count: u32 = 0,
+/// Amount of tables in the `import` section.
+imported_tables_count: u32 = 0,
 
 /// Represents a single item within a section (depending on its `type`)
 const RelocatableData = struct {
@@ -121,7 +130,7 @@ pub const InitError = error{NotObjectFile} || ParseError || std.fs.File.ReadErro
 pub fn create(gpa: Allocator, file: std.fs.File, name: []const u8, maybe_max_size: ?usize) InitError!Object {
     var object: Object = .{
         .file = file,
-        .name = try gpa.dupe(u8, name),
+        .path = try gpa.dupe(u8, name),
     };
 
     var is_object_file: bool = false;
@@ -199,29 +208,17 @@ pub fn deinit(object: *Object, gpa: Allocator) void {
 
 /// Finds the import within the list of imports from a given kind and index of that kind.
 /// Asserts the import exists
-pub fn findImport(object: *const Object, import_kind: std.wasm.ExternalKind, index: u32) types.Import {
+pub fn findImport(object: *const Object, index: u32) types.Import {
+    const sym = object.symtable[index];
     var i: u32 = 0;
     return for (object.imports) |import| {
-        if (std.meta.activeTag(import.kind) == import_kind) {
+        if (std.meta.activeTag(import.kind) == sym.tag) {
             if (i == index) return import;
             i += 1;
         }
     } else unreachable; // Only existing imports are allowed to be found
 }
 
-/// Counts the entries of imported `kind` and returns the result
-pub fn importedCountByKind(object: *const Object, kind: std.wasm.ExternalKind) u32 {
-    var i: u32 = 0;
-    return for (object.imports) |imp| {
-        if (@as(std.wasm.ExternalKind, imp.kind) == kind) i += 1;
-    } else i;
-}
-
-/// From a given `RelocatableDate`, find the corresponding debug section name
-pub fn getDebugName(object: *const Object, relocatable_data: RelocatableData) []const u8 {
-    return object.string_table.get(relocatable_data.index);
-}
-
 /// Checks if the object file is an MVP version.
 /// When that's the case, we check if there's an import table definiton with its name
 /// set to '__indirect_function_table". When that's also the case,
@@ -427,16 +424,25 @@ fn Parser(comptime ReaderType: type) type {
 
                             const kind = try readEnum(std.wasm.ExternalKind, reader);
                             const kind_value: std.wasm.Import.Kind = switch (kind) {
-                                .function => .{ .function = try readLeb(u32, reader) },
+                                .function => val: {
+                                    parser.object.imported_functions_count += 1;
+                                    break :val .{ .function = try readLeb(u32, reader) };
+                                },
                                 .memory => .{ .memory = try readLimits(reader) },
-                                .global => .{ .global = .{
-                                    .valtype = try readEnum(std.wasm.Valtype, reader),
-                                    .mutable = (try reader.readByte()) == 0x01,
-                                } },
-                                .table => .{ .table = .{
-                                    .reftype = try readEnum(std.wasm.RefType, reader),
-                                    .limits = try readLimits(reader),
-                                } },
+                                .global => val: {
+                                    parser.object.imported_globals_count += 1;
+                                    break :val .{ .global = .{
+                                        .valtype = try readEnum(std.wasm.Valtype, reader),
+                                        .mutable = (try reader.readByte()) == 0x01,
+                                    } };
+                                },
+                                .table => val: {
+                                    parser.object.imported_tables_count += 1;
+                                    break :val .{ .table = .{
+                                        .reftype = try readEnum(std.wasm.RefType, reader),
+                                        .limits = try readLimits(reader),
+                                    } };
+                                },
                             };
 
                             import.* = .{
@@ -904,7 +910,7 @@ fn assertEnd(reader: anytype) !void {
 }
 
 /// Parses an object file into atoms, for code and data sections
-pub fn parseSymbolIntoAtom(object: *Object, object_index: u16, symbol_index: u32, wasm: *Wasm) !Atom.Index {
+pub fn parseSymbolIntoAtom(object: *Object, wasm: *Wasm, symbol_index: u32) !Atom.Index {
     const comp = wasm.base.comp;
     const gpa = comp.gpa;
     const symbol = &object.symtable[symbol_index];
@@ -922,19 +928,16 @@ pub fn parseSymbolIntoAtom(object: *Object, object_index: u16, symbol_index: u32
         },
         else => unreachable,
     };
-    const final_index = try wasm.getMatchingSegment(object_index, symbol_index);
-    const atom_index = @as(Atom.Index, @intCast(wasm.managed_atoms.items.len));
-    const atom = try wasm.managed_atoms.addOne(gpa);
-    atom.* = Atom.empty;
+    const final_index = try wasm.getMatchingSegment(object.index, symbol_index);
+    const atom_index = try wasm.createAtom(symbol_index, object.index);
     try wasm.appendAtomAtIndex(final_index, atom_index);
 
-    atom.sym_index = symbol_index;
-    atom.file = object_index;
+    const atom = wasm.getAtomPtr(atom_index);
     atom.size = relocatable_data.size;
     atom.alignment = relocatable_data.getAlignment(object);
     atom.code = std.ArrayListUnmanaged(u8).fromOwnedSlice(relocatable_data.data[0..relocatable_data.size]);
     atom.original_offset = relocatable_data.offset;
-    try wasm.symbol_atom.putNoClobber(gpa, atom.symbolLoc(), atom_index);
+
     const segment: *Wasm.Segment = &wasm.segments.items[final_index];
     if (relocatable_data.type == .data) { //code section and custom sections are 1-byte aligned
         segment.alignment = segment.alignment.max(atom.alignment);
@@ -952,7 +955,7 @@ pub fn parseSymbolIntoAtom(object: *Object, object_index: u16, symbol_index: u32
                 .R_WASM_TABLE_INDEX_SLEB64,
                 => {
                     try wasm.function_table.put(gpa, .{
-                        .file = object_index,
+                        .file = object.index,
                         .index = reloc.index,
                     }, 0);
                 },
@@ -963,7 +966,7 @@ pub fn parseSymbolIntoAtom(object: *Object, object_index: u16, symbol_index: u32
                     if (sym.tag != .global) {
                         try wasm.got_symbols.append(
                             gpa,
-                            .{ .file = object_index, .index = reloc.index },
+                            .{ .file = object.index, .index = reloc.index },
                         );
                     }
                 },
src/link/Wasm/ZigObject.zig
@@ -11,6 +11,9 @@ index: File.Index,
 decls: std.AutoHashMapUnmanaged(InternPool.DeclIndex, Atom.Index) = .{},
 /// List of function type signatures for this Zig module.
 func_types: std.ArrayListUnmanaged(std.wasm.Type) = .{},
+/// List of `std.wasm.Func`. Each entry contains the function signature,
+/// rather than the actual body.
+functions: std.ArrayListUnmanaged(std.wasm.Func) = .{},
 /// Map of symbol locations, represented by its `types.Import`.
 imports: std.AutoHashMapUnmanaged(u32, types.Import) = .{},
 /// List of WebAssembly globals.
@@ -1152,6 +1155,39 @@ pub fn storeDeclType(zig_object: *ZigObject, gpa: std.mem.Allocator, decl_index:
     return index;
 }
 
+/// The symbols in ZigObject are already represented by an atom as we need to store its data.
+/// So rather than creating a new Atom and returning its index, we use this oppertunity to scan
+/// its relocations and create any GOT symbols or function table indexes it may require.
+pub fn parseSymbolIntoAtom(zig_object: *ZigObject, wasm_file: *Wasm, index: u32) Atom.Index {
+    const gpa = wasm_file.base.comp.gpa;
+    const loc: Wasm.SymbolLoc = .{ .file = @intFromEnum(zig_object.index), .index = index };
+    const final_index = try wasm_file.getMatchingSegment(zig_object.index, index);
+    const atom_index = wasm_file.symbol_atom.get(loc).?;
+    try wasm_file.appendAtomAtIndex(final_index, atom_index);
+    const atom = wasm_file.getAtom(atom_index);
+    for (atom.relocs.items) |reloc| {
+        switch (reloc.relocation_type) {
+            .R_WASM_TABLE_INDEX_I32,
+            .R_WASM_TABLE_INDEX_I64,
+            .R_WASM_TABLE_INDEX_SLEB,
+            .R_WASM_TABLE_INDEX_SLEB64,
+            => {
+                try wasm_file.function_table.put(gpa, loc, 0);
+            },
+            .R_WASM_GLOBAL_INDEX_I32,
+            .R_WASM_GLOBAL_INDEX_LEB,
+            => {
+                const sym = zig_object.symbol(reloc.index);
+                if (sym.tag != .global) {
+                    try wasm_file.got_symbols.append(gpa, loc);
+                }
+            },
+            else => {},
+        }
+    }
+    return atom_index;
+}
+
 const build_options = @import("build_options");
 const builtin = @import("builtin");
 const codegen = @import("../../codegen.zig");
src/link/Wasm.zig
@@ -152,7 +152,7 @@ entry: ?u32 = null,
 function_table: std.AutoHashMapUnmanaged(SymbolLoc, u32) = .{},
 
 /// All object files and their data which are linked into the final binary
-objects: std.ArrayListUnmanaged(Object) = .{},
+objects: std.ArrayListUnmanaged(File.Index) = .{},
 /// All archive files that are lazy loaded.
 /// e.g. when an undefined symbol references a symbol from the archive.
 archives: std.ArrayListUnmanaged(Archive) = .{},
@@ -442,7 +442,7 @@ pub fn createEmpty(
     // can be passed to LLD.
     const sub_path = if (use_lld) zcu_object_sub_path.? else emit.sub_path;
 
-    const file = try emit.directory.handle.createFile(sub_path, .{
+    wasm.base.file = try emit.directory.handle.createFile(sub_path, .{
         .truncate = true,
         .read = true,
         .mode = if (fs.has_executable_bit)
@@ -453,7 +453,6 @@ pub fn createEmpty(
         else
             0,
     });
-    wasm.base.file = file;
     wasm.name = sub_path;
 
     // create stack pointer symbol
@@ -582,6 +581,15 @@ pub fn createEmpty(
     return wasm;
 }
 
+pub fn file(wasm: *Wasm, index: File.Index) ?File {
+    const tag = wasm.files.items(.tags)[index];
+    return switch (tag) {
+        .null => null,
+        .zig_object => .{ .zig_object = &wasm.files.items(.data)[index].zig_object },
+        .object => .{ .object = &wasm.files.items(.data)[index].object },
+    };
+}
+
 pub fn zigObjectPtr(wasm: *Wasm) ?*ZigObject {
     if (wasm.zig_object_index == .null) return null;
     return &wasm.files.items(.data)[@intFromEnum(wasm.zig_object_index)].zig_object;
@@ -650,16 +658,18 @@ fn parseInputFiles(wasm: *Wasm, files: []const []const u8) !void {
 /// file and parsed successfully. Returns false when file is not an object file.
 /// May return an error instead when parsing failed.
 fn parseObjectFile(wasm: *Wasm, path: []const u8) !bool {
-    const file = try fs.cwd().openFile(path, .{});
-    errdefer file.close();
+    const obj_file = try fs.cwd().openFile(path, .{});
+    errdefer obj_file.close();
 
     const gpa = wasm.base.comp.gpa;
-    var object = Object.create(gpa, file, path, null) catch |err| switch (err) {
+    var object = Object.create(gpa, obj_file, path, null) catch |err| switch (err) {
         error.InvalidMagicByte, error.NotObjectFile => return false,
         else => |e| return e,
     };
     errdefer object.deinit(gpa);
-    try wasm.objects.append(gpa, object);
+    object.index = @enumFromInt(wasm.files.len);
+    try wasm.files.append(gpa, .{ .object = object });
+    try wasm.objects.append(gpa, object.index);
     return true;
 }
 
@@ -693,11 +703,11 @@ pub inline fn getAtomPtr(wasm: *Wasm, index: Atom.Index) *Atom {
 fn parseArchive(wasm: *Wasm, path: []const u8, force_load: bool) !bool {
     const gpa = wasm.base.comp.gpa;
 
-    const file = try fs.cwd().openFile(path, .{});
-    errdefer file.close();
+    const archive_file = try fs.cwd().openFile(path, .{});
+    errdefer archive_file.close();
 
     var archive: Archive = .{
-        .file = file,
+        .file = archive_file,
         .name = path,
     };
     archive.parse(gpa) catch |err| switch (err) {
@@ -727,8 +737,10 @@ fn parseArchive(wasm: *Wasm, path: []const u8, force_load: bool) !bool {
     }
 
     for (offsets.keys()) |file_offset| {
-        const object = try wasm.objects.addOne(gpa);
-        object.* = try archive.parseObject(gpa, file_offset);
+        var object = try archive.parseObject(gpa, file_offset);
+        object.index = @enumFromInt(wasm.files.len);
+        try wasm.files.append(gpa, .{ .object = object });
+        try wasm.objects.append(gpa, object.index);
     }
 
     return true;
@@ -784,8 +796,8 @@ fn resolveSymbolsInObject(wasm: *Wasm, object_index: u16) !void {
         const existing_loc = maybe_existing.value_ptr.*;
         const existing_sym: *Symbol = existing_loc.getSymbol(wasm);
 
-        const existing_file_path = if (existing_loc.file) |file| blk: {
-            break :blk wasm.objects.items[file].name;
+        const existing_file_path = if (existing_loc.file) |file_index| blk: {
+            break :blk wasm.objects.items[file_index].name;
         } else wasm.name;
 
         if (!existing_sym.isUndefined()) outer: {
@@ -911,10 +923,11 @@ fn resolveSymbolsInArchives(wasm: *Wasm) !void {
             // Symbol is found in unparsed object file within current archive.
             // Parse object and and resolve symbols again before we check remaining
             // undefined symbols.
-            const object_file_index: u16 = @intCast(wasm.objects.items.len);
-            const object = try archive.parseObject(gpa, offset.items[0]);
-            try wasm.objects.append(gpa, object);
-            try wasm.resolveSymbolsInObject(object_file_index);
+            var object = try archive.parseObject(gpa, offset.items[0]);
+            object.index = @enumFromInt(wasm.files.len);
+            try wasm.files.append(gpa, .{ .object = object });
+            try wasm.objects.append(gpa, object.index);
+            try wasm.resolveSymbolsInObject(object.index);
 
             // continue loop for any remaining undefined symbols that still exist
             // after resolving last object file
@@ -1176,9 +1189,10 @@ fn validateFeatures(
 
     // extract all the used, disallowed and required features from each
     // linked object file so we can test them.
-    for (wasm.objects.items, 0..) |object, object_index| {
+    for (wasm.objects.items) |file_index| {
+        const object: Object = wasm.files.items(.data)[file_index].object;
         for (object.features) |feature| {
-            const value = @as(u16, @intCast(object_index)) << 1 | @as(u1, 1);
+            const value = @as(u16, @intFromEnum(file_index)) << 1 | @as(u1, 1);
             switch (feature.prefix) {
                 .used => {
                     used[@intFromEnum(feature.tag)] = value;
@@ -1210,7 +1224,7 @@ fn validateFeatures(
             emit_features_count.* += @intFromBool(is_enabled);
         } else if (is_enabled and !allowed[used_index]) {
             log.err("feature '{}' not allowed, but used by linked object", .{@as(types.Feature.Tag, @enumFromInt(used_index))});
-            log.err("  defined in '{s}'", .{wasm.objects.items[used_set >> 1].name});
+            log.err("  defined in '{s}'", .{wasm.files.items(.data)[used_set >> 1].object.path});
             valid_feature_set = false;
         }
     }
@@ -1224,7 +1238,7 @@ fn validateFeatures(
         if (@as(u1, @truncate(disallowed_feature)) != 0) {
             log.err(
                 "shared-memory is disallowed by '{s}' because it wasn't compiled with 'atomics' and 'bulk-memory' features enabled",
-                .{wasm.objects.items[disallowed_feature >> 1].name},
+                .{wasm.files.items(.data)[disallowed_feature >> 1].object.path},
             );
             valid_feature_set = false;
         }
@@ -1244,16 +1258,17 @@ fn validateFeatures(
         }
     }
     // For each linked object, validate the required and disallowed features
-    for (wasm.objects.items) |object| {
+    for (wasm.objects.items) |file_index| {
         var object_used_features = [_]bool{false} ** known_features_count;
+        const object = wasm.files.items(.data)[file_index].object;
         for (object.features) |feature| {
             if (feature.prefix == .disallowed) continue; // already defined in 'disallowed' set.
             // from here a feature is always used
             const disallowed_feature = disallowed[@intFromEnum(feature.tag)];
             if (@as(u1, @truncate(disallowed_feature)) != 0) {
                 log.err("feature '{}' is disallowed, but used by linked object", .{feature.tag});
-                log.err("  disallowed by '{s}'", .{wasm.objects.items[disallowed_feature >> 1].name});
-                log.err("  used in '{s}'", .{object.name});
+                log.err("  disallowed by '{s}'", .{wasm.files.items(.data)[disallowed_feature >> 1].object.path});
+                log.err("  used in '{s}'", .{object.path});
                 valid_feature_set = false;
             }
 
@@ -1265,8 +1280,8 @@ fn validateFeatures(
             const is_required = @as(u1, @truncate(required_feature)) != 0;
             if (is_required and !object_used_features[feature_index]) {
                 log.err("feature '{}' is required but not used in linked object", .{@as(types.Feature.Tag, @enumFromInt(feature_index))});
-                log.err("  required by '{s}'", .{wasm.objects.items[required_feature >> 1].name});
-                log.err("  missing in '{s}'", .{object.name});
+                log.err("  required by '{s}'", .{wasm.files.items(.data)[required_feature >> 1].object.path});
+                log.err("  missing in '{s}'", .{object.path});
                 valid_feature_set = false;
             }
         }
@@ -1346,9 +1361,10 @@ fn checkUndefinedSymbols(wasm: *const Wasm) !void {
         const symbol = undef.getSymbol(wasm);
         if (symbol.tag == .data) {
             found_undefined_symbols = true;
-            const file_name = if (undef.file) |file_index| name: {
-                break :name wasm.objects.items[file_index].name;
-            } else wasm.name;
+            const file_name = if (undef.file) |file_index|
+                wasm.file(file_index).?.path()
+            else
+                wasm.name;
             const symbol_name = undef.getName(wasm);
             log.err("could not resolve undefined symbol '{s}'", .{symbol_name});
             log.err("  defined in '{s}'", .{file_name});
@@ -1369,8 +1385,11 @@ pub fn deinit(wasm: *Wasm) void {
     for (wasm.segment_info.values()) |segment_info| {
         gpa.free(segment_info.name);
     }
-    for (wasm.objects.items) |*object| {
-        object.deinit(gpa);
+    if (wasm.zigObjectPtr()) |zig_obj| {
+        zig_obj.deinit(gpa);
+    }
+    for (wasm.objects.items) |obj_index| {
+        wasm.file(obj_index).?.object.deinit(gpa);
     }
 
     for (wasm.archives.items) |*archive| {
@@ -1441,12 +1460,11 @@ fn getGlobalType(wasm: *const Wasm, loc: SymbolLoc) std.wasm.GlobalType {
     assert(symbol.tag == .global);
     const is_undefined = symbol.isUndefined();
     if (loc.file) |file_index| {
-        const obj: Object = wasm.objects.items[file_index];
+        const obj_file = wasm.file(@enumFromInt(file_index)).?;
         if (is_undefined) {
-            return obj.findImport(.global, symbol.index).kind.global;
+            return obj_file.import(loc.index).kind.global;
         }
-        const import_global_count = obj.importedCountByKind(.global);
-        return obj.globals[symbol.index - import_global_count].global_type;
+        return obj_file.globals()[symbol.index - obj_file.importedGlobals()].global_type;
     }
     if (is_undefined) {
         return wasm.imports.get(loc).?.kind.global;
@@ -1461,14 +1479,13 @@ fn getFunctionSignature(wasm: *const Wasm, loc: SymbolLoc) std.wasm.Type {
     assert(symbol.tag == .function);
     const is_undefined = symbol.isUndefined();
     if (loc.file) |file_index| {
-        const obj: Object = wasm.objects.items[file_index];
+        const obj_file = wasm.file(@enumFromInt(file_index)).?;
         if (is_undefined) {
-            const ty_index = obj.findImport(.function, symbol.index).kind.function;
-            return obj.func_types[ty_index];
+            const ty_index = obj_file.import(loc.index).kind.function;
+            return obj_file.funcTypes()[ty_index];
         }
-        const import_function_count = obj.importedCountByKind(.function);
-        const type_index = obj.functions[symbol.index - import_function_count].type_index;
-        return obj.func_types[type_index];
+        const type_index = obj_file.functions()[symbol.index - obj_file.importedFunctions()].type_index;
+        return obj_file.funcTypes()[type_index];
     }
     if (is_undefined) {
         const ty_index = wasm.imports.get(loc).?.kind.function;
@@ -1606,10 +1623,10 @@ fn allocateAtoms(wasm: *Wasm) !void {
             // Ensure we get the original symbol, so we verify the correct symbol on whether
             // it is dead or not and ensure an atom is removed when dead.
             // This is required as we may have parsed aliases into atoms.
-            const sym = if (symbol_loc.file) |object_index| sym: {
-                const object = wasm.objects.items[object_index];
-                break :sym object.symtable[symbol_loc.index];
-            } else wasm.synthetic_symbols.items[symbol_loc.index];
+            const sym = if (symbol_loc.file) |object_index|
+                wasm.file(object_index).?.symbol(symbol_loc.index).*
+            else
+                wasm.synthetic_symbols.items[symbol_loc.index];
 
             // Dead symbols must be unlinked from the linked-list to prevent them
             // from being emit into the binary.
@@ -1655,9 +1672,10 @@ fn allocateVirtualAddresses(wasm: *Wasm) void {
 
         const atom = wasm.getAtom(atom_index);
         const merge_segment = wasm.base.comp.config.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_info = if (atom.file) |object_index|
+            wasm.file(object_index).?.segmentInfo()
+        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];
@@ -1713,7 +1731,8 @@ fn sortDataSegments(wasm: *Wasm) !void {
 /// contain any parameters.
 fn setupInitFunctions(wasm: *Wasm) !void {
     const gpa = wasm.base.comp.gpa;
-    for (wasm.objects.items, 0..) |object, file_index| {
+    for (wasm.objects.items) |file_index| {
+        const object = wasm.files.items(.data)[file_index].object;
         try wasm.init_funcs.ensureUnusedCapacity(gpa, object.init_funcs.len);
         for (object.init_funcs) |init_func| {
             const symbol = object.symtable[init_func.symbol_index];
@@ -1961,7 +1980,7 @@ fn setupImports(wasm: *Wasm) !void {
 
     for (wasm.resolved_symbols.keys()) |symbol_loc| {
         const file_index = symbol_loc.file orelse {
-            // imports generated by Zig code are already in the `import` section
+            // Synthetic symbols will already exist in the `import` section
             continue;
         };
 
@@ -1974,14 +1993,14 @@ fn setupImports(wasm: *Wasm) !void {
         }
 
         log.debug("Symbol '{s}' will be imported from the host", .{symbol_loc.getName(wasm)});
-        const object = wasm.objects.items[file_index];
-        const import = object.findImport(symbol.tag.externalType(), symbol.index);
+        const obj_file = wasm.file(file_index).?;
+        const import = obj_file.import(symbol_loc.index);
 
         // We copy the import to a new import to ensure the names contain references
         // to the internal string table, rather than of the object file.
         const new_imp: types.Import = .{
-            .module_name = try wasm.string_table.put(gpa, object.string_table.get(import.module_name)),
-            .name = try wasm.string_table.put(gpa, object.string_table.get(import.name)),
+            .module_name = try wasm.string_table.put(gpa, obj_file.string(import.module_name)),
+            .name = try wasm.string_table.put(gpa, obj_file.string(import.name)),
             .kind = import.kind,
         };
         // TODO: De-duplicate imports when they contain the same names and type
@@ -2032,28 +2051,23 @@ fn mergeSections(wasm: *Wasm) !void {
     defer removed_duplicates.deinit();
 
     for (wasm.resolved_symbols.keys()) |sym_loc| {
-        if (sym_loc.file == null) {
+        const file_index = sym_loc.file orelse {
             // Zig code-generated symbols are already within the sections and do not
             // require to be merged
             continue;
-        }
+        };
 
-        const object = &wasm.objects.items[sym_loc.file.?];
-        const symbol = &object.symtable[sym_loc.index];
+        const obj_file = wasm.file(@enumFromInt(file_index)).?;
+        const symbol = obj_file.symbol[sym_loc.index];
 
-        if (symbol.isDead() or
-            symbol.isUndefined() or
-            (symbol.tag != .function and symbol.tag != .global and symbol.tag != .table))
-        {
+        if (symbol.isDead() or symbol.isUndefined()) {
             // Skip undefined symbols as they go in the `import` section
-            // Also skip symbols that do not need to have a section merged.
             continue;
         }
 
-        const offset = object.importedCountByKind(symbol.tag.externalType());
-        const index = symbol.index - offset;
         switch (symbol.tag) {
             .function => {
+                const index = symbol.index - obj_file.importedFunctions();
                 const gop = try wasm.functions.getOrPut(
                     gpa,
                     .{ .file = sym_loc.file, .index = symbol.index },
@@ -2071,20 +2085,24 @@ fn mergeSections(wasm: *Wasm) !void {
                     try removed_duplicates.append(sym_loc);
                     continue;
                 }
-                gop.value_ptr.* = .{ .func = object.functions[index], .sym_index = sym_loc.index };
+                gop.value_ptr.* = .{ .func = obj_file.functions()[index], .sym_index = sym_loc.index };
                 symbol.index = @as(u32, @intCast(gop.index)) + wasm.imported_functions_count;
             },
             .global => {
-                const original_global = object.globals[index];
+                const index = symbol.index - obj_file.importedFunctions();
+                const original_global = obj_file.globals()[index];
                 symbol.index = @as(u32, @intCast(wasm.wasm_globals.items.len)) + wasm.imported_globals_count;
                 try wasm.wasm_globals.append(gpa, original_global);
             },
             .table => {
-                const original_table = object.tables[index];
+                const index = symbol.index - obj_file.importedFunctions();
+                // assert it's a regular relocatable object file as `ZigObject` will never
+                // contain a table.
+                const original_table = obj_file.object.tables[index];
                 symbol.index = @as(u32, @intCast(wasm.tables.items.len)) + wasm.imported_tables_count;
                 try wasm.tables.append(gpa, original_table);
             },
-            else => unreachable,
+            else => continue,
         }
     }
 
@@ -2111,12 +2129,13 @@ fn mergeTypes(wasm: *Wasm) !void {
     defer dirty.deinit();
 
     for (wasm.resolved_symbols.keys()) |sym_loc| {
-        if (sym_loc.file == null) {
+        const file_index = sym_loc.file orelse {
             // zig code-generated symbols are already present in final type section
             continue;
-        }
-        const object = wasm.objects.items[sym_loc.file.?];
-        const symbol = object.symtable[sym_loc.index];
+        };
+
+        const obj_file = wasm.file(@enumFromInt(file_index)).?;
+        const symbol = obj_file.symbol(sym_loc.index);
         if (symbol.tag != .function or symbol.isDead()) {
             // Only functions have types. Only retrieve the type of referenced functions.
             continue;
@@ -2125,12 +2144,12 @@ fn mergeTypes(wasm: *Wasm) !void {
         if (symbol.isUndefined()) {
             log.debug("Adding type from extern function '{s}'", .{sym_loc.getName(wasm)});
             const import: *types.Import = wasm.imports.getPtr(sym_loc) orelse continue;
-            const original_type = object.func_types[import.kind.function];
+            const original_type = obj_file.funcTypes()[import.kind.function];
             import.kind.function = try wasm.putOrGetFuncType(original_type);
         } else if (!dirty.contains(symbol.index)) {
             log.debug("Adding type from function '{s}'", .{sym_loc.getName(wasm)});
             const func = &wasm.functions.values()[symbol.index - wasm.imported_functions_count].func;
-            func.type_index = try wasm.putOrGetFuncType(object.func_types[func.type_index]);
+            func.type_index = try wasm.putOrGetFuncType(obj_file.funcTypes()[func.type_index]);
             dirty.putAssumeCapacityNoClobber(symbol.index, {});
         }
     }
@@ -2240,11 +2259,18 @@ fn setupMemory(wasm: *Wasm) !void {
 
     const is_obj = comp.config.output_mode == .Obj;
 
+    const stack_ptr = if (wasm.findGlobalSymbol("__stack_pointer")) |loc| index: {
+        const sym = loc.getSymbol(wasm);
+        break :index sym.index - wasm.imported_globals_count;
+    } else null;
+
     if (place_stack_first and !is_obj) {
         memory_ptr = stack_alignment.forward(memory_ptr);
         memory_ptr += wasm.base.stack_size;
         // We always put the stack pointer global at index 0
-        wasm.wasm_globals.items[0].init.i32_const = @as(i32, @bitCast(@as(u32, @intCast(memory_ptr))));
+        if (stack_ptr) |index| {
+            wasm.wasm_globals.items[index].init.i32_const = @as(i32, @bitCast(@as(u32, @intCast(memory_ptr))));
+        }
     }
 
     var offset: u32 = @as(u32, @intCast(memory_ptr));
@@ -2290,7 +2316,9 @@ fn setupMemory(wasm: *Wasm) !void {
     if (!place_stack_first and !is_obj) {
         memory_ptr = stack_alignment.forward(memory_ptr);
         memory_ptr += wasm.base.stack_size;
-        wasm.wasm_globals.items[0].init.i32_const = @as(i32, @bitCast(@as(u32, @intCast(memory_ptr))));
+        if (stack_ptr) |index| {
+            wasm.wasm_globals.items[index].init.i32_const = @as(i32, @bitCast(@as(u32, @intCast(memory_ptr))));
+        }
     }
 
     // One of the linked object files has a reference to the __heap_base symbol.
@@ -2355,17 +2383,17 @@ fn setupMemory(wasm: *Wasm) !void {
 /// From a given object's index and the index of the segment, returns the corresponding
 /// index of the segment within the final data section. When the segment does not yet
 /// exist, a new one will be initialized and appended. The new index will be returned in that case.
-pub fn getMatchingSegment(wasm: *Wasm, object_index: u16, symbol_index: u32) !u32 {
+pub fn getMatchingSegment(wasm: *Wasm, file_index: File.Index, symbol_index: u32) !u32 {
     const comp = wasm.base.comp;
     const gpa = comp.gpa;
-    const object: Object = wasm.objects.items[object_index];
-    const symbol = object.symtable[symbol_index];
+    const obj_file = wasm.file(file_index).?;
+    const symbol = obj_file.symbols()[symbol_index];
     const index: u32 = @intCast(wasm.segments.items.len);
     const shared_memory = comp.config.shared_memory;
 
     switch (symbol.tag) {
         .data => {
-            const segment_info = object.segment_info[symbol.index];
+            const segment_info = obj_file.segmentInfo()[symbol.index];
             const merge_segment = comp.config.output_mode != .Obj;
             const result = try wasm.data_segments.getOrPut(gpa, segment_info.outputName(merge_segment));
             if (!result.found_existing) {
@@ -2394,7 +2422,7 @@ pub fn getMatchingSegment(wasm: *Wasm, object_index: u16, symbol_index: u32) !u3
             break :blk index;
         },
         .section => {
-            const section_name = object.string_table.get(symbol.name);
+            const section_name = file.symbolName(symbol.index);
             if (mem.eql(u8, section_name, ".debug_info")) {
                 return wasm.debug_info_index orelse blk: {
                     wasm.debug_info_index = index;
@@ -4291,12 +4319,10 @@ fn markReferences(wasm: *Wasm) !void {
         // Debug sections may require to be parsed and marked when it contains
         // relocations to alive symbols.
         if (sym.tag == .section and comp.config.debug_format != .strip) {
-            const file = sym_loc.file orelse continue; // Incremental debug info is done independently
-            const object = &wasm.objects.items[file];
-            const atom_index = try Object.parseSymbolIntoAtom(object, file, sym_loc.index, wasm);
-            const atom = wasm.getAtom(atom_index);
-            const atom_sym = atom.symbolLoc().getSymbol(wasm);
-            atom_sym.mark();
+            const file_index = sym_loc.file orelse continue; // Incremental debug info is done independently
+            const obj_file = wasm.file(@enumFromInt(file_index)).?;
+            _ = try obj_file.parseSymbolIntoAtom(wasm, sym_loc.index);
+            sym.mark();
         }
     }
 }
@@ -4319,9 +4345,8 @@ fn mark(wasm: *Wasm, loc: SymbolLoc) !void {
     }
 
     const atom_index = if (loc.file) |file_index| idx: {
-        const object = &wasm.objects.items[file_index];
-        const atom_index = try object.parseSymbolIntoAtom(file_index, loc.index, wasm);
-        break :idx atom_index;
+        const obj_file = wasm.file(@enumFromInt(file_index)).?;
+        break :idx try obj_file.parseSymbolIntoAtom(wasm, loc.index);
     } else wasm.symbol_atom.get(loc) orelse return;
 
     const atom = wasm.getAtom(atom_index);